1

I’m working on a website that teachers are entering their availability based on their local time zone David from California is available Monday and Tuesday at 4 PM PST for example. I want to show that availability to everyone else internationally in their local format John from New York can see David is available at 7 PM EST Is there standard way of doing this without storing local time zone in the db? I was thinking just pick a random date (or even now) and stick the hour/minute to it, save it in UTC and to display it just ignore the date part. does that sound reasonable or there is a better way?

nightograph
  • 2,179
  • 5
  • 31
  • 49
  • Do you want to show time in the user's local timezone, in that of the person they're looking at, or both? And yes, you need to store each user's timezone data, preferably using IANA representative locations, e.g. America/Los_Angeles for California/PST and America/New_York for EST. That will also accommodate daylight saving so that 4 pm local time is appropriately converted to other local times. Look at [*Calculate Timezone offset only for one particular timezone*](https://stackoverflow.com/questions/61361914/calculate-timezone-offset-only-for-one-particular-timezone). – RobG May 01 '20 at 04:18
  • right, daylight saving seems to be the problem! – nightograph May 01 '20 at 11:40

3 Answers3

2

When storing local times, the related timezone data should be stored as well. The most portable identifiers at the moment are IANA representative locations like 'America/New_York'. That way changes to standard and daylight saving offsets are accommodated so that given a particular date, you can get details for one person's time and show it as a date and time for another person's location, adjusting for their offset on that date.

The following shows an algorithm, it uses a rough function from here, but I would strongly suggest using a library like Luxon instead, I just wanted to keep this plain JS.

The following gets a time and location for one user, then displays it as an equivalent time in the location of another user. Hopefully it's something along the lines of what you want to do.

// Function from https://stackoverflow.com/a/61364310/257182
/* @param {string} isoString - ISO 8601 timestamp without timezone
**                             e.g. YYYY-MM-DDTHH:mm:ss or YYYY-MM-DD HH:mm:ss
** @param {string} loc - IANA representateive location
**                       e.g. Europe/Berlin
** @param {boolean} returnOffset - if true, return the offset instead of timestamp
** @returns {string} if returnOffset is true, offset is ±HH:mm[:ss] (seconds only if not zero)
**                   if returnOffset is false, equivalent ISO 8601 UTC timestamp
*/
let getUTCTime = (function() {

  let n = 'numeric';
  let formatterOpts = {year:n, month:n, day:n, hour:n, minute:n, second:n, hour12: false};
  function parse (isoString) {
    let [Y,M,D,H,m,s] = isoString.split(/[\DT\s]/);
    return new Date(Date.UTC(Y,M-1,D,H,m,s));
  }
  function toParts(date, formatter) {
    return formatter.formatToParts(date).reduce((acc, part) => {
      acc[part.type] = part.value;
      return acc;
    }, Object.create(null));
  }

  return function (isoString, loc, returnOffset = false) {
 
    formatterOpts.timeZone = loc;
    let formatter = new Intl.DateTimeFormat('en', formatterOpts);
    let oDate = parse(isoString);
    let utcDate = new Date(oDate);
    let maxLoops = 3,
        p, diff;
    do {
      p = toParts(utcDate, formatter);
      diff = new Date(Date.UTC(p.year, p.month-1, p.day, p.hour, p.minute, p.second)) - oDate;
      if (diff) {
        utcDate.setTime(utcDate.getTime() - diff);
      }
    } while (diff && maxLoops--)
    let dDiff = null;
    if (maxLoops < 0) {
      p = toParts(utcDate, formatter);
      dDiff = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - utcDate;
      let msg = isoString + ' does not exist at ' + loc + ' due to ' +
                'daylight saving change-over, shifting into DST';
    }
    let oDiff = dDiff || oDate - utcDate;
    let sign = oDiff > 0? '+' : '-';
    oDiff = Math.abs(oDiff);
    let offH = oDiff / 3.6e6 | 0;
    let offM = (oDiff % 3.6e6) / 6e4 | 0;
    let offS = (oDiff % 6e4) / 1e3 | 0;

    let z = n=>(n<10?'0':'')+n;
    return returnOffset? `${sign}${z(offH)}:${z(offM)}${offS? ':' + z(offS) : ''}` :
                         utcDate.toISOString();
  }
})();
// Given a local timestmap in format YYYY-MM-DDTHH:mm:ss and
// loc as IANA representative location
// Return equivalent ISO 8061 UTC timestmap
function getUTCString(timestamp, loc) {
  return getUTCTime(timestamp, loc);
}
// Given a local timestmap in format YYYY-MM-DDTHH:mm:ss and
// loc as IANA representative location
// Return offset at loc as ±HH:mm[:ss]
//  - seconds only included if not zero (typically pre-1900)
function getUTCOffset(timestamp, loc) {
  return getUTCTime(timestamp, loc, true);
}

/* @param {string} person - name of person
** @param {string} date - date to get times in YYYY-MM-DD format
** @param {string} loc - IANA rep. loc. e.g. America/New_York
** @returns {string} timestamp for loc
*/
function showTimes(person, date, loc) {
  // Get loc and time for person
  let sourceLoc  = data[person].loc; 
  let sourceTime = data[person].time;
  // Get UTC date for time
  let sourceDate = date + 'T' + sourceTime + ':00';
  let sourceOffset =  getUTCOffset(sourceDate, sourceLoc);
  let utcDate = new Date(sourceDate + sourceOffset);
  // Return local date for loc
  return utcDate.toLocaleString('en-CA',{timeZone: loc, timeZoneName:'long', hour12: false});
}

let data = {
  john: {
    loc: 'America/Los_Angeles', // IANA representative location
    time: '16:15'               // Must be in HH:mm format
  },
  sally: {
    loc: 'America/New_York',
    time: '08:30'
  }
}

let date = '2020-02-03';
let user1 = 'john';
let user2 = 'sally';

// Standard time
// Show John's time in Sally's location on Monday, 3 February 2020
console.log(
`${date} ${data[user1].time} for ${user1} in ${data[user1].loc } is\n\
${showTimes(user1,date, data[user2].loc)} for ${user2}`
 );
 
 // Daylight saving time
 // Show Sally's time in John's location on Friday, 26 June 2020
 date = '2020-06-26';
console.log(
`${date} ${data[user2].time} for ${user2} in ${data[user2].loc } is\n\
${showTimes(user2,date, data[user1].loc)} for ${user1}`
 );

Here's an example similar to the above using Luxon:

let DateTime  = luxon.DateTime;

let data = {
  john: {
    loc: 'America/Los_Angeles', // IANA representative location
    startTime: '16:15'          // Must be in HH:mm format
  },
  sally: {
    loc: 'America/New_York',
    startTime: '08:30'
  }
}

console.log('----- Standard time -----');
// What is the date and time at Sally's location when John starts on
// on Monday, 3 February 2020?
let targetDate = '2020-02-03';
let johnStartString = targetDate + 'T' + data.john.startTime;
let johnStartDate = DateTime.fromISO(johnStartString, {zone: data.john.loc});

// ISO string for John's startTime
console.log('When John starts at : ' + johnStartDate.toISO());

// Create a date for Sally's loc based on John's
let sallyDate = johnStartDate.setZone(data.sally.loc);
console.log('For Sally it\'s      : ' + sallyDate.toISO());

console.log('----- Daylight Saving time -----');
// What is the date and time at John's location when Sally starts on
// on Monday, 1 June 2020?
targetDate = '2020-06-01';
let sallyStartString = targetDate + 'T' + data.sally.startTime;
sallyStartDate = DateTime.fromISO(sallyStartString, {zone: data.sally.loc});

// ISO string for Sally's startTime
console.log('When Sally starts at: ' + sallyStartDate.toISO());

// Create a date for John's loc based on Sally's
let johnDate = sallyStartDate.setZone(data.john.loc);
console.log('For John it\'s       : ' + johnDate.toISO());
<script src="https://cdn.jsdelivr.net/npm/luxon@1.23.0/build/global/luxon.min.js"></script>
RobG
  • 142,382
  • 31
  • 172
  • 209
0

I would store:

  • initial time of the day: int
  • end time of the day: int
  • original timezone: string

then, showing that to users is a UI problem. you could calculate dynamically two dates (based on the stored times) in the original timezone and convert it to any target timezone on the fly.

an alternative is checking the time difference between original and target timezones (without calculating any date) and adding it to the initial/end times.. but I guess it's easier to go for the first option as the date classes have that kind of utils.

Luís Soares
  • 5,726
  • 4
  • 39
  • 66
0

Keeping track of start and end hours can result in weird timezone errors.

For example, if someone selects Monday 6pm-9pm in EST, that's actually Monday 11pm - Tuesday 2am in UTC. That means the time range stored in UTC is Start: 11pm and End: 2am, which requires lots of code to work around these different scenarios.

A better idea may be to keep track of the starting hour and the number of hours until the ending time (elapsed time).

thesilican
  • 581
  • 5
  • 17
  • actually just updated the question, the range is not that important, the important thing is not to keep the time zone in the db and work off of a universal time zone to keep the time , i guess + - or to the timezone offset could work? – nightograph Apr 30 '20 at 23:04