I would like to create a method which takes either a filename as a string
or a FileInfo
and adds an incremented number to the filename if the file
This is an answer to question in this Link, but they marked it as a duplicate, so I post my answer here.
I created this proof of concept class (may contain bugs). More explanation in code comments.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace ConsoleApp
{
class Program
{
static void Main( string[] args )
{
var testFilePaths = new List<string>
{
@"c:\test\file.txt",
@"c:\test\file(1).txt",
@"c:\test\file(2).txt",
@"c:\TEST2\file(3).txt",
@"c:\test\file(5).txt",
@"c:\test\file(5)abc.txt",
@"c:\test\file(5).avi"
};
// inspect in debbuger for correct values
var withSuffix = new DecomposedFilePath( "c:\\files\\file(13).txt");
var withoutSuffix = new DecomposedFilePath( "c:\\files\\file(abc).txt");
var withExtraNumber = new DecomposedFilePath( "c:\\files\\file(34)xyz(35).txt"); // "(34)" in the middle should be ignored
DecomposedFilePath changedSuffix = withExtraNumber.ReplaceSuffix( 1999 ); // "file(34)xyz(35).txt" -> "file(34)xyz(1999).txt"
DecomposedFilePath removedSuffix = changedSuffix.ReplaceSuffix( null ); // "file(34)xyz(1999).txt" -> "file(34)xyz.txt"
var testPath = new DecomposedFilePath( "c:\\test\\file.txt");
DecomposedFilePath nextPath1 = testPath.GetFirstFreeFilePath( testFilePaths );
// update our list
testFilePaths.Add( nextPath1.FullFilePath );
DecomposedFilePath nextPath2 = testPath.GetFirstFreeFilePath( testFilePaths );
testFilePaths.Add( nextPath2.FullFilePath );
DecomposedFilePath nextPath3 = testPath.GetFirstFreeFilePath( testFilePaths );
}
}
public sealed class DecomposedFilePath
{
public DecomposedFilePath( string filePath )
{
FullFilePath = Path.GetFullPath( filePath );
}
// "c:\myfiles\file(4).txt"
public string FullFilePath { get; }
// "file" or "file(1)"
public string FileNameWithoutExt => Path.GetFileNameWithoutExtension( FullFilePath );
// "file(13)" -> "file"
public string FileNameWithoutExtAndSuffix => FileNameWithoutExt.Substring( 0, FileNameWithoutExt.Length - Suffix.Length ); // removes suffix
// ".txt"
public string Extenstion => Path.GetExtension( FullFilePath );
// "c:\myfiles"
public string DirectoryPath => Path.GetDirectoryName( FullFilePath );
// "file(23)" -> "23", file -> stirng.Empty
public string Suffix
{
get
{
// we want to extract suffix from file name, e.g. "(34)" from "file(34)"
// I am not good at regex, but I hope it will work correctly
var regex = new Regex( @"\([0-9]+\)$" );
Match match = regex.Match( FileNameWithoutExt );
if (!match.Success) return string.Empty; // suffix not found
return match.Value; // return "(number)"
}
}
// tranlates suffix "(33)" to 33. If suffix is does not exist (string.empty), returns null (int?)
public int? SuffixAsInt
{
get
{
if (Suffix == string.Empty) return null;
string numberOnly = Suffix.Substring( 1, Suffix.Length - 2 ); // remove '(' from beginning and ')' from end
return int.Parse( numberOnly );
}
}
// e.g. input is suffix: 56 then it changes file name from "file(34)" to "file(56)"
public DecomposedFilePath ReplaceSuffix( int? suffix ) // null - removes suffix
{
string strSuffix = suffix is null ? string.Empty : $"({suffix})"; // add ( and )
string path = Path.Combine( DirectoryPath, FileNameWithoutExtAndSuffix + strSuffix + Extenstion ); // build full path
return new DecomposedFilePath( path );
}
public DecomposedFilePath GetFirstFreeFilePath( IEnumerable<string> filesInDir )
{
var decomposed = filesInDir
// convert all paths to our class
.Select( x => new DecomposedFilePath( x ) )
// pick files only with the same extensionm as our base file, ignore case
.Where( x => string.Equals( Extenstion, x.Extenstion, StringComparison.OrdinalIgnoreCase) )
// pick files only with the same name (ignoring suffix)
.Where( x => string.Equals( FileNameWithoutExtAndSuffix, x.FileNameWithoutExtAndSuffix, StringComparison.OrdinalIgnoreCase) )
// with the same directory
.Where( x => string.Equals( DirectoryPath, x.DirectoryPath, StringComparison.OrdinalIgnoreCase) )
.ToList(); // create copy for easier debugging
if (decomposed.Count == 0) return this; // no name collision
int? firstFreeSuffix = Enumerable.Range( 1, int.MaxValue) // start numbering duplicates from 1
.Select( x => (int?) x) // change to int? because SuffixAsInt is of that type
.Except( decomposed.Select( x => x.SuffixAsInt) ) // remove existing suffixes
.First(); // get first free suffix
return ReplaceSuffix( firstFreeSuffix );
}
public override string ToString() => FullFilePath;
}
}
Hope this self iterating function may help. It works fine for me.
public string getUniqueFileName(int i, string filepath, string filename)
{
string path = Path.Combine(filepath, filename);
if (System.IO.File.Exists(path))
{
string name = Path.GetFileNameWithoutExtension(filename);
string ext = Path.GetExtension(filename);
i++;
filename = getUniqueFileName(i, filepath, name + "_" + i + ext);
}
return filename;
}
Lots of good advice here. I ended up using a method written by Marc in an answer to a different question. Reformatted it a tiny bit and added another method to make it a bit easier to use "from the outside". Here is the result:
private static string numberPattern = " ({0})";
public static string NextAvailableFilename(string path)
{
// Short-cut if already available
if (!File.Exists(path))
return path;
// If path has extension then insert the number pattern just before the extension and return next filename
if (Path.HasExtension(path))
return GetNextFilename(path.Insert(path.LastIndexOf(Path.GetExtension(path)), numberPattern));
// Otherwise just append the pattern to the path and return next filename
return GetNextFilename(path + numberPattern);
}
private static string GetNextFilename(string pattern)
{
string tmp = string.Format(pattern, 1);
if (tmp == pattern)
throw new ArgumentException("The pattern must include an index place-holder", "pattern");
if (!File.Exists(tmp))
return tmp; // short-circuit if no matches
int min = 1, max = 2; // min is inclusive, max is exclusive/untested
while (File.Exists(string.Format(pattern, max)))
{
min = max;
max *= 2;
}
while (max != min + 1)
{
int pivot = (max + min) / 2;
if (File.Exists(string.Format(pattern, pivot)))
min = pivot;
else
max = pivot;
}
return string.Format(pattern, max);
}
Only partially tested it so far, but will update if I find any bugs with it. (Marcs code works nicely!) If you find any problems with it, please comment or edit or something :)
Here's one that decouples the numbered naming question from the check of the filesystem:
/// <summary>
/// Finds the next unused unique (numbered) filename.
/// </summary>
/// <param name="fileName">Name of the file.</param>
/// <param name="inUse">Function that will determine if the name is already in use</param>
/// <returns>The original filename if it wasn't already used, or the filename with " (n)"
/// added to the name if the original filename is already in use.</returns>
private static string NextUniqueFilename(string fileName, Func<string, bool> inUse)
{
if (!inUse(fileName))
{
// this filename has not been seen before, return it unmodified
return fileName;
}
// this filename is already in use, add " (n)" to the end
var name = Path.GetFileNameWithoutExtension(fileName);
var extension = Path.GetExtension(fileName);
if (name == null)
{
throw new Exception("File name without extension returned null.");
}
const int max = 9999;
for (var i = 1; i < max; i++)
{
var nextUniqueFilename = string.Format("{0} ({1}){2}", name, i, extension);
if (!inUse(nextUniqueFilename))
{
return nextUniqueFilename;
}
}
throw new Exception(string.Format("Too many files by this name. Limit: {0}", max));
}
And here's how you might call it if you are using the filesystem
var safeName = NextUniqueFilename(filename, f => File.Exists(Path.Combine(folder, f)));
/// <summary>
/// Create a unique filename for the given filename
/// </summary>
/// <param name="filename">A full filename, e.g., C:\temp\myfile.tmp</param>
/// <returns>A filename like C:\temp\myfile633822247336197902.tmp</returns>
public string GetUniqueFilename(string filename)
{
string basename = Path.Combine(Path.GetDirectoryName(filename),
Path.GetFileNameWithoutExtension(filename));
string uniquefilename = string.Format("{0}{1}{2}",
basename,
DateTime.Now.Ticks,
Path.GetExtension(filename));
// Thread.Sleep(1); // To really prevent collisions, but usually not needed
return uniquefilename;
}
As DateTime.Ticks has a resolution of 100 nanoseconds, collisions are extremely unlikely. However, a Thread.Sleep(1) will ensure that, but I doubt that it's needed
public FileInfo MakeUnique(string path)
{
string dir = Path.GetDirectoryName(path);
string fileName = Path.GetFileNameWithoutExtension(path);
string fileExt = Path.GetExtension(path);
for (int i = 1; ;++i) {
if (!File.Exists(path))
return new FileInfo(path);
path = Path.Combine(dir, fileName + " " + i + fileExt);
}
}
Obviously, this is vulnerable to race conditions as noted in other answers.