Skip to content

Commit 9a988d0

Browse files
authored
feat: accept comparator function as deterministic option (#49)
Accept `Array#sort(comparator)` comparator method as deterministic option value to use that comparator for sorting object keys. ```js import { configure } from 'safe-stable-stringify' const object = { a: 1, b: 2, c: 3, } const stringify = configure({ deterministic: (a, b) => b.localeCompare(a) }) stringify(object) // '{"c": 3,"b":2,"a":1}'
1 parent 7b7ec1b commit 9a988d0

File tree

3 files changed

+68
-9
lines changed

3 files changed

+68
-9
lines changed

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function stringify(value: unknown, replacer?: ((key: string, value: unkno
77
export interface StringifyOptions {
88
bigint?: boolean,
99
circularValue?: string | null | TypeErrorConstructor | ErrorConstructor,
10-
deterministic?: boolean,
10+
deterministic?: boolean | ((a: string, b: string) => number),
1111
maximumBreadth?: number,
1212
maximumDepth?: number,
1313
strict?: boolean,

index.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ function strEscape (str) {
3232
return JSON.stringify(str)
3333
}
3434

35-
function insertSort (array) {
36-
// Insertion sort is very efficient for small input sizes but it has a bad
35+
function insertSort (array, comparator) {
36+
// Insertion sort is very efficient for small input sizes, but it has a bad
3737
// worst case complexity. Thus, use native array sort for bigger values.
38-
if (array.length > 2e2) {
39-
return array.sort()
38+
if (array.length > 2e2 || comparator) {
39+
return array.sort(comparator)
4040
}
4141
for (let i = 1; i < array.length; i++) {
4242
const currentValue = array[i]
@@ -97,6 +97,23 @@ function getCircularValueOption (options) {
9797
return '"[Circular]"'
9898
}
9999

100+
function getDeterministicOption (options, key) {
101+
let value
102+
if (hasOwnProperty.call(options, key)) {
103+
value = options[key]
104+
if (typeof value === 'boolean') {
105+
return value
106+
}
107+
if (typeof value === 'function') {
108+
return value
109+
}
110+
}
111+
if (value === undefined) {
112+
return true
113+
}
114+
throw new TypeError(`The "${key}" argument must be of type boolean or comparator function`)
115+
}
116+
100117
function getBooleanOption (options, key) {
101118
let value
102119
if (hasOwnProperty.call(options, key)) {
@@ -171,7 +188,8 @@ function configure (options) {
171188
}
172189
const circularValue = getCircularValueOption(options)
173190
const bigint = getBooleanOption(options, 'bigint')
174-
const deterministic = getBooleanOption(options, 'deterministic')
191+
const deterministic = getDeterministicOption(options, 'deterministic')
192+
const comparator = typeof deterministic === 'function' ? deterministic : undefined
175193
const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth')
176194
const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth')
177195

@@ -248,7 +266,7 @@ function configure (options) {
248266
}
249267
const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
250268
if (deterministic && !isTypedArrayWithEntries(value)) {
251-
keys = insertSort(keys)
269+
keys = insertSort(keys, comparator)
252270
}
253271
stack.push(value)
254272
for (let i = 0; i < maximumPropertiesToStringify; i++) {
@@ -447,7 +465,7 @@ function configure (options) {
447465
separator = join
448466
}
449467
if (deterministic) {
450-
keys = insertSort(keys)
468+
keys = insertSort(keys, comparator)
451469
}
452470
stack.push(value)
453471
for (let i = 0; i < maximumPropertiesToStringify; i++) {
@@ -551,7 +569,7 @@ function configure (options) {
551569
separator = ','
552570
}
553571
if (deterministic) {
554-
keys = insertSort(keys)
572+
keys = insertSort(keys, comparator)
555573
}
556574
stack.push(value)
557575
for (let i = 0; i < maximumPropertiesToStringify; i++) {

test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,3 +1303,44 @@ test('strict option replacer array', (assert) => {
13031303

13041304
assert.end()
13051305
})
1306+
1307+
test('deterministic option possibilities', (assert) => {
1308+
assert.throws(() => {
1309+
// @ts-expect-error
1310+
stringify.configure({ deterministic: 1 })
1311+
}, {
1312+
message: 'The "deterministic" argument must be of type boolean or comparator function',
1313+
name: 'TypeError'
1314+
})
1315+
1316+
const serializer1 = stringify.configure({ deterministic: false })
1317+
serializer1(NaN)
1318+
1319+
const serializer2 = stringify.configure({ deterministic: (a, b) => a.localeCompare(b) })
1320+
serializer2(NaN)
1321+
1322+
assert.end()
1323+
})
1324+
1325+
test('deterministic default sorting', function (assert) {
1326+
const serializer = stringify.configure({ deterministic: true })
1327+
1328+
const obj = { b: 2, c: 3, a: 1 }
1329+
const expected = '{\n "a": 1,\n "b": 2,\n "c": 3\n}'
1330+
const actual = serializer(obj, null, 1)
1331+
assert.equal(actual, expected)
1332+
1333+
assert.end()
1334+
})
1335+
1336+
test('deterministic custom sorting', function (assert) {
1337+
// Descending
1338+
const serializer = stringify.configure({ deterministic: (a, b) => b.localeCompare(a) })
1339+
1340+
const obj = { b: 2, c: 3, a: 1 }
1341+
const expected = '{\n "c": 3,\n "b": 2,\n "a": 1\n}'
1342+
const actual = serializer(obj, null, 1)
1343+
assert.equal(actual, expected)
1344+
1345+
assert.end()
1346+
})

0 commit comments

Comments
 (0)