How to convert an array into a hierarchical array - javascript

I have some data which is
var currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
The data is flat and I need to convert it into something like:
{
'ticket': 'CAP',
children : [{
'ticket' : 'CT-1',
'children' : [{
'ticket' : 'CT-1-A',
'children' : []
}, {
'ticket' : 'CT-1-B',
'children' : []
}],
[{
'ticket' : 'CT-2',
'children' : []
}]
}]
}
(I think the above is valid)?
I'm very lost as to how. I am going to show my effort but, I'm not sure if my approach is correct or not.
var currentData = [{'ticket':'cap', 'child':'CT-1'},{'ticket':'cap', 'child':'CT-2'}, {'ticket':'CT-1', 'child':'CT-1-A'},{'ticket':'CT-1', 'child':'CT-1-B'}];
var newList = [];
function convert(list){
if (newList.length <= 0){
var child = [];
var emptyChild = [];
child.push({'ticket': list[0].child, 'child': emptyChild });
newList.push({'ticket': list[0].ticket, 'children' : child});
list.splice(0,1);
} // the if statement above works fine
for(var i = 0; i < list.length; i++) {
var ticket = list[i].ticket;
for(var j = 0; j < newList.length; j++) {
if (newList[j].ticket == ticket){
var child;
var emptyChild = [];
child = {'ticket': list[i].child, 'child': emptyChild };
newList[j].children.push(child);
list.splice(i,1);
break;
} // the if above works
else{
var child2 = getFromChildren(ticket, newList, list[i]); // child2 is Always null, even if getFromChildren returns an object
newList[j].children.push(child2);
list.splice(i,1);
break;
}
}
}
if (list.length > 0){
convert(list);
}
}
function getFromChildren(ticket, list, itemToAdd){
if (list == null || list[0].children == null)
return;
for(var i = 0; i < list.length; i++) {
if (list[i] == null)
return;
if (list[i].ticket == ticket){
list[i].child.push(itemToAdd.child); // ** can't do this, javascript passes by value, not by reference :(
} else{
getFromChildren(ticket, list[i].children, itemToAdd);
}
}
}
convert(currentData);
I think I've made a mess of it. In the comments I've put a ** explaining that it isn't working due to JavaScript not passing by reference, however upon further reading I don't think that is correct as I'm passing the object which is by reference?
Edit
The data, shown with currentData will not always start at the root sadly either

function convert(arr) {
var children = {}; // this object will hold a reference to all children arrays
var res = arr.reduce(function(res, o) { // for each object o in the array arr
if(!res[o.ticket]) { // if there is no object for the element o.ticket
res[o.ticket] = {ticket: o.ticket, children: []}; // then creates an object for it
children[o.ticket] = res[o.ticket].children; // and store a reference to its children array
}
if(!res[o.child]) { // if there is no object for the element o.child
res[o.child] = {ticket: o.child, children: []}; // then creates an object for it
children[o.child] = res[o.child].children; // and store a reference to its children array
}
return res;
}, {});
arr.forEach(function(o) { // now for each object o in the array arr
children[o.ticket].push(res[o.child]); // add the object of o.child (from res) to its children array
delete res[o.child]; // and remove the child object from the object res
});
return res;
}
var currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
console.log(convert(currentData));
Explanation:
The reduce part creates an object of the form: { ticket: "...", children: [] } for each element (child or not). So right after reduce, the object res will be:
res = {
'CAP': { ticket: 'CAP', children: [] },
'CT-1': { ticket: 'CT-1', children: [] },
'CT-2': { ticket: 'CT-2', children: [] },
'CT-1-A': { ticket: 'CT-1-A', children: [] },
'CT-1-B': { ticket: 'CT-1-B', children: [] },
}
Now comes the forEach bit which loops over the array once more, and now for each object it fetches the object of .child from res above, push it into .ticket object's children (which a reference to it is stored in children object), then remove the .child object from the object res.

Below uses reduce to get the data grouped to a Map, then I convert the data to an object like you've shown above. You'll need a modern browser to run below snippet, or use a transpiler like babeljs to convert it to es5 syntax.
let currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
let children = currentData.map(e => e.child);
currentData.sort((a,b) => children.indexOf(a.ticket));
let res = currentData.reduce((a,b) => {
if (! children.includes(b.ticket)) {
return a.set(b.ticket, (a.get(b.ticket) || [])
.concat({ticket: b.child,
children: currentData
.filter(el => el.ticket === b.child)
.map(el => ({ticket: el.child, children: []}))}))
}
return a;
}, new Map);
let r = {};
for (let [key,value] of res.entries()) {
r.ticket = key;
r.children = value;
}
console.log(r);

Solution using recursion, starting node can be changed.
var currentData = [{'ticket': 'cap','child': 'CT-1'}, {'ticket': 'cap','child': 'CT-2'}, {'ticket': 'CT-1','child': 'CT-1-A'}, {'ticket': 'CT-1','child': 'CT-1-B'}];
function convert(data, start){
return {
ticket: start,
childs: data.filter(d => d.ticket == start)
.reduce((curr, next) => curr.concat([next.child]), [])
.map(c => convert(data, c))
}
}
let result = convert(currentData, 'cap');
console.log(result);
.as-console-wrapper{top: 0; max-height: none!important;}

I would go with a simple for approach, like this:
var currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
var leafs = {};
var roots = {};
var tickets = {};
for(var i=0; i<currentData.length; i++){
var ticket = currentData[i].ticket;
var child = currentData[i].child;
if(!tickets[ticket]){
tickets[ticket] = {ticket:ticket,children:[]};
if(!leafs[ticket]){
roots[ticket] = true;
}
}
if(!tickets[child]){
tickets[child] = {ticket:child,children:[]};
}
delete roots[child];
leafs[child] = true;
tickets[ticket].children.push(tickets[child]);
}
for(var ticket in roots){
console.log(tickets[ticket]);
}

Well, if you are not familiar with reduce, map , forEach with callbacks to iterate, then here is a approach I came with, where the code is flat, storing object references in another map object and iterating exactly once the source array.
The code is much cleaner, if something is understandable add comments I will explain;
var currentData = [
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'},
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'}
];
function buildHierarchy(flatArr) {
let root = {},
nonRoot = {},
tempMap = {};
Object.setPrototypeOf(root, nonRoot);
for (let idx = 0; idx < flatArr.length; idx++) {
let currTicket = flatArr[idx];
let tempTicket = tempMap[currTicket.ticket] || {ticket: currTicket.ticket, children: []};
tempMap[currTicket.ticket] = tempTicket;
if (currTicket.child) {
let tempChild = tempMap[currTicket.child] || {ticket: currTicket.child, children: []};
tempTicket.children.push(tempChild);
tempMap[currTicket.child] = tempChild;
delete root[tempChild.ticket];
nonRoot[tempChild.ticket] = true;
}
root[tempTicket.ticket] = true;
}
return tempMap[Object.keys(root)[0]];
}
console.log(buildHierarchy(currentData));
I have changed the sequence of your source array in order to put the root object anywhere, and the code should work on that.

Related

Transform array into object with custom properties

I have this array
myarr = [
'=title1',
'longText0...',
'longtText1...',
'=title2',
'longTextA...',
'longtTextB...',
'longtTextC...'
];
symbol = indicates that is is a property, next to that is a list of items that belongs to that property
I want to transform that array into object
myObj = {
title1: [
'longText0...',
'longtText1...',
],
title2: [
'longTextA...',
'longtTextB...',
'longtTextC...'
]
}
I come up with this code so far:
const arrayToObject = (array) =>
array.reduce((obj, item) => {
if(item.startsWith('=')) {
const itemName = item.replace('=', '')
obj[itemName] = itemName;
} else {
//add the rest....
}
return obj
}, {})
console.log(arrayToObject(myarr))
My challenges so far is that I am not sure how to turn obj[itemName] so I can assign the items to it. Any ideas how to do that?
A reduce based approach which does not depend on outer scope references for keeping track of the currently to be built/aggregated property makes this information part of the reducer function's first parameter, the previousValue which serves as an accumulator/collector object.
Thus, as for the OP's task, this collector would feature two properties, the currentKey and the result, where the former holds the state of the currently processed property name and the latter being the programmatically built result.
// - reducer function which aggregates entries at time,
// either by creating a new property or by pushing a
// value into the currently processed property value.
// - keeps the state of the currently processed property
// by the accumulator`s/collector's `currentKey` property
// whereas the result programmatically gets build as
// the accumulator`s/collector's `result` property.
function aggregateEntry({ currentKey = null, result = {} }, item) {
const key = (item.startsWith('=') && item.slice(1));
if (
(key !== false) &&
(key !== currentKey)
) {
// keep track of the currently processed property name.
currentKey = key;
// create a new entry (key value pair).
result[currentKey] = [];
} else {
// push value into the currently processed property value.
result[currentKey].push(item);
}
return { currentKey, result };
}
console.log([
'=title1',
'longText0...',
'longtText1...',
'=title2',
'longTextA...',
'longtTextB...',
'longtTextC...',
].reduce(aggregateEntry, { result: {} }).result);
.as-console-wrapper { min-height: 100%!important; top: 0; }
I wouldn't do that with reduce but with a simple for loop, because you have to carry the itemname over multiple iterations
let o = {}, n = '';
for (let k of arr) {
if (k.startsWith('=')) {
n = k.substring(1);
o[n] = []
} else {
o[n].push(k);
}
}
You can of course also do it with reduce, but you have to put the declaration of itemname outside of the callback
let n = '';
let o = arr.reduce((a, c) => {
if (c.startsWith('=')) {
n = c.substring(1);
a[n] = [];
} else {
a[n].push(c);
}
return a;
}, {});
Please be aware, there is no error handling, ie the code assumes your array is well structured and the first element in the array must start with =
The following function will give you the desired results
function arrayToObject(arr)
{
let returnObj={};
for(let i =0; i <arr.length; i++)
{
if(arr[i].startsWith('='))
{
let itemName = arr[i].replace('=','');
returnObj[itemName]=[];
for(let j=i+1; j <arr.length;j++)
{
if(arr[j].startsWith('='))
{
break;
}
else
{
let value = arr[j];
returnObj[itemName].push(value) ;
}
}
}
}
return returnObj;
}
Here a version with reduce
const myarr = [
'=title1',
'longText0...',
'longtText1...',
'=title2',
'longTextA...',
'longtTextB...',
'longtTextC...'
];
const obj = myarr.reduce((res, el) => {
if(el.startsWith('=')){
const key = el.substring(1)
return {
data: {
...res.data,
[key]: [],
},
key
}
}
return {
...res,
data:{
...res.data,
[res.key]: [...res.data[res.key], el]
}
}
}, {
data: {},
key: ''
}).data
console.log(obj)
You don't need to keep the key somewhere separate for a reduce method:
const myarr = ['=title1', 'longText0...', 'longtText1...', '=title2', 'longTextA...', 'longtTextB...', 'longtTextC...'];
const res = Object.fromEntries(
myarr.reduce((acc, item) => {
if(item.startsWith('='))
acc.push([item.substring(1), []]);
else
acc[acc.length - 1]?.[1].push(item);
return acc;
}, [])
);
console.log(JSON.stringify( res ));

Merge 2 arrays keeping inner elements array values

I'm trying to merge 2 objects which contains arrays in one of their elements. I don't achieve the disered result when using spread syntax and the first object array is being replaced by the second one. The objects are like the following:
const objectA1 = {
keyA1:'valueA1',
keyArr:[{
arrKeyA01:'arrValueA01',
arrKeyA02:'arrValueA02',
},
{
arrKeyA11:'arrValueA11',
arrKeyA12:'arrValueA12',
}
]
}
const objectB1 = {
keyB1:'valueB1',
keyArr:[{
arrKeyB01:'arrValueB01',
arrKeyB02:'arrValueB02',
},
{
arrKeyB11:'arrValueB11',
arrKeyB12:'arrValueB12',
}
]
}
And I want to get:
const objectRes = {
keyA1:'valueA1',
keyB1:'valueB1',
keyArr:[{
arrKeyA01:'arrValueA01',
arrKeyA02:'arrValueA02',
arrKeyB01:'arrValueB01',
arrKeyB02:'arrValueB02',
},
{
arrKeyA11:'arrValueA11',
arrKeyA12:'arrValueA12',
arrKeyB11:'arrValueB11',
arrKeyB12:'arrValueB12',
}
]
}
What I'm using is
{...objectA1 ,...objectB1}
But as said, the keyArr doesn't keep the objectA1 elements.
How I can merge both objects and keep the array data using spread syntax?
Thanks for any comment/help :)
Create an object and place the first 2 values from A1 and B2 object. Customize the array separately by using reduce
const objectA1 = {
keyA1: 'valueA1',
keyArr: [{
arrKeyA01: 'arrValueA01',
arrKeyA02: 'arrValueA02',
},
{
arrKeyA11: 'arrValueA11',
arrKeyA12: 'arrValueA12',
}
]
}
const objectB1 = {
keyB1: 'valueB1',
keyArr: [{
arrKeyB01: 'arrValueB01',
arrKeyB02: 'arrValueB02',
},
{
arrKeyB11: 'arrValueB11',
arrKeyB12: 'arrValueB12',
}
]
}
const arr = objectA1.keyArr.reduce((acc, x) => {
const res1 = objectB1.keyArr.reduce((acc2, y) => ({...x,...y}), {})
return acc = [...acc, res1];
}, [])
const result = {
keyA1: objectA1.keyA1,
keyB1: objectB1.keyB1,
keyArr: arr
}
console.log(result)
I wanted to share my attemp solving this problem, I take the array and merge it in one using loops:
const objectA1 = {
keyA1:'valueA1',
keyArr:[{
arrKeyA01:'arrValueA01',
arrKeyA02:'arrValueA02',
},
{
arrKeyA11:'arrValueA11',
arrKeyA12:'arrValueA12',
}
]
}
const objectB1 = {
keyB1:'valueB1',
keyArr:[{
arrKeyB01:'arrValueB01',
arrKeyB02:'arrValueB02',
},
{
arrKeyB11:'arrValueB11',
arrKeyB12:'arrValueB12',
}
]
}
objects = [objectA1, objectB1];
let i = 0;
new_array = {};
for(i; i < objects.length; i++){
object = objects[i];
keys = Object.keys(object);
for(j = 0; j < keys.length; j++){
//if property already exists, example keyArr
this_key = keys[j];
console.log(this_key);
if(new_array[this_key] != undefined){
//loop through that property in the object
object[this_key].forEach((object_value, index) => {
//add all properties that previous object did not have
Object.assign(new_array[this_key][index], object_value);
});
}else{
//initialize that value with the first element array
new_array[this_key] = object[this_key];
}
}
}
console.log(objects);
console.log(new_array);

Flat array to multi dimensional array (JavaScript)

I have the following array:
var sampleArray = [
"CONTAINER",
"BODY",
"NEWS",
"TITLE"];
I want to have the following output:
var desiredOutput = [{
"CONTAINER": [{
"BODY": [{
"NEWS": [{
"TITLE": []
}]
}]
}]
}];
How can I achieve this in JavaScript?
Already tried with recursive loop, but it does not work, gives me undefined.
dataChange(sampleArray);
function dataChange(data) {
for (var i = 0; i < data.length; i++) {
changeTheArray[data[i]] = data[i + 1];
data.splice(i, 1);
dataChange(changeTheArray[data[i]]);
}
}
Thanks
This does what you're asking for, in one line, and with no additional variables:
let desiredOutput = sampleArray.reduceRight((obj, key) => [ { [key]: obj } ], []);
The reduceRight call, starting from the right hand end of the array, progressively accumulates the current data (seeded with the initial value of []) as the value of the single key in a new object { [key] : _value_ } where that object is itself the single entry in an array [ ... ].
This will do it:
const sampleArray = ["CONTAINER", "BODY", "NEWS", "TITLE"];
const data = []; // Starting element.
let current = data; // Pointer to the current element in the loop
sampleArray.forEach(key => { // For every entry, named `key` in `sampleArray`,
const next = []; // New array
current.push({[key]: next}); // Add `{key: []}` to the current array,
current = next; // Move the pointer to the array we just added.
});
console.log(data);
{[key]: next} is relatively new syntax. They're computed property names.
This:
const a = 'foo';
const b = {[a]: 'bar'};
Is similar to:
const a = 'foo';
const b = {};
b[a] = 'bar';
You could re-write the forEach as a one-liner:
const sampleArray = ["CONTAINER", "BODY", "NEWS", "TITLE"];
const data = []; // Starting element.
let current = data; // Pointer to the current element in the loop
sampleArray.forEach(key => current.push({[key]: current = [] }));
console.log(data);
This current.push works a little counter-intuitively:
Construct a new element to push. This assigns a new value to current.
Push the new element to the reference .push was called on.
That reference is the value of current before current = [].
Hi i made a little demo :
var sampleArray = [
"CONTAINER",
"BODY",
"NEWS",
"TITLE"
],
generateArray = [],
tmp = null;
for(var i = 0; i < sampleArray.length; i++) {
if(tmp===null){
generateArray[sampleArray[i]] = {};
tmp = generateArray[sampleArray[i]];
}else{
tmp[sampleArray[i]] = {};
tmp = tmp[sampleArray[i]];
}
}
console.log(generateArray);

How to convert an array of paths into JSON structure?

I found the question How to convert a file path into treeview?, but I'm not sure how to get the desired result in JavaScript:
I'm trying to turn an array of paths into a JSON tree:
https://jsfiddle.net/tfkdagzv/16/
But my path is being overwritten.
I'm trying to take something like this:
[
'/org/openbmc/path1',
'/org/openbmc/path2',
...
]
... and turn it into...
output = {
org: {
openbmc: {
path1: {},
path2: {}
}
}
}
I'm sure this is pretty easy, but I'm missing something.
const data = [
"/org/openbmc/examples/path0/PythonObj",
"/org/openbmc/UserManager/Group",
"/org/openbmc/HostIpmi/1",
"/org/openbmc/HostServices",
"/org/openbmc/UserManager/Users",
"/org/openbmc/records/events",
"/org/openbmc/examples/path1/SDBusObj",
"/org/openbmc/UserManager/User",
"/org/openbmc/examples/path0/SDBusObj",
"/org/openbmc/examples/path1/PythonObj",
"/org/openbmc/UserManager/Groups",
"/org/openbmc/NetworkManager/Interface"
];
const output = {};
let current;
for (const path of data) {
current = output;
for (const segment of path.split('/')) {
if (segment !== '') {
if (!(segment in current)) {
current[segment] = {};
}
current = current[segment];
}
}
}
console.log(output);
Your solution was close, you just didn't reset the current variable properly. Use this:
current = output;
instead of this:
current = output[path[0]];
This function should do :
var parsePathArray = function() {
var parsed = {};
for(var i = 0; i < paths.length; i++) {
var position = parsed;
var split = paths[i].split('/');
for(var j = 0; j < split.length; j++) {
if(split[j] !== "") {
if(typeof position[split[j]] === 'undefined')
position[split[j]] = {};
position = position[split[j]];
}
}
}
return parsed;
}
Demo
var paths = [
"/org/openbmc/UserManager/Group",
"/org/stackExchange/StackOverflow",
"/org/stackExchange/StackOverflow/Meta",
"/org/stackExchange/Programmers",
"/org/stackExchange/Philosophy",
"/org/stackExchange/Religion/Christianity",
"/org/openbmc/records/events",
"/org/stackExchange/Religion/Hinduism",
"/org/openbmc/HostServices",
"/org/openbmc/UserManager/Users",
"/org/openbmc/records/transactions",
"/org/stackExchange/Religion/Islam",
"/org/openbmc/UserManager/Groups",
"/org/openbmc/NetworkManager/Interface"
];
var parsePathArray = function() {
var parsed = {};
for(var i = 0; i < paths.length; i++) {
var position = parsed;
var split = paths[i].split('/');
for(var j = 0; j < split.length; j++) {
if(split[j] !== "") {
if(typeof position[split[j]] === 'undefined')
position[split[j]] = {};
position = position[split[j]];
}
}
}
return parsed;
}
document.body.innerHTML = '<pre>' +
JSON.stringify(parsePathArray(), null, '\t')
'</pre>';
(see also this Fiddle)
NB: The resulting arrays need to be merged
This method works for both files & directories, and by using only arrays as the data format.
The structure is based upon arrays being folders, the first element being the folder name and the second - the contents array.
Files are just regular strings inside the array (but could easily be objects containing properties)
Converts =>
[
'/home/',
'/home/user/.bashrc',
'/var/',
'/var/test.conf',
'/var/www/',
'/var/www/index.html',
'/var/www/index2.html'
]
To =>
[
['home', [
['user', [
'.bashrc'
]]
]],
['var', [
'test.conf',
['www', [
'index.html',
'index2.html'
]]
]]
]
Script:
var paths = [
'/var/',
'/var/test.conf',
'/var/www/',
'/var/www/index.html',
'/var/www/index2.html'
]
var parsed = []
for (let path of paths) {
let tree = path.split('/')
let previous = parsed
console.groupCollapsed(path)
for (let item in tree) {
const name = tree[item]
const last = item == tree.length - 1
if (name) {
if (last) {
console.log('File:', name)
previous.push(name) - 1
} else {
console.log('Folder:', name)
let i = previous.push([name, []]) - 1
previous = previous[i][1]
}
}
}
console.groupEnd(path)
}
console.warn(JSON.stringify(parsed))

How can I find which index in an array contains an object whose value for a specific key is x? [duplicate]

I would like to find index in array. Positions in array are objects, and I want to filter on their properties. I know which keys I want to filter and their values. Problem is to get index of array which meets the criteria.
For now I made code to filter data and gives me back object data, but not index of array.
var data = [
{
"text":"one","siteid":"1","chid":"default","userid":"8","time":1374156747
},
{
"text":"two","siteid":"1","chid":"default","userid":"7","time":1374156735
}
];
var filterparams = {userid:'7', chid: 'default'};
function getIndexOfArray(thelist, props){
var pnames = _.keys(props)
return _.find(thelist, function(obj){
return _.all(pnames, function(pname){return obj[pname] == props[pname]})
})};
var check = getIndexOfArray(data, filterparams ); // Want to get '2', not key => val
Using Lo-Dash in place of underscore you can do it pretty easily with _.findIndex().
var index = _.findIndex(array, { userid: '7', chid: 'default' })
here is thefiddle hope it helps you
for(var intIndex=0;intIndex < data.length; intIndex++){
eachobj = data[intIndex];
var flag = true;
for (var k in filterparams) {
if (eachobj.hasOwnProperty(k)) {
if(eachobj[k].toString() != filterparams[k].toString()){
flag = false;
}
}
}
if(flag){
alert(intIndex);
}
}
I'm not sure, but I think that this is what you need:
var data = [{
"text":"one","siteid":"1","chid":"default","userid":"8","time":1374156747
}, {
"text":"two","siteid":"1","chid":"default","userid":"7","time":1374156735
}];
var filterparams = {userid:'7', chid: 'default'};
var index = data.indexOf( _.findWhere( data, filterparams ) );
I don't think you need underscore for that just regular ole js - hope this is what you are looking for
var data = [
{
"text":"one","siteid":"1","chid":"default","userid":"8","time":1374156747
},
{
"text":"two","siteid":"1","chid":"default","userid":"7","time":1374156735
}
];
var userid = "userid"
var filterparams = {userid:'7', chid: 'default'};
var index;
for (i=0; i < data.length; i++) {
for (prop in data[i]) {
if ((prop === userid) && (data[i]['userid'] === filterparams.userid)) {
index = i
}
}
}
alert(index);

Categories

Resources