4 * @classdesc DateTimeFormatter for the Discordian calendar.
6 * Provides various methods needed for formatting dates and times. This
7 * implementation implements the [Discordian calendar](https://en.wikipedia.org/wiki/Discordian_calendar),
8 * mainly for testing with something very different from the usual Gregorian
11 * Being intended mainly for testing, niceties like i18n and better
12 * configurability have been omitted.
15 * @extends mw.widgets.datetime.DateTimeFormatter
18 * @description Create an instance of `mw.widgets.datetime.DiscordianDateTimeFormatter`.
19 * @param {Object} [config] Configuration options
21 mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) {
22 config = Object.assign( {}, config );
25 mw.widgets.datetime.DiscordianDateTimeFormatter.super.call( this, config );
30 OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
35 * Default format specifications.
37 * See the `format` parameter in {@link mw.widgets.datetime.DateTimeFormatter}.
39 * @memberof mw.widgets.datetime.DiscordianDateTimeFormatter
40 * @type {Object.<string,string>}
43 mw.widgets.datetime.DiscordianDateTimeFormatter.static.formats = {
44 '@time': '${hour|0}:${minute|0}:${second|0}',
45 '@date': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#}',
46 '@datetime': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
47 '@default': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
53 * Turn a tag into a field specification object.
55 * Additional fields implemented here are:
56 * - ${year|#}: Year as a number
57 * - ${season|#}: Season as a number
58 * - ${season|full}: Season as a string
59 * - ${day|#}: Day of the month as a number
60 * - ${day|0}: Day of the month as a number with leading 0
61 * - ${dow|full}: Day of the week as a string
62 * - ${hour|#}: Hour as a number
63 * - ${hour|0}: Hour as a number with leading 0
64 * - ${minute|#}: Minute as a number
65 * - ${minute|0}: Minute as a number with leading 0
66 * - ${second|#}: Second as a number
67 * - ${second|0}: Second as a number with leading 0
68 * - ${millisecond|#}: Millisecond as a number
69 * - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
73 * @param {string[]} params
74 * @return {FieldSpecificationObject} Field specification object, or null if the tag+params are unrecognized.
76 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
79 switch ( tag + '|' + params[ 0 ] ) {
83 calendarComponent: true,
93 calendarComponent: true,
96 intercalarySize: { 1: 0 },
104 calendarComponent: true,
106 intercalarySize: { 1: 0 },
120 calendarComponent: true,
123 intercalarySize: { 1: 0 },
129 3: 'Prickle-Prickle',
139 calendarComponent: true,
142 intercalarySize: { 1: 13 },
143 zeropad: params[ 0 ] === '0',
144 formatValue: function ( v ) {
146 return 'St. Tib\'s Day';
148 return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
150 parseValue: function ( v ) {
152 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
155 return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
167 component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
168 calendarComponent: false,
171 zeropad: params[ 0 ] === '0'
175 case 'millisecond|#':
176 case 'millisecond|0':
178 component: 'Millisecond',
179 calendarComponent: false,
182 zeropad: params[ 0 ] === '0'
187 return mw.widgets.datetime.DiscordianDateTimeFormatter.super.prototype.getFieldForTag.call( this, tag, params );
191 if ( spec.editable === undefined ) {
192 spec.editable = true;
194 if ( spec.component !== 'Day' ) {
195 spec.formatValue = this.formatSpecValue;
196 spec.parseValue = this.parseSpecValue;
199 spec.size = Math.max.apply(
200 // eslint-disable-next-line no-jquery/no-map-util
201 null, $.map( spec.values, ( v ) => v.length )
210 * Get components from a Date object.
214 * - Season {number} 1-5
215 * - Day {number|string} 1-73 or 'tib'
216 * - DOW {number} 0-4, or -1 on St. Tib's Day
217 * - Hour {number} 0-23
218 * - Minute {number} 0-59
219 * - Second {number} 0-59
220 * - Millisecond {number} 0-999
221 * - intercalary {string} '1' on St. Tib's Day
223 * @param {Date|null} date
224 * @return {Object} Components
226 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
229 const monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
231 if ( !( date instanceof Date ) ) {
232 date = this.defaultDate;
236 day = date.getDate();
237 month = date.getMonth();
239 Year: date.getFullYear() + 1166,
240 Hour: date.getHours(),
241 Minute: date.getMinutes(),
242 Second: date.getSeconds(),
243 Millisecond: date.getMilliseconds(),
244 zone: date.getTimezoneOffset()
247 day = date.getUTCDate();
248 month = date.getUTCMonth();
250 Year: date.getUTCFullYear() + 1166,
251 Hour: date.getUTCHours(),
252 Minute: date.getUTCMinutes(),
253 Second: date.getUTCSeconds(),
254 Millisecond: date.getUTCMilliseconds(),
259 if ( month === 1 && day === 29 ) {
263 ret.intercalary = '1';
265 day = monthDays[ month ] + day - 1;
266 ret.Season = Math.floor( day / 73 ) + 1;
267 ret.Day = ( day % 73 ) + 1;
269 ret.intercalary = '';
278 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
279 return this.getDateFromComponents(
280 this.adjustComponentInternal(
281 this.getComponentsFromDate( date ), component, delta, mode
287 * Adjust the components directly.
290 * @param {Object} components Modified in place
291 * @param {string} component
292 * @param {number} delta
293 * @param {string} mode
294 * @return {Object} components
296 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
297 let i, min, max, range, next, preTib, postTib, wasTib;
303 switch ( component ) {
315 if ( components.Day === 'tib' ) {
316 components.Day = 59; // Could choose either one...
317 components.Season = 1;
358 if ( component === 'Day' ) {
359 i = Math.abs( delta );
360 delta = delta < 0 ? -1 : 1;
361 preTib = delta > 0 ? 59 : 60;
362 postTib = delta > 0 ? 60 : 59;
364 if ( components.Day === preTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
365 components.Day = 'tib';
366 } else if ( components.Day === 'tib' ) {
367 components.Day = postTib;
368 components.Season = 1;
370 components.Day += delta;
371 if ( components.Day < min ) {
374 components.Day = max;
375 this.adjustComponentInternal( components, 'Season', -1, mode );
378 components.Day = max;
381 components.Day = min;
386 if ( components.Day > max ) {
389 components.Day = min;
390 this.adjustComponentInternal( components, 'Season', 1, mode );
393 components.Day = min;
396 components.Day = max;
404 if ( component === 'Week' ) {
408 if ( components.Day === 'tib' ) {
409 components.Season = 1;
413 if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
414 components.Day = 59; // Could choose either one...
419 i = Math.abs( delta );
420 delta = delta < 0 ? -1 : 1;
422 components[ component ] += delta;
423 if ( components[ component ] < min ) {
424 components[ component ] = max;
425 components = this.adjustComponentInternal( components, next, -1, mode );
427 if ( components[ component ] > max ) {
428 components[ component ] = min;
429 components = this.adjustComponentInternal( components, next, 1, mode );
432 if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
433 components.Day = 'tib';
437 range = max - min + 1;
438 components[ component ] += delta;
439 while ( components[ component ] < min ) {
440 components[ component ] += range;
442 while ( components[ component ] > max ) {
443 components[ component ] -= range;
447 components[ component ] += delta;
448 if ( components[ component ] < min ) {
449 components[ component ] = min;
451 if ( components[ component ] > max ) {
452 components[ component ] = max;
456 if ( components.Day === 'tib' &&
457 ( components.Season !== 1 || !this.isLeapYear( components.Year ) )
459 components.Day = 59; // Could choose either one...
469 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
474 monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ];
476 components = Object.assign( {}, this.getComponentsFromDate( null ), components );
477 if ( components.Day === 'tib' ) {
481 const days = components.Season * 73 + components.Day - 74;
483 while ( days >= monthDays[ month + 1 ] ) {
486 day = days - monthDays[ month ] + 1;
489 if ( components.zone ) {
490 // Can't just use the constructor because that's stupid about ancient years.
491 date.setFullYear( components.Year - 1166, month, day );
492 date.setHours( components.Hour, components.Minute, components.Second, components.Millisecond );
494 // Date.UTC() is stupid about ancient years too.
495 date.setUTCFullYear( components.Year - 1166, month, day );
496 date.setUTCHours( components.Hour, components.Minute, components.Second, components.Millisecond );
503 * Get whether the year is a leap year.
506 * @param {number} year
509 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
513 } else if ( year % 100 ) {
516 return ( year % 400 ) === 0;
522 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
523 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
529 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
530 const components1 = this.getComponentsFromDate( date1 ),
531 components2 = this.getComponentsFromDate( date2 );
533 return components1.Year === components2.Year && components1.Season === components2.Season;
539 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
543 weekComponent: 'Week',
544 monthComponent: 'Season'
546 seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
547 seasonStart = [ 0, -3, -1, -4, -2 ];
549 if ( !( date instanceof Date ) ) {
550 date = this.defaultDate;
553 const components = this.getComponentsFromDate( date );
555 const season = components.Season;
557 ret.header = seasons[ season - 1 ] + ' ' + components.Year;
559 if ( seasonStart[ season - 1 ] ) {
560 this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
566 for ( let i = 0; i < 6; i++ ) {
567 const dt = this.getDateFromComponents( components );
569 display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
571 extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
574 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
575 if ( components.Day !== 'tib' && i === 3 ) {
580 ret.rows.push( row );
581 } while ( components.Season === season );