Skip to content

Commit 9e7f8a1

Browse files
authored
Code Quality: Improve string comparer performance (#16523)
1 parent 378ed01 commit 9e7f8a1

File tree

1 file changed

+108
-22
lines changed

1 file changed

+108
-22
lines changed

src/Files.App/Helpers/NaturalStringComparer.cs

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,118 @@ public sealed class NaturalStringComparer
77
{
88
public static IComparer<object> GetForProcessor()
99
{
10-
return Win32Helper.IsRunningOnArm ? new StringComparerArm64() : new StringComparerDefault();
10+
return new NaturalComparer(StringComparison.CurrentCulture);
1111
}
1212

13-
private sealed class StringComparerArm64 : IComparer<object>
13+
/// <summary>
14+
/// Provides functionality to compare and sort strings in a natural (human-readable) order.
15+
/// </summary>
16+
/// <remarks>
17+
/// This class implements string comparison that respects the natural numeric order in strings,
18+
/// such as "file10" being ordered after "file2".
19+
/// It is designed to handle cases where alphanumeric sorting is required.
20+
/// </remarks>
21+
private sealed class NaturalComparer : IComparer<object?>, IComparer<string?>, IComparer<ReadOnlyMemory<char>>
1422
{
15-
public int Compare(object a, object b)
16-
{
17-
return StringComparer.CurrentCulture.Compare(a, b);
18-
}
19-
}
23+
private readonly StringComparison stringComparison;
2024

21-
private sealed class StringComparerDefault : IComparer<object>
22-
{
23-
public int Compare(object a, object b)
24-
{
25-
return Win32PInvoke.CompareStringEx(
26-
Win32PInvoke.LOCALE_NAME_USER_DEFAULT,
27-
Win32PInvoke.SORT_DIGITSASNUMBERS, // Add other flags if required.
28-
a?.ToString(),
29-
a?.ToString().Length ?? 0,
30-
b?.ToString(),
31-
b?.ToString().Length ?? 0,
32-
IntPtr.Zero,
33-
IntPtr.Zero,
34-
0) - 2;
35-
}
25+
public NaturalComparer(StringComparison stringComparison = StringComparison.Ordinal)
26+
{
27+
this.stringComparison = stringComparison;
28+
}
29+
30+
public int Compare(object? x, object? y)
31+
{
32+
if (x == y) return 0;
33+
if (x == null) return -1;
34+
if (y == null) return 1;
35+
36+
return x switch
37+
{
38+
string x1 when y is string y1 => Compare(x1.AsSpan(), y1.AsSpan(), stringComparison),
39+
IComparable comparable => comparable.CompareTo(y),
40+
_ => StringComparer.FromComparison(stringComparison).Compare(x, y)
41+
};
42+
}
43+
44+
public int Compare(string? x, string? y)
45+
{
46+
if (ReferenceEquals(x, y)) return 0;
47+
if (x is null) return -1;
48+
if (y is null) return 1;
49+
50+
return Compare(x.AsSpan(), y.AsSpan(), stringComparison);
51+
}
52+
53+
public int Compare(ReadOnlySpan<char> x, ReadOnlySpan<char> y)
54+
{
55+
return Compare(x, y, stringComparison);
56+
}
57+
58+
public int Compare(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y)
59+
{
60+
return Compare(x.Span, y.Span, stringComparison);
61+
}
62+
63+
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;
77+
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+
93+
private static ReadOnlySpan<char> GetNumber(ReadOnlySpan<char> span, out ReadOnlySpan<char> number)
94+
{
95+
var i = 0;
96+
while (i < span.Length && char.IsDigit(span[i]))
97+
{
98+
i++;
99+
}
100+
101+
number = span.Slice(0, i);
102+
return span.Slice(i);
103+
}
104+
105+
private static int CompareNumValues(ReadOnlySpan<char> numValue1, ReadOnlySpan<char> numValue2)
106+
{
107+
var num1AsSpan = numValue1.TrimStart('0');
108+
var num2AsSpan = numValue2.TrimStart('0');
109+
110+
if (num1AsSpan.Length < num2AsSpan.Length) return -1;
111+
112+
if (num1AsSpan.Length > num2AsSpan.Length) return 1;
113+
114+
var compareResult = num1AsSpan.CompareTo(num2AsSpan, StringComparison.Ordinal);
115+
116+
if (compareResult != 0) return Math.Sign(compareResult);
117+
118+
if (numValue2.Length == numValue1.Length) return compareResult;
119+
120+
return numValue2.Length < numValue1.Length ? -1 : 1; // "033" < "33" == true
121+
}
36122
}
37123
}
38124
}

0 commit comments

Comments
 (0)