javascript array of string to deep merged object - javascript

I am trying to convert an array of strings (with many more items):
fullRoutes = ['POST /api/v1/user/login','POST /api/v1/user/logout']
Into a deep nested object like this (to use in the following module react-checkbox-tree):
const nodes = [{
value: 'api',
label: 'api',
children: [
{ value: 'v1',
label: 'v1',
children: [
{ value: 'user',
label: 'user',
children: [
{ value: login, label: login},
{ value: logout, label: logout}
]
}
]
}
]
I managed to get to:
fullRoutes.forEach(function(route){
let path = route.split(" ")[1].split("/").filter(function(e){ return e === 0 || e })
let object = {}
path.reduce(function(o, s) {
return o['children'] = {label: s, value: s, children: []}
}, object)
routes.push(object)
})
Which returns the object with the 'children', but I am struggling to merge them correctly

I believe this will work:
fullRoutes = [
'POST /api/v1/user/login',
'POST /api/v1/user/logout',
'POST /api/v2/user/login'
];
routes = [];
fullRoutes.forEach(route => {
let path = route.split(' ')[1].split('/').filter(e => e);
let rs = routes;
for (let i = 0, n = path.length; i < n; i++) {
let seg = path[i];
let segp = path.slice(0, i + 1).join('/');
let node = rs.find(r => r.label == seg);
if (!node)
rs.push(node = {
label: seg,
value: segp,
children: []
});
rs = node.children;
}
});
console.log(routes);

One way is to reduce everything to an object including the children and use the path name as key within the children
Then recursively loop through all children and use Object#values() to convert them from objects to arrays
const fullRoutes = ['POST /api/v1/user/login', 'POST /api/v1/user/logout'];
const tmp = fullRoutes.reduce(function(tmp, route){
let path = route.split(" ")[1].split("/");
path.reduce(function(o, s, i) {
o[s] = o[s] || {label: s, value: s, children: {}};
return o[s].children;
}, tmp);
return tmp;
},{});
const nodes = Object.values(tmp);
nodes.forEach(childrenToArray);
console.log(nodes)
//recursive helper
function childrenToArray(obj) {
obj.children = Object.values(obj.children);
obj.children.forEach(childrenToArray)
}
.as-console-wrapper {max-height: 100%!important;}

Related

Convert string array to multidimensional array

I have a javascript string array which I want to convert to a multidimensional array:
const maps = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"as://fff/ggg/hhh",
];
I want to convert it to this:
const maps = [
{label: "aaa", children: [
{label: "bbb", children: [
{label: "ccc", children: []},
{label: "ddd", children: []},
{label: "eee", children: []}
]}
]},
{label: "fff", children: [
{label: "ggg", children: [
{label: "hhh", children: []}
]}
]}
];
I've tried to do it like this, but it turns out that its not working correctly and I think this is also not the appropiate way to handle this:
interface DialogItem {
label: string,
children: DialogItem[]
};
const dialogs: string[] = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"as://fff/ggg/hhh",
];
const mapFolder = (dialogs: string[]) => {
const maps: DialogItem[] = [];
for (const dialog of dialogs) {
const dialogStr: string = dialog.replace(/(\w+):\/\//gi, "");
const dialogArr: string[] = dialogStr.split("/");
const parent = maps.find(mapped => mapped.label === dialogArr[0]);
if (parent === undefined) {
maps.push({label: dialogArr[0], children: []});
} else {
dialogArr.shift();
const child = parent.children.find(mapped => mapped.label === dialogArr[0]);
if (child === undefined) {
parent.children.push({label: dialogArr[0], children: []});
} else {
child.children.push({label: dialogArr[1], children: []});
}
}
}
};
mapFolder(dialogs);
You could split the string to get an array of paths. Use a mapper object to keep track of each nested object and it's path. reduce the chunks and return the nested object in each iteration
const maps = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"as://fff/ggg/hhh"
],
mapper = {},
tree = { children: [] } // root object
for (const str of maps) {
let chunks = str.split('//')[1].split("/"),
path = '';
chunks.reduce((parent, label) => {
if (path)
path += `.${label}`
else
path = label
if (!mapper[path]) {
const o = { label, children: [] };
mapper[path] = o;
parent.children.push(o)
}
return mapper[path];
}, tree)
}
console.log(tree.children)
The expected output can be obtained by using nested maps and recursively appending the data to the sub-array. Also, have handled couple of more test cases.
var dialogs = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"local://aaa/iii/jjj",
"",
"as://",
"as://fff/ggg/hhh",
];
var maps = [];
dialogs.map((dialog) => {
const dialogStr = dialog.substr(dialog.indexOf("//") + 2, dialog.length);
const dialogArr = dialogStr.split("/");
var localObj = maps;
dialogArr.map((elem) => {
if (elem.trim() != "") {
var data = localObj.find(ele => ele.label == elem);
if (data) {
localObj = data['children'];
} else {
localObj.push({
label: elem,
children: []
});
localObj = localObj.find(ele => ele.label == elem)['children'];
}
}
});
});
console.log(maps);

tree from array of dot-separated strings

I have an array of dot delimited strings which looks like the following
data = [
'Europe.UK.London.TrafalgarSq',
'Europe.UK.London.HydePark',
'Europe.UK.London.OxfordStreet',
'Europe.UK.London.City.Bank',
'Europe.France.Paris',
'Europe.France.Bordeaux'},
]
and I want to build the following tree of of nested objects. In case it matters, this is for a leaflet map where the Tree Layers Control is going to be used
var tree = {
label: 'Places',
selectAllCheckbox: 'Un/select all',
children: [
{
label: 'Europe',
selectAllCheckbox: true,
children: [
{
label: 'Europe.UK',
selectAllCheckbox: true,
children: [
{
label: 'Europe.UK.London',
selectAllCheckbox: true,
children: [
{label: 'Europe.UK.London.TrafalgarSq'},
{label: 'Europe.UK.London.HydePark'},
{label: 'Europe.UK.London.OxfordStreet'},
{
label: 'Europe.UK.London.City',
selectAllCheckbox: true,
children: [
{label: 'Europe.UK.London.City.Bank'},
]
},
]
},
{
label: 'Europe.France',
selectAllCheckbox: true,
children: [
{label: 'Europe.France.Paris'},
{label: 'Europe.France.Bordeaux'},
]
},
]
}
]
}
]
};
How do I do this tree please?
You could use a mapper object which has partial paths (or label) as key and a reference to the object in the tree as it's value. split the path at . and reduce the array with tree as the initialValue. If the path doesn't exist yet, add it to mapper and tree. Return the nested object in each iteration.
const data = ["Europe.UK.London.TrafalgarSq","Europe.UK.London.HydePark","Europe.UK.London.OxfordStreet","Europe.UK.London.City.Bank","Europe.France.Paris","Europe.France.Bordeaux"],
mapper = {},
tree = {
label: 'Places',
selectAllCheckbox: 'Un/select all',
children: []
}
for (const str of data) {
let splits = str.split('.'),
label = '';
splits.reduce((parent, place) => {
if (label)
label += `.${place}`
else
label = place
if (!mapper[label]) {
const o = { label };
mapper[label] = o;
parent.selectAllCheckbox = true
parent.children = parent.children || [];
parent.children.push(o)
}
return mapper[label];
}, tree)
}
console.log(tree)
You could an iterative approach with a reduceing of the nested objects.
var data = ['Europe.UK.London.TrafalgarSq', 'Europe.UK.London.HydePark', 'Europe.UK.London.OxfordStreet', 'Europe.UK.London.City.Bank', 'Europe.France.Paris', 'Europe.France.Bordeaux'],
children = data.reduce((r, s) => {
s.split('.').reduce((q, _, i, a) => {
q.selectAllCheckbox = true;
var label = a.slice(0, i + 1).join('.'),
temp = (q.children = q.children || []).find(o => o.label === label);
if (!temp) q.children.push(temp = { label });
return temp;
}, r);
return r;
}, { children: [] }).children,
tree = { label: 'Places', selectAllCheckbox: 'Un/select all', children };
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Merge Array of same level

I have an array which I need to combine with comma-separated of the same level and form a new array.
Input:
let arr = [
[{ LEVEL: 1, NAME: 'Mark' }, { LEVEL: 1, NAME: 'Adams' }, { LEVEL: 2, NAME: 'Robin' }],
[{ LEVEL: 3, NAME: 'Williams' }],
[{ LEVEL: 4, NAME: 'Matthew' }, { LEVEL: 4, NAME: 'Robert' }],
];
Output
[
[{ LEVEL: 1, NAME: 'Mark,Adams' }, { LEVEL: 2, NAME: 'Robin' }],
[{ LEVEL: 3, NAME: 'Williams' }],
[{ LEVEL: 4, NAME: 'Matthew,Robert' }],
];
I tried with the following code but not getting the correct result
let finalArr = [];
arr.forEach(o => {
let temp = finalArr.find(x => {
if (x && x.LEVEL === o.LEVEL) {
x.NAME += ', ' + o.NAME;
return true;
}
if (!temp) finalArr.push(o);
});
});
console.log(finalArr);
You could map the outer array and reduce the inner array by finding the same level and add NAME, if found. Otherwise create a new object.
var data = [[{ LEVEL: 1, NAME: "Mark" }, { LEVEL: 1, NAME: "Adams" }, { LEVEL: 2, NAME: "Robin"}], [{ LEVEL: 3, NAME: "Williams" }], [{ LEVEL: 4, NAME: "Matthew" }, { LEVEL: 4, NAME: "Robert" }]],
result = data.map(a => a.reduce((r, { LEVEL, NAME }) => {
var temp = r.find(q => q.LEVEL === LEVEL);
if (temp) temp.NAME += ',' + NAME;
else r.push({ LEVEL, NAME });
return r;
}, []));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Assuming you only want to merge within the same array and not across arrays, and assuming there aren't all that many entries (e.g., fewer than several hundred thousand), the simple thing is to build a new array checking to see if it already has the same level in it:
let result = arr.map(entry => {
let newEntry = [];
for (const {LEVEL, NAME} of entry) {
const existing = newEntry.find(e => e.LEVEL === LEVEL);
if (existing) {
existing.NAME += "," + NAME;
} else {
newEntry.push({LEVEL, NAME});
}
}
return newEntry;
});
let arr= [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"}]
];
let result = arr.map(entry => {
let newEntry = [];
for (const {LEVEL, NAME} of entry) {
const existing = newEntry.find(e => e.LEVEL === LEVEL);
if (existing) {
existing.NAME += "," + NAME;
} else {
newEntry.push({LEVEL, NAME});
}
}
return newEntry;
});
console.log(result);
If the nested arrays can be truly massively long, you'd want to build a map rather than doing the linear search (.find) each time.
I'd try to do as much of this in constant time as possible.
var m = new Map();
array.forEach( refine.bind(m) );
function refine({ LABEL, NAME }) {
var o = this.get(NAME)
, has = !!o
, name = NAME
;
if (has) name = `${NAME}, ${o.NAME}`;
this.delete(NAME);
this.set(name, { NAME: name, LABEL });
}
var result = Array.from( m.values() );
I haven't tested this as I wrote it on my phone at the airport, but this should at least convey the approach I would advise.
EDIT
Well... looks like the question was edited... So... I'd recommend adding a check at the top of the function to see if it's an array and, if so, call refine with an early return. Something like:
var m = new Map();
array.forEach( refine.bind(m) );
function refine(item) {
var { LABEL, NAME } = item;
if (!NAME) return item.forEach( refine.bind(this) ); // assume array
var o = this.get(NAME)
, has = !!o
, name = NAME
;
if (has) name = `${NAME}, ${o.NAME}`;
this.delete(NAME);
this.set(name, { NAME: name, LABEL });
}
var result = Array.from( m.values() );
That way, it should work with both your original question and your edit.
EDIT
Looks like the question changed again... I give up.
Map the array values: every element to an intermediate object, then create the desired object from the resulting entries:
const basicArr = [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"}]
];
const leveled = basicArr.map( val => {
let obj = {};
val.forEach(v => {
obj[v.LEVEL] = obj[v.LEVEL] || {NAME: []};
obj[v.LEVEL].NAME = obj[v.LEVEL].NAME.concat(v.NAME);
});
return Object.entries(obj)
.map( ([key, val]) => ({LEVEL: +key, NAME: val.NAME.join(", ")}));
}
);
console.log(leveled);
.as-console-wrapper { top: 0; max-height: 100% !important; }
if you want to flatten all levels
const basicArr = [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"},
{"LEVEL":2,"NAME":"Cynthia"}],
[{"LEVEL":3,"NAME":"Jean"},
{"LEVEL":4,"NAME":"Martha"},
{"LEVEL":2,"NAME":"Jeff"}],
];
const leveled = basicArr.map( val => Object.entries (
val.reduce( (acc, val) => {
acc[val.LEVEL] = acc[val.LEVEL] || {NAME: []};
acc[val.LEVEL].NAME = acc[val.LEVEL].NAME.concat(val.NAME);
return acc;
}, {}))
.map( ([key, val]) => ({LEVEL: +key, NAME: val.NAME.join(", ")})) )
.flat() // (use .reduce((acc, val) => acc.concat(val), []) for IE/Edge)
.reduce( (acc, val) => {
const exists = acc.filter(x => x.LEVEL === val.LEVEL);
if (exists.length) {
exists[0].NAME = `${val.NAME}, ${exists.map(v => v.NAME).join(", ")}`;
return acc;
}
return [... acc, val];
}, [] );
console.log(leveled);
.as-console-wrapper { top: 0; max-height: 100% !important; }
ES6 way:
let say attributes is multidimensional array having multimple entries which need to combine like following:
let combinedArray = [];
attributes.map( attributes => {
combined = combinedArray.concat(...attributes);
});

JS array of arrays to object

I have a JS array (shown 4 examples actual has 66 )
[["A","Example1"],["A","Example2"],["B","Example3"],["B","Example4"]]
that I am trying to get into an object for a multi select drop down menu:
var opt = [{
label: 'A', children:[
{"label":"Example1","value":"Example1","selected":"TRUE"},
{"label":"Example2","value":"Example2","selected":"TRUE"}
]
},
{
label: 'B', children:[
{"label":"Example3","value":"Example3","selected":"TRUE"},
{"label":"Example4","value":"Example4","selected":"TRUE"}
]
}
]
Is there a easy way to do this ?
Updated:
Using reduce() and filter() to get expected results.
const result = [['A', 'Example1'], ['A', 'Example2'], ['B', 'Example3'], ['B', 'Example4']].reduce((acc, cur) => {
const objFromAccumulator = acc.filter((row) => row.label === cur[0]);
const newChild = {label: cur[1], value: cur[1], selected: 'TRUE'};
if (objFromAccumulator.length) {
objFromAccumulator[0].children.push(newChild);
} else {
acc.push({label: cur[0], children: [newChild]});
}
return acc;
}, []);
console.log(result);
Something like this should work:
const raw = [["A","Example1"],["A","Example2"],["B","Example3"],["B","Example4"]];
const seen = new Map();
const processed = raw.reduce((arr, [key, label]) => {
if (!seen.has(key)) {
const item = {
label: key,
children: []
};
seen.set(key, item);
arr.push(item);
}
seen.get(key).children.push({
label,
value: label,
selected: "TRUE"
})
return arr;
}, []);
console.log(processed);
Here's a rather efficient and concise take on the problem using an object as a map:
const data = [["A","Example1"],["A","Example2"],["B","Example3"],["B","Example4"]];
const opt = data.reduce((results,[key,val]) => {
if(!results[0][key]) //first element of results is lookup map of other elements
results.push(results[0][key] = { label: key, children: [] });
results[0][key].children.push({ label: val, value: val, selected:"TRUE" });
return results;
}, [{}]).slice(1); //slice off map as it's no longer needed
console.log(opt);

How to detect object differences between two arrays?

I'm trying to compare two arrays of objects and returns a list of updated objects. I don't want to use lodash just the javascript data structures and functions.
E.g:
I have a first array which named arr1 = [
{
name: 'attribute 1',
id: 12,
value: 40,
docs:[],
version: 1,
},
{
name: 'attribute 41',
id: 12,
value: 6,
version: 1,
}
]
And another array:
array2 = [
{
name: 'attribute 1',
attributeTypeId: 12,
value: 65,
docs: ['bla bla']
}
]
I'm trying to iterate through the two arrays and detect the differences and returns an array like that:
result = [
{
name: 'attribute 1',
id: 12,
value: 65,
docs:['bla bla'],
version: 1,
},
{
name: 'attribute 41',
id: 12,
value: 6,
version: 1,
}]
I wrote some uncomplete function (not optimized yet just a brute force solution):
const filterProperties = (e) => {
return e.toLowerCase() !== 'name' && e.toLowerCase() !== 'id'
}
// function sort
const sortProperties = (a, b) => a < b ? -1 : 1;
let result = []
attributesUpdate.forEach(attr => {
const attrProps = Object.getOwnPropertyNames(attr);
// iterate the attributes
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i];
// check if the attribute to update has a different name or attributeTypeId
if (attribute.name !== attr.name) {
result = result.concat(attr);
}
// check if the attribute to update has the same name, id
// of the originalOne
if (attribute.name === attr.name && attribute.id=== attr.id) {
let obj = {
name: attribute.name,
id: attribute.id,
}
// get the properties of the attribute
const attributeProps = Object.getOwnPropertyNames(attribute);
// extract the name and id from the list
const filtredAttributeProps = attributeProps.filter(filterProperties);
const filteredattrProps = attrProps.filter(filterProperties);
// returns the length of each array of properties
const attrLength = filteredattrProps.length;
const attributeLength = filtredAttributeProps.length;
if (attrLength === attributeLength) {
for (let j = 0; j < attrLength; j++) {
const propName = filteredattrProps[j];
obj[propName] = attr[propName];
}
result = result.filter(e => e.name === attr.name
&& e.id=== attr.id)
.map(e => Object.assign(e, {obj}))
}
if (attrLength !== attributeLength) {
// sort the array of properties
const sortedAttrProps = filteredattrProps.sort(sortProperties);
const sortedAttributeProps = filtredAttributeProps.sort(sortProperties);
// check the shortest object
const min = attrLength < attributeLength ? attrLength : attributeLength;
// get the biggest object
const longestObjProps = attrLength === min ? sortedAttributeProps : sortedAttrProps;
const longestObj = attrLength === min ? attribute : attr
const shortestProps = attrLength === min ? sortedAttrProps: sortedAttributeProps;
const shortestObj = attrLength === min ? attr : attribute
// fill the object with attr properties
for(let j = 0; j < min; j++) {
const propName = shortestProps[j];
obj[propName] = shortestObj[propName];
}
// fill the remaining properties in the object
const remainingProperties = longestObjProps.filter(e => !shortestProps.includes(e));
for (let j = 0; j < remainingProperties.length; j++) {
const propName = remainingProperties[j];
obj[propName] = longestObj[propName]
}
if (!result.length || result.filter(e => e.name !== attr.name &&
e.id!== attr.id).length === 0) {
result.concat(obj);
}
}
}
}
})
console.log('result: ', result);
I got such a result :
[
{
name: 'attribute 1',
attributeTypeId: 12,
value: 65,
docs: ['bla bla']
}
]
How can I fix this code to get the desired results? I hope that my question will not be downvoted. Any suggestion will be welcome.
What this code does is loop through the objects in array2, and then when it finds that there is a matching name/id in arr1, it simply updates the properties of that object. If not found, it will add the object to arr1.
arr1 = [{
name: 'attribute 1',
id: 12,
value: 40,
docs: [],
version: 1,
},
{
name: 'attribute 41',
id: 12,
value: 6,
version: 1,
}
];
array2 = [{
name: 'attribute 1',
attributeTypeId: 12,
value: 65,
docs: ['bla bla']
}];
updateArray(arr1, array2);
console.log(arr1);
function updateArray(arrayToUpdate, dataToUpdateWith) {
dataToUpdateWith.forEach(function(obj) {
var objToUpdate = checkIfNameIdExists(arrayToUpdate, obj.name, obj.attributeTypeId);
if (objToUpdate === false) {
objToUpdate = obj;
arrayToUpdate.push(objToUpdate);
} else {
for (var prop in obj) {
if (objToUpdate.hasOwnProperty(prop)) {
var nameInFinalObject = prop;
if (prop === "attributeTypeId") {
nameInFinalObject = "id";
}
objToUpdate[nameInFinalObject] = obj[prop];
}
}
}
});
}
function checkIfNameIdExists(arrOfObj, name, id) {
if (name === null) {
return false;
}
var output = false;
arrOfObj.forEach(function(obj) {
if (obj.name === name) {
output = obj;
return true;
}
});
return output;
}
Assumptions:
The values in each of the objects are same type and values are not nested so there is a need to recursively traverse the tree to compare equality etc.
The first array is the source and the subsequent (with the same name) is the mutated form.
We are not handling removals of properties from the source object. From what is given by the OP we are only accounting for value changes.
const d1 = [{ name: 'attribute 1', id: 12, value: 40, docs: [], version: 1, }, { name: 'attribute 41', id: 12, value: 6, version: 1, } ]
const d2 = [{ name: 'attribute 1', attributeTypeId: 12, value: 65, docs: ['bla bla'] }]
const isChanged = (a, b) =>
Array.isArray(a) ? !a.every(x => b.includes(x)) : a !== b
const compare = (o1, o2) => Object.entries(o1).reduce((r, [k,v]) => {
if(k in o2 && isChanged(o2[k], v))
Object.assign(r, {[k]: o2[k]})
return r
}, o1)
const group = (a, b) => [...a, ...b].reduce((r,c) =>
(r[c.name] = [...r[c.name] || [], c], r), {})
const result = Object.values(group(d1,d2)).reduce((r,c) =>
(r.push(c.length == 2 ? compare(...c) : c[0]), r), [])
console.log(result)
The idea is to merge the objects in one array, group them by name and if there ware any changes the groups with length of 2 would be compared by the compare function. Otherwise just added to the end result.

Categories