1 /* eslint-disable no-restricted-properties */
5 * Provides various methods needed for formatting dates and times. This
6 * implementation implements the proleptic Gregorian calendar over years
10 * @extends mw.widgets.datetime.DateTimeFormatter
13 * @param {Object} [config] Configuration options
14 * @cfg {Object} [fullMonthNames] Mapping 1–12 to full month names.
15 * @cfg {Object} [shortMonthNames] Mapping 1–12 to abbreviated month names.
16 * If {@link #fullMonthNames fullMonthNames} is given and this is not,
17 * defaults to the first three characters from that setting.
18 * @cfg {Object} [fullDayNames] Mapping 0–6 to full day of week names. 0 is Sunday, 6 is Saturday.
19 * @cfg {Object} [shortDayNames] Mapping 0–6 to abbreviated day of week names. 0 is Sunday, 6 is Saturday.
20 * If {@link #fullDayNames fullDayNames} is given and this is not, defaults to
21 * the first three characters from that setting.
22 * @cfg {string[]} [dayLetters] Weekday column headers for a calendar. Array of 7 strings.
23 * If {@link #fullDayNames fullDayNames} or {@link #shortDayNames shortDayNames}
24 * are given and this is not, defaults to the first character from
26 * @cfg {string[]} [hour12Periods] AM and PM texts. Array of 2 strings, AM and PM.
27 * @cfg {number} [weekStartsOn=0] What day the week starts on: 0 is Sunday, 1 is Monday, 6 is Saturday.
29 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
= function MwWidgetsDatetimeProlepticGregorianDateTimeFormatter( config
) {
30 this.constructor.static.setupDefaults();
34 hour12Periods
: this.constructor.static.hour12Periods
37 if ( config
.fullMonthNames
&& !config
.shortMonthNames
) {
38 config
.shortMonthNames
= {};
39 // eslint-disable-next-line jquery/no-each-util
40 $.each( config
.fullMonthNames
, function ( k
, v
) {
41 config
.shortMonthNames
[ k
] = v
.substr( 0, 3 );
44 if ( config
.shortDayNames
&& !config
.dayLetters
) {
45 config
.dayLetters
= [];
46 // eslint-disable-next-line jquery/no-each-util
47 $.each( config
.shortDayNames
, function ( k
, v
) {
48 config
.dayLetters
[ k
] = v
.substr( 0, 1 );
51 if ( config
.fullDayNames
&& !config
.dayLetters
) {
52 config
.dayLetters
= [];
53 // eslint-disable-next-line jquery/no-each-util
54 $.each( config
.fullDayNames
, function ( k
, v
) {
55 config
.dayLetters
[ k
] = v
.substr( 0, 1 );
58 if ( config
.fullDayNames
&& !config
.shortDayNames
) {
59 config
.shortDayNames
= {};
60 // eslint-disable-next-line jquery/no-each-util
61 $.each( config
.fullDayNames
, function ( k
, v
) {
62 config
.shortDayNames
[ k
] = v
.substr( 0, 3 );
66 fullMonthNames
: this.constructor.static.fullMonthNames
,
67 shortMonthNames
: this.constructor.static.shortMonthNames
,
68 fullDayNames
: this.constructor.static.fullDayNames
,
69 shortDayNames
: this.constructor.static.shortDayNames
,
70 dayLetters
: this.constructor.static.dayLetters
74 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
[ 'super' ].call( this, config
);
77 this.weekStartsOn
= config
.weekStartsOn
% 7;
78 this.fullMonthNames
= config
.fullMonthNames
;
79 this.shortMonthNames
= config
.shortMonthNames
;
80 this.fullDayNames
= config
.fullDayNames
;
81 this.shortDayNames
= config
.shortDayNames
;
82 this.dayLetters
= config
.dayLetters
;
83 this.hour12Periods
= config
.hour12Periods
;
88 OO
.inheritClass( mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
, mw
.widgets
.datetime
.DateTimeFormatter
);
95 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.formats
= {
96 '@time': '${hour|0}:${minute|0}:${second|0}',
97 '@date': '$!{dow|short} ${day|#} ${month|short} ${year|#}',
98 '@datetime': '$!{dow|short} ${day|#} ${month|short} ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
99 '@default': '$!{dow|short} ${day|#} ${month|short} ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
103 * Default full month names.
109 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.fullMonthNames
= null;
112 * Default abbreviated month names.
118 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.shortMonthNames
= null;
121 * Default full day of week names.
127 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.fullDayNames
= null;
130 * Default abbreviated day of week names.
136 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.shortDayNames
= null;
139 * Default day letters.
143 * @property {string[]}
145 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.dayLetters
= null;
148 * Default AM/PM indicators
152 * @property {string[]}
154 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.hour12Periods
= null;
156 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.static.setupDefaults = function () {
157 mw
.widgets
.datetime
.DateTimeFormatter
.static.setupDefaults
.call( this );
159 if ( this.fullMonthNames
&& !this.shortMonthNames
) {
160 this.shortMonthNames
= {};
161 // eslint-disable-next-line jquery/no-each-util
162 $.each( this.fullMonthNames
, function ( k
, v
) {
163 this.shortMonthNames
[ k
] = v
.substr( 0, 3 );
166 if ( this.shortDayNames
&& !this.dayLetters
) {
167 this.dayLetters
= [];
168 // eslint-disable-next-line jquery/no-each-util
169 $.each( this.shortDayNames
, function ( k
, v
) {
170 this.dayLetters
[ k
] = v
.substr( 0, 1 );
173 if ( this.fullDayNames
&& !this.dayLetters
) {
174 this.dayLetters
= [];
175 // eslint-disable-next-line jquery/no-each-util
176 $.each( this.fullDayNames
, function ( k
, v
) {
177 this.dayLetters
[ k
] = v
.substr( 0, 1 );
180 if ( this.fullDayNames
&& !this.shortDayNames
) {
181 this.shortDayNames
= {};
182 // eslint-disable-next-line jquery/no-each-util
183 $.each( this.fullDayNames
, function ( k
, v
) {
184 this.shortDayNames
[ k
] = v
.substr( 0, 3 );
188 if ( !this.fullMonthNames
) {
189 this.fullMonthNames
= {
190 1: mw
.msg( 'january' ),
191 2: mw
.msg( 'february' ),
192 3: mw
.msg( 'march' ),
193 4: mw
.msg( 'april' ),
194 5: mw
.msg( 'may_long' ),
197 8: mw
.msg( 'august' ),
198 9: mw
.msg( 'september' ),
199 10: mw
.msg( 'october' ),
200 11: mw
.msg( 'november' ),
201 12: mw
.msg( 'december' )
204 if ( !this.shortMonthNames
) {
205 this.shortMonthNames
= {
221 if ( !this.fullDayNames
) {
222 this.fullDayNames
= {
223 0: mw
.msg( 'sunday' ),
224 1: mw
.msg( 'monday' ),
225 2: mw
.msg( 'tuesday' ),
226 3: mw
.msg( 'wednesday' ),
227 4: mw
.msg( 'thursday' ),
228 5: mw
.msg( 'friday' ),
229 6: mw
.msg( 'saturday' )
232 if ( !this.shortDayNames
) {
233 this.shortDayNames
= {
243 if ( !this.dayLetters
) {
244 this.dayLetters
= [];
245 // eslint-disable-next-line jquery/no-each-util
246 $.each( this.shortDayNames
, function ( k
, v
) {
247 this.dayLetters
[ k
] = v
.substr( 0, 1 );
251 if ( !this.hour12Periods
) {
252 this.hour12Periods
= [
253 mw
.msg( 'period-am' ),
254 mw
.msg( 'period-pm' )
264 * Additional fields implemented here are:
265 * - ${year|#}: Year as a number
266 * - ${year|0}: Year as a number, zero-padded to 4 digits
267 * - ${month|#}: Month as a number
268 * - ${month|0}: Month as a number with leading 0
269 * - ${month|short}: Month from 'shortMonthNames' configuration setting
270 * - ${month|full}: Month from 'fullMonthNames' configuration setting
271 * - ${day|#}: Day of the month as a number
272 * - ${day|0}: Day of the month as a number with leading 0
273 * - ${dow|short}: Day of the week from 'shortDayNames' configuration setting
274 * - ${dow|full}: Day of the week from 'fullDayNames' configuration setting
275 * - ${hour|#}: Hour as a number
276 * - ${hour|0}: Hour as a number with leading 0
277 * - ${hour|12}: Hour in a 12-hour clock as a number
278 * - ${hour|012}: Hour in a 12-hour clock as a number, with leading 0
279 * - ${hour|period}: Value from 'hour12Periods' configuration setting
280 * - ${minute|#}: Minute as a number
281 * - ${minute|0}: Minute as a number with leading 0
282 * - ${second|#}: Second as a number
283 * - ${second|0}: Second as a number with leading 0
284 * - ${millisecond|#}: Millisecond as a number
285 * - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
287 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.getFieldForTag = function ( tag
, params
) {
290 switch ( tag
+ '|' + params
[ 0 ] ) {
295 calendarComponent
: true,
298 zeropad
: params
[ 0 ] === '0'
306 calendarComponent
: true,
308 values
: params
[ 0 ] === 'short' ? this.shortMonthNames
: this.fullMonthNames
316 calendarComponent
: true,
319 values
: params
[ 0 ] === 'short' ? this.shortDayNames
: this.fullDayNames
329 calendarComponent
: true,
332 zeropad
: params
[ 0 ] === '0'
344 calendarComponent
: false,
347 zeropad
: params
[ 0 ] === '0'
355 calendarComponent
: false,
358 zeropad
: params
[ 0 ] === '012'
364 component
: 'hour12period',
365 calendarComponent
: false,
367 values
: this.hour12Periods
371 case 'millisecond|#':
372 case 'millisecond|0':
374 component
: 'millisecond',
375 calendarComponent
: false,
378 zeropad
: params
[ 0 ] === '0'
383 return mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
[ 'super' ].prototype.getFieldForTag
.call( this, tag
, params
);
387 if ( spec
.editable
=== undefined ) {
388 spec
.editable
= true;
390 spec
.formatValue
= this.formatSpecValue
;
391 spec
.parseValue
= this.parseSpecValue
;
393 spec
.size
= Math
.max
.apply(
394 // eslint-disable-next-line jquery/no-map-util
395 null, $.map( spec
.values
, function ( v
) { return v
.length
; } )
404 * Get components from a Date object
408 * - month {number} (1-12)
409 * - day {number} (1-31)
410 * - dow {number} (0-6, 0 is Sunday)
411 * - hour {number} (0-23)
412 * - hour12 {number} (1-12)
413 * - hour12period {boolean}
414 * - minute {number} (0-59)
415 * - second {number} (0-59)
416 * - millisecond {number} (0-999)
419 * @param {Date|null} date
420 * @return {Object} Components
422 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.getComponentsFromDate = function ( date
) {
425 if ( !( date
instanceof Date
) ) {
426 date
= this.defaultDate
;
431 year
: date
.getFullYear(),
432 month
: date
.getMonth() + 1,
434 dow
: date
.getDay() % 7,
435 hour
: date
.getHours(),
436 minute
: date
.getMinutes(),
437 second
: date
.getSeconds(),
438 millisecond
: date
.getMilliseconds(),
439 zone
: date
.getTimezoneOffset()
443 year
: date
.getUTCFullYear(),
444 month
: date
.getUTCMonth() + 1,
445 day
: date
.getUTCDate(),
446 dow
: date
.getUTCDay() % 7,
447 hour
: date
.getUTCHours(),
448 minute
: date
.getUTCMinutes(),
449 second
: date
.getUTCSeconds(),
450 millisecond
: date
.getUTCMilliseconds(),
455 ret
.hour12period
= ret
.hour
>= 12 ? 1 : 0;
456 ret
.hour12
= ret
.hour
% 12;
457 if ( ret
.hour12
=== 0 ) {
467 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.getDateFromComponents = function ( components
) {
468 var date
= new Date();
470 components
= $.extend( {}, components
);
471 if ( components
.hour
=== undefined && components
.hour12
!== undefined && components
.hour12period
!== undefined ) {
472 components
.hour
= ( components
.hour12
% 12 ) + ( components
.hour12period
? 12 : 0 );
474 components
= $.extend( {}, this.getComponentsFromDate( null ), components
);
476 if ( components
.zone
) {
477 // Can't just use the constructor because that's stupid about ancient years.
478 date
.setFullYear( components
.year
, components
.month
- 1, components
.day
);
479 date
.setHours( components
.hour
, components
.minute
, components
.second
, components
.millisecond
);
481 // Date.UTC() is stupid about ancient years too.
482 date
.setUTCFullYear( components
.year
, components
.month
- 1, components
.day
);
483 date
.setUTCHours( components
.hour
, components
.minute
, components
.second
, components
.millisecond
);
492 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.adjustComponent = function ( date
, component
, delta
, mode
) {
493 var min
, max
, range
, components
;
495 if ( !( date
instanceof Date
) ) {
496 date
= this.defaultDate
;
498 components
= this.getComponentsFromDate( date
);
500 switch ( component
) {
511 max
= this.getDaysInMonth( components
.month
, components
.year
);
534 min
= components
.hour12period
? 12 : 0;
535 max
= components
.hour12period
? 23 : 11;
538 return new Date( date
.getTime() );
541 components
[ component
] += delta
;
542 range
= max
- min
+ 1;
545 // Date() will mostly handle it automatically. But months need
546 // manual handling to prevent e.g. Jan 31 => Mar 3.
547 if ( component
=== 'month' || component
=== 'year' ) {
548 while ( components
.month
< 1 ) {
549 components
[ component
] += 12;
552 while ( components
.month
> 12 ) {
553 components
[ component
] -= 12;
559 while ( components
[ component
] < min
) {
560 components
[ component
] += range
;
562 while ( components
[ component
] > max
) {
563 components
[ component
] -= range
;
567 if ( components
[ component
] < min
) {
568 components
[ component
] = min
;
570 if ( components
[ component
] < max
) {
571 components
[ component
] = max
;
575 if ( component
=== 'month' || component
=== 'year' ) {
576 components
.day
= Math
.min( components
.day
, this.getDaysInMonth( components
.month
, components
.year
) );
579 return this.getDateFromComponents( components
);
583 * Get the number of days in a month
586 * @param {number} month
587 * @param {number} year
590 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.getDaysInMonth = function ( month
, year
) {
600 } else if ( year
% 100 ) {
603 return ( year
% 400 ) ? 28 : 29;
612 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.getCalendarHeadings = function () {
613 var a
= this.dayLetters
;
615 if ( this.weekStartsOn
) {
616 return a
.slice( this.weekStartsOn
).concat( a
.slice( 0, this.weekStartsOn
) );
618 return a
.slice( 0 ); // clone
625 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.sameCalendarGrid = function ( date1
, date2
) {
627 return date1
.getFullYear() === date2
.getFullYear() && date1
.getMonth() === date2
.getMonth();
629 return date1
.getUTCFullYear() === date2
.getUTCFullYear() && date1
.getUTCMonth() === date2
.getUTCMonth();
636 mw
.widgets
.datetime
.ProlepticGregorianDateTimeFormatter
.prototype.getCalendarData = function ( date
) {
637 var dt
, t
, d
, e
, i
, row
,
638 getDate
= this.local
? 'getDate' : 'getUTCDate',
639 setDate
= this.local
? 'setDate' : 'setUTCDate',
642 monthComponent
: 'month'
645 if ( !( date
instanceof Date
) ) {
646 date
= this.defaultDate
;
649 dt
= new Date( date
.getTime() );
654 ret
.header
= this.fullMonthNames
[ dt
.getMonth() + 1 ] + ' ' + dt
.getFullYear();
656 e
= this.getDaysInMonth( dt
.getMonth() + 1, dt
.getFullYear() );
658 ret
.header
= this.fullMonthNames
[ dt
.getUTCMonth() + 1 ] + ' ' + dt
.getUTCFullYear();
659 d
= dt
.getUTCDay() % 7;
660 e
= this.getDaysInMonth( dt
.getUTCMonth() + 1, dt
.getUTCFullYear() );
663 if ( this.weekStartsOn
) {
664 d
= ( d
+ 7 - this.weekStartsOn
) % 7;
671 for ( i
= 0; i
< 7; i
++, d
++ ) {
675 display
: String( dt
[ getDate
]() ),
677 extra
: d
< 1 ? 'prev' : d
> e
? 'next' : null
680 ret
.rows
.push( row
);