diff --git a/src/Files.App/Helpers/NaturalStringComparer.cs b/src/Files.App/Helpers/NaturalStringComparer.cs index 2e22c266417c..6f1ffc6d558b 100644 --- a/src/Files.App/Helpers/NaturalStringComparer.cs +++ b/src/Files.App/Helpers/NaturalStringComparer.cs @@ -7,32 +7,118 @@ public sealed class NaturalStringComparer { public static IComparer GetForProcessor() { - return Win32Helper.IsRunningOnArm ? new StringComparerArm64() : new StringComparerDefault(); + return new NaturalComparer(StringComparison.CurrentCulture); } - private sealed class StringComparerArm64 : IComparer + /// + /// Provides functionality to compare and sort strings in a natural (human-readable) order. + /// + /// + /// This class implements string comparison that respects the natural numeric order in strings, + /// such as "file10" being ordered after "file2". + /// It is designed to handle cases where alphanumeric sorting is required. + /// + private sealed class NaturalComparer : IComparer, IComparer, IComparer> { - public int Compare(object a, object b) - { - return StringComparer.CurrentCulture.Compare(a, b); - } - } + private readonly StringComparison stringComparison; - private sealed class StringComparerDefault : IComparer - { - public int Compare(object a, object b) - { - return Win32PInvoke.CompareStringEx( - Win32PInvoke.LOCALE_NAME_USER_DEFAULT, - Win32PInvoke.SORT_DIGITSASNUMBERS, // Add other flags if required. - a?.ToString(), - a?.ToString().Length ?? 0, - b?.ToString(), - b?.ToString().Length ?? 0, - IntPtr.Zero, - IntPtr.Zero, - 0) - 2; - } + public NaturalComparer(StringComparison stringComparison = StringComparison.Ordinal) + { + this.stringComparison = stringComparison; + } + + public int Compare(object? x, object? y) + { + if (x == y) return 0; + if (x == null) return -1; + if (y == null) return 1; + + return x switch + { + string x1 when y is string y1 => Compare(x1.AsSpan(), y1.AsSpan(), stringComparison), + IComparable comparable => comparable.CompareTo(y), + _ => StringComparer.FromComparison(stringComparison).Compare(x, y) + }; + } + + public int Compare(string? x, string? y) + { + if (ReferenceEquals(x, y)) return 0; + if (x is null) return -1; + if (y is null) return 1; + + return Compare(x.AsSpan(), y.AsSpan(), stringComparison); + } + + public int Compare(ReadOnlySpan x, ReadOnlySpan y) + { + return Compare(x, y, stringComparison); + } + + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) + { + return Compare(x.Span, y.Span, stringComparison); + } + + public static int Compare(ReadOnlySpan x, ReadOnlySpan y, StringComparison stringComparison) + { + var length = Math.Min(x.Length, y.Length); + + for (var i = 0; i < length; i++) + { + if (char.IsDigit(x[i]) && char.IsDigit(y[i])) + { + var xOut = GetNumber(x.Slice(i), out var xNumAsSpan); + var yOut = GetNumber(y.Slice(i), out var yNumAsSpan); + + var compareResult = CompareNumValues(xNumAsSpan, yNumAsSpan); + + if (compareResult != 0) return compareResult; + + i = -1; + length = Math.Min(xOut.Length, yOut.Length); + + x = xOut; + y = yOut; + continue; + } + + var charCompareResult = x.Slice(i, 1).CompareTo(y.Slice(i, 1), stringComparison); + if (charCompareResult != 0) return charCompareResult; + } + + return x.Length.CompareTo(y.Length); + } + + private static ReadOnlySpan GetNumber(ReadOnlySpan span, out ReadOnlySpan number) + { + var i = 0; + while (i < span.Length && char.IsDigit(span[i])) + { + i++; + } + + number = span.Slice(0, i); + return span.Slice(i); + } + + private static int CompareNumValues(ReadOnlySpan numValue1, ReadOnlySpan numValue2) + { + var num1AsSpan = numValue1.TrimStart('0'); + var num2AsSpan = numValue2.TrimStart('0'); + + if (num1AsSpan.Length < num2AsSpan.Length) return -1; + + if (num1AsSpan.Length > num2AsSpan.Length) return 1; + + var compareResult = num1AsSpan.CompareTo(num2AsSpan, StringComparison.Ordinal); + + if (compareResult != 0) return Math.Sign(compareResult); + + if (numValue2.Length == numValue1.Length) return compareResult; + + return numValue2.Length < numValue1.Length ? -1 : 1; // "033" < "33" == true + } } } }