Calculating the average time based on total on a Array - javascript

Using Lodash and moment packages.
I have a array with objects. Each has a 2 important properties.
Assume the trades are like this
const trades = [
{ RealizedPnL: 20, OpenTradeAt: '2023-02-16T19:04:16.2163527' },
{ RealizedPnL: 30, OpenTradeAt: '2023-01-23T13:45:32.4256713' },
{ RealizedPnL: 10, OpenTradeAt: '2023-02-03T08:12:01.0152846' },
{ RealizedPnL: -15, OpenTradeAt: '2023-01-10T16:39:52.1198765' },
{ RealizedPnL: -5, OpenTradeAt: '2023-02-24T11:27:56.3297178' },
{ RealizedPnL: -5, OpenTradeAt: '2023-02-24T11:27:56.3297178' },
{ RealizedPnL: 25, OpenTradeAt: '2023-02-26T13:17:56.3297128' },
{ RealizedPnL: 60, OpenTradeAt: '2023-02-24T17:27:56.3297178' }
];
I want to know based on this information whetever the best time is to take a trade in average. Let's say there are 4 very good trades (positive RealizedPnL) at around 17:00 and there is 2 (positive RealizedPnL) trades at arround 20:00 then the best trade in average shall be 17:00 in this case.
This is basically a indication for which time the trades is best to open based on information from the past.
I want to actually know both the time which the trader is best to open a trade at and the worst time to trade but I want to focus on best time first.
What I already got:
const timeRegex = /(\d{2}):(\d{2}):(\d{2})/;
const times = _.map(trades, (trade) => {
const [, hours, minutes] = trade.OpenTradeAt.match(timeRegex);
return { time: `${hours}:${minutes}`, PnL: trade.RealizedPnL };
});
Result:
[
{
"time": "19:04",
"PnL": 20
},
{
"time": "13:45",
"PnL": 30
},
{
"time": "08:12",
"PnL": 10
},
{
"time": "16:39",
"PnL": -15
},
{
"time": "11:27",
"PnL": -5
},
{
"time": "11:27",
"PnL": -5
},
{
"time": "13:17",
"PnL": 25
},
{
"time": "17:27",
"PnL": 60
}
]
Is this even possible to know with these information?

Related

How to implement a reduce correctly?

I am getting a table with data coming from an array, but there was some data that was repeated and I was advised to do a reduce.
The data arrives well, but there are some that I cannot obtain.
This is what I'm trying to implement, from what I understand about reduce, accumulate the array and then do the traversal
this.headerVariedades = response['objParams'].monthlyTitle;
const aux = response['objParams'].monthlyDetail.reduce((acc, curr: {
mesTemporada: string,
codigo_producto: number,
monto_moneda_norm: number
}) => {
if (acc[curr.mesTemporada]) {
acc[curr.mesTemporada][curr.codigo_producto] = curr.monto_moneda_norm;
} else {
const aux = {
mesTemporada: curr.mesTemporada
};
aux[curr.codigo_producto] = curr.monto_moneda_norm;
acc[curr.mesTemporada] = aux;
}
return acc;
}, {});
this.listadoResultado = Object.values(aux);
As seen in the image I need to fill in the "fecha" and "fecHas"
This is what you get when you check with postman.
"weeklyTitle": [
{
"codigo": 27,
"pro_des": "Maíz Amarillo, FOB Puerto Argentino",
"gru_cod": 2
},
{
"codigo": 18,
"pro_des": "Maíz Yellow N° 2, FOB Golfo, USA",
"gru_cod": 2
},
{
"codigo": 51,
"pro_des": "Maíz Yellow N° 3, FOB Golfo, USA",
"gru_cod": 2
}
],
"weeklyDetail": [
{
"semana": 20,
"temporada": 2021,
"fecha": 1621224000000,
"fecHas": 1621742400000,
"codigo_producto": 18,
"monto_moneda_norm": 305.4700
},
{
"semana": 20,
"temporada": 2021,
"fecha": 1621224000000,
"fecHas": 1621742400000,
"codigo_producto": 27,
"monto_moneda_norm": 259.8000
},
{
"semana": 20,
"temporada": 2021,
"fecha": 1621224000000,
"fecHas": 1621742400000,
"codigo_producto": 51,
"monto_moneda_norm": 304.4700
},
{
"semana": 21,
"temporada": 2021,
"fecha": 1621828800000,
"fecHas": 1622347200000,
"codigo_producto": 18,
"monto_moneda_norm": 299.3500
},
{
"semana": 21,
"temporada": 2021,
"fecha": 1621828800000,
"fecHas": 1622347200000,
"codigo_producto": 27,
"monto_moneda_norm": 254.3333
},
{
"semana": 21,
"temporada": 2021,
"fecha": 1621828800000,
"fecHas": 1622347200000,
"codigo_producto": 51,
"monto_moneda_norm": 298.3500
}
],
In this way I am assembling the table, as I receive the header in another array I do a forEach.
this.columnasGrilla.push({
header: 'Semana ',
field: 'semana'
});
this.columnasGrilla.push({
header: 'Desde',
field: 'fecha'
});
this.columnasGrilla.push({
header: 'Hasta',
field: 'fecHas'
});
this.headerVariedades.forEach((item) => {
this.columnasGrilla.push({
header: item.pro_des,
field: 'item.codigo'
})
})
This is the example that repeat values. This happens when I choose more variety of a product.
in this case I don't use the reduce . I just call it this way
this.listadoResultado = response['objParams'].weeklyDetail;
that's why I was recommended to use reduce to sort the table in these cases

Getting multiple percentage from a reduce map of a json data

I would like to apologize in advance if there's already question like this but I've been searching all day and I really can find anything.
I call an api that returns a response. Then I only get the details/data that I need and currently now have a json file with this sample value:
{
"total": 563,
"shipping_fee": 58,
"e_charges": ???,
"order_items": [
{
"item_id": 6291020872,
"quantity": 1,
"price": 88,
"total": 88
},
{
"item_id": 7755274567,
"quantity": 1,
"price": 150,
"total": 150
},
{
"item_id": 7980571205,
"quantity": 1,
"price": 45,
"total": 45
},
{
"item_id": 12612977930,
"quantity": 1,
"price": 280,
"total": 280
}
]
}, ... {} {} {}....
My problem is that, I need to get the sum of all total in order_items [] then after that, I need to add the shipping_fee and get 2 sets of percentage
10%
8%
Note that, I'm getting the values in run-time.
const resDetails = await request.post(baseURL, {
data: viewData
});
info = await JSON.parse(JSON.stringify(await resDetails.json()));
This is where the json file came from:
orders = await info.data.orders.map((x) => (total: x.order_items.map(y => Number(y.order_price) * y.amount).reduce((total, y) => y+total),
shipping_fee : Number(x.shipping_fee),
e_charges: Number(Number((x.order_items.map(y=> Number(y.order_price) * y.amount).reduce((total, y) => Number(y+total)) + Number(x.shipping_fee)) * 0.1).toFixed()),
order_items: x.order_items.map((y) => ({
item_id : y.item_id,
quantity: y.amount,
price: Number(y.order_price),
total: Number(y.order_price) * y.amount)}))
I was able to get the 10% but my main problem is adding the other 8% without doing mapping, reducing and adding the shipping_fee again.
Number(Number((x.order_items.map(y=> Number(y.order_price) * y.amount).reduce((total, y) => Number(y+total)) + Number(x.shipping_fee)) * 0.1).toFixed())
I'm fairly new to the javascript/typescript world. I hope someone can help me find a more optimized way to achieve what I need. Thank you.
EDIT:
Here's the actual code:
You may please go through the below code. I'm not a big fan of doing map, reduce and calculations in single line because it takes away the readability. So, I broken down the calculations to a number of methods.
At the end, order total is calculated only once and saved the e_charges percentages in 2 different keys e_charges_8 and e_charges_10, which I believe you can modify as per your need.
let orders = [
{
"shipping_fee": 58,
"order_items": [
{
"item_id": 6291020872,
"quantity": 1,
"price": 88
},
{
"item_id": 7755274567,
"quantity": 1,
"price": 150
},
{
"item_id": 7980571205,
"quantity": 1,
"price": 45
},
{
"item_id": 12612977930,
"quantity": 1,
"price": 280
}
]
},
{
"shipping_fee": 58,
"order_items": [
{
"item_id": 6291020872,
"quantity": 1,
"price": 88
},
{
"item_id": 7755274567,
"quantity": 1,
"price": 150
},
{
"item_id": 7980571205,
"quantity": 1,
"price": 45
},
{
"item_id": 12612977930,
"quantity": 1,
"price": 280
}
]
}
]
const getOrderItemTotal = (orderItem) => {
return orderItem.quantity * orderItem.price
}
const getPercentage = (shippingFee, total, percentage) => {
return ((total + shippingFee) * percentage).toFixed()
}
const getOrderItemsTotal = (orderItems) => {
return orderItems.reduce((total, orderItem) => (total + orderItem.total), 0)
}
orders = orders.map(order => {
order.order_items.map(orderItem => {
orderItem.total = getOrderItemTotal(orderItem)
return orderItem
})
order.total = getOrderItemsTotal(order.order_items)
order.e_charges_8 = getPercentage(order.shipping_fee, order.total, 0.1)
order.e_charges_10 = getPercentage(order.shipping_fee, order.total, 0.08)
return order
})
console.log(orders)

Process any array without using For Loop

I want to achieve Output dynamically without using FOR Loop.
Input is array of multiple objects.
var input = [
{
"name": "Siner1",
"RTime": 40,
"FTime": 30
},
{
"name": "Siner2",
"RTime": 50,
"FTime": 60
}
]
var output = [
{
"RTime": {
"Siner1": 40,
"Siner2": 50
},
"FTime": {
"Siner1": 30,
"Siner2": 60
}
}
]
//console.log(input)
console.log(output);
Can someone assist me on this.
Approach 1
var input = [
{
"name": "Siner1",
"RTime": 40,
"FTime": 30
},
{
"name": "Siner2",
"RTime": 50,
"FTime": 60
}
]
var output = input.reduce(function(initial,next){
initial["RTime"][next["name"]]=(initial["RTime"][next["name"]] || 0 ) + next["RTime"];
initial["FTime"][next["name"]]=(initial["FTime"][next["name"]] || 0 ) + next["FTime"];
return initial;
},{"RTime":{},"FTime":{}});
console.log(output)
Approach 2
In the event that there are other keys or values you would like to aggregate
var input = [
{
"name": "Siner1",
"RTime": 40,
"FTime": 30
},
{
"name": "Siner2",
"RTime": 50,
"FTime": 60
}
]
var keys = ['RTime','FTime'];
output=input.reduce(function(initial,next){
keys.map(function(key){
initial[key][next["name"]]=(initial[key][next["name"]] || 0 ) + next[key];
});
return initial;
},keys.reduce(function(data,key){
data[key]={};
return data;
},{}));
console.log(output)
The provided generic approach is based on Array.prototype.reduce. Since it uses the accumulator argument of its callback function as a configurable collector it is totally agnostic about the keys of any processed item of any given list. One just needs to provide the (source) key of a processed item that's value then serves as target key of newly grouped key values ...
function restructureKeysAndValues(collector, item) {
const {
keyOfTargetValue,
registry,
index,
list
} = collector;
const targetKey = item[keyOfTargetValue];
Object.entries(item).forEach(([key, value]) => {
if (key !== keyOfTargetValue) {
let keyGroup = registry[key];
if (!keyGroup) {
keyGroup = registry[key] = { [key]: {} };
Object.assign(index, keyGroup);
list.push(keyGroup);
}
keyGroup[key][targetKey] = value;
}
});
return collector;
}
console.log('a list of group objects ...', [{
"name": "Siner1",
"RTime": 40,
"FTime": 30
}, {
"name": "Siner2",
"RTime": 50,
"FTime": 60
}].reduce(restructureKeysAndValues, {
keyOfTargetValue: 'name',
registry: {}, // - for internal tracking
index: {}, // - for external use, output as map/index.
list: [] // - for external use, output as array/list.
}).list
);
console.log('a single grouped object ...', [{
"name": "Siner1",
"RTime": 40,
"FTime": 30
}, {
"name": "Siner2",
"RTime": 50,
"FTime": 60
}].reduce(restructureKeysAndValues, {
keyOfTargetValue: 'name',
registry: {}, // - for internal tracking
index: {}, // - for external use, output as map/index.
list: [] // - for external use, output as array/list.
}).index
);
console.log('a list of group objects ...', [{
"name": "Siner1",
"RTime": 40,
"FTime": 30,
"CTime": 70,
"ATime": 90
}, {
"name": "Siner2",
"RTime": 50,
"FTime": 60,
"CTime": 30,
"ATime": 40
}, {
"name": "Siner3",
"RTime": 90,
"FTime": 20,
"CTime": 10,
"ATime": 20
}].reduce(restructureKeysAndValues, {
keyOfTargetValue: 'name',
registry: {}, // - for internal tracking
index: {}, // - for external use, output as map/index.
list: [] // - for external use, output as array/list.
}).list
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Sort array by child's child

The situation is: I have an array of objects, in which every object has an array of objects. The array looks like this:
[
{
"dislikes": [
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511001,
"timezoneOffset": -60,
"year": 118
},
},
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511008,
"timezoneOffset": -60,
"year": 118
},
}
],
},
{
"dislikes": [
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511011,
"timezoneOffset": -60,
"year": 118
},
},
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511028,
"timezoneOffset": -60,
"year": 118
},
}
],
}
]
So I want to sort the users, and the dislikes by the time in their dislikes. So the user with the earliest dislike would be first, as well as the earliest dislike would be first in each users' dislikes array. I believe I have to do multiple sorts, but how can I do that exactly?
You can map the items and add a property to it containing the earliest dislike and then sort on that:
const data = [{"dislikes":[{"createDate":{"date":11,"day":0,"hours":18,"minutes":15,"month":10,"seconds":11,"time":1541956511001,"timezoneOffset":-60,"year":118}},{"createDate":{"date":11,"day":0,"hours":18,"minutes":15,"month":10,"seconds":11,"time":1541956511008,"timezoneOffset":-60,"year":118}}]},{"dislikes":[{"createDate":{"date":11,"day":0,"hours":18,"minutes":15,"month":10,"seconds":11,"time":1541956511011,"timezoneOffset":-60,"year":118}},{"createDate":{"date":11,"day":0,"hours":18,"minutes":15,"month":10,"seconds":11,"time":1541956511028,"timezoneOffset":-60,"year":118}}]}];
console.log(
data
//map and add newestDislike property
.map((d) => ({
...d,
//reduce and only takes the lowest time value
newestDislike: (d.dislikes || []).reduce(
(result, item) =>
item.createDate.time < result
? item.createDate.time
: result,
Infinity, //defaults to infinity (if no dislikes)
),
}))
.sort((a, b) => a.newestDislike - b.newestDislike),
);
If the dislikes in the user are already sorted by oldest date first then you can skip the map and reduce part. If a user can have empty dislikes or undefined then make sure you use a getter function with a default so your code won't crash:
//gets a nested prop from object or returns defaultValue
const get = (o = {}, path, defaultValue) => {
const recur = (o, path, defaultValue) => {
if (o === undefined) return defaultValue;
if (path.length === 0) return o;
if (!(path[0] in o)) return defaultValue;
return recur(o[path[0]], path.slice(1), defaultValue);
};
return recur(o, path, defaultValue);
};
console.log(
data.sort(
(a, b) =>
get(
a,
['dislikes', 0, 'createDate', 'time'],
Infinity,
) -
get(
b,
['dislikes', 0, 'createDate', 'time'],
Infinity,
),
),
);
//Supply the array you've metioned as the argument users to the below method, sortDislikesForAllUsers
let sortDislikesForAllUsers = function(users) {
return users.map(user => {
return {
dislikes: user.dislikes.sort((dislikeA, dislikeB) => ((dislikeA.createDate.time < dislikeB.createDate.time) ? -1 : (dislikeA.createDate.time > dislikeB.createDate.time) ? 1 : 0))
}
})
}
//Supply the array returned in the above method as input to the below method, sortUsers
let sortUsers = function(arrayOfSortedDislikesPerUser) {
return arrayOfSortedDislikesPerUser.sort((userA, userB) => ((userA.dislikes[0].createDate.time < userB.dislikes[0].createDate.time) ? -1 : (userA.dislikes[0].createDate.time > userB.dislikes[0].createDate.time) ? 1 : 0))
}
let arrayOfSortedDislikesPerUser = sortDislikesForAllUsers(users);
let finalSortedArray = sortUsers(arrayOfSortedDislikesPerUser);
console.log(finalSortedArray);
In the below snippet,
sortDislikesForAllUsers This method sorts the dislikes for individual
users
sortUsers This method sorts the users based on the first dislike time
of the sorted dislikes array obtained from the above method
Simple :)
Run the below snippet. You can directly copy paste it in your code!
let users = [{
"dislikes": [
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511001,
"timezoneOffset": -60,
"year": 118
},
},
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511008,
"timezoneOffset": -60,
"year": 118
},
}
],
},
{
"dislikes": [
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511011,
"timezoneOffset": -60,
"year": 118
},
},
{
"createDate": {
"date": 11,
"day": 0,
"hours": 18,
"minutes": 15,
"month": 10,
"seconds": 11,
"time": 1541956511028,
"timezoneOffset": -60,
"year": 118
},
}
],
}]
let sortDislikesForAllUsers = function(users) {
return users.map(user => {
return {
dislikes: user.dislikes.sort((dislikeA, dislikeB) => ((dislikeA.createDate.time < dislikeB.createDate.time) ? -1 : (dislikeA.createDate.time > dislikeB.createDate.time) ? 1 : 0))
}
})
}
let sortUsers = function(arrayOfSortedDislikesPerUser) {
return arrayOfSortedDislikesPerUser.sort((userA, userB) => ((userA.dislikes[0].createDate.time < userB.dislikes[0].createDate.time) ? -1 : (userA.dislikes[0].createDate.time > userB.dislikes[0].createDate.time) ? 1 : 0))
}
let arrayOfSortedDislikesPerUser = sortDislikesForAllUsers(users);
let finalSortedArray = sortUsers(arrayOfSortedDislikesPerUser);
console.log(finalSortedArray);
EDIT: WRT to the comment by #HMR:
1. It mutates the original array. Yes. If you want to avoid mutation, you must create a copy of the sent array.
let noRefCopy = new Array()
noRefCopy = noRefCopy.concat(originalArr)
Now, perform sorting on the copy and return the same.
2. If you wanna have checks for undefined etc, sure you can.
The above answer attempts to address the logic. Sure we can address the above 2 concerns if the question is really specific to them.
Cheers,
Kruthika
Check the code below. This will let you sort based on time:
function sortByTime(obj1, obj2){
return obj1.time - obj2.time;
}
array.sort((obj1, obj2)=>{
obj1.dislikes.sort(sortByTime);
obj2.dislikes.sort(sortByTime);
return obj1.dislikes[0].time - obj2.dislikes[0].time;
});
I did not get what you meant by earliest time. The above code sorts time in ascending order.
NOTE: The above code does not handle edge cases where a property night be missing
Something like as follows (with lodash.js)
_.each(users, (u) => { u.dislikes = _.sortBy(u.dislikes, 'createdDate.time'); });
users = _.sortBy(users, 'dislikes[0].createdDate.time');

Group sum and transform json object with values in nested array

I am trying to aggregate and transform the following json :
[
{
"orderId" : "01",
"date" : "2017-01-02T06:00:00.000Z",
"items" : [
{
"itemId": 100,
"itemCost": 12,
"itemQuantity": 10
},
{
"itemId": 102,
"itemCost": 25,
"itemQuantity": 4
}
]
},
{
"orderId": "02",
"date" : "2017-01-08T06:00:00.000Z",
"items" : [
{
"itemId": 100,
"itemCost": 15,
"itemQuantity": 2
},
{
"itemId": 101,
"itemCost": 20,
"itemQuantity": 5
},
{
"itemId": 102,
"itemCost": 25,
"itemQuantity": 1
}
]
},
{
"orderId": "03",
"date" : "2017-02-08T06:00:00.000Z",
"items" : [
{
"itemId": 100,
"itemCost": 15,
"itemQuantity": 2
},
{
"itemId": 101,
"itemCost": 20,
"itemQuantity": 5
},
{
"itemId": 102,
"itemCost": 25,
"itemQuantity": 1
}
]
}]
into an object that is grouped by itemId, and then aggregated by quantity, and aggregated by total cost (item cost * item quantity for each order) by month. Example:
[
{
"itemId": 100,
"period": [
{
"month": "01/17",
"quantity": 12,
"cost": 130
}
]
},
{
"itemId": 101,
"period": [
{
"month": "01/17",
"quantity": 5,
"cost": 100
},
{
"month": "02/17",
"quantity": 5,
"cost": 100
}
]
},
{
"itemId": 102,
"period": [
{
"month": "01/17",
"quantity": 5,
"cost": 125
},
{
"month": "02/17",
"quantity": 1,
"cost": 25
}
]
}
]
I have a small indention on my desk in which I have been beating my head trying to figure how to do this using native map/reduce or lodash.
You can do like this:
var orders = [{orderId:"01",date:"2017-01-02T06:00:00.000Z",items:[{itemId:100,itemCost:12,itemQuantity:10},{itemId:102,itemCost:25,itemQuantity:4}]},{orderId:"02",date:"2017-01-08T06:00:00.000Z",items:[{itemId:100,itemCost:15,itemQuantity:2},{itemId:101,itemCost:20,itemQuantity:5},{itemId:102,itemCost:25,itemQuantity:1}]},{orderId:"03",date:"2017-02-08T06:00:00.000Z",items:[{itemId:100,itemCost:15,itemQuantity:2},{itemId:101,itemCost:20,itemQuantity:5},{itemId:102,itemCost:25,itemQuantity:1}]}];
// First, map your orders by items
var items = {};
orders.forEach(function(order) {
// set the month of each order
var month = new Date(order.date);
month = ('0' + (month.getMonth() + 1)).slice(-2) + '/' + String(month.getFullYear()).slice(-2);
// for each item in this order
order.items.forEach(function(item) {
// here we already have both keys: "id" and "month"
// then, we make sure they have an object to match
var id = item.itemId;
if (!items[id]) {
items[id] = {};
}
if (!items[id][month]) {
items[id][month] = { cost:0, quantity:0 };
}
// keep calculating the total cost
items[id][month].cost += item.itemCost * item.itemQuantity;
items[id][month].quantity += item.itemQuantity;
});
});
// Now, we format the calculated values to your required output:
var result = Object.keys(items).map(function(id) {
var obj = {
itemId: id,
period: Object.keys(items[id]).map(function(month) {
items[id][month].month = month;
return items[id][month];
}),
};
return obj;
});
console.log(result);
Hope it helps.
You could use this transformation:
const result = Object.values(myList.reduce( (acc, o) => {
const month = o.date.substr(5,2) + '/' + o.date.substr(2,2);
return o.items.reduce ( (acc, item) => {
const it = acc[item.itemId] || {
itemId: item.itemId,
period: {}
},
m = it.period[month] || {
month: month,
quantity: 0,
cost: 0
};
m.cost += item.itemCost * item.itemQuantity;
m.quantity += item.itemQuantity;
it.period[month] = m;
acc[item.itemId] = it;
return acc;
}, acc);
}, {})).map( o =>
Object.assign({}, o, { period: Object.values(o.period) })
);
const myList = [
{
"orderId" : "01",
"date" : "2017-01-02T06:00:00.000Z",
"items" : [
{
"itemId": 100,
"itemCost": 12,
"itemQuantity": 10
},
{
"itemId": 102,
"itemCost": 25,
"itemQuantity": 4
}
]
},
{
"orderId": "02",
"date" : "2017-01-08T06:00:00.000Z",
"items" : [
{
"itemId": 100,
"itemCost": 15,
"itemQuantity": 2
},
{
"itemId": 101,
"itemCost": 20,
"itemQuantity": 5
},
{
"itemId": 102,
"itemCost": 25,
"itemQuantity": 1
}
]
},
{
"orderId": "03",
"date" : "2017-02-08T06:00:00.000Z",
"items" : [
{
"itemId": 100,
"itemCost": 15,
"itemQuantity": 2
},
{
"itemId": 101,
"itemCost": 20,
"itemQuantity": 5
},
{
"itemId": 102,
"itemCost": 25,
"itemQuantity": 1
}
]
}];
const result = Object.values(myList.reduce( (acc, o) => {
const month = o.date.substr(5,2) + '/' + o.date.substr(2,2);
return o.items.reduce ( (acc, item) => {
const it = acc[item.itemId] || {
itemId: item.itemId,
period: {}
},
m = it.period[month] || {
month: month,
quantity: 0,
cost: 0
};
m.cost += item.itemCost * item.itemQuantity;
m.quantity += item.itemQuantity;
it.period[month] = m;
acc[item.itemId] = it;
return acc;
}, acc);
}, {})).map( o =>
Object.assign({}, o, { period: Object.values(o.period) })
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I think the other answers out there do a pretty good job from the vanilla angle, so I wanted to take a stab at a more lodash-intensive approach since you mentioned it as a tag. This is mainly just a fun challenge, but I hope the solution is elegant enough for you to lift components from.
Before we begin, I'll be using both the vanilla lodash module and the functional programming flavor of lodash. Let fp be the functional programming module and _ be vanilla (and let orders be your original data structure). Also, as a challenge, I'll do my best to minimize vanilla JS methods and arrow funcs to maximize lodash methods and function creation methods.
First, let's get all the items in a row, paired with their order information:
const items = _.flatMap(orders, o=> _.map(o.items, i=> [i, o]));
I know I said I wanted to minimize arrow functions, but I couldn't think of any other way to get the order object to the end of the chain. Challenge yourself to rewrite the above in terms of a composition (e.g. fp.compose or _.flow) and see what happens.
I'd say now's as good a time as any to group up our pairs by the item id:
const id_to_orders = _.groupBy(items, fp.get('[0].itemId'));
Here, fp.get('[0].itemId') gives us a function which, given an array, returns the itemId of the first element (in our case, we have a list of pairs, the first element of which is the item, the second of which is the relevant order object). Therefore, id_to_orders is a map from an item's ID to a list of all the times it was ordered.
This id_to_orders map looks pretty close to the data structure we're after. At a high level, all that's left is transforming the order data for each item into the quantity and cost, grouped by month.
const result = _.mapValues(id_map, fp.flow(
// Arrange the item's orders into groups by month
fp.groupBy(month)
// We're done with the order objects, so fp.get('[0]') filters them
// out, and the second function pairs the item's cost and quantity
, fp.mapValues(fp.flow(
fp.map(fp.flow(fp.get('[0]'), i=> [i.itemCost, i.itemQuantity]))
// Sum up the cost (left) and quantity (right) for the item for the month
, fp.reduce(add_pair, [0, 0])))
// These last couple lines just transform the resulting data to look
// closer to the desired structure.
, _.toPairs
, fp.map(([month, [cost, count]])=> ({month, cost, count}))
));
And the helpers month and add_pair referenced above:
function month([item, order]){
const date = new Date(order.date)
, month = date.getMonth() + 1
, year = date.getFullYear().toString().slice(-2);
return `${month}/${year}`;
}
function add_pair(p1, p2){
return [p1[0] + p2[0], p1[1] + p2[1]];
}
Just out of curiosity (or sadism), let's see what this whole thing would look like chained together as a single pipeline:
const get_order_data = fp.flow(
fp.flatMap(o=> _.map(o.items, i=> [i, o]))
, fp.groupBy(fp.get('[0].itemId'))
, fp.mapValues(fp.flow(
fp.groupBy(month)
, fp.mapValues(fp.flow(
fp.map(fp.flow(fp.get('[0]'), i=> [i.itemCost, i.itemQuantity]))
, fp.reduce(add_pair, [0, 0])))
, _.toPairs
, fp.map(([month, [cost, count]])=> ({month, cost, count})))
));
const result = get_order_data(orders);
You'll notice this composed version has a lot more fp (as opposed to _). If you're curious why it's easier this way, I encourage you to read the lodash FP guide.
jsfiddle with everything.
Finally, if you'd like to transform the result from the code above exactly into the output format you mentioned in your post, here's what I recommend:
const formatted = _.keys(result).map(k=> ({itemId: k, periods: result[k]}));

Categories

Resources