Group (by year) and sum in javascript collection - javascript

I want to sum the values of the same year in a single value for each year, in a node javascript application. I would like to avoid a for(;;) iteration, discovering the year, sum the values.... Maybe there is something in the Underscore library (like _.groupBy but I cannot figure how use it in this case).
The json data is this (i.e quarterly income data):
{ "data":
["2015-07-22",125677000000.0],
["2015-04-28",129006000000.0],
["2015-01-28",123328000000.0],
["2014-10-27",111547000000.0],
["2014-07-23",120940000000.0],
["2014-04-24",120178999999.99998],
["2014-01-28",129684000000.0],
["2013-10-30",123549000000.0],
["2013-07-24",123354000000.00002],
["2013-04-24",135490000000.0],
["2013-01-24",127346000000.0],
["2012-10-31",118210000000.0],
[etc...]}
the result should be:
{ "data":
["2015",sum of all 2015 data],
["2014",sum of all 2014 data],
["2013",sum of all 2013 data],
[etc...]}

Not a problem at all, no loops, no underscore needed.
a = { "data":[
["2015-07-22",125677000000.0],
["2015-04-28",129006000000.0],
["2015-01-28",123328000000.0],
["2014-10-27",111547000000.0],
["2014-07-23",120940000000.0],
["2014-04-24",120178999999.99998],
["2014-01-28",129684000000.0],
["2013-10-30",123549000000.0],
["2013-07-24",123354000000.00002],
["2013-04-24",135490000000.0],
["2013-01-24",127346000000.0],
["2012-10-31",118210000000.0]]}
var years = a.data.map(function(d) {
return [new Date(d[0]).getFullYear(), d[1]];
});
var sums = years.reduce(function(prev, curr, idx, arr) {
var sum = prev[curr[0]];
prev[curr[0]] = sum ? sum + curr[1] : curr[1];
return prev;
}, {});
> sums
Object {2012: 118210000000, 2013: 509739000000, 2014: 482350000000, 2015: 378011000000}
Note that I had to fix your JSON data. The one you posted wasn't valid JSON, so I just added array brackets around the list of lists.
You can even do it in 1 line if you want.
a.data.map(function(d) { return [new Date(d[0]).getFullYear(), d[1]]; }).reduce(function(prev, curr, idx, arr) { var sum = prev[curr[0]]; prev[curr[0]] = sum ? sum + curr[1] : curr[1]; return prev; }, {});
Just kidding, please don't do that :)

var data = [
["2015-07-22",125677000000.0],
["2015-04-28",129006000000.0],
["2015-01-28",123328000000.0],
["2014-10-27",111547000000.0],
["2014-07-23",120940000000.0],
["2014-04-24",120178999999.99998],
["2014-01-28",129684000000.0],
["2013-10-30",123549000000.0],
["2013-07-24",123354000000.00002],
["2013-04-24",135490000000.0],
["2013-01-24",127346000000.0],
["2012-10-31",118210000000.0]
];
Create the sums in a map (years -> sums) - then turn the map into an array with _.pairs:
var result = _.pairs(data.reduce(function(prev, curr) {
var year = curr[0].substr(0, 4);
prev[year] = prev[year] ? prev[year] + curr[1] : curr[1];
return prev;
}, {}));
Then order by first element, descending:
result.sort(function(a, b) { return a[0] < a[1]; });

Now that you mention underscore, iterating once and calling the indexes do it
var json = { "data": [["2015-07-22",125677000000.0], ["2015-04-28",129006000000.0], ["2015-01-28",123328000000.0], ["2014-10-27",111547000000.0], ["2014-07-23",120940000000.0], ["2014-04-24",120178999999.99998], ["2014-01-28",129684000000.0], ["2013-10-30",123549000000.0], ["2013-07-24",123354000000.00002], ["2013-04-24",135490000000.0], ["2013-01-24",127346000000.0], ["2012-10-31",118210000000.0]]
};
var sums = {};
_.each(json.data, function(arr) {
var index = arr[0].split('-')[0];
if (typeof(sums[index]) !== 'undefined') {
sums[index] += arr[1];
} else {
sums[index] = arr[1];
} });
And yes, as #bbill points out, I fix your JSON as well :)
Here is a JSFiddle for it

Related

How to transform a js array to make it by day

const arr = [
{"datetime":"2018/8/5","value":85,"type":"A"},
{"datetime":"2018/8/10","value":7,"type":"B"},
{"datetime":"2018/8/10","value":73,"type":"A"}
];
I have an array as you can see in the snippet. My issue is I need to check something per day:
For each day when A value > 60 or when B value > 6 then do something.
Else when A value <= 60 and when B value <= 6 then do something else.
And I don't know how to do this check with the current array structure as each step in the loop is a different day. I want to compare all values for one day at the same time.
Is it possible to transform the array to look like this? Then I will be able to compare day per day...
const arr = [
{"datetime":"2018/8/5","valueA":85,"valueB":undefined},
{"datetime":"2018/8/10","valueB":7,"valueA":73}
];
Thank you!
You can make a the date groups by reducing into an object. Then just set the appropriate value in that object. In the end your array will be in the Object.keys() of the grouped object.
[As you might surmise from the comments, the order of the final array is not guaranteed because object keys and values are not guaranteed. If your original data is ordered by date, you should say so in the question because there will be more efficient ways to do this if the order is guaranteed].
const arr = [{"datetime":"2018/8/5","value":85,"type":"A"},{"datetime":"2018/8/10","value":7,"type":"B"},{"datetime":"2018/8/10","value":73,"type":"A"}];
let groups = arr.reduce((obj, {datetime, value, type}) => {
if (!obj[datetime]) obj[datetime] = {datetime, valueA:undefined, valueB:undefined}
let currentKey = type == 'A' ? "valueA" : "valueB"
obj[datetime][currentKey] = value
return obj
},{})
let newArr = Object.values(groups)
console.log(newArr)
This will transform the array as OP asked for, and will respect the order.
const arr = [{"datetime":"2018/8/5","value":85,"type":"A"},
{"datetime":"2018/8/10","value":7,"type":"B"},
{"datetime":"2018/8/10","value":73,"type":"A"}];
var daysArr = []
arr.map(function(day){
var keyName = 'value'+day.type
var found = false
var dayObj = {}
for (var i=0; i < daysArr.length; i++) {
if (daysArr[i].datetime === day.datetime) {
daysArr[i][keyName] = day.value;
found = true
break
}
}
if (!found) {
dayObj = {"datetime":day.datetime,valueA:undefined,valueB:undefined}
dayObj[keyName] = day.value
daysArr.push(dayObj)
}
})
console.log(daysArr);
One solution could be using reduce(). Note that if a key is not defined will return undefined (this is exemplified on the second log to the console), so I consider redundant to define, for example "value-B": undefined, unless you want to assign to it another default value.
Warning: As discussed on the comments, you should note that the order of the final result, may not be preserved.
const arr = [
{"datetime":"2018/8/5","value":85,"type":"A"},
{"datetime":"2018/8/10","value":7,"type":"B"},
{"datetime":"2018/8/10","value":73,"type":"A"}
];
let res = arr.reduce((acc, {datetime, value, type: type}) =>
{
acc[datetime] = acc[datetime] || {};
Object.assign(acc[datetime], {datetime, [`value-${type}`]: value});
return acc;
}, {});
console.log(Object.values(res));
console.log(Object.values(res)[0]["value-B"]);
You could do this:
<html>
<head>
<meta charset="UTF-8"></meta>
<script type="text/javascript">
const arr = [{"datetime":"2018/8/5","value":85,"type":"A"},
{"datetime":"2018/8/10","value":7,"type":"B"},
{"datetime":"2018/8/10","value":73,"type":"A"}];
var new_arr = group_items(arr)
console.log(new_arr)
function group_items(arr)
{
var ret_arr = []
for(var x=0;x<arr.length;x++)
{
var cur_date = arr[x].datetime
var pos = lookup_date(cur_date, ret_arr)
var obj = {}
obj.datetime = cur_date
if(pos != false)
{
//add to existing item
if(arr[x].type == 'A')
{
ret_arr[pos].valueA = arr[x].value
}
else if(arr[x].type == 'B')
{
ret_arr[pos].valueB = arr[x].value
}
}
else{
if(arr[x].type == 'A')
{
obj.valueA = arr[x].value
}
else if(arr[x].type == 'B')
{
obj.valueB = arr[x].value
}
ret_arr.push(obj)
}
}
return ret_arr
}
function lookup_date(date, arr)
{
/*
returns the position in arr of date
*/
var retval = false
for(var x=0;x<arr.length;x++)
{
if(arr[x].datetime == date)
{
retval = x
break
}
}
return retval
}
</script>
</head>
<body>
</body>
If you don't need the final array to include the datetimes in the same order as the original, then you can just make an object that maps datetimes to the corresponding values and then use Object.values to get the final array. This approach does not guarantee order, since objects are unordered data structures:
const arr = [
{"datetime":"2018/8/5","value":85,"type":"A"},
{"datetime":"2018/8/10","value":7,"type":"B"},
{"datetime":"2018/8/10","value":73,"type":"A"}
];
const values_by_date = { };
arr.forEach( ({ datetime, type, value }) =>
values_by_date[ datetime ] = {
datetime/*, valueA: undefined, valueB: undefined*/,
...values_by_date[ datetime ], [`value${type}`]: value
}
);
const result = Object.values( values_by_date );
console.log( result );
If you need the final array to include the datetimes in the same order as the original array and the original array is already sorted by datetime, you can do it in a single pass like this:
const arr = [
{"datetime":"2018/8/5","value":85,"type":"A"},
{"datetime":"2018/8/10","value":7,"type":"B"},
{"datetime":"2018/8/10","value":73,"type":"A"}
];
const result = arr.reduce( ({ result, datetime: prev }, { datetime, type, value }) => {
if ( datetime !== prev )
result.push( { datetime/*, valueA: undefined, valueB: undefined*/ } );
Object.assign( result[ result.length - 1 ], { [`value${type}`]: value } );
return { result, datetime };
}, { result: [] } ).result;
console.log( result );
Note: In either snippet you can uncomment /*, valueA: undefined, valueB: undefined*/ if you want the resulting objects to include properties for the missing values.

How to compare consecutive date/time items in array and filter based on a specific time

I have the following array of objects:
var transactions = [
[
{"id":1,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:33:00.000Z"},
{"id":2,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:33:50.000Z"},
{"id":3,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:34:30.000Z"},
{"id":4,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:36:00.000Z"}
],
[
{"id":5,"sourceAccount":"A","targetAccount":"C","amount":250,"category":"other","time":"2018-03-02T10:33:00.000Z"},
{"id":6,"sourceAccount":"A","targetAccount":"C","amount":250,"category":"other","time":"2018-03-02T10:33:05.000Z"}
]
]
I need to compare time property of each object consecutively, and keep only those properties which time difference between each consecutive transaction is less than 1 minute.
The array format should be stay same, this is what I did try, but no luck, didn't work. What's the problem?
var newArray = transactions.map(g => g.reduce((r, o, i, a) => {
if (!i || new Date(o.time).getTime() - new Date(a[i - 1].time).getTime() >= 60000) {
r.push([o]);
} else {
r[r.length - 1].push(o);
}
return r;
}, []));
The expected output is something like this :
var output = [
[
{"id":1,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:33:00.000Z"},
{"id":2,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:33:50.000Z"},
{"id":3,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:34:30.000Z"}
],
[
{"id":5,"sourceAccount":"A","targetAccount":"C","amount":250,"category":"other","time":"2018-03-02T10:33:00.000Z"},
{"id":6,"sourceAccount":"A","targetAccount":"C","amount":250,"category":"other","time":"2018-03-02T10:33:05.000Z"}
]
]
You can Array#map your source array, and in each iteration, Array#filter the desired elements by comparing the time of current element with the time of previous element.
var transactions = [[{"id":1,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:33:00.000Z"},{"id":2,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:33:50.000Z"},{"id":3,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:34:30.000Z"},{"id":4,"sourceAccount":"A","targetAccount":"B","amount":100,"category":"food","time":"2018-03-02T10:36:00.000Z"}],[{"id":5,"sourceAccount":"A","targetAccount":"C","amount":250,"category":"other","time":"2018-03-02T10:33:00.000Z"},{"id":6,"sourceAccount":"A","targetAccount":"C","amount":250,"category":"other","time":"2018-03-02T10:33:05.000Z"}]];
var result = transactions.map((tr, i) => {
return tr.filter((t, j) => {
if (transactions[i][j - 1]) {
var d1 = new Date(t.time);
var d2 = new Date(transactions[i][j - 1].time);
return (d1.getTime() - d2.getTime()) <= 60000;
}
return true;
});
});
console.log(result);

Find indexes of multiple minimum in an array in javascript

I want to find the indexes of the minimum of an array.
E.g.
var array = [1,2,3,4,1,7,8,9,1];
var min = Math.min.apply( Math, array )
var key = array.indexOf(min);
The problem is that Math.min.apply return only the first index
min = 1 and key = 0
What I want to do is to return
min = 1 and key = [0,4,8];
Is it possible to do it with Math.min.apply?
var array = [1,2,3,4,1,7,8,9,1];
var min = Math.min.apply( Math, array )
var arrayWithIndex = array.map(function(element, index) {
return element + "-" + index;
});
var minValues = arrayWithIndex.filter(function(element) {
return Number(element.split('-')[0]) === min;
});
var keys = minValues.map(function(element) {
return Number(element.split('-')[1]);
});
My algorithm is mapping the original array to include a new array that contains both the number and index. I'm then filtering that array for elements, where the number is equal to min, and then mapping again to find the index.
You can also chain these methods
return array.map(function(element, index) {
return element + "-" + index;
})
.filter(function(element) {
return Number(element.split('-')[0]) === min;
}).
.filter(function(element) {
return Number(element.split('-')[1]);
});
An Array.prototype.reduce should do the work.
var array = [1, 2, 3, 4, 1, 7, 8, 9, 1];
function getMinKeys(array) {
var min = Math.min.apply(Math, array);
return array.reduce(function (r, a, i) {
a === min && r.push(i);
return r;
}, []);
}
document.write('<pre>' + JSON.stringify(getMinKeys(array), 0, 4) + '</pre>');
var array = [1,2,3,4,1,7,8,9,1];
function findMinimumIndexes(array) {
var min = Math.min.apply(Math, array);
var indexes = [];
array.forEach(function(item, idx, arr) {
if (item === min) {
indexes.push(idx);
}
});
return indexes;
}
console.log(findMinimumIndexes(array));
The function declares the variable indexes and assigns to it an empty array which then it fills up using a forEach call with elements' indexes whose value matches min.
Notice that this only works for primitive values since it uses === to check for equivalence.
To make it work on arrays or objects that are 'deeply equal' to the one you are looking for, you would need a more rigorous implementation.

Counting array elements with specific date in javascript

I have an array of Date() objects in javascript and I want to count the number of events on each day.
Here is an example:
What I have is:
Array [ Date 2014-12-04T10:30:20.000Z, Date 2014-12-05T11:04:58.056Z, Date 2014-12-05T11:04:58.056Z, Date 2014-12-05T11:04:58.056Z ]
What I want is:
Array [{date: '2014-12-04', counts: 1}, {date: '2014-12-05', counts: 3}]
Thanks a lot!
Max
Basic answer:
var arr = [], // fill it with array with your data
results = {}, rarr = [], i, date;
for (i=0; i<arr.length; i++) {
// get the date
date = [arr[i].getFullYear(),arr[i].getMonth(),arr[i].getDate()].join("-");
results[date] = results[date] || 0;
results[date]++;
}
// you can always convert it into an array of objects, if you must
for (i in results) {
if (results.hasOwnProperty(i)) {
rarr.push({date:i,counts:results[i]});
}
}
These can be made much easier with lodash functions, and Array.forEach() in ES5
You much better off having a simple object with the keys as the date and the value as the count. I've added a simple pad function that prefixes a zero where the number is a single digit as per your output requirements.
function pad(n) {
return n.toString().length == 1 ? '0' + n : n;
}
function getCount(arr) {
var obj = {};
for (var i = 0, l = arr.length; i < l; i++) {
var thisDate = arr[i];
var day = pad(thisDate.getDate());
var month = pad(thisDate.getMonth() + 1);
var year = thisDate.getFullYear();
var key = [year, day, month].join('-');
obj[key] = obj[key] || 0;
obj[key]++;
}
return obj;
}
getCount(arr); // Object { 2014-04-12: 1, 2014-05-12: 3 }
DEMO
I came across the same issue and found this solution which uses Map()
`
calc = (obj) => {
const orders = []
const dates_map = new Map()
//iterate through all the objects inside the orders array
orders.forEach(order => {
// format and get the date
const date = new Date(order.created_at).toLocaleDateString('en-GB')
//check if the date key exists in the Map() and save it in a temp
const temp = dates_map.get(date) || false
// if it does not exist
if (temp) {
// clone the object
const previous = {...temp}
// increase counter
previous.count += 1
dates_map.set(date, previous)
}else{
//create new object to avoid overwriting
const result = {}
result.count = 1
dates_map.set(date, result)
}
})
console.log(dates_map)
}
And this is the output
Output: Map(3) {
'08/05/2021' => { count: 2 },
'09/05/2021' => { count: 1 },
'11/05/2021' => { count: 2,}
}
`

Remove duplicate objects from array by a certain merge algorithm

Here's what I have in mind:
Given an array of objects:
[
{
"name": "Kirk",
"count": 1
},
{
"name": "Spock",
"count": 1
},
{
"name": "Kirk",
"count": 1
}
]
I am trying to get:
[
{
"name": "Kirk",
"count": 2
},
{
"name": "Spock",
"count": 1
}
]
I am wondering if there's already an algorithm, perhaps combining some higher order functions to achieve this. I could do this easily with loops, but I am looking for a way to solve it using higher order functions. If someone could point me to what I should use to achieve this, it would be great. Again, I'm looking for something as elegant as possible (two maps and a filter would not be a big improvement from loops).
This is my current solution and I'm looking for something better (and by better I mean more expressive):
function mergeDuplicates(input) {
var output = [];
var existingItem = null;
input.forEach(function (inputItem) {
existingItem = _.find(output, function (outputItem) {
return inputItem.name === outputItem.name;
});
existingItem ? existingItem.count += 1 : output.push({
name: inputItem.name,
count: 1
});
existingItem = null;
});
return output;
}
To make line #10 more clear: in the original array, count might be either non-existing or 1, hence I set it to 1.
I think the best way would be to hash each object if it does not already exist, and delete the ones that you found already hashed in your structure. This way, you'd be checking the existence of each object only 1 (depends on your hash scheme).
Just a function if you would like to use.
function merge(arr) {
for(var o = {}, i; i=arr.shift(); o[i.name] = i.count + (o[i.name] || 0));
for(i in o) arr.push({name:i, count:o[i]});
}
Calling :
var myArray = [{"name":"Kirk","count":1},
{"name":"Spock","count":1},
{"name":"Kirk","count":1}];
merge(myArray);
// myArray is now : [{"name":"Kirk","count":2}, {"name":"Spock","count":1}]
You can use reduce which is actually a fold.
a.reduce(function(p, c) {
var n = c.name;
if (p[n])
p[n].count++;
else
p[n] = c;
return p;
}, {})
will give you a object with "Kirk" and "Spock" as the key, what you want as values.
I know this is an old question, but I couldn't resist trying to solve it. Instead of two maps and a filter, we use a sort and then a reduce. This was a fun one to sort out :-)
function mergeDuplicates(list, prop, cb){
return list.sort(function(a,b){
if(a[prop] < b[prop]){ return -1;}
if(a[prop] > b[prop]){return 1;}
return 0;
}).reduce(function(acc, item, index, array){
if(index > 0 && array[index-1][prop] === item[prop]){
cb(acc[acc.length-1], item);
return acc;
}else{
var newItem = Object.assign({}, item);
cb(newItem);
acc.push(newItem);
return acc;
}
}, []);
}
Then use it like this:
var newList = mergeDuplicates(list, "name", function(item, dup){
if(dup){
item.count++;
}else{
item.count = 1;
}
});
EDIT: Here's another take at it using reduce and using an object as a hashmap to store duplicates (similar to some of the other answers). This one uses ramdajs
const mergeDups = (cb, prop, list) => R.pipe(
R.reduce((acc, item) => (
R.has(item[prop], acc) ?
R.assoc(item[prop], cb(acc[item[prop]], item), acc) :
R.assoc(item[prop], cb(item), acc)
), {}),
R.values
)(list);
const cb = (i, d) => ( !R.isNil(d) ?
R.assoc('count', i.count + 1, i) :
R.assoc('count', 1, i) )
mergeDups(cb, 'name', items);
Here it is in the repl on Ramda's site
Try this better i'll useful resolve your issues
cleanup(arrayOfObj, 'name');
function cleanup(arr, prop) {
var new_arr = [];
var lookup = {};
for (var i in arr) {
lookup[arr[i][prop]] = arr[i];
}
for (i in lookup) {
new_arr.push(lookup[i]);
}
return new_arr;
}
Yet another version using reduce function:
var items =
[
{
"name": "Kirk",
"count": 1
},
{
"name": "Spock",
"count": 1
},
{
"name": "Kirk",
"count": 1
}
];
var filtered = items.reduce(function(prev, current,index){
if(!(current.name in prev.keys)) {
prev.keys[current.name] = index;
prev.result.push(current);
}
else{
prev.result[prev.keys[current.name]].count += current.count;
}
return prev;
},{result: [], keys: []}).result;
document.getElementById("output").innerHTML = JSON.stringify(filtered,null,2);
<pre id='output' />

Categories