From 7b2ad593fa0599d3171f0378a8e90f1d5a39032c Mon Sep 17 00:00:00 2001 From: ThomasR Date: Fri, 13 Jan 2017 22:42:00 +0100 Subject: [PATCH] GH-6 Add the possibility to use custom sorter --- dist/JSON.sortify.js | 4 +-- lib/index.js | 54 +++++++++++++++++++++++++++++++++++------ package.json | 6 ++--- test/test.js | 58 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/dist/JSON.sortify.js b/dist/JSON.sortify.js index eec0530..6be4c44 100644 --- a/dist/JSON.sortify.js +++ b/dist/JSON.sortify.js @@ -1,4 +1,4 @@ -"use strict";(function(factory){if(typeof define=="function"&&typeof define.amd=="object")define("json.sortify",factory);else JSON.sortify=factory()})(function(){ /*! +"use strict";(function(factory){if(typeof define=="function"&&typeof define.amd=="object")define("json.sortify",factory);else JSON.sortify=factory()})(function(){/*! * Copyright 2015-2016 Thomas Rosenau * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,4 +12,4 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. -*/"use strict";var sortKeys=function sortKeys(o){if(Array.isArray(o)){return o.map(sortKeys)}else if(o instanceof Object){var _ret=function(){var numeric=[];var nonNumeric=[];Object.keys(o).forEach(function(key){if(/^(0|[1-9][0-9]*)$/.test(key)){numeric.push(+key)}else {nonNumeric.push(key)}});return {v:numeric.sort(function(a,b){return a-b}).concat(nonNumeric.sort()).reduce(function(result,key){result[key]=sortKeys(o[key]);return result},{})}}();if(typeof _ret==="object")return _ret.v}return o};var jsonStringify=JSON.stringify.bind(JSON);var sortify=function sortify(value,replacer,space){var native=jsonStringify(value,replacer,0);if(!native||native[0]!=="{"&&native[0]!=="["){return native}var cleanObj=JSON.parse(native);return jsonStringify(sortKeys(cleanObj),null,space)};return sortify}); \ No newline at end of file +*/"use strict";var numericRE=/^(0|[1-9][0-9]*)$/;var sortKeys=function sortKeys(o,customOrder){if(Array.isArray(o)){return o.map(function(x){return sortKeys(x,customOrder)})}else if(o instanceof Object){var _ret=function(){var numeric=[];var nonNumeric=[];Object.keys(o).forEach(function(key){if(numericRE.test(key)){numeric.push(+key)}else{nonNumeric.push(key)}});return{v:numeric.sort(function(a,b){return a-b}).concat(nonNumeric.sort(customOrder)).reduce(function(result,key){result[key]=sortKeys(o[key],customOrder);return result},{})}}();if(typeof _ret==="object")return _ret.v}return o};var jsonStringify=JSON.stringify.bind(JSON);var orderFromArray=function orderFromArray(keyList){return function(a,b){var indexA=keyList.indexOf(a);var indexB=keyList.indexOf(b);if(indexA!=-1){if(indexB!=-1){return indexA-indexB}return-1}if(indexB!=-1){return 1}return a { +const sortKeys = (o, customOrder) => { if (Array.isArray(o)) { - return o.map(sortKeys); + return o.map(x => sortKeys(x, customOrder)); } else if (o instanceof Object) { // put numeric keys first let numeric = []; let nonNumeric = []; Object.keys(o).forEach(key => { - if (/^(0|[1-9][0-9]*)$/.test(key)) { + if (numericRE.test(key)) { numeric.push(+key); } else { nonNumeric.push(key); @@ -43,8 +47,8 @@ const sortKeys = o => { // do the rearrangement return numeric.sort(function (a, b) { return a - b; - }).concat(nonNumeric.sort()).reduce((result, key) => { - result[key] = sortKeys(o[key]); // recurse! + }).concat(nonNumeric.sort(customOrder)).reduce((result, key) => { + result[key] = sortKeys(o[key], customOrder); // recurse! return result; }, {}); } @@ -53,16 +57,50 @@ const sortKeys = o => { const jsonStringify = JSON.stringify.bind(JSON); // this allows redefinition like JSON.stringify = require('json.sortify') -const sortify = (value, replacer, space) => { +/** +* Helper that creates a comparison function from a given list. This function need not handle numeric keys, only non-numeric strings +* @param {Array} keyList The keys that should be respected in the given order +* @return {Function} The comparison function +*/ +const orderFromArray = keyList => function (a, b) { + // one of the items is contained in the list + let indexA = keyList.indexOf(a); + let indexB = keyList.indexOf(b); + + if (indexA != -1) { + if (indexB != -1) { + return indexA - indexB; + } + return -1; + } + if (indexB != -1) { + return 1; + } + // Both items not in the list. Fall-back to normal sorting + return a < b ? -1 : 1; // a == b cannot happen, since we're dealing with object keys +}; + +const sortify = function (value, replacer, space /*, customOrder */) { // replacer, toJSON(), cyclic references and other stuff is better handled by native stringifier. - // So we do JSON.stringify(sortKeys( JSON.parse(JSON.stringify()) )). + // So we do JSON.stringify(sortKeys( JSON.parse(JSON.stringify(…)) )). // This approach is slightly slower but much safer than a manual stringification. let nativeJson = jsonStringify(value, replacer, 0); if (!nativeJson || nativeJson[0] !== '{' && nativeJson[0] !== '[') { // if value is not an Object or Array return nativeJson; } let cleanObj = JSON.parse(nativeJson); - return jsonStringify(sortKeys(cleanObj), null, space); + let customOrder = arguments[3]; // we do this because JSON.sortify.length should equal 3 for maximal compatibility with JSON.stringify + if (Array.isArray(customOrder)) { + customOrder.forEach(entry => { + if (numericRE.test(entry)) { + throw new TypeError('Custom order must not contain numeric keys'); + } + }); + customOrder = orderFromArray(customOrder); + } else if (typeof customOrder != 'function') { + customOrder = undefined; + } + return jsonStringify(sortKeys(cleanObj, customOrder), null, space); }; module.exports = sortify; diff --git a/package.json b/package.json index 478b529..f7aacf6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "json.sortify", "description": "A deterministic version of JSON.stringify that sorts object keys alphabetically.", - "version": "2.1.0", + "version": "2.3.0", "engines": { "node": ">=0.10.0" }, @@ -74,8 +74,8 @@ "babel-plugin-transform-es2015-unicode-regex": "^6.5.0", "babel-plugin-transform-regenerator": "^6.6.5", "expect": "^1.15.2", - "istanbul": "^0.3.17", - "mocha": "^2.2.5", + "istanbul": "^0.4.5", + "mocha": "^3.2.0", "mocha-phantomjs": "^4.0.2" }, "scripts": { diff --git a/test/test.js b/test/test.js index 3f3890c..f90d8c5 100755 --- a/test/test.js +++ b/test/test.js @@ -189,6 +189,64 @@ describe('JSON.sortify', function () { }); }); + describe('custom sorting', function () { + var fixInput = { + 'name' : 'John', + 'age' : 55, + 'sex' : 'M', + 42: { + 'something': 'foo', + 'true': 'false', + 'all': 'good' + }, + 111: 'one-eleven' + }; + + it('should accept a function as the fourth parameter', function () { + var fixtures = [ + [function (a, b) { // keys containing 'a' come first + if (/a/.test(a)) { + if (/a/.test(b)) { + return a < b ? -1 : 1; + } + return -1; + } + if (/a/.test(b)) { + return 1; + } + return a < b ? -1 : 1; + }, '{"42":{"all":"good","something":"foo","true":"false"},"111":"one-eleven","age":55,"name":"John","sex":"M"}'], + [function (a, b) { // sort by length of key + if (a.length == b.length) { + return a < b ? -1 : 1; + } + return a.length - b.length; + }, '{"42":{"all":"good","true":"false","something":"foo"},"111":"one-eleven","age":55,"sex":"M","name":"John"}'] + + ]; + fixtures.forEach(function (fixture) { + expect(JSON.sortify(fixInput, null, null, fixture[0])).toEqual(fixture[1]); + }); + }); + + it('should accept a key list as the fourth parameter', function () { + var fixtures = [ + [['age', 'name', 'sex'], '{"42":{"all":"good","something":"foo","true":"false"},"111":"one-eleven","age":55,"name":"John","sex":"M"}'], + [['name', 'age', 'sex'], '{"42":{"all":"good","something":"foo","true":"false"},"111":"one-eleven","name":"John","age":55,"sex":"M"}'], + [['name', 'sex', 'age'], '{"42":{"all":"good","something":"foo","true":"false"},"111":"one-eleven","name":"John","sex":"M","age":55}'], + [['name'], '{"42":{"all":"good","something":"foo","true":"false"},"111":"one-eleven","name":"John","age":55,"sex":"M"}'], + [['name', 'true'], '{"42":{"true":"false","all":"good","something":"foo"},"111":"one-eleven","name":"John","age":55,"sex":"M"}'] + ]; + fixtures.forEach(function (fixture) { + expect(JSON.sortify(fixInput, null, null, fixture[0])).toEqual(fixture[1]); + }); + }); + + it('should prohibit numeric keys in custom order', function () { + expect(JSON.sortify.bind(JSON, fixInput, null, null, ['a', 1])).toThrow(TypeError); + }); + }); + describe('interoperability / interchangeability', function () { var fixtures = [ 1,