Skip to content

Commit

Permalink
GH-6 Add the possibility to use custom sorter
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasR committed Jan 13, 2017
1 parent fda2d90 commit 7b2ad59
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 13 deletions.
4 changes: 2 additions & 2 deletions dist/JSON.sortify.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 46 additions & 8 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
'use strict';

// detector for (decimal) numeric strings
const numericRE = /^(0|[1-9][0-9]*)$/;

/**
* Create a “sorted” version of an object.
*
Expand All @@ -25,16 +28,17 @@
* alphabetical order (numerical keys first, since v8 does so, and there's
* nothing we can do about it).
* @param {*} o The object to be sorted
* @param {Function?} customOrder (optional) A sorting function that should be used instead of the default order
*/
const sortKeys = o => {
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);
Expand All @@ -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;
}, {});
}
Expand All @@ -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;
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down Expand Up @@ -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": {
Expand Down
58 changes: 58 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 7b2ad59

Please sign in to comment.