Skip to content

Commit

Permalink
fix: performance issue of sorting the keys of large objects
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed Nov 5, 2024
1 parent 187e994 commit 3708998
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 40 deletions.
35 changes: 9 additions & 26 deletions src/lib/logic/sort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,15 @@ describe('sort', () => {
const object = { b: 1, c: 1, a: 1 }

assert.deepStrictEqual(sortJson(object, undefined, undefined, 1), [
{ op: 'move', from: '/a', path: '/a' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/c', path: '/c' }
{ op: 'replace', path: '', value: { a: 1, b: 1, c: 1 } }
])
})

test('should sort an arbitrary json object in descending order', () => {
const object = { b: 1, c: 1, a: 1 }

assert.deepStrictEqual(sortJson(object, undefined, undefined, -1), [
{ op: 'move', from: '/c', path: '/c' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/a', path: '/a' }
{ op: 'replace', path: '', value: { c: 1, b: 1, a: 1 } }
])
})

Expand All @@ -41,19 +37,15 @@ describe('sort', () => {
}

assert.deepStrictEqual(sortJson(object, ['root', 'path']), [
{ op: 'move', from: '/root/path/a', path: '/root/path/a' },
{ op: 'move', from: '/root/path/b', path: '/root/path/b' },
{ op: 'move', from: '/root/path/c', path: '/root/path/c' }
{ op: 'replace', path: '/root/path', value: { a: 1, b: 1, c: 1 } }
])
})

test('should sort a nested object inside an array', () => {
const object = [{ b: 1, c: 1, a: 1 }]

assert.deepStrictEqual(sortJson(object, ['0']), [
{ op: 'move', from: '/0/a', path: '/0/a' },
{ op: 'move', from: '/0/b', path: '/0/b' },
{ op: 'move', from: '/0/c', path: '/0/c' }
{ op: 'replace', path: '/0', value: { a: 1, b: 1, c: 1 } }
])
})

Expand Down Expand Up @@ -97,21 +89,15 @@ describe('sort', () => {
const object = { b: 1, c: 1, a: 1 }

assert.deepStrictEqual(sortObjectKeys(object), [
{ op: 'move', from: '/a', path: '/a' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/c', path: '/c' }
{ op: 'replace', path: '', value: { a: 1, b: 1, c: 1 } }
])

assert.deepStrictEqual(sortObjectKeys(object, undefined, 1), [
{ op: 'move', from: '/a', path: '/a' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/c', path: '/c' }
{ op: 'replace', path: '', value: { a: 1, b: 1, c: 1 } }
])

assert.deepStrictEqual(sortObjectKeys(object, undefined, -1), [
{ op: 'move', from: '/c', path: '/c' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/a', path: '/a' }
{ op: 'replace', path: '', value: { c: 1, b: 1, a: 1 } }
])
})

Expand All @@ -123,18 +109,15 @@ describe('sort', () => {
}

assert.deepStrictEqual(sortObjectKeys(object, ['root', 'path']), [
{ op: 'move', from: '/root/path/a', path: '/root/path/a' },
{ op: 'move', from: '/root/path/b', path: '/root/path/b' },
{ op: 'move', from: '/root/path/c', path: '/root/path/c' }
{ op: 'replace', path: '/root/path', value: { a: 1, b: 1, c: 1 } }
])
})

test('should sort object keys case insensitive', () => {
const object = { B: 1, a: 1 }

assert.deepStrictEqual(sortObjectKeys(object), [
{ op: 'move', from: '/a', path: '/a' },
{ op: 'move', from: '/B', path: '/B' }
{ op: 'replace', path: '', value: { a: 1, B: 1 } }
])
})

Expand Down
26 changes: 12 additions & 14 deletions src/lib/logic/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,35 +61,33 @@ export function sortJson(
* object to be sorted
* @param [rootPath=[]] Relative path when the array was located
* @param [direction=1] Pass 1 to sort ascending, -1 to sort descending
* @return Returns a JSONPatch document with move operation
* @return Returns a JSONPatch document with operations
* to get the array sorted.
*/
export function sortObjectKeys(
json: unknown,
rootPath: JSONPath = [],
direction: 1 | -1 = 1
): JSONPatchDocument {
const object = getIn(json, rootPath)
const object = getIn(json, rootPath) as Record<string, unknown>
const keys = Object.keys(object as unknown as Record<string, unknown>)
const sortedKeys = keys.slice()

sortedKeys.sort((keyA, keyB) => {
return direction * caseInsensitiveNaturalCompare(keyA, keyB)
})

// TODO: can we make this more efficient? check if the first couple of keys are already in order and if so ignore them
const operations: JSONPatchDocument = []
for (let i = 0; i < sortedKeys.length; i++) {
const key = sortedKeys[i]
const path = compileJSONPointer(rootPath.concat(key))
operations.push({
op: 'move',
from: path,
path
})
}
// for performance reasons, do a full replace (we could also create a move operation for every key)
const sortedObject: Record<string, unknown> = {}
sortedKeys.forEach((key) => (sortedObject[key] = object[key]))

return operations
return [
{
op: 'replace',
path: compileJSONPointer(rootPath),
value: sortedObject
}
]
}

/**
Expand Down

0 comments on commit 3708998

Please sign in to comment.