简体   繁体   中英

Parse date in javascript using CET as default timezone

I have some legacy webservices that sometimes does not localize dates. Sometimes they do, so I have to support both cases.

They shall always use Italy's locale (UTC+1 for standard time, and UTC+2 for daylight saving time), but sometimes they return dates omitting the timezone at the end of date ISO strings.

For example, the italian new year should be 2018-01-01T00:00:00+0100 and they instead return only 2018-01-01T00:00:00

This causes bad behaviour in Javascript, especially when dealing with deadlines and clients that are in other timezones.

I'd like to be able to write a piece of code that parses a date string in ISO format, assuming italian localization if there is no timezone specified.

My code is almost ok (it doesn't parse milliseconds, but I can live with that), unfortunately it miserably fails when executed by browsers in timezones that doesn't have daylight saving time. What should I do? Am I missing something?

Thanks in advance

/**
 * Get the local timezone using standard time (no daylight saving time).
 */
Date.prototype.stdTimezoneOffset = function() {
  var jan = new Date(this.getFullYear(), 0, 1);
  var jul = new Date(this.getFullYear(), 6, 1);
  return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
}

/**
 * Check whether current time is daylight saving time.
 */
Date.prototype.isDst = function() {
  return this.getTimezoneOffset() < this.stdTimezoneOffset();
}

/**
 * Check whether daylight saving time is observed in current timezone.
 */
Date.prototype.isDstObserved = function() {
  var jan = new Date(this.getFullYear(), 0, 1);
  var jul = new Date(this.getFullYear(), 6, 1);
  return jan.getTimezoneOffset() != jul.getTimezoneOffset();
}

/**
 * Cross-browser parse of a date using CET as default timezone.
 */
Date.parseFromCET = function(str) {
  if (str == null) return null;

  // split the input string into an array of integers
  var a = str.split(/[^0-9]/)
    .map(function(s) {
      return parseInt(s, 10)
    });

  var b = new Date(
    a[0], // yyyy
    a[1] - 1 || 0, // MM
    a[2] || 1, // dd
    a[3] || 0, // hh
    a[4] || 0, // mm
    a[5] || 0 // ss
  );

  // if no timezone is present, force to CET
  if (str.lastIndexOf('-') <= 7 && str.indexOf('+') == -1 && str.indexOf('Z') == -1) {
    var CET_timezone_offset = b.isDst() ? '+0200' : '+0100'
    var isoString = a[0] + '-' + a[1] + '-' + a[2] + 'T' +
      a[3] + ':' + a[4] + ':' + a[5] + CET_timezone_offset;
    return Date.parseFromCET(isoString);
  }

  // remove local timezone offset to go from UTC time to local time
  b.setMinutes(b.getMinutes() - b.getTimezoneOffset());

  // add/remove forced timezone offset to calculate exact local time
  if (str.indexOf('+') > -1) {
    let hours = Math.floor(a[a.length - 1] / 100);
    let minutes = a[a.length - 1] % 100;
    b.setMinutes(b.getMinutes() - minutes);
    b.setHours(b.getHours() - hours);
  }
  if (str.lastIndexOf('-') > 7) {
    let hours = Math.floor(a[a.length - 1] / 100);
    let minutes = a[a.length - 1] % 100;
    b.setMinutes(b.getMinutes() + minutes);
    b.setHours(b.getHours() + hours);
  }
  return b;
}

The timestamp isn't consistent with the format in ECMA-262 as it's missing a colon in the offset. So parsing is implementation dependent and you may get an invalid date (eg in Safari).

The standard offset for Rome is +01:00. Daylight saving starts at 02:00 on the last Sunday in March (change to +02:00), and ends at 02:00 on the last Sunday in October (back to +01:00).

Note that there have been historical changes. Italy started using daylight saving in 1916, but there have been periods where it wasn't observed. It has been observed continuously since 1965, so as long as your dates are after that, you don't have to worry about past changes, only future ones.

The following is one way to go about it, it needs a lot more testing and should do validation of the input string and resulting Date object. You should probably also handle a timezone of "Z".

If you're going to do this a lot, a library that manages timezones will help greatly as it should also handle historical changes, past and future.

 /** Get a Date for the last Sunday of the month * @param {number|string} year - year for month * @param {number|string} month - calendar month number, 1 = Jan, 2 = Feb, etc. * @returns {Date} date for last Sunday for given year and month */ function getLastSunday(year, month) { // Date for last day of month var d = new Date(Date.UTC(year, month, 0)); // Adjust to previous Sunday d.setUTCDate(d.getUTCDate() - d.getUTCDay()); return d; } /** Return a date set to the UTC start of Italian DST * Starts at +0300 UTC on the last Sunday in March * * @param {number|string} year to get start of DST for * @returns {Date} set to start date and +0300Z */ function getDSTStart(year) { var d = getLastSunday(year, 3); d.setUTCHours(3); return d; } /** Return a date set to the UTC end of Italian DST * Ends at +0400 UTC on the last Sunday in October * * @param {number|string} year to get start of DST for * @returns {Date} set to start date and +0400Z */ function getDSTEnd(year) { var d = getLastSunday(year, 10); d.setUTCHours(4); return d; } /** Given a year, month, day and hour, return * true or false depending on whether DST is * being observed in Italy. * Use UTC to avoid local influences, assume standard time * * @param {number|string} year - subject year * @param {number|string} month - subject calendar month * @param {number|string} day - subject day * @param {number|string} hour - subject hour * @returns {number} offset for provided date and time */ function getItalianOffset(year, month, day, hour) { var d = new Date(Date.UTC(year, month-1, day, +hour + 1)); return d >= getDSTStart(year) && d < getDSTEnd(year)? '+0200' : '+0100'; } /** Convert offset in format +0000 to minutes * EMCAScript offset has opposite sign * * @param {string} offset - in format +/-HHmm * @reaturns {number} offset in minutes, + for west, - for east */ function offsetToMins(offset) { sign = /^\\+/.test(offset)? -1 : 1; tz = 60 * offset.slice(-4, -2) + (1 * offset.slice(-2)); tz *= sign; return tz; } /** Parse timestamp that may or may not have a timezone. * If no timezone, assume Italian timezone (+0100), adjusting for * daylight saving. * DST starts at +0300Z on last Sunday in March * DST ends at +0400Z on last Sunday in October * 2018-01-01T00:00:00+0100 or 2018-01-01T00:00:00 */ function parseItalianDate(s) { var b = s.split(/\\D/); var hasTz = /[+-]\\d{4}$/.test(s); var d, sign, tz; // If has offset, get from string // Otherwise, calculate offset if (hasTz) { tz = s.slice(-5); } else { tz = getItalianOffset(b[0], b[1], b[2], b[3]); } // Create Date using correct offset d = new Date(Date.UTC(b[0], b[1]-1, b[2], b[3], b[4], b[5])); d.setUTCMinutes(d.getUTCMinutes() + offsetToMins(tz)); return d; } // Tests ['2018-01-01T00:00:00', // New year '2018-03-25T01:00:00', // One hour before change over '2018-03-25T03:00:00', // One hour after change over '2018-03-25T01:00:00+0100',// As above but with timzone offset '2018-03-25T03:00:00+0200', '2018-10-27T03:00:00', // Still in DST '2018-10-28T03:00:00', // After DST ended '2018-10-28T03:00:00+0100' ].forEach(function(s) { console.log(`${s} => ${formatDate(parseItalianDate(s))}`); }); // Helper to format a date in Europe/Rome timezone function formatDate(d) { return d.toLocaleString('en-GB',{ year : 'numeric', month : 'short', day : '2-digit', weekday: 'short', hour : '2-digit', minute : '2-digit', second : '2-digit', hour12 : 'false', timeZone: 'Europe/Rome', timeZoneName: 'short' }); } 

As RobG mentioned in his great answer, you'll have an easier time if you use a library for this. There are several to choose from. A good choice for modern applications is Luxon , which will support all of the scenarios you described. Here are some examples:

let dt = DateTime.fromISO('2018-01-01T00:00:00+0100', { setZone: true, zone: 'Europe/Rome'});
console.log(dt.toISO());  //=> "2018-01-01T00:00:00.000+01:00"

let dt = DateTime.fromISO('2018-01-01T00:00:00', { setZone: true, zone: 'Europe/Rome'});
console.log(dt.toISO());  //=> "2018-01-01T00:00:00.000+01:00"

let dt = DateTime.fromISO('2018-07-01T00:00:00', { setZone: true, zone: 'Europe/Rome'});
console.log(dt.toISO());  //=> "2018-07-01T00:00:00.000+02:00"

A few notes about the above:

  • fromISO will accept the offset missing, or with Z , or with an offset with or without a colon.
  • setZone: true tells it to keep the offset from the string if one is provided.
  • zone: 'Europe/Rome' tells it to use the time zone for Italy when no offset is provided.
  • You can, of course, use the Luxon DateTime object ( dt here) in a variety of ways other than just emitting a string with toISO() .

There are other libraries that can do such things, such as moment-timezone , and js-joda , but I prefer Luxon lately.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM