4 * Provides various methods needed for formatting dates and times.
8 * @mixins OO.EventEmitter
11 * @param {Object} [config] Configuration options
12 * @cfg {string} [format='@default'] May be a key from the {@link #static-formats static formats},
13 * or a format specification as defined by {@link #method-parseFieldSpec parseFieldSpec}
14 * and {@link #method-getFieldForTag getFieldForTag}.
15 * @cfg {boolean} [local=false] Whether dates are local time or UTC
16 * @cfg {string[]} [fullZones] Time zone indicators. Array of 2 strings, for
18 * @cfg {string[]} [shortZones] Abbreviated time zone indicators. Array of 2
19 * strings, for UTC and local time.
20 * @cfg {Date} [defaultDate] Default date, for filling unspecified components.
21 * Defaults to the current date and time (with 0 milliseconds).
23 mw.widgets.datetime.DateTimeFormatter = function MwWidgetsDatetimeDateTimeFormatter( config ) {
24 this.constructor.static.setupDefaults();
29 fullZones: this.constructor.static.fullZones,
30 shortZones: this.constructor.static.shortZones
34 OO.EventEmitter.call( this );
37 if ( this.constructor.static.formats[ config.format ] ) {
38 this.format = this.constructor.static.formats[ config.format ];
40 this.format = config.format;
42 this.local = !!config.local;
43 this.fullZones = config.fullZones;
44 this.shortZones = config.shortZones;
45 if ( config.defaultDate instanceof Date ) {
46 this.defaultDate = config.defaultDate;
48 this.defaultDate = new Date();
50 this.defaultDate.setMilliseconds( 0 );
52 this.defaultDate.setUTCMilliseconds( 0 );
59 OO.initClass( mw.widgets.datetime.DateTimeFormatter );
60 OO.mixinClass( mw.widgets.datetime.DateTimeFormatter, OO.EventEmitter );
65 * Default format specifications. See the {@link #format format} parameter.
71 mw.widgets.datetime.DateTimeFormatter.static.formats = {};
74 * Default time zone indicators
78 * @property {string[]}
80 mw.widgets.datetime.DateTimeFormatter.static.fullZones = null;
83 * Default abbreviated time zone indicators
87 * @property {string[]}
89 mw.widgets.datetime.DateTimeFormatter.static.shortZones = null;
91 mw.widgets.datetime.DateTimeFormatter.static.setupDefaults = function () {
92 if ( !this.fullZones ) {
94 mw.msg( 'timezone-utc' ),
95 mw.msg( 'timezone-local' )
98 if ( !this.shortZones ) {
101 this.fullZones[ 1 ].substr( 0, 1 ).toUpperCase()
103 if ( this.shortZones[ 1 ] === 'Z' ) {
104 this.shortZones[ 1 ] = 'L';
112 * A `local` event is emitted when the 'local' flag is changed.
120 * Whether dates are in local time or UTC
122 * @return {boolean} True if local time
124 mw.widgets.datetime.DateTimeFormatter.prototype.getLocal = function () {
128 // eslint-disable-next-line valid-jsdoc
130 * Toggle whether dates are in local time or UTC
132 * @param {boolean} [flag] Set the flag instead of toggling it
136 mw.widgets.datetime.DateTimeFormatter.prototype.toggleLocal = function ( flag ) {
137 if ( flag === undefined ) {
142 if ( this.local !== flag ) {
144 this.emit( 'local', this.local );
150 * Get the default date
154 mw.widgets.datetime.DateTimeFormatter.prototype.getDefaultDate = function () {
155 return new Date( this.defaultDate.getTime() );
159 * Fetch the field specification array for this object.
161 * See {@link #parseFieldSpec parseFieldSpec} for details on the return value structure.
165 mw.widgets.datetime.DateTimeFormatter.prototype.getFieldSpec = function () {
166 return this.parseFieldSpec( this.format );
170 * Parse a format string into a field specification
172 * The input is a string containing tags formatted as ${tag|param|param...}
173 * (for editable fields) and $!{tag|param|param...} (for non-editable fields).
174 * Most tags are defined by {@link #getFieldForTag getFieldForTag}, but a few
176 * - ${intercalary|X|text}: Text that is only displayed when the 'intercalary'
178 * - ${not-intercalary|X|text}: Text that is displayed unless the 'intercalary'
181 * Elements of the returned array are strings or objects. Strings are meant to
182 * be displayed as-is. Objects are as returned by {@link #getFieldForTag getFieldForTag}.
185 * @param {string} format
188 mw.widgets.datetime.DateTimeFormatter.prototype.parseFieldSpec = function ( format ) {
189 var m, last, tag, params, spec,
191 re = /(.*?)(\$(!?)\{([^}]+)\})/g;
194 while ( ( m = re.exec( format ) ) !== null ) {
197 if ( m[ 1 ] !== '' ) {
201 params = m[ 4 ].split( '|' );
202 tag = params.shift();
203 spec = this.getFieldForTag( tag, params );
205 if ( m[ 3 ] === '!' ) {
206 spec.editable = false;
213 if ( last < format.length ) {
214 ret.push( format.substr( last ) );
221 * Turn a tag into a field specification object
223 * Fields implemented here are:
224 * - ${intercalary|X|text}: Text that is only displayed when the 'intercalary'
226 * - ${not-intercalary|X|text}: Text that is displayed unless the 'intercalary'
228 * - ${zone|#}: Timezone offset, "+0000" format.
229 * - ${zone|:}: Timezone offset, "+00:00" format.
230 * - ${zone|short}: Timezone from 'shortZones' configuration setting.
231 * - ${zone|full}: Timezone from 'fullZones' configuration setting.
235 * @param {string} tag
236 * @param {string[]} params
237 * @return {Object|null} Field specification object, or null if the tag+params are unrecognized.
238 * @return {string|null} return.component Date component corresponding to this field, if any.
239 * @return {boolean} return.editable Whether this field is editable.
240 * @return {string} return.type What kind of field this is:
241 * - 'static': The field is a static string; component will be null.
242 * - 'number': The field is generally numeric.
243 * - 'string': The field is generally textual.
244 * - 'boolean': The field is a boolean.
245 * - 'toggleLocal': The field represents {@link #getLocal this.getLocal()}.
246 * Editing should directly call {@link #toggleLocal this.toggleLocal()}.
247 * @return {number} return.size Maximum number of characters in the field (when
248 * the 'intercalary' component is falsey). If 0, the field should be hidden entirely.
249 * @return {Object.<string,number>} return.intercalarySize Map from
250 * 'intercalary' component values to overridden sizes.
251 * @return {string} return.value For type='static', the string to display.
252 * @return {function(Mixed): string} return.formatValue A function to format a
253 * component value as a display string.
254 * @return {function(string): Mixed} return.parseValue A function to parse a
255 * display string into a component value. If parsing fails, returns undefined.
257 mw.widgets.datetime.DateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
262 case 'not-intercalary':
263 if ( params.length < 2 || !params[ 0 ] ) {
270 value: params.slice( 1 ).join( '|' ),
274 if ( tag === 'intercalary' ) {
275 spec.intercalarySize[ params[ 0 ] ] = spec.value.length;
277 spec.size = spec.value.length;
278 spec.intercalarySize[ params[ 0 ] ] = 0;
283 switch ( params[ 0 ] ) {
286 c = params[ 0 ] === '#' ? '' : ':';
292 formatValue: function ( v ) {
295 o = new Date().getTimezoneOffset();
296 r = String( Math.abs( o ) % 60 );
297 while ( r.length < 2 ) {
300 r = String( Math.floor( Math.abs( o ) / 60 ) ) + c + r;
301 while ( r.length < 4 + c.length ) {
304 return ( o <= 0 ? '+' : '−' ) + r;
306 return '+00' + c + '00';
309 parseValue: function ( v ) {
311 v = String( v ).trim();
312 if ( ( m = /^([+-−])([0-9]{1,2}):?([0-9]{2})$/.test( v ) ) ) {
313 return ( m[ 2 ] * 60 + m[ 3 ] ) * ( m[ 1 ] === '+' ? -1 : 1 );
326 values: params[ 0 ] === 'short' ? this.shortZones : this.fullZones,
327 formatValue: this.formatSpecValue,
328 parseValue: this.parseSpecValue
330 spec.size = Math.max.apply(
331 null, $.map( spec.values, function ( v ) { return v.length; } )
343 * Format a value for a field specification
345 * 'this' must be the field specification object. The intention is that you
346 * could just assign this function as the 'formatValue' for each field spec.
348 * Besides the publicly-documented fields, uses the following:
349 * - values: Enumerated values for the field
350 * - zeropad: Whether to pad the number with zeros.
356 mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue = function ( v ) {
357 if ( v === undefined || v === null ) {
361 if ( typeof v === 'boolean' || this.type === 'toggleLocal' ) {
366 return this.values[ v ];
370 if ( this.zeropad ) {
371 while ( v.length < this.size ) {
379 * Parse a value for a field specification
381 * 'this' must be the field specification object. The intention is that you
382 * could just assign this function as the 'parseValue' for each field spec.
384 * Besides the publicly-documented fields, uses the following:
385 * - values: Enumerated values for the field
389 * @return {number|string|null}
391 mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue = function ( v ) {
398 if ( !this.values ) {
400 if ( this.type === 'boolean' || this.type === 'toggleLocal' ) {
401 return isNaN( v ) ? undefined : !!v;
403 return isNaN( v ) ? undefined : v;
410 re = new RegExp( '^\\s*' + v.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ), 'i' );
411 for ( k in this.values ) {
413 if ( !isNaN( k ) && re.test( this.values[ k ] ) ) {
414 if ( this.type === 'boolean' || this.type === 'toggleLocal' ) {
425 * Get components from a Date object
427 * Most specific components are defined by the subclass. "Global" components
429 * - intercalary: {string} Non-falsey values are used to indicate intercalary days.
430 * - zone: {number} Timezone offset in minutes.
433 * @param {Date|null} date
434 * @return {Object} Components
436 mw.widgets.datetime.DateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
437 // Should be overridden by subclass
439 zone: this.local ? date.getTimezoneOffset() : 0
444 * Get a Date object from components
446 * @param {Object} components Date components
449 mw.widgets.datetime.DateTimeFormatter.prototype.getDateFromComponents = function ( /* components */ ) {
450 // Should be overridden by subclass
457 * @param {Date|null} date To be adjusted
458 * @param {string} component To adjust
459 * @param {number} delta Adjustment amount
460 * @param {string} mode Adjustment mode:
461 * - 'overflow': "Jan 32" => "Feb 1", "Jan 33" => "Feb 2", "Feb 0" => "Jan 31", etc.
462 * - 'wrap': "Jan 32" => "Jan 1", "Jan 33" => "Jan 2", "Jan 0" => "Jan 31", etc.
463 * - 'clip': "Jan 32" => "Jan 31", "Feb 32" => "Feb 28" (or 29), "Feb 0" => "Feb 1", etc.
464 * @return {Date} Adjusted date
466 mw.widgets.datetime.DateTimeFormatter.prototype.adjustComponent = function ( date /* , component, delta, mode */ ) {
467 // Should be overridden by subclass
472 * Get the column headings (weekday abbreviations) for a calendar grid
474 * Null-valued columns are hidden if getCalendarData() returns no "day" object
475 * for all days in that column.
478 * @return {Array} string or null
480 mw.widgets.datetime.DateTimeFormatter.prototype.getCalendarHeadings = function () {
481 // Should be overridden by subclass
486 * Test whether two dates are in the same calendar grid
489 * @param {Date} date1
490 * @param {Date} date2
493 mw.widgets.datetime.DateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
494 // Should be overridden by subclass
495 return date1.getTime() === date2.getTime();
499 * Test whether the date parts of two Dates are equal
501 * @param {Date} date1
502 * @param {Date} date2
505 mw.widgets.datetime.DateTimeFormatter.prototype.datePartIsEqual = function ( date1, date2 ) {
508 date1.getFullYear() === date2.getFullYear() &&
509 date1.getMonth() === date2.getMonth() &&
510 date1.getDate() === date2.getDate()
514 date1.getUTCFullYear() === date2.getUTCFullYear() &&
515 date1.getUTCMonth() === date2.getUTCMonth() &&
516 date1.getUTCDate() === date2.getUTCDate()
522 * Test whether the time parts of two Dates are equal
524 * @param {Date} date1
525 * @param {Date} date2
528 mw.widgets.datetime.DateTimeFormatter.prototype.timePartIsEqual = function ( date1, date2 ) {
531 date1.getHours() === date2.getHours() &&
532 date1.getMinutes() === date2.getMinutes() &&
533 date1.getSeconds() === date2.getSeconds() &&
534 date1.getMilliseconds() === date2.getMilliseconds()
538 date1.getUTCHours() === date2.getUTCHours() &&
539 date1.getUTCMinutes() === date2.getUTCMinutes() &&
540 date1.getUTCSeconds() === date2.getUTCSeconds() &&
541 date1.getUTCMilliseconds() === date2.getUTCMilliseconds()
547 * Test whether toggleLocal() changes the date part
552 mw.widgets.datetime.DateTimeFormatter.prototype.localChangesDatePart = function ( date ) {
554 date.getUTCFullYear() !== date.getFullYear() ||
555 date.getUTCMonth() !== date.getMonth() ||
556 date.getUTCDate() !== date.getDate()
561 * Create a new Date by merging the date part from one with the time part from
564 * @param {Date} datepart
565 * @param {Date} timepart
568 mw.widgets.datetime.DateTimeFormatter.prototype.mergeDateAndTime = function ( datepart, timepart ) {
569 var ret = new Date( datepart.getTime() );
574 timepart.getMinutes(),
575 timepart.getSeconds(),
576 timepart.getMilliseconds()
580 timepart.getUTCHours(),
581 timepart.getUTCMinutes(),
582 timepart.getUTCSeconds(),
583 timepart.getUTCMilliseconds()
591 * Get data for a calendar grid
594 * - display: {string} Display text for the day.
595 * - date: {Date} Date to use when the day is selected.
596 * - extra: {string|null} 'prev' or 'next' on days used to fill out the weeks
597 * at the start and end of the month.
599 * In any one result object, 'extra' + 'display' will always be unique.
602 * @param {Date|null} current Current date
603 * @return {Object} Data
604 * @return {string} return.header String to display as the calendar header
605 * @return {string} return.monthComponent Component to adjust by ±1 to change months.
606 * @return {string} return.dayComponent Component to adjust by ±1 to change days.
607 * @return {string} [return.weekComponent] Component to adjust by ±1 to change
608 * weeks. If omitted, the dayComponent should be adjusted by ±the number of
609 * non-nullable columns returned by this.getCalendarHeadings() to change weeks.
610 * @return {Array} return.rows Array of arrays of "day" objects or null/undefined.
612 mw.widgets.datetime.DateTimeFormatter.prototype.getCalendarData = function ( /* components */ ) {
613 // Should be overridden by subclass
616 monthComponent: 'month',
622 }( jQuery, mediaWiki ) );