ES6 Nested Grouping with Sum - javascript

I have an array like this:
[{
id: 1,
amount: 10,
date: '21/01/2017'
},{
id: 1,
amount: 10,
date: '21/01/2017'
},{
id: 1,
amount: 30,
date: '22/01/2017'
},{
id: 2,
amount: 10,
date: '21/01/2017'
},]
And I would like this to output like:
{
'21/01/2017': {
1: {
amount: 20
},
2: {
amount: 10
}
},
'22/01/2017': {
1: {
amount: 30
}
}
}
Essentially grouping by data, nested grouping by id, whilst summing the relevant amounts.
So far I have tried using reduce, and have looked at lodash functions but have been unsuccessful. Reduce makes it easy to group by and sum by one prop, but I cannot find an elegant way to create the second level. I have created the a sandbox to demonstrate:
https://codesandbox.io/s/woj9xz6nq5
const dataToConsolidate = [{
id: 1,
amount: 10,
date: '21/01/2017'
}, {
id: 1,
amount: 10,
date: '21/01/2017'
}, {
id: 1,
amount: 30,
date: '22/01/2017'
}, {
id: 2,
amount: 10,
date: '21/01/2017'
},]
export const consolidate = (data) => {
const result = data.reduce(function (res, obj) {
(!(obj.date in res)) ?
res.__array.push(res[obj.date] = obj) :
res[obj.date].amount += obj.amount;
return res;
}, { __array: [] });
return result;
};
console.log(consolidate(dataToConsolidate))

You could take the object as hash table and add the properties with a default style.
var array = [{ id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 30, date: '22/01/2017' }, { id: 2, amount: 10, date: '21/01/2017' }],
result = {};
array.forEach(function (o) {
result[o.date] = result[o.date] || {};
result[o.date][o.id] = result[o.date][o.id] || { amount: 0 };
result[o.date][o.id].amount += o.amount;
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Here's a relatively ES6-y solution using reduce:
const initial = [{ id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 30, date: '22/01/2017' }, { id: 2, amount: 10, date: '21/01/2017' }];
const parse = obj => obj.reduce((final, { date, id, amount }) => {
let group = final[date] = final[date] || {};
group[id] = group[id] || { amount: 0 };
group[id].amount += amount;
return final;
}, {});
const final = parse(initial);
console.log(final);
You'll want to add appropriate logic to handle missing/erroneous date, id, or amount keys.

As a fan of the library Ramda (disclaimer: I'm one of the authors), I might well use it to make everything a bit more explicit:
const {pipe, groupBy, prop, map, pluck, sum} = R
const data = [
{"amount": 10, "date": "21/01/2017", "id": 1},
{"amount": 10, "date": "21/01/2017", "id": 1},
{"amount": 30, "date": "22/01/2017", "id": 1},
{"amount": 10, "date": "21/01/2017", "id": 2}
]
const consolidate = pipe(
groupBy(prop('date')),
map(groupBy(prop('id'))),
map(map(pluck('amount'))),
map(map(sum))
)
console.log(consolidate(data))
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Of course using a library just to solve this problem is simply overkill, when something like the solution from #NinaSholz does just fine. But if you find several places where it could help, it might be worth looking into something like that.

you can do it thru _.groupBy:
const res = _.chain(data)
.groupBy('date')
.mapValues(dateObj => _.chain(dateObj)
.groupBy('id')
.mapValues(idObj => ({ amount: _.sumBy(idObj, 'amount') }))
.value()
)
.value();

Related

Conditional copying properties and values from one array of objects to another

Got two arrays of objects and my goal is to check if the value under property id from array1 matches the value under the property categoryId of array2. When find a match want to add the missing property amount to the relevant member of array1 or create a new array containing all the properties and values I need - id, name, amount
Here are the two arrays:
const array1 = [{
id: 8,
name: 'Online Shopping',
},
{
id: 12,
name: 'Subscriptions',
},
{
id: 5,
name: 'Patreon donations',
}]
and
const array2 = [
{
expence: {
amount: -66.66,
},
categoryId: 5,
},
{
expence: {
amount: 100018.85,
},
categoryId: 0,
},
{
expence: {
amount: -43340.9,
},
categoryId: 12,
},]
Tried to combine different approaches from answers to similar but simpler cases already posted in the community but didn't managed to make them work in my case.
Loop through each item in array1, then loop through each item in array2 inside the loop and check whether the categoryId is equal to the id.
const array1 = [{
id: 8,
name: 'Online Shopping',
},
{
id: 12,
name: 'Subscriptions',
},
{
id: 5,
name: 'Patreon donations',
}
]
const array2 = [{
expence: {
amount: -66.66,
},
categoryId: 5,
},
{
expence: {
amount: 100018.85,
},
categoryId: 0,
},
{
expence: {
amount: -43340.9,
},
categoryId: 12,
},
]
array1.forEach((e) => {
array2.forEach((f) => {
if (f.categoryId == e.id) {
e.amount = f.expence.amount;
}
})
})
console.log(array1);
You can also make use of Array.filter to find the item where the categoryId is equal to the id:
const array1 = [{
id: 8,
name: 'Online Shopping',
},
{
id: 12,
name: 'Subscriptions',
},
{
id: 5,
name: 'Patreon donations',
}
]
const array2 = [{
expence: {
amount: -66.66,
},
categoryId: 5,
},
{
expence: {
amount: 100018.85,
},
categoryId: 0,
},
{
expence: {
amount: -43340.9,
},
categoryId: 12,
},
]
array1.forEach((e) => {
var arr = array2.filter(f => f.categoryId == e.id);
if(arr.length > 0) e.amount = arr[0].expence.amount;
})
console.log(array1);

Javascript Filter an Array to give subset from two Range Parameters

How do I filter an array two give me the subset created by overlap of two ranges?
For example, suppose Here's my array with a bunch of objects:
const list = [{
id: 1,
price: "10",
weight: "19.45"
},{
id: 2,
price: "14",
weight: "27.8"
},{
id: 3,
price: "45",
weight: "65.7"
},{
id: 4,
price: "37",
weight: "120.5"
},{
id: 5,
price: "65",
weight: "26.9"
},{
id: 6,
price: "120",
weight: "19.3"
},{
id: 7,
price: "20",
weight: "45.7"
}]
Now I want to filter the above array of objects based on a range for two parameters price and weight.
let range = {
startPrice: 15,
endPrice: 60,
startWeight: 22.0,
endWeight: 70.5,
}
I want to filter my list with these range parameters which will return me an array with a subset of objects satisfying both the filter ranges. Hence, the output array should be:
filtered = [{
id: 3,
price: "45",
weight: "65.7"
},{
id: 7,
price: "20",
weight: "45.7"
}]
Because items with id: 3 and id: 5 satisfy the subset of both of the two ranges. How should my list.filter() function look like? Any help is greatly appreciated, thank you.
Note: I've used parseFloat because values are stored as a String. There are other ways to get a Number out of a String.
var list = [{"id":1,"price":"10","weight":"19.45"},{"id":2,"price":"14","weight":"27.8"},{"id":3,"price":"45","weight":"65.7"},{"id":4,"price":"37","weight":"120.5"},{"id":5,"price":"65","weight":"26.9"},{"id":6,"price":"120","weight":"19.3"},{"id":7,"price":"20","weight":"45.7"}];
var range = {
startPrice: 15,
endPrice: 60,
startWeight: 22.0,
endWeight: 70.5
};
var filtered=list.filter(
element=> {
var elementprice=parseFloat(element.price);
var elementweight=parseFloat(element.weight);
if(
elementprice>=range.startPrice &&
elementprice<=range.endPrice &&
elementweight>=range.startWeight &&
elementweight<=range.endWeight
) {
return true;
}
return false;
}
);
console.log(filtered);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You mean 3 and 7
Also you need to either remove the quotes or cast to number. Note a filter will expect a true or false to a Boolean test is enough. No if or else needed
const list = [{ id: 1, price: "10", weight: "19.45" }, { id: 2, price: "14", weight: "27.8" }, { id: 3, price: "45", weight: "65.7" }, { id: 4, price: "37", weight: "120.5" }, { id: 5, price: "65", weight: "26.9" }, { id: 6, price: "120", weight: "19.3" }, { id: 7, price: "20", weight: "45.7" }];
let range = { startPrice: 15, endPrice: 60, startWeight: 22.0, endWeight: 70.5, }
const res = list.filter(item =>
+item.price >= range.startPrice &&
+item.price <= range.endPrice &&
+item.weight >= range.startWeight &&
+item.weight <= range.endWeight);
console.log(res);

Sort grouped objects by Lodash

I have following array of objects:
[{
id: 1,
amount: 2000,
date: "2018-01-31T00:00:00.000Z"
},{
id: 2,
amount: 3000,
date: "2017-07-31T00:00:00.000Z"
},{
id: 3,
amount: 6000,
date: "2018-01-31T00:00:00.000Z"
},{
id: 4,
amount: 7000,
date: "2017-01-31T00:00:00.000Z"
},{
id: 5,
amount: 5000,
date: "2017-03-31T00:00:00.000Z"
},{
id: 6,
amount: 3000,
date: "2018-02-22T00:00:00.000Z"
},{
id: 7,
amount: 4500,
date: "2017-01-31T00:00:00.000Z"
}]
I am calling following command to group objects by year and date:
_(data)
.groupBy(o => new Date(o.date).getFullYear() + '-' + new Date(o.date).getMonth())
.map(o1 => _(o1).map(o2 => o2.amount).sum())
Code above give me array of sums like [xx, yy, aaa, bbb, ...]
Now I need ensure that these values in array will be ordered (sum of 2018-2 will be first, and sum of 2017-1 will be on the end).
Also will be nice when result will be contains array of sorted objects as I describe above, where each object will be contains also period key "year-month" to detect what current value is. Expected output will be something like this:
[
{period: "2018-2", amount:3000}, // sum of 2018-2
{period: "2018-1", amount: 8000 }, // sum of 2018-1
{period: "2017-7", amount: 3000} // sum of 2017-7
{period: "2017-3", amount: 5000} // sum of 2017-3
{period: "2017-1" amount: 11500} // sum of 2017-1
]
Is possible edit this chain to get required result please? Thanks!
You can use the function reduce along with the function Object.values
Just vanilla javascript
var data = [{ id: 1, amount: 2000, date: "2018-01-31T00:00:00.000Z"},{ id: 2, amount: 3000, date: "2017-07-31T00:00:00.000Z"},{ id: 3, amount: 6000, date: "2018-01-31T00:00:00.000Z"},{ id: 4, amount: 7000, date: "2017-01-31T00:00:00.000Z"},{ id: 5, amount: 5000, date: "2017-03-31T00:00:00.000Z"},{ id: 6, amount: 3000, date: "2018-02-22T00:00:00.000Z"},{ id: 7, amount: 4500, date: "2017-01-31T00:00:00.000Z"}];
var result = Object.values(data.reduce((a, c) => {
var [year, month] = c.date.split('-');
var key = `${year}-${+month}`;
(a[key] || (a[key] = {period: key, amount: 0})).amount += c.amount;
return a;
}, {})).sort((a, b) => b.period.localeCompare(a.period));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You could get the grouped data and sort it ascending (the only possible way with lodash) and reverse the order.
var data = [{ id: 1, amount: 2000, date: "2018-01-31T00:00:00.000Z" }, { id: 2, amount: 3000, date: "2017-07-31T00:00:00.000Z" },{ id: 3, amount: 6000, date: "2018-01-31T00:00:00.000Z" }, { id: 4, amount: 7000, date: "2017-01-31T00:00:00.000Z" }, { id: 5, amount: 5000, date: "2017-03-31T00:00:00.000Z" }, { id: 6, amount: 3000, date: "2018-02-22T00:00:00.000Z" }, { id: 7, amount: 4500, date: "2017-01-31T00:00:00.000Z" }],
result = _(data)
.groupBy(o => o.date.slice(0, 7))
.map((array, sort) => ({ sort, date: sort.split('-').map(Number).join('-'), amount: _.sumBy(array, 'amount') }))
.sortBy('sort')
.reverse()
.map(({ date, amount }) => ({ date, amount }))
.value();
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>
With lodash you could first groupBy, then sort entries and then map those entries to get objects with period and sum of amount for each period.
const data = [{"id":1,"amount":2000,"date":"2018-01-31T00:00:00.000Z"},{"id":2,"amount":3000,"date":"2017-07-31T00:00:00.000Z"},{"id":3,"amount":6000,"date":"2018-01-31T00:00:00.000Z"},{"id":4,"amount":7000,"date":"2017-01-31T00:00:00.000Z"},{"id":5,"amount":5000,"date":"2017-03-31T00:00:00.000Z"},{"id":6,"amount":3000,"date":"2018-02-22T00:00:00.000Z"},{"id":7,"amount":4500,"date":"2017-01-31T00:00:00.000Z"}]
const result = _.chain(data)
.groupBy(({date}) => {
const d = new Date(date)
return d.getFullYear() + '-' + (d.getMonth() + 1)
})
.entries()
.sort(([a], [b]) => b.localeCompare(a))
.map(([period, arr]) => ({period, amount: _.sumBy(arr, ({amount}) => amount)}))
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js"></script>

What's the most efficent way to populate an a property of an array of objects with a larger array with data?

I have a small array of objects with properties, like so:
[
{
topicName: 'Clicks',
topic: 1,
dates: [ <PLACE VALUES HERE> ],
},
{
topicName: 'Cost',
topic: 2,
dates: [ <PLACE VALUES HERE> ],
},
];
Then I have a large array of objects that I wish to extract some of the properties from in to the above dates array.
Here's what the data I wish to extract from:
[
{
"date": "2014-02-01",
"device": "Computer",
"match-type": "NA",
"clicks": 66,
"revenue": 1037,
"conversions": 2,
"cost": 284.35,
"impressions": 5330,
"ROI": 3.64691401441885
},
{
"date": "2014-02-01",
"device": "Tablet",
"match-type": "NA",
"clicks": 38,
"revenue": 587,
"conversions": 2,
"cost": 194.01000000000005,
"impressions": 1934,
"ROI": 3.025617236224936
},
{
"date": "2014-02-02",
"device": "Tablet",
"match-type": "NA",
"clicks": 40,
"revenue": 587,
"conversions": 2,
"cost": 190,
"impressions": 1934,
"ROI": 3.025617236224936
},
]
Now I need the data from all of the members of the last array and insert that releveant data for the particular object in the first array (totalling where necessary), like so:
[
{
topicName: 'Clicks',
topic: 1,
dates: [
{
date: '2014-02-01',
value: 104
},
{
date: '2014-02-02',
value: 40
}
],
},
{
topicName: 'Cost',
topic: 2,
dates: [
{
date: '2014-02-01',
value: 284,3519401
},
{
date: '2014-02-02',
value: 190
}
],
},
];
The target is the latest version of Chrome and I'm using Webpack with Babel so all the latest stuff is available.
Assuming the last dataset can be pretty large, what's the most efficient way to go about this?
[EDIT]
This is what I've come up with so far:
const dataAdapter = rawData => {
const topics = ['clicks', 'revenue', 'cost', 'roi'];
const topicsData = topics.map((topic, index) => {
const thisTopic = {};
thisTopic.topicName = topic;
thisTopic.topic = index;
thisTopic.dates = [];
return thisTopic;
});
const convertedData = topicsData.map(topicData => {
const thisTopic = topicData;
const map = new Map();
rawData.forEach(elem => {
map.set(elem.date, (map.get(elem.date) || 0) + elem[[thisTopic.topicName]]);
});
thisTopic.dates = Array.from(map);
return thisTopic;
});
return convertedData;
};
Thanks,
/J
You could take an object as reference to the wanted keys and date. Then iterate data and check if a reference to a result set exists. If not, create a new result set.
var result = [{ topicName: 'Clicks', topic: 1, dates: [], }, { topicName: 'Cost', topic: 2, dates: [], }],
data = [{ date: "2014-02-01", device: "Computer", "match-type": "NA", clicks: 66, revenue: 1037, conversions: 2, cost: 284.35, impressions: 5330, ROI: 3.64691401441885 }, { date: "2014-02-01", device: "Tablet", "match-type": "NA", clicks: 38, revenue: 587, conversions: 2, cost: 194.01000000000005, impressions: 1934, ROI: 3.025617236224936 }, { date: "2014-02-02", device: "Tablet", "match-type": "NA", clicks: 40, revenue: 587, conversions: 2, cost: 190, impressions: 1934, ROI: 3.025617236224936 }],
hash = { clicks: { _: result[0].dates }, cost: { _: result[1].dates }, };
data.forEach(function (o) {
['clicks', 'cost'].forEach(function (k) {
if (!hash[k][o.date]) {
hash[k][o.date] = { date: o.date, value: o[k] };
hash[k]._.push(hash[k][o.date]);
return;
}
hash[k][o.date].value += o[k];
});
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Assuming your data is in data variable and topics are in topics variable. This solution uses only javascript builtin objects.
const getPropDateMap = (obj, prop) => obj
.reduce((accObj, item) => {
return Object.assign(accObj, {
[item.date]: item.clicks + (accObj[item.date] || 0)
})
}, {})
topics.forEach(topic => {
topic.dates = Object
.entries(getPropDateMap(data, topic.topicName.toLowerCase()))
.map(entry => ({date: entry[0], value: entry[1]}))
})

Javascript functional programming / Passing parameters

I started experimenting with functional programming lately and I'm trying to convert an old module I have written using imperative programming.
Let's say I have two arrays of objects i.e
orders: [
{
idOrder: 1,
amount: 100,
customerId: 25,
},
{
idOrder: 2,
amount: 200,
customerId: 20,
}
]
customers: [
{
customerId: 20,
name: "John Doe",
orders: []
},
{
customerId: 25,
name: "Mary Jane",
orders: []
}
]
I want to push all the orders to their respective customer. Is there a clean way of doing it?
I have tried this , but obviously it doesn't work that way :
customers.orders = orders.filter((x) => {
if (x.customerId === customerId) {
customer.orders.push(x);
}
});
Thanks
You could use a Map and get all customers first and then push the orders to the customers.
var object = { orders: [{ idOrder: 1, amount: 100, customerId: 25 }, { idOrder: 2, amount: 200, customerId: 20 }], customers: [{ customerId: 20, name: "John Doe", orders: [] }, { customerId: 25, name: "Mary Jane", orders: [] }] },
map = object.customers.reduce((m, a) => m.set(a.customerId, a), new Map);
object.orders.forEach(a => map.get(a.customerId).orders.push(a));
console.log(object.customers);
Possible solution:
for (c of customers){
c.orders.push(orders.filter( function(o){ return o.customerId === c.customerId} ));
}
If you think of customers as your accumulator you can Reduce orders with customers as your initial value.
NOTE: this does mutate customers if you do not want this as a side-effect you would have to clone customers. Also there is not error handling for customerId not found.
var orders = [{ idOrder: 1, amount: 100, customerId: 25 }, { idOrder: 2, amount: 200, customerId: 20}];
var customers = [{ customerId: 20, name: "John Doe", orders: [] }, { customerId: 25, name: "Mary Jane", orders: [] } ];
var customers_orders = orders.reduce(
(accum, v) =>
{ accum.find(
c => c.customerId == v.customerId).orders.push(v);
return accum;
}, customers);
console.log(customers_orders);
You can write a function and pass it to reduce method and compose it with map
Just one things: once it's created, it may never change. You can user Object.assign and concat.
var customersWithOrders = customers.map(function(customer) {
var relatedOrders = orders.filter(function(order) { return order.customerId === customer.customerId })
return Object.assign(
customer,
{
orders: customer.orders.concat(relatedOrders)
}
)
})

Categories