2022-02-16 21:32:42 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic ;
2022-12-01 17:43:34 +00:00
using System.IO ;
2022-02-16 22:00:45 +00:00
using System.Text.RegularExpressions ;
2022-02-16 21:32:42 +00:00
namespace osu.Game.Utils
{
public static class NamingUtils
{
2022-02-16 22:00:45 +00:00
/// <summary>
/// Given a set of <paramref name="existingNames"/> and a target <paramref name="desiredName"/>,
/// finds a "best" name closest to <paramref name="desiredName"/> that is not in <paramref name="existingNames"/>.
/// </summary>
/// <remarks>
/// <para>
/// This helper is most useful in scenarios when creating new objects in a set
/// (such as adding new difficulties to a beatmap set, or creating a clone of an existing object that needs a unique name).
/// If <paramref name="desiredName"/> is already present in <paramref name="existingNames"/>,
/// this method will append the lowest possible number in brackets that doesn't conflict with <paramref name="existingNames"/>
/// to <paramref name="desiredName"/> and return that.
/// See <c>osu.Game.Tests.Utils.NamingUtilsTest</c> for concrete examples of behaviour.
/// </para>
/// <para>
/// <paramref name="desiredName"/> and <paramref name="existingNames"/> are compared in a case-insensitive manner,
/// so this method is safe to use for naming files in a platform-invariant manner.
/// </para>
/// </remarks>
2022-02-16 21:32:42 +00:00
public static string GetNextBestName ( IEnumerable < string > existingNames , string desiredName )
{
2022-12-03 15:59:43 +00:00
string pattern = $@"^{getBaselineNameDetectingPattern(desiredName)}$" ;
2022-02-16 22:00:45 +00:00
var regex = new Regex ( pattern , RegexOptions . Compiled ) ;
2022-12-01 17:42:52 +00:00
int bestNumber = findBestNumber ( existingNames , regex ) ;
return bestNumber = = 0
? desiredName
: $"{desiredName} ({bestNumber.ToString()})" ;
}
/// <summary>
2022-12-01 17:43:34 +00:00
/// Given a set of <paramref name="existingFilenames"/> and a desired target <paramref name="desiredFilename"/>
/// finds a filename closest to <paramref name="desiredFilename"/> that is not in <paramref name="existingFilenames"/>
2022-12-01 17:42:52 +00:00
/// </summary>
2022-12-01 17:43:34 +00:00
public static string GetNextBestFilename ( IEnumerable < string > existingFilenames , string desiredFilename )
2022-12-01 17:42:52 +00:00
{
2022-12-01 17:43:34 +00:00
string name = Path . GetFileNameWithoutExtension ( desiredFilename ) ;
string extension = Path . GetExtension ( desiredFilename ) ;
2022-12-03 15:59:43 +00:00
string pattern = $@"^{getBaselineNameDetectingPattern(name)}(?i){Regex.Escape(extension)}(?-i)$" ;
2022-12-01 17:43:34 +00:00
var regex = new Regex ( pattern , RegexOptions . Compiled ) ;
int bestNumber = findBestNumber ( existingFilenames , regex ) ;
2022-12-01 17:42:52 +00:00
2022-12-01 17:43:34 +00:00
return bestNumber = = 0
? desiredFilename
: $"{name} ({bestNumber.ToString()}){extension}" ;
2022-12-01 17:42:52 +00:00
}
2022-12-03 15:59:43 +00:00
/// <summary>
/// Generates a basic regex pattern that will match all possible conflicting filenames when picking the best available name, given the <paramref name="desiredName"/>.
/// The generated pattern can be composed into more complicated regexes for particular uses, such as picking filenames, which need additional file extension handling.
/// </summary>
/// <remarks>
/// The regex shall detect:
/// <list type="bullet">
/// <item>all strings that are equal to <paramref name="desiredName"/>,</item>
/// <item>all strings of the format <c>desiredName (number)</c>, where <c>number</c> is a number written using Arabic numerals.</item>
/// </list>
/// All comparisons are made in a case-insensitive manner.
/// If a number is detected in the matches, it will be output to the <c>copyNumber</c> named group.
/// </remarks>
private static string getBaselineNameDetectingPattern ( string desiredName )
= > $@"(?i){Regex.Escape(desiredName)}(?-i)( \((?<copyNumber>[1-9][0-9]*)\))?" ;
2022-12-01 17:42:52 +00:00
private static int findBestNumber ( IEnumerable < string > existingNames , Regex regex )
{
2022-02-16 22:00:45 +00:00
var takenNumbers = new HashSet < int > ( ) ;
foreach ( string name in existingNames )
{
var match = regex . Match ( name ) ;
if ( ! match . Success )
continue ;
string copyNumberString = match . Groups [ @"copyNumber" ] . Value ;
if ( string . IsNullOrEmpty ( copyNumberString ) )
{
takenNumbers . Add ( 0 ) ;
continue ;
}
takenNumbers . Add ( int . Parse ( copyNumberString ) ) ;
}
int bestNumber = 0 ;
while ( takenNumbers . Contains ( bestNumber ) )
bestNumber + = 1 ;
2022-12-01 17:42:52 +00:00
return bestNumber ;
2022-11-30 17:32:14 +00:00
}
2022-02-16 21:32:42 +00:00
}
}