4 * @classdesc DateTimeFormatter for the proleptic Gregorian calendar.
6 * Provides various methods needed for formatting dates and times. This
7 * implementation implements the proleptic Gregorian calendar over years
11 * @extends mw.widgets.datetime.DateTimeFormatter
14 * @description Create an instance of `mw.widgets.datetime.ProlepticGregorianDateTimeFormatter`.
15 * @param {Object} [config] Configuration options
16 * @param {Object} [config.fullMonthNames] Mapping 1–12 to full month names.
17 * @param {Object} [config.shortMonthNames] Mapping 1–12 to abbreviated month names.
18 * If {@link #fullMonthNames fullMonthNames} is given and this is not,
19 * defaults to the first three characters from that setting.
20 * @param {Object} [config.fullDayNames] Mapping 0–6 to full day of week names. 0 is Sunday, 6 is Saturday.
21 * @param {Object} [config.shortDayNames] Mapping 0–6 to abbreviated day of week names. 0 is Sunday, 6 is Saturday.
22 * If {@link #fullDayNames fullDayNames} is given and this is not, defaults to
23 * the first three characters from that setting.
24 * @param {string[]} [config.dayLetters] Weekday column headers for a calendar. Array of 7 strings.
25 * If {@link #fullDayNames fullDayNames} or {@link #shortDayNames shortDayNames}
26 * are given and this is not, defaults to the first character from
28 * @param {string[]} [config.hour12Periods] AM and PM texts. Array of 2 strings, AM and PM.
29 * @param {number} [config.weekStartsOn=0] What day the week starts on: 0 is Sunday, 1 is Monday, 6 is Saturday.
31 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter = function MwWidgetsDatetimeProlepticGregorianDateTimeFormatter( config ) {
32 this.constructor.static.setupDefaults();
34 config = Object.assign( {
36 hour12Periods: this.constructor.static.hour12Periods
39 if ( config.fullMonthNames && !config.shortMonthNames ) {
40 config.shortMonthNames = {};
41 // eslint-disable-next-line no-jquery/no-each-util
42 $.each( config.fullMonthNames, ( k, v ) => {
43 config.shortMonthNames[ k ] = v.slice( 0, 3 );
46 if ( config.shortDayNames && !config.dayLetters ) {
47 config.dayLetters = [];
48 // eslint-disable-next-line no-jquery/no-each-util
49 $.each( config.shortDayNames, ( k, v ) => {
50 config.dayLetters[ k ] = v.slice( 0, 1 );
53 if ( config.fullDayNames && !config.dayLetters ) {
54 config.dayLetters = [];
55 // eslint-disable-next-line no-jquery/no-each-util
56 $.each( config.fullDayNames, ( k, v ) => {
57 config.dayLetters[ k ] = v.slice( 0, 1 );
60 if ( config.fullDayNames && !config.shortDayNames ) {
61 config.shortDayNames = {};
62 // eslint-disable-next-line no-jquery/no-each-util
63 $.each( config.fullDayNames, ( k, v ) => {
64 config.shortDayNames[ k ] = v.slice( 0, 3 );
67 config = Object.assign( {
68 fullMonthNames: this.constructor.static.fullMonthNames,
69 shortMonthNames: this.constructor.static.shortMonthNames,
70 fullDayNames: this.constructor.static.fullDayNames,
71 shortDayNames: this.constructor.static.shortDayNames,
72 dayLetters: this.constructor.static.dayLetters
76 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.super.call( this, config );
79 this.weekStartsOn = config.weekStartsOn % 7;
80 this.fullMonthNames = config.fullMonthNames;
81 this.shortMonthNames = config.shortMonthNames;
82 this.fullDayNames = config.fullDayNames;
83 this.shortDayNames = config.shortDayNames;
84 this.dayLetters = config.dayLetters;
85 this.hour12Periods = config.hour12Periods;
90 OO.inheritClass( mw.widgets.datetime.ProlepticGregorianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
95 * Default format specifications.
97 * See the `format` parameter in {@link mw.widgets.datetime.DateTimeFormatter}.
99 * @memberof mw.widgets.datetime.ProlepticGregorianDateTimeFormatter
100 * @type {Object.<string,string>}
103 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.formats = {
104 '@time': '${hour|0}:${minute|0}:${second|0}',
105 '@date': '$!{dow|short} ${day|#} ${month|short} ${year|#}',
106 '@datetime': '$!{dow|short} ${day|#} ${month|short} ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
107 '@default': '$!{dow|short} ${day|#} ${month|short} ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
111 * Default full month names.
116 * @name mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.fullMonthNames
118 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.fullMonthNames = null;
121 * Default abbreviated month names.
126 * @name mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.shortMonthNames
128 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.shortMonthNames = null;
131 * Default full day of week names.
136 * @name mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.fullDayNames
138 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.fullDayNames = null;
141 * Default abbreviated day of week names.
146 * @name mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.shortDayNames
148 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.shortDayNames = null;
151 * Default day letters.
156 * @name mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.dayLetters
158 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.dayLetters = null;
161 * Default AM/PM indicators.
166 * @name mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.hour12Periods
168 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.hour12Periods = null;
170 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.setupDefaults = function () {
171 mw.widgets.datetime.DateTimeFormatter.static.setupDefaults.call( this );
173 if ( this.fullMonthNames && !this.shortMonthNames ) {
174 this.shortMonthNames = {};
175 // eslint-disable-next-line no-jquery/no-each-util
176 $.each( this.fullMonthNames, ( k, v ) => {
177 this.shortMonthNames[ k ] = v.slice( 0, 3 );
180 if ( this.shortDayNames && !this.dayLetters ) {
181 this.dayLetters = [];
182 // eslint-disable-next-line no-jquery/no-each-util
183 $.each( this.shortDayNames, ( k, v ) => {
184 this.dayLetters[ k ] = v.slice( 0, 1 );
187 if ( this.fullDayNames && !this.dayLetters ) {
188 this.dayLetters = [];
189 // eslint-disable-next-line no-jquery/no-each-util
190 $.each( this.fullDayNames, ( k, v ) => {
191 this.dayLetters[ k ] = v.slice( 0, 1 );
194 if ( this.fullDayNames && !this.shortDayNames ) {
195 this.shortDayNames = {};
196 // eslint-disable-next-line no-jquery/no-each-util
197 $.each( this.fullDayNames, ( k, v ) => {
198 this.shortDayNames[ k ] = v.slice( 0, 3 );
202 if ( !this.fullMonthNames ) {
203 this.fullMonthNames = {
204 1: mw.msg( 'january' ),
205 2: mw.msg( 'february' ),
206 3: mw.msg( 'march' ),
207 4: mw.msg( 'april' ),
208 5: mw.msg( 'may_long' ),
211 8: mw.msg( 'august' ),
212 9: mw.msg( 'september' ),
213 10: mw.msg( 'october' ),
214 11: mw.msg( 'november' ),
215 12: mw.msg( 'december' )
218 if ( !this.shortMonthNames ) {
219 this.shortMonthNames = {
235 if ( !this.fullDayNames ) {
236 this.fullDayNames = {
237 0: mw.msg( 'sunday' ),
238 1: mw.msg( 'monday' ),
239 2: mw.msg( 'tuesday' ),
240 3: mw.msg( 'wednesday' ),
241 4: mw.msg( 'thursday' ),
242 5: mw.msg( 'friday' ),
243 6: mw.msg( 'saturday' )
246 if ( !this.shortDayNames ) {
247 this.shortDayNames = {
257 if ( !this.dayLetters ) {
258 const dayLetters = [];
259 const shortDayNames = this.shortDayNames;
260 for ( const dayOfWeek in shortDayNames ) {
261 const shortDayName = shortDayNames[ dayOfWeek ];
262 dayLetters[ dayOfWeek ] = shortDayName.slice( 0, 1 );
264 this.dayLetters = dayLetters;
267 if ( !this.hour12Periods ) {
268 this.hour12Periods = [
269 mw.msg( 'period-am' ),
270 mw.msg( 'period-pm' )
278 * Turn a tag into a field specification object.
280 * Additional fields implemented here are:
281 * - ${year|#}: Year as a number
282 * - ${year|0}: Year as a number, zero-padded to 4 digits
283 * - ${month|#}: Month as a number
284 * - ${month|0}: Month as a number with leading 0
285 * - ${month|short}: Month from 'shortMonthNames' configuration setting
286 * - ${month|full}: Month from 'fullMonthNames' configuration setting
287 * - ${day|#}: Day of the month as a number
288 * - ${day|0}: Day of the month as a number with leading 0
289 * - ${dow|short}: Day of the week from 'shortDayNames' configuration setting
290 * - ${dow|full}: Day of the week from 'fullDayNames' configuration setting
291 * - ${hour|#}: Hour as a number
292 * - ${hour|0}: Hour as a number with leading 0
293 * - ${hour|12}: Hour in a 12-hour clock as a number
294 * - ${hour|012}: Hour in a 12-hour clock as a number, with leading 0
295 * - ${hour|period}: Value from 'hour12Periods' configuration setting
296 * - ${minute|#}: Minute as a number
297 * - ${minute|0}: Minute as a number with leading 0
298 * - ${second|#}: Second as a number
299 * - ${second|0}: Second as a number with leading 0
300 * - ${millisecond|#}: Millisecond as a number
301 * - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
304 * @param {string} tag
305 * @param {string[]} params
306 * @return {FieldSpecificationObject} Field specification object, or null if the tag+params are unrecognized.
308 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
311 switch ( tag + '|' + params[ 0 ] ) {
316 calendarComponent: true,
319 zeropad: params[ 0 ] === '0'
327 calendarComponent: true,
329 values: params[ 0 ] === 'short' ? this.shortMonthNames : this.fullMonthNames
337 calendarComponent: true,
340 values: params[ 0 ] === 'short' ? this.shortDayNames : this.fullDayNames
350 calendarComponent: true,
353 zeropad: params[ 0 ] === '0'
365 calendarComponent: false,
368 zeropad: params[ 0 ] === '0'
376 calendarComponent: false,
379 zeropad: params[ 0 ] === '012'
385 component: 'hour12period',
386 calendarComponent: false,
388 values: this.hour12Periods
392 case 'millisecond|#':
393 case 'millisecond|0':
395 component: 'millisecond',
396 calendarComponent: false,
399 zeropad: params[ 0 ] === '0'
404 return mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.super.prototype.getFieldForTag.call( this, tag, params );
408 if ( spec.editable === undefined ) {
409 spec.editable = true;
411 spec.formatValue = this.formatSpecValue;
412 spec.parseValue = this.parseSpecValue;
414 spec.size = Math.max.apply(
415 // eslint-disable-next-line no-jquery/no-map-util
416 null, $.map( spec.values, ( v ) => v.length )
425 * Get components from a Date object.
429 * - month {number} (1-12)
430 * - day {number} (1-31)
431 * - dow {number} (0-6, 0 is Sunday)
432 * - hour {number} (0-23)
433 * - hour12 {number} (1-12)
434 * - hour12period {boolean}
435 * - minute {number} (0-59)
436 * - second {number} (0-59)
437 * - millisecond {number} (0-999)
440 * @param {Date|null} date
441 * @return {Object} Components
443 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
446 if ( !( date instanceof Date ) ) {
447 date = this.defaultDate;
452 year: date.getFullYear(),
453 month: date.getMonth() + 1,
455 dow: date.getDay() % 7,
456 hour: date.getHours(),
457 minute: date.getMinutes(),
458 second: date.getSeconds(),
459 millisecond: date.getMilliseconds(),
460 zone: date.getTimezoneOffset()
464 year: date.getUTCFullYear(),
465 month: date.getUTCMonth() + 1,
466 day: date.getUTCDate(),
467 dow: date.getUTCDay() % 7,
468 hour: date.getUTCHours(),
469 minute: date.getUTCMinutes(),
470 second: date.getUTCSeconds(),
471 millisecond: date.getUTCMilliseconds(),
476 ret.hour12period = ret.hour >= 12 ? 1 : 0;
477 ret.hour12 = ret.hour % 12;
478 if ( ret.hour12 === 0 ) {
488 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
489 const date = new Date();
491 components = Object.assign( {}, components );
492 if ( components.hour === undefined && components.hour12 !== undefined && components.hour12period !== undefined ) {
493 components.hour = ( components.hour12 % 12 ) + ( components.hour12period ? 12 : 0 );
495 components = Object.assign( {}, this.getComponentsFromDate( null ), components );
497 if ( components.zone ) {
498 // Can't just use the constructor because that's stupid about ancient years.
499 date.setFullYear( components.year, components.month - 1, components.day );
500 date.setHours( components.hour, components.minute, components.second, components.millisecond );
502 // Date.UTC() is stupid about ancient years too.
503 date.setUTCFullYear( components.year, components.month - 1, components.day );
504 date.setUTCHours( components.hour, components.minute, components.second, components.millisecond );
513 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
516 if ( !( date instanceof Date ) ) {
517 date = this.defaultDate;
519 const components = this.getComponentsFromDate( date );
521 switch ( component ) {
532 max = this.getDaysInMonth( components.month, components.year );
555 min = components.hour12period ? 12 : 0;
556 max = components.hour12period ? 23 : 11;
559 return new Date( date.getTime() );
562 components[ component ] += delta;
563 const range = max - min + 1;
566 // Date() will mostly handle it automatically. But months need
567 // manual handling to prevent e.g. Jan 31 => Mar 3.
568 if ( component === 'month' || component === 'year' ) {
569 while ( components.month < 1 ) {
570 components[ component ] += 12;
573 while ( components.month > 12 ) {
574 components[ component ] -= 12;
580 while ( components[ component ] < min ) {
581 components[ component ] += range;
583 while ( components[ component ] > max ) {
584 components[ component ] -= range;
588 if ( components[ component ] < min ) {
589 components[ component ] = min;
591 if ( components[ component ] < max ) {
592 components[ component ] = max;
596 if ( component === 'month' || component === 'year' ) {
597 components.day = Math.min( components.day, this.getDaysInMonth( components.month, components.year ) );
600 return this.getDateFromComponents( components );
604 * Get the number of days in a month.
607 * @param {number} month
608 * @param {number} year
611 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getDaysInMonth = function ( month, year ) {
621 } else if ( year % 100 ) {
624 return ( year % 400 ) ? 28 : 29;
633 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getCalendarHeadings = function () {
634 const a = this.dayLetters;
636 if ( this.weekStartsOn ) {
637 return a.slice( this.weekStartsOn ).concat( a.slice( 0, this.weekStartsOn ) );
639 return a.slice( 0 ); // clone
646 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
648 return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth();
650 return date1.getUTCFullYear() === date2.getUTCFullYear() && date1.getUTCMonth() === date2.getUTCMonth();
657 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
658 const getDate = this.local ? 'getDate' : 'getUTCDate',
659 setDate = this.local ? 'setDate' : 'setUTCDate';
663 monthComponent: 'month'
666 if ( !( date instanceof Date ) ) {
667 date = this.defaultDate;
670 let dt = new Date( date.getTime() );
672 const t = dt.getTime();
676 ret.header = this.fullMonthNames[ dt.getMonth() + 1 ] + ' ' + dt.getFullYear();
678 e = this.getDaysInMonth( dt.getMonth() + 1, dt.getFullYear() );
680 ret.header = this.fullMonthNames[ dt.getUTCMonth() + 1 ] + ' ' + dt.getUTCFullYear();
681 d = dt.getUTCDay() % 7;
682 e = this.getDaysInMonth( dt.getUTCMonth() + 1, dt.getUTCFullYear() );
685 if ( this.weekStartsOn ) {
686 d = ( d + 7 - this.weekStartsOn ) % 7;
693 for ( let i = 0; i < 7; i++, d++ ) {
697 display: String( dt[ getDate ]() ),
699 extra: d < 1 ? 'prev' : d > e ? 'next' : null
702 ret.rows.push( row );