Skip to content

Commit

Permalink
Replace 'localeCompare' with function independent from locale (#2876)
Browse files Browse the repository at this point in the history
Fixes #2869
  • Loading branch information
IvanGoncharov committed Jan 15, 2021
1 parent edbe218 commit 16d2535
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 5 deletions.
72 changes: 72 additions & 0 deletions src/jsutils/__tests__/naturalCompare-test.js
@@ -0,0 +1,72 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import naturalCompare from '../naturalCompare';

describe('naturalCompare', () => {
it('Handles empty strings', () => {
expect(naturalCompare('', '')).to.equal(0);

expect(naturalCompare('', 'a')).to.equal(-1);
expect(naturalCompare('', '1')).to.equal(-1);

expect(naturalCompare('a', '')).to.equal(1);
expect(naturalCompare('1', '')).to.equal(1);
});

it('Handles strings of different length', () => {
expect(naturalCompare('A', 'A')).to.equal(0);
expect(naturalCompare('A1', 'A1')).to.equal(0);

expect(naturalCompare('A', 'AA')).to.equal(-1);
expect(naturalCompare('A1', 'A1A')).to.equal(-1);

expect(naturalCompare('AA', 'A')).to.equal(1);
expect(naturalCompare('A1A', 'A1')).to.equal(1);
});

it('Handles numbers', () => {
expect(naturalCompare('0', '0')).to.equal(0);
expect(naturalCompare('1', '1')).to.equal(0);

expect(naturalCompare('1', '2')).to.equal(-1);
expect(naturalCompare('2', '1')).to.equal(1);

expect(naturalCompare('2', '11')).to.equal(-1);
expect(naturalCompare('11', '2')).to.equal(1);
});

it('Handles numbers with leading zeros', () => {
expect(naturalCompare('00', '00')).to.equal(0);
expect(naturalCompare('0', '00')).to.equal(-1);
expect(naturalCompare('00', '0')).to.equal(1);

expect(naturalCompare('02', '11')).to.equal(-1);
expect(naturalCompare('11', '02')).to.equal(1);

expect(naturalCompare('011', '200')).to.equal(-1);
expect(naturalCompare('200', '011')).to.equal(1);
});

it('Handles numbers embedded into names', () => {
expect(naturalCompare('a0a', 'a0a')).to.equal(0);
expect(naturalCompare('a0a', 'a9a')).to.equal(-1);
expect(naturalCompare('a9a', 'a0a')).to.equal(1);

expect(naturalCompare('a00a', 'a00a')).to.equal(0);
expect(naturalCompare('a00a', 'a09a')).to.equal(-1);
expect(naturalCompare('a09a', 'a00a')).to.equal(1);

expect(naturalCompare('a0a1', 'a0a1')).to.equal(0);
expect(naturalCompare('a0a1', 'a0a9')).to.equal(-1);
expect(naturalCompare('a0a9', 'a0a1')).to.equal(1);

expect(naturalCompare('a10a11a', 'a10a11a')).to.equal(0);
expect(naturalCompare('a10a11a', 'a10a19a')).to.equal(-1);
expect(naturalCompare('a10a19a', 'a10a11a')).to.equal(1);

expect(naturalCompare('a10a11a', 'a10a11a')).to.equal(0);
expect(naturalCompare('a10a11a', 'a10a11b')).to.equal(-1);
expect(naturalCompare('a10a11b', 'a10a11a')).to.equal(1);
});
});
58 changes: 58 additions & 0 deletions src/jsutils/naturalCompare.js
@@ -0,0 +1,58 @@
/**
* Returns a number indicating whether a reference string comes before, or after,
* or is the same as the given string in natural sort order.
*
* See: https://en.wikipedia.org/wiki/Natural_sort_order
*
*/
export default function naturalCompare(aStr: string, bStr: string): number {
let aIdx = 0;
let bIdx = 0;

while (aIdx < aStr.length && bIdx < bStr.length) {
let aChar = aStr.charCodeAt(aIdx);
let bChar = bStr.charCodeAt(bIdx);

if (isDigit(aChar) && isDigit(bChar)) {
let aNum = 0;
do {
++aIdx;
aNum = aNum * 10 + aChar - DIGIT_0;
aChar = aStr.charCodeAt(aIdx);
} while (isDigit(aChar) && aNum > 0);

let bNum = 0;
do {
++bIdx;
bNum = bNum * 10 + bChar - DIGIT_0;
bChar = bStr.charCodeAt(bIdx);
} while (isDigit(bChar) && bNum > 0);

if (aNum < bNum) {
return -1;
}

if (aNum > bNum) {
return 1;
}
} else {
if (aChar < bChar) {
return -1;
}
if (aChar > bChar) {
return 1;
}
++aIdx;
++bIdx;
}
}

return aStr.length - bStr.length;
}

const DIGIT_0 = 48;
const DIGIT_9 = 57;

function isDigit(code: number): boolean {
return !isNaN(code) && DIGIT_0 <= code && code <= DIGIT_9;
}
4 changes: 3 additions & 1 deletion src/jsutils/suggestionList.js
@@ -1,3 +1,5 @@
import naturalCompare from './naturalCompare';

/**
* Given an invalid input string and a list of valid options, returns a filtered
* list of valid options sorted based on their similarity with the input.
Expand All @@ -19,7 +21,7 @@ export default function suggestionList(

return Object.keys(optionsByDistance).sort((a, b) => {
const distanceDiff = optionsByDistance[a] - optionsByDistance[b];
return distanceDiff !== 0 ? distanceDiff : a.localeCompare(b);
return distanceDiff !== 0 ? distanceDiff : naturalCompare(a, b);
});
}

Expand Down
8 changes: 6 additions & 2 deletions src/utilities/findBreakingChanges.js
Expand Up @@ -3,6 +3,7 @@ import objectValues from '../polyfills/objectValues';
import keyMap from '../jsutils/keyMap';
import inspect from '../jsutils/inspect';
import invariant from '../jsutils/invariant';
import naturalCompare from '../jsutils/naturalCompare';

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

const sortedAST = visit(ast, {
ObjectValue(objectNode) {
const fields = [...objectNode.fields].sort((fieldA, fieldB) =>
fieldA.name.value.localeCompare(fieldB.name.value),
// Make a copy since sort mutates array
const fields = [...objectNode.fields];

fields.sort((fieldA, fieldB) =>
naturalCompare(fieldA.name.value, fieldB.name.value),
);
return { ...objectNode, fields };
},
Expand Down
3 changes: 2 additions & 1 deletion src/utilities/lexicographicSortSchema.js
Expand Up @@ -4,6 +4,7 @@ import type { ObjMap } from '../jsutils/ObjMap';
import inspect from '../jsutils/inspect';
import invariant from '../jsutils/invariant';
import keyValMap from '../jsutils/keyValMap';
import naturalCompare from '../jsutils/naturalCompare';

import type {
GraphQLType,
Expand Down Expand Up @@ -180,6 +181,6 @@ function sortBy<T>(
return array.slice().sort((obj1, obj2) => {
const key1 = mapToKey(obj1);
const key2 = mapToKey(obj2);
return key1.localeCompare(key2);
return naturalCompare(key1, key2);
});
}
3 changes: 2 additions & 1 deletion src/validation/rules/FieldsOnCorrectTypeRule.js
Expand Up @@ -2,6 +2,7 @@ import arrayFrom from '../../polyfills/arrayFrom';

import didYouMean from '../../jsutils/didYouMean';
import suggestionList from '../../jsutils/suggestionList';
import naturalCompare from '../../jsutils/naturalCompare';

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

Expand Down Expand Up @@ -122,7 +123,7 @@ function getSuggestedTypeNames(
return 1;
}

return typeA.name.localeCompare(typeB.name);
return naturalCompare(typeA.name, typeB.name);
})
.map((x) => x.name);
}
Expand Down

0 comments on commit 16d2535

Please sign in to comment.