I'm trying to convert an array of arrays into an array of nested objects in JavaScript. Let's assume each subarray in the array represents a file path. I want to create an array of objects where each object has 2 properties, the name of the current file and any files/children that come after the current file/parent.
So for example, if I have this array of arrays where each subarray represents a file path:
[['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']]
I want to get this as the result:
[
{
name :'A',
children: [
{
name: 'B',
children: [
{
name: 'C',
children: []
},
{
name: 'D',
children: []
}
]
}
]
},
{
name: 'L',
children: [
{
name: 'M',
children: [
{
name: 'N',
children: []
}
]
}
]
}
]
I tried mapping through the array of arrays and creating an object for the current file/parent if it hasn't been created yet. I think I may be on the right track but I can't seem to think of the best way to do so.
Something I could do in 5 minutes, it probably can be improved. This could also be written as a recursive function, I believe.
const result = [];
arr.forEach((subArr) => {
var ref = result;
subArr.forEach((name) => {
const obj = ref.find((obj) => obj.name == name);
if (obj) {
ref = obj.children;
} else {
ref.push({ name, children: [] });
ref = ref[ref.length - 1].children;
}
});
});
Here's mine:
// Function to convert path array ‘a’ from position ‘i’ into tree structure in ‘v’.
const tree = (a, i, v) => {
if (i < a.length) { tree(a, i+1, v[a[i]] ||= { }) }
}
// Function to convert a simple node into the desired record format.
const record = (v) => {
const a = [];
for (const [k, w] of Object.entries(v)) {
a.push({ name: k, children: record(w) });
}
return a;
}
const m = { }
for (const a of [['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']]) {
tree(a, 0, m);
}
const result = record(m);
While it might be overkill for this requirement, I have handy a variant of one of my utility functions, setPath, that is used for adding such a path to an existing array. This is a bit different from the other answers in that it does this in an immutable way, returning a new object that shares as much structure as possible with the original one. I always prefer to work with immutable data.
Using that, we can write a hydrate function to do this job as the one-liner, const hydrate = (paths) => paths .reduce (setPath, []).
This is quite likely overkill here, as there is probably no reason to build your output one immutable level after another. But it is a demonstration of the value of keeping utility functions handy.
The code looks like this:
const call = (fn, ...args) => fn (...args)
const setPath = (xs, [name, ...names]) => call (
(i = ((xs .findIndex (x => x .name == name) + 1) || xs .length + 1) - 1) =>
name == undefined
? [...xs]
: [
...xs .slice (0, i),
{name, children: setPath ((i == xs .length) ? [] : xs [i] .children, names)},
...xs .slice (i + 1)
]
)
const hydrate = (paths) => paths .reduce (setPath, [])
console .log (
hydrate ([['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']])
)
.as-console-wrapper {max-height: 100% !important; top: 0}
We have a trivial call helper function that I use here to avoid statements. As much as possible, I prefer to work with expressions, as these avoid temporal notions in code brought on by statements, and they compose much better into larger pieces. We could alternatively do this with default parameters, but they have other problems. A trivial call function handles this nicely.
In our main function, we destructure apart the first name in the input from the remaining ones. Then we pass to call a function that calculates the index of the element with our existing name in the input array. If it doesn't exist, the index will be the length of that array. This is perhaps over-tricky. findIndex returns -1 if no element matches. We add 1 to the result, and then, if it's 0, we choose one more than the length of the array. Finally we subtract 1 from the result, and now the index will be where we found our target or the length of the array if it wasn't found.
Now, if the path is empty, we return a copy of our array. (I prefer the copy just for consistency, but it would be legitimate to just return it directly.) If it's not empty, we use the index to tear apart our input array, keeping everything before it, building a new item for that index by recursively calling setPath with the remaining node names, and then keeping everything after that index.
And now, as noted, our hydrate function is a trivial fold of setPath starting with an empty array.
Related
Let's say I want to do something like:
const result = 'someKey' in myVar
? myVar.someKey
: myVar.anotherKey
I can achieve the same using get(myVar, 'someKey', get(myVar, 'anotherKey')). I was wondering if there is another lodash method that looks cleaner than that?
No built in function in Lodash allows this (sadly). However, as Alexander Nied said in his answer, you can easily create a custom function for this. I will use Lodash functionality instead of vanilla JavaScript just to demonstrate how it can work:
function getFirst(item, paths) {
const notFound = Symbol("not found");
return _(paths)
.map(path => _.get(item, path, notFound))
.filter(result => result !== notFound)
.first();
}
const obj = {
a: {
b: {
c: 42
}
},
x: {
y: 1
},
z: 2
};
console.log(getFirst(obj, ['g']));
console.log(getFirst(obj, ['g', 'h', 'z']));
console.log(getFirst(obj, ['g', 'h', 'a.b.c', 'z']));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
Chaining using Lodash is lazily evaluated, so the sequence is map first path to _.get(item, path) and if it fails returns a unique notFound value. Then if the notFound is discarded the next value would be mapped. This continues until either there is a match or all the members of paths are exhausted and the value is undefined.
I don't believe such a lodash function exists, but a simple utility could be written to provide that behavior:
function getFirst(obj, ...paths) {
for (const path of paths) {
if (_.has(obj, path)) {
return _.get(obj, path);
}
}
return;
}
I have a JavaScript object array with the following structure:
somedata = {
foo: {
bar: [
{
baz: [
{
someprop: 'a'
},
{
someprop: 'b'
},
{
someprop: 'c'
}
]
},
{
baz: [
{
someprop: 'd'
},
{
someprop: 'e'
},
{
someprop: 'f'
}
]
}
]
}
}
I want to extract someprop field from this JavaScript object as an array ['a', 'b', 'c', 'd', 'e', 'f']
currently, this is my code logic to extract someprop field as an array:
const result = []
somedata.foo.bar.forEach(x => {
x.baz.forEach(y => {
result.push(y.someprop)
})
})
console.log(result) // prints ["a", "b", "c", "d", "e", "f"]
i tried to make the code more reusable by creating a function:
function extractToArray(data, arr, prop) {
let result = []
data.forEach(x => {
x[arr].forEach(y => {
result.push(y[prop])
})
})
return result;
}
console.log(extractToArray(somedata.foo.bar, 'baz', 'someprop'))
But is there a more concise, elegant, cleaner way to achieve this?
Note: possible duplicate covers an array of objects, but this is regarding an array of objects of an array of objects (so a simple map solution won't work).
You can use flatMap for that:
const somedata = {foo:{bar:[{baz:[{someprop:"a"},{someprop:"b"},{someprop:"c"}]},{baz:[{someprop:"d"},{someprop:"e"},{someprop:"f"}]}]}};
const result = somedata.foo.bar.flatMap(({baz}) => baz.map(({someprop}) => someprop));
console.log(result);
Note that not every current browser supports this yet, so you might want to use a polyfill.
You could create recursive function that will find your prop on any level and return array as a result.
const somedata = {"foo":{"bar":[{"baz":[{"someprop":"a"},{"someprop":"b"},{"someprop":"c"}]},{"baz":[{"someprop":"d"},{"someprop":"e"},{"someprop":"f"}]}]}}
function get(data, prop) {
const result = [];
for (let i in data) {
if (i == prop) result.push(data[prop]);
if (typeof data[i] == 'object') result.push(...get(data[i], prop))
}
return result;
}
console.log(get(somedata, 'someprop'))
A recursive function that does it in one functional expression:
const extractToArray = (data, prop) => Object(data) !== data ? []
: Object.values(data).flatMap(v => extractToArray(v, prop))
.concat(prop in data ? data[prop] : []);
var somedata = {foo: {bar: [{baz: [{someprop: 'a'},{someprop: 'b'},{someprop: 'c'}]},{baz: [{someprop: 'd'},{someprop: 'e'},{someprop: 'f'}]}]}}
console.log(extractToArray(somedata, "someprop"));
This is reusable in the sense that it also works when the property is not always present, or not always at the same depth within the data structure.
For others with similar question, I am adding a more generic (but possibly a bit less efficient) alternative using the JSON.parse reviver parameter
var arr = [], obj = {foo:{bar:[{baz:[{someprop:"a"},{someprop:"b"},{someprop:"c"}]},{baz:[{someprop:"d"},{someprop:"e"},{someprop:"f"}]}]}}
JSON.parse(JSON.stringify(obj), (k, v) => k === 'someprop' && arr.push(v))
console.log(arr)
I was experimenting with the spread syntax and am having difficulty making rational sense out of its behavior in a particular situation.
In one instance, when I use:
const art = ["hello"]
console.log( [{...art}] )
the return value is
=> [ { '0': 'hello' } ]
However, when I iterate over the single array value it produces an entirely different effect:
const art2 = art.map((item) => ({ ...item }))
console.log(art2)
=> [ { '0': 'h', '1': 'e', '2': 'l', '3': 'l', '4': 'o' } ]
Why does using the spread syntax in the first example only combine it with a single index, but in the second example with .map being used break it down into different index elements? Since there is only a single item in the art array I would have assumed the results would be the same.
In the first code, you're spreading an array which contains one item, a string at the zeroth index:
console.log({ ...["hello"] });
All is as expected. But in the second code, you're calling .map on the array first, and then spreading the first argument provided to the .map function - the items being spreaded aren't arrays, but the items the array contains, which, in this case, is a string. When you spread a string, you'll get properties matching the values at each character index:
console.log(['hello'].map((item) => ({ ...item })))
// same as:
console.log({ ...'hello' });
They're entirely different situations.
Both results are as expected.
In first case you are passing whole array to spread operator
const art = ["hello"]
console.log( [{...art}] )
So spread operator is applied to whole array at once.
In second case, first you are iterating the array using .map() i.e. you are picking each item and passing that item to spread operator.
const art2 = art.map((item) => ({ ...item }))
console.log(art2)
So spread operator is applied to each item.
const art = ["hello"]
In above case, you tried to spread the array. So it will give you following output :
[ { '0': 'hello' } ]
Now you tried to execute following one
const art2 = art.map((item) => ({ ...item }))
So here you have used map. Map is something which takes one element of array and applies mapper to it.
In your case your mapper is to spread the given element. So now it will spread the element you have passed. So you got the output as :
[ { '0': 'h', '1': 'e', '2': 'l', '3': 'l', '4': 'o' } ]
Assume , we have :
var all=[
{firstname:'Ahmed', age:12},
{firstname:'Saleh', children:5 }
{fullname: 'Xod BOD', children: 1}
];
The expected result is ['firstname','age', 'children', 'fullname']: the union of keys of all objects of that array:
all.map((e) => Object.keys(e) ).reduce((a,b)=>[...a,...b],[]);
This is work fine , However, i am seeking a solution more performance using directly reduce method without map , I did the following and it is failed.
all.reduce((a,b) =>Object.assign([...Object.keys(a),...Object.keys(b)]),[])
You can use Set, reduce() and Object.keys() there is no need for map.
var all=[
{firstname:'Ahmed', age:12},
{firstname:'Saleh', children:5 },
{fullname: 'Xod BOD', children: 1}
];
var result = [...new Set(all.reduce((r, e) => [...r, ...Object.keys(e)], []))];
console.log(result)
Here's a solution using generic procedures concat, flatMap, and the ES6 Set.
It's similar to #NenadVracar's solution but uses higher-order functions instead of a complex, do-it-all-in-one-line implementation. This reduces complexity in your transformation and makes it easier to re-use procedures in other areas of your program.
Not that ... spread syntax is bad, but you'll also notice this solution does not necessitate it.
var all = [
{firstname:'Ahmed', age:12},
{firstname:'Saleh', children:5 },
{fullname: 'Xod BOD', children: 1}
];
const concat = (x,y) => x.concat(y);
const flatMap = f => xs => xs.map(f).reduce(concat, []);
const unionKeys = xs =>
Array.from(new Set(flatMap (Object.keys) (xs)));
console.log(unionKeys(all));
// [ 'firstname', 'age', 'children', 'fullname' ]
Just out of curiosity, I've been benchmarking some solutions to your problem using different approaches (reduce vs foreach vs set). Looks like Set behaves well for small arrays but it's extremely slow for bigger arrays (being the best solution the foreach one).
Hope it helps.
var all = [{
firstname: 'Ahmed',
age: 12
}, {
firstname: 'Saleh',
children: 5
}, {
fullname: 'Xod BOD',
children: 1
}],
result,
res = {};
const concat = (x,y) => x.concat(y);
const flatMap = f => xs => xs.map(f).reduce(concat, []);
const unionKeys = xs =>
Array.from(new Set(flatMap (Object.keys) (xs)));
for(var i = 0; i < 10; i++)
all = all.concat(all);
console.time("Reduce");
result = Object.keys(all.reduce((memo, obj) => Object.assign(memo, obj), {}));
console.timeEnd("Reduce");
console.time("foreach");
all.forEach(obj => Object.assign(res, obj));
result = Object.keys(res);
console.timeEnd("foreach");
console.time("Set");
result = [...new Set(all.reduce((r, e) => r.concat(Object.keys(e)), []))];
console.timeEnd("Set");
console.time("Set2");
result = unionKeys(all);
console.timeEnd("Set2");
Try this code:
var union = new Set(getKeys(all));
console.log(union);
// if you need it to be array
console.log(Array.from(union));
//returns the keys of the objects inside the collection
function getKeys(collection) {
return collection.reduce(
function(union, current) {
if(!(union instanceof Array)) {
union = Object.keys(union);
}
return union.concat(Object.keys(current));
});
}
For example the following
var data = {
'States': ['NSW', 'VIC'],
'Countries': ['GBR', 'AUS'],
'Capitals': ['SYD', 'MEL']
}
for (var item in data) {
console.log(item);
}
prints
States
Countries
Capitals
Is there a way to sort alphabetically so that it prints
Capitals
Countries
States
Not within the object itself: the property collection of an object is unordered.
One thing you could do is use Object.keys(), and sort the Array, then iterate it.
Object.keys(data)
.sort()
.forEach(function(v, i) {
console.log(v, data[v]);
});
Patches (implementations) for browsers that do not support ECMAScript 5th edition:
Object.keys
Array.forEach
here's a nice functional solution:
basically,
extract the keys into a list with Object.keys
sort the keys
reduce list back down to an object to get desired result
ES5 Solution:
not_sorted = {b: false, a: true};
sorted = Object.keys(not_sorted)
.sort()
.reduce(function (acc, key) {
acc[key] = not_sorted[key];
return acc;
}, {});
console.log(sorted) //{a: true, b: false}
ES6 Solution:
not_sorted = {b: false, a: true}
sorted = Object.keys(not_sorted)
.sort()
.reduce((acc, key) => ({
...acc, [key]: not_sorted[key]
}), {})
console.log(sorted) //{a: true, b: false}
Yes, there is. Not within ECMAScript standard, but supported across browsers and Node.js, and apparently stable. See https://stackoverflow.com/a/23202095/645715.
EDIT: This returns an object in which the keys are ordered. You can use Object.keys(...) to get the ordered keys from the object.
Why worry about object key order? The difference can matter in some applications, such as parsing XML with xml2js which represents XML as nested objects, and uses XML tags as hash keys.
There are a couple of notes:
keys that look like integers appear first and in numeric order.
keys that look like strings appear next and in insertion order.
this order is reported by Object.keys(obj)
the order as reported by for (var key in obj) {...} may differ in Safari, Firefox
The function returns an object with sorted keys inserted in alphabetic order:
function orderKeys(obj) {
var keys = Object.keys(obj).sort(function keyOrder(k1, k2) {
if (k1 < k2) return -1;
else if (k1 > k2) return +1;
else return 0;
});
var i, after = {};
for (i = 0; i < keys.length; i++) {
after[keys[i]] = obj[keys[i]];
delete obj[keys[i]];
}
for (i = 0; i < keys.length; i++) {
obj[keys[i]] = after[keys[i]];
}
return obj;
}
Here's a quick test:
var example = {
"3": "charlie",
"p:style": "c",
"berries": "e",
"p:nvSpPr": "a",
"p:txBody": "d",
"apples": "e",
"5": "eagle",
"p:spPr": "b"
}
var obj = orderKeys(example);
this returns
{ '3': 'charlie',
'5': 'eagle',
apples: 'e',
berries: 'e',
'p:nvSpPr': 'a',
'p:spPr': 'b',
'p:style': 'c',
'p:txBody': 'd' }
You can then get the ordered keys as:
Object.keys(obj)
Which returns
["3", "5", "apples", "berries", "p:nvSpPr", "p:spPr", "p:style", "p:txBody"]
You can also sort it in this way.
const data = {
States: ['NSW', 'VIC'],
Countries: ['GBR', 'AUS'],
Capitals: ['SYD', 'MEL']
}
const sortedObject = Object.fromEntries(Object.entries(data).sort())
I cannot explain in detail why it works but apparently it's working flawlessly :) Try yourself if you're not believing me :D
Note that your browser or Node.js version should support Object.fromEntries
For browser compatibility check bottom of this page
For Node, it will work for versions from 12 and higher.
For Deno from the v1.
I would add this as a comment to #prototype's post, but my rep isn't high enough.
If anyone needs a version of orderKeys that #prototype wrote that is compliant with eslint-config-airbnb:
/**
* Returns and modifies the input object so that its keys are returned in sorted
* order when `Object.keys(obj)` is invoked
*
* #param {object} obj The object to have its keys sorted
*
* #returns {object} The inputted object with its keys in sorted order
*/
const orderKeys = (obj) => {
// Complying with `no-param-reassign`, and JavaScript seems to assign by reference here
const newObj = obj;
// Default `.sort()` chained method seems to work for me
const keys = Object.keys(newObj).sort();
const after = {};
// Add keys to `after` in sorted order of `obj`'s keys
keys.forEach((key) => {
after[key] = newObj[key];
delete newObj[key];
});
// Add keys back to `obj` in sorted order
keys.forEach((key) => {
newObj[key] = after[key];
});
return newObj;
};
Using #prototype's tests:
const example = {
3: 'charlie',
'p:style': 'c',
berries: 'e',
'p:nvSpPr': 'a',
'p:txBody': 'd',
apples: 'e',
5: 'eagle',
p:spPr: 'b'
}
const obj = orderKeys(example);
console.log(obj);
console.log(Object.keys(obj));
Outputs the following:
Babel Compiler v6.4.4
Copyright (c) 2014-2015 Sebastian McKenzie
{ 3: 'charlie',
5: 'eagle',
apples: 'e',
berries: 'e',
'p:nvSpPr': 'a',
'p:spPr': 'b',
'p:style': 'c',
'p:txBody': 'd' }
[ '3',
'5',
'apples',
'berries',
'p:nvSpPr',
'p:spPr',
'p:style',
'p:txBody' ]
For whatever it's worth, I needed this in my React app so that I could sort the options of a dropdown that was based on a state object, assigned after a response from my API.
Initially I did
return (
// ...
<Select
options={Object.keys(obj).sort()}
// ...
/>
// ...
);
But realized the .sort() method would be invoked on each re-render, hence needing #prototype's implementation of orderKeys.
https://stackoverflow.com/users/645715/prototype
Here's a one-liner to sort an object's keys using lodash
_.chain(obj).toPairs().sortBy(0).fromPairs().value()
With your example data:
var data = {
'States': ['NSW', 'VIC'],
'Countries': ['GBR', 'AUS'],
'Capitals': ['SYD', 'MEL']
}
data = _.chain(data)
.toPairs() // turn the object into an array of [key, value] pairs
.sortBy(0) // sort these pairs by index [0] which is [key]
.fromPairs() // convert array of pairs back into an object {key: value}
.value() // return value
/*
{
Capitals: [ 'SYD', 'MEL' ],
Countries: [ 'GBR', 'AUS' ],
States: [ 'NSW', 'VIC' ]
}
*/
Here is a nice solution if you have a more complex object where some of the properties are also objects.
const sortObject = (obj: any) => {
const sorted = Object.keys(obj)
.sort()
.reduce((accumulator, key) => {
if (typeof obj[key] === "object") {
// recurse nested properties that are also objects
if (obj[key] == null) {
accumulator[key] = null;
} else if (isArray(obj[key])) {
accumulator[key] = obj[key].map((item: any) => {
if (typeof item === "object") {
return sortObject(item);
} else {
return item;
}
});
} else {
accumulator[key] = sortObject(obj[key]);
}
} else {
accumulator[key] = obj[key];
}
return accumulator;
}, {});
return sorted;
};
Improved this answer to ES6, shortened to one loop and with typescript
function orderKeys(obj: {}) {
var keys = (Object.keys(obj) as Array<keyof typeof obj>).sort((k1, k2) => {
if (k1 < k2) return -1;
else if (k1 > k2) return +1;
else return 0;
});
var helpArr = {};
for (var elem of keys) {
helpArr[elem] = obj[elem];
delete obj[elem];
obj[elem] = helpArr[elem]
}
return obj;
}
var data = {
'States': ['NSW', 'VIC'],
'Countries': ['GBR', 'AUS'],
'Capitals': ['SYD', 'MEL']
}
Or with ES6 and reduce
const sorted = (Object.keys(data) as Array<keyof typeof data>).sort().reduce((r: any, k) => ({ ...r, [k]: data[k] }), {});
Result ordered by key
console.log(orderKeys(data)) # similar: console.log(sorted)
{
Capitals: [ 'SYD', 'MEL' ],
Countries: [ 'GBR', 'AUS' ],
States: [ 'NSW', 'VIC' ]
}
If conversion to an array does not suit your template and you know the keys of your object you can also do something like this:
In your controller define an array with the keys in the correct order:
this.displayOrder = [
'firstKey',
'secondKey',
'thirdKey'
];
In your template repeat the keys of your displayOrder and then use ng-init to reference back to your object.
<div ng-repeat="key in ctrl.displayOrder" ng-init="entry = ctrl.object[key]">
{{ entry.detail }}
</div>