views:

195

answers:

6

I'm outputting files in C# and want to handle files being saved with the same name by adding brackets and a number:

FooBar.xml
FooBar(1).xml
FooBar(2).xml
...
FooBar(N).xml

Is there a simple way to do that in .NET? And is there a special name for the (#) construct?

+8  A: 

You'll just have to count up and manipulate the file names manually. The (pseudo)code below is dirty, but it's the basic algorithm. Refactor it to your needs.

var filenameFormat = "FooBar{0}.xml";
var filename = string.Format(filenameFormat, "");
int i = 1;
while(File.Exists(filename))
    filename = string.Format(filenameFormat, "(" + (i++) + ")");

return filename;

If you can get away with it, you could always just tack on DateTime.Now in a format of your choice. That's what we do for temporary files, anyway.

Stuart Branham
+1 Beat me to it
Nifle
You are doing repeated calls to the file system, getting the files as suggested by person-b with the GetFiles method may be faster for large number of files.
Yuriy Faktorovich
Would it be more preferable to say `filenameFormat = "FooBar({0}).xml";` in this case?
maxwellb
@Yuriy Agreed. If performance is a concern, I'd grab a List<string> of file names in the directory and replace File.Exists() with list.Contains(). There's always more than one way to skin a cat.
Stuart Branham
@mpbloch No, because then you would get FooBar().xml if no FooBar*.xmls exist yet.
Stuart Branham
@Stuart B: Ah, right, I think I meant add, take away the var filename = string.Format( filenameFormat,"");, and assume that filename is already FooBar.xml.
maxwellb
A: 

I think you would have to do something along the lines of having a dictionary of file names, like this:

Dictionary<string, int> fileNameOccurences = new Dictionary<string, int>();
// ...
string fileName = "FooBar";
if ( fileNameOccurences.ContainsKey(fileName) ) { 
    fileNameOccurences[fileName]++;
    fileName += "(" + fileNameOccurences[fileName].ToString() + ")";
}
else { fileNameOccurences.Add(fileName, 1); }
SaveFile(fileName + ".xml");

In this case, you may need to parse out the extension or refactor or something like that.

If you don't have control over the names of files in the directory, then you can count the occurences manually:

string fileName = "FooBar";
string[] fileNames = Directory.GetFiles(theDirectory, fileName + "*.xml");
fileName += "(" + (fileNames.Count + 1).ToString() + ")";
SaveFile(fileName + ".xml");

EDIT: As has been pointed out in the comments, this is a quick-and-dirty solution with a major bug.

Here's a slower (I imagine), but more robust solution:

string fileName = "FooBar", directory = @"C:\Output";
int no = 0;
while ( ++no > 0 && File.Exists(Path.Combine(directory, fileName + "(" + no.ToString() + ").xml")) );
Lucas Jones
You would be skipping over numbers, if a file was deleted, not sure whether that is the intended functionality.
Yuriy Faktorovich
Ah. That's quite a serious bug -- if they had 1..5, deleted 4, then it would try to write to #5.
Lucas Jones
+2  A: 
/// <summary>
/// Provides a filename given if it does not exist.
/// If the filename exists, provides the lowest numeric number such that
/// filename-number.ext does not exist.
/// </summary>
public static string GetNextFilename( string desiredFilename )
{
    // using System.IO;
    int num = 0;
    FileInfo fi = new FileInfo( desiredFilename );

    string basename = fi.FullName.Substring( 0, fi.FullName.Length - fi.Extension.Length );
    string extension = fi.Extension;

    while( fi.Exists )
    {
        fi = new FileInfo( String.Format( "{0}({1}){2}",
            basename,
            i++,
            extension ) );
    }

    return fi.FullName; // or fi.Name;
}

Then, if you have a method that saves to next file: log.SaveTo( GetNextFileName( log.txt ) ); will save to log.txt or log(0).txt or log(1).txt or log(2).txt, etc.

If you want to be able to sort all the files by name all the time, use a standard numeric format string in the String.Format section.

maxwellb
A: 

For a LINQ-ish solution to this, check out Keith Dahlby's recent blog post, "Improve Your Code Golf Game with LINQ" He covers this same issue quite elegantly.

David Alpert
A: 
string fileNameFormat = "FooBar{0}.xml";
string fileName = "FooBar.xml";
string filePath = "C:/";
string[] existingFiles = Directory.GetFiles(filePath, "FooBar*.xml");
int i = 1;
while (existingFiles.Contains(filePath + fileName))
{
    fileName = string.Format(fileNameFormat, "(" + i + ")");
    i += 1;
}
Yuriy Faktorovich
+1  A: 

In the end I utilized the accepted answer to create an extension method that looks like this:

public static string GetNextFilename(this string filename)
{
    int i = 1;
    string dir = Path.GetDirectoryName(filename);
    string file = Path.GetFileNameWithoutExtension(filename) + "{0}";
    string extension = Path.GetExtension(filename);

    while (File.Exists(filename))
        filename = Path.Combine(dir, string.Format(file, "(" + i++ + ")") + extension);

    return filename;
}
Gavin Miller