Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eliminating Javascript daylight saving time gap, a cross-browser solution

After lots of hassle I finally found the actual problem. It's the gap induced by daylight saving and the fact that different browsers act differently if timezone is set on UTC+3:30 (I'm not sure of other timezones).

Here's a snippet to generate the problem (the problem is reproducible if your system's TZ is set to UTC+3:30):

function zeroPad(n) {
  n = n + '';
  return n.length >= 2 ? n : new Array(2 - n.length + 1).join('0') + n;
}

document.write("<table border='1' cellpadding='3'><tr><td>Input date</td><td>Parsed timestamp</td><td>Output date</td></tr>");

var m = 22 * 60;
for (var i=0; i<8; i++) {
  var input = "3/21/2015 " + zeroPad(Math.floor(m / 60)) + ":" + zeroPad(m % 60) + ":00";
  var d = new Date(input);
  var output = d.getFullYear()
    +'-'+zeroPad(d.getMonth()+1)
    +'-'+zeroPad(d.getDate())
    +' '+zeroPad(d.getHours())
    +':'+zeroPad(d.getMinutes())
    +':'+zeroPad(d.getSeconds());
  
  
  document.write("<tr><td>" + input + "</td><td>" + d.getTime() + "</td><td>" + output + "</td></tr>");
  m = m + 15;
}
m = 0;
for (var i=0; i<7; i++) {
  var input = "3/22/2015 " + zeroPad(Math.floor(m / 60)) + ":" + zeroPad(m % 60) + ":00";
  var d = new Date(input);
  var output = d.getFullYear()
    +'-'+zeroPad(d.getMonth()+1)
    +'-'+zeroPad(d.getDate())
    +' '+zeroPad(d.getHours())
    +':'+zeroPad(d.getMinutes())
    +':'+zeroPad(d.getSeconds());
  
  
  document.write("<tr><td>" + input + "</td><td>" + d.getTime() + "</td><td>" + output + "</td></tr>");
  m = m + 15;
}

document.write("</table>");

I've run it on Firefox and Chromium and here are what they say:

enter image description here

The parts within the red boxes are the range of times in which the gap happens. My problem is that components like calendars usually depend on date objects with time part set to "00:00:00" and they've got a loop generating new dates by adding a day worth of timestamp to the previous date. So once an object falls into 3/22/2015 00:00:00 it will be considered 3/22/2015 01:00:00 or 21/3/2015 23:00:00 (depending on the browser) and hence the generated dates will be invalid from that point of time forth!

The question is how to detect such date objects and how to treat them?

like image 369
Mehran Avatar asked Mar 11 '15 14:03

Mehran


3 Answers

Using moment.js will save you lots of headache, and is the easiest way to achieve cross-browser compatibility for this sort of thing.

var m = moment.utc("3/22/2015","M/D/YYYY")
var s = m.format("YYYY-MM-DD HH:mm:ss")

Using UTC for this is important since you don't want to be affected by the user's time zone. Otherwise, if your date fell into a DST transition, it could be adjusted to some other value. (You're not really intersted in UTC, you're just using it for stability.)


In response to this part of your updated question:

To simplify the question, I'm looking for a function like this:

function date_decomposition(d) {
    ...
}

console.log(date_decomposition(new Date("3/22/2015 00:00:00")));
=> [2015, 3, 22, 0, 0, 0]

While it's now clear what you are asking for, you must understand it is not possible to achieve your exact requirements, at least not in a cross-browser, cross-region, cross-timezone manner.

  • Each browser has it's own way of implementing the string-to-date parsing. When you use either the constructor new Date(string) or the Date.parse(string) method, you're invoking functionality that is implementation specific.

    There's a chart showing many of the formatting differences here.

  • Even if the implementation were consistent across all environments, you'd have regional formatting and time zone differences to contend with.

    • In the case of regional formatting issues, consider 01/02/2015. Some regions use mm/dd/yyyy ordering and will treat this as January 2nd, while other regions use dd/mm/yyyy ordering and will treat this as February 1st. (Also, some parts of the world use yyyy/mm/dd formatting.)

      Wikipedia has a list and map of where in the world different date formats are used.

    • In the case of time zones, consider that October 19th, 2014 at Midnight (00:00) in Brazil did not exist, and November 2nd, 2014 at Midnight (00:00) in Cuba existed twice.

      The same thing happens on other dates and times in different time zones. From the information you provided, I can deduce that you are in Iran time zone, which uses UTC+03:30 during standard time, and UTC+04:30 during daylight time. Indeed, March 22, 2105 at Midnight (00:00) did not exist in Iran.

      When you try to parse these invalid or ambiguous values, each browser has its own behavior, and there indeed differences between the browsers.

      • For invalid times, some browsers will jump forward an hour, while others will jump backwards an hour.

      • For ambiguous times, some browsers will assume you meant the first (daylight-time) instance, while others will assume you meant the second (standard-time) instance.

Now with all of that said, you can certainly take a Date object and deconstruct its parts, quite simply:

function date_decomposition(d) {
    return [d.getFullYear(), d.getMonth()+1, d.getDate(),
            d.getHours(), d.getMinutes(), d.getSeconds()];
}

But this will always be based on the local time zone where the code is running. You see, inside the Date object, there is just one value - a number representing the elapsed milliseconds since 1970-01-01T00:00:00Z (without leap seconds being considered). That number is UTC-based.

So, in recap, all of the issues you are having are related to the way the string was parsed into the Date object to begin with. No amount of focusing on the output functions will help you to resolve that in a completely safe manner. Whether you use a library or write your own code, you'll need to obtain that original string of data to get the result you are looking for. By the time it's in a Date object, you've lost the information you need to make this work.

By the way, you might consider watching my Pluralsight course, Date and Time Fundamentals, which covers much of this in even greater detail. Module 7 is entirely about JavaScript and these sorts of gotchas.

like image 146
Matt Johnson-Pint Avatar answered Nov 15 '22 00:11

Matt Johnson-Pint


JavaScript dates are parsed in the local time of the browser, unless you use the ISO date format "2015-03-22", which will be parsed as UTC. In your case, since you control the server, and you know the dates are actually UTC, you could leave the format the way it is and parse the date, then subtract the timezone offset value from the date to convert it to UTC. The following function should return exactly the results you requested in your last use case. You could obviously modify this to format the string in any way you see fit.:

function date_decomposition(dateString) {
    var local = new Date(dateString);
    var d = new Date(local.valueOf() - (local.getTimezoneOffset() * 60000));

    return [ d.getUTCFullYear(), 
            (d.getUTCMonth() +1 ),
            d.getUTCDate(),
            d.getUTCHours(), 
            d.getUTCMinutes(),
            d.getUTCSeconds() ];
}

The only gotcha about this approach is that if you change your date format on the server to use the ISO 8601 date format, it will still subtract the timezone offset and you'll end up with the wrong output.

To be 100% safe, you should format the date string using the ISO 8601 date format, like "2015-03-22" or "2015-03-22T00:00:00Z". Then you could delete the line that subtracts the timezone offset from the parsed date and it'll work correctly with any ISO date string:

function date_decomposition(dateString) {
    var d = new Date(dateString);

    return [ d.getUTCFullYear(), 
            (d.getUTCMonth() +1 ),
            d.getUTCDate(),
            d.getUTCHours(), 
            d.getUTCMinutes(),
            d.getUTCSeconds() ];
}
like image 31
Brian Shamblen Avatar answered Nov 15 '22 01:11

Brian Shamblen


Date Parse (1st answer before modified question)

This would parse input and isolate each value.

function dateDecomposition(d) {
    return [ d.getFullYear(),d.getMonth()+1,d.getDate(),
             d.getHours(),d.getMinutes(),d.getSeconds() ];
};
function dateUTCDecomposition(d) {
    return [ d.getUTCFullYear(),d.getUTCMonth()+1,d.getUTCDate(),
             d.getUTCHours(),d.getUTCMinutes(),d.getUTCSeconds() ];
};

function pdte() {
  var ndte=new Date(Date.parse(document.getElementById('datein').value));
  var ar=dateDecomposition(ndte);
  document.getElementById('yyyy').innerHTML=ar[0];
  document.getElementById('mm').innerHTML=ar[1];
  document.getElementById('dd').innerHTML=ar[2];
  document.getElementById('HH').innerHTML=ar[3];
  document.getElementById('MN').innerHTML=ar[4];
  document.getElementById('SS').innerHTML=ar[5];
  ar=dateUTCDecomposition(ndte);
  document.getElementById('Uyyyy').innerHTML=ar[0];
  document.getElementById('Umm').innerHTML=ar[1];
  document.getElementById('Udd').innerHTML=ar[2];
  document.getElementById('UHH').innerHTML=ar[3];
  document.getElementById('UMN').innerHTML=ar[4];
  document.getElementById('USS').innerHTML=ar[5];

  document.getElementById('test').innerHTML='';
  for (var i=1426896000000;i<1427068800000;i+=1800000) {
      ar=dateUTCDecomposition(new Date(i));
      var cmp=Date.parse(ar[0]+"/"+ar[1]+"/"+ar[2]+" "+ar[3]+":"+ar[4]+":"+ar[5]);
      var re=dateDecomposition(new Date(cmp));
      var fail=0;
      for (var l=0;l<6;l++) { if (ar[l]!==re[l]) fail=1 };
      document.getElementById('test').innerHTML+=fail+" -- "+ar.join(":")+'<br />';
  };
};

document.getElementById('datein').addEventListener('change',pdte,true);
window.onload=pdte
<input id="datein" value="2015/03/14 12:34:56" />
<table border=1>
  <tr><td>Locale</td>
    <td id="yyyy"></td>
    <td id="mm"></td>
    <td id="dd"></td>
    <td id="HH"></td>
    <td id="MN"></td>
    <td id="SS"></td>
  </tr><tr><td>UTC</td>
    <td id="Uyyyy"></td>
    <td id="Umm"></td>
    <td id="Udd"></td>
    <td id="UHH"></td>
    <td id="UMN"></td>
    <td id="USS"></td>
    </tr>
  </table>
  <div id="test"></div>

Testing rule #1

Searching for gaps from year 2000 to year 2015, by 1/2hour steps

function dateDecomposition(d) {
    return [ d.getFullYear(),d.getMonth()+1,d.getDate(),
             d.getHours(),d.getMinutes(),d.getSeconds() ];
};
function dateUTCDecomposition(d) {
    return [ d.getUTCFullYear(),d.getUTCMonth()+1,
             d.getUTCDate(), d.getUTCHours(),
             d.getUTCMinutes(),d.getUTCSeconds() ];
};

for (var i=946684800000;i<1420070400000;i+=1800000) {
      ar=dateUTCDecomposition(new Date(i));
      var cmp=Date.parse(
          ar[0]+"/"+ar[1]+"/"+ar[2]+" "+
          ar[3]+":"+ar[4]+":"+ar[5]);
      var re=dateDecomposition(new Date(cmp));
      var fail=0;
      for (var l=0;l<6;l++) { if (ar[l]!==re[l]) fail=1 };
    if (fail!==0) {
 document.getElementById('test').innerHTML+=fail+
          " -- "+new Date(i)+" -- "+ar.join(":")+'<br />';
    }
}
div#test {
  font-family: mono, monospace, terminal;
  font-size: .8em;
}
   <div id="test"> </div>

At all, this seem not to be a javascript bug, but I found this about ExtJs: Ext.Date.parse - problem when parsing dates near daylight time change.

Second testing rule

As this question was modified several times, there is a test rule working on last 10 years, by 15 minutes steps, for showing gaps AND overlaps:

function dateDecomposition(d) {
    return [ d.getFullYear(),d.getMonth()+1,d.getDate(),
             d.getHours(),d.getMinutes(),d.getSeconds() ];
};
function dateUTCDecomposition(d) {
    return [ d.getUTCFullYear(),d.getUTCMonth()+1,
             d.getUTCDate(), d.getUTCHours(),
             d.getUTCMinutes(),d.getUTCSeconds() ];
};

var end=(Date.now()/900000).toFixed(0)*900000;
var start=end-365250*86400*10;
for (var i=start;i<end;i+=900000) {
  ar=dateUTCDecomposition(new Date(i));
  var cmp=Date.parse(
      ar[0]+"/"+ar[1]+"/"+ar[2]+" "+
      ar[3]+":"+ar[4]+":"+ar[5]);
  var re=dateDecomposition(new Date(cmp));
  var fail=0;
  for (var l=0;l<6;l++) { if (ar[l]!==re[l]) fail++ };
  if (fail!==0) {
    document.getElementById('test').innerHTML+=
      fail+" -- "+new Date(i)+" -- "+ar.join(":")+'<br />';
  } else {
    ar=dateDecomposition(new Date(i));
    var cmp=Date.parse(
          ar[0]+"/"+ar[1]+"/"+ar[2]+" "+
          ar[3]+":"+ar[4]+":"+ar[5]);
    if (cmp != i) {
      document.getElementById('test').innerHTML+=
      fail+" -- "+new Date(i)+" -- "+ar.join(":")+'<br />';
    }
  }
}
div#test {
  font-family: mono, monospace, terminal;
  font-size: .8em;
}
   <div id="test"> </div>

You could test this by running your prefered browser under some differents timezones:

env TZ=Iran firefox http://stackoverflow.com/a/29048205/1765658
env TZ=Iran opera http://stackoverflow.com/a/29048205/1765658
env TZ=Iran chrome http://stackoverflow.com/a/29048205/1765658
env TZ=Iran chromium http://stackoverflow.com/a/29048205/1765658
env TZ=Europe/Berlin firefox http://stackoverflow.com/a/29048205/1765658
env TZ=Europe/Berlin opera http://stackoverflow.com/a/29048205/1765658
env TZ=Europe/Berlin chrome http://stackoverflow.com/a/29048205/1765658
env TZ=US/Central firefox http://stackoverflow.com/a/29048205/1765658
env TZ=US/Central opera http://stackoverflow.com/a/29048205/1765658
env TZ=US/Central chrome http://stackoverflow.com/a/29048205/1765658
env TZ=Asia/Gaza firefox http://stackoverflow.com/a/29048205/1765658
env TZ=Asia/Gaza opera http://stackoverflow.com/a/29048205/1765658
env TZ=Asia/Gaza chromium http://stackoverflow.com/a/29048205/1765658

I hope that you will be able to create your own testing rules for your ExtJS extensions, taking ideas from this last snippet.

Screenshoots

TZ=Iran Chromium

TZ=Iran chromium

TZ=Iran Iceweasel (firefox)

TZ=Iran iceweasel

TZ=US/Central Chromium

TZ=US/Central chromium

TZ=US/Central Iceweasel

TZ=US/Central iceweasel

TZ=Asia/Gaza Chromium

TZ=Asia/Gaza chromium

TZ=Asia/Gaza Iceweasel

TZ=Asia/Gaza iceweasel

like image 27
F. Hauri Avatar answered Nov 15 '22 00:11

F. Hauri