UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,265 lines (1,169 loc) 108 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ //Provides the locale object sap.ui.core.LocaleData sap.ui.define([ "./Locale", "sap/base/assert", "sap/base/i18n/Formatting", "sap/base/i18n/LanguageTag", "sap/base/i18n/Localization", "sap/base/i18n/date/CalendarType", "sap/base/i18n/date/CalendarWeekNumbering", "sap/base/util/extend", "sap/base/util/LoaderExtensions", "sap/ui/base/Object", "sap/ui/base/SyncPromise" ], function(Locale, assert, Formatting, LanguageTag, Localization, CalendarType, CalendarWeekNumbering, extend, LoaderExtensions, BaseObject, SyncPromise) { "use strict"; var rCIgnoreCase = /c/i, rEIgnoreCase = /e/i, rNumberInScientificNotation = /^([+-]?)((\d+)(?:\.(\d+))?)[eE]([+-]?\d+)$/, rTrailingZeroes = /0+$/; const rFallbackPatternTextParts = /(.*)?\{[0|1]}(.*)?\{[0|1]}(.*)?/; const rOnlyZeros = /^0+$/; const aSupportedWidths = ["narrow", "abbreviated", "wide"]; /** * With the upgrade of the CLDR to version 41 some unit keys have changed. * For compatibility reasons this map is used for formatting units. * It maps a legacy unit key to its renamed key. * * @deprecated As of version 1.122.0, this map is no longer maintained and stays for compatibility reasons * only. Reason for the depreciation: The assumption of homogeneous unit keys in the CLDR data has been proven * wrong. Additionally, it is unclear if, those CLDR unit keys are actually used. Implementing a complex logic * to maintain potentially unused entries did not seem reasonable. Therefore, it was decided to deprecate this * feature. * This map was last updated with CLDR V43, in 1.119.0. * @private */ const mLegacyUnit2CurrentUnit = { "acceleration-meter-per-second-squared": "acceleration-meter-per-square-second", "concentr-milligram-per-deciliter": "concentr-milligram-ofglucose-per-deciliter", "concentr-part-per-million": "concentr-permillion", "consumption-liter-per-100kilometers": "consumption-liter-per-100-kilometer", "mass-metric-ton": "mass-tonne", "pressure-millimeter-of-mercury": "pressure-millimeter-ofhg", "pressure-pound-per-square-inch": "pressure-pound-force-per-square-inch", "pressure-inch-hg": "pressure-inch-ofhg", "torque-pound-foot": "torque-pound-force-foot" }; /** * The locale data cache. Maps a locale ID, formatted as either the language_region (e.g. "ar_SA"), * language_script (e.g. "sr_Latn") or just the language code (e.g. "de") to its set of loaded * CLDR data. In case of asynchronous loading, the locale ID is mapped to a <code>Promise</code> which resolves * with the loaded CLDR data. As soon as the data is loaded the <code>Promise</code> is replaced by it. * * @type {Object<string, Object<string, any>|Promise<Object<string, any>>>} * @private */ let mLocaleIdToData = {}; /** * DO NOT call the constructor for <code>LocaleData</code>; use <code>LocaleData.getInstance</code> instead. * * @param {sap.ui.core.Locale} oLocale The locale * @param {boolean} bAsync Whether to load the data asynchronously * * @alias sap.ui.core.LocaleData * @author SAP SE * @extends sap.ui.base.Object * @class Provides access to locale-specific data, such as date formats, number formats, and currencies. For more * information on terminology, such as field names used in the methods of this class, see * {@link https://cldr.unicode.org/ Unicode CLDR}. * @hideconstructor * @public * @version 1.147.0 */ var LocaleData = BaseObject.extend("sap.ui.core.LocaleData", /** @lends sap.ui.core.LocaleData.prototype */ { constructor: function(oLocale, bAsync) { BaseObject.apply(this); this.oLocale = Locale._getCoreLocale(oLocale); this.loaded = loadData(this.oLocale, bAsync).then((oResult) => { this.mData = oResult.mData; this.sCLDRLocaleId = oResult.sCLDRLocaleId; return this; }); this.loaded.finally(() => { delete this.loaded; }); }, /** * @private * @ui5-restricted UI5 Web Components */ _get: function() { return this._getDeep(this.mData, arguments); }, /** * Retrieves merged object if overlay data is available * @private * @return {object} merged object */ _getMerged: function() { return this._get.apply(this, arguments); }, /** * Get month names in the given width. Result may contain alternative month names. * * @param {"abbreviated"|"narrow"|"wide"} sWidth * The required width for the month names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] * The type of calendar; defaults to the calendar type either set in configuration or calculated from the * locale * @returns {array} * The array of month names; if no alternative exists the entry for the month is its name as a string; if * there are alternative month names the entry for the month is an array of strings with the alternative names * @private */ _getMonthsWithAlternatives: function(sWidth, sCalendarType) { return this._get(getCLDRCalendarName(sCalendarType), "months", "format", sWidth); }, /** * Get standalone month names in the given width. Result may contain alternative month * names. * * @param {"abbreviated"|"narrow"|"wide"} sWidth * The required width for the month names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] * The type of calendar; defaults to the calendar type either set in configuration or calculated from the * locale * @returns {array} * The array of month names; if no alternative exists the entry for the month is its name as a string; if * there are alternative month names the entry for the month is an array of strings with the alternative names * @private */ _getMonthsStandAloneWithAlternatives: function(sWidth, sCalendarType) { return this._get(getCLDRCalendarName(sCalendarType), "months", "stand-alone", sWidth); }, _getDeep: function(oObject, aPropertyNames) { var oResult = oObject; for (var i = 0; i < aPropertyNames.length; i++) { oResult = oResult[aPropertyNames[i]]; if (oResult === undefined) { break; } } return oResult; }, /** * Gets the text orientation. * * @returns {"left-to-right"|"right-to-left"} text orientation * @public */ getOrientation: function() { return this._get("orientation"); }, /** * Get a display name for the language of the Locale of this LocaleData, using * the CLDR display names for languages. * * The lookup logic works as follows: * 1. language code and region is checked (e.g. "en-GB") * 2. If not found: language code and script is checked (e.g. "zh-Hant") * 3. If not found language code is checked (e.g. "en") * 4. If it is then still not found <code>undefined</code> is returned. * * @returns {string} language name, e.g. "English", "British English", "American English" * or <code>undefined</code> if language cannot be found * @private * @ui5-restricted sap.ushell */ getCurrentLanguageName: function () { return this.getLanguageName(this.oLocale.toString()); }, /** * Gets the locale-specific language name for the given language tag. * * The languages returned by {@link #getLanguages} from the CLDR raw data do not contain the * language names if they can be derived from the language and the script or the territory. * If the map of languages contains no entry for the given language tag, derive the language * name from the used script or region. * * @param {string} sLanguageTag * The language tag, for example "en", "en-US", "en_US", "zh-Hant", or "zh_Hant" * @returns {string|undefined} * The language name, or <code>undefined</code> if the name cannot be determined * @throws {TypeError} When the given language tag isn't valid * * @public */ getLanguageName: function (sLanguageTag) { const oLanguageTag = new LanguageTag(sLanguageTag); let sLanguage = Localization.getModernLanguage(oLanguageTag.language); let sScript = oLanguageTag.script; // special case for "sr_Latn" language: "sh" should then be used if (sLanguage === "sr" && sScript === "Latn") { sLanguage = "sh"; sScript = null; } const sRegion = oLanguageTag.region; const oLanguages = this._get("languages"); const sLanguageText = oLanguages[sLanguage]; if (!sScript && !sRegion || !sLanguageText) { return sLanguageText; } const sResult = oLanguages[sLanguage + "_" + sRegion] || oLanguages[sLanguage + "_" + sScript]; if (sResult) { return sResult; } if (sScript) { const sScriptText = this._get("scripts")[sScript]; if (sScriptText) { return sLanguageText + " (" + sScriptText + ")"; } } if (sRegion) { const sRegionText = this._get("territories")[sRegion]; if (sRegionText) { return sLanguageText + " (" + sRegionText + ")"; } } return sLanguageText; }, /** * Gets locale-specific language names, as available in the CLDR raw data. * * To avoid redundancies, with CLDR version 43 only language names are contained which cannot be derived from * the language and the script or the territory. If a language tag is not contained in the map, use * {@link #getLanguageName} to get the derived locale-specific language name for that language tag. * * @returns {Object<string, string>} Maps a language tag to the locale-specific language name * * @public */ getLanguages: function() { const oLanguages = this._get("languages"); /** @deprecated As of version 1.120.0 */ [ "ar_001", "de_AT", "de_CH", "en_AU", "en_CA", "en_GB", "en_US", "es_419", "es_ES", "es_MX", "fa_AF", "fr_CA", "fr_CH", "nds_NL", "nl_BE", "pt_BR", "pt_PT", "ro_MD", "sw_CD", "zh_Hans", "zh_Hant" ].forEach((sLanguageTag) => { // for compatibility reasons, ensure that for these language tags the corresponding language names are // available if (!oLanguages[sLanguageTag]) { oLanguages[sLanguageTag] = this.getLanguageName(sLanguageTag); } }); return oLanguages; }, /** * Gets locale-specific script names, as available in the CLDR raw data. * * To avoid redundancies, with CLDR version 43 only scripts are contained for which the language-specific name * is different from the script key. If a script key is not contained in the map, use the script key as script * name. * * @returns {Object<string, string>} Maps a script key to the locale-specific script name * * @public */ getScripts: function() { return this._get("scripts"); }, /** * Gets locale-specific territory names, as available in the CLDR raw data. * * To avoid redundancies, with CLDR version 43 only territories are contained for which the language-specific * name is different from the territory key. * * @returns {Object<string, string>} Maps a territory key to the locale-specific territory name * * @public */ getTerritories: function() { return this._get("territories"); }, /** * Get month names in the given width. * * @param {"abbreviated"|"narrow"|"wide"} sWidth * The required width for the month names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] * The type of calendar; defaults to the calendar type either set in configuration or calculated from the * locale * @returns {string[]} * The array of month names * @public */ getMonths: function(sWidth, sCalendarType) { assert(aSupportedWidths.includes(sWidth), "sWidth must be narrow, abbreviated or wide"); return this._get(getCLDRCalendarName(sCalendarType), "months", "format", sWidth).map((vMonthName) => { return Array.isArray(vMonthName) ? vMonthName[0] : vMonthName; }); }, /** * Get standalone month names in the given width. * * @param {"abbreviated"|"narrow"|"wide"} sWidth * The required width for the month names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] * The type of calendar; defaults to the calendar type either set in configuration or calculated from the * locale * @returns {string[]} * The array of standalone month names * @public */ getMonthsStandAlone: function(sWidth, sCalendarType) { assert(aSupportedWidths.includes(sWidth), "sWidth must be narrow, abbreviated or wide"); return this._get(getCLDRCalendarName(sCalendarType), "months", "stand-alone", sWidth).map((vMonthName) => { return Array.isArray(vMonthName) ? vMonthName[0] : vMonthName; }); }, /** * Get day names in the given width. * * @param {"abbreviated"|"narrow"|"short"|"wide"} sWidth the required width for the day names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string[]} array of day names (starting with Sunday) * @public */ getDays: function(sWidth, sCalendarType) { assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide" || sWidth == "short", "sWidth must be narrow, abbreviate, wide or short"); return this._get(getCLDRCalendarName(sCalendarType), "days", "format", sWidth); }, /** * Get standalone day names in the given width. * * @param {"abbreviated"|"narrow"|"short"|"wide"} sWidth the required width for the day names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string[]} array of day names (starting with Sunday) * @public */ getDaysStandAlone: function(sWidth, sCalendarType) { assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide" || sWidth == "short", "sWidth must be narrow, abbreviated, wide or short"); return this._get(getCLDRCalendarName(sCalendarType), "days", "stand-alone", sWidth); }, /** * Get quarter names in the given width. * * @param {"abbreviated"|"narrow"|"wide"} sWidth the required width for the quarter names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string[]} array of quarters * @public */ getQuarters: function(sWidth, sCalendarType) { assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide"); return this._get(getCLDRCalendarName(sCalendarType), "quarters", "format", sWidth); }, /** * Get standalone quarter names in the given width. * * @param {"abbreviated"|"narrow"|"wide"} sWidth the required width for the quarter names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string[]} array of quarters * @public */ getQuartersStandAlone: function(sWidth, sCalendarType) { assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide"); return this._get(getCLDRCalendarName(sCalendarType), "quarters", "stand-alone", sWidth); }, /** * Get day periods in the given width. * * @param {"abbreviated"|"narrow"|"wide"} sWidth the required width for the day period names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string[]} array of day periods (AM, PM) * @public */ getDayPeriods: function(sWidth, sCalendarType) { assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide"); return this._get(getCLDRCalendarName(sCalendarType), "dayPeriods", "format", sWidth); }, /** * Get standalone day periods in the given width. * * @param {"abbreviated"|"narrow"|"wide"} sWidth the required width for the day period names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string[]} array of day periods (AM, PM) * @public */ getDayPeriodsStandAlone: function(sWidth, sCalendarType) { assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide"); return this._get(getCLDRCalendarName(sCalendarType), "dayPeriods", "stand-alone", sWidth); }, /** * Get date pattern in the given style. * * @param {"full"|"long"|"medium"|"short"} sStyle the required style for the date pattern * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} the selected date pattern * @public */ getDatePattern: function(sStyle, sCalendarType) { assert(sStyle == "short" || sStyle == "medium" || sStyle == "long" || sStyle == "full", "sStyle must be short, medium, long or full"); return this._get(getCLDRCalendarName(sCalendarType), "dateFormats", sStyle); }, /** * Get flexible day periods in style format "abbreviated", "narrow" or "wide". * * @param {string} sWidth * The required width for the flexible day period names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] * The type of calendar. If it's not set, it falls back to the calendar type either set in * configuration or calculated from locale. * @returns {object|undefined} * Object of flexible day periods or 'undefined' if none can be found * * @example <caption>Output</caption> * { * "midnight": "midnight", * "noon": "noon", * "morning1": "in the morning", * "afternoon1": "in the afternoon", * "evening1": "in the evening", * "night1": "at night" * } * * @private */ getFlexibleDayPeriods : function (sWidth, sCalendarType) { return this._get(getCLDRCalendarName(sCalendarType), "flexibleDayPeriods", "format", sWidth); }, /** * Get flexible day periods in style format "abbreviated", "narrow" or "wide" for case * "stand-alone". * * @param {string} sWidth * The required width for the flexible day period names * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] * The type of calendar. If it's not set, it falls back to the calendar type either set in * configuration or calculated from locale. * @returns {object|undefined} * Object of flexible day periods or 'undefined' if none can be found * * @example <caption>Output</caption> * { * "midnight": "midnight", * "noon": "noon", * "morning1": "in the morning", * "afternoon1": "in the afternoon", * "evening1": "in the evening", * "night1": "at night" * } * * @private */ getFlexibleDayPeriodsStandAlone : function (sWidth, sCalendarType) { return this._get(getCLDRCalendarName(sCalendarType), "flexibleDayPeriods", "stand-alone", sWidth); }, /** * Get flexible day period of time or a point in time * * @param {int} iHour Hour * @param {int} iMinute Minute * @returns {string} Key of flexible day period of time e.g. <code>afternoon2</code> * * @private */ getFlexibleDayPeriodOfTime : function (iHour, iMinute) { var iAbsoluteMinutes, oDayPeriodRules, sPeriodMatch; iAbsoluteMinutes = (iHour * 60 + iMinute) % 1440; oDayPeriodRules = this._get("dayPeriodRules"); function parseToAbsoluteMinutes(sValue) { var aSplit = sValue.split(":"), sHour = aSplit[0], sMinute = aSplit[1]; return parseInt(sHour) * 60 + parseInt(sMinute); } // unfortunately there are some overlaps: // e.g. en.json // "afternoon1": { // "_before": "18:00", // "_from": "12:00" // }, // "noon": { // "_at": "12:00" // } // -> 12:00 can be either "noon" or "afternoon1" because "_from" is inclusive // therefore first check all exact periods sPeriodMatch = Object.keys(oDayPeriodRules).find(function (sDayPeriodRule) { var oDayPeriodRule = oDayPeriodRules[sDayPeriodRule]; return oDayPeriodRule["_at"] && parseToAbsoluteMinutes(oDayPeriodRule["_at"]) === iAbsoluteMinutes; }); if (sPeriodMatch) { return sPeriodMatch; } return Object.keys(oDayPeriodRules).find(function (sDayPeriodRule) { var iEndValue, aIntervals, iStartValue, oDayPeriodRule = oDayPeriodRules[sDayPeriodRule]; if (oDayPeriodRule["_at"]) { return false; } iStartValue = parseToAbsoluteMinutes(oDayPeriodRule["_from"]); iEndValue = parseToAbsoluteMinutes(oDayPeriodRule["_before"]); // periods which span across days need to be split into individual intervals // e.g. "22:00 - 03:00" becomes "22:00 - 24:00" and "00:00 - 03:00" if (iStartValue > iEndValue) { aIntervals = [ {start : iStartValue, end : 1440}, // 24 * 60 {start : 0, end : iEndValue} ]; } else { aIntervals = [ {start : iStartValue, end : iEndValue} ]; } return aIntervals.some(function (oInterval) { return oInterval.start <= iAbsoluteMinutes && oInterval.end > iAbsoluteMinutes; }); }); }, /** * Get time pattern in the given style. * * @param {"full"|"long"|"medium"|"short"} sStyle the required style for the time pattern * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} the selected time pattern * @public */ getTimePattern: function(sStyle, sCalendarType) { assert(sStyle == "short" || sStyle == "medium" || sStyle == "long" || sStyle == "full", "sStyle must be short, medium, long or full"); return this._get(getCLDRCalendarName(sCalendarType), "timeFormats", sStyle); }, /** * Get datetime pattern in the given style. * * @param {"full"|"long"|"medium"|"short"} sStyle the required style for the datetime pattern * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} the selected datetime pattern * @public */ getDateTimePattern: function(sStyle, sCalendarType) { assert(sStyle == "short" || sStyle == "medium" || sStyle == "long" || sStyle == "full", "sStyle must be short, medium, long or full"); return this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", sStyle); }, /** * Get combined datetime pattern with given date and time style. The combined datetime pattern is the datetime * pattern as returned by {@link #getDateTimePattern}, where date and time placeholder are replaced with * the corresponding patterns for the given styles. * * @param {"full"|"long"|"medium"|"short"} sDateStyle the required style for the date part * @param {"full"|"long"|"medium"|"short"} sTimeStyle the required style for the time part * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} the combined datetime pattern * @public */ getCombinedDateTimePattern: function(sDateStyle, sTimeStyle, sCalendarType) { assert(sDateStyle == "short" || sDateStyle == "medium" || sDateStyle == "long" || sDateStyle == "full", "sStyle must be short, medium, long or full"); assert(sTimeStyle == "short" || sTimeStyle == "medium" || sTimeStyle == "long" || sTimeStyle == "full", "sStyle must be short, medium, long or full"); var sDateTimePattern = this.getDateTimePattern(sDateStyle, sCalendarType), sDatePattern = this.getDatePattern(sDateStyle, sCalendarType), sTimePattern = this.getTimePattern(sTimeStyle, sCalendarType); return sDateTimePattern.replace("{0}", sTimePattern).replace("{1}", sDatePattern); }, /** * Get combined pattern with datetime and timezone for the given date and time style. * * @example * // locale de * oLocaleData.getCombinedDateTimeWithTimezonePattern("long", "long"); * // "d. MMMM y 'um' HH:mm:ss z VV" * * // locale en_GB * oLocaleData.getCombinedDateTimeWithTimezonePattern("long", "long"); * // "d MMMM y 'at' HH:mm:ss z VV" * * @param {string} sDateStyle The required style for the date part * @param {string} sTimeStyle The required style for the time part * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] The type of calendar. If it's not set, * it falls back to the calendar type either set in the configuration or calculated from * the locale. * @returns {string} the combined pattern with datetime and timezone * @private * @ui5-restricted sap.ui.core.format.DateFormat * @since 1.101 */ getCombinedDateTimeWithTimezonePattern: function(sDateStyle, sTimeStyle, sCalendarType) { return this.applyTimezonePattern(this.getCombinedDateTimePattern(sDateStyle, sTimeStyle, sCalendarType)); }, /** * Applies the timezone to the pattern * * @param {string} sPattern pattern, e.g. <code>y</code> * @returns {string} applied timezone, e.g. <code>y VV</code> * @private * @ui5-restricted sap.ui.core.format.DateFormat * @since 1.101 */ applyTimezonePattern: function(sPattern) { var aPatterns = [sPattern]; var aMissingTokens = [{ group: "Timezone", length: 2, field: "zone", symbol: "V" }]; this._appendItems(aPatterns, aMissingTokens); return aPatterns[0]; }, /** * Retrieves all timezone translations. * * E.g. for locale "en" * <pre> * { * "America/New_York": "Americas, New York" * ... * } * </pre> * * @return {Object<string, string>} the mapping, with 'key' being the IANA timezone ID, and * 'value' being the translation. * @ui5-restricted sap.ui.core.format.DateFormat, sap.ui.export, sap.ushell * @private */ getTimezoneTranslations: function() { var sLocale = this.oLocale.toString(); var mTranslations = LocaleData._mTimezoneTranslations[sLocale]; if (!mTranslations) { LocaleData._mTimezoneTranslations[sLocale] = mTranslations = _resolveTimezoneTranslationStructure(this._get("timezoneNames")); } // retrieve a copy such that the original object won't be modified. return Object.assign({}, mTranslations); }, /** * Get custom datetime pattern for a given skeleton format. * * The format string does contain pattern symbols (e.g. "yMMMd" or "Hms") and will be converted into the pattern in the used * locale, which matches the wanted symbols best. The symbols must be in canonical order, that is: * Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D), * Hour (h/H/k/K/), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x) * * See https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems * * @param {string} sSkeleton the wanted skeleton format for the datetime pattern * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} the best matching datetime pattern * @since 1.34 * @public */ getCustomDateTimePattern: function(sSkeleton, sCalendarType) { var oAvailableFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "availableFormats"); return this._getFormatPattern(sSkeleton, oAvailableFormats, sCalendarType); }, /** * Returns the interval format with the given Id (see CLDR documentation for valid Ids) * or the fallback format if no interval format with that Id is known. * * The empty Id ("") might be used to retrieve the interval format fallback. * * @param {string} sId Id of the interval format, e.g. "d-d" * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} interval format string with placeholders {0} and {1} * @public * @since 1.17.0 */ getIntervalPattern : function(sId, sCalendarType) { var oIntervalFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "intervalFormats"), aIdParts, sIntervalId, sDifference, oInterval, sPattern; if (sId) { aIdParts = sId.split("-"); sIntervalId = aIdParts[0]; sDifference = aIdParts[1]; oInterval = oIntervalFormats[sIntervalId]; if (oInterval) { sPattern = oInterval[sDifference]; if (sPattern) { return sPattern; } } } return oIntervalFormats.intervalFormatFallback; }, /** * Get combined interval pattern using a given pattern and the fallback interval pattern. * * If a skeleton based pattern is not available or not wanted, this method can be used to create an interval * pattern based on a given pattern, using the fallback interval pattern. * * @param {string} sPattern the single date pattern to use within the interval pattern * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string} the calculated interval pattern * @since 1.46 * @public */ getCombinedIntervalPattern: function (sPattern, sCalendarType) { const oIntervalFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "intervalFormats"); const [/*sAll*/, sTextBefore, sTextBetween, sTextAfter] = rFallbackPatternTextParts.exec(oIntervalFormats.intervalFormatFallback); // text part of intervalFormatFallback is not escaped return LocaleData._escapeIfNeeded(sTextBefore) + sPattern + LocaleData._escapeIfNeeded(sTextBetween) + sPattern + LocaleData._escapeIfNeeded(sTextAfter); }, /** * @typedef {object} sap.ui.core.LocaleData.DateFieldGroupsDifference * * Type which describes the difference in the date field groups of the two dates of an date time interval. * The keys are the names of the date field symbol groups. If one of them is set, the value should be set to * <code>true</code>. * * @property {boolean} [Era] The era date field symbol group * @property {boolean} [Year] The year date field symbol group * @property {boolean} [Quarter] The quarter date field symbol group * @property {boolean} [Month] The month date field symbol group * @property {boolean} [Week] The week date field symbol group * @property {boolean} [Day] The day date field symbol group * @property {boolean} [DayPeriod] The day period date field symbol group * @property {boolean} [Hour] The hour date field symbol group * @property {boolean} [Minute] The minute date field symbol group * @property {boolean} [Second] The second date field symbol group * * @public */ /** * Get interval pattern for a given skeleton format. * * The format string does contain pattern symbols (e.g. "yMMMd" or "Hms") and will be converted into the pattern in the used * locale, which matches the wanted symbols best. The symbols must be in canonical order, that is: * Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D), * Hour (h/H/k/K/), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x) * * See {@link https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems * Unicode - Available Formats} * * @param {string} sSkeleton the wanted skeleton format for the datetime pattern * @param {sap.ui.core.LocaleData.DateFieldGroupsDifference|string} vGreatestDiff * is either a string which represents the symbol matching the greatest difference in the two dates to * format or an object which contains key-value pairs. The value is always <code>true</code>. The key is one * of the date field symbol groups whose value are different between the two dates. The key can only be set * with: * <code>'Era', 'Year', 'Quarter', 'Month', 'Week', 'Day', 'DayPeriod', 'Hour','Minute', 'Second'</code>. * For more information, see {@link https://unicode.org/reports/tr35/tr35-dates.html#element-intervalformats * Unicode - Element intervalFormats}. * @param {module:sap/base/i18n/date/CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale. * @returns {string|string[]} the best matching interval pattern if interval difference is given otherwise an array with all possible interval patterns which match the given skeleton format * @since 1.46 * @public */ getCustomIntervalPattern : function(sSkeleton, vGreatestDiff, sCalendarType) { var oAvailableFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "intervalFormats"); return this._getFormatPattern(sSkeleton, oAvailableFormats, sCalendarType, vGreatestDiff); }, /* Helper functions for skeleton pattern processing */ _getFormatPattern: function(sSkeleton, oAvailableFormats, sCalendarType, vDiff) { var vPattern, aPatterns, oIntervalFormats; if (!vDiff) { // the call is from getCustomDateTimePattern vPattern = oAvailableFormats[sSkeleton]; } else if (typeof vDiff === "string") { // vDiff is given as a symbol if (vDiff == "j" || vDiff == "J") { vDiff = this.getPreferredHourSymbol(); } oIntervalFormats = oAvailableFormats[sSkeleton]; vPattern = oIntervalFormats && oIntervalFormats[vDiff]; } if (vPattern) { if (typeof vPattern === "object") { aPatterns = Object.keys(vPattern).map(function(sKey) { return vPattern[sKey]; }); } else { return vPattern; } } if (!aPatterns) { aPatterns = this._createFormatPattern(sSkeleton, oAvailableFormats, sCalendarType, vDiff); } if (aPatterns && aPatterns.length === 1) { return aPatterns[0]; } return aPatterns; }, _createFormatPattern: function(sSkeleton, oAvailableFormats, sCalendarType, vDiff) { var aTokens = this._parseSkeletonFormat(sSkeleton), aPatterns, oBestMatch = this._findBestMatch(aTokens, sSkeleton, oAvailableFormats), oToken, oAvailableDateTimeFormats, oSymbol, oGroup, sPattern, sSinglePattern, sDiffSymbol, sDiffGroup, rMixedSkeleton = /^([GyYqQMLwWEecdD]+)([hHkKjJmszZvVOXx]+)$/, bSingleDate, i; if (vDiff) { if (typeof vDiff === "string") { sDiffGroup = mCLDRSymbols[vDiff] ? mCLDRSymbols[vDiff].group : ""; if (sDiffGroup) { // if the index of interval diff is greater than the index of the last field // in the sSkeleton, which means the diff unit is smaller than all units in // the skeleton, return a single date pattern which is generated using the // given skeleton bSingleDate = mCLDRSymbolGroups[sDiffGroup].index > aTokens[aTokens.length - 1].index; } sDiffSymbol = vDiff; } else { bSingleDate = true; // Special handling of "y" (Year) in case patterns contains also "G" (Era) if (aTokens[0].symbol === "y" && oBestMatch && oBestMatch.pattern.G) { oSymbol = mCLDRSymbols["G"]; oGroup = mCLDRSymbolGroups[oSymbol.group]; aTokens.splice(0, 0, { symbol: "G", group: oSymbol.group, match: oSymbol.match, index: oGroup.index, field: oGroup.field, length: 1 }); } // Check if at least one token's group appears in the interval diff // If not, a single date pattern is returned for (i = aTokens.length - 1; i >= 0; i--){ oToken = aTokens[i]; if (vDiff[oToken.group]) { bSingleDate = false; break; } } // select the greatest diff symbol for (i = 0; i < aTokens.length; i++){ oToken = aTokens[i]; if (vDiff[oToken.group]) { sDiffSymbol = oToken.symbol; break; } } // Special handling of "a" (Dayperiod) // Find out whether dayperiod is different between the dates // If yes, set the diff symbol with 'a' Dayperiod symbol if ((sDiffSymbol == "h" || sDiffSymbol == "K") && vDiff.DayPeriod) { sDiffSymbol = "a"; } } if (bSingleDate) { return [this.getCustomDateTimePattern(sSkeleton, sCalendarType)]; } // Only use best match, if there are no missing tokens, as there is no possibility // to append items on interval formats if (oBestMatch && oBestMatch.missingTokens.length === 0) { sPattern = oBestMatch.pattern[sDiffSymbol]; // if there is no exact match, we need to do further processing if (sPattern && oBestMatch.distance > 0) { sPattern = this._expandFields(sPattern, oBestMatch.patternTokens, aTokens); } } // If no pattern could be found, get the best availableFormat for the skeleton // and use the fallbackIntervalFormat to create the pattern if (!sPattern) { oAvailableDateTimeFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "availableFormats"); // If it is a mixed skeleton and the greatest interval on time, create a mixed pattern if (rMixedSkeleton.test(sSkeleton) && "ahHkKjJms".indexOf(sDiffSymbol) >= 0) { sPattern = this._getMixedFormatPattern(sSkeleton, oAvailableDateTimeFormats, sCalendarType, vDiff); } else { sSinglePattern = this._getFormatPattern(sSkeleton, oAvailableDateTimeFormats, sCalendarType); sPattern = this.getCombinedIntervalPattern(sSinglePattern, sCalendarType); } } aPatterns = [sPattern]; } else if (!oBestMatch) { sPattern = sSkeleton; aPatterns = [sPattern]; } else { if (typeof oBestMatch.pattern === "string") { aPatterns = [oBestMatch.pattern]; } else if (typeof oBestMatch.pattern === "object") { aPatterns = []; for (var sKey in oBestMatch.pattern) { sPattern = oBestMatch.pattern[sKey]; aPatterns.push(sPattern); } } // if there is no exact match, we need to do further processing if (oBestMatch.distance > 0) { if (oBestMatch.missingTokens.length > 0) { // if tokens are missing create a pattern containing all, otherwise just adjust pattern if (rMixedSkeleton.test(sSkeleton)) { aPatterns = [this._getMixedFormatPattern(sSkeleton, oAvailableFormats, sCalendarType)]; } else { aPatterns = this._expandFields(aPatterns, oBestMatch.patternTokens, aTokens); aPatterns = this._appendItems(aPatterns, oBestMatch.missingTokens, sCalendarType); } } else { aPatterns = this._expandFields(aPatterns, oBestMatch.patternTokens, aTokens); } } } // If special input token "J" was used, remove dayperiod from pattern if (sSkeleton.indexOf("J") >= 0) { aPatterns.forEach(function(sPattern, iIndex) { aPatterns[iIndex] = sPattern.replace(/ ?[abB](?=([^']*'[^']*')*[^']*)$/g, ""); }); } return aPatterns; }, _parseSkeletonFormat: function(sSkeleton) { var aTokens = [], oToken = {index: -1}, sSymbol, oSymbol, oGroup; for (var i = 0; i < sSkeleton.length; i++) { sSymbol = sSkeleton.charAt(i); // Handle special input symbols if (sSymbol == "j" || sSymbol == "J") { sSymbol = this.getPreferredHourSymbol(); } // if the symbol is the same as current token, increase the length if (sSymbol == oToken.symbol) { oToken.length++; continue; } // get symbol group oSymbol = mCLDRSymbols[sSymbol]; oGroup = mCLDRSymbolGroups[oSymbol.group]; // if group is other, the symbol is not allowed in skeleton tokens if (oSymbol.group == "Other" || oGroup.diffOnly) { throw new Error("Symbol '" + sSymbol + "' is not allowed in skeleton format '" + sSkeleton + "'"); } // if group index the same or lower, format is invalid if (oGroup.index <= oToken.index) { throw new Error("Symbol '" + sSymbol + "' at wrong position or duplicate in skeleton format '" + sSkeleton + "'"); } // create token and add it the token array oToken = { symbol: sSymbol, group: oSymbol.group, match: oSymbol.match, index: oGroup.index, field: oGroup.field, length: 1 }; aTokens.push(oToken); } return aTokens; }, _findBestMatch: function(aTokens, sSkeleton, oAvailableFormats) { var aTestTokens, aMissingTokens, oToken, oTestToken, iTest, iDistance, bMatch, iFirstDiffPos, oTokenSymbol, oTestTokenSymbol, oBestMatch = { distance: 10000, firstDiffPos: -1 }; // Loop through all available tokens, find matches and calculate distance for (var sTestSkeleton in oAvailableFormats) { // Skip patterns with symbol "B" (which is introduced from CLDR v32.0.0) which isn't supported in DateFormat yet if (sTestSkeleton === "intervalFormatFallback" || sTestSkeleton.indexOf("B") > -1) { continue; } aTestTokens = this._parseSkeletonFormat(sTestSkeleton); iDistance = 0; aMissingTokens = []; bMatch = true; // if test format contains more tokens, it cannot be a best match if (aTokens.length < aTestTokens.length) { continue; } iTest = 0; iFirstDiffPos = aTokens.length; for (var i = 0; i < aTokens.length; i++) { oToken = aTokens[i]; oTestToken = aTestTokens[iTest]; if (iFirstDiffPos === aTokens.length) { iFirstDiffPos = i; } if (oTestToken) { oTokenSymbol = mCLDRSymbols[oToken.symbol]; oTestTokenSymbol = mCLDRSymbols[oTestToken.symbol]; // if the symbol matches, just add the length difference to the distance if (oToken.symbol === oTestToken.symbol) { if (oToken.length === oTestToken.length) { // both symbol and length match, check the next token // clear the first difference position if (iFirstDiffPos === i) { iFirstDiffPos = aTokens.length; } } else { if (oToken.length < oTokenSymbol.numericCeiling ? oTestToken.length < oTestTokenSymbol.numericCeiling : oTestToken.length >= oTestTokenSymbol.numericCeiling) { // if the symbols are in the same category (either numeric or text representation), add the length diff iDistance += Math.abs(oToken.length - oTestToken.length); } else { // otherwise add 5 which is bigger than any length difference iDistance += 5; } } iTest++; continue; } else { // if only the group matches, add some more distance in addition to length difference if (oToken.match == oTestToken.match) { iDistance += Math.abs(oToken.length - oTestToken.length) + 10; iTest++; continue; } } } // if neither symbol nor group matched, add it to the missing tokens and add distance aMissingTokens.push(oToken); iDistance += 50 - i; } // if not all test tokens have been found, the format does not match if (iTest < aTestTokens.length) { bMatch = false; } // The current pattern is saved as the best pattern when there is a match and // 1. the distance is smaller than the best distance or // 2. the distance equals the best distance and the position of the token in the given skeleton which // isn't the same between the given skeleton and the available skeleton is bigger than the best one's. if (bMatch && (iDistance < oBestMatch.distance || (iDistance === oBestMatch.distance && iFirstDiffPos > oBestMatch.firstDiffPos))) { oBestMatch.distance = iDistance; oBestMatch.firstDiffPos = iFirstDiffPos; oBestMatch.missingTokens = aMissingTokens; oBestMatch.pattern = oAvailableFormats[sTestSkeleton]; oBestMatch.patternTokens = aTestTokens; } } if (oBestMatch.pattern) { return oBestMatch; } }, _expandFields: function(vPattern, aPatternTokens, aTokens) { var bSinglePattern = (typeof vPattern === "string"); var aPatterns; if (bSinglePattern) { aPatterns = [vPattern]; } else { aPatterns = vPattern; } var aResult = aPatterns.map(function(sPattern) { var mGroups = {}, mPatternGroups = {}, sResultPatterm = "", bQuoted = false, i = 0, iSkeletonLength, iPatternLength, iBestLength, iNewLength, oSkeletonToken, oBestToken, oSymbol, sChar; // Create a map of group names to token aTokens.forEach(function(oToken) { mGroups[oToken.group] = oToken; }); // Create a map of group names to token in best pattern aPatternTokens.forEach(function(oToken) { mPatternGroups[oToken.group] = oToken; }); // Loop through pattern and adjust symbol length while (i < sPattern.length) { sChar = sPattern.charAt(i); if (bQuoted) { sResultPatterm += sChar; if (sChar == "'") { bQuoted = false; } } else { oSymbol = mCLDRSymbols[sChar]; // If symbol is a CLDR symbol and is contained in the group, expand length if (oSymbol && mGroups[oSymbol.group] && mPatternGroups[oSymbol.group]) { oSkeletonToken = mGroups[oSymbol.group]; oBestToken = mPatternGroups[oSymbol.group]; iSkeletonLength = oSkeletonToken.length; iBestLength = oBestToken.length; iPatternLength = 1; while (sPattern.charAt(i + 1) == sChar) { i++; iPatternLength++; } // Prevent expanding the length of the field when: // 1. The length in the best matching skeleton (iBestLength) matches the length of the application provided skeleton (iSkeletonLength) or // 2. The length of the provided skeleton (iSkeletonLength) and the length of the result pattern (iPatternLength) are not in the same category (numeric or text) // because switching between numeric to text representation is wrong in all cases if (iSkeletonLength === iBestLength || ((iSkeletonLength < oSymbol.numericCeiling) ? (iPatternLength >= oSymbol.numericCeiling) : (iPatternLength < oSymbol.numericCeiling) )) { iNewLength = iPatternLength; } else { iNewLength = Math.max(iPatternLength, iSkeletonLength); } for (var j = 0; j < iNewLength; j++) { sResultPatterm += sChar; } } else { sResultPatterm += sChar; if (sChar == "'") { bQuoted = true; } } } i++; } return sResultPatterm; }); return bSinglePattern ? aResult[0] : aResult; }, _appendItems: function(aPatterns, aMissingTokens, sCalendarType) { var oAppendItems = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "appendItems"); aPatterns.forEach(function(sPattern, iIndex) { var sDisplayName, sAppendPattern, sAppendField; aMissingTokens.forEach(function(oToken) { sAppendPattern = oAppendItems[oToken.group]; sDisplayName = "'" + this.getDisplayName(oToken.field) + "'"; sAppendField = ""; for (var i = 0; i < oToken.length; i++) { sAppendField += oToken.symbol; } aPatterns[iIndex] = sAppendPattern.replace(/\{0\}/, sPattern).replace(/\{1\}/, sAppendField).replace(/\{2\}/, sDisplayName); }.bind(this)); }.bind(this)); return aPatterns; }, _getMixedFormatPattern: function(sSkeleton, oAvailableFormats, sCalendarType, vDiff) { var rMixedSkeleton = /^([GyYqQMLwWEecdD]+)([hHkKjJmszZvVOXx]+)$/, rWideMonth = /MMMM|LLLL/, rAbbrevMonth = /MMM|LLL/, rWeekDay = /E|e|c/, oResult, sDateSkeleton, sTimeSkeleton, sStyle, sDatePattern, sTimePattern, sDateTimePattern, sResultPattern; // Split skeleton into date and time part oResult = rMixedSkeleton.exec(sSkeleton); sDateSkeleton = oResult[1]; sTimeSkeleton = oResult[2]; // Get patterns for date and time separately sDatePattern = this._getFormatPattern(sDateSkeleton, oAvailableFormats, sCalendarType); if (vDiff) { sTimePattern = this.getCustomIntervalPattern(sTimeSkeleton, vDiff, sCalendarType); } else { sTimePattern = this._getFormatPattern(sTimeSkeleton, oAvailableFormats, sCalendarType); } // Combine patterns with datetime pattern, dependent on month and weekday if (rWideMonth.test(sDateSkeleton)) { sStyle = rWeekDay.test(sDateSkeleton) ? "full" : "long"; } else if (rAbbrevMonth.test(sDateSkeleton)) { sStyle = "medium"; } else { sStyle = "short"; } sDateTimePattern = this.getDateTimePattern(sStyle, sCalendarType); sResultPattern = sDateTimePattern.replace(/\{1\}/, sDatePattern).replace(/\{0\}/, sTimePattern); return sResultPattern; }, /** * Get number symbol for the given type. * * @param {"decimal"|"group"|"minusSign"|"percentSign"|"plusSign"} sType the required type of symbol * @returns {string} the selected number symbol * @public */ getNumberSymbol: function(sType) { assert(sType == "decimal" || sType == "group" || sType == "plusSign" || sType == "minusSign" || sType == "percentSign", "sType must be decimal, group, plusSign, minusSign or percentSign"); ret