Skip to content

Commit ed0fc0e

Browse files
GiteaBotwxiaoguang
andauthored
Fix natural sort (#31384) (#31394)
Backport #31384 by wxiaoguang Fix #31374 Co-authored-by: wxiaoguang <[email protected]>
1 parent fa30716 commit ed0fc0e

File tree

2 files changed

+87
-17
lines changed

2 files changed

+87
-17
lines changed

modules/base/natural_sort.go

+56-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,67 @@
44
package base
55

66
import (
7+
"unicode/utf8"
8+
79
"golang.org/x/text/collate"
810
"golang.org/x/text/language"
911
)
1012

13+
func naturalSortGetRune(str string, pos int) (r rune, size int, has bool) {
14+
if pos >= len(str) {
15+
return 0, 0, false
16+
}
17+
r, size = utf8.DecodeRuneInString(str[pos:])
18+
if r == utf8.RuneError {
19+
r, size = rune(str[pos]), 1 // if invalid input, treat it as a single byte ascii
20+
}
21+
return r, size, true
22+
}
23+
24+
func naturalSortAdvance(str string, pos int) (end int, isNumber bool) {
25+
end = pos
26+
for {
27+
r, size, has := naturalSortGetRune(str, end)
28+
if !has {
29+
break
30+
}
31+
isCurRuneNum := '0' <= r && r <= '9'
32+
if end == pos {
33+
isNumber = isCurRuneNum
34+
end += size
35+
} else if isCurRuneNum == isNumber {
36+
end += size
37+
} else {
38+
break
39+
}
40+
}
41+
return end, isNumber
42+
}
43+
1144
// NaturalSortLess compares two strings so that they could be sorted in natural order
1245
func NaturalSortLess(s1, s2 string) bool {
46+
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
47+
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
48+
// So we need to handle the number parts by ourselves
1349
c := collate.New(language.English, collate.Numeric)
14-
return c.CompareString(s1, s2) < 0
50+
pos1, pos2 := 0, 0
51+
for pos1 < len(s1) && pos2 < len(s2) {
52+
end1, isNum1 := naturalSortAdvance(s1, pos1)
53+
end2, isNum2 := naturalSortAdvance(s2, pos2)
54+
part1, part2 := s1[pos1:end1], s2[pos2:end2]
55+
if isNum1 && isNum2 {
56+
if part1 != part2 {
57+
if len(part1) != len(part2) {
58+
return len(part1) < len(part2)
59+
}
60+
return part1 < part2
61+
}
62+
} else {
63+
if cmp := c.CompareString(part1, part2); cmp != 0 {
64+
return cmp < 0
65+
}
66+
}
67+
pos1, pos2 = end1, end2
68+
}
69+
return len(s1) < len(s2)
1570
}

modules/base/natural_sort_test.go

+31-16
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,36 @@ import (
1010
)
1111

1212
func TestNaturalSortLess(t *testing.T) {
13-
test := func(s1, s2 string, less bool) {
14-
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
13+
testLess := func(s1, s2 string) {
14+
assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
15+
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
1516
}
16-
test("v1.20.0", "v1.2.0", false)
17-
test("v1.20.0", "v1.29.0", true)
18-
test("v1.20.0", "v1.20.0", false)
19-
test("abc", "bcd", true)
20-
test("a-1-a", "a-1-b", true)
21-
test("2", "12", true)
22-
test("a", "ab", true)
23-
24-
test("A", "b", true)
25-
test("a", "B", true)
26-
27-
test("cafe", "café", true)
28-
test("café", "cafe", false)
29-
test("caff", "café", false)
17+
testEqual := func(s1, s2 string) {
18+
assert.False(t, NaturalSortLess(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
19+
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
20+
}
21+
22+
testEqual("", "")
23+
testLess("", "a")
24+
testLess("", "1")
25+
26+
testLess("v1.2", "v1.2.0")
27+
testLess("v1.2.0", "v1.10.0")
28+
testLess("v1.20.0", "v1.29.0")
29+
testEqual("v1.20.0", "v1.20.0")
30+
31+
testLess("a", "A")
32+
testLess("a", "B")
33+
testLess("A", "b")
34+
testLess("A", "ab")
35+
36+
testLess("abc", "bcd")
37+
testLess("a-1-a", "a-1-b")
38+
testLess("2", "12")
39+
40+
testLess("cafe", "café")
41+
testLess("café", "caff")
42+
43+
testLess("A-2", "A-11")
44+
testLess("0.txt", "1.txt")
3045
}

0 commit comments

Comments
 (0)