Why does my reduce accumulator reset? - javascript

How do I retain the accumulative value of my reduce function? Each iteration resets the object value.
const a = [1, 2, 3, 4, 5];
const b = {
1: {
name: 'Dan',
age: 25
},
2: {
name: 'Peter',
age: 28
},
3: {
name: 'Mark',
age: 38
},
4: {
name: 'Larry',
age: 32
},
5: {
name: 'Simon',
age: 25
},
}
const f = a.reduce((acc, val) => {
console.log({
acc
})
return {
[val]: {
age: b[val].age
}
}
}, {})
console.log(f); // 5: {age: 25}
My desired outcome would be:
{
1: { age: 25 },
2: { age: 28 },
3: { age: 38 },
4: { age: 32 },
5: { age: 25 },
}
(This example is a demo)

Add the previous accumulator to the returned value using object spread (like this example) or Object.assign():
const a = [1, 2, 3, 4, 5];
const b = {"1":{"name":"Dan","age":25},"2":{"name":"Peter","age":28},"3":{"name":"Mark","age":38},"4":{"name":"Larry","age":32},"5":{"name":"Simon","age":25}};
const f = a.reduce((acc, val) => ({
...acc, // previous accumulator
[val]: {
age: b[val].age
}
}), {})
console.log(f); // 5: {age: 25}

As per MDN
The reduce() method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.
On each iteration you return new object inside of reduce() function and you are not storing previous value of that accumulator. So you need to be merge or assign previous value with new value.
One way you can use Object.assign() method to get the required result.
DEMO
const a = [1, 2, 3, 4, 5];
const b = {1: {name: 'Dan',age: 25},2: {name: 'Peter',age: 28},3: {name: 'Mark',age: 38},4: {name: 'Larry',age: 32},5: {name: 'Simon',age: 25}}
let result = a.reduce((acc, val) => Object.assign(acc,{[val]: {age:b[val].age}}), {});
console.log(result);
.as-console-wrapper {max-height: 100% !important;top: 0;}
second way you can do like this obje[val]=newValue and return accumulator.
DEMO
const a = [1, 2, 3, 4, 5];
const b = {1: {name: 'Dan',age: 25},2: {name: 'Peter',age: 28},3: {name: 'Mark',age: 38},4: {name: 'Larry',age: 32},5: {name: 'Simon',age: 25}}
let result = a.reduce((acc, val) =>{
acc[val]= {age:b[val].age};
return acc;
}, {});
console.log(result);
.as-console-wrapper {max-height: 100% !important;top: 0;}
Another way you can combine using the spread syntax
DEMO
const a = [1, 2, 3, 4, 5];
const b = {1: {name: 'Dan',age: 25},2: {name: 'Peter',age: 28},3: {name: 'Mark',age: 38},4: {name: 'Larry',age: 32},5: {name: 'Simon',age: 25}}
let result = a.reduce((acc, val) => {
return {...acc,...{[val]:{age:b[val].age}}}
}, {});
console.log(result);
.as-console-wrapper {max-height: 100% !important;top: 0;}

Your reduce accumulator can also reset if you happen to have an if else statement in your reduce function, whereby you return an accumulator for the if statement but forget to handle the else statement and do not return an accumulator - in that case when the else statement is triggered no accumulator is returned and the accumulator becomes undefined.
For example this works:
let reducedArray = array.reduce((acc, curr) => {
if (
curr[
property_name
]
) {
return {
...acc,
...{
[curr.id]:
(acc[curr.id] ?? 0) +
curr[
property_name
]!
},
};
} else {
return acc;
}
}, {} as { [key: string]: number });
But this will not work:
let reducedArray = array.reduce((acc, curr) => {
if (
curr[
property_name
]
) {
return {
...acc,
...{
[curr.id]:
(acc[curr.id] ?? 0) +
curr[
property_name
]!
},
};
}
}, {} as { [key: string]: number });

Related

Transform nested object of objects of arrays in JS

Besides the horrible name of the question my question is quite simple. I have this object:
let test = {
date1: [
{
time: 1,
value: 5,
},
{
time: 2,
value: 6,
},
],
date2: [
{
time: 1,
value: 20,
},
{
time: 2,
value: 10,
},
],
};
That I want to transform to something like this:
let result = {
date1: {
values: [5, 6],
times: [1, 2],
},
date2: {
values: [1, 2], // easier to summarise?!
times: [10, 20],
},
};
I actually want to do this in order to summarise the value-values for each date. I thought that if I have them in an array it would be easier to summarise them. I know there are other forms to do this (and I'd be happy to see any solutions).
My current approach does not what I want it to do. It looks like this:
let keys = Object.keys(test);
let red = keys.reduce((acc, curr) => {
return (acc[curr] = test[curr].map((e) => e.value));
}, {});
console.log(`red: `, red);
And produces this:
red: [ 20, 10 ]
This
return (acc[curr] = test[curr].map((e) => e.value));
is equivalent to
acc[curr] = test[curr].map((e) => e.value);
return acc[curr];
going inside a nested key of the accumulator on every iteration - which isn't the logic you want. Return the whole accumulator on a separate line, so previously assigned values don't get lost, and you also need to account for both the time and value properties of the array being iterated over - your => e.value only extracts one of the two properties you want.
let test = {
date1: [
{
time: 1,
value: 5,
},
{
time: 2,
value: 6,
},
],
date2: [
{
time: 1,
value: 20,
},
{
time: 2,
value: 10,
},
],
};
const keys = Object.keys(test);
const result = keys.reduce((acc, key) => {
acc[key] = {
values: test[key].map(({ value }) => value),
times: test[key].map(({ time }) => time),
};
return acc;
return acc;
}, {});
console.log(result);
or do
let test = {
date1: [
{
time: 1,
value: 5,
},
{
time: 2,
value: 6,
},
],
date2: [
{
time: 1,
value: 20,
},
{
time: 2,
value: 10,
},
],
};
const result = Object.fromEntries(
Object.entries(test).map(([key, arr]) => [
key,
{
values: arr.map(({ value }) => value),
times: arr.map(({ time }) => time),
}
])
);
console.log(result);
Try modifying it a little like this:
let result = Object.keys(test).reduce((acc, key) => {
test[key].forEach((item) => {
acc.push({
date: key,
time: item.time,
value: item.value,
});
});
return acc;
}
, []);
console.log(result);
Assuming all inner objects have the same keys and no date array is empty:
let test = {date1:[{time:1,value:5},{time:2,value:6},],date2:[{time:1,value:20},{time:2,value:10},]};
let keys = Object.keys(test);
let red = keys.reduce((acc, curr) => ({
...acc,
[curr]: Object.keys(test[curr][0])
.reduce((acc, key) => ({
...acc,
[key + 's']: test[curr].map(o => o[key])
}), {})
}), {});
console.log(`red: `, red);
There is no need to first create arrays when you want to sum up values from different objects. It looks like you want to achieve this result:
{
date1: 11
date2: 30
}
The idea to use reduce is fine (for summing up values). You can use Object.entries and Object.fromEntries on top of that, in order to create the new object structure:
const test = {date1: [{time: 1,value: 5,},{time: 2,value: 6,},],date2: [{time: 1,value: 20,},{time: 2,value: 10,},],};
const result = Object.fromEntries(
Object.entries(test).map(([key, arr]) =>
[key, arr.reduce((sum, {value}) => sum + value, 0)]
)
);
console.log(result);

array reduce amount value into string format

I have an array of items, where i need to get a string of each product price.
const input = [{id: 1, amount: 20}, {id: 2, amount: 40}, {id: 3, amount: 90}]
const output = input?.reduce((acc, curr) => {
acc += `$${curr.amount}+`;
return acc;
}, '')
console.log(output)
Expected output is $20+$40+$90
But when i am trying this code i am getting the sum as $150 and i don't want to have + at the last if there are no more items.
Why Array.reduce()? This is a classic example for Array.map():
const input = [{id: 1, amount: 20}, {id: 2, amount: 40}, {id: 3, amount: 90}]
const expression = input.map(
({ amount }) => `$${amount}` // destructure the object, keep only .amount
).join('+');
console.log(expression);
Read about destructuring in the JavaScript documentation.
You can use map to extract the values followed by a join to create the string.
input.map(i => `$${i.amount}`).join('+')
Use split, and your code almost works
const input = [{id: 1, amount: 20}, {id: 2, amount: 40}, {id: 3, amount: 90}]
const output = input?.reduce((acc, curr) => {
acc.push(curr.amount + "$");
return acc;
}, []).join("+")
console.log(output)
const input = [{ id: 1, amount: 20 }, { id: 2, amount: 40 }, { id: 3, amount: 90 }]
const output = input?.slice(1).reduce((acc, curr) => {
acc += `+$${curr.amount}`;
return acc;
}, input.length ? `$${input[0].amount}` : '');
console.log(output)
with minimum manipulation
To add to the answers, we can use the currentIndex in the callback function in reduce as the third argument.
const input = [{id: 1, amount: 20}, {id: 2, amount: 40}, {id: 3, amount: 90}, {id: 4, amount: 55}]
const output = input?.reduce((acc, curr, index) => {
acc += `$${curr.amount}`;
if (index < input.length - 1) acc += '+'
return acc;
}, '')
console.log(output)
`

How can I build an exception handler?

I need to handle "null","yes"/"no" as exeptions that are = to 0 and strings with "4" as points. When they are ordered it should be printed out as it was given as input. I tried to use a for loop to reach the scores but it doesn't work. How can I build an exception handler?
function reorder(people) {
const arr = people;
for (let i in arr) {
for (let a in i.scores ) {
if (a === null || a === "yes" || a === "no") {
a = 0;
} else if (typeof a === "string") {
a = Number(a);
}
}
}
const sorted = arr
.map((e) => ({
...e,
sum: e.scores.reduce((total, score) => total + score, 0),
}))
.sort((a, b) => b.sum - a.sum || a.name.localeCompare(b.name))
.map(({ sum, ...e }) => e);
return sorted;
}
const input = [
{ name: "Joe", scores: [1, 2, "4.1"] },
{ name: "Jane", scores: [1, null, 3] },
{ name: "John", scores: [1, 2, 3] },
];
const output = reorder(input);
console.log(output);
You coud adjust reduce callback and convert the score to number or take zero for adding.
function reorder(people) {
return people
.map(e => ({ ...e, sum: e.scores.reduce((total, score) => total + (+score || 0), 0) }))
.sort((a, b) => b.sum - a.sum || a.name.localeCompare(b.name))
.map(({ sum, ...e }) => e);
}
const
input = [{ name: "Joe", scores: [1, 2, "4.1"] }, { name: "Jane", scores: [1, null, 3] }, { name: "John", scores: [1, 2, 3] }],
output = reorder(input);
console.log(output);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Format data for chart

I'm having some trouble formatting/transforming some simple data into a format that I can use to graph, and I'm hoping someone might help me solve. Currently, I have something like this
somedata=
{test1: {good: 3, bad: 2, redo: 2}}
{test2: {good: 4, bad: 3}}
{test3: {good: 3, redo: 4}}
into something like
series:
[{name: "good", data: [3,4,3]},
{name: "bad", data: [2,3,0]},
{name: "redo", data: [2,0,4]}]
I can grab the categories by using Object.keys(somedata) easy enough i.e. ['test1', 'test2', 'test3'] but having problem formatting the rest of the data. I tried something like
let combine = {};
Object.values(somedata).map((row) => {
for (const [key, value] of Object.entries(row)) {
combine.hasOwnProperty(key)
? combine[key].push(value)
: (combine[key] = [value]);
}
console.log("combined", combine);
});
but quickly realized that it won't add 0 when key doesn't exist, which is required for the chart to compare between the different series, such as bar charts. So, any help is appreciated.
You can first collect all unique values and then using array#reduce and other array methods generate all the values corresponding to each key in an object accumaltor.
const somedata = [{test1: {good: 3, bad: 2, redo: 2}}, {test2: {good: 4, bad: 3}}, {test3: {good: 3, redo: 4}}],
uniqueValues = [...new Set(
somedata.reduce((r,o) => {
Object.values(o).forEach(ob => {
r.push(...Object.keys(ob));
});
return r;
}, [])
)];
result = Object.values(somedata.reduce((r, o) => {
Object.values(o).forEach(ob => {
uniqueValues.forEach(k => {
r[k] = r[k] || { name: k, data: []};
ob[k] ? r[k].data.push(ob[k]): r[k].data.push(0);
});
});
return r;
},{}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
something like this: for each test, group categories (can optionally restrict to a subset) - assume Zero for missing category
const someData = {test1: {good: 3, bad: 2, redo: 2}, test2: {good: 4, bad: 3}, test3: {good: 3, redo: 4}};
function prepMyGraphData(data, fields) {
let out = {
}
for (const [k, el] of Object.entries(data)) {
const _fields = new Set((fields || Object.keys(el)).concat(Object.keys(out)));
for (const f of _fields) {
const v = el.hasOwnProperty(f) ? el[f] || 0 : 0 ; // own field or 0
if (out.hasOwnProperty(f)) {
out[f].data.push(v) // existing category
}else{
out[f] = {name: f, data: [v]} // new category entry
}
}
}
return Object.values(out)
}
let fields = ['good', 'bad', 'redo']; // OR, undefined, for ALL own properties
const data = prepMyGraphData(someData, fields);

If an object's key's value is an array, it will generate a new object

How to ensure that you get the desired output, such as using lodash
I want to pass a method to generate my mongoose query conditions.
let obj = {
age: [22],
sex: [1, 2],
name: [{first: "Wang", last: "Hao"}, {first: "Tang", last: "Jie"}]
}
/* expected
newObj = {
$and: [
{
age: 22
},
{
$or: [
{sex: 1},
{sex: 2},
],
},
{
$or: [
{name: {first: "Wang", last: "Hao"}},
{name: {first: "Tang", last: "Jie"}}
],
},
]
}
*/
Plain JavaScript
You don't strictly need Lodash to do it. Here is how you can achieve this with plain JavaScript. The code is slightly more verbose for descriptive purposes:
let obj = {
age: [22],
sex: [1, 2],
name: [{first: "Wang", last: "Hao"}, {first: "Tang", last: "Jie"}]
}
/*
* take a key-value pair where the value is an array
* and turn it into an array where each object
* is a key-value pair. For example:
* `["age", [22]]` into [{age: 22}]
* `["sex", [1, 2]]` into `[{sex: 1}, {sex: 2}]`
*/
const toFilterObjects = ([key, values]) => values.map(value => ({[key]: value}));
/*
* combine objects into an OR relationship
* or a single one into just equality. For example:
* `[{age: 22}]` into `{age: 22}`
* `[{sex: 1}, {sex: 2}]` into `{'$or': [{sex: 1}, {sex: 2}]}`
*/
const combineOr = arr => {
if (arr.length === 1) return arr[0];
return {
'$or': arr
}
}
/*
* combine objects into an AND relationship
* or a single one into just equality. For example:
* `[{age: 22}]` into `{age: 22}`
* `[{sex: 1}, {sex: 2}]` into `{'$and': [{sex: 1}, {sex: 2}]}`
*/
const combineAnd = arr => {
if (arr.length === 1) return arr[0];
return {
'$and': arr
}
}
//manipulate all values of the object to turn them into OR filters
const temp = Object.entries(obj)
.map(toFilterObjects)
.map(combineOr);
//AND all filters
const result = combineAnd(temp);
console.log(result);
The biggest improvement to this code is that combineOr and combineAnd can actually be generalised in a single combine operation and you can derive the two from it. This will reduce the code duplication:
const combine = key => arr => {
if (arr.length === 1) return arr[0];
return {
[key]: arr
}
}
const combineOr = combine('$or');
const combineAnd = combine('$and');
In addition, the temp variable is generally unneeded but wanted to make the separation between the operations within the array and the operation on the whole array.
Normal Lodash
Lodash can help here. It's not reducing the operations to a single function call but it can help clean up some of the extra cruft and produce slightly better looking code slightly easier than using vanilla JavaScript functionality by using chaining:
let obj = {
age: [22],
sex: [1, 2],
name: [{first: "Wang", last: "Hao"}, {first: "Tang", last: "Jie"}]
}
const toFilterObjects = ([key, values]) => values.map(value => ({[key]: value}) );
const combine = key => arr => {
if (arr.length === 1) return arr[0];
return {
[key]: arr
}
}
const result = _(obj)
.entries()
.map(toFilterObjects)
.map(combine('$or'))
.thru(combine('$and'));
console.log(result)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
Lodash FP
Lodash also has a functional programming build that allows structuring the code using more functional approach. If already using functional style in the project, Lodash FP is preferable to plain Lodash but even without having a predominantly functional code, this is an alternative:
const { entries, map, flow } = _;
let obj = {
age: [22],
sex: [1, 2],
name: [{first: "Wang", last: "Hao"}, {first: "Tang", last: "Jie"}]
}
const toFilterObjects = ([key, values]) => values.map(value => ({[key]: value}) );
const combine = key => arr => {
if (arr.length === 1) return arr[0];
return {
[key]: arr
}
}
const process = flow(
entries,
map(toFilterObjects),
map(combine('$or')),
combine('$and')
)
const result = process(obj)
console.log(result)
<script src="https://cdn.jsdelivr.net/g/lodash#4(lodash.min.js+lodash.fp.min.js)"></script>
If I understand you mean correctly, you are looking for something like this:
let obj = {
age: [22],
sex: [1, 2],
name: [{first: "Wang", last: "Hao"}, {first: "Tang", last: "Jie"}]
}
let output = { $and: [] };
for (let prop in obj) {
if (obj[prop] instanceof Array) {
if (obj[prop].length === 1) {
output.$and.push({ [prop]: obj[prop][0] })
} else {
let $or = [];
for (let item of obj[prop]) {
$or.push({ [prop]: item });
}
output.$and.push({ $or: $or });
}
}
}
console.log(output);
Use Array.prototype.reduce()
let obj = {
age: [22],
sex: [1, 2],
name: [{first: "Wang", last: "Hao"}, {first: "Tang", last: "Jie"}]
}
var newObj = Object.keys(obj).reduce((accumulator, key)=> {
let elem = {}
if (Array.isArray(obj[key])){
if (obj[key].length > 1) {
elem['$or'] = obj[key].map(item => ({[key]: item}))
} else {
elem[key] = obj[key][0]
}
} else {
elem[key] = obj[key]
}
accumulator['$and'].push(elem)
return accumulator
}, {'$and': []})
console.log(newObj)
let obj = {
age: [22],
sex: [1, 2],
name: [{ first: "Wang", last: "Hao" }, { first: "Tang", last: "Jie" }]
}
function generateQuery(obj) {
let andQuery = [];
for (let key in obj) {
let value = obj[key];
if (value.length === 1)
andQuery.push({ [key]: value[0] });
else {
let orQuery = value.map(v => {
return { [key]: v };
});
andQuery.push({ "$or": orQuery });
}
}
return { "$and": andQuery }
}
generateQuery(obj);

Categories

Resources