Refactor sorting to return a JSONPatchDocument

This commit is contained in:
Jos de Jong 2020-08-30 14:38:12 +02:00
parent a28335fe57
commit a24045cb42
7 changed files with 208 additions and 105 deletions

10
package-lock.json generated
View File

@ -1351,11 +1351,6 @@
"iterate-iterator": "^1.0.1" "iterate-iterator": "^1.0.1"
} }
}, },
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k="
},
"jest-worker": { "jest-worker": {
"version": "26.0.0", "version": "26.0.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz",
@ -1635,6 +1630,11 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true "dev": true
}, },
"natural-compare-lite": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
"integrity": "sha1-F7CVgZiJef3a/gIB6TG6kzyWy7Q="
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",

View File

@ -21,8 +21,8 @@
"ace-builds": "1.4.12", "ace-builds": "1.4.12",
"ajv": "6.12.3", "ajv": "6.12.3",
"classnames": "2.2.6", "classnames": "2.2.6",
"javascript-natural-sort": "0.7.1",
"lodash-es": "4.17.15", "lodash-es": "4.17.15",
"natural-compare-lite": "1.4.0",
"svelte-awesome": "2.3.0", "svelte-awesome": "2.3.0",
"svelte-select": "3.11.1", "svelte-select": "3.11.1",
"svelte-simple-modal": "0.6.0" "svelte-simple-modal": "0.6.0"

View File

@ -120,12 +120,14 @@
console.time('create large json') console.time('create large json')
const largeJson = {} const largeJson = {}
largeJson.numbers = [] largeJson.numbers = []
largeJson.randomNumbers = []
largeJson.array = [] largeJson.array = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const longitude = 4 + i / count const longitude = 4 + i / count
const latitude = 51 + i / count const latitude = 51 + i / count
largeJson.numbers.push(i) largeJson.numbers.push(i)
largeJson.randomNumbers.push(Math.round(Math.random() * 1000))
largeJson.array.push({ largeJson.array.push({
name: 'Item ' + i, name: 'Item ' + i,
id: String(i), id: String(i),

View File

@ -10,7 +10,7 @@
import { sortArray, sortObjectKeys } from '../../logic/sort.js' import { sortArray, sortObjectKeys } from '../../logic/sort.js'
export let json export let json
export let path export let rootPath
export let onSort export let onSort
const {close} = getContext('simple-modal') const {close} = getContext('simple-modal')
@ -57,17 +57,14 @@
const property = selectedProperty.value const property = selectedProperty.value
const direction = selectedDirection.value const direction = selectedDirection.value
const sortedJson = sortArray(json, property, direction) const operations = sortArray(json, rootPath, property, direction)
onSort(sortedJson) onSort(operations)
} else if (isObject(json)) { } else if (isObject(json)) {
const direction = selectedDirection.value const direction = selectedDirection.value
const sortedJson = sortObjectKeys(json, direction) const operations = sortObjectKeys(json, rootPath, direction)
// FIXME: the keys are now sorted, but the JSONEditor refuses to reorder when already rendered -> need to do a JSONPatch onSort(operations)
console.log('sorted object keys:', Object.keys(sortedJson))
onSort(sortedJson)
} else { } else {
console.error('Cannot sort: no array or object') console.error('Cannot sort: no array or object')
} }
@ -86,7 +83,7 @@
<col width="75%"> <col width="75%">
</colgroup> </colgroup>
<tbody> <tbody>
{#if path.length > 0} {#if rootPath.length > 0}
<tr> <tr>
<th>Path</th> <th>Path</th>
<td> <td>
@ -94,7 +91,7 @@
class="path" class="path"
type="text" type="text"
readonly readonly
value={stringifyPath(path)} value={stringifyPath(rootPath)}
/> />
</td> </td>
</tr> </tr>

View File

@ -263,29 +263,23 @@
} }
function handleSort () { function handleSort () {
const path = selection && selection.paths const rootPath = selection && selection.paths
? selection.paths.length > 1 ? selection.paths.length > 1
? initial(first(selection.paths)) // the parent path of the paths ? initial(first(selection.paths)) // the parent path of the paths
: first(selection.paths) // the first and only path : first(selection.paths) // the first and only path
: [] : []
open(SortModal, { open(SortModal, {
json: getIn(doc, path), json: getIn(doc, rootPath),
path, rootPath,
onSort: async sortedJson => { onSort: async (operations) => {
console.log('onSort', path, sortedJson) console.log('onSort', rootPath, operations)
// TODO: replace this with move events instead of a big replace (currently we lose state)
const operations = [{
op: 'replace',
path: compileJSONPointer(path),
value: sortedJson
}]
patch(operations, selection) patch(operations, selection)
await tick() await tick()
handleExpand(path, true, false) handleExpand(rootPath, true, false)
} }
}, { }, {
...SIMPLE_MODAL_OPTIONS, ...SIMPLE_MODAL_OPTIONS,

View File

@ -1,17 +1,80 @@
import naturalCompare from 'natural-compare-lite'
import { getIn } from '../utils/immutabilityHelpers.js' import { getIn } from '../utils/immutabilityHelpers.js'
import naturalSort from 'javascript-natural-sort' import { compileJSONPointer } from '../utils/jsonPointer.js'
function caseInsensitiveNaturalCompare (a, b) {
const aLower = typeof a === 'string' ? a.toLowerCase() : a
const bLower = typeof b === 'string' ? b.toLowerCase() : b
return naturalCompare(aLower, bLower)
}
/**
* Sort the keys of an object
* @param {Object} object The object to be sorted
* @param {Path} [rootPath=[]] Relative path when the array was located
* @param {1 | -1} [direction=1] Pass 1 to sort ascending, -1 to sort descending
* @return {JSONPatchDocument} Returns a JSONPatch document with move operation
* to get the array sorted.
*/
export function sortObjectKeys (object, rootPath = [], direction = 1) {
const keys = Object.keys(object)
const sortedKeys = keys.slice()
sortedKeys.sort((keyA, keyB) => {
return direction * caseInsensitiveNaturalCompare(keyA, keyB)
})
// const sortedObject = {}
// keys.forEach(key => {
// sortedObject[key] = object[key]
// })
// TODO: only move the properties that are needed to move
const operations = []
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
})
}
return operations
}
/** /**
* Sort the items of an array * Sort the items of an array
* @param {Array} array The array to be sorted * @param {Array} array The array to be sorted
* @param {Path} [path=[]] Nested path to the property on which to sort the contents * @param {Path} [rootPath=[]] Relative path when the array was located
* @param {1 | -1} [direction=1] Pass 1 to sort ascending, -1 to sort descending * @param {Path} [propertyPath=[]] Nested path to the property on which to sort the contents
* @return {Array} Returns a sorted shallow copy of the array * @param {1 | -1} [direction=1] Pass 1 to sort ascending, -1 to sort descending
* @return {JSONPatchDocument} Returns a JSONPatch document with move operation
* to get the array sorted.
*/ */
export function sortArray (array, path = [], direction = 1) { export function sortArray (array, rootPath = [], propertyPath = [], direction = 1) {
function comparator (a, b) { const comparator = createObjectComparator(propertyPath, direction)
const valueA = getIn(a, path)
const valueB = getIn(b, path) return getSortingMoves(array, comparator).map(({ fromIndex, toIndex }) => {
return {
op: 'move',
from: compileJSONPointer(rootPath.concat(fromIndex)),
path: compileJSONPointer(rootPath.concat(toIndex))
}
})
}
/**
* Create a comparator function to compare nested properties in an array
* @param {Path} propertyPath
* @param {1 | -1} direction
*/
function createObjectComparator (propertyPath, direction) {
return function comparator (a, b) {
const valueA = getIn(a, propertyPath)
const valueB = getIn(b, propertyPath)
if (valueA === undefined) { if (valueA === undefined) {
return direction return direction
@ -22,54 +85,31 @@ export function sortArray (array, path = [], direction = 1) {
if (typeof valueA !== 'string' && typeof valueB !== 'string') { if (typeof valueA !== 'string' && typeof valueB !== 'string') {
// both values are a number, boolean, or null -> use simple, fast sorting // both values are a number, boolean, or null -> use simple, fast sorting
return valueA > valueB return valueA > valueB
? direction ? direction
: valueA < valueB : valueA < valueB
? -direction ? -direction
: 0 : 0
} }
return direction * naturalSort(valueA, valueB) return direction * caseInsensitiveNaturalCompare(valueA, valueB)
} }
// TODO: use lodash orderBy, split comparator and direction?
const sortedArray = array.slice()
sortedArray.sort(comparator)
return sortedArray
}
/**
* Sort the keys of an object
* @param {Object} object The object to be sorted
* @param {1 | -1} [direction=1] Pass 1 to sort ascending, -1 to sort descending
* @return {Object} Returns a sorted shallow copy of the object
*/
export function sortObjectKeys (object, direction = 1) {
const keys = Object.keys(object)
keys.sort((keyA, keyB) => {
return direction * naturalSort(keyA, keyB)
})
const sortedObject = {}
keys.forEach(key => sortedObject[key] = object[key])
return sortedObject
} }
/** /**
* Create an array containing all move operations * Create an array containing all move operations
* needed to sort the array contents. * needed to sort the array contents.
* @param {Array} array * @param {Array} array
* @param {function (a, b) => number} comparator * @param {function (a, b) => number} comparator
* @param {Array.<{fromIndex: number, toIndex: number}>} * @param {Array.<{fromIndex: number, toIndex: number}>}
*/ */
export function sortMoveOperations (array, comparator) { export function getSortingMoves (array, comparator) {
const operations = [] const operations = []
const sorted = [] const sorted = []
// TODO: rewrite the function to pass a callback instead of returning an array?
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
// TODO: implement a faster way to sort (binary tree sort?) // TODO: implement a faster way to sort. Something with longest increasing subsequence?
// TODO: can we simplify the following code? // TODO: can we simplify the following code?
const item = array[i] const item = array[i]
if (i > 0 && comparator(sorted[i - 1], item) > 0) { if (i > 0 && comparator(sorted[i - 1], item) > 0) {
@ -83,7 +123,7 @@ export function sortMoveOperations (array, comparator) {
toIndex: j toIndex: j
}) })
sorted.splice(j, 0, [item]) sorted.splice(j, 0, item)
} else { } else {
sorted.push(item) sorted.push(item)
} }

View File

@ -1,61 +1,131 @@
import assert from 'assert' import assert from 'assert'
import { sortArray, sortObjectKeys, sortMoveOperations } from './sort.js' import { sortArray, sortObjectKeys, getSortingMoves } from './sort.js'
describe.only('sort', () => { describe('sort', () => {
it('should sort array', () => {
assert.deepStrictEqual(sortArray([ 2, 3, 1 ]), [1, 2, 3])
assert.deepStrictEqual(sortArray([ 2, 3, 1 ], undefined, -1), [3, 2, 1])
})
it('should sort array using natural sort', () => {
assert.deepStrictEqual(sortArray([ '10', '2', '1' ]), ['1', '2', '10'])
})
it('should sort array by nested properties', () => {
const a = {data: { value: 1 }}
const b = {data: { value: 2 }}
const c = {data: { value: 3 }}
assert.deepStrictEqual(sortArray([ b, c, a ], ['data', 'value']), [a, b, c])
assert.deepStrictEqual(sortArray([ b, a, c ], ['data', 'value']), [a, b, c])
assert.deepStrictEqual(sortArray([ b, a, c ], ['data', 'value'], 1), [a, b, c])
assert.deepStrictEqual(sortArray([ b, a, c ], ['data', 'value'], -1), [c, b, a])
})
it('should sort object keys', () => { it('should sort object keys', () => {
const object = { b: 1, c: 1, a: 1 } const object = { b: 1, c: 1, a: 1 }
assert.deepStrictEqual(Object.keys(sortObjectKeys(object)), ['a', 'b', 'c']) assert.deepStrictEqual(sortObjectKeys(object), [
assert.deepStrictEqual(Object.keys(sortObjectKeys(object, 1)), ['a', 'b', 'c']) { op: 'move', from: '/a', path: '/a' },
assert.deepStrictEqual(Object.keys(sortObjectKeys(object, -1)), ['c', 'b', 'a']) { op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/c', path: '/c' }
])
assert.deepStrictEqual(sortObjectKeys(object, undefined, 1), [
{ op: 'move', from: '/a', path: '/a' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/c', path: '/c' }
])
assert.deepStrictEqual(sortObjectKeys(object, undefined, -1), [
{ op: 'move', from: '/c', path: '/c' },
{ op: 'move', from: '/b', path: '/b' },
{ op: 'move', from: '/a', path: '/a' }
])
})
it('should sort object keys using a rootPath', () => {
const object = { b: 1, c: 1, a: 1 }
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' }
])
})
it('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' }
])
})
it('should sort array', () => {
assert.deepStrictEqual(sortArray([2, 3, 1]), [
{ op: 'move', from: '/2', path: '/0' }
])
assert.deepStrictEqual(sortArray([2, 3, 1], undefined, undefined, -1), [
{ op: 'move', from: '/1', path: '/0' }
])
})
it('should sort array using natural sort', () => {
assert.deepStrictEqual(sortArray(['10', '2', '1']), [
{ op: 'move', from: '/1', path: '/0' },
{ op: 'move', from: '/2', path: '/0' }
])
})
it('should sort array case insensitive', () => {
assert.deepStrictEqual(sortArray(['B', 'a']), [
{ op: 'move', from: '/1', path: '/0' }
])
})
it('should sort array using a rootPath', () => {
assert.deepStrictEqual(sortArray([2, 3, 1], ['root', 'path']), [
{ op: 'move', from: '/root/path/2', path: '/root/path/0' }
])
})
it('should sort array by nested properties and custom direction', () => {
const a = { data: { value: 1 } }
const b = { data: { value: 2 } }
const c = { data: { value: 3 } }
assert.deepStrictEqual(sortArray([b, a, c], undefined, ['data', 'value']), [
{ op: 'move', from: '/1', path: '/0' }
])
assert.deepStrictEqual(sortArray([b, a, c], undefined, ['data', 'value'], 1), [
{ op: 'move', from: '/1', path: '/0' }
])
assert.deepStrictEqual(sortArray([b, a, c], undefined, ['data', 'value'], -1), [
{ op: 'move', from: '/2', path: '/0' }
])
}) })
it('should give the move operations needed to sort given array', () => { it('should give the move operations needed to sort given array', () => {
const comparator = (a, b) => a - b const comparator = (a, b) => a - b
assert.deepStrictEqual(sortMoveOperations([ 1, 2, 3 ], comparator), []) assert.deepStrictEqual(getSortingMoves([1, 2, 3], comparator), [])
assert.deepStrictEqual(sortMoveOperations([ 2, 3, 1 ], comparator), [ assert.deepStrictEqual(getSortingMoves([2, 3, 1], comparator), [
{ fromIndex: 2, toIndex: 0 } { fromIndex: 2, toIndex: 0 }
]) ])
assert.deepStrictEqual(sortMoveOperations([ 2, 1, 3 ], comparator), [ assert.deepStrictEqual(getSortingMoves([2, 1, 3], comparator), [
{ fromIndex: 1, toIndex: 0 } { fromIndex: 1, toIndex: 0 }
]) ])
assert.deepStrictEqual(sortMoveOperations([ 1, 3, 2 ], comparator), [ assert.deepStrictEqual(getSortingMoves([1, 3, 2], comparator), [
{ fromIndex: 2, toIndex: 1 } { fromIndex: 2, toIndex: 1 }
]) ])
assert.deepStrictEqual(sortMoveOperations([ 3, 2, 1 ], comparator), [ assert.deepStrictEqual(getSortingMoves([3, 2, 1], comparator), [
{ fromIndex: 1, toIndex: 0 }, { fromIndex: 1, toIndex: 0 },
{ fromIndex: 2, toIndex: 0 } { fromIndex: 2, toIndex: 0 }
]) ])
assert.deepStrictEqual(sortMoveOperations([ 3, 1, 2 ], comparator), [ assert.deepStrictEqual(getSortingMoves([3, 1, 2], comparator), [
{ fromIndex: 1, toIndex: 0 }, { fromIndex: 1, toIndex: 0 },
{ fromIndex: 2, toIndex: 1 } { fromIndex: 2, toIndex: 1 }
]) ])
}) })
it('should give the move operations needed to sort given array containing objects', () => {
const comparator = (a, b) => a.id - b.id
const actual = getSortingMoves([{ id: 4 }, { id: 3 }, { id: 1 }, { id: 2 }], comparator)
const expected = [
{ fromIndex: 1, toIndex: 0 },
{ fromIndex: 2, toIndex: 0 },
{ fromIndex: 3, toIndex: 1 }
]
assert.deepStrictEqual(actual, expected)
})
}) })