Convert objects in nested array of objects - javascript

I'm having a data structure that looks like this:
[
[
{ word: "china", count: 0 },
{ word: "kids", count: 1 },
{ word: "music", count: 0 },
],
[
{ word: "china", count: 3 },
{ word: "kids", count: 0 },
{ word: "music", count: 2 },
],
[
{ word: "china", count: 10 },
{ word: "kids", count: 3 },
{ word: "music", count: 2 },
]
];
I want to calculate the minimum and maximum number of counts for each value of the property word in the nested array.
For example, the maximum counts for the word "china" is 10 and the minimum is 0.
I would like to accomplish something like this:
{ word: "china", min: 0, max: 10 }
How do I do this?

There are lots of ways to solve this. If you first transform the data into an object whose keys are the words and whose values are the counts of each word, then you can perform any number of operations on the counts (not just finding min and max):
function transform (array) {
const counts = {};
for (const {count, word} of array.flat()) {
(counts[word] ??= []).push(count);
}
return counts;
}
const input = [
[
{ word: "china", count: 0 },
{ word: "kids", count: 1 },
{ word: "music", count: 0 },
],
[
{ word: "china", count: 3 },
{ word: "kids", count: 0 },
{ word: "music", count: 2 },
],
[
{ word: "china", count: 10 },
{ word: "kids", count: 3 },
{ word: "music", count: 2 },
],
];
const transformed = transform(input);
console.log(transformed);
//=> { china: [ 0, 3, 10 ], kids: [ 1, 0, 3 ], music: [ 0, 2, 2 ] }
for (const [word, counts] of Object.entries(transformed)) {
const sorted = [...counts].sort((a, b) => a - b);
console.log({
word,
min: sorted.at(0),
max: sorted.at(-1),
});
}
//=>
// { word: "china", min: 0, max: 10 }
// { word: "kids", min: 0, max: 3 }
// { word: "music", min: 0, max: 2 }
Ref:
Array.prototype.flat()
Logical nullish assignment operator (??=)
Object.entries()
Shallow cloning an array using spread syntax (...)
Using a custom compare function with Array.prototype.sort()
Array.prototype.at()

You can use Array#reduce to collect the minimum and maximum values for a given word while looping over the array.
let arr=[[{word:"china",count:0},{word:"kids",count:1},{word:"music",count:0},],[{word:"china",count:3},{word:"kids",count:0},{word:"music",count:2},],[{word:"china",count:10},{word:"kids",count:3},{word:"music",count:2},]];
const getStats = (arr, w) => arr.flat().reduce((acc, {word, count})=>{
if (w === word) acc.min = Math.min(acc.min, count),
acc.max = Math.max(acc.max, count);
return acc;
}, { word : w, min: Infinity, max: -Infinity});
console.log(getStats(arr, 'china'));

Related

Extract ranges from sequential values [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 12 months ago.
Improve this question
Task 1:
I have my collection with documents in mongodb with value from sequential ranges as follow :
{x:1}
{x:2}
{x:3}
{x:5}
{x:6}
{x:7}
{x:8}
{x:20}
{x:21}
I need to extract a list of sequential ranges in the form(the count is not compulsory , but I need at least the first and last values from the range):
{x:[1,3] , count:3}
{x:[5,8], count:4}
{x:[20,21],count:2}
or
{ min:1 , max:3 , count:3}
{ min:5 , max:8 , count:4}
{ min:20 , max:21 , count:2}
Please, advice suitable solution , collection has ~100M docs , some of the values are in 10 digit ranges others in 15 digit ranges , but they are all sequentially incremental in their range?
Task 2:
Same think like in Task 1 , but taken based on custom sequence step ,
for example if the sequence step is 3:
{y:1}
{y:3}
{y:5}
{y:20}
{y:22}
need to produce:
{y:[1,5] ,count:3}
{y:[20,22]} , count:2}
Thanks!
P.S.
I succeeded partially to get some ranges picture by fetch distribution by number of digits range , but this seems to be very general:
db.collection.aggregate([
{
$addFields: {
range: {
$strLenCP: {
$toString: "$x"
}
}
}
},
{
$group: {
_id: "$range",
minValue: {
$min: "$x"
},
maxValue: {
$max: "$x"
},
Count: {
$sum: 1
}
}
},
{
$addFields: {
x: [
{
$toString: "$minValue"
},
{
$toString: "$maxValue"
}
]
}
},
{
$project: {
range: "$_id",
"_id": 0,
x: 1,
Count: 1
}
},
{
$sort: {
range: 1
}
}
])
playground
Here is another way of querying - produces result with format [ { min: 1 , max: 3 , count: 3 }, ... ]:
db.collection.aggregate([
{
$sort: { x: 1 }
},
{
$group: {
_id: null,
docs: { $push: "$x" },
firstVal: { $first: "$x" },
lastVal: { $last: "$x" }
}
},
{
$project: {
_id: 0,
output: {
$let: {
vars: {
result: {
$reduce: {
input: "$docs",
initialValue: {
prev: { $add: [ "$firstVal", -1 ] },
val: { min: "$firstVal", max: 0, count: 0 },
vals: [ ]
},
in: {
$cond: [
{ $eq: [ { $subtract: [ "$$this", "$$value.prev" ] }, 1 ] },
{
prev: "$$this",
val: {
min : "$$value.val.min",
max: "$$value.val.max",
count: { $add: [ "$$value.val.count", 1 ] }
},
vals: "$$value.vals"
},
{
vals: {
$concatArrays: [
"$$value.vals",
[ { min : "$$value.val.min", max: "$$value.prev", count: "$$value.val.count" } ]
]
},
val: { min: "$$this", max: "$lastVal", count: 1 },
prev: "$$this"
},
]
}
}
}
},
in: {
$concatArrays: [ "$$result.vals", [ "$$result.val" ] ]
}
}
}
}
},
])
Use $setWindowFields instead of $group all data
db.collection.aggregate([
{
$setWindowFields: {
partitionBy: "",
sortBy: { x: 1 },
output: {
c: {
$push: "$x",
window: {
range: [ -3, 0 ]
}
}
}
}
},
{
$set: {
"c": {
"$cond": {
"if": { "$gt": [ { "$size": "$c" }, 1 ] },
"then": 0,
"else": 1
}
}
}
},
{
$setWindowFields: {
partitionBy: "",
sortBy: { x: 1 },
output: {
g: {
$sum: "$c",
window: {
documents: [ "unbounded", "current" ]
}
}
}
}
},
{
$group: {
_id: "$g",
count: { $sum: 1 },
max: { "$max": "$x" },
min: { "$min": "$x" }
}
}
])
mongoplayground
In PostgreSQL
CREATE TABLE test (
id INT,
x INT
);
INSERT INTO test VALUES (1, 1);
INSERT INTO test VALUES (2, 3);
INSERT INTO test VALUES (3, 5);
INSERT INTO test VALUES (4, 20);
INSERT INTO test VALUES (5, 22);
SELECT
MAX(x) AS max, MIN(x) AS min, COUNT(*) AS count
FROM (
SELECT *, SUM(inc) OVER(ORDER BY x) AS grp
FROM (
SELECT *, CASE WHEN x - LAG(x) OVER(ORDER BY x) < 4 THEN NULL ELSE 1 END AS inc
FROM test
) q
) q
GROUP BY grp
db-fiddle
using $reduce
if i'm not mistaken for task2 just change 1 in $cond, $ne to any sequence step you want
playground
db.collection.aggregate([
{
"$sort": {
x: 1
}
},
{
$group: {
_id: null,
temp: {
$push: "$$ROOT"
}
}
},
{
"$project": {
_id: 0,
"temp_field": {
"$reduce": {
"input": "$temp",
"initialValue": {
"prev": -999999,
"min": -999999,
"count": 0,
"ranges": []
},
"in": {
"prev": "$$this.x",
"count": {
"$cond": [
{
$gt: [
{
"$subtract": [
"$$this.x",
"$$value.prev"
]
},
1//sequence step
],
},
1,
{
"$add": [
"$$value.count",
1
]
}
]
},
"min": {
"$cond": [
{
$gt: [
{
"$subtract": [
"$$this.x",
"$$value.prev"
]
},
1//sequence step
],
},
"$$this.x",
"$$value.min"
]
},
"ranges": {
"$concatArrays": [
"$$value.ranges",
{
"$cond": [
{
$gt: [
{
"$subtract": [
"$$this.x",
"$$value.prev"
]
},
1//sequence step
],
},
[
{
max: "$$value.prev",
min: "$$value.min",
count: "$$value.count"
}
],
[]
]
}
]
}
}
}
}
}
},
{
"$project": {
ranges: {
"$concatArrays": [
"$temp_field.ranges",
[
{
max: "$temp_field.prev",
min: "$temp_field.min",
count: "$temp_field.count"
}
]
]
}
}
}
])
and at the end pop the first element from array
Comment by R2D2 after testing in the real use case I hit the memory limit with allowDiskUse: true:
2022-02-14T09:38:27.575+0100 E QUERY [js] Error: command failed: {
"ok" : 0,
"errmsg" : "$push used too much memory and cannot spill to disk. Memory limit: 104857600 bytes",
"code" : 146,
"codeName" : "ExceededMemoryLimit",
Increased the memory to 2GB ( max allowed ) with:
db.adminCommand({setParameter:1 , internalQueryMaxPushBytes: 2048576000 })
But still faced the limit , then decided to split the collection to small ones so finally got my results , thank you once again!

Javascript to manipulate object structure

I have a javascript object with the following schema:
[{
face:"n",
values:[
{time:"15:00",value:5}
{time:"15:02",value:6}
]
},{
face:"e",
values:[
{time:"15:01",value:7}
{time:"15:02",value:8}
]
},{
face:"s",
values:[
{time:"15:01",value:7}
{time:"15:02",value:8}
]
},{
face:"w",
values:[
{time:"15:01",value:7}
{time:"15:02",value:8}
]
}]
How to convert it to the following structure:
[
{time:"15:00","n":5,"e":null,"s":null,"w":null},
{time:"15:01","n":null,"e":7,"s":7,"w":7},
{time:"15:02","n":6,"e":8,"s":8,"w":8},
]
The number of faces will be fixed (north, east, south, west)
It is possible that some timestamps are missing. In that case we need to fill the value with 'null' (see example above).
You can easily achieve the result using Map, reduce and forEach
with this approach the ordering is not particular, If you want the exact time ordering then you can sort it using the following custorm sort comparator function
result.sort((a, b) => +a.time.match(/\d+/g).join("") - b.time.match(/\d+/g).join(""))
const arr = [
{
face: "n",
values: [
{ time: "15:00", value: 5 },
{ time: "15:02", value: 6 },
],
},
{
face: "e",
values: [
{ time: "15:01", value: 7 },
{ time: "15:02", value: 8 },
],
},
{
face: "s",
values: [
{ time: "15:01", value: 7 },
{ time: "15:02", value: 8 },
],
},
{
face: "w",
values: [
{ time: "15:01", value: 7 },
{ time: "15:02", value: 8 },
],
},
];
const map = new Map();
const result = [
...arr.reduce((acc, { face, values }) => {
values.forEach(({ time, value }) => {
if (!acc.has(time))
acc.set(time, { time, n: null, e: null, s: null, w: null });
acc.get(time)[face] = value;
});
return acc;
}, map)
.values(),
];
console.log(result);
/* This is not a part of answer. It is just to give the output full height. So IGNORE IT */
.as-console-wrapper { max-height: 100% !important; top: 0; }
Because you haven't added what you tried so far (which is kinda useless for this task, IMHO), I will just answer in form of how I would tackle this task.
Locate your unique value in your expected output: time.
Loop through your array, and create an object with keys based on time. Save the properties name and each of the four faces, by first creating null values for all faces, and then filling it out with one value at time as a new timestamp appears in the array.
Here is how the whole object will look like:
{
"15:00": {time:"15:00","n":5,"e":null,"s":null,"w":null},
"15:01": {time:"15:01","n":null,"e":7,"s":7,"w":7},
"15:02": {time:"15:02","n":6,"e":8,"s":8,"w":8
}
Loop through the object—with for... in, [edit] or by simply using Object.values—to create an array out of it.
You can probably use some smart Array.reduce() functionality to minimize the code.
let old_array = [{
face: "n",
values: [
{ time: "15:00", value: 5 },
{ time: "15:02", value: 6 }
]
}, {
face: "e",
values: [
{ time: "15:01", value: 7 },
{ time: "15:02", value: 8 }
]
}, {
face: "s",
values: [
{ time: "15:01", value: 7 },
{ time: "15:02", value: 8 }
]
}, {
face: "w",
values: [
{ time: "15:01", value: 7 },
{ time: "15:02", value: 8 }
]
}];
let new_array = []
let times = {}
let faces = {"n": "north", "e": "east", "s": "south", "w": "west"}
old_array.forEach((obj) => {
obj.values.forEach((val) => {
if(val.time in times)
times[val.time][faces[obj.face]] = val.value
else {
let newT = {time: val.time, "north": null, "east": null, "south": null, "west": null}
newT[faces[obj.face]] = val.value
times[val.time] = newT
new_array.push(newT)
}
})
})
new_array.sort((a,b)=>a.time < b.time ? -1 : (a.time > b.time ? 1 : 0))
console.log(new_array)
// Expected output
// [
// { time: '15:00', north: 5, east: null, south: null, west: null },
// { time: '15:01', north: null, east: 7, south: 7, west: 7 },
// { time: '15:02', north: 6, east: 8, south: 8, west: 8 }
// ]

How to aggregate nested values in array of objects

I have an existing function that creates a summary object from two arrays (one array is transaction data, the other is master data).
Currently, the returned object contains a sum of the transaction quantity by year and month - example below.
I'm looking for help to add a sum of sums at the root level for each item - example also below.
I've reviewed answers for similar questions but wasn't able to find a solution close enough to my use case.
Here's the existing code:
const
array1 = [{ material: "ABC123", cost: 100 },
{ material: "DEF456", cost: 150 }],
array2 = [{ material: "ABC123", date: "1/1/20", quantity: 4 },
{ material: "ABC123", date: "1/15/20", quantity: 1 },
{ material: "ABC123", date: "2/15/20", quantity: 3 },
{ material: "ABC123", date: "4/15/21", quantity: 1 },
{ material: "DEF456", date: "3/05/20", quantity: 6 },
{ material: "DEF456", date: "3/18/20", quantity: 1 },
{ material: "DEF456", date: "5/15/21", quantity: 2 }],
groups = [
({ material }) => material,
_ => 'byYear',
({ date }) => '20' + date.split('/')[2],
_ => 'byMonth'
],
sum = {
key: ({ date }) => date.split('/')[0],
value: ({ quantity }) => quantity
},
result = array2.reduce(
(r, o) => {
const temp = groups.reduce((t, fn) => t[fn(o)] ??= {}, r);
temp[sum.key(o)] ??= 0;
temp[sum.key(o)] += sum.value(o);
return r;
},
Object.fromEntries(array1.map(({ material, cost }) => [material, { cost }]))
);
console.log(result);
As mentioned above, I would like it to add a "totalSum" key/value at the root level. The result would look like:
{
"ABC123": {
"totalSum": 8,
"cost": 100,
"byYear": {
"2020": {
"byMonth": {
"1": {
"sum": 5,
"max": 4
},
"2": {
"sum": 3,
"max": 3
}
}
}
}
}
}
Any help is greatly appreciated!
You can actually achieve the entire refactoring with a single Array#reduce() call, combined with a Map of the master array to retrieve cost.
Adding a total sum is then trivial since you have the all the properties to hand and don't have to remap your aggregated data.
const
array1 = [{ material: "ABC123", cost: 100, critical: true }, { material: "DEF456", cost: 150, critical: false }],
array2 = [{ material: "ABC123", date: "1/1/20", quantity: 4 }, { material: "ABC123", date: "1/15/20", quantity: 1 }, { material: "ABC123", date: "2/15/20", quantity: 3 }, { material: "ABC123", date: "4/15/21", quantity: 1 }, { material: "DEF456", date: "3/05/20", quantity: 6 }, { material: "DEF456", date: "3/18/20", quantity: 1 }, { material: "DEF456", date: "5/15/21", quantity: 2 }],
master = new Map(array1.map(({ material, ...item }) => [material, item])),
result = array2.reduce((acc, { material, date, quantity }) => {
let [month, , year] = date.split('/');
year = '20' + year;
const
_material = (acc[material] ??= { ...master.get(material), totalSum: 0, byYear: {} }),
_byYear = (_material.byYear[year] ??= { byMonth: {} }),
_byMonth = (_byYear.byMonth[month] ??= { sum: 0, max: 0 });
_material.totalSum += quantity;
_byMonth.sum += quantity;
_byMonth.max = Math.max(_byMonth.max, quantity);
return acc;
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Edit
In a comment you mentioned that you might have more properties in array1 that should be included in the final result. I've edited to accommodate this by changing the Map to reference the entire object as found in array1. We can then include all the properties available using spread syntax when we first declare the relevant object in the accumulator.
// split out 'material' from the rest of the object in the Map
const master = new Map(array1.map(({ material, ...item }) => [material, item]));
// later access the object by material and spread its props into the accumulator object.
_material = (acc[material] ??= { ...master.get(material), totalSum: 0, byYear: {} }),

Javascript .find() not working for nested object array [duplicate]

This question already has answers here:
Curly Brackets in Arrow Functions
(3 answers)
Closed 1 year ago.
I have two nested object arrays, one is an array describing a school (id, name, tests it conducts along with their counts), and another is an array of school teachers corresponding to the schools in the first array. Here is some sample data for both arrays:
import { ObjectId } from 'mongodb'
monthlySchoolTests = [{
schoolId: ObjectId('804d8527f390ghz26e3426j6'),
schoolName: "School1"
schoolTests: [
{ testName: "Chemistry", count: 15 },
{ testName: "Music", count: 8 }
]
},
{
schoolId: ObjectId('804ef074384b3d43f125ghs5'),
schoolName: "School2"
schoolTests: [
{ testName: "Math", count: 3 },
{ testName: "Physics", count: 12 },
{ testName: "Biology", count: 10 }
]
}
]
schoolTeachers = [{
schoolId: ObjectId('804ef074384b3d43f125ghs5')
schoolName: "School2"
teachers: [
{ name: "Michael", email: "michael#school2.edu" },
{ name: "Jane", count: "jane#school2.edu" },
{ name: "Lukas", count: "lukas#school2.edu" }
]
},
{
schoolId: ObjectId('804d8527f390ghz26e3426j6')
schoolName: "School1"
teachers: [
{ name: "Cleo", email: "cleo#school1.edu" },
{ name: "Celine", count: "celine#school1.edu" },
{ name: "Ike", count: "ike#school1.edu" }
]
}
]
I obtained the second array by passing as an argument an array of schoolIds that I got from the query results for the first array, so I know that both arrays have about the same number of objects (assuming queries don't return null values). I am trying to link the test information in the first array with the school teacher in the second array but the .find() method is not working for me. Here is my code:
monthlySchoolTests.map(testObj => {
const selectedSchoolTeachers = schoolTeachers.find(teacherObj => {
String(testObj.schoolId) === String(teacherObj.schoolId)
})
console.log(selectedSchoolTeachers)
})
For some reason, I only get undefined even though I have tested this by mapping through each object in the first array and asserting that there is a match for every schoolId in the second array. (console.log yields true).
Sorry for any errors as this is my first time posting. Would appreciate any help!
Well, you could just quick-generate a combined array that had everything you need in it:
let combined = monthlySchoolTests.map(e => ({...e, teachers: schoolTeachers.filter(t => t.schoolId === e.schoolId)[0].teachers}))
const ObjectId = (v) => v; // for testing
let monthlySchoolTests = [{
schoolId: ObjectId('804d8527f390ghz26e3426j6'),
schoolName: "School1",
schoolTests: [
{ testName: "Chemistry", count: 15 },
{ testName: "Music", count: 8 }
]
},
{
schoolId: ObjectId('804ef074384b3d43f125ghs5'),
schoolName: "School2",
schoolTests: [
{ testName: "Math", count: 3 },
{ testName: "Physics", count: 12 },
{ testName: "Biology", count: 10 }
]
}
]
let schoolTeachers = [{
schoolId: ObjectId('804ef074384b3d43f125ghs5'),
schoolName: "School2",
teachers: [
{ name: "Michael", email: "michael#school2.edu" },
{ name: "Jane", count: "jane#school2.edu" },
{ name: "Lukas", count: "lukas#school2.edu" }
]
},
{
schoolId: ObjectId('804d8527f390ghz26e3426j6'),
schoolName: "School1",
teachers: [
{ name: "Cleo", email: "cleo#school1.edu" },
{ name: "Celine", count: "celine#school1.edu" },
{ name: "Ike", count: "ike#school1.edu" }
]
}
]
let combined = monthlySchoolTests.map(e => ({...e, teachers: schoolTeachers.filter(t => t.schoolId === e.schoolId)[0].teachers}))
console.log(combined)

Reduce and sum array of objects (JS) [duplicate]

This question already has answers here:
Sum JavaScript object propertyA values with the same object propertyB in an array of objects
(12 answers)
Group by, and sum, and generate an object for each array in JavaScript
(4 answers)
How to group by and sum an array of objects? [duplicate]
(2 answers)
Group objects by multiple properties in array then sum up their values
(16 answers)
Closed 3 years ago.
My data:
arr: [],
models: [
{ id: 1, name: "samsung", seller_id: 1, count: 56 },
{ id: 1, name: "samsung", seller_id: 2, count: 68 },
{ id: 2, name: "nokia", seller_id: 2, count: 45 },
{ id: 2, name: "nokia", seller_id: 3, count: 49 }
]
Expected Arr:
arr: [
{ id: 1, name: "samsung", count: 124 },
{ id: 2, name: "nokia", count: 94 }
]
It's my code to simplify models by removing repeated id:
this.models.forEach(mdl => {
if (!this.arr.some(obj => obj.id === mdl.id)) {
this.arr.push(mdl);
}
});
But I can't sum counts.
How can I do that?
You can use Array.reduce():
var models = [
{ id: 1, name: "samsung", seller_id: 1, count: 56 },
{ id: 1, name: "samsung", seller_id: 2, count: 68 },
{ id: 2, name: "nokia", seller_id: 2, count: 45 },
{ id: 2, name: "nokia", seller_id: 3, count: 49 }
];
var arr = models.reduce((acc, item) => {
let existItem = acc.find(({id}) => item.id === id);
if(existItem) {
existItem.count += item.count;
} else {
acc.push(item);
}
return acc;
}, []);
console.log(arr);
So, for your code, you can use this.arr and this.models replacing those variables from above which will look something like:
this.arr = this.models.reduce((acc, item) => {
let existItem = acc.find(({id}) => item.id === id);
if(existItem) {
existItem.count += item.count;
} else {
acc.push(item);
}
return acc;
}, []);
You can use Object.values instead of some. Inside the reduce callback create an object with key as id and value from the models. Then use Object.values to create an array of the values
let models = [{
id: 1,
name: "samsung",
seller_id: 1,
count: 56
},
{
id: 1,
name: "samsung",
seller_id: 2,
count: 68
},
{
id: 2,
name: "nokia",
seller_id: 2,
count: 45
},
{
id: 2,
name: "nokia",
seller_id: 3,
count: 49
}
]
let data = models.reduce((acc, curr) => {
if (!acc[curr.id]) {
acc[curr.id] = curr;
} else {
acc[curr.id].count += curr.count
}
return acc;
}, {})
console.log(Object.values(data))

Categories