/* Taken from https://gist.github.com/mstum/63a6e3e8cf54e8ae55b6aa28ca6f20c5 Modified slightly to remove the need for unsafe and changed namespace to plugin namespace */ using System; using System.Collections.Generic; namespace MeidoPhotoStudio.Plugin { /// /// A string comparer that behaves like StrCmpLogicalW /// https://msdn.microsoft.com/en-us/library/windows/desktop/bb759947 /// /// This means: /// * case insensitive (ZA == za) /// * numbers are treated as numbers (z20 > z3) and assumed positive /// (-100 comes AFTER 10 and 100, because the minus is seen /// as a char, not as part of the number) /// * leading zeroes come before anything else (z001 < z01 < z1) /// /// Note: Instead of instantiating this, you can also use /// /// if you don't need an but can /// use a delegate instead. /// /// /// NOTE: This behaves slightly different than StrCmpLogicalW because /// it handles large numbers. /// /// At some point, StrCmpLogicalW just gives up trying to parse /// something as a number (see the Test cases), while we keep going. /// Since we want to sort lexicographily as much as possible, /// that difference makes sense. /// public class LexicographicStringComparer : IComparer { /// /// A delegate. /// public static int Comparison(string x, string y) { // 1 = x > y, -1 = y > x, 0 = x == y // Rules: Numbers < Letters. Space < everything if (x == y) return 0; if (string.IsNullOrEmpty(x) && !string.IsNullOrEmpty(y)) return -1; if (!string.IsNullOrEmpty(x) && string.IsNullOrEmpty(y)) return 1; if (string.IsNullOrEmpty(x) && string.IsNullOrEmpty(y)) return 0; // "" and null are the same for the purposes of this var yl = y.Length; for (int i = 0; i < x.Length; i++) { if (yl <= i) return 1; var cx = x[i]; var cy = y[i]; if (Char.IsWhiteSpace(cx) && !Char.IsWhiteSpace(cy)) return -1; if (!Char.IsWhiteSpace(cx) && Char.IsWhiteSpace(cy)) return 1; if (IsDigit(cx)) { if (!IsDigit(cy)) { return -1; } // Both are digits, but now we need to look at them as a whole, since // 10 > 2, but 10 > 002 > 02 > 2 var numCmp = CompareNumbers(x, y, i, out int numChars); if (numCmp != 0) return numCmp; i += numChars; // We might have looked at more than one char, e.g., "10" is 2 chars } else if (IsDigit(cy)) { return 1; } else { // Do this after the digit check // Case insensitive // Normalize to Uppercase: // https://docs.microsoft.com/en-US/visualstudio/code-quality/ca1308-normalize-strings-to-uppercase var cmp = Char.ToUpper(cx).CompareTo(Char.ToUpper(cy)); if (cmp != 0) return cmp; } } // Strings are equal to that point, and y is at least as large as x if (y.Length > x.Length) return -1; return 0; } /// /// /// public int Compare(string x, string y) => Comparison(x, y); private static int CompareNumbers(string x, string y, int ix, out int numChars) { var xParsed = ParseNumber(x, ix); var yParsed = ParseNumber(y, ix); numChars = yParsed.NumCharsRead > xParsed.NumCharsRead ? xParsed.NumCharsRead : yParsed.NumCharsRead; return xParsed.CompareTo(yParsed); } private static ParsedNumber ParseNumber(string str, int offset) { var result = 0; var numChars = 0; var leadingZeroes = 0; var numOverflows = 0; bool countZeroes = true; for (int j = offset; j < str.Length; j++) { char c = str[j]; if (IsDigit(c)) { var cInt = (c - 48); // 48/0x30 is '0' checked { long tmp = (result * 10L) + cInt; if (tmp > int.MaxValue) { numOverflows++; tmp = tmp % int.MaxValue; } result = (int)tmp; numChars++; } if (cInt == 0 && countZeroes) { leadingZeroes++; } else { countZeroes = false; } } else { break; } } return new ParsedNumber(result, numOverflows, leadingZeroes, numChars); } private static bool IsDigit(char c) => (c >= '0' && c <= '9'); /// /// Note that the ParsedNumber is not very useful as a number, /// but purely as a way to compare two numbers that are stored in a string. /// private struct ParsedNumber : IComparable, IComparer { /// /// The remainder, that is, the part of the number that /// didn't overflow int.MaxValue. /// public int Remainder; /// /// How often did the number overflow int.MaxValue during parsing? /// public int Overflows; /// /// How many leading zeroes were there in the string during parsing? /// "001" has a LeadingZeroesCount of 2. /// "100" has a LeadingZeroesCount of 0. /// "010" has a LeadingZeroesCount of 1. /// /// This is important, because 001 comes before 01 comes before 1. /// public int LeadingZeroesCount; /// /// How many characters were read from the input during parsing? /// public int NumCharsRead; public ParsedNumber(int remainder, int overflows, int leadingZeroes, int numChars) { Remainder = remainder; Overflows = overflows; LeadingZeroesCount = leadingZeroes; NumCharsRead = numChars; } public int Compare(ParsedNumber x, ParsedNumber y) { // Note: if numCharsX and Y aren't equal, this doesn't matter // as the return value will be either -1 or 1 anyway if (x.Overflows > y.Overflows) return 1; if (x.Overflows < y.Overflows) return -1; // 001 > 01 > 1 if (x.Remainder == y.Remainder) { if (x.LeadingZeroesCount > y.LeadingZeroesCount) return -1; if (x.LeadingZeroesCount < y.LeadingZeroesCount) return 1; } if (x.Remainder > y.Remainder) return 1; if (x.Remainder < y.Remainder) return -1; return 0; } public int CompareTo(ParsedNumber other) => Compare(this, other); } } }