0

I am trying to build an app for a person who is teaching online and is based in Germany. I want to store the person's schedule in an array of appointment starting times. For example:

let schedule = [
  new Date(`${currentDate}T07:00:00Z`),
  new Date(`${currentDate}T08:00:00Z`),
  new Date(`${currentDate}T09:00:00Z`)
  ...
]

The problem is that in some countries, like Germany, there are Standart and Summer times. So, while in summer the starting times in Germany would be 9:00, 10:00 and 11:00, in winter they would shift one hour earlier. At the same time in countries, like Singapore, they are gonna stay the same: 15:00, 16:00, 17:00... What do I have to do make the starting times in Germany stable throughout the year and change elsewhere respectively:

if (summerTimeInGermany) {
  schedule = [
    new Date(`${currentDate}T07:00:00Z`), // 9:00 in Germany
    new Date(`${currentDate}T08:00:00Z`), // 10:00
    new Date(`${currentDate}T09:00:00Z`) // 11:00
    ...
  ]
} else {
  schedule = [
    new Date(`${currentDate}T08:00:00Z`), // 9:00
    new Date(`${currentDate}T09:00:00Z`), // 10:00
    new Date(`${currentDate}T10:00:00Z`) // 11:00
    ...
  ]
}

I need this schedule list so that I could compare it with the list of appointments per day, which I fetch from Google Calender, and exclude the ones that are already taken.

  • It's not really a duplicate of your question, but the answer to this question should help you solve your problem: https://stackoverflow.com/questions/11887934/how-to-check-if-the-dst-daylight-saving-time-is-in-effect-and-if-it-is-whats – icecub Apr 22 '20 at 09:51
  • I had checked this post before, but I couldn't figure out how to make use of it. The problem is that I need the same outcome for Germany, regardless of the offset. – Petar Karagenov Apr 22 '20 at 10:01
  • @icecub—that answer ([How to check if the DST (Daylight Saving Time) is in effect and if it is what's the offset?](https://stackoverflow.com/questions/11887934/how-to-check-if-the-dst-daylight-saving-time-is-in-effect-and-if-it-is-whats)) is for where the code is run on a local computer. As I understand the OP, the requirement is to generate a UTC string that represents a particular time in some other timezone, so independent of host settings. – RobG Apr 22 '20 at 22:16

1 Answers1

1

What I think you're trying to do is to generate a UTC timestamp based on a timestamp in another timezone and given the IANA representative location.

One way of doing that is to generate a Date for the location using the timestamp as UTC and see what the difference in hours and minutes are. That should be the offset. Then apply the offset to the original timestamp. That should then generate a timestamp with the required date and time for the target location when the timezone offset is applied.

The following function implements this algorithm using Intl.DateTimeFormat and IANA representative location. It's only been lightly tested so please test further. It does a loop for the adjustments to check that applying the offset doesn't move the date over a DST boundary and hence needs to readjust the offset.

By default it returns an ISO 8601 UTC timestamp for the equivalent time, if the optional returnOffset parameter is set to true, it returns the offset instead.

I've provided examples of standard and daylight saving timestamps both east and west of GMT (but not many). Please test thoroughly and modify as required.

/* @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};

  // Parse YYYY-MM-DDTHH:mm:ss as UTC (T can be space)
  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));
  }
  
  // Get date parts, use supplied formatter
  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) {
 
    // Update formatter options with loc so get target loc values
    formatterOpts.timeZone = loc;
    // Formatter
    let formatter = new Intl.DateTimeFormat('en', formatterOpts);
  
    // Parse input string as UTC
    let oDate = parse(isoString);
    // Date to adjust to wanted values
    let utcDate = new Date(oDate);

    // maxLoops limits do..while in dilemma zone, ensures sensible value
    let maxLoops = 3,
        p, diff;

    // Adjust utcDate so it generates required local date values
    // Adjustment may shift over DST boundary so may need 2nd adjustment
    // Limit number of loops (shouldn't be required but just in case...)
    do {
      // Get date parts in target timezone
      p = toParts(utcDate, formatter);
   
      // Get difference between local and adjusted values
      diff = new Date(Date.UTC(p.year, p.month-1, p.day, p.hour, p.minute, p.second)) - oDate;
   
      // If necessary, adjust utcDate so it generates required values when shifted
      if (diff) {
        utcDate.setTime(utcDate.getTime() - diff);
      }
    // Loop until generated values match original or maxLoops
    } while (diff && maxLoops--)
    
    // If maxLoops is -1, hit DST dilemma zone: time doesn't exist on that date
    // E.g. going into daylight saving at 02:00, then 02:30 doesn't exist
    // and loop will flip in/out of DST until stopped by maxLoops
    // So generate valid date and offset in DST period
    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';
      // console.log(msg);
      // throw new RangeError(msg);
    }

    // Convert diff between local and adjusted to get ±HH:mm offset
    // Use dilemma diff (dDiff) if has been set
    let oDiff = dDiff || oDate - utcDate;
    let sign = oDiff > 0? '+' : '-';
    oDiff = Math.abs(oDiff);
//    console.log(sign + new Date(oDiff).toISOString().substring(11,19).replace(/:00$/,''));
    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 offset (with offset seconds if not zero) or ISO 8601 UTC string
    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);
}

// Examples
window.onload = function() {
  let t = document.getElementById('t0');
  let params = ['Local time', 'UTC time', false];
  let showData = (localTime, loc, offset, timeUTC) => {
    let row = t.insertRow();
    [localTime, loc, timeUTC, null, null].forEach((s, i) => {
      cell = row.insertCell();
      cell.textContent = i == 0? localTime.replace('T',' '):
                         i == 1? loc:
                         i == 2? offset  :
                         i == 3? timeUTC :
                         new Date(timeUTC).toLocaleString('en-CA', {timeZone: loc, hour12: false}).replace(',','') ;
    });
    return new Date(timeUTC).toLocaleString('en-CA', {timeZone: loc, hour12: false}).replace(',','');
  };
  // Local time required   Location
  [['2020-04-22T09:00:00','Europe/Berlin'],   // DST offset +2
   ['2020-01-22T09:00:00','Europe/Berlin'],   // Std offset +1
   ['2020-04-22T09:00:00','America/Denver'],  // DST offset -6
   ['2020-01-22T09:00:00','America/Denver'],  // Std offset -7
   ['2020-04-22T09:00:00','US/Aleutian'],     // DST offset -9
   ['2020-01-22T09:00:00','US/Aleutian'],     // Std offset -10
   ['2020-01-22T09:00:00','Pacific/Honolulu'],// Std offset -11
   ['2020-01-22T19:00:00','Pacific/Honolulu'],// Std offset -11
   ['2020-04-22T09:00:00','Asia/Singapore'],  // Std offset +8
   ['2020-04-22T09:00:00','Pacific/Apia'],    // Std offset +13
   ['2020-01-22T09:00:00','Pacific/Apia'],    // DST offset +14
   ['2020-01-22T09:00:00','Asia/Yangon'],     // Std offset +6:30
   ['2020-04-22T09:00:00','Pacific/Chatham'], // Std offset +12:45
   ['2020-01-22T09:00:00','Pacific/Chatham'], // DST offset +13:45
   
   // Historic offsets pre 1900
   ['1857-01-01T00:00:00','Europe/Berlin'],   // Std offset +00:53:28
   ['1857-01-01T00:00:00','Australia/Sydney'],// Std offset +10:04:52
   ['1857-01-01T00:00:00','America/New_York'],// Std offset -04:56:02
   ['1857-01-01T00:00:00','America/Sao_Paulo'],//Std offset -03:06:28
   
   // DST boundary check out of DST (2 to 3 am counted as "out")
   ['2020-04-05T01:45:00','Australia/Sydney'],// DST offset +11:00
   ['2020-04-05T01:59:59','Australia/Sydney'],// DST offset +11:00
   ['2020-04-05T02:00:00','Australia/Sydney'],// Std offset +10:00
   ['2020-04-05T02:30:00','Australia/Sydney'],// Std offset +10:00
   ['2020-04-05T03:00:00','Australia/Sydney'],// Std offset +10:00
   ['2020-04-05T03:15:00','Australia/Sydney'],// Std offset +10:00
   
   // DST boundary check into DST (2 to 3 am counted as "in")
   ['2020-10-04T01:45:00','Australia/Sydney'],// Std offset +10:00
   ['2020-10-04T02:00:00','Australia/Sydney'],// DST offset +11:00
   ['2020-10-04T02:30:00','Australia/Sydney'],// DST offset +11:00
   ['2020-10-04T02:59:59','Australia/Sydney'],// DST offset +11:00
   ['2020-10-04T03:00:00','Australia/Sydney'],// DST offset +11:00
   ['2020-10-04T03:15:00','Australia/Sydney'] // DST offset +11:00
  ].forEach(([localTime,loc]) => {
    // Show results
    let timeUTC = getUTCString(localTime, loc);
    let offset  = getUTCOffset(localTime, loc);
    showData(localTime, loc, offset, timeUTC);
  });
};

// Example use
let timestamp = '2020-06-30 08:30:00';
let locBer = 'Europe/Berlin';
let locSng = 'Asia/Singapore';
// Get UTC timestamp and offset for Berlin
let utc = getUTCString(timestamp, locBer);
let off = getUTCOffset(timestamp, locBer);
// Show times and offset - offset is just for display, not used to
// generate Singapore timestamp
console.log('Berlin   : ' + timestamp + ' ' + off); // 
console.log('Singapore: ' + new Date(utc).toLocaleString(
  'en-CA',{hour12:false, timeZone:locSng, timeZoneName:'short'}
).replace(',',''));
table {
  border-collapse: collapse;
}
td {
  font-family: geneva, arial;
  font-size: 80%;
  padding: 5px;
  border: 1px solid #bbbbbb;
}
td:nth-child(2) {
  font-family: monospace;
  font-size: 100%;
}
<table id="t0">
  <tr><th>Local time<th>Place<th>Offset<th>UTC<th>Check
</table>
RobG
  • 142,382
  • 31
  • 172
  • 209
  • No, not exactly. What I am trying to achieve is to create an array, which is going to show the write starting times for an appointment, based on country. So, if I access is on the 24.04 in Germany, I get ['2020-04-24T09:00:00+02:00']. Then if I access it on 12.12 again in Germany, I would get ['2020-04-24T09:00:00+01:00'] but if I access it from SG, the first time I would get ['2020-04-24T15:00:00+08:00'], but the second: ['2020-04-24T16:00:00+08:00']. That is what I need to do. – Petar Karagenov Apr 22 '20 at 12:44
  • The question is how do I check what is the offset for a given date in Berin time, when the app is accessed from another country. – Petar Karagenov Apr 22 '20 at 13:00
  • @PetarKaragenov—I've added an optional *returnOffset* parameter that, if true, returns the offset instead of a timestamp. Call it as `getUTCTime(localTime, location[, true])`. You might want to Curry the function to two new functions: *getUTCString* and *getUTCOffset*. – RobG Apr 22 '20 at 23:39
  • @PetarKaragenov—for your Berlin/Singapore example, use `getUTCTime(string, 'Europe/Berlin')` to get the equivalent UTC string (and offset if you like for display). Then use `Intl.DateTimeFormat` using the generated UTC string and timeZone Asia/Singapore to get a timestamp for the equivalent time Singapore. If you want more help on how to do that, ask another question. :-) – RobG Apr 23 '20 at 00:19
  • @RobG Please tell me why you use `utcDate` in `dDiff = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - utcDate;`, but `utcDate` could have changed in the `do-while` loop? Is there no mistake here? – Alex Dec 22 '20 at 16:09
  • @Alex—the idea is to generate a UTC timestamp that parses to a Date that will match the input timestamp in the specified zone. Nearly always it will work first time, but if the offset has moved it over a DST boundary, a second attempt is used. If it has moved it to a time that doesn't exist, or exists twice (i.e. during the DST changeover time, often between 2 am and 3 am), then a 3rd attempt is used. But 99.999% of the time just one change is used. – RobG Dec 22 '20 at 22:36