@ui5/webcomponents-localization
Version:
Localization for UI5 Web Components
319 lines (304 loc) • 12 kB
JavaScript
/**
* Static collection of utility functions to handle time zone related conversions
*
* @author SAP SE
* @version 1.120.17
* @namespace
* @alias module:sap/base/i18n/date/TimezoneUtils
* @private
* @ui5-restricted sap.ui.core.Configuration, sap/base/i18n/format/DateFormat
*/ /*!
* OpenUI5
* (c) Copyright 2009-2024 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
var TimezoneUtils = {};
/**
* Cache for the (browser's) local IANA timezone ID
*
* @type {string}
*/
var sLocalTimezone = "";
/**
* Cache for valid time zones provided by <code>Intl.supportedValuesOf("timeZone")</code>
*
* @type {Array}
*/
var aSupportedTimezoneIDs;
/**
* Cache for Intl.DateTimeFormat instances
*/
var oIntlDateTimeFormatCache = {
_oCache: new Map(),
/**
* When cache limit is reached, it gets cleared
*/
_iCacheLimit: 10,
/**
* Creates or gets an instance of Intl.DateTimeFormat.
*
* @param {string} sTimezone IANA timezone ID
* @returns {Intl.DateTimeFormat} Intl.DateTimeFormat instance
*/
get: function (sTimezone) {
var cacheEntry = this._oCache.get(sTimezone);
if (cacheEntry) {
return cacheEntry;
}
var oOptions = {
hourCycle: "h23",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: sTimezone,
timeZoneName: 'short',
era: 'narrow',
weekday: "short"
};
var oInstance = new Intl.DateTimeFormat("en-US", oOptions);
// only store a limited number of entries in the cache
if (this._oCache.size === this._iCacheLimit) {
this._oCache = new Map();
}
this._oCache.set(sTimezone, oInstance);
return oInstance;
}
};
/**
* Uses the <code>Intl.supportedValuesOf('timeZone')</code> and <code>Intl.DateTimeFormat</code>
* API to check if the browser can handle the given IANA timezone ID.
* <code>Intl.supportedValuesOf('timeZone')</code> offers direct access to the list of supported
* time zones. It is not yet supported by all browsers but if it is supported and the given time
* zone is in the list it is faster than probing.
*
* <code>Intl.supportedValuesOf('timeZone')</code> does not return all IANA timezone IDs which
* the <code>Intl.DateTimeFormat</code> can handle, e.g. "Japan", "Etc/UTC".
*
* @param {string} sTimezone The IANA timezone ID which is checked, e.g <code>"Europe/Berlin"</code>
* @returns {boolean} Whether the time zone is a valid IANA timezone ID
* @private
* @ui5-restricted sap.ui.core.Configuration, sap.ui.core.format.DateFormat
*/
TimezoneUtils.isValidTimezone = function (sTimezone) {
if (!sTimezone) {
return false;
}
if (Intl.supportedValuesOf) {
try {
aSupportedTimezoneIDs = aSupportedTimezoneIDs || Intl.supportedValuesOf('timeZone');
if (aSupportedTimezoneIDs.includes(sTimezone)) {
return true;
}
// although not contained in the supportedValues it still can be valid, therefore continue
} catch (oError) {
// ignore error
aSupportedTimezoneIDs = [];
}
}
try {
oIntlDateTimeFormatCache.get(sTimezone);
return true;
} catch (oError) {
return false;
}
};
/**
* Converts a date to a specific time zone.
* The resulting date reflects the given time zone such that the "UTC" Date methods
* can be used, e.g. Date#getUTCHours() to display the hours in the given time zone.
*
* @example
* var oDate = new Date("2021-10-13T15:22:33Z"); // UTC
* // time zone difference UTC-4 (DST)
* TimezoneUtils.convertToTimezone(oDate, "America/New_York");
* // result is:
* // 2021-10-13 11:22:33 in America/New_York
* // same as new Date("2021-10-13T11:22:33Z"); // UTC
*
* @param {Date} oDate The date which should be converted.
* @param {string} sTargetTimezone The target IANA timezone ID, e.g <code>"Europe/Berlin"</code>
* @returns {Date} The new date in the target time zone.
* @private
* @ui5-restricted sap.ui.core.format.DateFormat, sap.ui.unified, sap.m
*/
TimezoneUtils.convertToTimezone = function (oDate, sTargetTimezone) {
var oFormatParts = this._getParts(oDate, sTargetTimezone);
return TimezoneUtils._getDateFromParts(oFormatParts);
};
/**
* Uses the <code>Intl.DateTimeFormat</code> API to convert a date to a specific time zone.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts
* @param {Date} oDate The date which should be converted.
* @param {string} sTargetTimezone The target IANA timezone ID, e.g <code>"Europe/Berlin"</code>
* @returns {{
* day: string,
* era: string,
* fractionalSecond: string,
* hour: string,
* minute: string,
* month: string,
* second: string,
* timeZoneName: string,
* weekday: string,
* year: string
* }} An object containing the date and time fields considering the target time zone.
* @private
*/
TimezoneUtils._getParts = function (oDate, sTargetTimezone) {
var sKey,
oPart,
oDateParts = Object.create(null),
oIntlDate = oIntlDateTimeFormatCache.get(sTargetTimezone),
// clone the date object before passing it to the Intl API, to ensure that no
// UniversalDate gets passed to it;
// no need to use UI5Date.getInstance as only the UTC timestamp is used
oParts = oIntlDate.formatToParts(new Date(oDate.getTime()));
for (sKey in oParts) {
oPart = oParts[sKey];
if (oPart.type !== "literal") {
oDateParts[oPart.type] = oPart.value;
}
}
return oDateParts;
};
/**
* Creates a Date from the provided date parts.
*
* @param {object} oParts Separated date and time fields as object, see {@link #_getParts}.
* @returns {Date} Returns the date object created from the provided parts.
* @private
*/
TimezoneUtils._getDateFromParts = function (oParts) {
// no need to use UI5Date.getInstance as only the UTC timestamp is used
var oDate = new Date(0),
iUTCYear = parseInt(oParts.year);
if (oParts.era === "B") {
// The JS Date uses astronomical year numbering which supports year zero and negative
// year numbers.
// The Intl.DateTimeFormat API uses eras (no year zero and no negative year numbers).
// years around zero overview:
// | Astronomical | In Era
// | 2 | 2 Anno Domini (era: "A")
// | 1 | 1 Anno Domini (era: "A")
// | 0 | 1 Before Christ (era: "B")
// | -1 | 2 Before Christ (era: "B")
// | -2 | 3 Before Christ (era: "B")
// For the conversion to the JS Date the parts returned by the Intl.DateTimeFormat API
// need to be adapted.
iUTCYear = iUTCYear * -1 + 1;
}
// Date.UTC cannot be used here to be able to support dates before the UNIX epoch
oDate.setUTCFullYear(iUTCYear, parseInt(oParts.month) - 1, parseInt(oParts.day));
oDate.setUTCHours(parseInt(oParts.hour), parseInt(oParts.minute), parseInt(oParts.second), parseInt(oParts.fractionalSecond || 0)); // some older browsers don't support fractionalSecond, e.g. Safari < 14.1 */
return oDate;
};
/**
* Gets the offset to UTC in seconds for a given date in the time zone specified.
*
* For non-unique points in time, the daylight saving time takes precedence over the standard
* time shortly after the switch back (e.g. clock gets set back 1 hour, duplicate hour).
*
* @example
* var oDate = new Date("2021-10-13T13:22:33Z");
* TimezoneUtils.calculateOffset(oDate, "America/New_York");
* // => +14400 seconds (4 * 60 * 60 seconds)
*
* TimezoneUtils.calculateOffset(oDate, "Europe/Berlin");
* // => -7200 seconds (-2 * 60 * 60 seconds)
*
* // daylight saving time (2018 Sun, 25 Mar, 02:00 CET → CEST +1 hour (DST start) UTC+2h)
* // the given date is taken as it is in the time zone
* TimezoneUtils.calculateOffset(new Date("2018-03-25T00:00:00Z"), "Europe/Berlin");
* // => -3600 seconds (-1 * 60 * 60 seconds), interpreted as: 2018-03-25 00:00:00 (CET)
*
* TimezoneUtils.calculateOffset(new Date("2018-03-25T03:00:00Z"), "Europe/Berlin");
* // => -7200 seconds (-2 * 60 * 60 seconds)
*
* var oHistoricalDate = new Date("1800-10-13T13:22:33Z");
* TimezoneUtils.calculateOffset(oHistoricalDate, "Europe/Berlin");
* // => -3208 seconds (-3208 seconds)
*
* @param {Date} oDate The date in the time zone used to calculate the offset to UTC.
* @param {string} sTimezoneSource The source IANA timezone ID, e.g <code>"Europe/Berlin"</code>
* @returns {number} The difference to UTC between the date in the time zone.
* @private
* @ui5-restricted sap.ui.core.format.DateFormat
*/
TimezoneUtils.calculateOffset = function (oDate, sTimezoneSource) {
const oDateInTimezone = TimezoneUtils.convertToTimezone(oDate, sTimezoneSource);
const iGivenTimestamp = oDate.getTime();
const iInitialOffset = iGivenTimestamp - oDateInTimezone.getTime();
// no need to use UI5Date.getInstance as only the UTC timestamp is used
const oFirstGuess = new Date(iGivenTimestamp + iInitialOffset);
const oFirstGuessInTimezone = TimezoneUtils.convertToTimezone(oFirstGuess, sTimezoneSource);
const iFirstGuessInTimezoneTimestamp = oFirstGuessInTimezone.getTime();
const iSecondOffset = oFirstGuess.getTime() - iFirstGuessInTimezoneTimestamp;
let iTimezoneOffset = iSecondOffset;
if (iInitialOffset !== iSecondOffset) {
const oSecondGuess = new Date(iGivenTimestamp + iSecondOffset);
const oSecondGuessInTimezone = TimezoneUtils.convertToTimezone(oSecondGuess, sTimezoneSource);
const iSecondGuessInTimezoneTimestamp = oSecondGuessInTimezone.getTime();
// if time is different, the given date/time does not exist in the target time zone (switch to Daylight
// Saving Time) -> take the offset for the greater date
if (iSecondGuessInTimezoneTimestamp !== iGivenTimestamp && iFirstGuessInTimezoneTimestamp > iSecondGuessInTimezoneTimestamp) {
iTimezoneOffset = iInitialOffset;
}
}
return iTimezoneOffset / 1000;
};
/**
* Map outdated IANA timezone IDs used in CLDR to correct and up-to-date IANA IDs as maintained in ABAP systems.
*
* @private
*/
TimezoneUtils.mCLDR2ABAPTimezones = {
"America/Buenos_Aires": "America/Argentina/Buenos_Aires",
"America/Catamarca": "America/Argentina/Catamarca",
"America/Cordoba": "America/Argentina/Cordoba",
"America/Jujuy": "America/Argentina/Jujuy",
"America/Mendoza": "America/Argentina/Mendoza",
"America/Indianapolis": "America/Indiana/Indianapolis",
"America/Louisville": "America/Kentucky/Louisville",
"Africa/Asmera": "Africa/Asmara",
"Asia/Katmandu": "Asia/Kathmandu",
"Asia/Calcutta": "Asia/Kolkata",
"Atlantic/Faeroe": "Atlantic/Faroe",
"Pacific/Ponape": "Pacific/Pohnpei",
"Asia/Rangoon": "Asia/Yangon",
"Pacific/Truk": "Pacific/Chuuk",
"America/Godthab": "America/Nuuk",
"Asia/Saigon": "Asia/Ho_Chi_Minh",
"America/Coral_Harbour": "America/Atikokan"
};
/**
* Retrieves the browser's local IANA timezone ID; if the browser's timezone ID is not the up-to-date IANA
* timezone ID, the corresponding IANA timezone ID is returned.
*
* @returns {string} The local IANA timezone ID of the browser as up-to-date IANA timezone ID,
* e.g. <code>"Europe/Berlin"</code> or <code>"Asia/Kolkata"</code>
*
* @private
* @ui5-restricted sap.ui.core.Configuration,sap.m.DateTimeField
*/
TimezoneUtils.getLocalTimezone = function () {
if (sLocalTimezone === "") {
// timezone may be undefined, only value "" means empty cache
sLocalTimezone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
sLocalTimezone = TimezoneUtils.mCLDR2ABAPTimezones[sLocalTimezone] || sLocalTimezone;
}
return sLocalTimezone;
};
/**
* Clears the cache for the browser's local IANA timezone ID.
*
* @private
*/
TimezoneUtils._clearLocalTimezoneCache = function () {
sLocalTimezone = "";
};
export default TimezoneUtils;