Fix sorting numeric utilities when they have different magnitudes (#16414)

When sorting utilities it was possible for a utility like `duration-700`
to come before `duration-75` or `duration-1000` to come before
`duration-150`.

This PR fixes this behavior by reading the full "magnitude" of each
numbers before we compare them.
This commit is contained in:
Jordan Pittman 2025-02-10 17:08:34 -05:00 • committed by GitHub
parent 272c6dfa98
commit 121cf6bd80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 9 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Export `tailwindcss/lib/util/flattenColorPalette.js` for backward compatibility ([#16411](https://github.com/tailwindlabs/tailwindcss/pull/16411))
- Fix sorting numeric utilities when they have different magnitudes ([#16414](https://github.com/tailwindlabs/tailwindcss/pull/16414))
## [4.0.6] - 2025-02-10

View file

@ -21,6 +21,12 @@ it.each([
['2', '1', GREATER],
['1', '10', LESS],
['10', '1', GREATER],
// Numbers of different lengths
['75', '700', LESS],
['700', '75', GREATER],
['75', '770', LESS],
['770', '75', GREATER],
])('should compare "%s" with "%s" as "%d"', (a, b, expected) => {
expect(Math.sign(compare(a, b))).toBe(expected)
})
@ -124,3 +130,39 @@ it('should sort strings with multiple numbers consistently using the `compare` f
]
`)
})
it('sort is stable', () => {
// Heap's algorithm for permutations
function* permutations<T>(input: T[]) {
let pos = 1
let stack = input.map(() => 0)
yield input.slice()
while (pos < input.length) {
if (stack[pos] < pos) {
let k = pos % 2 == 0 ? 0 : stack[pos]
;[input[k], input[pos]] = [input[pos], input[k]]
yield input.slice()
++stack[pos]
pos = 1
} else {
stack[pos] = 0
++pos
}
}
}
let classes = ['duration-initial', 'duration-75', 'duration-150', 'duration-700', 'duration-1000']
for (let permutation of permutations(classes)) {
let sorted = [...permutation].sort(compare)
expect(sorted).toEqual([
'duration-75',
'duration-150',
'duration-700',
'duration-1000',
'duration-initial',
])
}
})

View file

@ -14,9 +14,6 @@ export function compare(a: string, z: string) {
let aCode = a.charCodeAt(i)
let zCode = z.charCodeAt(i)
// Continue if the characters are the same
if (aCode === zCode) continue
// If both are numbers, compare them as numbers instead of strings.
if (aCode >= ZERO && aCode <= NINE && zCode >= ZERO && zCode <= NINE) {
let aStart = i
@ -35,14 +32,24 @@ export function compare(a: string, z: string) {
let aNumber = a.slice(aStart, aEnd)
let zNumber = z.slice(zStart, zEnd)
return (
Number(aNumber) - Number(zNumber) ||
// Fallback case if numbers are the same but the string representation
// is not. Fallback to string sorting. E.g.: `0123` vs `123`
(aNumber < zNumber ? -1 : 1)
)
let diff = Number(aNumber) - Number(zNumber)
if (diff) return diff
// Fallback case if numbers are the same but the string representation
// is not. Fallback to string sorting. E.g.: `0123` vs `123`
if (aNumber < zNumber) return -1
if (aNumber > zNumber) return 1
// Continue with the next character otherwise short strings will appear
// after long ones when containing numbers. E.g.:
// - bg-red-500/70
// - bg-red-500
continue
}
// Continue if the characters are the same
if (aCode === zCode) continue
// Otherwise, compare them as strings
return aCode - zCode
}