Skip to content

Commit 121cf6b

Browse files
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.
1 parent 272c6df commit 121cf6b

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Export `tailwindcss/lib/util/flattenColorPalette.js` for backward compatibility ([#16411](https://github.com/tailwindlabs/tailwindcss/pull/16411))
13+
- Fix sorting numeric utilities when they have different magnitudes ([#16414](https://github.com/tailwindlabs/tailwindcss/pull/16414))
1314

1415
## [4.0.6] - 2025-02-10
1516

packages/tailwindcss/src/utils/compare.test.ts

+42
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ it.each([
2121
['2', '1', GREATER],
2222
['1', '10', LESS],
2323
['10', '1', GREATER],
24+
25+
// Numbers of different lengths
26+
['75', '700', LESS],
27+
['700', '75', GREATER],
28+
['75', '770', LESS],
29+
['770', '75', GREATER],
2430
])('should compare "%s" with "%s" as "%d"', (a, b, expected) => {
2531
expect(Math.sign(compare(a, b))).toBe(expected)
2632
})
@@ -124,3 +130,39 @@ it('should sort strings with multiple numbers consistently using the `compare` f
124130
]
125131
`)
126132
})
133+
134+
it('sort is stable', () => {
135+
// Heap's algorithm for permutations
136+
function* permutations<T>(input: T[]) {
137+
let pos = 1
138+
let stack = input.map(() => 0)
139+
140+
yield input.slice()
141+
142+
while (pos < input.length) {
143+
if (stack[pos] < pos) {
144+
let k = pos % 2 == 0 ? 0 : stack[pos]
145+
;[input[k], input[pos]] = [input[pos], input[k]]
146+
yield input.slice()
147+
++stack[pos]
148+
pos = 1
149+
} else {
150+
stack[pos] = 0
151+
++pos
152+
}
153+
}
154+
}
155+
156+
let classes = ['duration-initial', 'duration-75', 'duration-150', 'duration-700', 'duration-1000']
157+
158+
for (let permutation of permutations(classes)) {
159+
let sorted = [...permutation].sort(compare)
160+
expect(sorted).toEqual([
161+
'duration-75',
162+
'duration-150',
163+
'duration-700',
164+
'duration-1000',
165+
'duration-initial',
166+
])
167+
}
168+
})

packages/tailwindcss/src/utils/compare.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ export function compare(a: string, z: string) {
1414
let aCode = a.charCodeAt(i)
1515
let zCode = z.charCodeAt(i)
1616

17-
// Continue if the characters are the same
18-
if (aCode === zCode) continue
19-
2017
// If both are numbers, compare them as numbers instead of strings.
2118
if (aCode >= ZERO && aCode <= NINE && zCode >= ZERO && zCode <= NINE) {
2219
let aStart = i
@@ -35,14 +32,24 @@ export function compare(a: string, z: string) {
3532
let aNumber = a.slice(aStart, aEnd)
3633
let zNumber = z.slice(zStart, zEnd)
3734

38-
return (
39-
Number(aNumber) - Number(zNumber) ||
40-
// Fallback case if numbers are the same but the string representation
41-
// is not. Fallback to string sorting. E.g.: `0123` vs `123`
42-
(aNumber < zNumber ? -1 : 1)
43-
)
35+
let diff = Number(aNumber) - Number(zNumber)
36+
if (diff) return diff
37+
38+
// Fallback case if numbers are the same but the string representation
39+
// is not. Fallback to string sorting. E.g.: `0123` vs `123`
40+
if (aNumber < zNumber) return -1
41+
if (aNumber > zNumber) return 1
42+
43+
// Continue with the next character otherwise short strings will appear
44+
// after long ones when containing numbers. E.g.:
45+
// - bg-red-500/70
46+
// - bg-red-500
47+
continue
4448
}
4549

50+
// Continue if the characters are the same
51+
if (aCode === zCode) continue
52+
4653
// Otherwise, compare them as strings
4754
return aCode - zCode
4855
}

0 commit comments

Comments
 (0)