Skip to content

Commit 16d2535

Browse files
Replace 'localeCompare' with function independent from locale (#2876)
Fixes #2869
1 parent edbe218 commit 16d2535

File tree

6 files changed

+143
-5
lines changed

6 files changed

+143
-5
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import naturalCompare from '../naturalCompare';
5+
6+
describe('naturalCompare', () => {
7+
it('Handles empty strings', () => {
8+
expect(naturalCompare('', '')).to.equal(0);
9+
10+
expect(naturalCompare('', 'a')).to.equal(-1);
11+
expect(naturalCompare('', '1')).to.equal(-1);
12+
13+
expect(naturalCompare('a', '')).to.equal(1);
14+
expect(naturalCompare('1', '')).to.equal(1);
15+
});
16+
17+
it('Handles strings of different length', () => {
18+
expect(naturalCompare('A', 'A')).to.equal(0);
19+
expect(naturalCompare('A1', 'A1')).to.equal(0);
20+
21+
expect(naturalCompare('A', 'AA')).to.equal(-1);
22+
expect(naturalCompare('A1', 'A1A')).to.equal(-1);
23+
24+
expect(naturalCompare('AA', 'A')).to.equal(1);
25+
expect(naturalCompare('A1A', 'A1')).to.equal(1);
26+
});
27+
28+
it('Handles numbers', () => {
29+
expect(naturalCompare('0', '0')).to.equal(0);
30+
expect(naturalCompare('1', '1')).to.equal(0);
31+
32+
expect(naturalCompare('1', '2')).to.equal(-1);
33+
expect(naturalCompare('2', '1')).to.equal(1);
34+
35+
expect(naturalCompare('2', '11')).to.equal(-1);
36+
expect(naturalCompare('11', '2')).to.equal(1);
37+
});
38+
39+
it('Handles numbers with leading zeros', () => {
40+
expect(naturalCompare('00', '00')).to.equal(0);
41+
expect(naturalCompare('0', '00')).to.equal(-1);
42+
expect(naturalCompare('00', '0')).to.equal(1);
43+
44+
expect(naturalCompare('02', '11')).to.equal(-1);
45+
expect(naturalCompare('11', '02')).to.equal(1);
46+
47+
expect(naturalCompare('011', '200')).to.equal(-1);
48+
expect(naturalCompare('200', '011')).to.equal(1);
49+
});
50+
51+
it('Handles numbers embedded into names', () => {
52+
expect(naturalCompare('a0a', 'a0a')).to.equal(0);
53+
expect(naturalCompare('a0a', 'a9a')).to.equal(-1);
54+
expect(naturalCompare('a9a', 'a0a')).to.equal(1);
55+
56+
expect(naturalCompare('a00a', 'a00a')).to.equal(0);
57+
expect(naturalCompare('a00a', 'a09a')).to.equal(-1);
58+
expect(naturalCompare('a09a', 'a00a')).to.equal(1);
59+
60+
expect(naturalCompare('a0a1', 'a0a1')).to.equal(0);
61+
expect(naturalCompare('a0a1', 'a0a9')).to.equal(-1);
62+
expect(naturalCompare('a0a9', 'a0a1')).to.equal(1);
63+
64+
expect(naturalCompare('a10a11a', 'a10a11a')).to.equal(0);
65+
expect(naturalCompare('a10a11a', 'a10a19a')).to.equal(-1);
66+
expect(naturalCompare('a10a19a', 'a10a11a')).to.equal(1);
67+
68+
expect(naturalCompare('a10a11a', 'a10a11a')).to.equal(0);
69+
expect(naturalCompare('a10a11a', 'a10a11b')).to.equal(-1);
70+
expect(naturalCompare('a10a11b', 'a10a11a')).to.equal(1);
71+
});
72+
});

src/jsutils/naturalCompare.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Returns a number indicating whether a reference string comes before, or after,
3+
* or is the same as the given string in natural sort order.
4+
*
5+
* See: https://en.wikipedia.org/wiki/Natural_sort_order
6+
*
7+
*/
8+
export default function naturalCompare(aStr: string, bStr: string): number {
9+
let aIdx = 0;
10+
let bIdx = 0;
11+
12+
while (aIdx < aStr.length && bIdx < bStr.length) {
13+
let aChar = aStr.charCodeAt(aIdx);
14+
let bChar = bStr.charCodeAt(bIdx);
15+
16+
if (isDigit(aChar) && isDigit(bChar)) {
17+
let aNum = 0;
18+
do {
19+
++aIdx;
20+
aNum = aNum * 10 + aChar - DIGIT_0;
21+
aChar = aStr.charCodeAt(aIdx);
22+
} while (isDigit(aChar) && aNum > 0);
23+
24+
let bNum = 0;
25+
do {
26+
++bIdx;
27+
bNum = bNum * 10 + bChar - DIGIT_0;
28+
bChar = bStr.charCodeAt(bIdx);
29+
} while (isDigit(bChar) && bNum > 0);
30+
31+
if (aNum < bNum) {
32+
return -1;
33+
}
34+
35+
if (aNum > bNum) {
36+
return 1;
37+
}
38+
} else {
39+
if (aChar < bChar) {
40+
return -1;
41+
}
42+
if (aChar > bChar) {
43+
return 1;
44+
}
45+
++aIdx;
46+
++bIdx;
47+
}
48+
}
49+
50+
return aStr.length - bStr.length;
51+
}
52+
53+
const DIGIT_0 = 48;
54+
const DIGIT_9 = 57;
55+
56+
function isDigit(code: number): boolean {
57+
return !isNaN(code) && DIGIT_0 <= code && code <= DIGIT_9;
58+
}

src/jsutils/suggestionList.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import naturalCompare from './naturalCompare';
2+
13
/**
24
* Given an invalid input string and a list of valid options, returns a filtered
35
* list of valid options sorted based on their similarity with the input.
@@ -19,7 +21,7 @@ export default function suggestionList(
1921

2022
return Object.keys(optionsByDistance).sort((a, b) => {
2123
const distanceDiff = optionsByDistance[a] - optionsByDistance[b];
22-
return distanceDiff !== 0 ? distanceDiff : a.localeCompare(b);
24+
return distanceDiff !== 0 ? distanceDiff : naturalCompare(a, b);
2325
});
2426
}
2527

src/utilities/findBreakingChanges.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import objectValues from '../polyfills/objectValues';
33
import keyMap from '../jsutils/keyMap';
44
import inspect from '../jsutils/inspect';
55
import invariant from '../jsutils/invariant';
6+
import naturalCompare from '../jsutils/naturalCompare';
67

78
import { print } from '../language/printer';
89
import { visit } from '../language/visitor';
@@ -541,8 +542,11 @@ function stringifyValue(value: mixed, type: GraphQLInputType): string {
541542

542543
const sortedAST = visit(ast, {
543544
ObjectValue(objectNode) {
544-
const fields = [...objectNode.fields].sort((fieldA, fieldB) =>
545-
fieldA.name.value.localeCompare(fieldB.name.value),
545+
// Make a copy since sort mutates array
546+
const fields = [...objectNode.fields];
547+
548+
fields.sort((fieldA, fieldB) =>
549+
naturalCompare(fieldA.name.value, fieldB.name.value),
546550
);
547551
return { ...objectNode, fields };
548552
},

src/utilities/lexicographicSortSchema.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ObjMap } from '../jsutils/ObjMap';
44
import inspect from '../jsutils/inspect';
55
import invariant from '../jsutils/invariant';
66
import keyValMap from '../jsutils/keyValMap';
7+
import naturalCompare from '../jsutils/naturalCompare';
78

89
import type {
910
GraphQLType,
@@ -180,6 +181,6 @@ function sortBy<T>(
180181
return array.slice().sort((obj1, obj2) => {
181182
const key1 = mapToKey(obj1);
182183
const key2 = mapToKey(obj2);
183-
return key1.localeCompare(key2);
184+
return naturalCompare(key1, key2);
184185
});
185186
}

src/validation/rules/FieldsOnCorrectTypeRule.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import arrayFrom from '../../polyfills/arrayFrom';
22

33
import didYouMean from '../../jsutils/didYouMean';
44
import suggestionList from '../../jsutils/suggestionList';
5+
import naturalCompare from '../../jsutils/naturalCompare';
56

67
import { GraphQLError } from '../../error/GraphQLError';
78

@@ -122,7 +123,7 @@ function getSuggestedTypeNames(
122123
return 1;
123124
}
124125

125-
return typeA.name.localeCompare(typeB.name);
126+
return naturalCompare(typeA.name, typeB.name);
126127
})
127128
.map((x) => x.name);
128129
}

0 commit comments

Comments
 (0)