8
\$\begingroup\$

I needed a utility function/method that would get the next available filename to save as.

An example would be, if I need to save a file as MyTestFile.html but it already exists.

This method would then check to see what the next availble filename would be, and it would return MyTestFile2.html. If MyTestFile2.html exists, then it would return MyTestFile3.html, and so on.

I wrote the method, but it feels sloppy, and isn't really very elegant.

Any ideas for improving the readability of this code, or a better way to do it?

public string GetNextAvailableName(List<string> files, string baseFile)
{
    var baseFileWithoutExt = Path.GetFileNameWithoutExtension(baseFile);
    var baseExt = Path.GetExtension(baseFile);

    //clean files and get the ones containing oure baseFileName  
    var cleanFiles = files
                        .Select(i => Path.GetFileNameWithoutExtension(i.ToLower()))
                        .Where(i => i.Contains(baseFileWithoutExt.ToLower()));

    var count = cleanFiles.Count();

    if (count == 0)
        return baseFile;

    int indexCount = 1;

    do
    {
        indexCount++;
    } while (cleanFiles.Contains(baseFileWithoutExt.ToLower() + indexCount));

    return baseFileWithoutExt + indexCount + baseExt;
}

This would then be called like this:

GetNextAvailableName(Directory.GetFiles("C:\\MyFiles\\", "MyTestFile.html");

Then if MyTestFile.html is already in use, it will return MyTestFileX.html, where X is the lowest positive integer where the filename doesn't exist yet.

\$\endgroup\$
  • \$\begingroup\$ What is a baseFile example when you call GetNextAvailableName()? I'm trying to understand better what it is you want to do - then I can comment on a better solution. Thanks. \$\endgroup\$ – joebalt Oct 6 '13 at 4:24
  • \$\begingroup\$ I tried your method with the following data, and I don't think it does what you want it to do. I passed in var files = new List<string> { "MyTestFile.html", "MyTestFile2.html", "MyTestFile3.html", "MyTestFile4.html", "MyTestFile5.html", "MyTestFile6.html", "MyTestFile7.html", "MyTestFile8.html", "MyTestFile9.html" }; const string baseFile = "MyTestFile4.html"; and it returned "MyTestFile42.html". \$\endgroup\$ – joebalt Oct 6 '13 at 4:29
  • \$\begingroup\$ Why don't you check the extensions of files? \$\endgroup\$ – svick Oct 6 '13 at 9:57
  • \$\begingroup\$ @JoeBaltimore I updated the code with an example. It returned MyTestFile42.html, because you entered MyTestFile4.html as the base file. It saw that MyTestFile4.html was already taken, so it gave the next available filename, which was MyTestFile42.html. (Think of this as MyTestFile4 (version 2).html I think the basefile that should have passed in was "MyTestFile.html", and then it should have returned "MyTestFile10.html" \$\endgroup\$ – Kyle Oct 6 '13 at 14:30
7
\$\begingroup\$

I don't understand why you want to pass a list of files. Simply check the existence of your filename candidates. Start with the base filename. If it does not exist, start counting. In my eyes this a straightforward approach and therefore easy to understand.

public string GetNextAvailableFilename(string filename)
{
    if (!System.IO.File.Exists(filename)) return filename;

    string alternateFilename;
    int fileNameIndex = 1;
    do
    {
        fileNameIndex += 1;
        alternateFilename = CreateNumberedFilename(filename, fileNameIndex);
    } while (System.IO.File.Exists(alternateFilename));

    return alternateFilename;
}

private string CreateNumberedFilename(string filename, int number)
{ 
    string plainName = System.IO.Path.GetFileNameWithoutExtension(filename);
    string extension = System.IO.Path.GetExtension(filename);
    return string.Format("{0}{1}{2}", plainName, number, extension);
}
\$\endgroup\$
  • \$\begingroup\$ Hmm.. I pass in the filenames I guess because I needed to do it for a certain directory, and the baseFile isn't a fully qualified path. But I think this way makes more sense, and will probably switch it \$\endgroup\$ – Kyle Oct 6 '13 at 14:33
3
\$\begingroup\$

Your current code finds "holes" in the numbering. If that is not required (or desired) then you could parse the numbers of the end and find the max and return max + 1. Something along these lines:

public string GetNextAvailableName(List<string> files, string baseFile)
{
    var baseFileWithoutExt = Path.GetFileNameWithoutExtension(baseFile);
    var baseExt = Path.GetExtension(baseFile);

    //clean files and get the ones containing oure baseFileName  
    var maxUsedIndex = files
                        .Select(f => Path.GetFileNameWithoutExtension(f.ToLower()))
                        .Where(f => f.StartsWith(baseFileWithoutExt.ToLower()))
                        .Select(f => f.Substring(baseFileWithoutExt.Length))
                        .Select(f => Int32.Parse(f))
                        .Max();

    return baseFileWithoutExt + (maxUsedIndex + 1) + baseExt;
}

Note that the code above is not robust against malformed input (Int32.Parse() might throw). Also it requires that the passed in files are complete and no new file with a new index is created in the meantime.

Note: When you use Linq then imagine your were to write a foreach loop instead and try name your loop variables accordingly. i had me slightly confused when looping over files - I think foreach (var f in files) is more intuitive than foreach (var i in files) as i is generally something related to a loop index rather than an actual object.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.