Avoid pointless use of isset() in LBFactoryMulti()
[mediawiki.git] / resources / src / mediawiki.widgets.datetime / DiscordianDateTimeFormatter.js
blobb280eadc03cc0ac284024d6999e0cb1523a78d62
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                                         type: 'number',
71                                         size: 4,
72                                         zeropad: false
73                                 };
74                                 break;
76                         case 'season|#':
77                                 spec = {
78                                         component: 'Season',
79                                         type: 'number',
80                                         size: 1,
81                                         intercalarySize: { 1: 0 },
82                                         zeropad: false
83                                 };
84                                 break;
86                         case 'season|full':
87                                 spec = {
88                                         component: 'Season',
89                                         type: 'string',
90                                         intercalarySize: { 1: 0 },
91                                         values: {
92                                                 1: 'Chaos',
93                                                 2: 'Discord',
94                                                 3: 'Confusion',
95                                                 4: 'Bureaucracy',
96                                                 5: 'The Aftermath'
97                                         }
98                                 };
99                                 break;
101                         case 'dow|full':
102                                 spec = {
103                                         component: 'DOW',
104                                         editable: false,
105                                         type: 'string',
106                                         intercalarySize: { 1: 0 },
107                                         values: {
108                                                 '-1': 'N/A',
109                                                 0: 'Sweetmorn',
110                                                 1: 'Boomtime',
111                                                 2: 'Pungenday',
112                                                 3: 'Prickle-Prickle',
113                                                 4: 'Setting Orange'
114                                         }
115                                 };
116                                 break;
118                         case 'day|#':
119                         case 'day|0':
120                                 spec = {
121                                         component: 'Day',
122                                         type: 'string',
123                                         size: 2,
124                                         intercalarySize: { 1: 13 },
125                                         zeropad: params[ 0 ] === '0',
126                                         formatValue: function ( v ) {
127                                                 if ( v === 'tib' ) {
128                                                         return 'St. Tib\'s Day';
129                                                 }
130                                                 return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
131                                         },
132                                         parseValue: function ( v ) {
133                                                 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
134                                                         return 'tib';
135                                                 }
136                                                 return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
137                                         }
138                                 };
139                                 break;
141                         case 'hour|#':
142                         case 'hour|0':
143                         case 'minute|#':
144                         case 'minute|0':
145                         case 'second|#':
146                         case 'second|0':
147                                 spec = {
148                                         component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
149                                         type: 'number',
150                                         size: 2,
151                                         zeropad: params[ 0 ] === '0'
152                                 };
153                                 break;
155                         case 'millisecond|#':
156                         case 'millisecond|0':
157                                 spec = {
158                                         component: 'Millisecond',
159                                         type: 'number',
160                                         size: 3,
161                                         zeropad: params[ 0 ] === '0'
162                                 };
163                                 break;
165                         default:
166                                 return mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
167                 }
169                 if ( spec ) {
170                         if ( spec.editable === undefined ) {
171                                 spec.editable = true;
172                         }
173                         if ( spec.component !== 'Day' ) {
174                                 spec.formatValue = this.formatSpecValue;
175                                 spec.parseValue = this.parseSpecValue;
176                         }
177                         if ( spec.values ) {
178                                 spec.size = Math.max.apply(
179                                         null, $.map( spec.values, function ( v ) { return v.length; } )
180                                 );
181                         }
182                 }
184                 return spec;
185         };
187         /**
188          * Get components from a Date object
189          *
190          * Components are:
191          * - Year {number}
192          * - Season {number} 1-5
193          * - Day {number|string} 1-73 or 'tib'
194          * - DOW {number} 0-4, or -1 on St. Tib's Day
195          * - Hour {number} 0-23
196          * - Minute {number} 0-59
197          * - Second {number} 0-59
198          * - Millisecond {number} 0-999
199          * - intercalary {string} '1' on St. Tib's Day
200          *
201          * @param {Date|null} date
202          * @return {Object} Components
203          */
204         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
205                 var ret, day, month,
206                         monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
208                 if ( !( date instanceof Date ) ) {
209                         date = this.defaultDate;
210                 }
212                 if ( this.local ) {
213                         day = date.getDate();
214                         month = date.getMonth();
215                         ret = {
216                                 Year: date.getFullYear() + 1166,
217                                 Hour: date.getHours(),
218                                 Minute: date.getMinutes(),
219                                 Second: date.getSeconds(),
220                                 Millisecond: date.getMilliseconds(),
221                                 zone: date.getTimezoneOffset()
222                         };
223                 } else {
224                         day = date.getUTCDate();
225                         month = date.getUTCMonth();
226                         ret = {
227                                 Year: date.getUTCFullYear() + 1166,
228                                 Hour: date.getUTCHours(),
229                                 Minute: date.getUTCMinutes(),
230                                 Second: date.getUTCSeconds(),
231                                 Millisecond: date.getUTCMilliseconds(),
232                                 zone: 0
233                         };
234                 }
236                 if ( month === 1 && day === 29 ) {
237                         ret.Season = 1;
238                         ret.Day = 'tib';
239                         ret.DOW = -1;
240                         ret.intercalary = '1';
241                 } else {
242                         day = monthDays[ month ] + day - 1;
243                         ret.Season = Math.floor( day / 73 ) + 1;
244                         ret.Day = ( day % 73 ) + 1;
245                         ret.DOW = day % 5;
246                         ret.intercalary = '';
247                 }
249                 return ret;
250         };
252         /**
253          * @inheritdoc
254          */
255         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
256                 return this.getDateFromComponents(
257                         this.adjustComponentInternal(
258                                 this.getComponentsFromDate( date ), component, delta, mode
259                         )
260                 );
261         };
263         /**
264          * Adjust the components directly
265          *
266          * @private
267          * @param {Object} components Modified in place
268          * @param {string} component
269          * @param {number} delta
270          * @param {string} mode
271          * @return {Object} components
272          */
273         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
274                 var i, min, max, range, next, preTib, postTib, wasTib;
276                 if ( delta === 0 ) {
277                         return components;
278                 }
280                 switch ( component ) {
281                         case 'Year':
282                                 min = 1166;
283                                 max = 11165;
284                                 next = null;
285                                 break;
286                         case 'Season':
287                                 min = 1;
288                                 max = 5;
289                                 next = 'Year';
290                                 break;
291                         case 'Week':
292                                 if ( components.Day === 'tib' ) {
293                                         components.Day = 59; // Could choose either one...
294                                         components.Season = 1;
295                                 }
296                                 min = 1;
297                                 max = 73;
298                                 next = 'Season';
299                                 break;
300                         case 'Day':
301                                 min = 1;
302                                 max = 73;
303                                 next = 'Season';
304                                 break;
305                         case 'Hour':
306                                 min = 0;
307                                 max = 23;
308                                 next = 'Day';
309                                 break;
310                         case 'Minute':
311                                 min = 0;
312                                 max = 59;
313                                 next = 'Hour';
314                                 break;
315                         case 'Second':
316                                 min = 0;
317                                 max = 59;
318                                 next = 'Minute';
319                                 break;
320                         case 'Millisecond':
321                                 min = 0;
322                                 max = 999;
323                                 next = 'Second';
324                                 break;
325                         default:
326                                 return components;
327                 }
329                 switch ( mode ) {
330                         case 'overflow':
331                         case 'clip':
332                         case 'wrap':
333                 }
335                 if ( component === 'Day' ) {
336                         i = Math.abs( delta );
337                         delta = delta < 0 ? -1 : 1;
338                         preTib = delta > 0 ? 59 : 60;
339                         postTib = delta > 0 ? 60 : 59;
340                         while ( i-- > 0 ) {
341                                 if ( components.Day === preTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
342                                         components.Day = 'tib';
343                                 } else if ( components.Day === 'tib' ) {
344                                         components.Day = postTib;
345                                         components.Season = 1;
346                                 } else {
347                                         components.Day += delta;
348                                         if ( components.Day < min ) {
349                                                 switch ( mode ) {
350                                                         case 'overflow':
351                                                                 components.Day = max;
352                                                                 this.adjustComponentInternal( components, 'Season', -1, mode );
353                                                                 break;
354                                                         case 'wrap':
355                                                                 components.Day = max;
356                                                                 break;
357                                                         case 'clip':
358                                                                 components.Day = min;
359                                                                 i = 0;
360                                                                 break;
361                                                 }
362                                         }
363                                         if ( components.Day > max ) {
364                                                 switch ( mode ) {
365                                                         case 'overflow':
366                                                                 components.Day = min;
367                                                                 this.adjustComponentInternal( components, 'Season', 1, mode );
368                                                                 break;
369                                                         case 'wrap':
370                                                                 components.Day = min;
371                                                                 break;
372                                                         case 'clip':
373                                                                 components.Day = max;
374                                                                 i = 0;
375                                                                 break;
376                                                 }
377                                         }
378                                 }
379                         }
380                 } else {
381                         if ( component === 'Week' ) {
382                                 component = 'Day';
383                                 delta *= 5;
384                         }
385                         if ( components.Day === 'tib' ) {
386                                 // For sanity
387                                 components.Season = 1;
388                         }
389                         switch ( mode ) {
390                                 case 'overflow':
391                                         if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
392                                                 components.Day = 59; // Could choose either one...
393                                                 wasTib = true;
394                                         } else {
395                                                 wasTib = false;
396                                         }
397                                         i = Math.abs( delta );
398                                         delta = delta < 0 ? -1 : 1;
399                                         while ( i-- > 0 ) {
400                                                 components[ component ] += delta;
401                                                 if ( components[ component ] < min ) {
402                                                         components[ component ] = max;
403                                                         components = this.adjustComponentInternal( components, next, -1, mode );
404                                                 }
405                                                 if ( components[ component ] > max ) {
406                                                         components[ component ] = min;
407                                                         components = this.adjustComponentInternal( components, next, 1, mode );
408                                                 }
409                                         }
410                                         if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
411                                                 components.Day = 'tib';
412                                         }
413                                         break;
414                                 case 'wrap':
415                                         range = max - min + 1;
416                                         components[ component ] += delta;
417                                         while ( components[ component ] < min ) {
418                                                 components[ component ] += range;
419                                         }
420                                         while ( components[ component ] > max ) {
421                                                 components[ component ] -= range;
422                                         }
423                                         break;
424                                 case 'clip':
425                                         components[ component ] += delta;
426                                         if ( components[ component ] < min ) {
427                                                 components[ component ] = min;
428                                         }
429                                         if ( components[ component ] > max ) {
430                                                 components[ component ] = max;
431                                         }
432                                         break;
433                         }
434                         if ( components.Day === 'tib' &&
435                                 ( components.Season !== 1 || !this.isLeapYear( components.Year ) )
436                         ) {
437                                 components.Day = 59; // Could choose either one...
438                         }
439                 }
441                 return components;
442         };
444         /**
445          * @inheritdoc
446          */
447         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
448                 var month, day, days,
449                         date = new Date(),
450                         monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ];
452                 components = $.extend( {}, this.getComponentsFromDate( null ), components );
453                 if ( components.Day === 'tib' ) {
454                         month = 1;
455                         day = 29;
456                 } else {
457                         days = components.Season * 73 + components.Day - 74;
458                         month = 0;
459                         while ( days >= monthDays[ month + 1 ] ) {
460                                 month++;
461                         }
462                         day = days - monthDays[ month ] + 1;
463                 }
465                 if ( components.zone ) {
466                         // Can't just use the constructor because that's stupid about ancient years.
467                         date.setFullYear( components.Year - 1166, month, day );
468                         date.setHours( components.Hour, components.Minute, components.Second, components.Millisecond );
469                 } else {
470                         // Date.UTC() is stupid about ancient years too.
471                         date.setUTCFullYear( components.Year - 1166, month, day );
472                         date.setUTCHours( components.Hour, components.Minute, components.Second, components.Millisecond );
473                 }
475                 return date;
476         };
478         /**
479          * Get whether the year is a leap year
480          *
481          * @private
482          * @param {number} year
483          * @return {boolean}
484          */
485         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
486                 year -= 1166;
487                 if ( year % 4 ) {
488                         return false;
489                 } else if ( year % 100 ) {
490                         return true;
491                 }
492                 return ( year % 400 ) === 0;
493         };
495         /**
496          * @inheritdoc
497          */
498         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
499                 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
500         };
502         /**
503          * @inheritdoc
504          */
505         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
506                 var components1 = this.getComponentsFromDate( date1 ),
507                         components2 = this.getComponentsFromDate( date2 );
509                 return components1.Year === components2.Year && components1.Season === components2.Season;
510         };
512         /**
513          * @inheritdoc
514          */
515         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
516                 var dt, components, season, i, row,
517                         ret = {
518                                 dayComponent: 'Day',
519                                 weekComponent: 'Week',
520                                 monthComponent: 'Season'
521                         },
522                         seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
523                         seasonStart = [ 0, -3, -1, -4, -2 ];
525                 if ( !( date instanceof Date ) ) {
526                         date = this.defaultDate;
527                 }
529                 components = this.getComponentsFromDate( date );
530                 components.Day = 1;
531                 season = components.Season;
533                 ret.header = seasons[ season - 1 ] + ' ' + components.Year;
535                 if ( seasonStart[ season - 1 ] ) {
536                         this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
537                 }
539                 ret.rows = [];
540                 do {
541                         row = [];
542                         for ( i = 0; i < 6; i++ ) {
543                                 dt = this.getDateFromComponents( components );
544                                 row[ i ] = {
545                                         display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
546                                         date: dt,
547                                         extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
548                                 };
550                                 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
551                                 if ( components.Day !== 'tib' && i === 3 ) {
552                                         row[ ++i ] = null;
553                                 }
554                         }
556                         ret.rows.push( row );
557                 } while ( components.Season === season );
559                 return ret;
560         };
562 }( jQuery, mediaWiki ) );