mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor: use maruel/natural for NATURALSORT collation instead of custom impl
Replace the hand-rolled natural sort comparison with a thin wrapper around github.com/maruel/natural.Compare, which is already a dependency. The wrapper just lowercases both inputs for case-insensitive comparison.
This commit is contained in:
parent
b0cb40b029
commit
9bcefea0ca
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user