A better way than chaining _.groupBy? - javascript

When I do this I get an array which stores the data as grouped by the month and day of the date but not by the year (I am doing this to get maximum, minimum, and average values for each day there is data for)
The problem is that the array stores an array of 2-3 values for that day and month within the date which is the key value. Those 2-3 indices each have an array of length one that holds a reference to an object which has the actual data point (level) I need. The object contains three attributes, date, id (which is always null), and level which is a float.
I either need to find a way so those 2-3 indices hold the object directly, or find a way that _.each can access the level.
Any thoughts?
var groupedData = _.groupBy(data, "date");
var groupedLevels = _.groupBy(groupedData, function (points, date) {
var dateParsed = parseDate(date);
var month = dateParsed.getMonth();
var day = dateParsed.getDate();
var monthDay = month + "-" + day;
return monthDay;
});
_.each(groupedLevels, function (points, date) {
var levels = _.map(_.pluck(points, "level"), parseFloat);
minimum.push([ date, R.min(levels) ]);
maximum.push([ date, R.max(levels);
var averageLevel = R.sum(levels) / levels.length;
average.push([date, averageLevel]);
})
So the data, as is, which is the original input looks like this (a sample piece):
[ { date: "2009-01-01",
id: null,
level: "0.08",
},
// ...
]
Currently, groupedData is this:
{ "2009-01-01":
[ { date: "2009-01-01",
id: null,
level: "0.08"
}
],
// ...
}
groupedLevels looks like this, for example:
{ "0-1":
[ [ { date: "2009-01-01".
id: null,
level: "0.08"
}
],
// ...
],
// ...
}
I want to skip having all the arrays of length one and just have the object stored there.

I think you can fix the immediate issue by replacing this line:
var levels = _.map(_.pluck(points, "level"), parseFloat);
With this:
var levels = _.map(_.pluck(points[0], "level"), parseFloat);
...but I think the real problem might be that you're doing groupBy twice when you don't need to. This single groupBy ought to be equivalent, but without the extra nested array:
var groupedLevels = _.groupBy(data, function(item) {
var dateParsed = parseDate(item.date);
var month = dateParsed.getMonth();
var day = dateParsed.getDate();
return month + '-' + day;
});
With this, your each should work as expected.

Related

Javascript find all occurrences of name in array of objects and create new unique arrays

I'm trying to get all occurrences from a google calendar in Google Apps Script and create individual arrays for each event name.
Simplified version (example) of response array:
{summary=Name1, start={dateTime=2018-12-03T15:00:00+01:00}, end={dateTime=2018-12-03T23:00:00+01:00}},
{summary=Name2, start={dateTime=2018-12-04T11:00:00+01:00}, end={dateTime=2018-12-04T23:00:00+01:00}},
{summary=Name1, start={dateTime=2018-12-05T07:00:00+01:00}, end={dateTime=2018-12-05T15:00:00+01:00}}
What I can't figure out is how to filter/split (whatever you'd call it) this up so I'd end up with a new array with the following format:
EDIT
{Name1=[[2018-12-03, 15, 23, 8.0], [2018-12-04, 11, 23, 12.0], [2018-12-05, 7, 15, 8.0], [2018-12-06, 15, 23, 8.0]], Name2=[[2018-12-11, 7, 16, 9.0], [2018-12-12, 7, 16, 9.0]]}
The idea is to then iterate through this new array and do a foreach to get a list of all the dates for individual names.
This is as far as I've gotten
function hoursTally() {
var calendarId = [CALENDAR_ID];
var startDay = 24;
var endDay = 23;
var month = parseFloat(new Date().getMonth()).toFixed(0);
var year = new Date().getYear();
var startDate = new Date( year, month-1, startDay );
var endDate = new Date( year, month, endDay );
var optionalArgs = {
timeMin: startDate.toISOString(),
timeMax: endDate.toISOString(),
showDeleted: false,
singleEvents: true,
orderBy: 'startTime'
};
var response = Calendar.Events.list(calendarId, optionalArgs);
var events = response.items;
events.forEach(function(e){
Logger.log(e);
var name = e.summary;
var eventDateStart = new Date(e.start.dateTime);
var eventDateEnd = new Date(e.end.dateTime);
var startTime = parseFloat(eventDateStart.getHours()).toFixed(0);
var endTime = parseFloat(eventDateEnd.getHours()).toFixed(0);
var theDate = Utilities.formatDate(eventDateStart, 'GMT+1', 'yyyy-MM-dd');
var total = endTime-startTime;
});
}
Every attempt of looping the events and getting the aforementioned format has failed :(
Since your stated goal is to collect information from each similarly named event into a single summary object, your output data structure should not be an Array of objects - it should just be an associative object. An Array would be appropriate if you wanted equivalent objects to remain distinct, but you state this is not the case.
The solution is then to reduce the returned events into an object, where the key of the data is the name, and the value is an array of instance information. The instance information is itself an array (in your example, [2018-12-03, 15, 23, 8])
A simple example which you can adapt to your use case:
const summary = items.reduce(function (obj, item) {
var name = item.summary;
// If we haven't seen this name before, initialize an empty array
if (obj[name] === undefined) { obj[name] = []; }
...
// Create an array with the info we want to store
var info = [
eventStartDate,
...
];
// Store this info array with all the others for the same name
obj[name].push(info);
return obj;
}, {});
You then use this summary by iterating the object:
for (var name in summary) {
summary[name].forEach(function (info) {
...
});
...
}
Array#reduce

Traversing an array of objects and adding to other array based on condition

I have an array containing the following objects.
var notTotal = [{"Year":2012,"Value":800579},
{"Year":2012,"Value":654090},
{"Year":2012,"Value":758092},
{"Year":2013,"Value":343928},...More objects.. ]
What im trying to do is traverse this array of objects where only one Year exists instead of multiple and to add up the Values for that year. Using the example above..
var total = [{"Year":2012,"Value":2556689},
//Total of first three 2012 assuming there isnt anymore 2012 in the array
{"Year":2013,"Value":343928},...]
I have tried something like the following:
for(var i = 0; i < notTotal.length; i++) {
if (total.includes(notTotal[i].Year || notTotal[i])) {
//Add the value of the year in notTotal array to the same year in total array
} else {
total.push(notTotal[i]); //add year and value if it does not exist to the total array
}
}
Apologies if this is a duplicate. It seems like a pretty specific question.
Thanks!
An easy solution would be to create an object, holding totals by year.
var totalByYear = {};
You can then loop over the array using notTotal.forEach or a for loop as you've shown, adding to the value of the relevant year inside the totalByYear object.
notTotal.forEach(function(ele) { totalByYear[ele.Year] += ele.Value });
This yields an object with year keys and total values, e.g. using your example:
{'2012': 2556689, '2013': 343928 /* other years */}
The desired format (for D3) can then be built from the totalByYear object (and the totals by year printed):
var totals = [];
for (year in totalByYear) {
console.log('Total for year ' + year + ' is ' + totalByYear[year]);
//Build the correctly formatted array
totals.push({ Year: year, Value: totalByYear[year]});
}
//Prints:
//Total for year 2012 is 2556689
//etc.etc.
The totals array will then have the desired format.
Great question! To explain what's happening in your if (total.includes(notTotal[i].Year || notTotal[i])) is that you are looking through your total array for either just the year, or just an existing notTotal[i] exactly as it is. So your loop is trying to find a value that's exactly 2012 or exactly "Year":2012,"Value":2556689. Ergo, if your total array looked like this:
[{"Year":2012, "Value": 12345}]
your for loop would not find it even though there is an object with 2012 as its year. As for how to fix this, take a look at this previous question!
How to determine if Javascript array contains an object with an attribute that equals a given value?
Hopefully that helps :)
var notTotal = [{"Year":2012,"Value":800579},
{"Year":2012,"Value":654090},
{"Year":2012,"Value":758092},
{"Year":2013,"Value":343928}]
var totalObj = notTotal.reduce((sum, obj) => {
sum[obj.Year] = sum[obj.Year] + obj.Value || obj.Value;
return sum;
}, {});
// convert total to the format you need;
var total = Object.entries(totalObj).map(([Year, Value]) => ({Year, Value}))
console.log(total);
one more solution :
function yearlyValueFilter(array){
var yearlyValue = {}
array.forEach( (obj) => { //litterate on the input
var year = obj.Year
var value = obj.Value
if((year in yearlyValue)){ //if the array with not duplicated years conatins the litteration year just plus that value
yearlyValue[year] += value
}else{ //if not conatins, it gets as a basic value
yearlyValue[year] = value
}
})
return yearlyValue
}
You could built a hash table:
var hash = {},total=[];
for(const {Year,Value} of notTotal){
if(hash[Year]){
hash[Year].Value+=Value;
}else{
total.push(hash[Year]={Year,Value});
}
}
In action
Note: object properties are normally not capitalized...
You could use a hash table and check if the year does not exist, then generate a new result set. Then update the total count.
var values = [{ Year: 2012, Value: 800579 }, { Year: 2012, Value: 654090 }, { Year: 2012, Value: 758092 }, { Year: 2013, Value: 343928 }],
hash = Object.create(null),
totals = [];
values.forEach(function (o) {
hash[o.Year] || totals.push(hash[o.Year] = { Year: o.Year, Value: 0 });
hash[o.Year].Value += o.Value;
});
console.log(totals);

AngularJS: Sort by date in ng-repeat where key is date

I'm using LoDash to create statistics out of some records in a IndexedDB.
$scope.refreshStats = function() {
var dataByMonth = _.groupBy($scope.stats, function(record) {
return moment(record.date).format('MMMM YYYY');
});
dataByMonth = _.mapValues(dataByMonth, function(month) {
var obj = {};
obj.Cars = _.groupBy(month, 'car');
obj.Drivers = _.groupBy(month, 'driver');
_.each(obj, function(groupsValue, groupKey) {
obj[groupKey] = _.mapValues(groupsValue, function(groupValue) {
return _.reduce(groupValue, function(sum, trip) {
sum['trips']++;
sum['duration']+= moment.utc(trip.duration, 'HH:mm:ss');
sum['total'] = moment.utc(sum.duration). format('HH:mm:ss')
return sum;
}, {trips: 0, duration: 0, total:0})
});
})
return obj;
});
$scope.statistics = dataByMonth;
console.log($scope.statistics);
};
The result from my function is a collection of nested objects which key is always a month and a year:
Object {
July 2016: Object,
August 2016: Object,
September 2016: Object,
October 2016: Object
}
The problem is that when I display it in the frontend, the first ng-repeat grouped by months monthName will get ordered alphabetically (displaying August-July-September-October), and so far I didn't figure out how to order it by date.
Here's how the ng-repeat looks like:
<div ng-repeat="(monthName, monthValue) in statistics">
{{monthName}}
</div>
Is there any way to use orderBy:date when the date is they key of the object?
EDIT
The question is not repeated as my problem is the need to identify the Key as Date and then order by it. I haven't been able to solve this problem with the answers to the referred question.
Key order for object in JS can not be relied on, you'll have to convert your data in to a {date: , value: } object and sort it.
First step, conversion:
var step1 = _.map(_.pairs(dataByMonth), _.partial(_.zipObject, ['date', 'value'] ));
next you need to sort them:
var step2 = _.sortBy(step1, function(value){
return new Date(value.date);
});
step2 holds your sorted value
$scope.statistics = step2;
you can use it in your ng-repeat
<div ng-repeat="montStats in statistics">
{{monthStats.date}}
</div>
You can further optimize your code by:
Keeping the date as a real date and avoid the parsing by each sort iteration.
Chain the operations:
_(dataByMonth).pairs().map(
_.partial(_.zipObject, ['date', 'value'])).sortBy(function(value){
return new Date(value);
}).value();

JavaScript Reformatting JSON arrays

I am relatively new to the JSON notation, and have run into an issue when attempting to reformat. The current format contained in the database needs to be modified to a new format for importation into a project timeline graph.
Here is the current JSON format:
[
{
"name":"5-HP-N/A-N/A-F8",
"node":{
"name":"5",
"id":14
},
"timeline":{
"epc":null,
"m1":null,
"m2":null,
"m3":1554087600000,
"m4":1593572400000,
"m5":1625108400000,
"m6":1641006000000,
"m7":1656644400000
},
"fab":{
"name":"F8",
"id":1
}
},
However, in order to display in the graph, I need the following format:
{
'start': new Date(value from epc, or first non-null milestone),
'end': new Date(value from m1 or first non-null milestone), // end is optional
'content': 'label from start Date milestone'
'group' : ' value from name field above 5-HP'
'classname' : ' value from start Date milestone'
});
I am trying to write a function in order to accomplish this. Only epc, m1, or m2 may have the value of null, but the condition must be checked for to determine if an event range should be created and where it should end. What would be the best way to reformat this json data (preferrably from an external json sheet)?
Edit: Thanks for all the help I see how this is working now! I believe I didn't explain very well the first time though, I actually need multiple class items per "group".
The end result is that these will display inline on a timeline graph 'group' line, and so I am trying to figure out how to create multiple new objects per array element shown above.
So technically, the first one will have start date = m3, and end date = m4. Then, the next object would have the same group as the first (5-HP...), the start date = m4, end date = m5...etc. This would continue until m7 (always an end date but never a start date) is reached.
This is why the loop is not so simple, as many conditions to check.
see a working fiddle here: http://jsfiddle.net/K37Fa/
your input-data seems to be an array, so i build a loop around that. if not just see this fiddle where the input data is a simple object: http://jsfiddle.net/K37Fa/1/
var i
, result = [],
, current
, propCounter
, content = [ { "name":"5-HP-N/A-N/A-F8", "node":{ "name":"5", "id":14 }, "timeline":{ "epc":null, "m1":null, "m2":null, "m3":1554087600000, "m4":1593572400000, "m5":1625108400000, "m6":1641006000000, "m7":1656644400000 }, "fab":{ "name":"F8", "id":1 } }],
// get the milestone in a function
getMileStone = function(obj) {
propCounter = 1;
for(propCounter = 1; propCounter <= 7; propCounter++) {
// if m1, m2 and so on exists, return that value
if(obj.timeline["m" + propCounter]) {
return {key: "m" + propCounter, value: obj.timeline["m" + propCounter]};
}
}
};
// loop over content array (seems like you have an array of objects)
for(i=0;i< content.length;i++) {
current = content[i];
firstMileStone = getMileStone(current);
result.push({
'start': new Date(current.epc || firstMileStone.value),
'end': new Date(current.m1 || firstMileStone.value),
'content': firstMileStone.key,
'group' : current.name,
'classname' : firstMileStone.value
});
}
EDIT:
getMileStone is just a helper-function, so you could just call it with whatever you want. e.g. current[i+1]:
secondMileStone = getMileStone(current[i + 1])
you should just check, if you are not already at the last element of your array. if so current[i+1] is undefined, and the helperfunction should return undefined.
you could then use as fallback the firstMileStone:
secondMileStone = getMileStone(current[i + 1]) || firstMileStone;
see the updated fiddle (including check in the getMileStone-Helperfunction): http://jsfiddle.net/K37Fa/6/

Better data structure to handle this array

I have an array of data get from the server(ordered by date):
[ {date:"2012-8", name:"Tokyo"}, {date:"2012-3", name:"Beijing"}, {date:"2011-10", name:"New York"} ]
I'd like to :
get the name of the first element whose date is in a given year, for example, given 2012, I need Tokyo
get the year of a given name
change the date of a name
which data structure should I use to make this effective ?
because the array could be large, I prefer not to loop the array to find something
Since it appears that the data is probably already sorted by descending date you could use a binary search on that data to avoid performing a full linear scan.
To handle the unstated requirement that changing the date will then change the ordering, you would need to perform two searches, which as above could be binary searches. Having found the current index, and the index where it's supposed to be, you can use two calls to Array.splice() to move the element from one place in the array to another.
To handle searches by name, and assuming that each name is unique, you should create a secondary structure that maps from names to elements:
var map = {};
for (var i = 0, n = array.length; i < n; ++i) {
var name = array[i].name;
map[name] = array[i];
}
You can then use the map array to directly address requirements 2 and 3.
Because the map elements are actually just references to the array elements, changes to those elements will happen in both.
Assuming you are using unique cities, I would use the city names as a map key:
cities = {
Tokyo: {
date: "2012-8"
},
New York: {
date: "2011-10"
}
}
To search by date:
function byDate(date) {
for(el in cities) {
if(cities.hasOwnProperty(el) && cities[el].date === date)
return el;
}
}
Just for the record: without redesigning your date structure you could use sorting combined with the Array filter or map method:
function sortByDate(a,b){
return Number(a.date.replace(/[^\d]+/g,'')) >
Number(b.date.replace(/[^\d]+/g,''));
}
var example = [ {date:"2012-8", name:"Tokyo"},
{date:"2012-3", name:"Beijing"},
{date:"2011-10", name:"New York"} ]
.sort(sortByDate);
//first city with year 2012 (and the lowest month of that year)
var b = example.filter(function(a){return +(a.date.substr(0,4)) === 2012})[0];
b.name; //=> Beijing
//year of a given city
var city = 'Tokyo';
var c = example.filter(function(a){return a.city === city;})[0];
c.year; //=> 2012
//change year of 'New York', and resort data
var city = 'New York', date = '2010-10';
example = example.map(
function(a){if (a.name === city) {a.date = date;} return a;}
).sort(sortByDate);

Categories

Resources