|
3 | 3 |
|
4 | 4 | namespace Files.App.Helpers
|
5 | 5 | {
|
| 6 | + // Credit: https://github.com/GihanSoft/NaturalStringComparer |
6 | 7 | public sealed class NaturalStringComparer
|
7 | 8 | {
|
8 | 9 | public static IComparer<object> GetForProcessor()
|
@@ -61,34 +62,86 @@ public int Compare(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y)
|
61 | 62 | }
|
62 | 63 |
|
63 | 64 | public static int Compare(ReadOnlySpan<char> x, ReadOnlySpan<char> y, StringComparison stringComparison)
|
64 |
| - { |
65 |
| - var length = Math.Min(x.Length, y.Length); |
66 |
| - |
67 |
| - for (var i = 0; i < length; i++) |
68 |
| - { |
69 |
| - if (char.IsDigit(x[i]) && char.IsDigit(y[i])) |
70 |
| - { |
71 |
| - var xOut = GetNumber(x.Slice(i), out var xNumAsSpan); |
72 |
| - var yOut = GetNumber(y.Slice(i), out var yNumAsSpan); |
73 |
| - |
74 |
| - var compareResult = CompareNumValues(xNumAsSpan, yNumAsSpan); |
75 |
| - |
76 |
| - if (compareResult != 0) return compareResult; |
| 65 | + { |
| 66 | + // Handle file extensions specially |
| 67 | + int xExtPos = GetExtensionPosition(x); |
| 68 | + int yExtPos = GetExtensionPosition(y); |
| 69 | + |
| 70 | + // If both have extensions, compare the names first |
| 71 | + if (xExtPos >= 0 && yExtPos >= 0) |
| 72 | + { |
| 73 | + var xName = x.Slice(0, xExtPos); |
| 74 | + var yName = y.Slice(0, yExtPos); |
| 75 | + |
| 76 | + int nameCompare = CompareWithoutExtension(xName, yName, stringComparison); |
| 77 | + if (nameCompare != 0) |
| 78 | + return nameCompare; |
| 79 | + |
| 80 | + // If names match, compare extensions |
| 81 | + return x.Slice(xExtPos).CompareTo(y.Slice(yExtPos), stringComparison); |
| 82 | + } |
| 83 | + |
| 84 | + // Original comparison logic for non-extension cases |
| 85 | + return CompareWithoutExtension(x, y, stringComparison); |
| 86 | + } |
| 87 | + |
| 88 | + private static int CompareWithoutExtension(ReadOnlySpan<char> x, ReadOnlySpan<char> y, StringComparison stringComparison) |
| 89 | + { |
| 90 | + var length = Math.Min(x.Length, y.Length); |
| 91 | + |
| 92 | + for (var i = 0; i < length; i++) |
| 93 | + { |
| 94 | + while (i < x.Length && i < y.Length && IsIgnorableSeparator(x, i) && IsIgnorableSeparator(y, i)) |
| 95 | + i++; |
| 96 | + |
| 97 | + if (i >= x.Length || i >= y.Length) break; |
| 98 | + |
| 99 | + if (char.IsDigit(x[i]) && char.IsDigit(y[i])) |
| 100 | + { |
| 101 | + var xOut = GetNumber(x.Slice(i), out var xNumAsSpan); |
| 102 | + var yOut = GetNumber(y.Slice(i), out var yNumAsSpan); |
| 103 | + |
| 104 | + var compareResult = CompareNumValues(xNumAsSpan, yNumAsSpan); |
| 105 | + |
| 106 | + if (compareResult != 0) return compareResult; |
| 107 | + |
| 108 | + i = -1; |
| 109 | + length = Math.Min(xOut.Length, yOut.Length); |
| 110 | + |
| 111 | + x = xOut; |
| 112 | + y = yOut; |
| 113 | + continue; |
| 114 | + } |
| 115 | + |
| 116 | + var charCompareResult = x.Slice(i, 1).CompareTo(y.Slice(i, 1), stringComparison); |
| 117 | + if (charCompareResult != 0) return charCompareResult; |
| 118 | + } |
| 119 | + |
| 120 | + return x.Length.CompareTo(y.Length); |
| 121 | + } |
| 122 | + |
| 123 | + private static int GetExtensionPosition(ReadOnlySpan<char> text) |
| 124 | + { |
| 125 | + // Find the last period that's not at the beginning |
| 126 | + for (int i = text.Length - 1; i > 0; i--) |
| 127 | + { |
| 128 | + if (text[i] == '.') |
| 129 | + return i; |
| 130 | + } |
| 131 | + return -1; |
| 132 | + } |
| 133 | + |
| 134 | + private static bool IsIgnorableSeparator(ReadOnlySpan<char> span, int index) |
| 135 | + { |
| 136 | + if (span[index] != '-' && span[index] != '_') return false; |
| 137 | + |
| 138 | + // Check bounds before accessing span[index + 1] or span[index - 1] |
| 139 | + if (index == 0) return span.Length > 1 && char.IsLetterOrDigit(span[index + 1]); |
| 140 | + if (index == span.Length - 1) return span.Length > 1 && char.IsLetterOrDigit(span[index - 1]); |
| 141 | + |
| 142 | + return char.IsLetterOrDigit(span[index - 1]) && char.IsLetterOrDigit(span[index + 1]); |
| 143 | + } |
77 | 144 |
|
78 |
| - i = -1; |
79 |
| - length = Math.Min(xOut.Length, yOut.Length); |
80 |
| - |
81 |
| - x = xOut; |
82 |
| - y = yOut; |
83 |
| - continue; |
84 |
| - } |
85 |
| - |
86 |
| - var charCompareResult = x.Slice(i, 1).CompareTo(y.Slice(i, 1), stringComparison); |
87 |
| - if (charCompareResult != 0) return charCompareResult; |
88 |
| - } |
89 |
| - |
90 |
| - return x.Length.CompareTo(y.Length); |
91 |
| - } |
92 | 145 |
|
93 | 146 | private static ReadOnlySpan<char> GetNumber(ReadOnlySpan<char> span, out ReadOnlySpan<char> number)
|
94 | 147 | {
|
|
0 commit comments