Related
I am studying the use of reduce in javascript, and I am trying to restructure an Array of Objects in a generic way - need to be dynamic.
flowchart - i get totaly lost
I started with this through.
Every ID becomes a Key.
Every PARENT identifies which Key it belongs to.
i have this:
const in = [
{
"id": "Ball",
"parent": "Futebol"
},
{
"id": "Nike",
"parent": "Ball"
},
{
"id": "Volley",
"parent": null
}
]
i want this
out = {
"Futebol": {
"Ball": {
"Nike": {}
}
},
"Volley": {}
}
i try it - and i had miserably failed.
const tree = require('./mock10.json')
// Every ID becomes a Key.
// Every PARENT identifies which Key it belongs to.
const parsedTree = {}
tree.reduce((acc, item) => {
if (parsedTree.hasOwnProperty(item.parent)){
if (parsedTree[`${item.parent}`].length > 0) {
parsedTree[`${item.parent}`][`${item.id}`] = {}
} else {
parsedTree[`${item.parent}`] = { [`${item.id}`]: {} }
}
} else {
// i get lost in logic
}
}, parsedTree)
console.log(parsedTree)
Got a working code for you, feel free to ask me about the implementation
Hope it helps :)
const arrSample = [
{
"id": "Ball",
"parent": "Futebol"
},
{
"id": "Nike",
"parent": "Ball"
},
{
"id": "Volley",
"parent": null
}
]
const buildTree = (arr) => {
return arr.reduce(([tree, treeMap], { id, parent }) => {
const val = {}
treeMap.set(id, val)
if (!parent) {
tree[id] = val
return [tree, treeMap]
}
if (!treeMap.has(parent)) {
const parentVal = { [id]: val }
treeMap.set(parent, parentVal)
tree[parent] = parentVal
return [tree, treeMap]
}
const newParentValue = treeMap.get(parent)
newParentValue[id] = val
treeMap.set(parent, newParentValue)
return [tree, treeMap]
}, [{}, new Map()])
}
const [result] = buildTree(arrSample)
console.log(JSON.stringify(result, 0, 2))
You could use reduce method for this and store each id on the first level of the object. This solution will work if the objects in the array are in the correct order as in the tree structure.
const data = [{"id":"Futebol","parent":null},{"id":"Ball","parent":"Futebol"},{"id":"Nike","parent":"Ball"},{"id":"Volley","parent":null}]
const result = data.reduce((r, { id, parent }) => {
if (!parent) {
r[id] = {}
r.tree[id] = r[id]
} else if (r[parent]) {
r[parent][id] = {}
r[id] = r[parent][id]
}
return r
}, {tree: {}}).tree
console.log(result)
If reduce solution is just an option, you can try this way:
var input = [
{
"id": "Ball",
"parent": "Futebol"
},
{
"id": "Nike",
"parent": "Ball"
},
{
"id": "Volley",
"parent": null
}
];
var output = {};
input.forEach(item => {
var temp = input.find(x => x.id === item.parent);
if (temp) {
temp[item.id] = {};
}
});
input = input.filter(item => !input.find(x => x.hasOwnProperty(item.id)));
input.forEach(item => {
if (!item.parent) {
output[item.id] = {};
} else {
for (var [id, value] of Object.entries(item)) {
if (typeof value === 'object') {
output[item.parent] = { [item.id]: { id: {} } };
}
}
}
})
console.log(output);
I have tried many things, but none works if we use an Array.prototype.reduce
As there are missing parents, and the elements are out of order, plus the fact that there can be an infinity of levels, I really do not believe that this question can be resolved with a simple reduce
This code should work whatever the cases :
- if all parents are not declared
- if there are infinitely many levels
- if they are in disorder
const origin =
[ { id: 'Ball', parent: 'Futebol' }
, { id: 'Nike', parent: 'Ball' }
, { id: 'Volley', parent: null }
, { id: 'lastOne', parent: 'level4' } // added
, { id: 'level4', parent: 'Nike' } // added
, { id: 'bis', parent: 'Nike' } // added
];
const Result = {} // guess who ?
, Parents = [] // tempory array to keep parents elements address by key names
;
let nbTodo = origin.length // need this one to verify number of elements to track
;
// set all the first levels, add a todo flags
origin.forEach(({id,parent},i,ori)=>
{
ori[i].todo = true // adding todo flag
if (parent===null)
{
Result[id] = {} // new first level element
ori[i].todo = false // one less :)
nbTodo--
Parents.push(({ref:id,path:Result[id]}) ) // I know who you are!
}
else if (origin.filter(el=>el.id===parent).length===0) // if he has no parent...
{
Result[parent] = {} // we create it one
Parents.push({ref:parent,path:Result[parent]} )
}
})
// to put the children back in their parents' arms
while(nbTodo>0) // while there are still some
{
origin.forEach(({id,parent,todo},i,ori)=> // little by little we find them all
{
if(todo) // got one !
{
let pos = Parents.find(p=>p.ref===parent) // have parent already been placed?
if(pos)
{
ori[i].todo = false // to be sure not to repeat yourself unnecessarily
nbTodo-- // one less :)
pos.path[id] = {} // and voila, parentage is done
Parents.push(({ref:id,path:pos.path[id]}) ) // he can now take on the role of parent
}
}
})
}
for (let i=origin.length;i--;) { delete origin[i].todo } // remove todo flags
console.log( JSON.stringify(Result, 0, 2) )
.as-console-wrapper { max-height: 100% !important; top: 0; }
I finaly made this one, based on this previous on, and done with a first step by a reduce...
to by pass the Array of Parents, I made a recursive function for searching each parent elements thru the levels of parsedTree result.
here is the code:
const Tree =
[ { id: 'Ball', parent: 'Futebol' }
, { id: 'Nike', parent: 'Ball' }
, { id: 'Volley', parent: null }
, { id: 'lastOne', parent: 'level4' } // added
, { id: 'level4', parent: 'Nike' } // added
, { id: 'bis', parent: 'Nike' } // added
];
const parsedTree = Tree.reduce((parTree, {id,parent},i ) => {
Tree[i].todo = false
if (parent===null)
{ parTree[id] = {} }
else if (Tree.filter(el=>el.id===parent).length===0) // if he has no parent...
{ parTree[parent] = { [id]: {} } }
else
{ Tree[i].todo = true }
return parTree
}, {})
function parsedTreeSearch(id, part) {
let rep = null
for(let kId in part) {
if (kId===id)
{ rep = part[kId] }
else if (Object.keys(part[kId]).length)
{ rep = parsedTreeSearch(id, part[kId]) }
if (rep) break
}
return rep
}
while (Boolean(Tree.find(t=>t.todo))) {
Tree.forEach(({id,parent,todo},i)=>{ // little by little we find them all
if (todo) {
let Pelm = parsedTreeSearch(parent, parsedTree)
if (Boolean(Pelm)) {
Pelm[id] = {}
Tree[i].todo = false
} } }) }
for (let i=Tree.length;i--;) { delete Tree[i].todo } // remove todo flags
console.log( JSON.stringify( parsedTree ,0,2))
.as-console-wrapper { max-height: 100% !important; top: 0; }
Just wanted to share a little trick I learned to pass variables into the scope of your JS Array.forEach() method.
I had a situation where I needed to use a forEach loop to build a dataset. But I needed to access variables in the current scope as well (I needed to be able to reference this in the loop).
This is the situation I was in:
var dataset = {
data: [],
backgroundColor:[],
};
items.forEach(function (item) {
dataset.data.push(item.age);
if (item.age < 2) {
dataset.bgColor.push(this.green);
} else if (item.age < 5) {
dataset.bgColor.push(this.yellow);
} else {
dataset.bgColor.push(this.red);
}
}, this);
this.refreshGraph(dataset);
Dataset isn't accessible from within the loop. So how do we access it while iterating?
I haven't seen this solution on stack overflow and it didn't fit any question I could find.
Answer below:
With the abilities of es6
If you'll use an Arrow Function the this will be taken from
items.forEach(item => {
// You can use this as out of the forEach scope
});
From MDN Web Docs:
An arrow function does not have its own this. The this value of the
enclosing lexical scope is used; arrow functions follow the normal
variable lookup rules. So while searching for this which is not
present in current scope, an arrow function ends up finding the this
from its enclosing scope.
Another nice explanation:
https://hackernoon.com/javascript-es6-arrow-functions-and-lexical-this-f2a3e2a5e8c4
If you have a function out of scope of some data yet need to access it, you can use a curried function that takes that dataset as the first parameter and can still use this normally throughout:
//curried function that uses `dataset` and `this` but it is not
//in the context where the iteration happens
function makeLoopCallback(dataset) {
return function(item) {
dataset.data.push(item.age);
if (item.age < 2) {
dataset.bgColor.push(this.green);
} else if (item.age < 5) {
dataset.bgColor.push(this.yellow);
} else {
dataset.bgColor.push(this.red);
}
}
}
//object to serve as `this` context for a function
var obj = {
green: "Green",
yellow: "Yellow",
red: "Red",
doSomething: function(items) {
var data = {
data: [],
bgColor:[],
};
items.forEach(makeLoopCallback(data), this);
return data;
}
}
//set up some dummy data
var input = [ { age: 1 }, { age: 2 }, { age: 3 }, { age: 4 }, { age: 5 }, { age: 6 } ];
//call the function
console.log(obj.doSomething(input))
An alternative is to use Array#reduce instead of Array#forEach with a function that takes two parameters directly. Since .reduce cannot set the this context, you can just use Function#bind to do it:
//external function that uses `dataset` and `this` but it is not
//in the context where the iteration happens
function external(dataset, item) {
dataset.data.push(item.age);
if (item.age < 2) {
dataset.bgColor.push(this.green);
} else if (item.age < 5) {
dataset.bgColor.push(this.yellow);
} else {
dataset.bgColor.push(this.red);
}
return dataset;
}
//object to serve as `this` context for a function
var obj = {
green: "Green",
yellow: "Yellow",
red: "Red",
doSomething: function(items) {
var data = {
data: [],
bgColor:[],
};
return items.reduce(external.bind(this), data);
}
}
//set up some dummy data
var input = [ { age: 1 }, { age: 2 }, { age: 3 }, { age: 4 }, { age: 5 }, { age: 6 } ];
//call the function
console.log(obj.doSomething(input))
The solution is to pass a JSON object as the this argument.
so before we had:
Array.forEach(function(){}, this)
// "this" is just an object ^^^^ just like anything else in JavaScript
Now we have:
Array.forEach(function(){}, {_self: this, dataset: dataset})
// you can access _self and dataset just as if they were in scope
And now you can make data changes while iterating with an anonymous function :)
Full example:
var dataset = {
data: [],
backgroundColor:[],
};
items.forEach(function (item) {
dataset.data.push(item.age);
if (item.age < 2) {
dataset.bgColor.push(_self.green);
} else if (item.age < 5) {
dataset.bgColor.push(_self.yellow);
} else {
dataset.bgColor.push(_self.red);
}
}, { _self: this , dataset: dataset});
Array.prototype.forEach(callbackFun, ?this)
You can pass dataset as this argument to forEach
var dataset = {
data: [],
backgroundColor:[],
};
items.forEach(function (item) {
this.dataset.data.push(item.age);
if (item.age < 2) {
this.dataset.bgColor.push(this.tempThis.green);
} else if (item.age < 5) {
this.dataset.bgColor.push(this.tempThis.yellow);
} else {
this.dataset.bgColor.push(this.tempThis.red);
}
}, {tempThis:this,dataset:dataset});
this.refreshGraph(dataset);
i have created a custom object that it's job is to give some more information on the data it holds...
so besides data, i have an object that is a base / "myObject" for all data i "deal" with, it will always carry its declaration name e.g Name, i also intend to have it categories itself (this is not my invention, that's kinda what namespaces are in .net c# where i feel more comfortable programming)
so this is the cause, ...some established form of common properties / values to all program data dealt with.
the problem is when it's within a collection /seq / set /array
i would like to be able to index it (in c# i heavily use indexers and enums)
where i could create an object and access it's data via it's a key.
//pName pVal (usually a data/json object could be anything)
var someRprop = Rprp("HomePhone" , { dtT: "string", dt: "0044-01...", cat "cusDat", division: "portal"; catId: 32})
function Rprp(parName, parVal) {
var cretdprp = {
pName:pPropName,
pVal: pVal,
pValAsint: parseInt(this.pVal)
};
return cretdprp;
}
now if i need to create an indexed by key collection i can't understand how i could create a matching collection-object to be able to access it via the property name.
so for instance, in the main object properties of any program i create, i have one such as "datatypes" :
//btw it has "nice" combination of Parentheses
function ProgMan(){
var Core = {
// here is where it's get little complicated for my current level of javascript
DataTypes: [
{
DateTime : Rprp("DateTime", {LenType :shortName, mxL: 35, isNumeric: false//... etc})
}
],
DataNcnsts: { I32 : "int", str: "string", Dt : "DateTime" },
HelmFactory : { }
};
return Core;
}
Edit added context of usage
TabularsWHRM: function () {
var rtWH = {
DTNDataTypes: function () {
var rtarr = new Array();
rtarr =[ Rprp("DateTime", {dateTime: this.DbLengts.pVal.vals.NameShort}),Rprp("int32", {Int32 : this.DbLengts.pVal.vals.NameShort }), Rprp("bool", { Boolean : this.DbLengts.pVal.vals.NameShort }), Rprp("objct", { Object: undefined }), Rprp("csvString", { CsvString: this.DbLengts.pVal.vals.csvString }), Rprp("string", { String: this.DbLengts.pVal.vals.string }), Rprp("Json", { Json: undefined }), Rprp("FileObj", { File: this.DbLengts.pVal.vals.NameShort })];
var tk = rtarr["DateTime"];
console.log("MyNamedkey >" + key.pName + " has Length value " + rtarr[key].pVal);
for (var key in rtarr) {
console.log("key " + key.pName + " has value " + rtarr[key].pVal.dateTime);
if (key == this.DTNDataTypes.dateTime) {
//dateTime: "DateTime", int32: "Int32", bool : "Boolean", objct: "Object",csvString: "CsvString", string : "String", Json: "Json", aFileObj: "File"}
}
}
return rtarr;
},
DbLengts: Rprp ("DbLengts",
{
vals : {
NameShort: 25,
Name: 50, NameLong: 150,
PathIO: 450, ShortDesc: 150, Desc: 450,
CommentsL1: 1000, CommentsL2: 2000, Text: 4000
,generate: function (pDTNDataTypes){
var s = pDTNDataTypes;
var rtIval = -1;
switch (s) {
case this.names.nameShort : rtIval = this.NameShort;
case this.names.name: rtIval = this.Name;
case this.names.nameLong: rtIval = this.NameLong;
case this.names.pathIO: rtIval = this.PathIO;
case this.names.shortDesc: rtIval = this.ShortDesc;
case this.names.desc: rtIval = this.Desc;
case this.names.commentsL1: rtIval = this.CommentsL1;
case this.names.commentsL2: rtIval = this.CommentsL2;
case this.names.text: rtIval = this.Text;
default: rtIval = 800;
break;
}
return parseInt(rtIval);
}
},
names :
{
nameShort : "NameShort", name : "Name", nameLong : "NameLong", pathIO : "PathIO", shortDesc : "ShortDesc", desc : "Desc", commentsL1 : "CommentsL1", commentsL2 : "CommentsL2", text : "Text"
}
})
};
return rtWH;
},
I am trying to combine an array of objects while removing duplicates based of a particular value, in this case it's id. I want to merge the other properties in each of the objects.
This is what I have:
var myArray = [
{
id : 1,
rendering : 0,
completed : 1
},
{
id : 2,
rendering : 0,
completed : 1
},
{
id : 3,
rendering : 0,
completed : 1
},
{
id : 1,
rendering : 1,
completed : 0
},
{
id : 2,
rendering : 1,
completed : 0
},
{
id : 3,
rendering : 1,
completed : 0
},
]
This is what I want :
var myDesiredArray = [
{
id : 1,
rendering: 1,
completed: 1
},
{
id : 2,
rendering: 1,
completed: 1
},
{
id : 3,
rendering: 1,
completed: 1
},
]
I'd be happy with straight javascript or underscore/lodash. Any suggestions would be greatly appreciated.
Using underscore, here's one way to do it
Define a sum function
function sum(numbers) {
return _.reduce(numbers, function(result, current) {
return result + parseFloat(current);
}, 0);
}
Then groupby over id and pluck needed values and apply sum over them.
_.chain(myArray )
.groupBy("id")
.map(function(value, key) {
return {
id: key,
rendering : sum(_.pluck(value, "rendering")),
completed: sum(_.pluck(value, "completed"))
}
})
.value();
function merge(arr) {
var uniqItems = arr.reduce(function(memo, item) {
if (!memo[item.id]) {
memo[item.id] = item;
} else {
Object.keys(item).forEach(function(k) {
if (item[k]) { memo[item.id][k] = item[k]; }
});
}
return memo;
}, {});
return Object.keys(uniqItems).map(function(k) {
return uniqItems[k];
});
}
merge(myArray); // => myDesiredArray
There is multiple way to do, but as you want to check uniqueness, the easiest way would be to use a temporary map, indexed by your ids, on which you can accumulate the values of the other properties.
When the accumulation is done, convert the Map to a straight array, et voila.
You would do it like so:
var array; // defined elsewhere
var tmp = {}; // temporary map
// accumulate the properties in the map
array.forEach(function(elt) {
if (tmp[elt.id] == null)
tmp[elt.id] = elt
else {
tmp[elt.id].prop += elt.prop;
// (...) do the accumulation here
}
});
// then convert this map to an array by iterating on the ids
Object.keys(tmp).map(function(id) { return tmp[id]; })
Pure JS solution
The first thing you should do is use a map for fast lookup:
var map = {};
myArray.forEach(function(item){
var mapItem = map[item.id] || {
id : item.id,
rendering : 0,
completed : 0
}
mapItem.rendering += item.rendering;
mapItem.completed += item.completed;
map[item.id] = mapItem;
});
Then you can convert the map back to an array:
var myDesiredArray = [];
for (var id in map) {
myDesiredArray.push(map[id]);
}
Here is another vanilla js version using a functional approach:
myArray.reduce(function(prev, cur) {
if (prev[cur.id]) {
prev[cur.id].rendering += cur.rendering;
prev[cur.id].completed += cur.completed;
} else {
prev[cur.id] = cur;
}
return prev;
}, []).filter(function(val) {
return val;
});
Solution with less loop possible
function myMerge(myArray) {
var temp = {},
myDesiredArray = [];
for (var i = 0; i < myArray.length; ++i) {
var elem = myArray[i];
if (!temp[elem.id]) {
temp[elem.id] = {
'id': elem.id,
'rendering': elem.rendering,
'completed': elem.completed,
};
myDesiredArray.push(temp[elem.id])
}
temp[elem.id].rendering += elem.rendering;
temp[elem.id].completed += elem.completed;
}
return myDesiredArray;
}
I want to build an array of objects which look like this:
var someObject = {
id,
groupA {
propertyA: 0,
propertyB: 0,
},
groupB {
propertyA: 0,
propertyB: 0
totals {}
}
And add the following composite property:
Object.defineProperty(someObject.groupA, "propertyC",
{
get: function() {
return someObject.groupA.propertyA + someObject.groupA.propertyB;
}
});
And use the same method to add the properties:
groupB.propertyC -> groupB.propertyA + groupB.propertyB
totals.propertyA -> groupA.propertyA + groupB.propertyA
totals.propertyB -> groupA.propertyB + groupB.propertyB
totals.propertyC -> groupA.propertyC + groupB.propertyC
I got all this working by putting all this code in a function so it added someObject to an array.
But then I got to thinking that the read-only composite properties shouldn't need to be created for each object and could probably be in a prototype.
Does this make sense? And is it possible, and if so: how?
It can be done. You just need to make sure that groupA and groupB inherit from an object which has the composite property.
var proto = {};
Object.defineProperty(proto, 'propertyC', {
get : function() { return this.propertyA + this.propertyB; }
});
var someObj = {
id : '1',
groupA : Object.create(proto, {
propertyA : { value : 1 }, propertyB : { value : 2 }
}),
groupB : Object.create(proto, {
propertyA : { value : 3 }, propertyB : { value : 4 }
}),
totals : Object.create(proto, {
propertyA : { get : function() { return someObj.groupA.propertyA + someObj.groupB.propertyA; } },
propertyB : { get : function() { return someObj.groupA.propertyB + someObj.groupB.propertyB; } }
})
}
// Usage:
console.log(someObj.groupA.propertyC); // 3
console.log(someObj.groupB.propertyC); // 7
console.log(someObj.totals.propertyC); // 10
I don't know if understood well your question; but in general when you have members that you want to share across all the instances of a particular type then you should put them into the prototype of the constructor.
In your example, you're using object literal, which doesn't make it easy to do so, unless you extend the prototype of the Object constructor, which I would not recommend.
How about doing something like this:
var SomeType = function(){
this.id = 0;
this.groupA = {
propertyA: 0,
propertyB: 0
};
this.groupA = {
propertyA: 0,
propertyB: 0
};
this.total = {};
}
SomeType.prototype = {
constructor: SomeType
}
Object.defineProperty(SomeType.prototype, 'propertyC', {
get: function(){ return this.groupA.propertyA + this.groupA.propertyB }
});