diff --git a/.github/NOTICE.md b/.github/NOTICE.md index e3cb50c4d660..6c2754d8d82b 100644 --- a/.github/NOTICE.md +++ b/.github/NOTICE.md @@ -674,3 +674,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + +## NaturalStringComparer + +**Source**: [https://github.com/GihanSoft/NaturalStringComparer](https://github.com/GihanSoft/NaturalStringComparer) + +### License + +``` +MIT License + +Copyright (c) 2018 Mohammad Babayi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` \ No newline at end of file diff --git a/src/Files.App/Helpers/NaturalStringComparer.cs b/src/Files.App/Helpers/NaturalStringComparer.cs index 6b2555f92362..d4296ae05a45 100644 --- a/src/Files.App/Helpers/NaturalStringComparer.cs +++ b/src/Files.App/Helpers/NaturalStringComparer.cs @@ -3,6 +3,7 @@ namespace Files.App.Helpers { + // Credit: https://github.com/GihanSoft/NaturalStringComparer public sealed class NaturalStringComparer { public static IComparer GetForProcessor() @@ -61,34 +62,86 @@ public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) } 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; + { + // Handle file extensions specially + int xExtPos = GetExtensionPosition(x); + int yExtPos = GetExtensionPosition(y); + + // If both have extensions, compare the names first + if (xExtPos >= 0 && yExtPos >= 0) + { + var xName = x.Slice(0, xExtPos); + var yName = y.Slice(0, yExtPos); + + int nameCompare = CompareWithoutExtension(xName, yName, stringComparison); + if (nameCompare != 0) + return nameCompare; + + // If names match, compare extensions + return x.Slice(xExtPos).CompareTo(y.Slice(yExtPos), stringComparison); + } + + // Original comparison logic for non-extension cases + return CompareWithoutExtension(x, y, stringComparison); + } + + private static int CompareWithoutExtension(ReadOnlySpan x, ReadOnlySpan y, StringComparison stringComparison) + { + var length = Math.Min(x.Length, y.Length); + + for (var i = 0; i < length; i++) + { + while (i < x.Length && i < y.Length && IsIgnorableSeparator(x, i) && IsIgnorableSeparator(y, i)) + i++; + + if (i >= x.Length || i >= y.Length) break; + + 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 int GetExtensionPosition(ReadOnlySpan text) + { + // Find the last period that's not at the beginning + for (int i = text.Length - 1; i > 0; i--) + { + if (text[i] == '.') + return i; + } + return -1; + } + + private static bool IsIgnorableSeparator(ReadOnlySpan span, int index) + { + if (span[index] != '-' && span[index] != '_') return false; + + // Check bounds before accessing span[index + 1] or span[index - 1] + if (index == 0) return span.Length > 1 && char.IsLetterOrDigit(span[index + 1]); + if (index == span.Length - 1) return span.Length > 1 && char.IsLetterOrDigit(span[index - 1]); + + return char.IsLetterOrDigit(span[index - 1]) && char.IsLetterOrDigit(span[index + 1]); + } - 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) { diff --git a/src/Files.App/ViewModels/Settings/AboutViewModel.cs b/src/Files.App/ViewModels/Settings/AboutViewModel.cs index 62f68e2be6fc..bfa3a8ede6a1 100644 --- a/src/Files.App/ViewModels/Settings/AboutViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AboutViewModel.cs @@ -78,6 +78,7 @@ public AboutViewModel() new ("https://github.com/PowerShell/MMI", "MMI"), new ("https://github.com/microsoft/CsWin32", "CsWin32"), new ("https://github.com/microsoft/CsWinRT", "CsWinRT"), + new ("https://github.com/GihanSoft/NaturalStringComparer", "NaturalStringComparer"), ]; CopyAppVersionCommand = new RelayCommand(CopyAppVersion);