From 0e224974676b1ae3a62b9648b022b3c93718425b Mon Sep 17 00:00:00 2001 From: jos Date: Wed, 20 Dec 2017 08:36:55 +0100 Subject: [PATCH] Implemented sort action that keeps state --- package.json | 1 + src/actions.js | 79 ++++++++++++++++++++++++++++---------------- src/patchEson.js | 5 --- test/actions.test.js | 79 ++++++++++++++++++++++++++++++++++++++++++++ test/eson.test.js | 2 -- 5 files changed, 131 insertions(+), 35 deletions(-) create mode 100644 test/actions.test.js diff --git a/package.json b/package.json index f6ab2e7..74b35e0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test": "ava --verbose", "test-eson": "ava --verbose test/eson.test.js", "test-patch": "ava --verbose test/patchEson.test.js", + "test-actions": "ava --verbose test/actions.test.js", "watch:test": "ava --verbose --watch" }, "dependencies": { diff --git a/src/actions.js b/src/actions.js index d85090b..0619416 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,5 +1,6 @@ import last from 'lodash/last' import initial from 'lodash/initial' +import isEmpty from 'lodash/isEmpty' import { META, compileJSONPointer, esonToJson, findNextProp, @@ -304,54 +305,76 @@ export function sort (eson, path, order = null) { // console.log('sort', path, order) const compare = order === 'desc' ? compareDesc : compareAsc + const reverseCompare = (a, b) => -compare(a, b) const object = getIn(eson, path) if (object[META].type === 'Array') { - const orderedItems = cloneWithSymbols(object) + const items = object.map(item => item[META].value) + const createAction = (value, fromIndex, toIndex, array) => ({ + op: 'move', + from: compileJSONPointer(path.concat(String(fromIndex))), + path: compileJSONPointer(path.concat(String(toIndex))) + }) - // order the items by value - orderedItems.sort((a, b) => compare(a[META].value, b[META].value)) + const actions = sortWithComparator(items, compare, createAction) // when no order is provided, test whether ordering ascending // changed anything. If not, sort descending - if (!order && strictShallowEqual(object, orderedItems)) { - orderedItems.reverse() + if (!order && isEmpty(actions)) { + return sortWithComparator(items, reverseCompare, createAction) } - // TODO: refactor into a set of move actions, so we keep eson state of the items - - return [{ - op: 'replace', - path: compileJSONPointer(path), - value: esonToJson(orderedItems) - }] + return actions } else { // object[META].type === 'Object' - // order the properties by key - const orderedProps = object[META].props.slice().sort(compare) + const props = object[META].props + const createAction = (value, fromIndex, toIndex, objectProps) => ({ + op: 'move', + from: compileJSONPointer(path.concat(value)), + path: compileJSONPointer(path.concat(value)), + meta: { + before: props[toIndex] + } + }) + + const actions = sortWithComparator(props, compare, createAction) // when no order is provided, test whether ordering ascending // changed anything. If not, sort descending - if (!order && strictShallowEqual(object[META].props, orderedProps)) { - orderedProps.reverse() + if (!order && isEmpty(actions)) { + return sortWithComparator(props, reverseCompare, createAction) } - const orderedObject = setIn(object, [META, 'props'], orderedProps) - - // TODO: refactor into a set of move actions, so we keep eson state of the items - - return [{ - op: 'replace', - path: compileJSONPointer(path), - value: esonToJson(orderedObject), - meta: { - order: orderedProps // TODO: order isn't used right now in patchEson. - } - }] + return actions } } +// TODO: comment +function sortWithComparator (items, comparator, createAction) { + const orderedItems = items.slice() + + let actions = [] + for (let i = 0; i < orderedItems.length; i++) { + let firstIndex = i + for (let j = i; j < orderedItems.length; j++) { + if (comparator(orderedItems[firstIndex], orderedItems[j]) > 0) { + firstIndex = j + } + } + + if (i !== firstIndex) { + const firstItem = orderedItems[firstIndex] + orderedItems.splice(firstIndex, 1) + orderedItems.unshift(firstItem) + + actions.push(createAction(firstItem, firstIndex, i)) + } + } + + return actions +} + /** * Create a JSON entry * @param {ESONType} type diff --git a/src/patchEson.js b/src/patchEson.js index 7ef8dd5..1bd3e0d 100644 --- a/src/patchEson.js +++ b/src/patchEson.js @@ -138,11 +138,6 @@ export function replace (data, path, value) { // keep the original id let newValue = setIn(value, [META, 'id'], oldValue[META].id) - // FIXME: get the original expanded state of the copied value from JSON-Patch - if (newValue[META].type === 'Object' || newValue[META].type === 'Array') { - newValue = setIn(newValue, [META, 'expanded'], true) - } - return { data: setIn(data, path, newValue), revert: [{ diff --git a/test/actions.test.js b/test/actions.test.js new file mode 100644 index 0000000..89de924 --- /dev/null +++ b/test/actions.test.js @@ -0,0 +1,79 @@ +'use strict' + +import test from 'ava' +import { + sort +} from '../src/actions' +import { assertDeepEqualEson } from './utils/assertDeepEqualEson' +import {esonToJson, expandOne, jsonToEson, META} from '../src/eson' +import {patchEson} from '../src/patchEson' + +// TODO: test changeValue +// TODO: test changeProperty +// TODO: test changeType (or cleanup the function) +// TODO: test duplicate +// TODO: test insertBefore +// TODO: test replace +// TODO: test append +// TODO: test remove +// TODO: test removeAll + +// FIXME: sort root array +// test('sort root Array', t => { +// const eson = jsonToEson([1,3,2]) +// +// t.deepEqual(patchEson(eson, sort(eson, [])), jsonToEson([1,2,3])) +// t.deepEqual(patchEson(eson, sort(eson, [], 'asc')), jsonToEson([1,2,3])) +// t.deepEqual(patchEson(eson, sort(eson, [], 'desc')), jsonToEson([3,2,1])) +// }) + +test('sort nested Array', t => { + const eson = jsonToEson({arr: [4,1,8,5,3,9,2,7,6]}) + const actual = patchEson(eson, sort(eson, ['arr'])).data + const expected = jsonToEson({arr: [1,2,3,4,5,6,7,8,9]}) + assertDeepEqualEson(t, actual, expected) +}) + +test('sort nested Array reverse order', t => { + // no order provided -> order ascending, but if nothing changes, order descending + const eson = jsonToEson({arr: [1,2,3,4,5,6,7,8,9]}) + const actual = patchEson(eson, sort(eson, ['arr'])).data + const expected = jsonToEson({arr: [9,8,7,6,5,4,3,2,1]}) + assertDeepEqualEson(t, actual, expected) + + // id's and META should be the same + t.deepEqual(actual.arr[META].id, eson.arr[META].id) + t.deepEqual(actual.arr[7][META].id, eson.arr[1][META].id) +}) + + +// FIXME: sort root object +// test('sort root Object', t => { +// const eson = jsonToEson({c: 2, b: 3, a:4}) +// +// t.deepEqual(esonToJson(patchEson(eson, sort(eson, [])).data[META].props), ['a', 'b', 'c']) +// t.deepEqual(esonToJson(patchEson(eson, sort(eson, [], 'asc')).data[META].props), ['a', 'b', 'c']) +// t.deepEqual(esonToJson(patchEson(eson, sort(eson, [], 'desc')).data[META].props), ['c', 'b', 'a']) +// }) + +test('sort nested Object', t => { + const eson = jsonToEson({obj: {c: 2, b: 3, a:4}}) + eson.obj[META].expanded = true + eson.obj.c[META].expanded = true + + const actual = patchEson(eson, sort(eson, ['obj'])).data + + // should keep META data + t.deepEqual(actual.obj[META].props, ['a', 'b', 'c']) + t.deepEqual(actual.obj[META].expanded, true) + t.deepEqual(actual.obj.c[META].expanded, true) + t.deepEqual(actual.obj[META].id, eson.obj[META].id) + t.deepEqual(actual.obj.a[META].id, eson.obj.a[META].id) + t.deepEqual(actual.obj.b[META].id, eson.obj.b[META].id) + t.deepEqual(actual.obj.c[META].id, eson.obj.c[META].id) + + // asc, desc + t.deepEqual(patchEson(eson, sort(eson, ['obj'])).data.obj[META].props, ['a', 'b', 'c']) + t.deepEqual(patchEson(eson, sort(eson, ['obj'], 'asc')).data.obj[META].props, ['a', 'b', 'c']) + t.deepEqual(patchEson(eson, sort(eson, ['obj'], 'desc')).data.obj[META].props, ['c', 'b', 'a']) +}) diff --git a/test/eson.test.js b/test/eson.test.js index 83ea327..200cddb 100644 --- a/test/eson.test.js +++ b/test/eson.test.js @@ -17,8 +17,6 @@ import 'console.table' import repeat from 'lodash/repeat' import { assertDeepEqualEson } from './utils/assertDeepEqualEson' -const ESON2 = loadJSON('./resources/eson2.json') - test('jsonToEson', t => { assertDeepEqualEson(t, jsonToEson(1), {[META]: {id: '[ID]', path: [], type: 'value', value: 1}}) assertDeepEqualEson(t, jsonToEson("foo"), {[META]: {id: '[ID]', path: [], type: 'value', value: "foo"}})