// Copyright (c) ppy Pty Ltd . 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 { /// /// Given a set of and a target , /// finds a "best" name closest to that is not in . /// /// /// /// 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 is already present in , /// this method will append the lowest possible number in brackets that doesn't conflict with /// to and return that. /// See osu.Game.Tests.Utils.NamingUtilsTest for concrete examples of behaviour. /// /// /// and are compared in a case-insensitive manner, /// so this method is safe to use for naming files in a platform-invariant manner. /// /// public static string GetNextBestName(IEnumerable 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()})"; } /// /// Given a set of and a desired target /// finds a filename closest to that is not in /// public static string GetNextBestFilename(IEnumerable 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}"; } /// /// Generates a basic regex pattern that will match all possible conflicting filenames when picking the best available name, given the . /// The generated pattern can be composed into more complicated regexes for particular uses, such as picking filenames, which need additional file extension handling. /// /// /// The regex shall detect: /// /// all strings that are equal to , /// all strings of the format desiredName (number), where number is a number written using Arabic numerals. /// /// All comparisons are made in a case-insensitive manner. /// If a number is detected in the matches, it will be output to the copyNumber named group. /// private static string getBaselineNameDetectingPattern(string desiredName) => $@"(?i){Regex.Escape(desiredName)}(?-i)( \((?[1-9][0-9]*)\))?"; private static int findBestNumber(IEnumerable existingNames, Regex regex) { var takenNumbers = new HashSet(); 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; } } }