Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,28 @@ function strEscape (str) {
return JSON.stringify(str)
}

/**
* @param {string[]} array
* @param {(a: string, b: string) => number} [comparator]
*/
function sort (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 || comparator) {
return array.sort(comparator)
}
let moves = 0
for (let i = 1; i < array.length; i++) {
const currentValue = array[i]
if (array[i - 1] <= currentValue) continue
let position = i
while (position !== 0 && array[position - 1] > currentValue) {
do {
array[position] = array[position - 1]
position--
}
} while (position !== 0 && array[position - 1] > currentValue)
array[position] = currentValue
moves += i - position
if (moves > array.length && moves > 32) return array.sort()
}
return array
}
Expand Down
27 changes: 25 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -800,8 +800,10 @@ test('bigint option as string', function (assert) {
assert.end()
})

test('bigint option invalid values', function (assert) {
test('bigint option invalid values', /** @param {import('tap').Test} assert */ function (assert) {
// @ts-expect-error
assert.throws(() => stringify.configure({ bigint: 'nope' }), /bigint/)
// @ts-expect-error
assert.throws(() => stringify.configure({ bigint: 1 }), /bigint/)
assert.end()
})
Expand Down Expand Up @@ -1591,6 +1593,26 @@ test('deterministic default sorting', function (assert) {
assert.end()
})

test('deterministic sorting preserves keys across insertion and native sort boundaries', /** @param {import('tap').Test} assert */ (assert) => {
for (const size of [8, 9, 31, 32, 33, 34, 199, 200, 201]) {
const keys = Array.from({ length: size }, /** @param {unknown} value @param {number} index */ (value, index) => `key${String(index).padStart(3, '0')}`)
const adjacent = keys.slice()
;[adjacent[1], adjacent[2]] = [adjacent[2], adjacent[1]]
for (const order of [keys, keys.slice().reverse(), adjacent, [keys[size - 1], ...keys.slice(0, -1)]]) {
const value = {}
const expected = {}
for (const key of order) value[key] = key
for (const key of keys) expected[key] = key
for (const replacer of [undefined, identityReplacer]) {
for (const space of [undefined, 2]) {
assert.equal(stringify({ nested: value }, replacer, space), JSON.stringify({ nested: expected }, null, space))
}
}
}
}
assert.end()
})

test('deterministic custom sorting', function (assert) {
// Descending
const serializer = stringify.configure({ deterministic: (a, b) => b.localeCompare(a) })
Expand All @@ -1613,7 +1635,8 @@ test('safe option defaults and explicit false', function (assert) {
assert.end()
})

test('safe option must be boolean', function (assert) {
test('safe option must be boolean', /** @param {import('tap').Test} assert */ function (assert) {
// @ts-expect-error
assert.throws(() => stringify.configure({ safe: 'yes' }), /safe/)
assert.end()
})
Expand Down
Loading