Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / resources / src / mediawiki.widgets.datetime / DiscordianDateTimeFormatter.js
blob6db2d062693f1f5ca7b4c7cc1a2db8eb44c1a8eb
1 ( function ( $, mw ) {
3         /**
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.
7          *
8          * Being intended mainly for testing, niceties like i18n and better
9          * configurability have been omitted.
10          *
11          * [1]: https://en.wikipedia.org/wiki/Discordian_calendar
12          *
13          * @class
14          * @extends mw.widgets.datetime.DateTimeFormatter
15          *
16          * @constructor
17          * @param {Object} [config] Configuration options
18          */
19         mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) {
20                 config = $.extend( {}, config );
22                 // Parent constructor
23                 mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].call( this, config );
24         };
26         /* Setup */
28         OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
30         /* Static */
32         /**
33          * @inheritdoc
34          */
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}'
40         };
42         /* Methods */
44         /**
45          * @inheritdoc
46          *
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
62          */
63         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
64                 var spec = null;
66                 switch ( tag + '|' + params[ 0 ] ) {
67                         case 'year|#':
68                                 spec = {
69                                         component: 'Year',
70                                         calendarComponent: true,
71                                         type: 'number',
72                                         size: 4,
73                                         zeropad: false
74                                 };
75                                 break;
77                         case 'season|#':
78                                 spec = {
79                                         component: 'Season',
80                                         calendarComponent: true,
81                                         type: 'number',
82                                         size: 1,
83                                         intercalarySize: { 1: 0 },
84                                         zeropad: false
85                                 };
86                                 break;
88                         case 'season|full':
89                                 spec = {
90                                         component: 'Season',
91                                         calendarComponent: true,
92                                         type: 'string',
93                                         intercalarySize: { 1: 0 },
94                                         values: {
95                                                 1: 'Chaos',
96                                                 2: 'Discord',
97                                                 3: 'Confusion',
98                                                 4: 'Bureaucracy',
99                                                 5: 'The Aftermath'
100                                         }
101                                 };
102                                 break;
104                         case 'dow|full':
105                                 spec = {
106                                         component: 'DOW',
107                                         calendarComponent: true,
108                                         editable: false,
109                                         type: 'string',
110                                         intercalarySize: { 1: 0 },
111                                         values: {
112                                                 '-1': 'N/A',
113                                                 0: 'Sweetmorn',
114                                                 1: 'Boomtime',
115                                                 2: 'Pungenday',
116                                                 3: 'Prickle-Prickle',
117                                                 4: 'Setting Orange'
118                                         }
119                                 };
120                                 break;
122                         case 'day|#':
123                         case 'day|0':
124                                 spec = {
125                                         component: 'Day',
126                                         calendarComponent: true,
127                                         type: 'string',
128                                         size: 2,
129                                         intercalarySize: { 1: 13 },
130                                         zeropad: params[ 0 ] === '0',
131                                         formatValue: function ( v ) {
132                                                 if ( v === 'tib' ) {
133                                                         return 'St. Tib\'s Day';
134                                                 }
135                                                 return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
136                                         },
137                                         parseValue: function ( v ) {
138                                                 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
139                                                         return 'tib';
140                                                 }
141                                                 return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
142                                         }
143                                 };
144                                 break;
146                         case 'hour|#':
147                         case 'hour|0':
148                         case 'minute|#':
149                         case 'minute|0':
150                         case 'second|#':
151                         case 'second|0':
152                                 spec = {
153                                         component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
154                                         calendarComponent: false,
155                                         type: 'number',
156                                         size: 2,
157                                         zeropad: params[ 0 ] === '0'
158                                 };
159                                 break;
161                         case 'millisecond|#':
162                         case 'millisecond|0':
163                                 spec = {
164                                         component: 'Millisecond',
165                                         calendarComponent: false,
166                                         type: 'number',
167                                         size: 3,
168                                         zeropad: params[ 0 ] === '0'
169                                 };
170                                 break;
172                         default:
173                                 return mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
174                 }
176                 if ( spec ) {
177                         if ( spec.editable === undefined ) {
178                                 spec.editable = true;
179                         }
180                         if ( spec.component !== 'Day' ) {
181                                 spec.formatValue = this.formatSpecValue;
182                                 spec.parseValue = this.parseSpecValue;
183                         }
184                         if ( spec.values ) {
185                                 spec.size = Math.max.apply(
186                                         null, $.map( spec.values, function ( v ) { return v.length; } )
187                                 );
188                         }
189                 }
191                 return spec;
192         };
194         /**
195          * Get components from a Date object
196          *
197          * Components are:
198          * - Year {number}
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
207          *
208          * @param {Date|null} date
209          * @return {Object} Components
210          */
211         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
212                 var ret, day, month,
213                         monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
215                 if ( !( date instanceof Date ) ) {
216                         date = this.defaultDate;
217                 }
219                 if ( this.local ) {
220                         day = date.getDate();
221                         month = date.getMonth();
222                         ret = {
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()
229                         };
230                 } else {
231                         day = date.getUTCDate();
232                         month = date.getUTCMonth();
233                         ret = {
234                                 Year: date.getUTCFullYear() + 1166,
235                                 Hour: date.getUTCHours(),
236                                 Minute: date.getUTCMinutes(),
237                                 Second: date.getUTCSeconds(),
238                                 Millisecond: date.getUTCMilliseconds(),
239                                 zone: 0
240                         };
241                 }
243                 if ( month === 1 && day === 29 ) {
244                         ret.Season = 1;
245                         ret.Day = 'tib';
246                         ret.DOW = -1;
247                         ret.intercalary = '1';
248                 } else {
249                         day = monthDays[ month ] + day - 1;
250                         ret.Season = Math.floor( day / 73 ) + 1;
251                         ret.Day = ( day % 73 ) + 1;
252                         ret.DOW = day % 5;
253                         ret.intercalary = '';
254                 }
256                 return ret;
257         };
259         /**
260          * @inheritdoc
261          */
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
266                         )
267                 );
268         };
270         /**
271          * Adjust the components directly
272          *
273          * @private
274          * @param {Object} components Modified in place
275          * @param {string} component
276          * @param {number} delta
277          * @param {string} mode
278          * @return {Object} components
279          */
280         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
281                 var i, min, max, range, next, preTib, postTib, wasTib;
283                 if ( delta === 0 ) {
284                         return components;
285                 }
287                 switch ( component ) {
288                         case 'Year':
289                                 min = 1166;
290                                 max = 11165;
291                                 next = null;
292                                 break;
293                         case 'Season':
294                                 min = 1;
295                                 max = 5;
296                                 next = 'Year';
297                                 break;
298                         case 'Week':
299                                 if ( components.Day === 'tib' ) {
300                                         components.Day = 59; // Could choose either one...
301                                         components.Season = 1;
302                                 }
303                                 min = 1;
304                                 max = 73;
305                                 next = 'Season';
306                                 break;
307                         case 'Day':
308                                 min = 1;
309                                 max = 73;
310                                 next = 'Season';
311                                 break;
312                         case 'Hour':
313                                 min = 0;
314                                 max = 23;
315                                 next = 'Day';
316                                 break;
317                         case 'Minute':
318                                 min = 0;
319                                 max = 59;
320                                 next = 'Hour';
321                                 break;
322                         case 'Second':
323                                 min = 0;
324                                 max = 59;
325                                 next = 'Minute';
326                                 break;
327                         case 'Millisecond':
328                                 min = 0;
329                                 max = 999;
330                                 next = 'Second';
331                                 break;
332                         default:
333                                 return components;
334                 }
336                 switch ( mode ) {
337                         case 'overflow':
338                         case 'clip':
339                         case 'wrap':
340                 }
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;
347                         while ( i-- > 0 ) {
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;
353                                 } else {
354                                         components.Day += delta;
355                                         if ( components.Day < min ) {
356                                                 switch ( mode ) {
357                                                         case 'overflow':
358                                                                 components.Day = max;
359                                                                 this.adjustComponentInternal( components, 'Season', -1, mode );
360                                                                 break;
361                                                         case 'wrap':
362                                                                 components.Day = max;
363                                                                 break;
364                                                         case 'clip':
365                                                                 components.Day = min;
366                                                                 i = 0;
367                                                                 break;
368                                                 }
369                                         }
370                                         if ( components.Day > max ) {
371                                                 switch ( mode ) {
372                                                         case 'overflow':
373                                                                 components.Day = min;
374                                                                 this.adjustComponentInternal( components, 'Season', 1, mode );
375                                                                 break;
376                                                         case 'wrap':
377                                                                 components.Day = min;
378                                                                 break;
379                                                         case 'clip':
380                                                                 components.Day = max;
381                                                                 i = 0;
382                                                                 break;
383                                                 }
384                                         }
385                                 }
386                         }
387                 } else {
388                         if ( component === 'Week' ) {
389                                 component = 'Day';
390                                 delta *= 5;
391                         }
392                         if ( components.Day === 'tib' ) {
393                                 // For sanity
394                                 components.Season = 1;
395                         }
396                         switch ( mode ) {
397                                 case 'overflow':
398                                         if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
399                                                 components.Day = 59; // Could choose either one...
400                                                 wasTib = true;
401                                         } else {
402                                                 wasTib = false;
403                                         }
404                                         i = Math.abs( delta );
405                                         delta = delta < 0 ? -1 : 1;
406                                         while ( i-- > 0 ) {
407                                                 components[ component ] += delta;
408                                                 if ( components[ component ] < min ) {
409                                                         components[ component ] = max;
410                                                         components = this.adjustComponentInternal( components, next, -1, mode );
411                                                 }
412                                                 if ( components[ component ] > max ) {
413                                                         components[ component ] = min;
414                                                         components = this.adjustComponentInternal( components, next, 1, mode );
415                                                 }
416                                         }
417                                         if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
418                                                 components.Day = 'tib';
419                                         }
420                                         break;
421                                 case 'wrap':
422                                         range = max - min + 1;
423                                         components[ component ] += delta;
424                                         while ( components[ component ] < min ) {
425                                                 components[ component ] += range;
426                                         }
427                                         while ( components[ component ] > max ) {
428                                                 components[ component ] -= range;
429                                         }
430                                         break;
431                                 case 'clip':
432                                         components[ component ] += delta;
433                                         if ( components[ component ] < min ) {
434                                                 components[ component ] = min;
435                                         }
436                                         if ( components[ component ] > max ) {
437                                                 components[ component ] = max;
438                                         }
439                                         break;
440                         }
441                         if ( components.Day === 'tib' &&
442                                 ( components.Season !== 1 || !this.isLeapYear( components.Year ) )
443                         ) {
444                                 components.Day = 59; // Could choose either one...
445                         }
446                 }
448                 return components;
449         };
451         /**
452          * @inheritdoc
453          */
454         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
455                 var month, day, days,
456                         date = new Date(),
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' ) {
461                         month = 1;
462                         day = 29;
463                 } else {
464                         days = components.Season * 73 + components.Day - 74;
465                         month = 0;
466                         while ( days >= monthDays[ month + 1 ] ) {
467                                 month++;
468                         }
469                         day = days - monthDays[ month ] + 1;
470                 }
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 );
476                 } else {
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 );
480                 }
482                 return date;
483         };
485         /**
486          * Get whether the year is a leap year
487          *
488          * @private
489          * @param {number} year
490          * @return {boolean}
491          */
492         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
493                 year -= 1166;
494                 if ( year % 4 ) {
495                         return false;
496                 } else if ( year % 100 ) {
497                         return true;
498                 }
499                 return ( year % 400 ) === 0;
500         };
502         /**
503          * @inheritdoc
504          */
505         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
506                 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
507         };
509         /**
510          * @inheritdoc
511          */
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;
517         };
519         /**
520          * @inheritdoc
521          */
522         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
523                 var dt, components, season, i, row,
524                         ret = {
525                                 dayComponent: 'Day',
526                                 weekComponent: 'Week',
527                                 monthComponent: 'Season'
528                         },
529                         seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
530                         seasonStart = [ 0, -3, -1, -4, -2 ];
532                 if ( !( date instanceof Date ) ) {
533                         date = this.defaultDate;
534                 }
536                 components = this.getComponentsFromDate( date );
537                 components.Day = 1;
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' );
544                 }
546                 ret.rows = [];
547                 do {
548                         row = [];
549                         for ( i = 0; i < 6; i++ ) {
550                                 dt = this.getDateFromComponents( components );
551                                 row[ i ] = {
552                                         display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
553                                         date: dt,
554                                         extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
555                                 };
557                                 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
558                                 if ( components.Day !== 'tib' && i === 3 ) {
559                                         row[ ++i ] = null;
560                                 }
561                         }
563                         ret.rows.push( row );
564                 } while ( components.Season === season );
566                 return ret;
567         };
569 }( jQuery, mediaWiki ) );