Path: blob/trunk/third_party/closure/goog/i18n/dateintervalformat.js
2868 views
// Copyright 2017 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview DateIntervalFormat provides methods to format a date interval16* into a string in a user friendly way and a locale sensitive manner.17*18* Similar to the ICU4J class com/ibm/icu/text/DateIntervalFormat:19* http://icu-project.org/apiref/icu4j/com/ibm/icu/text/DateIntervalFormat.html20*21* Example usage:22* var DateIntervalFormat = goog.require('goog.i18n.DateIntervalFormat');23* var DateRange = goog.require('goog.date.DateRange');24* var DateTime = goog.require('goog.date.DateTime');25* var DateTimeFormat = goog.require('goog.i18n.DateTimeFormat');26* var GDate = goog.require('goog.date.Date');27* var Interval = goog.require('goog.date.Interval');28*29* // Formatter.30* var dtIntFmt = new DateIntervalFormat(DateTimeFormat.Format.MEDIUM_DATE);31*32* // Format a date range.33* var dt1 = new GDate(2016, 8, 23);34* var dt2 = new GDate(2016, 8, 24);35* var dtRng = new DateRange(dt1, dt2);36* dtIntFmt.formatRange(dtRng); // --> 'Sep 23 – 24, 2016'37*38* // Format two dates.39* var dt3 = new DateTime(2016, 8, 23, 14, 53, 0);40* var dt4 = new DateTime(2016, 8, 23, 14, 54, 0);41* dtIntFmt.format(dt3, dt4); // --> 'Sep 23, 2016'42*43* // Format a date and an interval.44* var dt5 = new DateTime(2016, 8, 23, 14, 53, 0);45* var itv = new Interval(0, 1); // One month.46* dtIntFmt.format(dt5, itv); // --> 'Sep 23 – Oct 23, 2016'47*48*/4950goog.module('goog.i18n.DateIntervalFormat');5152var DateLike = goog.require('goog.date.DateLike');53var DateRange = goog.require('goog.date.DateRange');54var DateTime = goog.require('goog.date.DateTime');55var DateTimeFormat = goog.require('goog.i18n.DateTimeFormat');56var DateTimeSymbols = goog.require('goog.i18n.DateTimeSymbols');57var DateTimeSymbolsType = goog.require('goog.i18n.DateTimeSymbolsType');58var Interval = goog.require('goog.date.Interval');59var TimeZone = goog.require('goog.i18n.TimeZone');60var array = goog.require('goog.array');61var asserts = goog.require('goog.asserts');62var dateIntervalSymbols = goog.require('goog.i18n.dateIntervalSymbols');63var object = goog.require('goog.object');6465/**66* Constructs a DateIntervalFormat object based on the current locale.67*68* @param {number|!dateIntervalSymbols.DateIntervalPatternMap} pattern Pattern69* specification or pattern object.70* @param {!dateIntervalSymbols.DateIntervalSymbols=} opt_dateIntervalSymbols71* Optional DateIntervalSymbols to use for this instance rather than the72* global symbols.73* @param {!DateTimeSymbolsType=} opt_dateTimeSymbols Optional DateTimeSymbols74* to use for this instance rather than the global symbols.75* @constructor76* @struct77* @final78*/79var DateIntervalFormat = function(80pattern, opt_dateIntervalSymbols, opt_dateTimeSymbols) {81asserts.assert(goog.isDef(pattern), 'Pattern must be defined.');82asserts.assert(83goog.isDef(opt_dateIntervalSymbols) ||84goog.isDef(dateIntervalSymbols.getDateIntervalSymbols()),85'goog.i18n.DateIntervalSymbols or explicit symbols must be defined');86asserts.assert(87goog.isDef(opt_dateTimeSymbols) || goog.isDef(DateTimeSymbols),88'goog.i18n.DateTimeSymbols or explicit symbols must be defined');8990/**91* DateIntervalSymbols object that contains locale data required by the92* formatter.93* @private @const {!dateIntervalSymbols.DateIntervalSymbols}94*/95this.dateIntervalSymbols_ =96opt_dateIntervalSymbols || dateIntervalSymbols.getDateIntervalSymbols();9798/**99* DateTimeSymbols object that contain locale data required by the formatter.100* @private @const {!DateTimeSymbolsType}101*/102this.dateTimeSymbols_ = opt_dateTimeSymbols || DateTimeSymbols;103104/**105* Date interval pattern to use.106* @private @const {!dateIntervalSymbols.DateIntervalPatternMap}107*/108this.intervalPattern_ = this.getIntervalPattern_(pattern);109110/**111* Keys of the available date interval patterns. Used to lookup the key that112* contains a specific pattern letter (e.g. for ['Myd', 'hms'], the key that113* contains 'y' is 'Myd').114* @private @const {!Array<string>}115*/116this.intervalPatternKeys_ = object.getKeys(this.intervalPattern_);117118// Remove the default pattern's key ('_') from intervalPatternKeys_. Is not119// necesary when looking up for a key: when no key is found it will always120// default to the default pattern.121array.remove(this.intervalPatternKeys_, DEFAULT_PATTERN_KEY_);122123/**124* Default fallback pattern to use.125* @private @const {string}126*/127this.fallbackPattern_ =128this.dateIntervalSymbols_.FALLBACK || DEFAULT_FALLBACK_PATTERN_;129130// Determine which date should be used with each part of the interval131// pattern.132var indexOfFirstDate = this.fallbackPattern_.indexOf(FIRST_DATE_PLACEHOLDER_);133var indexOfSecondDate =134this.fallbackPattern_.indexOf(SECOND_DATE_PLACEHOLDER_);135if (indexOfFirstDate < 0 || indexOfSecondDate < 0) {136throw new Error('Malformed fallback interval pattern');137}138139/**140* True if the first date provided should be formatted with the first pattern141* of the interval pattern.142* @private @const {boolean}143*/144this.useFirstDateOnFirstPattern_ = indexOfFirstDate <= indexOfSecondDate;145146/**147* Map that stores a Formatter_ object per calendar field. Formatters will be148* instanced on demand and stored on this map until required again.149* @private @const {!Object<string, !Formatter_>}150*/151this.formatterMap_ = {};152};153154/**155* Default fallback interval pattern.156* @private @const {string}157*/158var DEFAULT_FALLBACK_PATTERN_ = '{0} – {1}';159160/**161* Interval pattern placeholder for the first date.162* @private @const {string}163*/164var FIRST_DATE_PLACEHOLDER_ = '{0}';165166/**167* Interval pattern placeholder for the second date.168* @private @const {string}169*/170var SECOND_DATE_PLACEHOLDER_ = '{1}';171172/**173* Key used by the default datetime pattern.174* @private @const {string}175*/176var DEFAULT_PATTERN_KEY_ = '_';177178/**179* Gregorian calendar Eras.180* @private @enum {number}181*/182var Era_ = {BC: 0, AD: 1};183184/**185* Am Pm markers.186* @private @enum {number}187*/188var AmPm_ = {AM: 0, PM: 1};189190/**191* String of all pattern letters representing the relevant calendar fields.192* Sorted according to the length of the datetime unit they represent.193* @private @const {string}194*/195var RELEVANT_CALENDAR_FIELDS_ = 'GyMdahms';196197/**198* Regex that matches all possible pattern letters.199* @private @const {!RegExp}200*/201var ALL_PATTERN_LETTERS_ = /[a-zA-Z]/;202203/**204* Returns the interval pattern from a pattern specification or from the pattern205* object.206* @param {number|!dateIntervalSymbols.DateIntervalPatternMap} pattern Pattern207* specification or pattern object.208* @return {!dateIntervalSymbols.DateIntervalPatternMap}209* @private210*/211DateIntervalFormat.prototype.getIntervalPattern_ = function(pattern) {212if (goog.isNumber(pattern)) {213switch (pattern) {214case DateTimeFormat.Format.FULL_DATE:215return this.dateIntervalSymbols_.FULL_DATE;216case DateTimeFormat.Format.LONG_DATE:217return this.dateIntervalSymbols_.LONG_DATE;218case DateTimeFormat.Format.MEDIUM_DATE:219return this.dateIntervalSymbols_.MEDIUM_DATE;220case DateTimeFormat.Format.SHORT_DATE:221return this.dateIntervalSymbols_.SHORT_DATE;222case DateTimeFormat.Format.FULL_TIME:223return this.dateIntervalSymbols_.FULL_TIME;224case DateTimeFormat.Format.LONG_TIME:225return this.dateIntervalSymbols_.LONG_TIME;226case DateTimeFormat.Format.MEDIUM_TIME:227return this.dateIntervalSymbols_.MEDIUM_TIME;228case DateTimeFormat.Format.SHORT_TIME:229return this.dateIntervalSymbols_.SHORT_TIME;230case DateTimeFormat.Format.FULL_DATETIME:231return this.dateIntervalSymbols_.FULL_DATETIME;232case DateTimeFormat.Format.LONG_DATETIME:233return this.dateIntervalSymbols_.LONG_DATETIME;234case DateTimeFormat.Format.MEDIUM_DATETIME:235return this.dateIntervalSymbols_.MEDIUM_DATETIME;236case DateTimeFormat.Format.SHORT_DATETIME:237return this.dateIntervalSymbols_.SHORT_DATETIME;238default:239return this.dateIntervalSymbols_.MEDIUM_DATETIME;240}241} else {242return pattern;243}244};245246/**247* Formats the given date or date interval objects according to the present248* pattern and current locale.249*250* Parameter combinations:251* * StartDate: {@link goog.date.DateLike}, EndDate: {@link goog.date.DateLike}252* * StartDate: {@link goog.date.DateLike}, Interval: {@link goog.date.Interval}253*254* @param {!DateLike} startDate Start date of the date range.255* @param {!DateLike|!Interval} endDate End date of the date range or an256* interval object.257* @param {!TimeZone=} opt_timeZone Timezone to be used in the target258* representation.259* @return {string} Formatted date interval.260*/261DateIntervalFormat.prototype.format = function(262startDate, endDate, opt_timeZone) {263asserts.assert(264startDate != null,265'The startDate parameter should be defined and not-null.');266asserts.assert(267endDate != null, 'The endDate parameter should be defined and not-null.');268269// Convert input to DateLike.270var endDt;271if (goog.isDateLike(endDate)) {272endDt = /** @type {!DateLike} */ (endDate);273} else {274asserts.assertInstanceof(275endDate, Interval,276'endDate parameter should be a goog.date.DateLike or ' +277'goog.date.Interval');278endDt = new DateTime(startDate);279endDt.add(endDate);280}281282// Obtain the largest different calendar field between the two dates.283var largestDifferentCalendarField =284DateIntervalFormat.getLargestDifferentCalendarField_(285startDate, endDt, opt_timeZone);286287// Get the Formatter_ required to format the specified calendar field and use288// it to format the dates.289var formatter =290this.getFormatterForCalendarField_(largestDifferentCalendarField);291return formatter.format(292startDate, endDt, largestDifferentCalendarField, opt_timeZone);293};294295/**296* Formats the given date range object according to the present pattern and297* current locale.298*299* @param {!DateRange} dateRange300* @param {!TimeZone=} opt_timeZone Timezone to be used in the target301* representation.302* @return {string} Formatted date interval.303*/304DateIntervalFormat.prototype.formatRange = function(dateRange, opt_timeZone) {305asserts.assert(306dateRange != null,307'The dateRange parameter should be defined and non-null.');308var startDate = dateRange.getStartDate();309var endDate = dateRange.getEndDate();310if (startDate == null) {311throw Error('The dateRange\'s startDate should be defined and non-null.');312}313if (endDate == null) {314throw Error('The dateRange\'s endDate should be defined and non-null.');315}316return this.format(startDate, endDate, opt_timeZone);317};318319/**320* Returns the Formatter_ to be used to format two dates for the given calendar321* field.322* @param {string} calendarField Pattern letter representing the calendar field.323* @return {!Formatter_}324* @private325*/326DateIntervalFormat.prototype.getFormatterForCalendarField_ = function(327calendarField) {328if (calendarField != '') {329for (var i = 0; i < this.intervalPatternKeys_.length; i++) {330if (this.intervalPatternKeys_[i].indexOf(calendarField) >= 0) {331return this.getOrCreateFormatterForKey_(this.intervalPatternKeys_[i]);332}333}334}335return this.getOrCreateFormatterForKey_(DEFAULT_PATTERN_KEY_);336};337338/**339* Returns and creates (if necessary) a formatter for the specified key.340* @param {string} key341* @return {!Formatter_}342* @private343*/344DateIntervalFormat.prototype.getOrCreateFormatterForKey_ = function(key) {345var fmt = this;346return object.setWithReturnValueIfNotSet(this.formatterMap_, key, function() {347var patternParts =348DateIntervalFormat.divideIntervalPattern_(fmt.intervalPattern_[key]);349if (patternParts === null) {350return new DateTimeFormatter_(351fmt.intervalPattern_[key], fmt.fallbackPattern_,352fmt.dateTimeSymbols_);353}354return new IntervalFormatter_(355patternParts.firstPart, patternParts.secondPart, fmt.dateTimeSymbols_,356fmt.useFirstDateOnFirstPattern_);357});358};359360/**361* Divides the interval pattern string into its two parts. Will return null if362* the pattern can't be divided (e.g. it's a datetime pattern).363* @param {string} intervalPattern364* @return {?{firstPart:string, secondPart:string}} Record containing the two365* parts of the interval pattern. Null if the pattern can't be divided.366* @private367*/368DateIntervalFormat.divideIntervalPattern_ = function(intervalPattern) {369var foundKeys = {};370var patternParts = null;371// Iterate over the pattern until a repeated calendar field is found.372DateIntervalFormat.executeForEveryCalendarField_(373intervalPattern, function(char, index) {374if (object.containsKey(foundKeys, char)) {375patternParts = {376firstPart: intervalPattern.substring(0, index),377secondPart: intervalPattern.substring(index)378};379return false;380}381object.set(foundKeys, char, true);382return true;383});384385return patternParts;386};387388/**389* Iterates over a pattern string and executes a function for every390* calendar field. The function will be executed once, independent of the width391* of the calendar field (number of repeated pattern letters). It will ignore392* all literal text (enclosed by quotes).393*394* For example, on: "H 'h' mm – H 'h' mm" it will call the function for:395* H (pos:0), m (pos:6), H (pos:11), m (pos:17).396*397* @param {string} pattern398* @param {function(string, number):boolean} func Function which accepts as399* parameters the current calendar field and the index of its first pattern400* letter; and returns a boolean which indicates if the iteration should401* continue.402* @private403*/404DateIntervalFormat.executeForEveryCalendarField_ = function(pattern, func) {405var inQuote = false;406var previousChar = '';407for (var i = 0; i < pattern.length; i++) {408var char = pattern.charAt(i);409if (inQuote) {410if (char == '\'') {411if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {412i++; // Literal quotation mark: ignore and advance.413} else {414inQuote = false;415}416}417} else {418if (char == '\'') {419inQuote = true;420} else if (char != previousChar && ALL_PATTERN_LETTERS_.test(char)) {421if (!func(char, i)) {422break;423}424}425}426previousChar = char;427}428};429430/**431* Returns a pattern letter representing the largest different calendar field432* between the two dates. This is calculated using the timezone used in the433* target representation.434* @param {!DateLike} startDate Start date of the date range.435* @param {!DateLike} endDate End date of the date range.436* @param {!TimeZone=} opt_timeZone Timezone to be used in the target437* representation.438* @return {string} Pattern letter representing the largest different calendar439* field or an empty string if all relevant fields for these dates are equal.440* @private441*/442DateIntervalFormat.getLargestDifferentCalendarField_ = function(443startDate, endDate, opt_timeZone) {444// Before comparing them, dates have to be adjusted by the target timezone's445// offset.446var startDiff = 0;447var endDiff = 0;448if (opt_timeZone != null) {449startDiff =450(startDate.getTimezoneOffset() - opt_timeZone.getOffset(startDate)) *45160000;452endDiff =453(endDate.getTimezoneOffset() - opt_timeZone.getOffset(endDate)) * 60000;454}455var startDt = new Date(startDate.getTime() + startDiff);456var endDt = new Date(endDate.getTime() + endDiff);457458if (DateIntervalFormat.getEra_(startDt) !=459DateIntervalFormat.getEra_(endDt)) {460return 'G';461} else if (startDt.getFullYear() != endDt.getFullYear()) {462return 'y';463} else if (startDt.getMonth() != endDt.getMonth()) {464return 'M';465} else if (startDt.getDate() != endDt.getDate()) {466return 'd';467} else if (468DateIntervalFormat.getAmPm_(startDt) !=469DateIntervalFormat.getAmPm_(endDt)) {470return 'a';471} else if (startDt.getHours() != endDt.getHours()) {472return 'h';473} else if (startDt.getMinutes() != endDt.getMinutes()) {474return 'm';475} else if (startDt.getSeconds() != endDt.getSeconds()) {476return 's';477}478return '';479};480481/**482* Returns the Era of a given DateLike object.483* @param {!Date} date484* @return {number}485* @private486*/487DateIntervalFormat.getEra_ = function(date) {488return date.getFullYear() > 0 ? Era_.AD : Era_.BC;489};490491/**492* Returns if the given date is in AM or PM.493* @param {!Date} date494* @return {number}495* @private496*/497DateIntervalFormat.getAmPm_ = function(date) {498var hours = date.getHours();499return (12 <= hours && hours < 24) ? AmPm_.PM : AmPm_.AM;500};501502/**503* Returns true if the calendar field field1 is a larger or equal than field2.504* Assumes that both string parameters have just one character. Field1 has to505* be part of the relevant calendar fields set.506* @param {string} field1507* @param {string} field2508* @return {boolean}509* @private510*/511DateIntervalFormat.isCalendarFieldLargerOrEqualThan_ = function(512field1, field2) {513return RELEVANT_CALENDAR_FIELDS_.indexOf(field1) <=514RELEVANT_CALENDAR_FIELDS_.indexOf(field2);515};516517/**518* Interface implemented by internal date interval formatters.519* @interface520* @private521*/522var Formatter_ = function() {};523524/**525* Formats two dates with the two parts of the date interval and returns the526* formatted string.527* @param {!DateLike} firstDate528* @param {!DateLike} secondDate529* @param {string} largestDifferentCalendarField530* @param {!TimeZone=} opt_timeZone Target timezone in which to format the531* dates.532* @return {string} String with the formatted date interval.533*/534Formatter_.prototype.format = function(535firstDate, secondDate, largestDifferentCalendarField, opt_timeZone) {};536537/**538* Constructs an IntervalFormatter_ object which implements the Formatter_539* interface.540*541* Internal object to construct and store a goog.i18n.DateTimeFormat for each542* part of the date interval pattern.543*544* @param {string} firstPattern First part of the date interval pattern.545* @param {string} secondPattern Second part of the date interval pattern.546* @param {!DateTimeSymbolsType} dateTimeSymbols Symbols to use with the547* datetime formatters.548* @param {boolean} useFirstDateOnFirstPattern Indicates if the first or the549* second date should be formatted with the first or second part of the date550* interval pattern.551* @constructor552* @implements {Formatter_}553* @private554*/555var IntervalFormatter_ = function(556firstPattern, secondPattern, dateTimeSymbols, useFirstDateOnFirstPattern) {557/**558* Formatter_ to format the first part of the date interval.559* @private {!DateTimeFormat}560*/561this.firstPartFormatter_ = new DateTimeFormat(firstPattern, dateTimeSymbols);562563/**564* Formatter_ to format the second part of the date interval.565* @private {!DateTimeFormat}566*/567this.secondPartFormatter_ =568new DateTimeFormat(secondPattern, dateTimeSymbols);569570/**571* Specifies if the first or the second date should be formatted by the572* formatter of the first or second part of the date interval.573* @private {boolean}574*/575this.useFirstDateOnFirstPattern_ = useFirstDateOnFirstPattern;576};577578/** @override */579IntervalFormatter_.prototype.format = function(580firstDate, secondDate, largestDifferentCalendarField, opt_timeZone) {581if (this.useFirstDateOnFirstPattern_) {582return this.firstPartFormatter_.format(firstDate, opt_timeZone) +583this.secondPartFormatter_.format(secondDate, opt_timeZone);584} else {585return this.firstPartFormatter_.format(secondDate, opt_timeZone) +586this.secondPartFormatter_.format(firstDate, opt_timeZone);587}588};589590/**591* Constructs a DateTimeFormatter_ object which implements the Formatter_592* interface.593*594* Internal object to construct and store a goog.i18n.DateTimeFormat for the595* a datetime pattern and formats dates using the fallback interval pattern596* (e.g. '{0} – {1}').597*598* @param {string} dateTimePattern Datetime pattern used to format the dates.599* @param {string} fallbackPattern Fallback interval pattern to be used with the600* datetime pattern.601* @param {!DateTimeSymbolsType} dateTimeSymbols Symbols to use with602* the datetime format.603* @constructor604* @implements {Formatter_}605* @private606*/607var DateTimeFormatter_ = function(608dateTimePattern, fallbackPattern, dateTimeSymbols) {609/**610* Date time pattern used to format the dates.611* @private {string}612*/613this.dateTimePattern_ = dateTimePattern;614615/**616* Date time formatter used to format the dates.617* @private {!DateTimeFormat}618*/619this.dateTimeFormatter_ =620new DateTimeFormat(dateTimePattern, dateTimeSymbols);621622/**623* Fallback interval pattern.624* @private {string}625*/626this.fallbackPattern_ = fallbackPattern;627};628629/** @override */630DateTimeFormatter_.prototype.format = function(631firstDate, secondDate, largestDifferentCalendarField, opt_timeZone) {632// Check if the largest different calendar field between the two dates is633// larger or equal than any calendar field in the datetime pattern. If true,634// format the string using the datetime pattern and the fallback interval635// pattern.636var shouldFormatWithFallbackPattern = false;637if (largestDifferentCalendarField != '') {638DateIntervalFormat.executeForEveryCalendarField_(639this.dateTimePattern_, function(char, index) {640if (DateIntervalFormat.isCalendarFieldLargerOrEqualThan_(641largestDifferentCalendarField, char)) {642shouldFormatWithFallbackPattern = true;643return false;644}645return true;646});647}648649if (shouldFormatWithFallbackPattern) {650return this.fallbackPattern_651.replace(652FIRST_DATE_PLACEHOLDER_,653this.dateTimeFormatter_.format(firstDate, opt_timeZone))654.replace(655SECOND_DATE_PLACEHOLDER_,656this.dateTimeFormatter_.format(secondDate, opt_timeZone));657}658// If not, format the first date using the datetime pattern.659return this.dateTimeFormatter_.format(firstDate, opt_timeZone);660};661662exports = DateIntervalFormat;663664665