UNPKG

google-closure-library

Version:
1,343 lines (1,229 loc) 48.9 kB
/** * @license * Copyright The Closure Library Authors. * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Functions for dealing with date/time formatting. */ /** * Namespace for i18n date/time formatting functions */ goog.provide('goog.i18n.DateTimeFormat'); goog.provide('goog.i18n.DateTimeFormat.Format'); goog.require('goog.asserts'); goog.require('goog.date'); goog.require('goog.date.UtcDateTime'); goog.require('goog.i18n.DateTimeSymbols'); goog.require('goog.i18n.DayPeriods'); goog.require('goog.i18n.LocaleFeature'); goog.require('goog.i18n.NativeLocaleDigits'); goog.require('goog.i18n.TimeZone'); goog.require('goog.string'); goog.requireType('goog.i18n.DateTimeSymbolsType'); goog.scope(function() { // For referencing modules const DayPeriods = goog.module.get('goog.i18n.DayPeriods'); const LocaleFeature = goog.module.get('goog.i18n.LocaleFeature'); const NativeLocaleDigits = goog.module.get('goog.i18n.NativeLocaleDigits'); /** * IMPORTANT: Datetime formatting results different between JavaScript and * native ECMAScript implementations. * * Native mode accepts a set of options for styles and also for specifying * a small set of choices for each individual field of a formatted output. These * effectively specify skeletons which direct the formatting according to * formats built into the ECMAScript DateTime implementation of * Intl.DateTimeFormat. * * The ECMAScript DateTimeFormat constructor and options are defined here: * {@link * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat} * * Datetime formatting functions in JavaScript mode are provided with * options to use standard styles, predefined patterns such as YEAR_FULL, * and other values in goog.i18n.DateTimeFormat.Format. * * Native mode date/time formatting is supported only for these standard * patterns because they can be directly mapped to native mode options. * * Native mode does not support custom patterns, which are discouraged. * Using such custom pattern strings will call the JavaScript (polyfill) * version of DateTimeFormat rather than native ECMAScript. * * Custom patterns can be used using the symbols below for date/time. * Other text can be included. However, standard patterns are preferred * because native EMCAScript code is more efficient in download size and time. * * The following symbols may be used in pattern specification, as defined * in JDK, ICU and CLDR, with minor modification for typical usage in JS. * * Pattern specification: * {@link * https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table} * <pre> * Symbol Meaning Presentation Example * ------ ------- ------------ ------- * G# era designator (Text) AD * y# year (Number) 1996 * Y year (week of year) (Number) 1997 * u* extended year (Number) 4601 * Q# quarter (Text) Q3 & 3rd quarter * M month in year (Text & Number) July & 07 * L month in year (standalone) (Text & Number) July & 07 * d day in month (Number) 10 * h hour in am/pm (1~12) (Number) 12 * H hour in day (0~23) (Number) 0 * m minute in hour (Number) 30 * s second in minute (Number) 55 * S fractional second (Number) 978 * E# day of week (Text) Tue & Tuesday * e* day of week (local 1~7) (Number) 2 * c# day of week (standalone) (Text & Number) 2 & Tues & Tuesday & T * D* day in year (Number) 189 * F* day of week in month (Number) 2 (2nd Wed in July) * w week in year (Number) 27 * W* week in month (Number) 2 * a am/pm marker (Text) PM * b am/pm/noon/midnight (Text) Noon * B flexible day periods (Text) de l’après-midi' * k hour in day (1~24) (Number) 24 * K hour in am/pm (0~11) (Number) 0 * z time zone (Text) Pacific Standard Time * Z# time zone (RFC 822) (Number) -0800 * v# time zone (generic) (Text) America/Los_Angeles * V# time zone (Text) Los Angeles Time * g* Julian day (Number) 2451334 * A* milliseconds in day (Number) 69540000 * ' escape for text (Delimiter) 'Date=' * '' single quote (Literal) 'o''clock' * * Item marked with '*' are not supported yet. * Item marked with '#' works different than java * * The count of pattern letters determine the format. * (Text): 4 or more, use full form, <4, use short or abbreviated form if it * exists. (e.g., "EEEE" produces "Monday", "EEE" produces "Mon") * * (Number): the minimum number of digits. Shorter numbers are zero-padded to * this amount (e.g. if "m" produces "6", "mm" produces "06"). Year is handled * specially; that is, if the count of 'y' is 2, the Year will be truncated to * 2 digits. (e.g., if "yyyy" produces "1997", "yy" produces "97".) Unlike other * fields, fractional seconds are padded on the right with zero. * * :(Text & Number) 3 or over, use text, otherwise use number. (e.g., "M" * produces "1", "MM" produces "01", "MMM" produces "Jan", and "MMMM" produces * "January".) * * Any characters in the pattern that are not in the ranges of ['a'..'z'] and * ['A'..'Z'] will be treated as quoted text. For instance, characters like ':', * '.', ' ', '#' and '@' will appear in the resulting time text even they are * not embraced within single quotes. * </pre> */ /** * Construct a DateTimeFormat object based on current locale. * @constructor * @param {string|number} pattern pattern specification or pattern type. * @param {!Object=} opt_dateTimeSymbols Optional symbols to use for this * instance rather than the global symbols. * You can use some of the predefined SHORT / MEDIUM / LONG / FULL patterns, * or the common patterns defined in goog.i18n.DateTimePatterns. * Examples: * <code><pre> * let fmt = new goog.i18n.DateTimeFormat( * goog.i18n.DateTimeFormat.Format.FULL_DATE); * let fmt = new goog.i18n.DateTimeFormat( * goog.i18n.DateTimePatterns.MONTH_DAY_YEAR_MEDIUM); * </pre></code> * * {@see goog.i18n.DateTimeFormat.Format} * {@see goog.i18n.DateTimePatterns} * @final */ goog.i18n.DateTimeFormat = function(pattern, opt_dateTimeSymbols) { 'use strict'; goog.asserts.assert(pattern !== undefined, 'Pattern must be defined'); goog.asserts.assert( opt_dateTimeSymbols !== undefined || goog.i18n.DateTimeSymbols !== undefined, 'goog.i18n.DateTimeSymbols or explicit symbols must be defined'); /** * Remember if the implementation is ECMAScript * @type {?goog.global.Intl.DateTimeFormat} * @private */ this.intlFormatter_ = null; /** * Remember the pattern applied for resetting Intl formatter. * @type {number|string} * @private @constant */ this.originalPattern_ = pattern; this.patternParts_ = []; // Try to look up pattern in the DateTimePattern data. // If it is a standard value for the locale, then use the options // with native formatter if possible if (LocaleFeature.USE_ECMASCRIPT_I18N_DATETIMEF && ((typeof pattern == 'number'))) { // Use Intl DateTimeFormat class with standard predefined- patterns // Assumes no time zone settings this.applyStandardEnumNative_(pattern, false, null); } else { /** * Use polyfill implementation with data defining locale-specific data such * as (day/month names, most common patterns, rules for week-end, etc.) * @private {!goog.i18n.DateTimeSymbolsType} * @const */ this.dateTimeSymbols_ = /** @type {!goog.i18n.DateTimeSymbolsType} */ ( opt_dateTimeSymbols || goog.i18n.DateTimeSymbols); if (typeof pattern == 'number') { this.applyStandardPattern_(pattern); } else { // Pattern is a string. This requires the polyfill implementation. this.applyPattern_(pattern); } } }; /** * Enum to identify predefined Date/Time format pattern. The format pattern to * output mapping can be found at go/closure-localization#fconst. * @enum {number} */ goog.i18n.DateTimeFormat.Format = { FULL_DATE: 0, LONG_DATE: 1, MEDIUM_DATE: 2, SHORT_DATE: 3, FULL_TIME: 4, LONG_TIME: 5, MEDIUM_TIME: 6, SHORT_TIME: 7, FULL_DATETIME: 8, LONG_DATETIME: 9, MEDIUM_DATETIME: 10, SHORT_DATETIME: 11, WEEKDAY_MONTH_DAY_FULL: 12 // From FULL_DATE by removing year pattern }; /** * regular expression pattern for parsing pattern string * @type {!Array<!RegExp>} * @private @const */ goog.i18n.DateTimeFormat.TOKENS_ = [ // quote string /^\'(?:[^\']|\'\')*(\'|$)/, // pattern chars /^(?:G+|y+|Y+|M+|k+|S+|E+|a+|b+|B+|h+|K+|H+|c+|L+|Q+|d+|m+|s+|v+|V+|w+|z+|Z+)/, /^[^\'GyYMkSEabBhKHcLQdmsvVwzZ]+/ // and all the other chars ]; /** * These are token types, corresponding to above token definitions. * @enum {number} * @private */ goog.i18n.DateTimeFormat.PartTypes_ = { QUOTED_STRING: 0, FIELD: 1, LITERAL: 2 }; /** * @param {!goog.date.DateLike} date * @return {number} * @private */ goog.i18n.DateTimeFormat.getHours_ = function(date) { 'use strict'; return /** @type {?} */ (date).getHours ? /** @type {?} */ (date).getHours() : 0; }; /** * @param {!goog.date.DateLike} date * @return {number} * @private */ goog.i18n.DateTimeFormat.getMinutes_ = function(date) { 'use strict'; return /** @type {?} */ (date).getMinutes ? /** @type {?} */ (date).getMinutes() : 0; }; /** * Apply specified pattern to this formatter object. * @param {string} pattern String specifying how the date should be formatted. * @private */ goog.i18n.DateTimeFormat.prototype.applyPattern_ = function(pattern) { 'use strict'; if (goog.i18n.DateTimeFormat.removeRlmInPatterns_) { // Remove RLM unicode control character from pattern. pattern = pattern.replace(/\u200f/g, ''); } // lex the pattern, once for all uses while (pattern) { const previousPattern = pattern; for (let i = 0; i < goog.i18n.DateTimeFormat.TOKENS_.length; ++i) { const m = pattern.match(goog.i18n.DateTimeFormat.TOKENS_[i]); if (m) { let part = m[0]; pattern = pattern.substring(part.length); if (i == goog.i18n.DateTimeFormat.PartTypes_.QUOTED_STRING) { if (part == '\'\'') { part = '\''; // '' -> ' } else { part = part.substring( 1, m[1] == '\'' ? part.length - 1 : part.length); // strip quotes part = part.replace(/\'\'/g, '\''); } } this.patternParts_.push({text: part, type: i}); break; } } if (previousPattern === pattern) { // On every iteration, part of the pattern string must be consumed. throw new Error('Malformed pattern part: ' + pattern); } } }; /** * Format the given date object according to preset pattern and current locale. * @param {?goog.date.DateLike|undefined} date The Date object that is being * formatted. * @param {?goog.i18n.TimeZone=} opt_timeZone optional, if specified, time * related fields will be formatted based on its setting. When this field * is not specified, "undefined" will be pass around and those function * that really need time zone service will create a default one. * @return {string} Formatted string for the given date. * Throws an error if the date is null or if one tries to format a date-only * object (for instance goog.date.Date) using a pattern with time fields. */ goog.i18n.DateTimeFormat.prototype.format = function(date, opt_timeZone) { 'use strict'; if (!date) throw new Error('The date to format must be non-null.'); // We don't want to write code to calculate each date field because we // want to maximize performance and minimize code size. // JavaScript only provide API to render local time. // Suppose target date is: 16:00 GMT-0400 // OS local time is: 12:00 GMT-0800 // We want to create a Local Date Object : 16:00 GMT-0800, and fix the // time zone display ourselves. // Thing get a little bit tricky when daylight time transition happens. For // example, suppose OS timeZone is America/Los_Angeles, it is impossible to // represent "2006/4/2 02:30" even for those timeZone that has no transition // at this time. Because 2:00 to 3:00 on that day does not exist in // America/Los_Angeles time zone. To avoid calculating date field through // our own code, we uses 3 Date object instead, one for "Year, month, day", // one for time within that day, and one for timeZone object since it need // the real time to figure out actual time zone offset. if (this.intlFormatter_ && LocaleFeature.USE_ECMASCRIPT_I18N_DATETIMEF) { // Use Native ECMASCript formatting // Compare the date for type UTC and formatter's timeZone setting. let changedUtcSettings = false; // Is the new date/time based on UTC or local time? const isDateUtc = (date instanceof goog.date.UtcDateTime); const options = this.intlFormatter_.resolvedOptions(); if (isDateUtc) { changedUtcSettings = (options.timeZone !== 'UTC'); } else { changedUtcSettings = (options.timeZone === 'UTC'); } if (goog.i18n.DateTimeFormat.resetEnforceAsciiDigits_ || changedUtcSettings || opt_timeZone) { // Create new Intl DateTimeFormat object with reset values. this.applyStandardEnumNative_( this.originalPattern_, isDateUtc, opt_timeZone); goog.i18n.DateTimeFormat.resetEnforceAsciiDigits_ = false; } /** * @type {!Date|number|undefined} realdate type match for Closure */ const realdate = date ? new Date(date.valueOf()) : undefined; return this.intlFormatter_.format(realdate); } else { // Format using polyfill. let diff = opt_timeZone ? (date.getTimezoneOffset() - opt_timeZone.getOffset(date)) * 60000 : 0; let dateForDate = diff ? new Date(date.getTime() + diff) : date; let dateForTime = dateForDate; // When the time manipulation applied above spans the DST on/off hour, this // could alter the time incorrectly by adding or subtracting an additional // hour. // We can mitigate this by: // - Adding the difference in timezone offset to the date. This ensures that // the dateForDate is still within the right day if the extra DST hour // affected the date. // - Move the time one day forward if we applied a timezone offset // backwards, // or vice versa. This trick ensures that the time is in the same offset // as the original date, so we remove the additional hour added or // subtracted by the DST switch. if (opt_timeZone && dateForDate.getTimezoneOffset() != date.getTimezoneOffset()) { const dstDiff = (dateForDate.getTimezoneOffset() - date.getTimezoneOffset()) * 60000; dateForDate = new Date(dateForDate.getTime() + dstDiff); diff += diff > 0 ? -goog.date.MS_PER_DAY : goog.date.MS_PER_DAY; dateForTime = new Date(date.getTime() + diff); } const out = []; for (let i = 0; i < this.patternParts_.length; ++i) { const text = this.patternParts_[i].text; if (goog.i18n.DateTimeFormat.PartTypes_.FIELD == this.patternParts_[i].type) { out.push(this.formatField_( text, date, dateForDate, dateForTime, opt_timeZone)); } else { out.push(text); } } return out.join(''); } }; /** * Parameters to Intl.DateTimeFormat constructor * @private @typedef {{ * calendar: (string|undefined), * dateStyle: (string|undefined), * timeStyle: (string|undefined), * era: (string|undefined), * formatMatcher: (string|undefined), * localeMatcher: (string|undefined), * year: (string|undefined), * month: (string|undefined), * day: (string|undefined), * weekday: (string|undefined), * hour: (string|undefined), * hour12: (boolean|undefined), * minute: (string|undefined), * second: (string|undefined), * timeZone: (string|undefined), * numberingSystem: (string|undefined), * timeZoneName: (string|undefined), * }} */ goog.i18n.DateTimeFormat.IntlOptions; /** * Create an ECMAScript Intl.DateTimeFormat object based on * a predefined skeleton of fields and settings. * @param {number|string} formatType A number that identified the predefined * pattern. * @param {boolean} isUtc Should values be fixed in UTC? * @param {?goog.i18n.TimeZone=} opt_timeZone explicit set time zone * @private */ goog.i18n.DateTimeFormat.prototype.applyStandardEnumNative_ = function( formatType, isUtc, opt_timeZone) { /** @type {!goog.i18n.DateTimeFormat.IntlOptions} */ const options = {calendar: 'gregory'}; // Only Gregorian calendar // When time zone is explicitly given if (isUtc) { options.timeZone = 'UTC'; } else if (opt_timeZone) { options.timeZone = opt_timeZone.getTimeZoneId(); } switch (formatType) { // DATEFORMATS case goog.i18n.DateTimeFormat.Format.FULL_DATE: options.dateStyle = 'full'; break; case goog.i18n.DateTimeFormat.Format.LONG_DATE: options.dateStyle = 'long'; break; case goog.i18n.DateTimeFormat.Format.MEDIUM_DATE: options.dateStyle = 'medium'; break; case goog.i18n.DateTimeFormat.Format.SHORT_DATE: default: options.dateStyle = 'short'; break; // TIMEFORMATS case goog.i18n.DateTimeFormat.Format.FULL_TIME: options.timeStyle = 'full'; break; case goog.i18n.DateTimeFormat.Format.LONG_TIME: options.timeStyle = 'long'; break; case goog.i18n.DateTimeFormat.Format.MEDIUM_TIME: options.timeStyle = 'medium'; break; case goog.i18n.DateTimeFormat.Format.SHORT_TIME: options.timeStyle = 'short'; break; // DATETIMEFORMATS case goog.i18n.DateTimeFormat.Format.FULL_DATETIME: options.dateStyle = 'full'; options.timeStyle = 'full'; // Can we modify how timezone is presented? // if (opt_timeZone) { // options.timeZoneName = 'long'; // } else { // options.timeZoneName = 'short'; // } break; case goog.i18n.DateTimeFormat.Format.LONG_DATETIME: options.dateStyle = 'long'; options.timeStyle = 'long'; break; case goog.i18n.DateTimeFormat.Format.MEDIUM_DATETIME: options.dateStyle = 'medium'; options.timeStyle = 'medium'; break; case goog.i18n.DateTimeFormat.Format.SHORT_DATETIME: options.dateStyle = 'short'; options.timeStyle = 'short'; break; case goog.i18n.DateTimeFormat.Format.WEEKDAY_MONTH_DAY_FULL: options.weekday = 'long'; options.month = 'long'; options.day = 'numeric'; break; } // Intl requires '-' instead of '_'. let fixedLocale = goog.LOCALE.replace(/_/g, '-'); if (!goog.LOCALE) { fixedLocale = 'en'; // The default } if (goog.i18n.DateTimeFormat.enforceAsciiDigits_) { options.numberingSystem = 'latn'; } else { if (fixedLocale in NativeLocaleDigits.FormatWithLocaleDigits) { options.numberingSystem = NativeLocaleDigits.FormatWithLocaleDigits[fixedLocale]; } } try { this.intlFormatter_ = new goog.global.Intl.DateTimeFormat(fixedLocale, options); } catch (e) { goog.asserts.assert(e != null); } }; /** * Apply a predefined pattern as identified by formatType, which is stored in * locale specific repository. * @param {number} formatType A number that identified the predefined pattern. * @private */ goog.i18n.DateTimeFormat.prototype.applyStandardPattern_ = function( formatType) { 'use strict'; let pattern; if (formatType < 4) { pattern = this.dateTimeSymbols_.DATEFORMATS[formatType]; } else if (formatType < 8) { pattern = this.dateTimeSymbols_.TIMEFORMATS[formatType - 4]; } else if (formatType < 12) { pattern = this.dateTimeSymbols_.DATETIMEFORMATS[formatType - 8]; pattern = pattern.replace( '{1}', this.dateTimeSymbols_.DATEFORMATS[formatType - 8]); pattern = pattern.replace( '{0}', this.dateTimeSymbols_.TIMEFORMATS[formatType - 8]); } else if ( formatType === goog.i18n.DateTimeFormat.Format.WEEKDAY_MONTH_DAY_FULL) { // WEEKDAY_MONTH_DAY_FULL is derived from FULL_DATE removing year patterns pattern = this.removeYearFormatFromPattern_(this.dateTimeSymbols_.DATEFORMATS[0]); } else { // Default this.applyStandardPattern_(goog.i18n.DateTimeFormat.Format.MEDIUM_DATETIME); return; } this.applyPattern_(pattern); }; /** * Localizes a string potentially containing numbers, replacing ASCII digits * with native digits if specified so by the locale. Leaves other characters. * @param {string} input the string to be localized, using ASCII digits. * @return {string} localized string, potentially using native digits. * @private */ goog.i18n.DateTimeFormat.prototype.localizeNumbers_ = function(input) { 'use strict'; return goog.i18n.DateTimeFormat.localizeNumbers(input, this.dateTimeSymbols_); }; /** * If the usage of Ascii digits should be enforced regardless of locale. * @type {boolean} * @private */ goog.i18n.DateTimeFormat.enforceAsciiDigits_ = false; /** * Records if ASCII digits was set after formatter construction. * @type {boolean} * @private */ goog.i18n.DateTimeFormat.resetEnforceAsciiDigits_ = false; /** * If RLM unicode characters should be removed from date/time patterns (useful * when enforcing ASCII digits for Arabic). See `#setEnforceAsciiDigits`. * @type {boolean} * @private */ goog.i18n.DateTimeFormat.removeRlmInPatterns_ = false; /** * Sets if the usage of Ascii digits in formatting should be enforced in * formatted date/time even for locales where native digits are indicated. * Also sets whether to remove RLM unicode control characters when using * standard enumerated patterns (they exist e.g. in standard d/M/y for Arabic). * Production code should call this once before any `DateTimeFormat` * object is instantiated. * Caveats: * * Enforcing ASCII digits affects all future formatting by new or existing * `DateTimeFormat` objects. * * Removal of RLM characters only applies to `DateTimeFormat` objects * instantiated after this call. * @param {boolean} enforceAsciiDigits Whether Ascii digits should be enforced. */ goog.i18n.DateTimeFormat.setEnforceAsciiDigits = function(enforceAsciiDigits) { 'use strict'; if (goog.i18n.DateTimeFormat.enforceAsciiDigits_ !== enforceAsciiDigits) { goog.i18n.DateTimeFormat.enforceAsciiDigits_ = enforceAsciiDigits; // And remember for resetting native formatter. goog.i18n.DateTimeFormat.resetEnforceAsciiDigits_ = true; } // Also setting removal of RLM chracters when forcing ASCII digits since it's // the right thing to do for Arabic standard patterns. One could add an // optional argument here or to the `DateTimeFormat` constructor to // enable an alternative behavior. goog.i18n.DateTimeFormat.removeRlmInPatterns_ = enforceAsciiDigits; }; /** * @return {boolean} Whether enforcing ASCII digits for all locales. See * `#setEnforceAsciiDigits` for more details. */ goog.i18n.DateTimeFormat.isEnforceAsciiDigits = function() { 'use strict'; return goog.i18n.DateTimeFormat.enforceAsciiDigits_; }; /** * Localizes a string potentially containing numbers, replacing ASCII digits * with native digits if specified so by the locale. Leaves other characters. * @param {number|string} input the string to be localized, using ASCII digits. * @param {!Object=} opt_dateTimeSymbols Optional symbols to use rather than * the global symbols. * @return {string} localized string, potentially using native digits. * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration */ goog.i18n.DateTimeFormat.localizeNumbers = function( input, opt_dateTimeSymbols) { 'use strict'; input = String(input); const dateTimeSymbols = opt_dateTimeSymbols || goog.i18n.DateTimeSymbols; if (dateTimeSymbols.ZERODIGIT === undefined || goog.i18n.DateTimeFormat.enforceAsciiDigits_) { return input; } const parts = []; for (let i = 0; i < input.length; i++) { const c = input.charCodeAt(i); parts.push( (0x30 <= c && c <= 0x39) ? // '0' <= c <= '9' String.fromCharCode(dateTimeSymbols.ZERODIGIT + c - 0x30) : input.charAt(i)); } return parts.join(''); }; /** * Formats Era field according to pattern specified. * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatEra_ = function(count, date) { 'use strict'; const value = date.getFullYear() > 0 ? 1 : 0; return count >= 4 ? this.dateTimeSymbols_.ERANAMES[value] : this.dateTimeSymbols_.ERAS[value]; }; /** * Formats Year field according to pattern specified * JavaScript Date object seems incapable handling 1BC and * year before. It can show you year 0 which does not exists. * following we just keep consistent with javascript's * toString method. But keep in mind those things should be * unsupported. * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatYear_ = function(count, date) { 'use strict'; let value = date.getFullYear(); if (value < 0) { value = -value; } if (count == 2) { // See comment about special casing 'yy' at the start of the file, this // matches ICU and CLDR behaviour. See also: // http://icu-project.org/apiref/icu4j/com/ibm/icu/text/SimpleDateFormat.html // http://www.unicode.org/reports/tr35/tr35-dates.html value = value % 100; } return this.localizeNumbers_(goog.string.padNumber(value, count)); }; /** * Formats Year (Week of Year) field according to pattern specified * JavaScript Date object seems incapable handling 1BC and * year before. It can show you year 0 which does not exists. * following we just keep consistent with javascript's * toString method. But keep in mind those things should be * unsupported. * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatYearOfWeek_ = function(count, date) { 'use strict'; let value = goog.date.getYearOfWeek( date.getFullYear(), date.getMonth(), date.getDate(), this.dateTimeSymbols_.FIRSTWEEKCUTOFFDAY, this.dateTimeSymbols_.FIRSTDAYOFWEEK); if (value < 0) { value = -value; } if (count == 2) { // See comment about special casing 'yy' at the start of the file, this // matches ICU and CLDR behaviour. See also: // http://icu-project.org/apiref/icu4j/com/ibm/icu/text/SimpleDateFormat.html // http://www.unicode.org/reports/tr35/tr35-dates.html value = value % 100; } return this.localizeNumbers_(goog.string.padNumber(value, count)); }; /** * Formats Month field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatMonth_ = function(count, date) { 'use strict'; const value = date.getMonth(); switch (count) { case 5: return this.dateTimeSymbols_.NARROWMONTHS[value]; case 4: return this.dateTimeSymbols_.MONTHS[value]; case 3: return this.dateTimeSymbols_.SHORTMONTHS[value]; default: return this.localizeNumbers_(goog.string.padNumber(value + 1, count)); } }; /** * Validates is the goog.date.DateLike object to format has a time. * DateLike means Date|goog.date.Date, and goog.date.DateTime inherits * from goog.date.Date. But goog.date.Date does not have time related * members (getHours, getMinutes, getSeconds). * Formatting can be done, if there are no time placeholders in the pattern. * * @param {!goog.date.DateLike} date the object to validate. * @private */ goog.i18n.DateTimeFormat.validateDateHasTime_ = function(date) { 'use strict'; let maybeHasTime = /** @type {?} */ (date); if (maybeHasTime.getHours && maybeHasTime.getSeconds && maybeHasTime.getMinutes) { return; } // if (date instanceof Date || date instanceof goog.date.DateTime) throw new Error( 'The date to format has no time (probably a goog.date.Date). ' + 'Use Date or goog.date.DateTime, or use a pattern without time fields.'); }; /** * Formats (1..24) Hours field according to pattern specified * * @param {number} count Number of time pattern char repeats. This controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.format24Hours_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date) || 24; return this.localizeNumbers_(goog.string.padNumber(hours, count)); }; /** * Formats Fractional seconds field according to pattern * specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatFractionalSeconds_ = function( count, date) { 'use strict'; // Fractional seconds left-justify, append 0 for precision beyond 3 const value = /** @type {!Date|!goog.date.DateTime} */ (date).getMilliseconds() / 1000; return this.localizeNumbers_( value.toFixed(Math.min(3, count)).slice(2) + (count > 3 ? goog.string.padNumber(0, count - 3) : '')); }; /** * Formats Day of week field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatDayOfWeek_ = function(count, date) { 'use strict'; const value = date.getDay(); return count >= 4 ? this.dateTimeSymbols_.WEEKDAYS[value] : this.dateTimeSymbols_.SHORTWEEKDAYS[value]; }; /** * Formats Am/Pm field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatAmPm_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date); // Must implement this with fallback if no data is found. return this.dateTimeSymbols_.AMPMS[hours >= 12 && hours < 24 ? 1 : 0]; }; /** * Formats am/pm/noon/midnight field according to pattern specified with 'b' * Handle noon and midnight if dayPeriod has data. * Otherwise, fallback to AM/PM for the locale. * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatAmPmNoonMidnight_ = function( count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date); const minutes = goog.i18n.DateTimeFormat.getMinutes_(date); /** {?goog.i18n.DayPeriods} */ const dayPeriods = goog.i18n.DayPeriods.getDayPeriods(); if (dayPeriods && minutes === 0) { // Check for noon & midnight data. if (dayPeriods.midnight && hours == 0) { return dayPeriods.midnight.formatNames[0]; } else if (dayPeriods.noon && hours === 12) { return dayPeriods.noon.formatNames[0]; } } // Must implement this with fallback if no data is found. return this.dateTimeSymbols_.AMPMS[hours >= 12 && hours < 24 ? 1 : 0]; }; /** * Formats flexible day periods according to pattern specified with 'B'. * Return string for flexible day period when data is available. * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatFlexibleDayPeriods_ = function( count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date); const minutes = goog.i18n.DateTimeFormat.getMinutes_(date); // String in HH:MM format for comparing. const fmtTime = hours.toString(10).padStart(2, '0') + ':' + minutes.toString().padStart(2, '0'); let period; /** {?goog.i18n.DayPeriods} */ const dayPeriods = goog.i18n.DayPeriods.getDayPeriods(); if (dayPeriods) { // Match time to ranges in DayPeriods to give the particular range. const keys = Object.keys(dayPeriods); for (let index = 0; index < keys.length; index++) { let testPeriod = dayPeriods[keys[index]]; if (fmtTime === testPeriod.at) { period = keys[index]; // A particular time. break; } // Check if the period straddles midnight if (testPeriod.before > testPeriod.from) { if (fmtTime >= testPeriod.from && fmtTime < testPeriod.before) { period = keys[index]; // A particular time. } } else { // Check before and after 00:00. // Two tests needed if (fmtTime >= testPeriod.from && fmtTime < '24:00' || fmtTime >= '00:00' && fmtTime < testPeriod.before) { period = keys[index]; // A particular time. break; } } } if (period) { // Get string for the period return dayPeriods[period].formatNames[0]; // Pick first style } } // Fall back to 'a' when no data is defined. return this.dateTimeSymbols_.AMPMS[hours >= 12 && hours < 24 ? 1 : 0]; }; /** * Formats (1..12) Hours field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.format1To12Hours_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date) % 12 || 12; return this.localizeNumbers_(goog.string.padNumber(hours, count)); }; /** * Formats (0..11) Hours field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.format0To11Hours_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date) % 12; return this.localizeNumbers_(goog.string.padNumber(hours, count)); }; /** * Formats (0..23) Hours field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.format0To23Hours_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); const hours = goog.i18n.DateTimeFormat.getHours_(date); return this.localizeNumbers_(goog.string.padNumber(hours, count)); }; /** * Formats Standalone weekday field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatStandaloneDay_ = function( count, date) { 'use strict'; const value = date.getDay(); switch (count) { case 5: return this.dateTimeSymbols_.STANDALONENARROWWEEKDAYS[value]; case 4: return this.dateTimeSymbols_.STANDALONEWEEKDAYS[value]; case 3: return this.dateTimeSymbols_.STANDALONESHORTWEEKDAYS[value]; default: return this.localizeNumbers_(goog.string.padNumber(value, 1)); } }; /** * Formats Standalone Month field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatStandaloneMonth_ = function( count, date) { 'use strict'; const value = date.getMonth(); switch (count) { case 5: return this.dateTimeSymbols_.STANDALONENARROWMONTHS[value]; case 4: return this.dateTimeSymbols_.STANDALONEMONTHS[value]; case 3: return this.dateTimeSymbols_.STANDALONESHORTMONTHS[value]; default: return this.localizeNumbers_(goog.string.padNumber(value + 1, count)); } }; /** * Formats Quarter field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatQuarter_ = function(count, date) { 'use strict'; const value = Math.floor(date.getMonth() / 3); return count < 4 ? this.dateTimeSymbols_.SHORTQUARTERS[value] : this.dateTimeSymbols_.QUARTERS[value]; }; /** * Formats Date field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatDate_ = function(count, date) { 'use strict'; return this.localizeNumbers_(goog.string.padNumber(date.getDate(), count)); }; /** * Formats Minutes field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatMinutes_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); return this.localizeNumbers_( goog.string.padNumber(goog.i18n.DateTimeFormat.getMinutes_(date), count)); }; /** * Formats Seconds field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatSeconds_ = function(count, date) { 'use strict'; goog.i18n.DateTimeFormat.validateDateHasTime_(date); return this.localizeNumbers_(goog.string.padNumber( /** @type {!goog.date.DateTime} */ (date).getSeconds(), count)); }; /** * Formats the week of year field according to pattern specified * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatWeekOfYear_ = function(count, date) { 'use strict'; const weekNum = goog.date.getWeekNumber( date.getFullYear(), date.getMonth(), date.getDate(), this.dateTimeSymbols_.FIRSTWEEKCUTOFFDAY, this.dateTimeSymbols_.FIRSTDAYOFWEEK); return this.localizeNumbers_(goog.string.padNumber(weekNum, count)); }; /** * Formats TimeZone field following RFC * * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date It holds the date object to be formatted. * @param {?goog.i18n.TimeZone=} opt_timeZone This holds current time zone info. * @return {string} Formatted string that represent this field. * @private */ goog.i18n.DateTimeFormat.prototype.formatTimeZoneRFC_ = function( count, date, opt_timeZone) { 'use strict'; opt_timeZone = opt_timeZone || goog.i18n.TimeZone.createTimeZone(date.getTimezoneOffset()); // RFC 822 formats should be kept in ASCII, but localized GMT formats may need // to use native digits. return count < 4 ? opt_timeZone.getRFCTimeZoneString(date) : this.localizeNumbers_(opt_timeZone.getGMTString(date)); }; /** * Generate GMT timeZone string for given date * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date Whose value being evaluated. * @param {?goog.i18n.TimeZone=} opt_timeZone This holds current time zone info. * @return {string} GMT timeZone string. * @private */ goog.i18n.DateTimeFormat.prototype.formatTimeZone_ = function( count, date, opt_timeZone) { 'use strict'; opt_timeZone = opt_timeZone || goog.i18n.TimeZone.createTimeZone(date.getTimezoneOffset()); return count < 4 ? opt_timeZone.getShortName(date) : opt_timeZone.getLongName(date); }; /** * Generate GMT timeZone string for given date * @param {!goog.date.DateLike} date Whose value being evaluated. * @param {?goog.i18n.TimeZone=} opt_timeZone This holds current time zone info. * @return {string} GMT timeZone string. * @private */ goog.i18n.DateTimeFormat.prototype.formatTimeZoneId_ = function( date, opt_timeZone) { 'use strict'; opt_timeZone = opt_timeZone || goog.i18n.TimeZone.createTimeZone(date.getTimezoneOffset()); return opt_timeZone.getTimeZoneId(); }; /** * Generate localized, location dependent time zone id * @param {number} count Number of time pattern char repeats, it controls * how a field should be formatted. * @param {!goog.date.DateLike} date Whose value being evaluated. * @param {?goog.i18n.TimeZone=} opt_timeZone This holds current time zone info. * @return {string} GMT timeZone string. * @private */ goog.i18n.DateTimeFormat.prototype.formatTimeZoneLocationId_ = function( count, date, opt_timeZone) { 'use strict'; opt_timeZone = opt_timeZone || goog.i18n.TimeZone.createTimeZone(date.getTimezoneOffset()); return count <= 2 ? opt_timeZone.getTimeZoneId() : opt_timeZone.getGenericLocation(date); }; /** * Formatting one date field. * @param {string} patternStr The pattern string for the field being formatted. * @param {!goog.date.DateLike} date represents the real date to be formatted. * @param {!goog.date.DateLike} dateForDate used to resolve date fields * for formatting. * @param {!goog.date.DateLike} dateForTime used to resolve time fields * for formatting. * @param {?goog.i18n.TimeZone=} opt_timeZone This holds current time zone info. * @return {string} string representation for the given field. * @private */ goog.i18n.DateTimeFormat.prototype.formatField_ = function( patternStr, date, dateForDate, dateForTime, opt_timeZone) { 'use strict'; const count = patternStr.length; /** {?goog.i18n.DayPeriods} */ const dayPeriods = goog.i18n.DayPeriods.getDayPeriods(); switch (patternStr.charAt(0)) { case 'G': return this.formatEra_(count, dateForDate); case 'y': return this.formatYear_(count, dateForDate); case 'Y': return this.formatYearOfWeek_(count, dateForDate); case 'M': return this.formatMonth_(count, dateForDate); case 'k': return this.format24Hours_(count, dateForTime); case 'S': return this.formatFractionalSeconds_(count, dateForTime); case 'E': return this.formatDayOfWeek_(count, dateForDate); case 'a': return this.formatAmPm_(count, dateForTime); case 'b': if (dayPeriods) { return this.formatAmPmNoonMidnight_(count, dateForTime); } else { return this.formatAmPm_(count, dateForTime); } case 'B': if (dayPeriods) { return this.formatFlexibleDayPeriods_(count, dateForTime); } else { return this.formatAmPm_(count, dateForTime); } case 'h': return this.format1To12Hours_(count, dateForTime); case 'K': return this.format0To11Hours_(count, dateForTime); case 'H': return this.format0To23Hours_(count, dateForTime); case 'c': return this.formatStandaloneDay_(count, dateForDate); case 'L': return this.formatStandaloneMonth_(count, dateForDate); case 'Q': return this.formatQuarter_(count, dateForDate); case 'd': return this.formatDate_(count, dateForDate); case 'm': return this.formatMinutes_(count, dateForTime); case 's': return this.formatSeconds_(count, dateForTime); case 'v': return this.formatTimeZoneId_(date, opt_timeZone); case 'V': return this.formatTimeZoneLocationId_(count, date, opt_timeZone); case 'w': return this.formatWeekOfYear_(count, dateForTime); case 'z': return this.formatTimeZone_(count, date, opt_timeZone); case 'Z': return this.formatTimeZoneRFC_(count, date, opt_timeZone); default: return ''; } }; /** * Removes year formatting from full date pattern for implementing * special case for WEEKDAY_MONTH_DAY_FULL, deriving from * FULL date pattern. * @param {string} patternStr The pattern string for the field being formatted. * @return {string} Modified pattern string without year field. * @private */ goog.i18n.DateTimeFormat.prototype.removeYearFormatFromPattern_ = function( patternStr) { // Remove all around the year except E*, d*, M*, e.g., space, punctuation const yearPattern = /[^EMd]*yy*[^EMd]*/; return patternStr.replace(yearPattern, ''); }; }); // End of scope for module data