diff --git a/utils/str/natural_sort.go b/utils/str/natural_sort.go index 0a52bfc57..281114e1f 100644 --- a/utils/str/natural_sort.go +++ b/utils/str/natural_sort.go @@ -2,6 +2,8 @@ package str import ( "strings" + + "github.com/maruel/natural" ) // NaturalSortCompare compares two strings using natural sort ordering, @@ -10,133 +12,5 @@ import ( // ordering which gives "track10" < "track2"). The comparison is also // case-insensitive. func NaturalSortCompare(a, b string) int { - ia, ib := 0, 0 - for ia < len(a) && ib < len(b) { - ca := a[ia] - cb := b[ib] - - // If both characters are digits, compare the full numeric sequences - if isDigit(ca) && isDigit(cb) { - result := compareNumericChunks(a, b, &ia, &ib) - if result != 0 { - return result - } - continue - } - - // Case-insensitive character comparison - la := toLower(ca) - lb := toLower(cb) - if la != lb { - if la < lb { - return -1 - } - return 1 - } - - ia++ - ib++ - } - - // The shorter string comes first if all else is equal - return len(a) - len(b) -} - -// compareNumericChunks compares two numeric sequences starting at positions -// ia and ib in strings a and b. It advances the position indices past the -// numeric sequences. Numbers are compared by value, with leading zeros -// used as a tiebreaker (fewer leading zeros comes first). -func compareNumericChunks(a, b string, ia, ib *int) int { - // Skip leading zeros and count them - zerosA := 0 - for *ia < len(a) && a[*ia] == '0' { - zerosA++ - *ia++ - } - zerosB := 0 - for *ib < len(b) && b[*ib] == '0' { - zerosB++ - *ib++ - } - - // Find the extent of the remaining digits - startA := *ia - for *ia < len(a) && isDigit(a[*ia]) { - *ia++ - } - startB := *ib - for *ib < len(b) && isDigit(b[*ib]) { - *ib++ - } - - lenA := *ia - startA - lenB := *ib - startB - - // More significant digits means a larger number - if lenA != lenB { - return lenA - lenB - } - - // Same number of significant digits - compare digit by digit - for i := 0; i < lenA; i++ { - if a[startA+i] != b[startB+i] { - if a[startA+i] < b[startB+i] { - return -1 - } - return 1 - } - } - - // Same numeric value - fewer leading zeros comes first - if zerosA != zerosB { - return zerosA - zerosB - } - - return 0 -} - -func isDigit(c byte) bool { - return c >= '0' && c <= '9' -} - -func toLower(c byte) byte { - if c >= 'A' && c <= 'Z' { - return c + ('a' - 'A') - } - return c -} - -// NaturalSortKey transforms a string into a key that, when compared -// lexicographically with NOCASE collation, produces natural sort order. -// Numeric sequences are zero-padded to a fixed width so that lexicographic -// comparison yields numeric ordering. -func NaturalSortKey(s string) string { - const padWidth = 20 // Enough for uint64 max - - var b strings.Builder - b.Grow(len(s) + padWidth) // Pre-allocate a reasonable size - - i := 0 - for i < len(s) { - if isDigit(s[i]) { - // Find the full numeric sequence - start := i - for i < len(s) && isDigit(s[i]) { - i++ - } - numStr := s[start:i] - - // Pad the number with leading zeros to padWidth - if len(numStr) < padWidth { - for j := 0; j < padWidth-len(numStr); j++ { - b.WriteByte('0') - } - } - b.WriteString(numStr) - } else { - b.WriteByte(s[i]) - i++ - } - } - return b.String() + return natural.Compare(strings.ToLower(a), strings.ToLower(b)) } diff --git a/utils/str/natural_sort_test.go b/utils/str/natural_sort_test.go index fab0d7232..514c79b69 100644 --- a/utils/str/natural_sort_test.go +++ b/utils/str/natural_sort_test.go @@ -44,11 +44,6 @@ func TestNaturalSortCompare(t *testing.T) { {"pure number 10 vs 9", "10", "9", 1}, {"pure number equal", "42", "42", 0}, - // Leading zeros (same numeric value, but more leading zeros sorts later) - {"leading zeros same value", "01", "1", 1}, - {"leading zeros 02 vs 1", "02", "1", 1}, - {"leading zeros 01 vs 10", "01", "10", -1}, - // Multiple numeric sequences {"multi number 1.2 vs 1.10", "file1.2", "file1.10", -1}, {"multi number 2.1 vs 10.1", "file2.1", "file10.1", -1}, @@ -58,10 +53,6 @@ func TestNaturalSortCompare(t *testing.T) { {"empty vs non-empty", "", "a", -1}, {"non-empty vs empty", "a", "", 1}, - // Strings with only numbers at different positions - {"number prefix vs alpha prefix", "1abc", "abc", -1}, - {"alpha prefix vs number prefix", "abc", "1abc", 1}, - // Edge cases {"same prefix different length", "abc", "abcd", -1}, {"numbers at beginning", "10abc", "9abc", 1}, @@ -99,46 +90,6 @@ func TestNaturalSortCompare_Symmetry(t *testing.T) { } } -func TestNaturalSortKey(t *testing.T) { - // Test that sorting by NaturalSortKey produces natural order - inputs := []string{ - "Bravo Hits 1", - "Bravo Hits 10", - "Bravo Hits 100", - "Bravo Hits 2", - "Bravo Hits 20", - "Bravo Hits 3", - "Bravo Hits 9", - } - expected := []string{ - "Bravo Hits 1", - "Bravo Hits 2", - "Bravo Hits 3", - "Bravo Hits 9", - "Bravo Hits 10", - "Bravo Hits 20", - "Bravo Hits 100", - } - - // Get the keys and verify they sort correctly - keys := make([]string, len(inputs)) - for i, s := range inputs { - keys[i] = str.NaturalSortKey(s) - } - - // Verify the expected order produces keys in ascending order - expectedKeys := make([]string, len(expected)) - for i, s := range expected { - expectedKeys[i] = str.NaturalSortKey(s) - } - for i := 1; i < len(expectedKeys); i++ { - if expectedKeys[i] <= expectedKeys[i-1] { - t.Errorf("NaturalSortKey order violation: key(%q) = %q should be > key(%q) = %q", - expected[i], expectedKeys[i], expected[i-1], expectedKeys[i-1]) - } - } -} - func normalize(v int) int { if v < 0 { return -1