diff --git a/index.d.ts b/index.d.ts index 442bda5..400fca0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,7 +7,7 @@ export function stringify(value: unknown, replacer?: ((key: string, value: unkno export interface StringifyOptions { bigint?: boolean, circularValue?: string | null | TypeErrorConstructor | ErrorConstructor, - deterministic?: boolean, + deterministic?: boolean | ((a: string, b: string) => number), maximumBreadth?: number, maximumDepth?: number, strict?: boolean, diff --git a/index.js b/index.js index e083198..95b4e48 100644 --- a/index.js +++ b/index.js @@ -32,11 +32,11 @@ function strEscape (str) { return JSON.stringify(str) } -function insertSort (array) { - // Insertion sort is very efficient for small input sizes but it has a bad +function insertSort (array, comparator) { + // Insertion sort is very efficient for small input sizes, but it has a bad // worst case complexity. Thus, use native array sort for bigger values. - if (array.length > 2e2) { - return array.sort() + if (array.length > 2e2 || comparator) { + return array.sort(comparator) } for (let i = 1; i < array.length; i++) { const currentValue = array[i] @@ -97,6 +97,23 @@ function getCircularValueOption (options) { return '"[Circular]"' } +function getDeterministicOption (options, key) { + let value + if (hasOwnProperty.call(options, key)) { + value = options[key] + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'function') { + return value + } + } + if (value === undefined) { + return true + } + throw new TypeError(`The "${key}" argument must be of type boolean or comparator function`) +} + function getBooleanOption (options, key) { let value if (hasOwnProperty.call(options, key)) { @@ -171,7 +188,8 @@ function configure (options) { } const circularValue = getCircularValueOption(options) const bigint = getBooleanOption(options, 'bigint') - const deterministic = getBooleanOption(options, 'deterministic') + const deterministic = getDeterministicOption(options, 'deterministic') + const comparator = typeof deterministic === 'function' ? deterministic : undefined const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth') const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth') @@ -248,7 +266,7 @@ function configure (options) { } const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) if (deterministic && !isTypedArrayWithEntries(value)) { - keys = insertSort(keys) + keys = insertSort(keys, comparator) } stack.push(value) for (let i = 0; i < maximumPropertiesToStringify; i++) { @@ -447,7 +465,7 @@ function configure (options) { separator = join } if (deterministic) { - keys = insertSort(keys) + keys = insertSort(keys, comparator) } stack.push(value) for (let i = 0; i < maximumPropertiesToStringify; i++) { @@ -551,7 +569,7 @@ function configure (options) { separator = ',' } if (deterministic) { - keys = insertSort(keys) + keys = insertSort(keys, comparator) } stack.push(value) for (let i = 0; i < maximumPropertiesToStringify; i++) { diff --git a/test.js b/test.js index 565ea40..163add9 100644 --- a/test.js +++ b/test.js @@ -1303,3 +1303,44 @@ test('strict option replacer array', (assert) => { assert.end() }) + +test('deterministic option possibilities', (assert) => { + assert.throws(() => { + // @ts-expect-error + stringify.configure({ deterministic: 1 }) + }, { + message: 'The "deterministic" argument must be of type boolean or comparator function', + name: 'TypeError' + }) + + const serializer1 = stringify.configure({ deterministic: false }) + serializer1(NaN) + + const serializer2 = stringify.configure({ deterministic: (a, b) => a.localeCompare(b) }) + serializer2(NaN) + + assert.end() +}) + +test('deterministic default sorting', function (assert) { + const serializer = stringify.configure({ deterministic: true }) + + const obj = { b: 2, c: 3, a: 1 } + const expected = '{\n "a": 1,\n "b": 2,\n "c": 3\n}' + const actual = serializer(obj, null, 1) + assert.equal(actual, expected) + + assert.end() +}) + +test('deterministic custom sorting', function (assert) { + // Descending + const serializer = stringify.configure({ deterministic: (a, b) => b.localeCompare(a) }) + + const obj = { b: 2, c: 3, a: 1 } + const expected = '{\n "c": 3,\n "b": 2,\n "a": 1\n}' + const actual = serializer(obj, null, 1) + assert.equal(actual, expected) + + assert.end() +})