Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.widgets.datetime / DiscordianDateTimeFormatter.js
blob442858eecab4170f0043203434bc7ba7fcbcb2be
1 ( function () {
3         /**
4          * @classdesc DateTimeFormatter for the Discordian calendar.
5          *
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
9          * calendar.
10          *
11          * Being intended mainly for testing, niceties like i18n and better
12          * configurability have been omitted.
13          *
14          * @class
15          * @extends mw.widgets.datetime.DateTimeFormatter
16          *
17          * @constructor
18          * @description Create an instance of `mw.widgets.datetime.DiscordianDateTimeFormatter`.
19          * @param {Object} [config] Configuration options
20          */
21         mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) {
22                 config = Object.assign( {}, config );
24                 // Parent constructor
25                 mw.widgets.datetime.DiscordianDateTimeFormatter.super.call( this, config );
26         };
28         /* Setup */
30         OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
32         /* Static */
34         /**
35          * Default format specifications.
36          *
37          * See the `format` parameter in {@link mw.widgets.datetime.DateTimeFormatter}.
38          *
39          * @memberof mw.widgets.datetime.DiscordianDateTimeFormatter
40          * @type {Object.<string,string>}
41          * @name formats
42          */
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}'
48         };
50         /* Methods */
52         /**
53          * Turn a tag into a field specification object.
54          *
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
70          *
71          * @protected
72          * @param {string} tag
73          * @param {string[]} params
74          * @return {FieldSpecificationObject} Field specification object, or null if the tag+params are unrecognized.
75          */
76         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
77                 let spec = null;
79                 switch ( tag + '|' + params[ 0 ] ) {
80                         case 'year|#':
81                                 spec = {
82                                         component: 'Year',
83                                         calendarComponent: true,
84                                         type: 'number',
85                                         size: 4,
86                                         zeropad: false
87                                 };
88                                 break;
90                         case 'season|#':
91                                 spec = {
92                                         component: 'Season',
93                                         calendarComponent: true,
94                                         type: 'number',
95                                         size: 1,
96                                         intercalarySize: { 1: 0 },
97                                         zeropad: false
98                                 };
99                                 break;
101                         case 'season|full':
102                                 spec = {
103                                         component: 'Season',
104                                         calendarComponent: true,
105                                         type: 'string',
106                                         intercalarySize: { 1: 0 },
107                                         values: {
108                                                 1: 'Chaos',
109                                                 2: 'Discord',
110                                                 3: 'Confusion',
111                                                 4: 'Bureaucracy',
112                                                 5: 'The Aftermath'
113                                         }
114                                 };
115                                 break;
117                         case 'dow|full':
118                                 spec = {
119                                         component: 'DOW',
120                                         calendarComponent: true,
121                                         editable: false,
122                                         type: 'string',
123                                         intercalarySize: { 1: 0 },
124                                         values: {
125                                                 '-1': 'N/A',
126                                                 0: 'Sweetmorn',
127                                                 1: 'Boomtime',
128                                                 2: 'Pungenday',
129                                                 3: 'Prickle-Prickle',
130                                                 4: 'Setting Orange'
131                                         }
132                                 };
133                                 break;
135                         case 'day|#':
136                         case 'day|0':
137                                 spec = {
138                                         component: 'Day',
139                                         calendarComponent: true,
140                                         type: 'string',
141                                         size: 2,
142                                         intercalarySize: { 1: 13 },
143                                         zeropad: params[ 0 ] === '0',
144                                         formatValue: function ( v ) {
145                                                 if ( v === 'tib' ) {
146                                                         return 'St. Tib\'s Day';
147                                                 }
148                                                 return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
149                                         },
150                                         parseValue: function ( v ) {
152                                                 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
153                                                         return 'tib';
154                                                 }
155                                                 return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
156                                         }
157                                 };
158                                 break;
160                         case 'hour|#':
161                         case 'hour|0':
162                         case 'minute|#':
163                         case 'minute|0':
164                         case 'second|#':
165                         case 'second|0':
166                                 spec = {
167                                         component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
168                                         calendarComponent: false,
169                                         type: 'number',
170                                         size: 2,
171                                         zeropad: params[ 0 ] === '0'
172                                 };
173                                 break;
175                         case 'millisecond|#':
176                         case 'millisecond|0':
177                                 spec = {
178                                         component: 'Millisecond',
179                                         calendarComponent: false,
180                                         type: 'number',
181                                         size: 3,
182                                         zeropad: params[ 0 ] === '0'
183                                 };
184                                 break;
186                         default:
187                                 return mw.widgets.datetime.DiscordianDateTimeFormatter.super.prototype.getFieldForTag.call( this, tag, params );
188                 }
190                 if ( spec ) {
191                         if ( spec.editable === undefined ) {
192                                 spec.editable = true;
193                         }
194                         if ( spec.component !== 'Day' ) {
195                                 spec.formatValue = this.formatSpecValue;
196                                 spec.parseValue = this.parseSpecValue;
197                         }
198                         if ( spec.values ) {
199                                 spec.size = Math.max.apply(
200                                         // eslint-disable-next-line no-jquery/no-map-util
201                                         null, $.map( spec.values, ( v ) => v.length )
202                                 );
203                         }
204                 }
206                 return spec;
207         };
209         /**
210          * Get components from a Date object.
211          *
212          * Components are:
213          * - Year {number}
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
222          *
223          * @param {Date|null} date
224          * @return {Object} Components
225          */
226         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
227                 let ret, day, month;
229                 const monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
231                 if ( !( date instanceof Date ) ) {
232                         date = this.defaultDate;
233                 }
235                 if ( this.local ) {
236                         day = date.getDate();
237                         month = date.getMonth();
238                         ret = {
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()
245                         };
246                 } else {
247                         day = date.getUTCDate();
248                         month = date.getUTCMonth();
249                         ret = {
250                                 Year: date.getUTCFullYear() + 1166,
251                                 Hour: date.getUTCHours(),
252                                 Minute: date.getUTCMinutes(),
253                                 Second: date.getUTCSeconds(),
254                                 Millisecond: date.getUTCMilliseconds(),
255                                 zone: 0
256                         };
257                 }
259                 if ( month === 1 && day === 29 ) {
260                         ret.Season = 1;
261                         ret.Day = 'tib';
262                         ret.DOW = -1;
263                         ret.intercalary = '1';
264                 } else {
265                         day = monthDays[ month ] + day - 1;
266                         ret.Season = Math.floor( day / 73 ) + 1;
267                         ret.Day = ( day % 73 ) + 1;
268                         ret.DOW = day % 5;
269                         ret.intercalary = '';
270                 }
272                 return ret;
273         };
275         /**
276          * @inheritdoc
277          */
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
282                         )
283                 );
284         };
286         /**
287          * Adjust the components directly.
288          *
289          * @private
290          * @param {Object} components Modified in place
291          * @param {string} component
292          * @param {number} delta
293          * @param {string} mode
294          * @return {Object} components
295          */
296         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
297                 let i, min, max, range, next, preTib, postTib, wasTib;
299                 if ( delta === 0 ) {
300                         return components;
301                 }
303                 switch ( component ) {
304                         case 'Year':
305                                 min = 1166;
306                                 max = 11165;
307                                 next = null;
308                                 break;
309                         case 'Season':
310                                 min = 1;
311                                 max = 5;
312                                 next = 'Year';
313                                 break;
314                         case 'Week':
315                                 if ( components.Day === 'tib' ) {
316                                         components.Day = 59; // Could choose either one...
317                                         components.Season = 1;
318                                 }
319                                 min = 1;
320                                 max = 73;
321                                 next = 'Season';
322                                 break;
323                         case 'Day':
324                                 min = 1;
325                                 max = 73;
326                                 next = 'Season';
327                                 break;
328                         case 'Hour':
329                                 min = 0;
330                                 max = 23;
331                                 next = 'Day';
332                                 break;
333                         case 'Minute':
334                                 min = 0;
335                                 max = 59;
336                                 next = 'Hour';
337                                 break;
338                         case 'Second':
339                                 min = 0;
340                                 max = 59;
341                                 next = 'Minute';
342                                 break;
343                         case 'Millisecond':
344                                 min = 0;
345                                 max = 999;
346                                 next = 'Second';
347                                 break;
348                         default:
349                                 return components;
350                 }
352                 switch ( mode ) {
353                         case 'overflow':
354                         case 'clip':
355                         case 'wrap':
356                 }
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;
363                         while ( i-- > 0 ) {
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;
369                                 } else {
370                                         components.Day += delta;
371                                         if ( components.Day < min ) {
372                                                 switch ( mode ) {
373                                                         case 'overflow':
374                                                                 components.Day = max;
375                                                                 this.adjustComponentInternal( components, 'Season', -1, mode );
376                                                                 break;
377                                                         case 'wrap':
378                                                                 components.Day = max;
379                                                                 break;
380                                                         case 'clip':
381                                                                 components.Day = min;
382                                                                 i = 0;
383                                                                 break;
384                                                 }
385                                         }
386                                         if ( components.Day > max ) {
387                                                 switch ( mode ) {
388                                                         case 'overflow':
389                                                                 components.Day = min;
390                                                                 this.adjustComponentInternal( components, 'Season', 1, mode );
391                                                                 break;
392                                                         case 'wrap':
393                                                                 components.Day = min;
394                                                                 break;
395                                                         case 'clip':
396                                                                 components.Day = max;
397                                                                 i = 0;
398                                                                 break;
399                                                 }
400                                         }
401                                 }
402                         }
403                 } else {
404                         if ( component === 'Week' ) {
405                                 component = 'Day';
406                                 delta *= 5;
407                         }
408                         if ( components.Day === 'tib' ) {
409                                 components.Season = 1;
410                         }
411                         switch ( mode ) {
412                                 case 'overflow':
413                                         if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
414                                                 components.Day = 59; // Could choose either one...
415                                                 wasTib = true;
416                                         } else {
417                                                 wasTib = false;
418                                         }
419                                         i = Math.abs( delta );
420                                         delta = delta < 0 ? -1 : 1;
421                                         while ( i-- > 0 ) {
422                                                 components[ component ] += delta;
423                                                 if ( components[ component ] < min ) {
424                                                         components[ component ] = max;
425                                                         components = this.adjustComponentInternal( components, next, -1, mode );
426                                                 }
427                                                 if ( components[ component ] > max ) {
428                                                         components[ component ] = min;
429                                                         components = this.adjustComponentInternal( components, next, 1, mode );
430                                                 }
431                                         }
432                                         if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
433                                                 components.Day = 'tib';
434                                         }
435                                         break;
436                                 case 'wrap':
437                                         range = max - min + 1;
438                                         components[ component ] += delta;
439                                         while ( components[ component ] < min ) {
440                                                 components[ component ] += range;
441                                         }
442                                         while ( components[ component ] > max ) {
443                                                 components[ component ] -= range;
444                                         }
445                                         break;
446                                 case 'clip':
447                                         components[ component ] += delta;
448                                         if ( components[ component ] < min ) {
449                                                 components[ component ] = min;
450                                         }
451                                         if ( components[ component ] > max ) {
452                                                 components[ component ] = max;
453                                         }
454                                         break;
455                         }
456                         if ( components.Day === 'tib' &&
457                                 ( components.Season !== 1 || !this.isLeapYear( components.Year ) )
458                         ) {
459                                 components.Day = 59; // Could choose either one...
460                         }
461                 }
463                 return components;
464         };
466         /**
467          * @inheritdoc
468          */
469         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
470                 let month, day;
472                 const
473                         date = new Date(),
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' ) {
478                         month = 1;
479                         day = 29;
480                 } else {
481                         const days = components.Season * 73 + components.Day - 74;
482                         month = 0;
483                         while ( days >= monthDays[ month + 1 ] ) {
484                                 month++;
485                         }
486                         day = days - monthDays[ month ] + 1;
487                 }
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 );
493                 } else {
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 );
497                 }
499                 return date;
500         };
502         /**
503          * Get whether the year is a leap year.
504          *
505          * @private
506          * @param {number} year
507          * @return {boolean}
508          */
509         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
510                 year -= 1166;
511                 if ( year % 4 ) {
512                         return false;
513                 } else if ( year % 100 ) {
514                         return true;
515                 }
516                 return ( year % 400 ) === 0;
517         };
519         /**
520          * @inheritdoc
521          */
522         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
523                 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
524         };
526         /**
527          * @inheritdoc
528          */
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;
534         };
536         /**
537          * @inheritdoc
538          */
539         mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
540                 const
541                         ret = {
542                                 dayComponent: 'Day',
543                                 weekComponent: 'Week',
544                                 monthComponent: 'Season'
545                         },
546                         seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
547                         seasonStart = [ 0, -3, -1, -4, -2 ];
549                 if ( !( date instanceof Date ) ) {
550                         date = this.defaultDate;
551                 }
553                 const components = this.getComponentsFromDate( date );
554                 components.Day = 1;
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' );
561                 }
563                 ret.rows = [];
564                 do {
565                         const row = [];
566                         for ( let i = 0; i < 6; i++ ) {
567                                 const dt = this.getDateFromComponents( components );
568                                 row[ i ] = {
569                                         display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
570                                         date: dt,
571                                         extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
572                                 };
574                                 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
575                                 if ( components.Day !== 'tib' && i === 3 ) {
576                                         row[ ++i ] = null;
577                                 }
578                         }
580                         ret.rows.push( row );
581                 } while ( components.Season === season );
583                 return ret;
584         };
586 }() );