Group duplicate of same value within object of an array - javascript

[{
"_id": {
"year": 2017,
"month": 4
},
"Confirm": 0
}, {
"_id": {
"year": 2017,
"month": 4
},
"Expired": 25
}, {
"_id": {
"year": 2017,
"month": 4
},
"Pending": 390
}, {
"_id": {
"year": 2017,
"month": 5
},
"Pending": 1400
}]
The array above contain same value month and year. Generated from MongoDB Aggregate. And I want to merge them into a single object and preserve whatever keys and values they have.
Expected output:
[{
month: 4,
year: 2017,
Expired: 25,
Pending: 390
}, {
month: 5,
year: 2017,
Pending: 1400
}]
I prefer the fastest execution implementation. Underscorejs or native are welcome. Thanks

This takes a little to pick apart, but it is linear:
const ary = [{
"_id": {
"year": 2017,
"month": 4
},
"Confirm": 0
}, {
"_id": {
"year": 2017,
"month": 4
},
"Expired": 25
}, {
"_id": {
"year": 2017,
"month": 4
},
"Pending": 390
}, {
"_id": {
"year": 2017,
"month": 5
},
"Pending": 1400
}];
const result = Object.values(ary.reduce((acc, cur) => {
const { month, year } = cur._id;
const key = `${month}-${year}`;
const obj = Object.assign({}, cur);
delete obj._id;
acc[key] = Object.assign(acc[key] || { month, year }, obj);
return acc;
}, {}));
console.log(result);

You could use a Map for grouping, and then Array.from to extract the final objects:
function merge(data) {
return Array.from(data.reduce( (acc, o) => {
const k = o._id.year * 100 + o._id.month;
const v = acc.get(k) || Object.assign({}, o._id);
for (let prop in o) {
if (prop !== '_id') v[prop] = o[prop];
}
return acc.set(k, v);
}, new Map), ([k, v]) => v);
}
// Sample data
const data = [{
"_id": {
"year": 2017,
"month": 4
},
"Confirm": 0
}, {
"_id": {
"year": 2017,
"month": 4
},
"Expired": 25
}, {
"_id": {
"year": 2017,
"month": 4
},
"Pending": 390
}, {
"_id": {
"year": 2017,
"month": 5
},
"Pending": 1400
}];
const result = merge(data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

This runs in O(N*logN) for sorting and O(N) for merging the json.
Hope this works for you!
var obj = [{
_id: {
year: 2017,
month: 5,
},
Pending: 1400,
}, {
_id: {
year: 2017,
month: 4,
},
Expired: 25,
}, {
_id: {
year: 2017,
month: 4,
},
Pending: 390,
}, {
_id: {
year: 2017,
month: 4,
},
Confirm: 0,
}];
function compare(a, b) {
return a._id.year !== b._id.year
? a._id.year - b._id.year
: a._id.month - b._id.month;
}
var sorted = obj.sort(compare);
function join(a, b) {
return {
_id: a._id,
Pending: (a.Pending? a.Pending : 0) + (b.Pending? b.Pending : 0),
Confirm: (a.Confirm? a.Confirm : 0) + (b.Confirm? b.Confirm : 0),
Expired: (a.Expired? a.Expired : 0) + (b.Expired? b.Expired : 0),
};
}
var compressed = sorted.filter(function (value, index) {
if (!sorted[index + 1]) {
return true;
}
if (compare(value, sorted[index + 1]) === 0) {
sorted[index + 1] = join(value, sorted[index + 1]);
return false;
}
return true;
});
console.log(compressed);
// if you want month and year formatted:
console.log(compressed.map(function (o) {
const result = {
month: o._id.month,
year: o._id.year,
};
if (o.Pending !== undefined) result.Pending = o.Pending;
if (o.Confirm !== undefined) result.Confirm = o.Confirm;
if (o.Expired !== undefined) result.Expired = o.Expired;
return result;
}));

Related

How to Add the values ​of an object array based on a condition? JS

I am looking to sum the fields of my objects according to a value of my object
For example, i have an array:
[
{
"month": 4,
"periodDays": 1,
"expected": 5
},
{
"month": 5,
"periodDays": 10,
"expected": 40
},
{
"month": 5,
"periodDays": 11,
"expected": 35
},
{
"month": 6,
"periodDays": 8,
"expected": 20
}
]
and i want:
[
{
"month": 4,
"periodDays": 1,
"expected": 5
},
{
"month": 5,
"periodDays": 21,
"expected": 75
},
{
"month": 6,
"periodDays": 8,
"expected": 20,
},
I know I can use the reducer but I can't make it work with a condition.
You can use Array.reduce() to create the desired result.
We start by creating a map using the month value as the key. We then initialize the periodDays and expected values to zero.
For each object, we then add the periodDays and expected to get the sum for each.
Finally, we use Object.values() to turn our map into an array:
let input = [ { "month": 4, "periodDays": 1, "expected": 5 }, { "month": 5, "periodDays": 10, "expected": 40 }, { "month": 5, "periodDays": 11, "expected": 35 }, { "month": 6, "periodDays": 8, "expected": 20 } ]
const result = Object.values(input.reduce((acc, { month, periodDays, expected }) => {
acc[month] = acc[month] || { month, periodDays: 0, expected: 0 };
acc[month].periodDays += periodDays;
acc[month].expected += expected;
return acc;
}, {}));
console.log('Result:', result)
.as-console-wrapper { max-height: 100% !important; }

Mongoose: How to sort aggregate response with two fields

I have this mongoose query:
MinutesSpentStudying.aggregate([
{ $match: { connected_user_id: ObjectId(user_id) } },
{
$project: {
minutes_spent_studying: 1,
year: { $year: "$date" },
day: { $dayOfMonth: "$date" },
},
},
{
$group: {
_id: {
day: "$day",
year: "$year",
},
total_minutes: { $sum: "$minutes_spent_studying" },
},
},
{ $sort: { _id: 1 } },
]);
It returns this response:
[
{
"_id": {
"day": 2,
"year": 2021
},
"total_minutes": 11
},
{
"_id": {
"day": 3,
"year": 2021
},
"total_minutes": 1
},
{
"_id": {
"day": 26,
"year": 2020
},
"total_minutes": 1
},
{
"_id": {
"day": 27,
"year": 2020
},
"total_minutes": 3
},
]
I'd like it to sort out by year, and then by day so that it returns the results of 2020 and then the result of 2021.
Any idea how to configure so as to achieve this result?
You can sort by multiple fields and use the dot notation for the nested ones:
{
$sort: {
"_id.year": 1,
"_id.day": 1
}
}
Mongo Playground

What is the most performant way to convert an Array of Object to an Object with unique keys

I am trying to figure out the most performant Javascript way to convert an array of objects, into an object with unique keys and an array full of objects as the value.
For Example:
const array = [
{ "name": "greg", "year": "2000" },
{ "name": "john", "year": "2002" },
{ "name": "bob", "year": "2005" },
{ "name": "ned", "year": "2000" },
{ "name": "pam", "year": "2000" },
];
I would like this converted to:
{
"2000": [
{ "name": "greg", "year": "2000" },
{ "name": "ned", "year": "2000" },
{ "name": "pam", "year": "2000" }
],
"2002": [ { "name": "john", "year": "2002" } ],
"2005": [ { "name": "bob", "year": "2005" } ],
}
As of now, this is what I've done so far:
let yearsObj = {};
for (let i=0; i<array.length; i++) {
if (!yearsObj[array[i].year]) {
yearsObj[array[i].year] = [];
}
yearsObj[array[i].year].push(array[i]);
}
you can use a more elegant way to do it by using array's reduce function
// # impl
const group = key => array =>
array.reduce(
(objectsByKeyValue, obj) => ({
...objectsByKeyValue,
[obj[key]]: (objectsByKeyValue[obj[key]] || []).concat(obj)
}),
{}
);
// # usage
console.log(
JSON.stringify({
byYear: group(array),
}, null, 1)
);
// output
VM278:1 {
"carsByBrand": {
"2000": [
{
"name": "greg",
"year": "2000"
},
{
"name": "ned",
"year": "2000"
},
{
"name": "pam",
"year": "2000"
}
],
"2002": [
{
"name": "john",
"year": "2002"
}
],
"2005": [
{
"name": "bob",
"year": "2005"
}
]
}
}
It could be as simple as that Object.fromEntries(array.map(obj => [obj.year,obj])) even it is not exactly what you need, but talking about performance it is way slower than all proposed, so i'm giving it as an bad example of showing how the short statement is not always the fastest.
Your way seems to be the fastest taking about performance.
Run the snippet below to see the actual timing.
// common
let array = [
{ "name": "greg", "year": "2000" },
{ "name": "john", "year": "2002" },
{ "name": "bob", "year": "2005" },
{ "name": "ned", "year": "2000" },
{ "name": "pam", "year": "2000" },
];
// simple as a statement way
console.time();
console.log(Object.fromEntries(array.map(obj => [obj.year,obj])));
console.timeEnd();
// using .reduce way
console.time();
const result = array.reduce((prev, curr) => {
const { year } = curr;
if (prev[year]) {
prev[year].push(curr);
} else {
prev[year] = [curr];
}
return prev;
}, {});
console.log(result);
console.timeEnd();
// your way
console.time();
let yearsObj = {};
for (let i=0; i<array.length; i++) {
if (!yearsObj[array[i].year]) {
yearsObj[array[i].year] = [];
}
yearsObj[array[i].year].push(array[i]);
}
console.log(yearsObj);
console.timeEnd();
A for loop (imperative style) like you have is likely to be the fastest in most situations. However, in this case you are not likely to see much of a difference. One thing you could do to improve the code in your example is to get the array length before the for loop and assign it to the variable, so that it's not calculated every iteration of the loop.
const yearsObj = {};
const arrayLength = array.length; // Only calculate array length once
for (let i=0; i<arrayLength; i++) {
if (!yearsObj[array[i].year]) {
yearsObj[array[i].year] = [];
}
yearsObj[array[i].year].push(array[i]);
}
In this situation, my preference would be to use Array.reduce(). It is more readable and the performance difference will be negligible.
const arr = [
{ name: 'greg', year: '2000' },
{ name: 'john', year: '2002' },
{ name: 'bob', year: '2005' },
{ name: 'ned', year: '2000' },
{ name: 'pam', year: '2000' },
];
const result = arr.reduce((prev, curr) => {
const { year } = curr;
if (prev[year]) {
prev[year].push(curr);
} else {
prev[year] = [curr];
}
return prev;
}, {});
/* Result:
{ '2000':
[ { name: 'greg', year: '2000' },
{ name: 'ned', year: '2000' },
{ name: 'pam', year: '2000' } ],
'2002': [ { name: 'john', year: '2002' } ],
'2005': [ { name: 'bob', year: '2005' } ] }
*/

Summing up value in array of objects

[
{
"id": {
"extId": "112",
"year": "2000"
},
"Count": 1
},
{
"id": {
"extId": "113",
"year": "2001"
},
"Count": 446
},
{
"id": {
"extId": "115",
"year": "2000"
},
"Count": 742
}, ...
]
I have a very long array of objects. I need to sum up the count based on the year. For e.g, I would like something like [{2000: 743}, {2001: 446},...].
I am not sure how to proceed with that in javascript. Should I loop through every object in the array and check for the year or is there some javascript function which can make this simpler.
Thanks.
You can use Array.reduce():
let countByYear = objects.reduce((acc, next) => {
acc[next.id.year] = (acc[next.id.year] || 0) + next.Count;
return acc;
}, {});
Note, this will produce a different structure from your example (because I read your question too sloppily):
{
2000: 743,
2001: 446
}
However I would say this is easier to work with than [ { 2000: 743 }, { 2001: 446 } ], since in that case you have an array of objects, that each have a single key, and you have no way of knowing what that key is, which I'd imagine makes it really difficult to iterate over them.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
You can use reduce:
arr.reduce((result, current) => {
result.push({[current.year]: current.Count});
return result
}, [])
This will give you this structure [{2000: 743}, {2001: 44}] and you can even do arr.filter(filterFn) first if you need to filter only certain years
You could use a Map and take the key/values for an array of objects.
var data = [{ id: { extId: "112", year: "2000" }, Count: 1 }, { id: { extId: "113", year: "2001" }, Count: 446 }, { id: { extId: "115", year: "2000" }, Count: 742 }],
count = Array.from(
data.reduce(
(m, { id: { year }, Count }) => m.set(year, (m.get(year) || 0) + Count),
new Map
),
([year, count]) => ({ [year]: count })
);
console.log(count);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script>
var arr=[
{
"id": {
"extId": "112",
"year": "2000"
},
"Count": 1
},
{
"id": {
"extId": "113",
"year": "2001"
},
"Count": 446
},
{
"id": {
"extId": "115",
"year": "2000"
},
"Count": 742
}
];
var result=arr.reduce((result, current) => {
result.push({[current.id.year]: current.Count});
return result;
}, []);
console.log(result);
</script>
reduce will do the trick here for you:
var arr = [
{
"id": {
"extId": "112",
"year": "2000"
},
"Count": 1
},
{
"id": {
"extId": "113",
"year": "2001"
},
"Count": 446
},
{
"id": {
"extId": "115",
"year": "2000"
},
"Count": 742
},
{
"id": {
"extId": "116",
"year": "2001"
},
"Count": 44
}
];
let count = arr.reduce((acc, next) => {
acc[next.id.year] = (acc[next.id.year] || 0) + next.Count;
return acc;
}, {});
console.log(count);
ES6
You could use reduce() function to get required result.
DEMO
const data = [{"id": {"extId": "112","year": "2000"},"Count": 1},{"id": {"extId": "113","year": "2001"},"Count": 446},{"id": {"extId": "115","year": "2000"},"Count": 742}];
let result = data.reduce((r, {Count,id: {year}}) => {
r[year] = (r[year] || 0) + Count;
return r;
}, {});
console.log([result])
.as-console-wrapper {max-height: 100% !important;top: 0;}
var yearCount={};
var temp=[
{
"id": {
"extId": "112",
"year": "2000"
},
"Count": 1
},
{
"id": {
"extId": "113",
"year": "2001"
},
"Count": 446
},
{
"id": {
"extId": "115",
"year": "2000"
},
"Count": 742
}
];
temp.forEach(item=>{
var val=yearCount[item.id.year];
if (val){
yearCount[item.id.year]=val+item.Count;
}
else{
yearCount[item.id.year]=item.Count;
}
})
console.log(yearCount);

merging 2 javascript arrays

I have two arrays:
const calendar = [
{"_id":"Jan"}, {"_id":"Feb"}, {"_id":"Mar"},
{"_id":"Apr"}, {"_id":"May"}, {"_id":"Jun"},
{"_id":"Jul"}, {"_id":"Aug"}, {"_id":"Sep"},
{"_id":"Oct"}, {"_id":"Nov"}, {"_id":"Dec"}
]
and
const count = [
{"_id":"Jan","count":1}, {"_id":"Apr","count":6},
{"_id":"May","count":5}, {"_id":"Feb","count":1},
{"_id":"Jul","count":1}, {"_id":"Mar","count":2},
{"_id":"Jun","count":2}
]
I would like to merge the two arrays and so that when there are no counts for that month, make it "count":0.
For example the new array should look like this:
const final = [
{"_id":"Jan","count":1}, {"_id":"Feb","count":1},
{"_id":"Mar","count":2}, {"_id":"Apr","count":6},
{"_id":"May","count":5}, {"_id":"Jun","count":2},
{"_id":"Jul","count":1}, {"_id":"Aug","count":0},
{"_id":"Sep","count":0}, {"_id":"Oct","count":0},
{"_id":"Nov","count":0}, {"_id":"Dec","count":0}
]
I'm a bit lost on this. Would be very grateful for anyones assistance.
Thanks
First create a map of the ids to the count. then map all calendar months to the count from the created map and default to 0 if no such exists.
var countMap = {};
count.forEach((a) => {
countMap[a._id] = a.count
});
const final = calendar.map((month) => ({_id: month._id, count: countMap[month._id] ||0}))
you can see a working exmaple here: https://jsfiddle.net/z4sdcuku/
You could use a Map and take all counts first. Then map the new objects.
var calendar = [{ _id: "Jan" }, { _id: "Feb" }, { _id: "Mar" }, { _id: "Apr" }, { _id: "May" }, { _id: "Jun" }, { _id: "Jul" }, { _id: "Aug" }, { _id: "Sep" }, { _id: "Oct" }, { _id: "Nov" }, { _id: "Dec" }],
count = [{ _id: "Jan", count: 1 }, { _id: "Apr", count: 6 }, { _id: "May", count: 5 }, { _id: "Feb", count: 1 }, { _id: "Jul", count: 1 }, { _id: "Mar", count: 2 }, { _id: "Jun", count: 2 }],
map = new Map(count.map(o => [o._id, o.count])),
final = calendar.map(o => Object.assign({}, o, { count: map.get(o._id) || 0 }));
console.log(final);
.as-console-wrapper { max-height: 100% !important; top: 0; }
This will do it for you :
const calendar = [{
"_id": "Jan"
}, {
"_id": "Feb"
}, {
"_id": "Mar"
}, {
"_id": "Apr"
}, {
"_id": "May"
}, {
"_id": "Jun"
}, {
"_id": "Jul"
}, {
"_id": "Aug"
}, {
"_id": "Sep"
}, {
"_id": "Oct"
}, {
"_id": "Nov"
}, {
"_id": "Dec"
}];
const count = [{
"_id": "Jan",
"count": 1
}, {
"_id": "Apr",
"count": 6
}, {
"_id": "May",
"count": 5
}, {
"_id": "Feb",
"count": 1
}, {
"_id": "Jul",
"count": 1
}, {
"_id": "Mar",
"count": 2
}, {
"_id": "Jun",
"count": 2
}]
var result = [];
for (var mth = 0; mth < calendar.length; mth++) {
var ct = 0;
for (var mthCt = 0; mthCt < count.length; mthCt++) {
if (calendar[mth]._id === count[mthCt]._id) {
ct = count[mthCt].count;
}
}
result[mth] = {
"id": calendar[mth]._id,
"count": ct
}
}
document.getElementById('output').textContent = JSON.stringify(result);
JSFiddle

Categories