mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:35:03 +01:00 
			
		
		
		
	Improve natural sort (#29611)
Hugely simplify the code, and add more tests (only new approach could pass)
This commit is contained in:
		
							parent
							
								
									3f3335ae51
								
							
						
					
					
						commit
						ebff37ae09
					
				| @ -4,85 +4,12 @@ | ||||
| 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 | ||||
| } | ||||
|  | ||||
| @ -11,7 +11,7 @@ import ( | ||||
| 
 | ||||
| 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) | ||||
| @ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) { | ||||
| 	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) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user