3

The Problem

I'm using nvd3 to render some date-specific data. However, if there was no data for a particular date, no record exists for it, and nvd3 does not render that data point, instead interpolating between the data points that do exist. I therefore need to generate the missing dates so that nvd3 displays dates which had no data associated with them.

What I want to do

I need to generate an array that looks like this:

[ {year: 2015, month: 1, day: 1},
  {year: 2015, month: 1, day: 2},
  ...
]

The function generating this array will take a starting date and an ending date, and generate all days between those two dates.

I will then merge it with my data array. That way dates that have data will have data, and dates that don't have data will still show up on the graph, without skipping over any missing dates.

Is there an easy way to do this with the Date object? I.e. when generating this array I need to know which months have 30 or 31 days, that February is special, leap years, etc, etc... I could brute-force it by specifying the number of days for each month, but I'd like to know if there's any easier way to do this.

sg.cc
  • 1,726
  • 19
  • 41

6 Answers6

3

Try this:

function getDateArray(startDate, days){
   return Array(days)
            .fill()
            .map(function(e,idx) { 
                var d = new Date(startDate); 
                d.setDate(d.getDate() + idx); 
                return {
                    year: d.getFullYear(), 
                    month: d.getMonth() + 1, 
                    day: d.getDate() }; 
                }
             );
}
Malk
  • 11,855
  • 4
  • 33
  • 32
  • This looks promising and cleaner than the solution I'm working on based on @zero298's answer. How do I specify an end date for this function? EDIT: Oh, do I have to put in the number of days I want to fill? Got it, thank you for the neat function. – sg.cc Feb 01 '16 at 23:26
  • 1
    I like this, I would just note that I don't think `Array.fill()` is supported in IE [according to MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill#Browser_compatibility). – zero298 Feb 01 '16 at 23:29
  • It depends on what kind of data you are working with. If you have 2 date fields you can use this: http://stackoverflow.com/questions/542938/how-do-i-get-the-number-of-days-between-two-dates-in-javascript – Malk Feb 01 '16 at 23:29
  • Do you realise that `new Date(startDate)` may return a date 1 day before the start depending on the host system offset? The string "2016-02-01" will be treated as UTC, so if the host is say UTC-0500 then the result will for the first day will be `{year:2016, month:01, day:31}`. – RobG Feb 02 '16 at 06:10
  • 1
    This function is very wasteful of resources, it creates a new base date object on every iteration, also *fill* seems superfluous—why build an array of "undefined" just so *map* can be used? A plain loop is about the same amount of code and [*much faster*](http://jsperf.com/date-object-vs-string/3) (and works in all browsers, not just modern ones). – RobG Feb 02 '16 at 06:30
  • You are right this is not an efficient function, and there is a lot of room for optimization. At the very least it could reuse 1 date object and increment by one each loop. I wanted to address the problem with a single chained command, and the question specifically asked how the Date object can be used for the math. If you want to set all that up yourself as your answer does (+1) then you will get a more efficient function but miss that point. – Malk Feb 02 '16 at 17:29
2

You don't neccessarily need to know the number of days in a particular month.

var date = new Date(2016, 0, 31);
console.log(date); // Sun Jan 31 2016 00:00:00 GMT-0600 (Central Standard Time)
date.setDate(date.getDate() + 1);
console.log(date); // Mon Feb 01 2016 00:00:00 GMT-0600 (Central Standard Time)

You could iterate a Date until you want to get to the end point.

See this question: Incrementing a date in JavaScript


Edit

I felt bad that I didn't include an implementation, so here is my way, which is slightly similar to Malk's answer except it uses String.repeat() instead of the Array constructor which keeps jslint from complaining and returns a Date object instead of an Object literal. String.repeat() isn't supported by IE either.

/*jslint node:true*/

"use strict";

/*
 * Function to fill a series of dates
 * @param {Date} start The staring date
 * @param {Number} n The number of days to fill out
 * @returns {Date[]} The Date objects of the series
 */
function fillDateSeries(start, n) {
    return "#".repeat(n).split("").map(function (e, i) {
        var date = new Date();
        date.setDate(start.getDate() + i);
        return date;
    });
}

// Usage
console.log(fillDateSeries(new Date(), 5));

Prints:

[ Mon Feb 01 2016 18:24:29 GMT-0600 (Central Standard Time),
  Tue Feb 02 2016 18:24:29 GMT-0600 (Central Standard Time),
  Wed Feb 03 2016 18:24:29 GMT-0600 (Central Standard Time),
  Thu Feb 04 2016 18:24:29 GMT-0600 (Central Standard Time),
  Fri Feb 05 2016 18:24:29 GMT-0600 (Central Standard Time) ]
Community
  • 1
  • 1
zero298
  • 25,467
  • 10
  • 75
  • 100
  • Thank you for including the edit. Though ideologically speaking, I do like your barer initial answer -- it gave me enough to start working, without feeding the ready-made answer to me. :) – sg.cc Feb 02 '16 at 01:11
2

Just as an alternative, you can do this purely with numbers and strings, there's no need for Date objects. It should be more efficient, but of course only testing will tell.

// Start is a date string in y-m-d format     
function genDates(start, count) {
  var dates = [];
  var d = start.split('-').map(Number);
  var monthLength = [,31,28,31,30,31,30,31,31,30,31,30,31];

  // Return true if year is a leap year
  function isLeap(year) {return !(year % 4) && !!(year % 100) || !(year % 400)}

  // Add one day to date array [year,month,day]
  function addDay(d){
    ++d[2];

    // Deal with possible leap year first
    if (isLeap(d[0]) && d[1] == 2 && d[2] <= 29) return d;

    // If day is too big, increment month and reset day
 if (d[2] > monthLength[d[1]]) {
   d[2] = 1;
   ++d[1];
 }

    // If month is too big, increment year and reset month
 if (d[1] > 12) {
   d[1] = 1;
   ++d[0];
 }
 return d;
  }
  while (count--) {
    dates.push({year:d[0], month:d[1], day:d[2]});
 addDay(d);
  }
  return dates;
}     

document.write(JSON.stringify(genDates('2000-02-27', 5)));
RobG
  • 142,382
  • 31
  • 172
  • 209
  • Very interesting, thank you! I used the accepted answer for now, but I feel a refactor coming for the component I needed this for (it's quite slow right now), and I'm going to refer back to this answer when I get around to it. – sg.cc Feb 02 '16 at 01:09
  • 1
    @sg.cc—you may be interested in the results of [*Date object vs string*](http://jsperf.com/date-object-vs-string/3). – RobG Feb 02 '16 at 06:29
1

It isn't hard iterating over dates to create the range array yourself. You can easily use date.setDate(date.getDate() +1), as zero298 suggested.

I've created a simple example, I think it's a pretty straight forward solution.

function date_range(from, to) {
    if (to instanceof Date === false)
        to = new Date();
    if (from instanceof Date === false || from > to)
        from = to;

    var results = [];
    while (from.getTime() !== to.getTime()) {
        results.push(date_to_object(from));
        from.setDate(from.getDate() +1);
    }
    results.push(date_to_object(to));
    return results;
}

function date_to_object(date) {
    return {
        year: date.getFullYear(),
        month: date.getMonth() +1,
        day: date.getDate()
    };
}

Code example

iMoses
  • 4,338
  • 1
  • 24
  • 39
0

You could try something like this:

function dateIntervalFormatter(fromDate, toDate){
    var formatedInterval = [];
    while(fromDate<=toDate){
      formatedInterval.push({
        year: fromDate.getFullYear(),
        month: fromDate.getMonth(),
        day: fromDate.getDate()
      });
      fromDate.setDate(fromDate.getDate()+1);
    };
  return formatedInterval;
}
TomasVeras
  • 151
  • 1
  • 5
  • A condition here is that *fromDate* and *toDate* have the same time component, which is unlikely if, say, *fromDate* is created using `new Date()`. It also modifies the original *fromDate*, which might have unexpected results for other code. – RobG Feb 02 '16 at 06:52
-1

You could try something like this using a template.

And pushing that to an array with a for loop until then end date is reached. incrementing the day every loop.

Duck
  • 1
  • 1