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