Sorting JavaScript Object by key value Recursively - javascript

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.

Related

Flatten nested object/array in javascript

I'm new to Javascript and I have nested objects and arrays that I would like to flatten.
I have...
[{ a: 2, b: [{ c: 3, d: [{e: 4, f: 5}, {e: 5,f: 6}]},
{ c: 4, d: [{e: 7, f: 8}]}
]
}]
and would like...
[{a:2,c:3,e:4,f:5}, {a:2,c:3,e:5,f:6}, {a:2,c:4,e:7,f:8}]
I've tried to adapt the following function written for an object for my array but i only get the final object within the array [{a:2,c:4,e:7,f:8}] https://stackoverflow.com/a/33158929/14313188. I think my issue is knowing how to iterate through arrays and objects?
original script:
function flatten(obj) {
var flattenedObj = {};
Object.keys(obj).forEach(function(key){
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
});
return flattenedObj;
}
my scripts (same result for both):
flat_array=[];
function superflat(array){
for (var i = 0; i < array.length; i++) {
var obj = array[i]
var flattenedObj = {};
Object.keys(obj).forEach(function(key){
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
});
flat_array.push(flattenedObj);
}
};
mega_flat_array=[];
function megaflatten(obj) {
Object.keys(obj).forEach(function(key){
var flattenedObj = {};
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
mega_flat_array.push(flattenedObj);
});
}
Thanks for your help
I would suggest starting with simpler data objects to test your function with, then progressively add more complex objects until your function performs as expected. forEach
Here you can see how I started with a simple test1 object, then test2 and so on so that the logic gets broken down into smaller increments.
To remove the duplicates we previously had, I had to throw and catch an error to break out of the recursive forEach loops, which added the unnecessary duplicate "rows" - perhaps it is better to use a normal for loop out of which you can simply break; and then use error handling for real errors.
The basic idea of the recursive function is to check the type of object (either array or object) and then loop through them to add values, but another check is needed on those to see if they are not Arrays and not Objects, if they are, call the function again. When a duplicate key is found i.e { c: 3}, remove the current key and add the new one before continuing the loop.
You can add some more tests if you have some more sample data, but there are better libraries to help you with TDD (test-driven development).
const isArray = (arr) => {
return Array.isArray(arr);
};
const isObject = (obj) => {
return typeof obj === "object" && obj !== null;
};
const flatten = (tree, row, result) => {
try {
if (isArray(tree)) {
tree.forEach((branch, index) => {
flatten(branch, row, result);
});
} else if (isObject(tree)) {
Object.keys(tree).forEach((key) => {
//we don't want to add objects or arrays to the row -
if (!isArray(tree[key]) && !isObject(tree[key])) {
if (key in row) {
// new row detected, get existing keys to work with
let keysArray = Object.keys(row);
// we are going to loop backwards and delete duplicate keys
let end = Object.keys(row).length;
let stopAt = Object.keys(row).indexOf(key);
//delete object keys from back of object to the newly found one
for (let z = end; z > stopAt; z--) {
delete row[keysArray[z - 1]];
}
row[key] = tree[key];
} else {
row[key] = tree[key];
}
} else {
flatten(tree[key], row, result);
throw "skip";
}
});
//all other rows in results will be overridden if we don't stringify
result.push(JSON.stringify(row));
}
} catch (e) {
//console.log(e)
} finally {
return result.map((row) => JSON.parse(row));
}
};
///tests
const test1 = [
{
a: 2,
b: 3,
},
];
const expected1 = [{ a: 2, b: 3 }];
const test2 = [
{
a: 2,
b: [
{
c: 3,
},
],
},
];
const expected2 = [{ a: 2, c: 3 }];
const test3 = [
{
a: 2,
b: [
{
c: 3,
},
{ c: 4 },
{ c: 5 },
],
},
];
const expected3 = [
{ a: 2, c: 3 },
{ a: 2, c: 4 },
{ a: 2, c: 5 },
];
let test4 = [
{
a: 2,
b: [
{
c: 3,
d: [
{ e: 4, f: 5 },
{ e: 5, f: 6 },
],
},
{ c: 4, d: [{ e: 7, f: 8 }] },
],
},
];
const expected4 = [
{ a: 2, c: 3, e: 4, f: 5 },
{ a: 2, c: 3, e: 5, f: 6 },
{ a: 2, c: 4, e: 7, f: 8 },
];
const test = (name, res, expected) => {
console.log(
`${name} passed ${JSON.stringify(res) === JSON.stringify(expected)}`
);
//console.log(res, expected);
};
//test("test1", flatten(test1, {}, []), expected1);
//test("test2", flatten(test2, {}, []), expected2);
//test("test3", flatten(test3, {}, []), expected3);
test("test4", flatten(test4, {}, []), expected4);
It's a bit of a behemoth, and it doesn't preserve the keys' order, but it does work with no duplicates.
It is recursive, so watch out for the call stack.
First, loop through the items in the array,
If an item is an array, make a recursive call.
On returning from that call, if the number of objects returned is more than there currently are in the final result, then update the returned objects with the properties from the objects in the final result, being careful to avoid overwriting pre-existing properties.
Otherwise update the final results with the properties in the returned result, again being careful not to overwrite existing properties.
If the item is not an array
If this is the first item put it into the final result
Otherwise add the item's properties to all the items in the final result, without overwriting any.
function makeFlat(arr) //assume you're always passing in an array
{
let objects = [];
arr.forEach(item =>
{
let currentObject = {};
const keys = Object.keys(item);
keys.forEach(key =>
{
const obj = item[key];
if(Array.isArray(obj))
{
let parts = makeFlat(obj);
if(objects.length > 0)
{
if(parts.length > objects.length)
{
parts.forEach(part =>
{
objects.forEach(ob =>
{
Object.keys(ob).forEach(k =>
{
if(Object.keys(part).indexOf(k) == -1)
{
part[k] = ob[k];
}
});
});
});
objects = parts;
}
else
{
objects.forEach(ob =>
{
parts.forEach(part =>
{
Object.keys(part).forEach(k =>
{
if(Object.keys(ob).indexOf(k) == -1)
{
ob[k] = part[k];
}
});
});
});
}
}
else
{
objects = parts;
}
}
else
{
if(Object.keys(currentObject).length == 0)
{
objects.push(currentObject);
}
currentObject[key] = item[key];
objects.forEach(ob =>
{
if(Object.keys(ob).indexOf(key) == -1)
{
ob[key] = currentObject[key]
}
});
}
});
});
return objects;
}
const inp = [{ a: 2, b: [{ c: 3, d: [{e: 4, f: 5}, {e: 5,f: 6}]},
{ c: 4, d: [{e: 7, f: 8}]}
], g:9
}];
let flattened = makeFlat(inp);
flattened.forEach(item => console.log(JSON.stringify(item)));

How to obtain a master structure for a json file?

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.

Is there a way to for an object filter to pass the object without a named reference?

I have the following javascript code which produces the desired results, i.e. returns both the 3rd and 4th objects in objectsArray since they both contain the max distance. However, I'm wondering if there is a way to not have to retype the name of the array when calling objectsArray.filter? I'm not trying to be lazy, just avoiding redundancy and the possibility of introducing a typo.
function meetsMax(obj) {
return obj.distance === Math.max.apply(Math, this.map(function(o) { return o.distance; }));
}
const objectsArray = [{ "distance": 1, "name": "first" }, { "distance": 2, "name": "second" }, { "distance": 3, "name": "third" }, { "distance": 3, "name": "fourth" }];
const objMax = objectsArray.filter(meetsMax, objectsArray);
console.log("objMax:", objMax);
I certainly wouldn't mind any other pointers on making the code more efficient and performant.
Function calls in JavaScript have some overhead, so native code is more efficient and performant:
var a = [ { "distance": 1, "name": "first" }, { "distance": 2, "name": "second" },
{ "distance": 3, "name": "third" }, { "distance": 3, "name": "fourth" } ]
for (var o = a[0], objMax = [o], m = o.distance, d, i = 1; i < a.length; i++)
if ((d = (o = a[i]).distance) > m) { objMax = [o]; m = d }
else if (d === m) objMax[objMax.length] = o
console.log(JSON.stringify(objMax))
There are also shorter and less efficient alternatives:
var a = [ { "distance": 1, "name": "first" }, { "distance": 2, "name": "second" },
{ "distance": 3, "name": "third" }, { "distance": 3, "name": "fourth" } ]
var d, b = []; a.forEach(o => (b[d = o.distance] = b[d] || []).push(o))
console.log(JSON.stringify(b[b.length - 1]))
Why don't you use for loop? It will be faster than your code.
"use strict";
let start = performance.now();
for (let z = 0; z < 1000; z++) {
function meetsMax(obj) {
return obj.distance === Math.max.apply(Math, this.map(function(o) { return o.distance; }));
}
const objectsArray = [{ "distance": 1, "name": "first" }, { "distance": 2, "name": "second" }, { "distance": 3, "name": "third" }, { "distance": 3, "name": "fourth" }];
const objMax = objectsArray.filter(meetsMax, objectsArray);
}
let fin = performance.now() - start;
console.log(fin); // 3.25ms
"use strict";
let start = performance.now();
for (let z = 0; z < 1000; z++) {
let a = [{ "distance": 1, "name": "first" }, { "distance": 2, "name": "second" }, { "distance": 3, "name": "third" }, { "distance": 3, "name": "fourth" }];
let maxDistance = 0;
let result = [];
for (let i = 0, max = a.length; i < max; i++) {
if (a[i].distance > maxDistance) {
maxDistance = a[i].distance;
}
}
for (let i = 0, max = a.length; i < max; i++) {
if (a[i].distance === maxDistance) {
result.push(a[i]);
}
}
}
let fin = performance.now() - start;
console.log(fin); // 1.28ms
.filter passes three arguments to the array: the current value, the index of the current value and the array itself. So you can change your filter function to:
function meetsMax(obj, index, objectsArray) {
return obj.distance === Math.max.apply(Math, objectsArray.map(function(o) { return o.distance; }));
}
and call .filter with
objectsArray.filter(meetsMax);
Always read the documentation of the functions you are using.
I certainly wouldn't mind any other pointers on making the code more efficient and performant.
If you, compute the maximum distance only once instead of in every iteration of the array. E.g. you could do:
function filterMax(arr, extractor) {
const max = arr.reduce(function(max, item) {
return max < extractor(item) ? extractor(item) : max;
}, extractor(arr[0]));
return arr.filter(function(item) {
return extractor(item) === max;
});
}
and call it as
filterMax(objectsArray, function(obj) { return obj.distance; });
function filterMax(arr, extractor) {
const max = arr.reduce(function(max, item) {
return max < extractor(item) ? extractor(item) : max;
}, extractor(arr[0]));
return arr.filter(function(item) {
return extractor(item) === max;
});
}
const objectsArray = [{ "distance": 1, "name": "first" }, { "distance": 2, "name": "second" }, { "distance": 3, "name": "third" }, { "distance": 3, "name": "fourth" }];
console.log(filterMax(objectsArray, function(obj) {
return obj.distance;
}));
According to MDN's Array.prototype.filter(), the array name is an optional override to the internal value of this.
So to answer the original question:
I'm wondering if there is a way to not have to retype the name of the array when calling objectsArray.filter?
Yes, you can safely leave it out.
var filter = function(x) { if (x > 5) return true; };
var arr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
alert(arr.filter(filter).join(","));
or even simpler (albeit harder to read):
alert([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(function(x) { if (x > 5) return true; }));
You specifically asked about an object, but you're not filtering objects, you're filtering an array of objects, so same applies.
console.log([ {foo: 1}, {foo: 2}, {foo: 3}, {foo: 4}, {foo: 5}, {foo: 6}, {foo: 7}, {foo: 8}, {foo: 9}, {foo: 10}].filter(function(x) { if (x.foo > 5) return true; }));

Filter array with object that contains array [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 5 years ago.
Improve this question
How can I make this
var foo = [{
"number":[1, 2, 3],
"id": [81, 82, 83]
}];
Into this
var foo = [{
"number": 1,
"id": 81
},{
"number": 2,
"id": 82
},{
"number": 3,
"id": 83
}]
I tried .map() and .filter() but they don't turn out the way I need it. Any suggestions? Thanks
You could create a function for that:
function transform(values) {
const result = [];
values.forEach(value => {
value.id.forEach((id, i) => {
result.push({id, number: value.number[i]});
});
});
return result;
}
While I find this to be an odd question, and I'm still hoping for a response regarding my suspicion this is an XY problem, here is a possible approach you can use for whatever you're trying to do.
Let's assume that foo is a single object which only contains enumerable properties that are all arrays of equal length:
var foo = {
"number": [1, 2, 3],
"id": [81, 82, 83]
}
function spread(obj) {
// get the enumerable keys of your object
var keys = Object.keys(obj)
// initialize an empty array
var array = []
// for each key...
keys.forEach(function (key) {
// for each element in the array of the property...
obj[key].forEach(function (value, index) {
// if the array element does not contain an object
// initialize an empty object in index
var base = index < array.length ? array[index] : (array[index] = {})
// assign the value to the key in the element
base[key] = value
})
})
// return the generated array
return array
}
console.log(spread(foo))
You can map the array's object, and thee number array of each object into a new array, then concat the results to flatten the sub arrays into one array.
var foo = [{
"number":[1, 2, 3],
"id": [81, 82, 83]
}];
var result = [].concat.apply([], foo.map(function(obj) { // map the array into sub arrays and flatten the results with concat
return obj.number.map(function(n, i) { // map the number array
return {
number: n,
id: obj.id[i] // get the id value by using the index
};
})
}));
console.log(result);
You need to create a list of objects based on the number of values within a given key.
So, you need to loop over the main list of objects. Inside that loop, you need to loop over the values for a key (i.e. pick the first). You will not need to use these values directly, they are just to determine how many records will be created in the final array. Lastly, you just iterate over the keys again and map the key-values pairs based on the current index.
The Array.prototype.concat.apply([], ...arrays) that happens at the end will combine all the arrays.
The function supports a single object or a list of objects.
var foo = [{
"number" : [ 1, 2, 3],
"id" : [81, 82, 83]
}, {
"number" : [ 4, 5, 6],
"id" : [84, 85, 86]
}];
console.log(JSON.stringify(mapValues(foo), null, 4));
function mapValues(arr) {
arr = !Array.isArray(arr) ? [arr] : arr;
return Array.prototype.concat.apply([], arr.map(item => {
return item[Object.keys(item)[0]].map((val, index) => {
return Object.keys(item).reduce((obj, key) => {
obj[key] = item[key][index];
return obj;
}, {});
});
}));
}
.as-console-wrapper { top: 0; max-height: 100% !important; }
Here is another version that does not introduce much complexity.
var foo = [{
"number" : [1, 2, 3],
"id" : [81, 82, 83]
}, {
"number" : [4, 5, 6],
"id" : [84, 85, 86]
}];
console.log(JSON.stringify(mapValues(foo), null, 4));
function mapValues(arr) {
arr = !Array.isArray(arr) ? [arr] : arr;
let records = [], fields;
arr.forEach(item => {
fields = fields || Object.keys(item);
item[fields[0]].forEach((val, index) => {
records.push(fields.reduce((obj, key) => {
obj[key] = item[key][index];
return obj;
}, {}));
});
});
return records;
}
.as-console-wrapper {
top: 0;
max-height: 100% !important;
}
Result
[{
"number": 1,
"id": 81
}, {
"number": 2,
"id": 82
}, {
"number": 3,
"id": 83
}, {
"number": 4,
"id": 84
}, {
"number": 5,
"id": 85
}, {
"number": 6,
"id": 86
}]

JavaScript flattening an array of arrays of objects

I have an array which contains several arrays, each containing several objects, similar to this.
[[object1, object2],[object1],[object1,object2,object3]]
Here is a screenhot of the object logged to the console.
What would be the best approach to flattening this out so it just an array of objects?
I've tried this with no luck:
console.log(searchData);
var m = [].concat.apply([],searchData);
console.log(m);
searchData logs out the screenshot above, but m logs out [ ]
Here is the actual contents of searchData:
[[{"_id":"55064111d06b96d974937a6f","title":"Generic Title","shortname":"generic-title","contents":"<p>The Healing Center offers practical, social, and spiritual support to individuals and families. Services include, but are not limited to: food and clothing, job skills training and job search assistance, auto repair (Saturdays only), mentoring, financial counseling, tutoring, prayer, life skills training, and helpful information about local community services.</p><p>Stay in touch with us:</p>","__v":0},{"_id":"5508e1405c621d4aad2d2969","title":"test english","shortname":"test-page","contents":"<h2>English Test</h2>","__v":0}],[{"_id":"550b336f33a326aaee84f883","shortname":"ok-url","title":"now english","contents":"<p>okokko</p>","category":"Transportation","__v":0}]]
You can use Array.concat like bellow:-
var arr = [['object1', 'object2'],['object1'],['object1','object2','object3']];
var flattened = [].concat.apply([],arr);
flattened will be your expected array.
ES 2020 gives the flat, also flatMap if you want to iterate over, to flat lists of lists:
[['object1'], ['object2']].flat() // ['object1', 'object2']
A recursive solution for deep (nested) flattening:
function flatten(a) {
return Array.isArray(a) ? [].concat.apply([], a.map(flatten)) : a;
}
A bit more compactly with ES6:
var flatten = a => Array.isArray(a) ? [].concat(...a.map(flatten)) : a;
For fun, using a generator named F for "flatten", to lazily generate flattened values:
function *F(a) {
if (Array.isArray(a)) for (var e of a) yield *F(e); else yield a;
}
>> console.log(Array.from(F([1, [2], 3])));
<< [ 1, 2, 3 ]
For those not familiar with generators the yield * syntax yields values from another generator. Array.from takes an iterator (such as results from invoking the generator function) and turns it into an array.
If you only need simple flatten, this may works:
var arr = [['object1', 'object2'],['object1'],['object1','object2','object3']];
var flatenned = arr.reduce(function(a,b){ return a.concat(b) }, []);
For more complex flattening, Lodash has the flatten function, which maybe what you need: https://lodash.com/docs#flatten
//Syntax: _.flatten(array, [isDeep])
_.flatten([1, [2, 3, [4]]]);
// → [1, 2, 3, [4]];
// using `isDeep` to recursive flatten
_.flatten([1, [2, 3, [4]]], true);
// → [1, 2, 3, 4];
you can use flat() :
const data = [ [{id:1}, {id:2}], [{id:3}] ];
const result = data.flat();
console.log(result);
// you can specify the depth
const data2 = [ [ [ {id:1} ], {id:2}], [{id:3}] ];
const result2 = data2.flat(2);
console.log(result2);
in your case :
const data = [[{"_id":"55064111d06b96d974937a6f","title":"Generic Title","shortname":"generic-title","contents":"<p>The Healing Center offers practical, social, and spiritual support to individuals and families. Services include, but are not limited to: food and clothing, job skills training and job search assistance, auto repair (Saturdays only), mentoring, financial counseling, tutoring, prayer, life skills training, and helpful information about local community services.</p><p>Stay in touch with us:</p>","__v":0},{"_id":"5508e1405c621d4aad2d2969","title":"test english","shortname":"test-page","contents":"<h2>English Test</h2>","__v":0}],[{"_id":"550b336f33a326aaee84f883","shortname":"ok-url","title":"now english","contents":"<p>okokko</p>","category":"Transportation","__v":0}]]
const result = data.flat();
console.log(result);
Using ES6 Spread Operator
Array.prototype.concat(...searchData)
OR
[].concat(...searchData)
You can use this custom recursive method to flattened any nested array
const arr = [
[1, 2],
[3, 4, 5],
[6, [7, 8], 9],
[10, 11, 12]
]
const flatenedArray = arr => {
let result = [];
if(!arr.constructor === Array) return;
arr.forEach(a => {
if(a.constructor === Array) return result.push(...flatenedArray(a));
result.push(a);
});
return result;
}
console.log(flatenedArray(arr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
Recursively flatten an array:
function flatten(array) {
return !Array.isArray(array) ? array : [].concat.apply([], array.map(flatten));
}
var yourFlattenedArray = flatten([[{"_id":"55064111d06b96d974937a6f","title":"Generic Title","shortname":"generic-title","contents":"<p>The Healing Center offers practical, social, and spiritual support to individuals and families. Services include, but are not limited to: food and clothing, job skills training and job search assistance, auto repair (Saturdays only), mentoring, financial counseling, tutoring, prayer, life skills training, and helpful information about local community services.</p><p>Stay in touch with us:</p>","__v":0},{"_id":"5508e1405c621d4aad2d2969","title":"test english","shortname":"test-page","contents":"<h2>English Test</h2>","__v":0}],[{"_id":"550b336f33a326aaee84f883","shortname":"ok-url","title":"now english","contents":"<p>okokko</p>","category":"Transportation","__v":0}]]
);
log(yourFlattenedArray);
function log(data) {
document.write('<pre>' + JSON.stringify(data, null, 2) + '</pre><hr>');
}
* {font-size: 12px; }
let functional = {
flatten (array) {
if (Array.isArray(array)) {
return Array.prototype.concat(...array.map(this.flatten, this));
}
return array;
}
};
functional.flatten([0, [1, 2], [[3, [4]]]]); // 0, 1, 2, 3, 4
I've noticed that people are using recursions which are not cost friendly, especially with new ES6 standards giving us the power of spread operators. When you're pushing the items into the master array just use ... and it will automatically add flattened objects. Something like
array.push(...subarray1) // subarray1 = [object1, object2]
array.push(...subarray2) // subarray2 = [object3]
array.push(...subarray3) // subarray3 = [object4,object5, object6]
// output -> array = [object1, object2, object3, object4, object5, object6]
My solution to flatten an array of objects and return a single array.
flattenArrayOfObject = (arr) => {
const flattened = {};
arr.forEach((obj) => {
Object.keys(obj).forEach((key) => {
flattened[key] = obj[key];
});
});
return flattened;
};
Example
const arr = [
{
verify: { '0': 'xyzNot verified', '1': 'xyzVerified' },
role_id: { '1': 'xyzMember', '2': 'xyzAdmin' },
two_factor_authentication: { '0': 'No', '1': 'Yes' }
},
{ status: { '0': 'xyzInactive', '1': 'Active', '2': 'xyzSuspend' } }
]
flattenArrayOfObject(arr)
// {
// verify: { '0': 'xyzNot verified', '1': 'xyzVerified' },
// status: { '0': 'xyzInactive', '1': 'Active', '2': 'xyzSuspend' },
// role_id: { '1': 'xyzMember', '2': 'xyzAdmin' },
// two_factor_authentication: { '0': 'No', '1': 'Yes' }
// }
If each object has an array and continues in the same way nested :
function flatten(i,arrayField){
if(Array.isArray(i)) return i.map(c=>flatten(c,arrayField));
if(i.hasOwnProperty(arrayField)) return [{...i,[arrayField]:null},...i[arrayField].map(c=>flatten(c,arrayField))];
return {...i,[arrayField]:null};
}
let data=flatten(myData,'childs');
mydata like this :
[
{
"id": 1,
"title": "t1",
"sort_order": 200,
"childs": [
{
"id": 2,
"title": "t2",
"sort_order": 200,
"childs": []
},
{
"id": 3,
"title":"mytitle",
"sort_order": 200,
"childs": []
},
{
"id": 4,
"title":"mytitle",
"sort_order": 200,
"childs": []
},
{
"id": 5,
"title":"mytitle",
"sort_order": 200,
"childs": []
},
{
"id": 6,
"title":"mytitle",
"sort_order": 200,
"childs": []
}
]
},
{
"id": 7,
"title": "راهنما",
"sort_order":"mytitle",
"childs": [
{
"id": 8,
"title":"mytitle",
"sort_order": 200,
"childs": []
},
{
"id": 9,
"title":"mytitle",
"sort_order": 200,
"childs": []
},
{
"id": 10,
"title":"mytitle",
"sort_order": 200,
"childs": []
}
]
}
]
let nestedArray = [[1, 2], [3, 4], [5, 6]];
let flattenArray = function(nestedArray) {
let flattenArr = [];
nestedArray.forEach(function(item) {
flattenArr.push(...item);
});
return flattenArr;
};
console.log(flattenArray(nestedArray)); // [1, 2, 3, 4, 5, 6]
var arr = [1,[9,22],[[3]]];
var res = [];
function flatten(arr){
for(let i=0;i<arr.length;i++){
if(typeof arr[i] == "number"){
res.push(arr[i]);
}
else if(typeof arr[i] == "object"){
fatten(arr[i]);
}
}
}
Calling function
flatten(arr);
console.log(res);
Result
 
[1, 9, 22, 3]
// Polyfill flat method
var flatten = a => Array.isArray(a) ? [].concat(...a.map(flatten)) : a;
var deepFlatten = (arr, depth = 1) => {
return depth > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? deepFlatten(val, depth - 1) : val), [])
: arr.slice();
}
console.log(deepFlatten([0, 1, 2, [[[3, 4]]]], Infinity));
// You can pass label in place of 'Infinity'

Categories

Resources