🏡 index : quine-global/quintodrome.git

// Package natural provides natural (alphanumeric) string comparison.
// When both strings have digit sequences at the same position, they are
// compared numerically (so "file2" < "file10"); otherwise bytes are
// compared one-by-one. No allocations are made.
package natural

import "strings"

// Compare returns a negative value if a < b, zero if a == b,
// or a positive value if a > b using natural sort ordering.
//
// When two numeric segments are numerically equal (e.g. "01" vs "1"),
// comparison continues with the remaining suffixes, and the padding
// difference is kept as a final tie-breaker that only decides strings
// that are otherwise equal (e.g. "a01" < "a1", "a0" < "a00"). Deferring
// it that way is what keeps the ordering transitive, which SQLite
// requires of a collating function.
func Compare(a, b string) int {
	return compare(a, b, false)
}

// CompareFold is Compare with ASCII case folding, matching SQLite's NOCASE
// collation: only A-Z fold, bytes >= 0x80 are compared as-is.
func CompareFold(a, b string) int {
	return compare(a, b, true)
}

func compare(a, b string, fold bool) int {
	ia, ib := 0, 0
	// Set when two runs are numerically equal but differently padded. Applying it
	// immediately would break transitivity, so it only decides otherwise-equal strings.
	padTie := 0
	for ia < len(a) && ib < len(b) {
		ca, cb := a[ia], b[ib]
		da, db := isDigit(ca), isDigit(cb)
		if fold {
			ca, cb = lower(ca), lower(cb)
		}

		switch {
		case da && db:
			// Both are in digit sequences — compare numerically.
			endA := ia
			for endA < len(a) && isDigit(a[endA]) {
				endA++
			}
			endB := ib
			for endB < len(b) && isDigit(b[endB]) {
				endB++
			}

			if c := compareNumbers(a[ia:endA], b[ib:endB]); c != 0 {
				return c
			}
			if t := strings.Compare(a[ia:endA], b[ib:endB]); t != 0 {
				padTie = t
			}
			ia = endA
			ib = endB
		case da != db:
			return int(ca) - int(cb)
		default:
			if ca != cb {
				return int(ca) - int(cb)
			}
			ia++
			ib++
		}
	}
	if c := (len(a) - ia) - (len(b) - ib); c != 0 {
		return c
	}
	return padTie
}

// compareNumbers compares two digit strings numerically.
// Leading zeros are stripped before comparison.
func compareNumbers(a, b string) int {
	// Strip leading zeros.
	sa := stripZeros(a)
	sb := stripZeros(b)

	// Different lengths after stripping means different magnitude.
	if len(sa) != len(sb) {
		return len(sa) - len(sb)
	}

	// Same length — compare digit by digit.
	for i := range len(sa) {
		if sa[i] != sb[i] {
			return int(sa[i]) - int(sb[i])
		}
	}
	return 0
}

// stripZeros returns s with leading '0' bytes removed.
// If s is all zeros, returns the last byte (a single "0").
func stripZeros(s string) string {
	i := 0
	for i < len(s) && s[i] == '0' {
		i++
	}
	if i == len(s) && len(s) > 0 {
		return s[len(s)-1:]
	}
	return s[i:]
}

func isDigit(c byte) bool {
	return c >= '0' && c <= '9'
}

func lower(c byte) byte {
	if c >= 'A' && c <= 'Z' {
		return c + 'a' - 'A'
	}
	return c
}