I am trying to transform a JSON file that comes from an API with a structure similar to this:
{
"fruitType": {"name": "bananas"},
"plantedIn": {"country": "USA", "state": "Minnesota"},
"harvestMethod": {"name": "manual"},
"product": {"id": "841023781723"},
},
... (other fruits, or the same fruit planted/harvested differently)
Into something like this:
"bananas": {
"USA": {
"manual": {
"841023781723": {
"fruitType": {"name": "bananas"},
"plantedIn": {"country": "USA", "state": "Minnesota"},
"harvestMethod": {"name": "manual"},
"product": {"id": "841023781723"},
}
}
}
},
...
So, essentially I want to group first by fruitType name, then by plantedIn country, then by harvestMethod name, and finally product id.
After some research I found that lodash is popular for this kind of grouping. I developed a very naive solution with their chain, groupBy and mapValues methods like so:
const _ = require('lodash');
const groupedData = _.chain(data)
.groupBy('fruitType.name')
.mapValues(values => _.chain(values)
.groupBy('plantedIn.country')
.mapValues(values => _.chain(values)
.groupBy('harvestMethod.name')
.mapValues(values => _.chain(values)
.groupBy('product.id')
.value()
).value()
).value()
).value()
This solution, however functional, feels very verbose and is likely inefficient. Therefore I would like to ask if there is any better alternative, either with loadash or any other way.
You could take an array of function to get the keys and build the structure.
const
data = [{ fruitType: { name: "bananas" }, plantedIn: { country: "USA", state: "Minnesota" }, harvestMethod: { name: "manual" }, product: { id: "841023781723" } }],
keys = [o => o.fruitType.name, o => o.plantedIn.country, o => o.harvestMethod.name, o => o.product.id],
result = data.reduce((r, o) => {
keys.reduce(
(q, fn, i, { length }) => q[fn(o)] ??= i + 1 === length ? o : {},
r
);
return r;
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
There is already an answer but here is maybe a more readable solution
const grouped = items.reduce((acumm, current, index) => {
acumm[current.fruitType.name] = {
[current.plantedIn.country]: {
[current.harvestMethod.name]: {
[current.product.id]: current,
...(acumm[current.fruitType.name]?.
[current.plantedIn.country]?.
[current.harvestMethod.name] ?? {}
),
},
},
};
return acumm;
}, {});
Stackblitz Example
Related
I have a array as follows:
data = [
{
"id":1
"name":"london"
},
{
"id":2
"name":"paris"
},
{
"id":3
"name":"london"
},
{
"id":4
"name":"paris"
},
{
"id":5
"name":"australia"
},
{
"id":6
"name":"newzearland"
}
]
At runtime this array can have n number of elements. I want to group this array with respect to name attribute. All the elements with same name should be moved to a separate array. I don't know the what value can name have in advance. This is coming at runtime. For example, from above array I want final output as follows:
output:
newArray1 = [
{
"id":1
"name":"london"
},
{
"id":3
"name":"london"
}
]
newArray2 = [
{
"id":2
"name":"paris"
},
{
"id":4
"name":"paris"
}
]
newArray3 = [
{
"id":5
"name":"australia"
}
]
newArray4 = [
{
"id":6
"name":"newzearland"
}
]
How can I do that?
As Teemu has already pointed out in a comment, creating new variables to store the data is not ideal. You would have no way of knowing how many groups you've created and using variables that you can't be sure exist is not the best way to write code. Fortunately, JavaScript has objects, which can store data like this in a much cleaner way. Here's the code I've come up with:
function groupBy(arr, key) {
let res = {}
for (let element of arr) {
if (res.hasOwnProperty(element[key])) {
res[element[key]].push(element)
} else {
res[element[key]] = [element]
}
}
return res
}
This code is not the best, most efficient code ever, but it is written to be easier to understand for someone still learning. This code loops over every element in your data and checks whether our result already contains an array for elements with that name. If there's already an array for elements with that name, the current element is added to it. If there isn't one, a new one is created with the current element inside it. To do exactly what you want, you'd call this function with groupBy(data, "name") and assign it to a new variable like groupedData (THIS DOES NOT MODIFY THE DATA, IT RETURNS A NEW OBJECT OF GROUPED DATA) .
Start by getting all the unique .names, then map them to the original array filtered by each .name:
const data = [{
"id": 1, "name": "london"
},
{
"id": 2, "name": "paris"
},
{
"id": 3, "name": "london"
},
{
"id": 4, "name": "paris"
},
{
"id": 5, "name": "australia"
},
{
"id": 6, "name": "newzearland"
}
];
const newData = [...new Set(data
//Get all names in an array
.map(({name}) => name))]
//For each name filter original array by name
.map(n => data.filter(({name}) => n === name));
console.log( newData );
//OUTPUT: [newArray1, newArray2, .....]
You can get the expected result with grouping by key approach.
const data = [{"id":1,"name":"london"},{"id":2,"name":"paris"},{"id":3,"name":"london"},{"id":4,"name":"paris"},{"id":5,"name":"australia"},{"id":6,"name":"newzearland"}];
const result = Object.values(data.reduce((acc, obj) =>
({ ...acc, [obj.name]: [...(acc[obj.name] ?? []), obj] }), {}));
console.log(result);
const [newArray1, newArray2, newArray3, newArray4, ...rest] = result;
console.log('newArray1:', newArray1);
console.log('newArray2:', newArray2);
console.log('newArray3:', newArray3);
console.log('newArray4:', newArray4);
.as-console-wrapper{min-height: 100%!important; top: 0}
Restructuring array of objects to new array
Problem
There’s an array of objects that contains plain strings and might contain nested arrays as well. We want to create a new Array that will contain a node for each item in the array and separate nodes for each array item connected to its parent. Each parent node should have the following structure:
{
id: uuidv4(),
position: { x: 0, y: 0 },
data: { label: <item data goes here> }
}
Each array node with the following schema above, should also have a connection edge item added to the array with the following properties:
{
id: ‘e<array item Id>-<parentId>’,
source: <array item Id>,
target: <parentId>,
}
Example
We have the following array of objects for example:
[
{
"author": "John Doe",
"age": 26,
"books": [
{
"title": "Book 1"
},
{
"title": "Book 2",
"chapters": [
{
"title": "No Way Home",
"page": 256
}
]
}
]
}
]
The expected output is:
[
{
"id": "1",
"data": {
"label": {
"author": "John Doe",
"age": 26,
}
}
},
{
"id": "2",
"data": {
"label": "books" // key of array
}
},
{
"id": "3",
"data": {
"label": {
"title": "Book 1"
}
}
},
{
"id": "4",
"data": {
"label": {
"title": "Book 2"
}
}
},
{
"id": "5",
"data": {
"label": "chapters" // key of array
}
},
{
"id": "6",
"data": {
"label": {
"title": "No Way Home",
"page": 256
}
}
},
{
"id": "e2-1",
"source": "2",
"target": "1"
},
{
"id": "e3-2",
"source": "3",
"target": "2"
},
{
"id": "e4-2",
"source": "4",
"target": "2"
},
{
"id": "e5-4",
"source": "5",
"target": "4"
},
{
"id": "e6-5",
"source": "6",
"target": "5"
}
]
First of all, I would not be answering if there was not already a good answer. Please, on StackOverflow, always show your own attempts and explain where you got stuck. But since there is already an answer, I think this version might be a bit simpler.
Second, I'm assuming this output format is some sort of directed graph, that the first half is your list of vertices and the second half a list of edges. If so I don't know if your output format is constrained here. But if you had the option, I would think a better structure would be an object with vertices and edges properties, each containing an array. You might then not need the edges' ids. And the code could also be simplified.
This version first converts to an intermediate structure like this:
[
{id: "1", data: {label: {author: "John Doe", age: 26}}, children: [
{id: "2", data: {label: "books"}, children: [
{id: "3", data: {label: {title: "Book 1"}}, children: []},
{id: "4", data: {label: {title: "Book 2"}}, children: [
{id: "5", data: {label: "chapters"}, children: [
{id: "6", data: {label: {title: "No Way Home"}}, children: []}
]}
]}
]}
]}
]
Then we flatten that structure into the first section of the output and use it to calculate the relationships (edges?) between nested nodes to go in the second section.
The code looks like this:
const transform = (input) => {
const extract = (os, nextId = ((id) => () => String (++ id)) (0)) => os .map ((o) => ({
id: nextId(),
data: {label: Object .fromEntries (Object .entries (o) .filter (([k, v]) => !Array .isArray (v)))},
children: Object .entries (o) .filter (([k, v]) => Array .isArray (v)) .flatMap (([k, v]) => [
{id: nextId(), data: {label: k}, children: extract (v, nextId)},
])
}))
const relationships = (xs) =>
xs .flatMap (({id: target, children = []}) => [
... children .map (({id: source}) => ({id: `e${source}-${target}`, source, target})),
... relationships (children),
])
const flatten = (xs) =>
xs .flatMap (({children, ...rest}) => [rest, ... flatten (children)])
const res = extract (input)
return [...flatten (res), ... relationships (res)]
}
const input = [{author: "John Doe", age : 26, books: [{title: "Book 1"}, {title: "Book 2", chapters: [{title: "No Way Home", page: 256}]}]}]
console .log (transform (input))
.as-console-wrapper {max-height: 100% !important; top: 0}
We use three separate recursive functions. One does the recursive extract into that intermediate format. Along the way, it adds id nodes using a nextId stateful function (something I usually avoid, but seems to simplify things here.) Then flatten simply recursively lifts the children to sit alongside their parents. And relationships (again recursively) uses the ids of the parent- and child-nodes to add an edge node.
Using these three separate recursive calls is probably less efficient than some other solutions, but I think it leads to much cleaner code.
One has to choose a self recursive approach which in a generic way can process both, array-items and object-entries. Also, while the recursive process takes place, one not only has to create and collect the consecutively/serially numbered (the incremented id value) data nodes, but one in addition needs to keep track of every data node's parent reference in order to finally concatenate the list of edge items (as the OP calls it) to the list of data nodes.
function flattenStructureRecursively(source = [], result = [], tracker = {}) {
let {
parent = null, edgeItems = [],
getId = (id => (() => ++id))(0),
} = tracker;
const createEdgeItem = (id, pid) => ({
id: `e${ id }-${ pid }`,
source: id,
target: pid,
});
const putNodeData = node => {
result.push(node);
if (parent !== null) {
edgeItems.push(createEdgeItem(node.id, parent.id));
}
// every data node is a parent entity too.
parent = node;
};
if (Array.isArray(source)) {
result.push(
...source.flatMap(item =>
flattenStructureRecursively(item, [], {
getId, parent, edgeItems,
})
)
);
} else {
let {
dataNode,
childEntries,
} = Object
.entries(source)
.reduce(({ dataNode, childEntries }, [key, value]) => {
if (value && (Array.isArray(value) || (typeof value === 'object'))) {
// collect any object's iterable properties.
childEntries.push([key, value]);
} else {
// aggregate any object's non iterable
// properties at data node level.
(dataNode ??= {
id: getId(),
data: { label: {} }
}).data.label[key] = value;
}
return { dataNode, childEntries };
}, { dataNode: null, childEntries: [] });
if (dataNode !== null) {
putNodeData(dataNode);
}
childEntries
.forEach(([key, value]) => {
// every object's iterable property is supposed
// to be created as an own parent entity.
dataNode = {
id: getId(),
data: { label: key },
};
putNodeData(dataNode);
result.push(
...flattenStructureRecursively(value, [], {
getId, parent, edgeItems,
})
);
});
}
if (parent === null) {
// append all additionally collected edge items
// in the end of all the recursion.
result.push(...edgeItems);
}
return result;
}
console.log(
flattenStructureRecursively([{
author: "John Doe",
pseudonym: "J.D.",
books: [{
title: "Book 1",
}, {
title: "Book 2",
chapters: [{
title: "No Way Home",
page: 256,
}],
}],
age: 26,
}])
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
This question already has answers here:
Most efficient method to groupby on an array of objects
(58 answers)
Closed 3 years ago.
I have an array of objects that looks like this:
let stuff = [
{
"id": "48202847",
"name": "Doe"
},
{
"id": "17508",
"name": "Marie"
},
{
"id": "175796",
"name": "Robert"
},
{
"id": "175796",
"name": "Ronald"
},
]
What I want to get is a dictionary looking something like this:
{
"D": [{"id": "48202847", "name": "Doe"}],
"M": [{"id": "17508", "name": "Marie"}],
"R": [{"id": "175796", "name": "Robert"}, {"id": "175796", "name": "Ronald"}]
}
Notice how all the people whose name starts with "R" are listed under one key.
This is my function that creates a dictionary with the person's name as the key:
const byId = (array) =>
array.reduce((obj, item) => {
obj[item.name] = item
return obj
}, {})
But this obviously doesn't do what I want it to. I do have some ideas of how to make this possible, but they are extremely legacy and I would love to know how to do this right.
Any help is appreciated!
You need the first character, uppercase and an array for collecting the objects.
const byId = array =>
array.reduce((obj, item) => {
var key = item.name[0].toUpperCase(); // take first character, uppercase
obj[key] = obj[key] || []; // create array if not exists
obj[key].push(item); // push item
return obj
}, {});
let stuff = [{ id: "48202847", name: "Doe" }, { id: "17508", name: "Marie" }, { id: "175796", name: "Robert" }, { id: "175796", name: "Ronald" }],
result = byId(stuff)
console.log(result);
Here's a solution based on Set, map, reduce and filter:
let stuff = [{"id": "48202847","name": "Doe"},{"id": "17508","name": "Marie"},{"id": "175796","name": "Robert"},{"id": "175796","name": "Ronald"}];
let result = [...new Set(stuff.map(x => x.name[0]))]
.reduce((acc, val) => {
return acc = { ...acc,
[val]: stuff.filter(x => x.name.startsWith(val))
}
}, {});
console.log(result);
Great solution Nina! Could be made a little cleaner by utilizing the spread operator.
const byId = (array) =>
array.reduce((obj, item) => {
var key = item.name[0].toUpperCase();
return {
...obj,
[key]: obj[key] ? [...obj[key], item] : [item],
}
}, {});
This is the input data in array named candidatesArray:
[
{"name":"george","languages":["php","javascript","java"],"age":19,"graduate_date":1044064800000,"phone":"32-991-511"},
{"name":"anna","languages":["java","javascript"],"age":23,"graduate_date":1391220000000,"phone":"32-991-512"},
{"name":"hailee","languages":["regex","javascript","perl","go","java"],"age":31,"graduate_date":1296525600000,"phone":"32-991-513"}
]
I need to transform in this collection as a result of the function:
{candidates: [
{name: "George", age: 19, phone: "32-991-511"},
{name: "Hailee", age: 31, phone: "32-991-513"},
{name: "Anna", age: 23, phone: "32-991-512"}
],
languages: [
{lang:"javascript",count:1},
{lang:"java", count:2},
{lang:"php", count:2},
{lang:"regex", count:1}
]}
The function repCandidates:
const reportCandidates = (candidatesArray) => {
return repObject}
I need to write it in javascript ES6
I shouldn't use loops(for, while, repeat) but foreach is allowed and it could be better if I use "reduce" function
The candidates should be return by their name, age and phone organized by their graduate_date.
The languages should be returned with their counter in alphabetic order .
Visit https://codepen.io/rillervincci/pen/NEyMoV?editors=0010 to see my code, please.
One option would be to first reduce into the candidates subobject, while pushing the langauges of each to an array.
After iterating, sort the candidates and remove the graduate_date property from each candidate, then use reduce again to transform the languages array into one indexed by language, incrementing the count property each time:
const input = [{
"name": "george",
"languages": ["php", "javascript", "java"],
"age": 19,
"graduate_date": 1044064800000,
"phone": "32-991-511"
}, {
"name": "anna",
"languages": ["java", "javascript"],
"age": 23,
"graduate_date": 1391220000000,
"phone": "32-991-512"
}, {
"name": "hailee",
"languages": ["regex", "javascript", "perl", "go", "java"],
"age": 31,
"graduate_date": 1296525600000,
"phone": "32-991-513"
}];
const output = input.reduce((a, { languages, ...rest }) => {
a.candidates.push(rest);
a.languages.push(...languages);
return a;
}, { candidates: [], languages: [] });
output.candidates.sort((a, b) => a.graduate_date - b.graduate_date);
output.candidates.forEach(candidate => delete candidate.graduate_date);
output.languages = Object.values(
output.languages.reduce((a, lang) => {
if (!a[lang]) a[lang] = { lang, count: 0 };
a[lang].count++;
return a;
}, {})
);
output.languages.sort((a, b) => a.lang.localeCompare(b.lang));
console.log(output);
It's common practice to do everything in a reduce(), but sometimes it's easier to read if you break it up a bit. This creates a counter object as a helper to to track the language counts. map()s over the array to pull out the languages and personal info and then puts it all together:
let arr = [ {"name":"george","languages":["php","javascript","java"],"age":19,"graduate_date":1044064800000,"phone":"32-991-511"},{"name":"anna","languages":["java","javascript"],"age":23,"graduate_date":1391220000000,"phone":"32-991-512"},{"name":"hailee","languages":["regex","javascript","perl","go","java"],"age":31,"graduate_date":1296525600000,"phone":"32-991-513"}]
let lang_counter = {
// helper keeps counts of unique items
counts:{},
add(arr){
arr.forEach(item => this.counts[item] = this.counts[item] ? this.counts[item] + 1 : 1)
},
toarray(){
return Object.entries(this.counts).map(([key, val]) => ({[key]: val}))
}
}
// iterate over object to create candidates
let candidates = arr.map(row => {
let {languages, ...person} = row
lang_counter.add(languages) // side effect
return person
})
// put them together
console.log({candidates, languages:lang_counter.toarray()})
You can use Array.reduce and Object.values like below
let arr = [{"name":"george","languages":["php","javascript","java"],"age":19,"graduate_date":1044064800000,"phone":"32-991-511"},{"name":"anna","languages":["java","javascript"],"age":23,"graduate_date":1391220000000,"phone":"32-991-512"},{"name":"hailee","languages":["regex","javascript","perl","go","java"],"age":31,"graduate_date":1296525600000,"phone":"32-991-513"}]
let res = arr.reduce((o, {name, age, phone, graduate_date, languages}) => {
o.candidates.push({name, age, phone, graduate_date})
languages.forEach(l => {
o.languages[l] = o.languages[l] || { lang:l, count: 0 }
o.languages[l].count++
})
return o
}
, { candidates: [], languages: {}})
res.candidates = res.candidates.sort((a,b) => a.graduate_date - b.graduate_date)
.map(({ graduate_date, ...rest }) => rest)
res.languages = Object.values(res.languages).sort((a,b) => a.lang.localeCompare(b.lang))
console.log(res)
I have an array like this:
const peopleArray = [
{
"id": "Antoine",
"country": "France"
},
{
"id": "Alejandro",
"country": "Spain"
}
]
That I would like to represent as on object like this (note that id is not a property):
{
"Antoine": {
"country": "France"
},
"Alejandro": {
"country": "Spain"
}
}
So far I've found I can do this (elegant!):
peopleArray.reduce( (ac, p) => ({...ac, [p.id]: p }), {} )
Which produces:
{
"Antoine": {
"id": "Antoine",
"country": "France"
},
"Alejandro": {
"id": "Alejandro",
"country": "Spain"
}
}
I'm at a loss of how to accomplish the same in a terse/elegant way such that id is omitted.
Looking for a pure javascript es2017 solution that works with Node.js version 8.11.1.
If you want to make and object with id as key and remaining object as value. You can try following using Rest Parameters
ES2017
const peopleArray = [{"id": "Antoine","country": "France"},{"id": "Alejandro","country": "Spain"}];
const result = peopleArray.reduce( (ac, o) => {
ac[o.id] = Object.assign({}, o);
delete ac[o.id].id;
return ac;
}, {});
console.log(result);
ES2018 - Will be able to use Rest Parameters for objects
const peopleArray = [{"id": "Antoine","country": "France"},{"id": "Alejandro","country": "Spain"}];
const result = peopleArray.reduce( (ac, {id, ...rest}) => Object.assign(ac, {[id]: rest}), {} );
console.log(result);
You can do :
peopleArray.reduce( (ac, p) => ({...ac, [p.id]: { country : p.country } }), {} )