Related
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 is my data with 5 arrays. What I wish to achieve is to combine id and name and the new array should have 5 different playname values. It can be in either an array or new key like playername1.
[
{
"id": 1,
"name": "Liquid",
"playername": "GH",
},
{
"id": 1,
"name": "Liquid",
"playername": "KuroKy",
},
{
"id": 1,
"name": "Liquid",
"playername": "Miracle",
},
{
"id": 1,
"name": "Liquid",
"playername": "w33",
},
{
"id": 1,
"name": "Liquid",
"playername": "Mind-Control",
}
]
I am using lodash to try and achieve this but I am not able to get the data format I want using the code examples I have searched online.
This is my current code that I have tried that gives an array that is grouped by the ID.
_.forOwn(this.state.teamsData, function(value, key) {
console.log(value);
});
The original data are not grouped by ID.
I am trying to get my data to look like this {"id": 1, "name": liquid, "playername": "GH", "playername2": "KuroKy" ....}
You could group by id and name properity and store the index for the same group.
var data = [{ id: 1, name: "Liquid", playername: "GH" }, { id: 1, name: "Liquid", playername: "KuroKy" }, { id: 1, name: "Liquid", playername: "Miracle" }, { id: 1, name: "Liquid", playername: "w33" }, { id: 1, name: "Liquid", playername: "Mind-Control" }],
result = Object
.values(data.reduce((r, { id, name, playername }) => {
var key = [id, name].join('|');
r[key] = r[key] || { data: { id, name }, index: 0 };
r[key].data['playername' + (r[key].index++ || '')] = playername;
return r;
}, {}))
.map(({ data }) => data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Group by a combination of id and name (${o.id}~~~${o.name}). Map the groups, extract the name and id from the 1st item, take the player names, and use _.mapKeys() to convert the indexes to object keys. Combine the id, name, and playername properties to a single object using spread.
const teamsData = [{"id":1,"name":"Liquid","playername":"GH"},{"id":1,"name":"Liquid","playername":"KuroKy"},{"id":1,"name":"Liquid","playername":"Miracle"},{"id":1,"name":"Liquid","playername":"w33"},{"id":1,"name":"Liquid","playername":"Mind-Control"}]
const result = _(teamsData)
.groupBy(o => `${o.id}~~~${o.name}`) // group by id and name
.map(group => ({ // map the groups
..._.pick(_.head(group), ['id', 'name']), // take id and name from 1st item
..._.mapKeys(_.map(group, 'playername'), // extract the player names
(v, k) => `playername${+k > 0 ? +k + 1 : ''}` // create the keys
)
}))
.value()
console.log(result)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
You can use reduce and Map. Here Map is used to keep track of number of palyername is used for particular id
const arr = [{"id":1,"name":"Liquid","playername":"GH"},{"id":1,"name":"Liquid","playername":"KuroKy"},{"id":1,"name":"Liquid","playername":"Miracle"},{"id":1,"name":"Liquid","playername":"w33"},{"id":1,"name":"Liquid","playername":"Mind-Control"}]
let groupData = (arr) => {
let mapper = new Map()
return Object.values(arr.reduce((op,{id, name, playername }) => {
mapper.set(id, ( mapper.get(id) || 0) + 1 )
let key = mapper.get(id)
op[id] = op[id] || { id, name }
op[id][`playername${key}`] = playername
return op
},{}))
}
console.log(groupData(arr))
Just using using reduce to group the array into an object and Object.values to convert the object into an array.
let list = [
{"id": 1,"name": "Liquid","playername": "GH",},
{"id": 2,"name": "Solid","playername": "KuroKy",},
{"id": 1,"name": "Liquid","playername": "Miracle",},
{"id": 1,"name": "Liquid","playername": "w33",},
{"id": 2,"name": "Solid","playername": "Mind-Control",}
];
let counter = {};
let result = Object.values(list.reduce((c, v) => {
if (!c[v.id]) {
counter[v.id] = 0;
c[v.id] = {...v};
} else c[v.id]["playername" + ++counter[v.id]] = v.playername;
return c;
}, {}));
console.log(result);
The data could have been structured better. With the given structure, following code is one way to solve it.
let data = [
{"id": 1,"name": "Liquid","playername": "GH",},
{"id": 2,"name": "Solid","playername": "KuroKy",},
{"id": 1,"name": "Liquid","playername": "Miracle",},
{"id": 1,"name": "Liquid","playername": "w33",},
{"id": 2,"name": "Solid","playername": "Mind-Control",}
]; // Your data
let new_data = {}; // New structured data
data.map(function(data_object) {
let team = new_data['id'+data_object.id];
if(team==null) {
// Creates a new object in new_data if an object
// for the id does not exists.
new_data['id'+data_object.id] = team = {};
team.players = [];
}
team.id = data_object.id;
team.name = data_object.name;
team.players.push(data_object.playername);
});
console.log(new_data);
With this code, you will have a new_data object of format
{
id1 : {
id : 1,
name : Liquid,
players : ['GH', 'Miracle', 'w33']
},
id2 : {
id : 2,
name : Solid,
players : ['Kuroky', 'Mind-control']
}
}
my backend service send me list of node as an array. but I need is, each next node is value of its previous node(SEE EXAMPLE). I want whole list as nested object in singe object.
WHAT I HAVE:
[
{
"nodeId": 1,
},
{
"nodeId": 3,
},
{
"nodeId": 16,
}
]
WHAT I NEED:
[
{
"nodeId": 1,
"staticChild": [
{
"nodeId": 3,
"staticChild": [
{
"nodeId": 16,
}
]
}
]
}
]
You could reduce the array from the right side and build a new object with a staticChild property.
var array = [{ nodeId: 1 }, { nodeId: 3 }, { nodeId: 16 }],
result = array.reduceRight((a, b) => ({ ...b, staticChild: [a] }));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Based on the input / output you provided, you can use a recursive funtion like :
const data = [{
nodeId: 1
},
{
nodeId: 3
},
{
nodeId: 16
}
];
const transform = data => {
const [node, ...rest] = data;
if (rest.length > 0) {
return {
...node,
staticChild: [transform(rest)]
};
} else {
return {
...node,
hasChildren: false
};
}
};
const result = transform(data);
console.log(result);
At first reverse the array and the make an iteration over the revered array using reduce() to make your desire format.
let data = [{"nodeId": 1},{"nodeId": 3},{"nodeId": 16}]
data = data.reverse().reduce((old, cur) => {
if (!old.length) {
old = [cur]
} else {
cur['staticChild'] = old
old = [cur]
}
return old
}, [])
console.log(data)
You can use the reduceRight() array method to perform the transformation.
const data = [{
"nodeId": 1,
},
{
"nodeId": 3,
},
{
"nodeId": 16,
}
]
const nested = data.reduceRight((acc, item) => {
return [ { ...item, staticChild: acc } ]
}, []);
console.log(nested);
Or more succinctly:
const nested = data.reduceRight((acc, item) => [ { ...item, staticChild: acc } ],[]);
I have a React component which is access JSON data for populate a tree component. The tree is showing nodes and ports. Here is a sample from the JSON:
"outputs": {
"graph": {
"nodes":[ {
"name":"nlabme3400",
"ports":[ {
"name": "GigabitEthernet 0/2", "id": "5bd350c7-d15b-4f8b-be70-18eda2bfe41a"
}
,
{
"name": "FastEthernet 0/19", "id": "5bd350c7-762d-4462-984b-e6f0a9edb6c7"
}
,
{
"name": "FastEthernet 0/21", "id": "5bd350c7-2927-43db-ae43-119b12636de6"
}
],
"id":"5bd350bf-8515-4dc2-9b12-16b221505593"
}
I have all of this information coming in to my component via the following axios get call:
axios.get('StepThreeFinalData.json').then(response => {
const nodess = response.data.outputs.graph.nodes.map(({id, name,
...children}) => ({value: id, label: name, children: children.ports}));
The output is working perfectly. However, the challenge is that I need to change the "name" and "id" tags in the children array to "label" and "value", respectively, because otherwise the label will not show up in the tree component. Not sure how to do this. Please help!
const ports = [ {
"name": "GigabitEthernet 0/2", "id": "5bd350c7-d15b-4f8b-be70-18eda2bfe41a"
}
,
{
"name": "FastEthernet 0/19", "id": "5bd350c7-762d-4462-984b-e6f0a9edb6c7"
}
,
{
"name": "FastEthernet 0/21", "id": "5bd350c7-2927-43db-ae43-119b12636de6"
}
]
const update_ports = (ports) => ports.map(({ id, name }) => {
return { label: name, value: id }
})
console.log(update_ports(ports)) // The new ports with the new keys and values.
You can use the map function and return new array of objects with new keys and values in each item in the array.
axios.get('StepThreeFinalData.json').then(response => {
const nodess = response.data.outputs.graph.nodes.map(({id, name,
...children}) => ({value: id, label: name, children: update_ports(children.ports)}));
Notice i've called to update_ports in your axios success.
const test = {
"outputs": {
"graph": {
"nodes": [{
"name":"nlabme3400",
"ports": [
{
"name": "GigabitEthernet 0/2", "id": "5bd350c7-d15b-4f8b-be70-18eda2bfe41a"
},
{
"name": "FastEthernet 0/19", "id": "5bd350c7-762d-4462-984b-e6f0a9edb6c7"
},
{
"name": "FastEthernet 0/21", "id": "5bd350c7-2927-43db-ae43-119b12636de6"
}
],
"id":"5bd350bf-8515-4dc2-9b12-16b221505593"
}]
}
}
};
const test2 = test.outputs.graph.nodes.map(({name, ports, id}) => ({
name,
id,
ports: ports.map(({name, id}) => ({
label: name,
value: id
}))
}));
console.log(test2);
Read more about map, filter, reduce, that will save your life
axios.get('StepThreeFinalData.json').then(response => {
const nodess = response.data.outputs.graph.nodes.map(({id, name,
...children}) => ({value: id, label: name, children: update_ports(children.ports)}));
I have an array of data. Some of the key in the array are same. I would like to create a new array based on the key and add the other data.
This is my array
var myObjOne = [
{
"name":"John",
"id":1,
"car":"maruti"
},
{
"name":"John",
"id":2,
"car":"wolks"
},
{
"name":"John",
"id":3,
"car":"bmw"
},
{
"name":"Peter",
"id":4,
"car":"alto"
},
{
"name":"Peter",
"id":5,
"car":"swift"
}
];
I would like to convert the array in to the below format.
var myObj = [
{
"name":"John",
"items": [
{ "id":1, "car":"maruti" },
{ "id":2, "car":"wolks" },
{ "id":3, "car":"bmw" }
]},
{
"name":"Peter",
"items": [
{ "id":4, "car":"alto" },
{ "id":5, "car":"swift" },
]
}
];
I am working on a node environment.
You can create an object using Array#reduce first which maps name with items, and then create the final array by looping over the intermediate map using a for...of loop:
var source = [{"name":"John","id":1,"car":"maruti"},{"name":"John","id":2,"car":"wolks"},{"name":"John","id":3,"car":"bmw"},{"name":"Peter","id":4,"cars":"alto"},{"name":"Peter","id":5,"cars":"swift"}];
const map = source.reduce((acc, {name, ...obj}) => {
if (!acc[name]) {
acc[name] = [];
}
acc[name].push(obj);
return acc;
}, {});
const result = [];
for (let[name, items] of Object.entries(map)) {
result.push({name, items});
}
console.log(result);
Array.reduce is at rescue.This method accepts an accumulator and current
item. Check in the accumulator if there exist an object where the value of name property is John or Peter
var myObjOne = [{
"name": "John",
"id": 1,
"car": "maruti"
},
{
"name": "John",
"id": 2,
"car": "wolks"
},
{
"name": "John",
"id": 3,
"car": "bmw"
},
{
"name": "Peter",
"id": 4,
"car": "alto"
},
{
"name": "Peter",
"id": 5,
"car": "swift"
}
];
var newObj = myObjOne.reduce(function(acc, curr, currIndex) {
// using findIndex to check if there exist an object
// where the value of the name property is John, Peter
// if it exist it will return the index else it will return -1
let ifNameExist = acc.findIndex(function(item) {
return item.name === curr.name;
})
// if -1 then create a object with name and item property and push
// it to the accumulator
if (ifNameExist === -1) {
let nameObj = {};
nameObj.name = curr.name;
nameObj.items = [];
nameObj.items.push({
id: curr.id,
car: curr.car
})
acc.push(nameObj)
} else {
// if such an object already exist then just update the item array
acc[ifNameExist].items.push({
id: curr.id,
car: curr.car
})
}
return acc;
}, []);
console.log(newObj)
Use .reduce to group by name, and use .find inside the reducer to find if the matching name has already been added:
const input=[{"name":"John","id":1,"car":"maruti"},{"name":"John","id":2,"car":"wolks"},{"name":"John","id":3,"car":"bmw"},{"name":"Peter","id":4,"cars":"alto"},{"name":"Peter","id":5,"cars":"swift"}]
const output = input.reduce((a, { name, ...item }) => {
const foundNameObj = a.find(nameObj => nameObj.name === name);
if (foundNameObj) foundNameObj.items.push(item);
else a.push({ name, items: [item] });
return a;
}, []);
console.log(output);