4 * Provides various methods needed for formatting dates and times. This
5 * implementation implments the [Discordian calendar][1], mainly for testing with
6 * something very different from the usual Gregorian calendar.
8 * Being intended mainly for testing, niceties like i18n and better
9 * configurability have been omitted.
11 * [1]: https://en.wikipedia.org/wiki/Discordian_calendar
14 * @extends mw.widgets.datetime.DateTimeFormatter
17 * @param {Object} [config] Configuration options
19 mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) {
20 config = $.extend( {}, config );
23 mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].call( this, config );
28 OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
35 mw.widgets.datetime.DiscordianDateTimeFormatter.static.formats = {
36 '@time': '${hour|0}:${minute|0}:${second|0}',
37 '@date': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#}',
38 '@datetime': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
39 '@default': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
47 * Additional fields implemented here are:
48 * - ${year|#}: Year as a number
49 * - ${season|#}: Season as a number
50 * - ${season|full}: Season as a string
51 * - ${day|#}: Day of the month as a number
52 * - ${day|0}: Day of the month as a number with leading 0
53 * - ${dow|full}: Day of the week as a string
54 * - ${hour|#}: Hour as a number
55 * - ${hour|0}: Hour as a number with leading 0
56 * - ${minute|#}: Minute as a number
57 * - ${minute|0}: Minute as a number with leading 0
58 * - ${second|#}: Second as a number
59 * - ${second|0}: Second as a number with leading 0
60 * - ${millisecond|#}: Millisecond as a number
61 * - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
63 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
66 switch ( tag + '|' + params[ 0 ] ) {
70 calendarComponent: true,
80 calendarComponent: true,
83 intercalarySize: { 1: 0 },
91 calendarComponent: true,
93 intercalarySize: { 1: 0 },
107 calendarComponent: true,
110 intercalarySize: { 1: 0 },
116 3: 'Prickle-Prickle',
126 calendarComponent: true,
129 intercalarySize: { 1: 13 },
130 zeropad: params[ 0 ] === '0',
131 formatValue: function ( v ) {
133 return 'St. Tib\'s Day';
135 return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
137 parseValue: function ( v ) {
138 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
141 return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
153 component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
154 calendarComponent: false,
157 zeropad: params[ 0 ] === '0'
161 case 'millisecond|#':
162 case 'millisecond|0':
164 component: 'Millisecond',
165 calendarComponent: false,
168 zeropad: params[ 0 ] === '0'
173 return mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
177 if ( spec.editable === undefined ) {
178 spec.editable = true;
180 if ( spec.component !== 'Day' ) {
181 spec.formatValue = this.formatSpecValue;
182 spec.parseValue = this.parseSpecValue;
185 spec.size = Math.max.apply(
186 null, $.map( spec.values, function ( v ) { return v.length; } )
195 * Get components from a Date object
199 * - Season {number} 1-5
200 * - Day {number|string} 1-73 or 'tib'
201 * - DOW {number} 0-4, or -1 on St. Tib's Day
202 * - Hour {number} 0-23
203 * - Minute {number} 0-59
204 * - Second {number} 0-59
205 * - Millisecond {number} 0-999
206 * - intercalary {string} '1' on St. Tib's Day
208 * @param {Date|null} date
209 * @return {Object} Components
211 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
213 monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
215 if ( !( date instanceof Date ) ) {
216 date = this.defaultDate;
220 day = date.getDate();
221 month = date.getMonth();
223 Year: date.getFullYear() + 1166,
224 Hour: date.getHours(),
225 Minute: date.getMinutes(),
226 Second: date.getSeconds(),
227 Millisecond: date.getMilliseconds(),
228 zone: date.getTimezoneOffset()
231 day = date.getUTCDate();
232 month = date.getUTCMonth();
234 Year: date.getUTCFullYear() + 1166,
235 Hour: date.getUTCHours(),
236 Minute: date.getUTCMinutes(),
237 Second: date.getUTCSeconds(),
238 Millisecond: date.getUTCMilliseconds(),
243 if ( month === 1 && day === 29 ) {
247 ret.intercalary = '1';
249 day = monthDays[ month ] + day - 1;
250 ret.Season = Math.floor( day / 73 ) + 1;
251 ret.Day = ( day % 73 ) + 1;
253 ret.intercalary = '';
262 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
263 return this.getDateFromComponents(
264 this.adjustComponentInternal(
265 this.getComponentsFromDate( date ), component, delta, mode
271 * Adjust the components directly
274 * @param {Object} components Modified in place
275 * @param {string} component
276 * @param {number} delta
277 * @param {string} mode
278 * @return {Object} components
280 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
281 var i, min, max, range, next, preTib, postTib, wasTib;
287 switch ( component ) {
299 if ( components.Day === 'tib' ) {
300 components.Day = 59; // Could choose either one...
301 components.Season = 1;
342 if ( component === 'Day' ) {
343 i = Math.abs( delta );
344 delta = delta < 0 ? -1 : 1;
345 preTib = delta > 0 ? 59 : 60;
346 postTib = delta > 0 ? 60 : 59;
348 if ( components.Day === preTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
349 components.Day = 'tib';
350 } else if ( components.Day === 'tib' ) {
351 components.Day = postTib;
352 components.Season = 1;
354 components.Day += delta;
355 if ( components.Day < min ) {
358 components.Day = max;
359 this.adjustComponentInternal( components, 'Season', -1, mode );
362 components.Day = max;
365 components.Day = min;
370 if ( components.Day > max ) {
373 components.Day = min;
374 this.adjustComponentInternal( components, 'Season', 1, mode );
377 components.Day = min;
380 components.Day = max;
388 if ( component === 'Week' ) {
392 if ( components.Day === 'tib' ) {
394 components.Season = 1;
398 if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
399 components.Day = 59; // Could choose either one...
404 i = Math.abs( delta );
405 delta = delta < 0 ? -1 : 1;
407 components[ component ] += delta;
408 if ( components[ component ] < min ) {
409 components[ component ] = max;
410 components = this.adjustComponentInternal( components, next, -1, mode );
412 if ( components[ component ] > max ) {
413 components[ component ] = min;
414 components = this.adjustComponentInternal( components, next, 1, mode );
417 if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
418 components.Day = 'tib';
422 range = max - min + 1;
423 components[ component ] += delta;
424 while ( components[ component ] < min ) {
425 components[ component ] += range;
427 while ( components[ component ] > max ) {
428 components[ component ] -= range;
432 components[ component ] += delta;
433 if ( components[ component ] < min ) {
434 components[ component ] = min;
436 if ( components[ component ] > max ) {
437 components[ component ] = max;
441 if ( components.Day === 'tib' &&
442 ( components.Season !== 1 || !this.isLeapYear( components.Year ) )
444 components.Day = 59; // Could choose either one...
454 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
455 var month, day, days,
457 monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ];
459 components = $.extend( {}, this.getComponentsFromDate( null ), components );
460 if ( components.Day === 'tib' ) {
464 days = components.Season * 73 + components.Day - 74;
466 while ( days >= monthDays[ month + 1 ] ) {
469 day = days - monthDays[ month ] + 1;
472 if ( components.zone ) {
473 // Can't just use the constructor because that's stupid about ancient years.
474 date.setFullYear( components.Year - 1166, month, day );
475 date.setHours( components.Hour, components.Minute, components.Second, components.Millisecond );
477 // Date.UTC() is stupid about ancient years too.
478 date.setUTCFullYear( components.Year - 1166, month, day );
479 date.setUTCHours( components.Hour, components.Minute, components.Second, components.Millisecond );
486 * Get whether the year is a leap year
489 * @param {number} year
492 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
496 } else if ( year % 100 ) {
499 return ( year % 400 ) === 0;
505 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
506 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
512 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
513 var components1 = this.getComponentsFromDate( date1 ),
514 components2 = this.getComponentsFromDate( date2 );
516 return components1.Year === components2.Year && components1.Season === components2.Season;
522 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
523 var dt, components, season, i, row,
526 weekComponent: 'Week',
527 monthComponent: 'Season'
529 seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
530 seasonStart = [ 0, -3, -1, -4, -2 ];
532 if ( !( date instanceof Date ) ) {
533 date = this.defaultDate;
536 components = this.getComponentsFromDate( date );
538 season = components.Season;
540 ret.header = seasons[ season - 1 ] + ' ' + components.Year;
542 if ( seasonStart[ season - 1 ] ) {
543 this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
549 for ( i = 0; i < 6; i++ ) {
550 dt = this.getDateFromComponents( components );
552 display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
554 extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
557 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
558 if ( components.Day !== 'tib' && i === 3 ) {
563 ret.rows.push( row );
564 } while ( components.Season === season );
569 }( jQuery, mediaWiki ) );