How to obtain a master structure for a json file? - javascript

I have a JSON file as follows:
[
{
"dog": "lmn",
"tiger": [
{
"bengoltiger": {
"height": {
"x": 4
}
},
"indiantiger": {
"paw": "a",
"foor": "b"
}
},
{
"bengoltiger": {
"width": {
"a": 8
}
},
"indiantiger": {
"b": 3
}
}
]
},
{
"dog": "pqr",
"tiger": [
{
"bengoltiger": {
"width": {
"m": 3
}
},
"indiantiger": {
"paw": "a",
"foor": "b"
}
},
{
"bengoltiger": {
"height": {
"n": 8
}
},
"indiantiger": {
"b": 3
}
}
],
"lion": 90
}
]
I want to transform this to obtain all possible properties of any object at any nesting level. For arrays, the first object should contain all the properties. The values are trivial, but the below solution considers the first encountered value for any property. (For ex. "lmn" is preserved for the "dog" property)
Expected output:
[
{
"dog": "lmn",
"tiger": [
{
"bengoltiger": {
"height": {
"x": 4,
"n": 8
},
"width": {
"a": 8,
"m": 3
}
},
"indiantiger": {
"paw": "a",
"foor": "b",
"b": 3
}
}
],
"lion": 90
}
]
Here's a recursive function I tried before this nesting problem struck me
function consolidateArray(json) {
if (Array.isArray(json)) {
const reference = json[0];
json.forEach(function(element) {
for (var key in element) {
if (!reference.hasOwnProperty(key)) {
reference[key] = element[key];
}
}
});
json.splice(1);
this.consolidateArray(json[0]);
} else if (typeof json === 'object') {
for (var key in json) {
if (json.hasOwnProperty(key)) {
this.consolidateArray(json[key]);
}
}
}
};
var json = [
{
"dog": "lmn",
"tiger": [
{
"bengoltiger": {
"height": {
"x": 4
}
},
"indiantiger": {
"paw": "a",
"foor": "b"
}
},
{
"bengoltiger": {
"width": {
"a": 8
}
},
"indiantiger": {
"b": 3
}
}
]
},
{
"dog": "pqr",
"tiger": [
{
"bengoltiger": {
"width": {
"m": 3
}
},
"indiantiger": {
"paw": "a",
"foor": "b"
}
},
{
"bengoltiger": {
"height": {
"n": 8
}
},
"indiantiger": {
"b": 3
}
}
],
"lion": 90
}
];
consolidateArray(json);
alert(JSON.stringify(json, null, 2));

General logic using this new JNode IIFE with comments - ask someone more clever if you do not understand something as me ;-)
And level starts from 1 as there is no root object #start.
var json;
function DamnDemo() {
json = DemoJSON();
var it = new JNode(json), it2 = it;
var levelKeys = []; /* A bit crazy structure:
[
levelN:{
keyA:[JNode, JNode,...],
keyB:[JNode, JNode,...],
...
},
levelM:...
]
*/
do {
var el = levelKeys[it.level]; // array of level say LevelN or undefined
el = levelKeys[it.level] = el || {}; // set 2 empty it if does not exist
el = el[it.key] = el[it.key] || []; // key array in say levelN
el.push(it); // save current node indexing by level, key -> array
} while (it = it.DepthFirst()) // traverse all nodes
for(var l1 in levelKeys) { // let start simply by iterating levels
l2(levelKeys[l1]);
}
console.log(JSON.stringify(json, null, 2));
}
function l2(arr) { // fun starts here...
var len = 0, items = []; // size of arr, his items to simple Array
for(var ln in arr) { // It's a kind of magic here ;-) Hate recursion, but who want to rewrite it ;-)
if (arr[ln] instanceof JNode) return 1; // End of chain - our JNode for traverse of length 1
len += l2(arr[ln]);
items.push(arr[ln]);
}
if (len == 2) { // we care only about 2 items to move (getting even 3-5)
//console.log(JSON.stringify(json));
if (!isNaN(items[0][0].key) || (items[0][0].key == items[1][0].key)) { // key is number -> ignore || string -> must be same
console.log("Keys 2B moved:", items[0][0].key, items[1][0].key, "/ level:", items[0][0].level);
var src = items[1][0]; // 2nd similar JNode
moveMissing(items[0][0].obj, src.obj); // move to 1st
//console.log(JSON.stringify(json));
if (src.level == 1) { // top level cleaning
delete src.obj;
delete json[src.key]; // remove array element
if (!json[json.length-1]) json.length--; // fix length - hope it was last one (there are other options, but do not want to overcomplicate logic)
} else {
var parent = src.parent;
var end = 0;
for(var i in parent.obj) {
end++;
if (parent.obj[i] == src.obj) { // we found removed in parent's array
delete src.obj; // delete this empty object
delete parent.obj[i]; // and link on
end = 1; // stupid marker
}
}
if (end == 1 && parent.obj instanceof Array) parent.obj.length--; // fix length - now only when we are on last element
}
} else console.log("Keys left:", items[0][0].key, items[1][0].key, "/ level:", items[0][0].level); // keys did not match - do not screw it up, but report it
}
return len;
}
function moveMissing(dest, src) {
for(var i in src) {
if (src[i] instanceof Object) {
if (!dest[i]) { // uff object, but not in dest
dest[i] = src[i];
} else { // copy object over object - let it bubble down...
moveMissing(dest[i], src[i]);
}
delete src[i];
} else { // we have value here, check if it does not exist, move and delete source
if (!dest[i]) {
dest[i] = src[i];
delete src[i];
}
}
}
}
// JSON_Node_Iterator_IIFE.js
'use strict';
var JNode = (function (jsNode) {
function JNode(json, parent, pred, key, obj, fill) {
var node, pred = null;
if (parent === undefined) {
parent = null;
} else if (fill) {
this.parent = parent;
this.pred = pred;
this.node = null;
this.next = null;
this.key = key;
this.obj = obj;
return this;
}
var current;
var parse = (json instanceof Array);
for (var child in json) {
if (parse) child = parseInt(child);
var sub = json[child];
node = new JNode(null, parent, pred, child, sub, true);
if (pred) {
pred.next = node;
node.pred = pred;
}
if (!current) current = node;
pred = node;
}
return current;
}
JNode.prototype = {
get hasNode() {
if (this.node) return this.node;
return (this.obj instanceof Object);
},
get hasOwnKey() { return this.key && (typeof this.key != "number"); },
get level() {
var level = 1, i = this;
while(i = i.parent) level++;
return level;
},
Down: function() {
if (!this.node && this.obj instanceof Object) {
this.node = new JNode(this.obj, this);
}
return this.node;
},
Stringify: function() { // Raw test stringify - #s are taken same as strings
var res;
if (typeof this.key == "number") {
res = '[';
var i = this;
do {
if (i.node) res += i.node.Stringify();
else res += "undefined";
i = i.next;
if (i) res += ','
} while(i);
res += ']';
} else {
res = '{' + '"' + this.key + '":';
res += (this.node?this.node.Stringify():this.hasNode?"undefined":'"'+this.obj+'"');
var i = this;
while (i = i.next) {
res += ',' + '"' + i.key + '":';
if (i.node) res += i.node.Stringify();
else {
if (i.obj instanceof Object) res += "undefined";
else res += '"' + i.obj + '"';
}
};
res += '}';
}
return res;
},
DepthFirst: function () {
if (this == null) return 0; // exit sign
if (this.node != null || this.obj instanceof Object) {
return this.Down(); // moved down
} else if (this.next != null) {
return this.next;// moved right
} else {
var i = this;
while (i != null) {
if (i.next != null) {
return i.next; // returned up & moved next
}
i = i.parent;
}
}
return 0; // exit sign
}
}
return JNode;
})();
// Fire test
DamnDemo();
function DemoJSON() {
return [
{
"dog": "lmn",
"tiger": [
{
"bengoltiger": {
"height": {
"x": 4
}
},
"indiantiger": {
"paw": "a",
"foor": "b"
}
},
{
"bengoltiger": {
"width": {
"a": 8
}
},
"indiantiger": {
"b": 3
}
}
]
},
{
"dog": "pqr",
"tiger": [
{
"bengoltiger": {
"width": {
"m": 3
}
},
"indiantiger": {
"paw": "a",
"foor": "b"
}
},
{
"bengoltiger": {
"height": {
"n": 8
}
},
"indiantiger": {
"b": 3
}
}
],
"lion": 90
}
]
;}

This was an interesting problem. Here's what I came up with:
// Utility functions
const isInt = Number.isInteger
const path = (ps = [], obj = {}) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
const assoc = (prop, val, obj) =>
isInt (prop) && Array .isArray (obj)
? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
: {...obj, [prop]: val}
const assocPath = ([p = undefined, ...ps], val, obj) =>
p == undefined
? obj
: ps.length == 0
? assoc(p, val, obj)
: assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)
// Helper functions
function * getPaths(o, p = []) {
if (Object(o) !== o || Object .keys (o) .length == 0) yield p
if (Object(o) === o)
for (let k of Object .keys (o))
yield * getPaths (o[k], [...p, isInt (Number (k)) ? Number (k) : k])
}
const canonicalPath = (path) =>
path.map (n => isInt (Number (n)) ? 0 : n)
const splitPaths = (xs) =>
Object .values ( xs.reduce (
(a, p, _, __, cp = canonicalPath (p), key = cp .join ('\u0000')) =>
({...a, [key]: a [key] || {canonical: cp, path: p} })
, {}
))
// Main function
const canonicalRep = (data) => splitPaths ([...getPaths (data)])
.reduce (
(a, {path:p, canonical}) => assocPath(canonical, path(p, data), a),
Array.isArray(data) ? [] : {}
)
// Test
const data = [{"dog": "lmn", "tiger": [{"bengoltiger": {"height": {"x": 4}}, "indiantiger": {"foor": "b", "paw": "a"}}, {"bengoltiger": {"width": {"a": 8}}, "indiantiger": {"b": 3}}]}, {"dog": "pqr", "lion": 90, "tiger": [{"bengoltiger": {"width": {"m": 3}}, "indiantiger": {"foor": "b", "paw": "a"}}, {"bengoltiger": {"height": {"n": 8}}, "indiantiger": {"b": 3}}]}]
console .log (
canonicalRep (data)
)
The first few functions are plain utility functions that I would keep in a system library. They have plenty of uses outside this code:
isInt is simply a first-class function alias to Number.isInteger
path finds the nested property of an object along a given pathway
path(['b', 1, 'c'], {a: 10, b: [{c: 20, d: 30}, {c: 40}], e: 50}) //=> 40
assoc returns a new object cloning your original, but with the value of a certain property set to or replaced with the supplied one.
assoc('c', 42, {a: 1, b: 2, c: 3, d: 4}) //=> {a: 1, b: 2, c: 42, d: 4}
Note that internal objects are shared by reference where possible.
assocPath does this same thing, but with a deeper path, building nodes as needed.
assocPath(['a', 'b', 1, 'c', 'd'], 42, {a: {b: [{x: 1}, {x: 2}], e: 3})
//=> {a: {b: [{x: 1}, {c: {d: 42}, x: 2}], e: 3}}
Except for isInt, these borrow their APIs from Ramda. (Disclaimer: I'm a Ramda author.) But these are unique implementations.
The next function, getPaths, is an adaptation of one from another SO answer. It lists all the paths in your object in the format used by path and assocPath, returning an array of values which are integers if the relevant nested object is an array and strings otherwise. Unlike the function from which is was borrowed, it only returns paths to leaf values.
For your original object, it returns an iterator for this data:
[
[0, "dog"],
[0, "tiger", 0, "bengoltiger", "height", "x"],
[0, "tiger", 0, "indiantiger", "foor"],
[0, "tiger", 0, "indiantiger", "paw"],
[0, "tiger", 1, "bengoltiger", "width", "a"],
[0, "tiger", 1, "indiantiger", "b"],
[1, "dog"],
[1, "lion"],
[1, "tiger", 0, "bengoltiger", "width", "m"],
[1, "tiger", 0, "indiantiger", "foor"],
[1, "tiger", 0, "indiantiger", "paw"],
[1, "tiger", 1, "bengoltiger", "height", "n"],
[1, "tiger", 1, "indiantiger", "b"]
]
If I wanted to spend more time on this, I would replace that version of getPaths with a non-generator version, just to keep this code consistent. It shouldn't be hard, but I'm not interested in spending more time on it.
We can't use those results directly to build your output, since they refer to array elements beyond the first one. That's where splitPaths and its helper canonicalPath come in. We create the canonical paths by replacing all integers with 0, giving us a data structure like this:
[{
canonical: [0, "dog"],
path: [0, "dog"]
}, {
canonical: [0, "tiger", 0, "bengoltiger", "height", "x"],
path: [0, "tiger", 0, "bengoltiger", "height", "x"]
}, {
canonical: [0, "tiger", 0, "indiantiger", "foor"],
path: [0, "tiger", 0, "indiantiger", "foor"]
}, {
canonical: [0, "tiger", 0, "indiantiger", "paw"],
path: [0, "tiger", 0, "indiantiger", "paw"]
}, {
canonical: [0, "tiger", 0, "bengoltiger", "width", "a"],
path: [0, "tiger", 1, "bengoltiger", "width", "a"]
}, {
canonical: [0, "tiger", 0, "indiantiger", "b"],
path: [0, "tiger", 1, "indiantiger", "b"]
}, {
canonical: [0, "lion"],
path: [1, "lion"]
}, {
canonical: [0, "tiger", 0, "bengoltiger", "width", "m"],
path: [1, "tiger", 0, "bengoltiger", "width", "m"]
}, {
canonical: [0, "tiger", 0, "bengoltiger", "height", "n"],
path: [1, "tiger", 1, "bengoltiger", "height", "n"]
}]
Note that this function also removes duplicate canonical paths. We originally had both [0, "tiger", 0, "indiantiger", "foor"] and [1, "tiger", 0, "indiantiger", "foor"], but the output only contains the first one.
It does this by storing them in an object under a key created by joining the path together with the non-printable character \u0000. This was the easiest way to accomplish this task, but there is an extremely unlikely failure mode possible 1 so if we really wanted we could do a more sophisticated duplicate checking. I wouldn't bother.
Finally, the main function, canonicalRep builds a representation out of your object by calling splitPaths and folding over the result, using canonical to say where to put the new data, and applying the path function to your path property and the original object.
Our final output, as requested, looks like this:
[
{
dog: "lmn",
lion: 90,
tiger: [
{
bengoltiger: {
height: {
n: 8,
x: 4
},
width: {
a: 8,
m: 3
}
},
indiantiger: {
b: 3,
foor: "b",
paw: "a"
}
}
]
}
]
What's fascinating for me is that I saw this as an interesting programming challenge, although I couldn't really imagine any practical uses for it. But now that I've coded it, I realize it will solve a problem in my current project that I'd put aside a few weeks ago. I will probably implement this on Monday!
Update
Some comments discuss a problem with a subsequent empty value tries to override a prior filled value, causing a loss in data.
This version attempts to alleviate this with the following main function:
const canonicalRep = (data) => splitPaths ([...getPaths (data)])
.reduce (
(a, {path: p, canonical}, _, __, val = path(p, data)) =>
isEmpty(val) && !isEmpty(path(canonical, a))
? a
: assocPath(canonical, val, a),
Array.isArray(data) ? [] : {}
)
using a simple isEmpty helper function:
const isEmpty = (x) =>
x == null || (typeof x == 'object' && Object.keys(x).length == 0)
You might want to update or expand this helper in various ways.
My first pass worked fine with the alternate data supplied, but not when I switched the two entries in the outer array. I fixed that, and also made sure that an empty value is kept if it's not overridden with actual data (that's the z property in my test object.)
I believe this snippet solves the original problem and the new one:
// Utility functions
const isInt = Number.isInteger
const path = (ps = [], obj = {}) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
const assoc = (prop, val, obj) =>
isInt (prop) && Array .isArray (obj)
? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
: {...obj, [prop]: val}
const assocPath = ([p = undefined, ...ps], val, obj) =>
p == undefined
? obj
: ps.length == 0
? assoc(p, val, obj)
: assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)
const isEmpty = (x) =>
x == null || (typeof x == 'object' && Object.keys(x).length == 0)
function * getPaths(o, p = []) {
if (Object(o) !== o || Object .keys (o) .length == 0) yield p
if (Object(o) === o)
for (let k of Object .keys (o))
yield * getPaths (o[k], [...p, isInt (Number (k)) ? Number (k) : k])
}
// Helper functions
const canonicalPath = (path) =>
path.map (n => isInt (Number (n)) ? 0 : n)
const splitPaths = (xs) =>
Object .values ( xs.reduce (
(a, p, _, __, cp = canonicalPath (p), key = cp .join ('\u0000')) =>
({...a, [key]: a [key] || {canonical: cp, path: p} })
, {}
))
// Main function
const canonicalRep = (data) => splitPaths ([...getPaths (data)])
.reduce (
(a, {path: p, canonical}, _, __, val = path(p, data)) =>
isEmpty(val) && !isEmpty(path(canonical, a))
? a
: assocPath(canonical, val, a),
Array.isArray(data) ? [] : {}
)
// Test data
const data1 = [{"dog": "lmn", "tiger": [{"bengoltiger": {"height": {"x": 4}}, "indiantiger": {"foor": "b", "paw": "a"}}, {"bengoltiger": {"width": {"a": 8}}, "indiantiger": {"b": 3}}]}, {"dog": "pqr", "lion": 90, "tiger": [{"bengoltiger": {"width": {"m": 3}}, "indiantiger": {"foor": "b", "paw": "a"}}, {"bengoltiger": {"height": {"n": 8}}, "indiantiger": {"b": 3}}]}]
const data2 = [{"d": "Foreign Trade: Export/Import: Header Data", "a": "false", "f": [{"g": "TRANSPORT_MODE", "i": "2"}, {"k": "System.String", "h": "6"}], "l": "true"}, {"a": "false", "f": [], "l": "false", "z": []}]
const data3 = [data2[1], data2[0]]
// Demo
console .log (canonicalRep (data1))
console .log (canonicalRep (data2))
console .log (canonicalRep (data3))
.as-console-wrapper {max-height: 100% !important; top: 0}
Why not change assoc?
This update grew out of discussion after I rejected an edit attempt to do the same sort of empty-checking inside assoc. I rejected that as too far removed from the original attempt. When I learned what it was supposed to do, I knew that what had to be changed was canonicalRep or one of its immediate helper functions.
The rationale is simple. assoc is a general-purpose utility function designed to do a shallow clone of an object, changing the named property to the new value. This should not have complex logic regarding whether the value is empty. It should remain simple.
By introducing the isEmpty helper function, we can do all this with only a minor tweak to canonicalRep.
1That failure mode could happen if you had certain nodes containing that separator, \u0000. For instance, if you had paths [...nodes, "abc\u0000", "def", ...nodes] and [...nodes, "abc", "\u0000def", ...nodes], they would both map to "...abc\u0000\u0000def...". If this is a real concern, we could certainly use other forms of deduplication.

Related

Iterative depth-first traversal with remembering value's paths

I need help with specific implementation of iterative depth first traversal algorithm.
I have an object like this (it's just an example, object might have more properties and be deeper nested):
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
What I need is a function that would traverse the whole object and return an array like this:
[
{"a": 1},
{"b.c.d.e": 2},
{"b.c.d.f": 3},
{"b.g.0.h": 4},
{"b.g.0.i": 5},
{"b.g.1.j": 6},
{"b.g.1.k": 7},
]
I managed to create an algorithm that sort of solves my problem, but needs one additional step in the end. Result of the algorithm is an array of strings like that:
[
'a^1',
'b.c.d.e^2',
'b.c.d.f^3',
'b.g.0.h^4',
'b.g.0.i^5',
'b.g.1.j^6',
'b.g.1.k^7'
]
so in order to achieve what I want I have to do one full iteration over the result of my algorithm, split strings by ^ symbol and then create objects based on that.
This is the part that I need help with - how can I improve/change my solution so I don't need to do that last step?
function dft(root) {
let stack = [];
let result = [];
const isObject = value => typeof value === "object";
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
if (isObject(node)) {
Object.entries(node).forEach(([childNodeKey, childNodeValue]) => {
if (isObject(childNodeValue)) {
const newObject = Object.fromEntries(
Object.entries(childNodeValue).map(([cnk, cnv]) => {
return [`${childNodeKey}.${cnk}`, cnv];
})
);
stack.push(newObject);
} else {
stack.push(`${childNodeKey}^${childNodeValue}`);
}
})
} else {
result.push(node);
}
}
return result.reverse();
}
I'd keep pairs <keys,value> in the stack and only create a string key when storing a newly created object:
function dft(obj) {
let stack = []
let res = []
stack.push([[], obj])
while (stack.length) {
let [keys, val] = stack.pop()
if (!val || typeof val !== 'object') {
res.push({
[keys.join('.')]: val
})
} else {
Object.entries(val).forEach(p => stack.push([
keys.concat(p[0]),
p[1],
]))
}
}
return res.reverse()
}
//
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
console.log(dft(root))
You can push the childNodeKey childNodeValue pair directly as an object to your result array.
Change
stack.push(`${childNodeKey}^${childNodeValue}`);
to
const newEntry = {}
newEntry[childNodeKey] = childNodeValue
result.push(newEntry);
or with ES2015 syntax (you would need a transpiler for browser compatibility)
result.push({[childNodeKey]: childNodeValue});
Complete function:
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
function dft(root) {
let stack = [];
let result = [];
const isObject = value => typeof value === "object";
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
if (isObject(node)) {
Object.entries(node).forEach(([childNodeKey, childNodeValue]) => {
if (isObject(childNodeValue)) {
const newObject = Object.fromEntries(
Object.entries(childNodeValue).map(([cnk, cnv]) => {
return [`${childNodeKey}.${cnk}`, cnv];
})
);
stack.unshift(newObject);
} else {
const newEntry = {}
newEntry[childNodeKey] = childNodeValue
result.push({[childNodeKey]: childNodeValue});
}
})
} else {
result.push(node);
}
}
return result;
}
console.log(dft(root))
As you mentioned, you almost got it complete. Just make the array entry an object just before pushing it into result. By splitting Array.prototype.split('^') you can get 'b.g.0.h^4' >>> ['b.g.0.h', '4']. So, rest is a cake:
if (isObject(node)) {
...
} else {
const keyAndValue = node.split('^')
// approach 1)
// const key = keyAndValue[0]
// const value = keyAndValue[1]
// dynamic key setting
// result.push({[key]: value});
// approach 2)
// or in short,
// dynamic key setting
result.push({[keyAndValue[0]]: keyAndValue[1]});
}
You could use a stack where each item has an iterator over the children, and the path up to that point:
function collect(root) {
const Node = (root, path) =>
({ iter: Object.entries(root)[Symbol.iterator](), path });
const result = [];
const stack = [Node(root, "")];
while (stack.length) {
const node = stack.pop();
const {value} = node.iter.next();
if (!value) continue;
stack.push(node);
const [key, child] = value;
const path = node.path ? node.path + "." + key : key;
if (Object(child) !== child) result.push({ [path]: child });
else stack.push(Node(child, path));
}
return result;
}
const root = {a:1,b:{c:{d:{e:2,f:3}},g:[{h:4,i:5},{j:6,k:7}]}};
console.log(collect(root));
I would suggest that the quickest fix to your code is simply to replace
return result.reverse();
with
return result.reverse()
.map ((s, _, __, [k, v] = s .split ('^')) => ({[k]: v}));
But I also think that we can write code to do this more simply. A function I use often will convert your input into something like this:
[
[["a"], 1],
[["b", "c", "d", "e"], 2],
[["b", "c", "d", "f"], 3],
[["b", "g", 0, "h"], 4],
[["b", "g", 0, "i"], 5],
[["b", "g", 1, "j"], 6],
[["b", "g", 1, "k"], 7]
]
and a fairly trivial wrapper can then convert this to your output. It could look like this:
const pathEntries = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (([p, v]) => [[Array.isArray(obj) ? Number(k) : k, ... p], v])
)
: [[[], obj]]
const transform = (o) =>
pathEntries (o)
.map (([k, v]) => ({[k .join ('.')] : v}))
const root = {a: 1, b: {c: {d: {e: 2, f: 3, }}, g: [{h: 4, i: 5, }, {j: 6, k: 7}]}}
console .log (transform (root))
.as-console-wrapper {max-height: 100% !important; top: 0}
I don't know your usecase, but I would find this output generally more helpful:
{
"a": 1,
"b.c.d.e": 2,
"b.c.d.f": 3,
"b.g.0.h": 4,
"b.g.0.i": 5,
"b.g.1.j": 6,
"b.g.1.k": 7
}
(that is, one object with a number of properties, rather than an array of single-property objects.)
And we could do this nearly as easily, with a small change to transform:
const transform = (o) =>
pathEntries (o)
.reduce ((a, [k, v]) => ((a[k .join ('.')] = v), a), {})

Flattening object and limitting number of nesting

I would like to limit number of flattened structures inside an object. Consider example below.
Input:
const exampleObj = {
foo: {
bar: {
biz: "hello"
}
}
};
Output:
{foo_bar_biz: "hello"}
I wanted to set a limiter (limit = 2) so that function will stop performing recursive function and return something like this
{foo_bar: "[Object] object"}
Here's a snippet:
const { useState, useEffect} = React;
const exampleObj = {
foo: {
bar: {
biz: "hello"
}
}
};
let limit = 0;
function App() {
const [state, setState] = useState({});
useEffect(() => {
flatten(exampleObj);
}, []);
const flatten = (data, parent, result = {}) => {
for (let key in data) {
const propName = parent ? parent + "_" + key : key;
if (typeof data[key] === "object") {
limit++;
if (limit <= 1) {
flatten(data[key], propName, result);
} else {
setState(prevState => {
return {
...prevState,
[propName]:
typeof data[key] === "object" ? "[Object] object" : data[key]
};
});
}
} else {
result[propName] = data[key];
setState({
...state,
[propName]: data[key]
});
}
}
};
console.log(state);
return (
<div>
<p>Start editing to see some magic happen :)</p>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I've prepared a solution for that, but it is quite cumbersome. https://stackblitz.com/edit/react-u5klvc
Here's one possible implementation of flatten. Notice how it's completely decoupled from React or a specific React component. It works on any JavaScript objects -
const snakecase = s =>
s.join("_")
const flatten = (t = {}, n = Infinity, join = snakecase) =>
{ const many = (t, n, path) =>
n >= 0 && Object(t) === t
? Object.entries(t).flatMap(_ => one(_, n - 1, path))
: [ [ join(path), t ] ]
const one = ([ k, v ], n, path) =>
many(v, n, [...path, k])
return Object.fromEntries(many(t, n, []))
}
Given some example data -
const data =
{ a1: 11
, a2: { b1: 21, b2: 22 }
, a3: { b1: { c1: 311, c2: 312 }
, b2: { c1: 321, c2: 322, c3: { d1: 3231 } }
}
}
A depth of 1 flattens one level -
flatten(data, 1)
{
"a1": 11,
"a2_b1": 21,
"a2_b2": 22,
"a3_b1": {
"c1": 311,
"c2": 312
},
"a3_b2": {
"c1": 321,
"c2": 322,
"c3": {
"d1": 3231
}
}
}
A depth of 2 flattens two levels -
flatten(data, 2) // => ...
{
"a1": 11,
"a2_b1": 21,
"a2_b2": 22,
"a3_b1_c1": 311,
"a3_b1_c2": 312,
"a3_b2_c1": 321,
"a3_b2_c2": 322,
"a3_b2_c3": {
"d1": 3231
}
}
The default depth is inifinity -
flatten(data) // => ...
{
"a1": 11,
"a2_b1": 21,
"a2_b2": 22,
"a3_b1_c1": 311,
"a3_b1_c2": 312,
"a3_b2_c1": 321,
"a3_b2_c2": 322,
"a3_b2_c3_d1": 3231
}
The default join is snakecase, but the parameter can be specified at the call site -
const camelCase = ([ first = "", ...rest ]) =>
first + rest.map(upperFirst).join("")
const upperFirst = ([ first = "", ...rest ]) =>
first.toUpperCase() + rest.join("")
flatten(data, 2, camelCase) // => ...
{
"a1": 11,
"a2B1": 21,
"a2B2": 22,
"a3B1C1": 311,
"a3B1C2": 312,
"a3B2C1": 321,
"a3B2C2": 322,
"a3B2C3": {
"d1": 3231
}
}
Expand the snippet below to verify the results in your own browser -
const data =
{ a1: 11
, a2: { b1: 21, b2: 22 }
, a3: { b1: { c1: 311, c2: 312 }
, b2: { c1: 321, c2: 322, c3: { d1: 3231 } }
}
}
const snakecase = s =>
s.join("_")
const flatten = (t = {}, n = Infinity, join = snakecase) =>
{ const many = (t, n, path) =>
n >= 0 && Object(t) === t
? Object.entries(t).flatMap(_ => one(_, n - 1, path))
: [ [ join(path), t ] ]
const one = ([ k, v ], n, path) =>
many(v, n, [...path, k])
return Object.fromEntries(many(t, n, []))
}
const result =
flatten(data, 2)
console.log(JSON.stringify(result, null, 2))
Thankyou's answer is great. But this is enough different to be worth posting, I think.
I keep handy functions like path and getPaths in my personal library. For this, I had to alter getPaths to accept a depth parameter (d) to escape earlier.
With these and Object.fromEntries we can easily write a partialFlatten function that does what I think you want:
const path = (ps = []) => (obj = {}) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
const getPaths = (obj, d = Infinity) =>
Object (obj) === obj && d > 0
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v, d - 1) .map (p => [Array.isArray(obj) ? Number(k) : k, ...p])
)
: [[]]
const partialFlatten = (obj, depth) =>
Object .fromEntries (
getPaths (obj, depth) .map (p => [p .join ('_'), path (p) (obj)])
)
const exampleObj = {foo: {bar: {biz: "hello"}, baz: 'goodbye'}}
console .log ('depth 1:', partialFlatten (exampleObj, 1))
console .log ('depth 2:', partialFlatten (exampleObj, 2))
console .log ('depth 3:', partialFlatten (exampleObj, 3))
.as-console-wrapper {max-height: 100% !important; top: 0}

How to find Object in Array which is nested as unknown times [duplicate]

I'm trying to figure out how to search for a node in this JSON object recursively. I have tried something but cannot get it:
var tree = {
"id": 1,
"label": "A",
"child": [
{
"id": 2,
"label": "B",
"child": [
{
"id": 5,
"label": "E",
"child": []
},
{
"id": 6,
"label": "F",
"child": []
},
{
"id": 7,
"label": "G",
"child": []
}
]
},
{
"id": 3,
"label": "C",
"child": []
},
{
"id": 4,
"label": "D",
"child": [
{
"id": 8,
"label": "H",
"child": []
},
{
"id": 9,
"label": "I",
"child": []
}
]
}
]
};
Here is my non-working solution, which is probably because the first node is just a value while children are in arrays:
function scan(id, tree) {
if(tree.id == id) {
return tree.label;
}
if(tree.child == 0) {
return
}
return scan(tree.child);
};
Your code is just missing a loop to inspect each child of a node in the child array. This recursive function will return the label property of a node or undefined if label not present in tree:
const search = (tree, target) => {
if (tree.id === target) {
return tree.label;
}
for (const child of tree.child) {
const found = search(child, target);
if (found) {
return found;
}
}
};
const tree = {"id":1,"label":"A","child":[{"id":2,"label":"B","child":[{"id":5,"label":"E","child":[]},{"id":6,"label":"F","child":[]},{"id":7,"label":"G","child":[]}]},{"id":3,"label":"C","child":[]},{"id":4,"label":"D","child":[{"id":8,"label":"H","child":[]},{"id":9,"label":"I","child":[]}]}]};
console.log(search(tree, 1));
console.log(search(tree, 6));
console.log(search(tree, 99));
You can also do it iteratively with an explicit stack which won't cause a stack overflow (but note that the shorthand stack.push(...curr.child); can overflow the argument size for some JS engines due to the spread syntax, so use an explicit loop or concat for massive child arrays):
const search = (tree, target) => {
for (const stack = [tree]; stack.length;) {
const curr = stack.pop();
if (curr.id === target) {
return curr.label;
}
stack.push(...curr.child);
}
};
const tree = {"id":1,"label":"A","child":[{"id":2,"label":"B","child":[{"id":5,"label":"E","child":[]},{"id":6,"label":"F","child":[]},{"id":7,"label":"G","child":[]}]},{"id":3,"label":"C","child":[]},{"id":4,"label":"D","child":[{"id":8,"label":"H","child":[]},{"id":9,"label":"I","child":[]}]}]};
for (let i = 0; ++i < 12; console.log(search(tree, i)));
A somewhat more generic design would return the node itself and let the caller access the .label property if they want to, or use the object in some other manner.
Note that JSON is purely a string format for serialized (stringified, raw) data. Once you've deserialized JSON into a JavaScript object structure, as is here, it's no longer JSON.
scan can be written recursively using a third parameter that models a queue of nodes to scan
const scan = (id, tree = {}, queue = [ tree ]) =>
// if id matches node id, return node label
id === tree.id
? tree.label
// base case: queue is empty
// id was not found, return false
: queue.length === 0
? false
// inductive case: at least one node
// recur on next tree node, append node children to queue
: scan (id, queue[0], queue.slice(1).concat(queue[0].child))
Becauase JavaScript supports default arguments, the call site for scan is unaltered
console.log
( scan (1, tree) // "A"
, scan (3, tree) // "C"
, scan (9, tree) // "I"
, scan (99, tree) // false
)
Verify it works in your browser below
const scan = (id, tree = {}, queue = [ tree ]) =>
id === tree.id
? tree.label
: queue.length === 0
? false
: scan (id, queue[0], queue.slice(1).concat(queue[0].child))
const tree =
{ id: 1
, label: "A"
, child:
[ { id: 2
, label: "B"
, child:
[ { id: 5
, label: "E"
, child: []
}
, { id: 6
, label: "F"
, child: []
}
, { id: 7
, label: "G"
, child: []
}
]
}
, { id: 3
, label: "C"
, child: []
}
, { id: 4
, label: "D"
, child:
[ { id: 8
, label: "H"
, child: []
}
, { id: 9
, label: "I"
, child: []
}
]
}
]
}
console.log
( scan (1, tree) // "A"
, scan (3, tree) // "C"
, scan (9, tree) // "I"
, scan (99, tree) // false
)
Related recursive search using higher-order functions
Here is a solution using object-scan
// const objectScan = require('object-scan');
const tree = {"id":1,"label":"A","child":[{"id":2,"label":"B","child":[{"id":5,"label":"E","child":[]},{"id":6,"label":"F","child":[]},{"id":7,"label":"G","child":[]}]},{"id":3,"label":"C","child":[]},{"id":4,"label":"D","child":[{"id":8,"label":"H","child":[]},{"id":9,"label":"I","child":[]}]}]};
const search = (obj, id) => objectScan(['**.id'], {
abort: true,
filterFn: ({ value, parent, context }) => {
if (value === id) {
context.push(parent.label);
return true;
}
return false;
}
})(obj, [])[0];
console.log(search(tree, 1));
// => A
console.log(search(tree, 6));
// => F
console.log(search(tree, 99));
// => undefined
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan

Dynamically calculate the average for a nested collection using lodash

I have a JSON array of objects (a collection) like:
[{
"x": {
"x1": 1
},
"y": {
"yt": 0,
"zt": 4,
"qa": 3,
"ft": 0,
...
}
},
{
"x": {
"x1": 5
},
"y": {
"yt": 10,
"zt": 2,
"qa": 0,
"ft": 0,
...
}
}]
I'd like to calculate average for each field. The result structure should be same. Like:
{
"x": {
"x1": 3
},
"y": {
"yt": 5,
"zt": 3,
"qa": 1.5,
"ft": 0,
...
}
}
Thanks
You can merge the objects using the spread syntax and lodash's _.mergeWith().
When merging, if the 2nd parameter (b) is a number divide it by the number of items in the original array to get it's respective contribution to the total average. If the 1st parameter (a) is a number, just add it without dividing (to avoid dividing the sum multiple times), or add 0 if it's undefined.
I've added examples of 2 objects array, and 3 objects array.
const getAvg = (data) => _.mergeWith({}, ...data, (a, b) => {
if(_.isNumber(b)) {
return ((b || 0) / data.length) + (_.isNumber(a) ? (a || 0) : 0);
}
});
const data1 = [
{"x":{"x1":1},"y":{"yt":0,"zt":4,"qa":3,"ft":0}},
{"x":{"x1":5},"y":{"yt":10,"zt":2,"qa":0,"ft":0}}
];
const data2 = [
{"x":{"x1":1},"y":{"yt":0,"zt":4,"qa":3,"ft":0}},
{"x":{"x1":5},"y":{"yt":10,"zt":2,"qa":0,"ft":0}},
{"x":{"x1":3},"y":{"yt":2,"zt":6,"qa":3,"ft":0}}
];
const result1 = getAvg(data1);
console.log('2 objects in the array: ', result1);
const result2 = getAvg(data2);
console.log('3 objects in the array: ', result2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
You could first collect and sum all values in the same data structure and then calculkate the average by a division with the length of the given array.
function getParts(array, result) {
function iter(o, r) {
Object.keys(o).forEach(function (k) {
if (o[k] && typeof o[k] === 'object') {
return iter(o[k], r[k] = r[k] || {});
}
r[k] = (r[k] || 0) + o[k];
});
}
function avr(o) {
Object.keys(o).forEach(function (k) {
if (o[k] && typeof o[k] === 'object') {
return avr(o[k]);
}
o[k] = o[k] /data.length;
});
}
data.forEach(function (a) {
iter(a, result);
});
avr(result);
}
var data = [{ x: { x1: 1 }, y: { yt: 0, zt: 4, qa: 3, ft: 0, } }, { x: { x1: 5 }, y: { yt: 10, zt: 2, qa: 0, ft: 0, } }],
result = {};
getParts(data, result);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
let objectArray = [{
"x": {
"x1": 1
},
"y": {
"yt": 0,
"zt": 4,
"qa": 3,
"ft": 0,
}
},
{
"x": {
"x1": 5
},
"y": {
"yt": 10,
"zt": 2,
"qa": 0,
"ft": 0,
}
}];
function findAverage(array) {
let counter = {},
result = {},
i,
obj,
key,
subKey;
// Iterate through array
for (i = 0; i < array.length; i++) {
obj = array[i];
// Copy each key in array element to counter object
for (key in obj) {
counter[key] = counter[key] || {};
// Increment and keep count of key-values of counter based on values in array element
for (subKey in obj[key]) {
counter[key][subKey] = counter[key][subKey] || {total: 0, numElements: 0};
counter[key][subKey].total += obj[key][subKey];
counter[key][subKey].numElements += 1;
}
}
}
// Go back through counter to find average of all existing subkeys (based on incremented total and the number of elements recorded) and throw it into result object
for (key in counter) {
result[key] = result[key] || {};
for (subKey in counter[key]) {
result[key][subKey] = counter[key][subKey].total / counter[key][subKey].numElements;
}
}
return result;
}
console.log(findAverage(objectArray));
Not designed to be absolutely optimal, and copying objects can be done recursively without knowing in advance their structure, but I wanted to keep the steps as clear as possible.
Edited to allow testing as snippet. Had no idea you could even do that on SO!
var array = [{
"x": {
"x1": 1
},
"y": {
"yt": 0,
"zt": 4,
"qa": 3,
"ft": 0
}
},
{
"x": {
"x1": 5
},
"y": {
"yt": 10,
"zt": 2,
"qa": 0,
"ft": 0
}
}];
function aintob(){
var o = {};
var first = array[0],
second = array[1];
var result = {x:{},y:{}};
var each = function(letter, oa, ob){
var i,
letter = {};
for(i in oa){
letter[i] = (oa[i]+ob[i])/2;
}
return letter;
}
o.x = each("x", first.x, second.x);
o.y = each("y", first.y, second.y);
return o;
}
console.log(aintob());

Sorting JavaScript Object by key value Recursively

Sort the objects by keys whose value is also an object and sort that internal object as well i.e sort the object recursively. Sorting should be as per key.
I looked into Stackoverflow's other questions but None of them is for Object Recursive Sorting.
Question I looked into:
Sorting JavaScript Object by property value
Example:
input = {
"Memo": {
"itemAmount1": "5",
"taxName1": "TAX",
"productPrice1": "10",
"accountName1": "Account Receivable (Debtors)"
},
"Footer": {
"productDescription2": "Maggie",
"itemQuantity2": "49.5",
"accountName2": "Account Receivable (Debtors)",
"taxName2": "TAX"
},
"Header": {
"itemDiscount3": "10",
"accountName3": "Account Receivable (Debtors)",
"productPrice3": "10",
"taxName3": "TAX"
}
}
Output
output = {
"Footer": {
"accountName2": "Account Receivable (Debtors)",
"itemQuantity2": "49.5",
"productDescription2": "Maggie",
"taxName2": "TAX"
},
"Header": {
"accountName3": "Account Receivable (Debtors)",
"itemDiscount3": "10",
"productPrice3": "10",
"taxName3": "TAX"
},
"Memo": {
"accountName1": "Account Receivable (Debtors)",
"itemAmount1": "5",
"productPrice1": "10",
"taxName1": "TAX"
}
}
It is not necessary that it is 2 level object hierarchy it may contain n level of object hierarchy which need to be sorted.
I think what #ksr89 means is that when we apply a for - in loop, we get keys in sorted order. I think this is a valid use case especially in the development of Node.js based ORMs
The following function should work and is I think what you are looking for.
input = {
"Memo": {
"itemAmount1": "5",
"taxName1": "TAX",
"productPrice1": "10",
"accountName1": "Account Receivable (Debtors)"
},
"Footer": {
"productDescription2": "Maggie",
"itemQuantity2": "49.5",
"accountName2": "Account Receivable (Debtors)",
"taxName2": "TAX"
},
"Header": {
"itemDiscount3": "10",
"accountName3": "Account Receivable (Debtors)",
"productPrice3": "10",
"taxName3": "TAX"
}
}
window.sortedObject = sort(input);
function sort(object){
if (typeof object != "object" || object instanceof Array) // Not to sort the array
return object;
var keys = Object.keys(object);
keys.sort();
var newObject = {};
for (var i = 0; i < keys.length; i++){
newObject[keys[i]] = sort(object[keys[i]])
}
return newObject;
}
for (var key in sortedObject){
console.log (key);
//Prints keys in order
}
I was on this page to write the following information. The code is based on Gaurav Ramanan's answer, but handles arrays and null differently.
Comparing JSON
To compare data from JSON files you may want to have them formatted the same way
from javascript: JSON.stringify(JSON.parse(jsonString), null, '\t')
the last parameter could also be a number of spaces
the last 2 parameters are optional (minified output if absent)
from Visual Studio Code: with the Prettify JSON extension
Verify indentation (i.e. TABs) and line endings (i.e. Unix).
Also, keys may be recursively sorted during formatting.
Sorting keys with javascript:
const {isArray} = Array
const {keys} = Object
function sortKeysRec(obj) {
if (isArray(obj)) {
const newArray = []
for (let i = 0, l = obj.length; i < l; i++)
newArray[i] = sortKeysRec(obj[i])
return newArray
}
if (typeof obj !== 'object' || obj === null)
return obj
const sortedKeys = keys(obj).sort()
const newObject = {}
for (let i = 0, l = sortedKeys.length; i < l; i++)
newObject[sortedKeys[i]] = sortKeysRec(obj[sortedKeys[i]])
return newObject
}
Ensure unix line endings with javascript: jsonString.replace(/\r\n/ug, '\n').
The solution above works only for the current implementation detail of node.js.
The ECMAScript standard doesn't guarantee any order for the keys iteration.
That said, the only solution I can think of is to use an array as support to sort the properties of the object and iterate on it:
var keys = Object.keys(object);
keys.sort();
for (var i = 0; i < keys.length; i++){
// this won't break if someone change NodeJS or Chrome implementation
console.log(keys[i]);
}
As this has recently been revived, I think it's worth pointing out again that we should generally treat objects as unordered collections of properties. Although ES6 did specify key traversal order (mostly first-added to last-added properties, but with a twist for integer-like keys), depending on this feels as though you're misusing your type. If it's ordered, use an array.
That said, if you are determined to do this, then it's relatively simple with ES6:
const sortKeys = (o) =>
Object (o) !== o || Array .isArray (o)
? o
: Object .keys (o) .sort () .reduce ((a, k) => ({...a, [k]: sortKeys (o [k])}), {})
const input = {Memo: {itemAmount1: "5", taxName1: "TAX", productPrice1: "10", accountName1: "Account Receivable (Debtors)"}, Footer: {productDescription2: "Maggie", itemQuantity2: "49.5", accountName2: "Account Receivable (Debtors)", taxName2: "TAX"}, Header: {itemDiscount3: "10", accountName3: "Account Receivable (Debtors)", productPrice3: "10", taxName3: "TAX"}}
console .log (
sortKeys(input)
)
.as-console-wrapper {min-height: 100% !important; top: 0}
Note that there is a potential performance issue here as described well by Rich Snapp. I would only spend time fixing it if it turned out to be a bottleneck in my application, but if we needed to we could fix that issue with a version more like this:
const sortKeys = (o) =>
Object (o) !== o || Array .isArray (o)
? o
: Object .keys (o) .sort () .reduce ((a, k) => ((a [k] = sortKeys (o [k]), a)), {})
While this works, the addition of the comma operator and the use of property assignment make it uglier to my mind. But either one should work.
Following up on #Gaurav Ramanan 's answer here's a shorter ES6 approach:
function sort(obj) {
if (typeof obj !== "object" || Array.isArray(obj))
return obj;
const sortedObject = {};
const keys = Object.keys(obj).sort();
keys.forEach(key => sortedObject[key] = sort(obj[key]));
return sortedObject;
}
The first condition will simply ensure that you only parse a valid object. So if it's not, it will return immediately with the original value unchanged.
Then an empty object is assigned because it will be used in the forEach loop where it will be mutated with the final sorted result.
The output will be a recursively sorted object.
This tested answer provides a recursive solution to a recursive problem. Note, it DOES NOT sort arrays (this is often undesired) but DOES sort objects within arrays (even nested arrays).
It uses lodash _.isPlainObject to simplify the logic of identifying an object but if you are not using lodash you can replace this with your own is it an object? logic.
const sortObjectProps = obj => {
return Object.keys(obj).sort().reduce((ordered, key) => {
let value = obj[key]
if (_.isPlainObject(value)) {
ordered[key] = sortObjectProps(value)
} else {
if (Array.isArray(value)) {
value = value.map(v => {
if (_.isPlainObject(v)) v = sortObjectProps(v)
return v
})
}
ordered[key] = value
}
return ordered
}, {})
}
const input = {
"Memo": {
"itemAmount1": "5",
"taxName1": "TAX",
"productPrice1": "10",
"accountName1": "Account Receivable (Debtors)"
},
"Footer": {
"productDescription2": "Maggie",
"itemQuantity2": "49.5",
"accountName2": "Account Receivable (Debtors)",
"taxName2": "TAX"
},
"Header": {
"itemDiscount3": "10",
"accountName3": "Account Receivable (Debtors)",
"productPrice3": "10",
"taxName3": "TAX"
}
}
const expected = {
"Footer": {
"accountName2": "Account Receivable (Debtors)",
"itemQuantity2": "49.5",
"productDescription2": "Maggie",
"taxName2": "TAX"
},
"Header": {
"accountName3": "Account Receivable (Debtors)",
"itemDiscount3": "10",
"productPrice3": "10",
"taxName3": "TAX"
},
"Memo": {
"accountName1": "Account Receivable (Debtors)",
"itemAmount1": "5",
"productPrice1": "10",
"taxName1": "TAX"
}
}
const actual = sortObjectProps(input)
const success = JSON.stringify(actual) === JSON.stringify(expected)
console.log(JSON.stringify(actual))
console.log('success (actual is expected)', success)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
Sorting everything in an object
Yes, that included nested objects, arrays, arrays of objects (and sorting those objects, too!)
I took #danday74's solution as a starting point and made my version work with arrays, arrays nested in arrays, and objects nested in arrays.
That is to say, even something like this:
const beforeSort = {
foo: {
b: 2,
a: [
{ b: 1, a: 10 },
{ y: 0, x: 5 },
],
},
};
Becomes this:
const afterSort = {
foo: {
a: [
{ x: 5, y: 0 }, // See note
{ a: 10, b: 1 },
],
b: 2,
},
};
/**
* Note:
* This object goes first in this array because 5 < 10.
* Unlike objects sorting by keys; arrays of objects sort
* by value of the first property, not by the key of the first property.
* This was important for me because arrays of objects are typically
* the same kinds of objects, so sorting alphabetically by key would be
* pretty pointless. Instead, it sorts by the value.
*/
My situation was I needed to compare an object (whose arrays' order didn't matter) to the JSON.stringify()'d object's string. Parsing the JSON into an object and performing a deep comparison between the objects wasn't an option as these strings were in a database.
And since the order of things could change randomly, I needed to make sure that the JSON generated was exactly the same every time. That meant sorting literally everything in the object, no matter how nested.
Using the above examples; the object beforeSort:
// After running through JSON.stringify()...
'{"foo":{"b":2,"a":[{"b":1,"a":10},{"y":0,"x":5}]}}'
Needs to match afterSort:
// After running through JSON.stringify()...
'{"foo":{"a":[{"x":5,"y":0},{"a":10,"b":1}],"b":2}}'
(Same object, different string.)
Obviously, if the order of an array is important to you, this won't be helpful.
Though... I'm not in the mood to look at it now, I imagined the idea that I could turn array-sorting on and off with a simple argument and a strategic if statement. Worth trying!
JavaScript version (with test objects)
// I use lodash's isEqual() is cloneDeep().
// Testing provided below.
function deepSortObject(object) {
const deepSort = (object) => {
// Null or undefined objects return immediately.
if (object == null) {
return object;
}
// Handle arrays.
if (Array.isArray(object)) {
return (
_.cloneDeep(object)
// Recursively sort each item in the array.
.map((item) => deepSort(item))
// Sort array itself.
.sort((a, b) => {
let workingA = a;
let workingB = b;
// Object or Array, we need to look at its first value...
if (typeof a === "object") {
workingA = a[Object.keys(a)[0]];
}
if (typeof b === "object") {
workingB = b[Object.keys(b)[0]];
}
if (Array.isArray(a)) {
workingA = a[0];
}
if (Array.isArray(b)) {
workingB = b[0];
}
// If either a or b was an object/array, we deep sort...
if (workingA !== a || workingB !== b) {
const sortedOrder = deepSort([workingA, workingB]);
if (_.isEqual(sortedOrder[0], workingA)) {
return -1;
} else {
return 1;
}
}
// If both were scalars, sort the normal way!
return a < b ? -1 : a > b ? 1 : 0;
})
);
}
// Anything other than Objects or Arrays just send it back.
if (typeof object != "object") {
return object;
}
// Handle objects.
const keys = Object.keys(object);
keys.sort();
const newObject = {};
for (let i = 0; i < keys.length; ++i) {
newObject[keys[i]] = deepSort(object[keys[i]]);
}
return newObject;
};
return deepSort(object);
}
// TESTING
const unsortedInput = {
ObjectC: {
propertyG_C: [[8, 7, 6], [5, 4, 3], [], [2, 1, 0]], // Array of arrays
propertyF_C: [
// This should result in sorting like: [2]'s a:0, [1]'s a:1, [0]'s a.x:5
{
b: 2,
a: [
{ b: 1, a: 10 }, // Sort array y by property a...
{ y: 0, x: 5 }, // vs property x
// Hot testing tip: change x to -1 and propertyF_C will sort it to the top!
],
},
{ c: 1, b: [1, 2, 0], a: 1 },
{ c: 0, b: [1, 2, 0], a: 0 },
],
propertyE_C: {
b: 2,
a: 1,
},
200: false,
100: true,
propertyB_C: true,
propertyC_C: 1,
propertyD_C: [2, 0, 1],
propertyA_C: "Blah",
},
ObjectA: {
propertyE_A: {
b: 2,
a: 1,
},
200: false,
100: true,
propertyB_A: true,
propertyC_A: 1,
propertyD_A: [2, 0, 1],
propertyA_A: "Blah",
},
ObjectB: {
propertyE_B: {
b: 2,
a: 1,
},
200: false,
100: true,
propertyB_B: true,
propertyC_B: 1,
propertyD_B: [2, 0, 1],
propertyA_B: "Blah",
},
};
const sortedOutput = {
ObjectA: {
100: true,
200: false,
propertyA_A: "Blah",
propertyB_A: true,
propertyC_A: 1,
propertyD_A: [0, 1, 2],
propertyE_A: {
a: 1,
b: 2,
},
},
ObjectB: {
100: true,
200: false,
propertyA_B: "Blah",
propertyB_B: true,
propertyC_B: 1,
propertyD_B: [0, 1, 2],
propertyE_B: {
a: 1,
b: 2,
},
},
ObjectC: {
100: true,
200: false,
propertyA_C: "Blah",
propertyB_C: true,
propertyC_C: 1,
propertyD_C: [0, 1, 2],
propertyE_C: {
a: 1,
b: 2,
},
propertyF_C: [
{ a: 0, b: [0, 1, 2], c: 0 },
{ a: 1, b: [0, 1, 2], c: 1 },
{
a: [
{ x: 5, y: 0 },
{ a: 10, b: 1 },
],
b: 2,
},
],
propertyG_C: [[0, 1, 2], [3, 4, 5], [6, 7, 8], []],
},
};
// Some basic testing...
console.log("Before sort, are the JSON strings the same?", JSON.stringify(unsortedInput) === JSON.stringify(sortedOutput));
console.log("After sort, are the JSON stirngs the same?", JSON.stringify(deepSortObject(unsortedInput)) === JSON.stringify(sortedOutput));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
TypeScript version
/* eslint-disable #typescript-eslint/no-explicit-any */
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
/**
* Takes an object that may have nested properties and returns a new shallow
* copy of the object with the keys sorted. It also sorts arrays, and arrays of
* objects.
*
* IF THERE IS ANY IMPORTANCE IN THE ORDER OF YOUR ARRAYS DO NOT USE THIS.
*
* Use this in conjunction with JSON.strigify() to create consistent string
* representations of the same object, even if the order of properties or arrays
* might be different.
*
* And if you're wondering. Yes, modern JS does maintain order in objects:
* https://exploringjs.com/es6/ch_oop-besides-classes.html#_traversal-order-of-properties
*
* #param object
* #returns object
*/
export function deepSortObject(object: any) {
const deepSort = (object: any): any => {
// Null or undefined objects return immediately.
if (object == null) {
return object;
}
// Handle arrays.
if (Array.isArray(object)) {
return (
cloneDeep(object)
// Recursively sort each item in the array.
.map((item) => deepSort(item))
// Sort array itself.
.sort((a, b) => {
let workingA = a;
let workingB = b;
// Object or Array, we need to look at its first value...
if (typeof a === "object") {
workingA = a[Object.keys(a)[0]];
}
if (typeof b === "object") {
workingB = b[Object.keys(b)[0]];
}
if (Array.isArray(a)) {
workingA = a[0];
}
if (Array.isArray(b)) {
workingB = b[0];
}
// If either a or b was an object/array, we deep sort...
if (workingA !== a || workingB !== b) {
const sortedOrder = deepSort([workingA, workingB]);
if (isEqual(sortedOrder[0], workingA)) {
return -1;
} else {
return 1;
}
}
// If both were scalars, sort the normal way!
return a < b ? -1 : a > b ? 1 : 0;
})
);
}
// Anything other than Objects or Arrays just send it back.
if (typeof object != "object") {
return object;
}
// Handle objects.
const keys = Object.keys(object);
keys.sort();
const newObject: Record<string, unknown> = {};
for (let i = 0; i < keys.length; ++i) {
newObject[keys[i]] = deepSort(object[keys[i]]);
}
return newObject;
};
return deepSort(object);
}
Unit tests
import { deepSortObject } from "#utils/object";
const unsortedInput = {
ObjectC: {
propertyG_C: [[8, 7, 6], [5, 4, 3], [], [2, 1, 0]], // Array of arrays
propertyF_C: [
// This should result in sorting like: [2]'s a:0, [1]'s a:1, [0]'s a.x:5
{
b: 2,
a: [
{ b: 1, a: 10 }, // Sort array y by property a...
{ y: 0, x: 5 }, // vs property x
// Hot testing tip: change x to -1 and propertyF_C will sort it to the top!
],
},
{ c: 1, b: [1, 2, 0], a: 1 },
{ c: 0, b: [1, 2, 0], a: 0 },
],
propertyE_C: {
b: 2,
a: 1,
},
200: false,
100: true,
propertyB_C: true,
propertyC_C: 1,
propertyD_C: [2, 0, 1],
propertyA_C: "Blah",
},
ObjectA: {
propertyE_A: {
b: 2,
a: 1,
},
200: false,
100: true,
propertyB_A: true,
propertyC_A: 1,
propertyD_A: [2, 0, 1],
propertyA_A: "Blah",
},
ObjectB: {
propertyE_B: {
b: 2,
a: 1,
},
200: false,
100: true,
propertyB_B: true,
propertyC_B: 1,
propertyD_B: [2, 0, 1],
propertyA_B: "Blah",
},
};
const sortedOutput = {
ObjectA: {
100: true,
200: false,
propertyA_A: "Blah",
propertyB_A: true,
propertyC_A: 1,
propertyD_A: [0, 1, 2],
propertyE_A: {
a: 1,
b: 2,
},
},
ObjectB: {
100: true,
200: false,
propertyA_B: "Blah",
propertyB_B: true,
propertyC_B: 1,
propertyD_B: [0, 1, 2],
propertyE_B: {
a: 1,
b: 2,
},
},
ObjectC: {
100: true,
200: false,
propertyA_C: "Blah",
propertyB_C: true,
propertyC_C: 1,
propertyD_C: [0, 1, 2],
propertyE_C: {
a: 1,
b: 2,
},
propertyF_C: [
{ a: 0, b: [0, 1, 2], c: 0 },
{ a: 1, b: [0, 1, 2], c: 1 },
{
a: [
{ x: 5, y: 0 },
{ a: 10, b: 1 },
],
b: 2,
},
],
propertyG_C: [[0, 1, 2], [3, 4, 5], [6, 7, 8], []],
},
};
describe("object utils", () => {
describe("sortObjectByKeys()", () => {
test("should sort correctly", () => {
expect(JSON.stringify(deepSortObject(unsortedInput))).toEqual(
JSON.stringify(sortedOutput)
);
});
});
});
My lead told me this might be worthy of cleaning up and hosting on NPM, I dunno. Too lazy. So I posted it here instead.

Categories

Resources