mirror of https://github.com/ppy/osu
106 lines
4.8 KiB
C#
106 lines
4.8 KiB
C#
// 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;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace osu.Game.Utils
|
|
{
|
|
public static class NamingUtils
|
|
{
|
|
/// <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>
|
|
public static string GetNextBestName(IEnumerable<string> existingNames, string desiredName)
|
|
{
|
|
string pattern = $@"^{getBaselineNameDetectingPattern(desiredName)}$";
|
|
var regex = new Regex(pattern, RegexOptions.Compiled);
|
|
|
|
int bestNumber = findBestNumber(existingNames, regex);
|
|
|
|
return bestNumber == 0
|
|
? desiredName
|
|
: $"{desiredName} ({bestNumber.ToString()})";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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"/>
|
|
/// </summary>
|
|
public static string GetNextBestFilename(IEnumerable<string> existingFilenames, string desiredFilename)
|
|
{
|
|
string name = Path.GetFileNameWithoutExtension(desiredFilename);
|
|
string extension = Path.GetExtension(desiredFilename);
|
|
|
|
string pattern = $@"^{getBaselineNameDetectingPattern(name)}(?i){Regex.Escape(extension)}(?-i)$";
|
|
var regex = new Regex(pattern, RegexOptions.Compiled);
|
|
|
|
int bestNumber = findBestNumber(existingFilenames, regex);
|
|
|
|
return bestNumber == 0
|
|
? desiredFilename
|
|
: $"{name} ({bestNumber.ToString()}){extension}";
|
|
}
|
|
|
|
/// <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]*)\))?";
|
|
|
|
private static int findBestNumber(IEnumerable<string> existingNames, Regex regex)
|
|
{
|
|
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;
|
|
|
|
return bestNumber;
|
|
}
|
|
}
|
|
}
|