package base
import (
- "math/big"
- "unicode/utf8"
+ "golang.org/x/text/collate"
+ "golang.org/x/text/language"
)
// NaturalSortLess compares two strings so that they could be sorted in natural order
func NaturalSortLess(s1, s2 string) bool {
- var i1, i2 int
- for {
- rune1, j1, end1 := getNextRune(s1, i1)
- rune2, j2, end2 := getNextRune(s2, i2)
- if end1 || end2 {
- return end1 != end2 && end1
- }
- dec1 := isDecimal(rune1)
- dec2 := isDecimal(rune2)
- var less, equal bool
- if dec1 && dec2 {
- i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
- } else if !dec1 && !dec2 {
- equal = rune1 == rune2
- less = rune1 < rune2
- i1 = j1
- i2 = j2
- } else {
- return rune1 < rune2
- }
- if !equal {
- return less
- }
- }
-}
-
-func getNextRune(str string, pos int) (rune, int, bool) {
- if pos < len(str) {
- r, w := utf8.DecodeRuneInString(str[pos:])
- // Fallback to ascii
- if r == utf8.RuneError {
- r = rune(str[pos])
- w = 1
- }
- return r, pos + w, false
- }
- return 0, pos, true
-}
-
-func isDecimal(r rune) bool {
- return '0' <= r && r <= '9'
-}
-
-func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
- d1, d2 := true, true
- var dec1, dec2 string
- for d1 || d2 {
- if d1 {
- r, j, end := getNextRune(str1, pos1)
- if !end && isDecimal(r) {
- dec1 += string(r)
- pos1 = j
- } else {
- d1 = false
- }
- }
- if d2 {
- r, j, end := getNextRune(str2, pos2)
- if !end && isDecimal(r) {
- dec2 += string(r)
- pos2 = j
- } else {
- d2 = false
- }
- }
- }
- less, equal = compareBigNumbers(dec1, dec2)
- return pos1, pos2, less, equal
-}
-
-func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
- d1, _ := big.NewInt(0).SetString(dec1, 10)
- d2, _ := big.NewInt(0).SetString(dec2, 10)
- cmp := d1.Cmp(d2)
- return cmp < 0, cmp == 0
+ c := collate.New(language.English, collate.Numeric)
+ return c.CompareString(s1, s2) < 0
}
func TestNaturalSortLess(t *testing.T) {
test := func(s1, s2 string, less bool) {
- assert.Equal(t, less, NaturalSortLess(s1, s2))
+ assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
}
test("v1.20.0", "v1.2.0", false)
test("v1.20.0", "v1.29.0", true)
test("a-1-a", "a-1-b", true)
test("2", "12", true)
test("a", "ab", true)
+
+ test("A", "b", true)
+ test("a", "B", true)
+
+ test("cafe", "café", true)
+ test("café", "cafe", false)
+ test("caff", "café", false)
}