Javascript flatten deeply nested Array with objects and renaming properties - javascript

I'm stuck again with some flattening and renaming of the following.
What I got:
test = [
{
date: '2020-03-30',
station: {
id: 0,
name: 'some description'
},
firstValues: [
{
result: 1,
type: 4,
max: 18,
min: 1,
},
{
result: 2,
type: 5,
max: 15,
min: 2,
}
],
lastValues: [
{
result: 1,
type: 3,
max: 17,
min: 1
},
{
result: 2,
type: 8,
max: 20,
min: 2
}
],
iD: 'xxx3',
count: 1
},
{
next object with same structure
}
]
What I try to achieve:
test = [
{
date: '2020-03-30',
station: 'some description',
first_E01_result: 1,
first_E01_type: 4,
first_E01_max: 18,
first_E01_min: 1,
first_E02_result: 2,
first_E02_type: 5,
first_E02_max: 15,
first_E02_min: 2,
last_E01_result: 1,
last_E01_type: 3,
last_E01_max: 17,
last_E01_min: 1,
last_E02_result: 2,
last_E02_type: 8,
last_E02_max: 20,
last_E02_min: 2,
iD: 'xxx3',
count: 1
},
{
next object with same structure
}
]
I'm quite aware that my approach isn't the right thing. I tried different things so far but couldn't get it working. I'm totally stuck again to find the right way because I do run into two main issues:
How can I make the difference between first and last values? (switch case or if and if else?)
and
How can I access the name property from the station object and assign it to the key of "station"
Here is my last approach which is still missing the right code for the mentioned problems:
convertTest(input) {
return input.map(obj => {
const obj1 = {};
const obj2 = {};
for (const prop in obj) {
if (obj.hasOwnProperty(prop) && Array.isArray(obj[prop])) {
for (let i = 0; i < obj[prop].length; i++) {
for (const [key, value] of Object.entries(obj[prop][i])) {
const name = 'first_EO' + (i + 1).toString() + '_' + key;
obj2[name] = value;
}
}
} else {
obj1[prop] = obj[prop];
}
const dataconverted = Object.assign({}, obj1, obj2);
return dataconverted;
}
});
}

You could take a recursive approach for all other nested objects except the first level with special cases.
var data = [{ date: '2020-03-30', station: { id: 0, name: 'some description' }, firstValues: [{ result: 1, type: 4, max: 18, min: 1 }, { result: 2, type: 5, max: 15, min: 2 }], lastValues: [{ result: 1, type: 3, max: 17, min: 1 }, { result: 2, type: 8, max: 20, min: 2 }], iD: 'xxx3', count: 1 }],
getPath = object => Object.entries(object).reduce((r, [k, v], i) => {
if (v && typeof v === 'object') {
r.push(...getPath(v).map(([left, right]) => [(Array.isArray(object) ? 'E' + (i + 1).toString().padStart(2, 0) : k) + '_' + left, right]));
} else {
r.push([k, v]);
}
return r;
}, []),
result = data.map(o => Object.fromEntries(Object.entries(o).reduce((r, [k, v]) => {
if (k === 'station') {
r.push([k, v.name]);
} else if (v && typeof v === 'object') {
if (k.endsWith('Values')) k = k.slice(0, -6);
r.push(...getPath(v).map(([left, right]) => [k + '_' + left, right]));
} else {
r.push([k, v]);
}
return r
}, [])));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

You should use Map and Object.keys
var test = [{"date":"2020-03-30","station":{"id":0,"name":"some description"},"firstValues":[{"result":1,"type":4,"max":18,"min":1},{"result":2,"type":5,"max":15,"min":2}],"lastValues":[{"result":1,"type":3,"max":17,"min":1},{"result":2,"type":8,"max":20,"min":2}],"iD":"xxx3","count":1}]
console.log(flatten(test));
function flatten(arr) {
return arr.map(el => ModifyObject(el))
}
function ModifyObject(el) {
const obj = {};
obj.date = el.date;
obj.iD = el.iD;
obj.count = el.count;
obj.station = el.station.name;
flattenObjectByProperty(obj, el, 'firstValues')
flattenObjectByProperty(obj, el, 'lastValues')
return obj;
}
function flattenObjectByProperty(obj, el, property) {
(el[property] || []).map((child, i) => {
Object.keys(child).forEach(key => {
obj[property + '_E' + i + '_' + key] = child[key]
});
});
}

Please try this.
test = test.map((elem) => {
elem.firstValues.forEach((child, index) => {
for(let key in child){
let v = `first_E${index+1}_${key}`
elem[v] = child[key];
}
})
elem.lastValues.forEach((child, index) => {
for(let key in child){
let v = `last_E${index+1}_${key}`
elem[v] = child[key];
}
})
elem['station'] = elem.station.name;
delete elem.firstValues;
delete elem.lastValues;
return elem;
})

You can use Array.prototype.reduce to flatten as per your requirement
const test = [
{
date: '2020-03-30',
station: {
id: 0,
name: 'some description'
},
firstValues: [
{
result: 1,
type: 4,
max: 18,
min: 1,
},
{
result: 2,
type: 5,
max: 15,
min: 2,
}
],
lastValues: [
{
result: 1,
type: 3,
max: 17,
min: 1
},
{
result: 2,
type: 8,
max: 20,
min: 2
}
],
iD: 'xxx3',
count: 1
}
];
const result = test.reduce((acc, curr) => {
const { firstValues, lastValues, ...rest } = curr;
const modifiedFirstValues = firstValues.reduce((r, c, i) => {
Object.entries(c).forEach(([key, value]) => {
const modifiedKey = `first_E${i + 1}_${key}`;
r[modifiedKey] = value;
});
return r;
}, Object.create(null));
const modifiedLastValues = lastValues.reduce((r, c, i) => {
Object.entries(c).forEach(([key, value]) => {
const modifiedKey = `last_E${i + 1}_${key}`;
r[modifiedKey] = value;
});
return r;
}, Object.create(null));
const finalObj = {
...rest,
...modifiedFirstValues,
...modifiedLastValues
};
acc.push(finalObj);
return acc;
}, []);
console.log(result);

Related

return indices if unit repeats in the array

I want the repeated indices of each unit in the sorted array below.
unit 39 is repeated from index 1 to index 3 right?
const array = [
{
unit: 38,
},
{
unit: 39,
},
{
unit: 39,
},
{
unit: 39,
},
{
unit: 40,
},
{
unit: 41,
},
{
unit: 41,
},
{
unit: 42,
},
]
So the desired result would be:
[
{
unit: 39,
fromIndex: 1,
toIndex: 3,
},
{
unit: 41,
fromIndex: 5,
toIndex: 6,
},
]
I tried to remmeber the index of each pair of the units but this idea is not working with more than two repetations . so I need a handI think
let u;
for (let i = 0; i < array.length; i++) {
const a = array[i];
const b = array[i + 1] || {
unit: null
};
if (a.unit == b.unit) {
u = a.unit; // remmeber the repeated unit here
...
}
}
You could check the lat item or in the result the last item.
const
array = [{ unit: 38 }, { unit: 39 }, { unit: 39 }, { unit: 39 }, { unit: 40 }, { unit: 41 }, { unit: 41 }, { unit: 42 }],
result = array.reduce((r, { unit }, i, a) => {
if (unit !== a[i - 1]?.unit) return r;
if (unit !== r[r.length - 1]?.unit) r.push({ unit, fromIndex: i - 1, toIndex: i });
else r[r.length - 1].toIndex = i;
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use array reduce and filter methods:
const result = array.reduce((acc, item, index) => {
if (index > 0 && array[index - 1].unit === item.unit) {
const last = acc[acc.length - 1];
last.toIndex = index;
} else {
acc.push({
unit: item.unit,
fromIndex: index,
toIndex: index
});
}
return acc;
}, []).filter(item => item.fromIndex !== item.toIndex);

sum array of objects inside an array of objects

I have an array of objects and inside the array there is another array of objects, I would like to sum the values. So the sum is based on the same picker_id. Then sum current_capacity, process_time_in_minutes, and picked_qty inside products which is array of objects.
Here is my data:
var arr = [
{
current_capacity: 6000,
picker_id: "icQrHPuE2fMZslceSG6liwuRar92",
process_time_in_minutes: 10,
products: [
{
product_id: 1,
picked_qty: 2
},
{
product_id: 2,
picked_qty: 3
}
]
},
{
current_capacity: 2500,
picker_id: "icQrHPuE2fMZslceSG6liwuRar92",
process_time_in_minutes: 20,
products: [
{
product_id: 1,
picked_qty: 10
}
]
},
{
current_capacity: 36000,
picker_id: "WIRzfIZALeftRk3DRGvh4nBdxQV2",
process_time_in_minutes: 15,
products: [
{
product_id: 1,
picked_qty: 2
},
{
product_id: 2,
picked_qty: 3
}
]
}
];
Here is my code:
var res = arr.reduce((acc, obj) => {
var existObj = acc.find((item) => item.picker_id === obj.picker_id);
if (existObj) {
let total_picked = obj.products.reduce((acc2, curr) => acc2 + curr);
// console.log("total_picked", total_picked);
existObj.current_capacity =
existObj.current_capacity + obj.current_capacity;
existObj.process_time_in_minutes =
existObj.process_time_in_minutes + obj.process_time_in_minutes;
existObj.total = existObj.total ? existObj.total : 0 + total_picked;
return acc;
}
acc.push(obj);
return acc;
}, []);
const formatted = res.map((el) => {
return {
picker_id: el.picker_id,
total_volume: el.current_capacity,
total_time: el.process_time_in_minutes,
total_products: el.total
};
});
The result is as below:
[
{
picker_id: "icQrHPuE2fMZslceSG6liwuRar92"
total_volume: 8500
total_time: 30
total_products: "0[object Object]"
},
{
picker_id: "WIRzfIZALeftRk3DRGvh4nBdxQV2"
total_volume: 36000
total_time: 15
total_products: undefined
}
]
Expected like below:
[
{
picker_id: "icQrHPuE2fMZslceSG6liwuRar92"
total_volume: 8500
total_time: 30
total_products: 15
},
{
picker_id: "WIRzfIZALeftRk3DRGvh4nBdxQV2"
total_volume: 36000
total_time: 15
total_products: 5
}
]
Using a little parameter destructuring, I think you can do a little further clean-up after you fix the problems others have described. My version might look like this:
const extract = (xs) => Object .values (xs .reduce (
(acc, {current_capacity, picker_id, process_time_in_minutes, products}) => {
const curr = acc [picker_id] || (acc [picker_id] = {
picker_id, total_volume: 0, total_time: 0, total_products: 0
})
curr .total_volume += current_capacity
curr .total_time += process_time_in_minutes
curr .total_products += products .reduce ((t, p) => t + p .picked_qty, 0)
return acc
},
{}
))
const arr = [{current_capacity: 6e3, picker_id: "icQrHPuE2fMZslceSG6liwuRar92", process_time_in_minutes: 10, products: [{product_id: 1, picked_qty: 2}, {product_id: 2, picked_qty: 3}]}, {current_capacity: 2500, picker_id: "icQrHPuE2fMZslceSG6liwuRar92", process_time_in_minutes: 20, products: [{product_id: 1, picked_qty: 10}]}, {current_capacity: 36e3, picker_id: "WIRzfIZALeftRk3DRGvh4nBdxQV2", process_time_in_minutes: 15, products: [{product_id: 1, picked_qty: 2}, {product_id: 2, picked_qty: 3}]}]
console .log (extract (arr))
.as-console-wrapper {max-height: 100% !important; top: 0}
You can also achieve your output by this
function getProductQty(arr){
let total = 0;
arr.forEach(prd => {
total += prd.picked_qty
})
return total;
}
const result = arr.reduce((acc,product) => {
if(!acc.hasOwnProperty(product.picker_id)){
acc[product.picker_id] = {
picker_id: product.picker_id,
total_volume: product.current_capacity,
total_time: product.process_time_in_minutes
}
acc[product.picker_id].total_products = getProductQty(product.products);
}else{
acc[product.picker_id].total_volume = acc[product.picker_id].total_volume + product.current_capacity
acc[product.picker_id].total_time = acc[product.picker_id].total_time + product.process_time_in_minutes
acc[product.picker_id].total_products = acc[product.picker_id].total_products + getProductQty(product.products);
}
return acc
},{})
console.log(Object.values(result),'result');
Issue with your implementation was if the existObj doesn't exit in your acc, you were directly pushing the obj instead you need to process the total first from the inner array of products.
I have updated your code to look cleaner and maintainable.
Approach:
build a dict for each picker_id which hold the computed data
convert dict to list
var result = arr.reduce((acc, obj) => {
if (!acc[obj.picker_id]) {
acc[obj.picker_id] = {
total_volume: 0,
total_time: 0,
total_products: 0
};
}
const selectedPicker = acc[obj.picker_id];
const total_picked = obj.products.reduce((acc2, item) => acc2 + item.picked_qty, 0);
selectedPicker.total_volume = selectedPicker.total_volume + obj.current_capacity;
selectedPicker.total_time =
selectedPicker.total_time + obj.process_time_in_minutes;
selectedPicker.total_products = selectedPicker.total_products + total_picked;
return acc;
}, {});
const formatted = Object.keys(result).reduce((acc, picker_id) => {
acc.push({
picker_id,
...result[picker_id]
})
return acc;
}, [])
console.log("formmated", formatted);

Rewriting reduce callback to stop reassigning accumulator

I have a function written like so:
type ChartData = {
x?: number[],
y?: number[],
z?: number[],
};
const data = [{ x: 1, y: 2, z: 3 }, { x: 4, y: 7, z: 10 }];
const formattedData = data.reduce((acc, el) => {
Object.entries(el!).forEach(([k, v]) => {
acc = { ...acc, [k]: [...(acc[k as keyof ChartData] || []), v] };
});
return acc;
}, {} as ChartData);
This code does what I want, but I'm getting an eslint error: no-param-reassign.
I know that I could disable the rule, but I wondered if there's a better way of writing this reduce callback so that I am not reassigning acc.
You can assign to acc[k] directly:
const data = [{ x: 1, y: 2, z: 3 }, { x: 4, y: 7, z: 10 }];
const formattedData = data.reduce((acc, el) => {
Object.entries(el).forEach(([k, v]) => {
acc[k] = (acc[k] || []).concat(v);
});
return acc;
}, {});
console.log(formattedData);
More code but also more efficient (fewer allocations and copies):
const data = [{ x: 1, y: 2, z: 3 }, { x: 4, y: 7, z: 10 }];
const formattedData = data.reduce((acc, el) => {
Object.entries(el).forEach(([k, v]) => {
if (!acc[k]) {
acc[k] = [];
}
acc[k].push(v);
});
return acc;
}, {});
console.log(formattedData);
This 'no-param-reassign' warning mean that you have modified the function params inside the function. So here maybe a workaround for this warning.
type ChartData = {
x?: number[],
y?: number[],
z?: number[],
};
const data = [{ x: 1, y: 2, z: 3 }, { x: 4, y: 7, z: 10 }];
const formattedData = data.reduce((acc, el) => {
let newAcc = { ...acc };
Object.entries(el!).forEach(([k, v]) => {
newAcc = { ...newAcc , [k]: [...(newAcc [k as keyof ChartData] || []), v] };
});
return newAcc ;
}, {} as ChartData);

Flatten JavaScript nested object

I have a nested object look like this:
let obj = {
F:{
asian: {
"35-44": 1,
"55-": 1,
},
"asian/black": {
"0-24": 1,
"35-44": 1,
"45-54": 2,
},
},
M:{
asian: {
"35-44": 1,
"55-": 1,
},
white: {
"0-24": 1,
"35-44": 1,
"45-54": 2,
},
},
}
And I want to flatten the object to this:
res = {
F: 6,
M: 6,
asian: 4,
"asian/black": 4,
white: 4,
"0-24": 2,
"35-44": 4,
"45-54": 4,
"55-": 2,
}
That every value in res should be the sum of the deepest object values(F, M) and object values with the same key(0-24, 35-44...). I feel this can be done using recursion and just can't get it right. The code I write:
let returnVal = 0
const flatten = (obj, prefix = '', res = {}) => {
return Object.entries(obj).reduce((r, [key, val]) => {
if(typeof val === 'object'){
flatten(val, key, r)
} else {
res[key] = val
returnVal = val;
}
if (key in res) {
res[key] += returnVal
} else {
res[key] = 0
res[key] += returnVal
}
return r
}, res)
}
console.log(flatten(obj))
it will output:
result = {
"0-24": 2,
"35-44": 2,
"45-54": 4,
"55-": 2,
F: 2,
M: 2,
asian: 2,
"asian/black": 2,
white: 2,
}
F, M, and some other keys are not correct. Thanks!
Another, perhaps simpler, approach is as follows:
const consolidate = (obj, path = [], results = {}) =>
Object .entries (obj) .reduce ((results, [k, v]) =>
Object (v) === v
? consolidate (v, [...path, k], results)
: [...path, k] .reduce (
(results, n) => ({...results, [n] : (results[n] || 0) + v}),
results
),
results)
const data = {F: {asian: {"35-44": 1, "55-": 1}, "asian/black": {"0-24": 1, "35-44": 1, "45-54": 2}}, M: {asian: {"35-44": 1, "55-": 1}, white: {"0-24": 1, "35-44": 1, "45-54": 2}}}
console .log (consolidate (data))
.as-console-wrapper {min-height: 100% !important; top: 0}
We recursively track paths taken through the object, such as ['F', 'asian/black', '45-54'] or ['M', 'white'] or simply ['f'] as well as an object containing the final results. When we the value at the current node is an object, we recur, adding the current property name to the path. When it's not (for this data it must therefore hit a number), we hit a base case in which we take each node in the current path, and update the results object by adding that number to the value for the node in the results object, or setting it to the current value if that value doesn't exist.
There is a potential issue with the default parameters, as described in another Q & A. If someone tried to map the consolidate function directly over an array of input objects, it would fail. If this is a concern, it's easy enough to swap the default parameters for a wrapper function:
const _consolidate = (obj, path, results) =>
Object .entries (obj) .reduce ((results, [k, v]) =>
Object (v) === v
? _consolidate (v, [...path, k], results)
: [...path, k] .reduce (
(results, n) => ({...results, [n] : (results[n] || 0) + v}),
results
),
results)
const consolidate = (obj) =>
_consolidate (obj, [], {})
const data = {
F: {
asian: {
"35-44": 1,
"55-": 1,
},
"asian/black": {
"0-24": 1,
"35-44": 1,
"45-54": 2,
},
},
M: {
asian: {
"35-44": 1,
"55-": 1,
},
white: {
"0-24": 1,
"35-44": 1,
"45-54": 2,
},
},
};
const isObject = obj => Object.prototype.toString.call(obj) === "[object Object]";
function nestKeys(obj, parent = "") {
return Object.keys(obj).map(key => {
const k = parent.length ? [parent, key].join(".") : key;
if (!isObject(obj[key])) {
return k;
}
return nestKeys(obj[key], k);
}).flat();
}
function flatObj(obj) {
const map = {};
const keys = nestKeys(obj);
keys.forEach(nestedKey => {
const splited = nestedKey.split(".");
const val = splited.reduce((acc, cur) => acc[cur], obj);
splited.forEach(k => {
map[k] = (map[k] || 0) + val;
})
});
return map;
}
console.log(flatObj(data));

Summarize array of objects and calculate average value for each unique object name

I have an array like so:
var array = [
{
name: "a",
value: 1
},
{
name: "a",
value: 2
},
{
name: "a",
value: 3
},
{
name: "b",
value: 0
},
{
name: "b",
value: 1
}
];
And I need an array like this:
var newarray = [
{
name: "a",
value: 2
},
{
name: "b",
value: 0.5
}
]
Where the new array has each unique name as an object with the average value.
Is there an easy way to accomplish this?
You'll have to loop through the array, computing the sum and counts for each object. Here's a quick implementation:
function average(arr) {
var sums = {}, counts = {}, results = [], name;
for (var i = 0; i < arr.length; i++) {
name = arr[i].name;
if (!(name in sums)) {
sums[name] = 0;
counts[name] = 0;
}
sums[name] += arr[i].value;
counts[name]++;
}
for(name in sums) {
results.push({ name: name, value: sums[name] / counts[name] });
}
return results;
}
Demonstration
Note, this kind of thing can be made much easier if you use a library like Underscore.js:
var averages = _.chain(array)
.groupBy('name')
.map(function(g, k) {
return {
name: k,
value: _.chain(g)
.pluck('value')
.reduce(function(x, y) { return x + y })
.value() / g.length
};
})
.value();
Demonstration
var array = [
{
name: "a",
value: 1
},
{
name: "a",
value: 2
},
{
name: "a",
value: 3
},
{
name: "b",
value: 0
},
{
name: "b",
value: 1
}
];
var sum = {};
for(var i = 0; i < array.length; i++) {
var ele = array[i];
if (!sum[ele.name]) {
sum[ele.name] = {};
sum[ele.name]["sum"] = 0;
sum[ele.name]["count"] = 0;
}
sum[ele.name]["sum"] += ele.value;
sum[ele.name]["count"]++;
}
var result = [];
for (var name in sum) {
result.push({name: name, value: sum[name]["sum"] / sum[name]["count"]});
}
console.log(result);
You can do it with Alasql library with one line of code:
var newArray = alasql('SELECT name, AVG([value]) AS [value] FROM ? GROUP BY name',
[array]);
Here I put "value" in square brackets, because VALUE is a keyword in SQL.
Try this example at jsFiddle
Here is a ES2015 version, using reduce
let arr = [
{ a: 1, b: 1 },
{ a: 2, b: 3 },
{ a: 6, b: 4 },
{ a: 2, b: 1 },
{ a: 8, b: 2 },
{ a: 0, b: 2 },
{ a: 4, b: 3 }
]
arr.reduce((a, b, index, self) => {
const keys = Object.keys(a)
let c = {}
keys.map((key) => {
c[key] = a[key] + b[key]
if (index + 1 === self.length) {
c[key] = c[key] / self.length
}
})
return c
})
And a possible solution using ECMA5 (as we seem to be missing one)
var sums = {},
averages = Object.keys(array.reduce(function (previous, element) {
if (previous.hasOwnProperty(element.name)) {
previous[element.name].value += element.value;
previous[element.name].count += 1;
} else {
previous[element.name] = {
value: element.value,
count: 1
};
}
return previous;
}, sums)).map(function (name) {
return {
name: name,
average: this[name].value / this[name].count
};
}, sums);
On jsFiddle
October 2020, I think this is the shortest way (ES6+)
const getAveragesByGroup = (arr, key, val) => {
const average = (a, b, i, self) => a + b[val] / self.length;
return Object.values(
arr.reduce((acc, elem, i, self) => (
(acc[elem[key]] = acc[elem[key]] || {
[key]: elem[key],
[val]: self.filter((x) => x[key] === elem[key]).reduce(average, 0),
}),acc),{})
);
};
console.log(getAveragesByGroup(array, 'name', 'value'))
Try by yourself :)

Categories

Resources