build: Use eslint-config-wikimedia v0.9.0 and make pass
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets.datetime / DiscordianDateTimeFormatter.js
1 /* eslint-disable no-restricted-properties */
2 ( function () {
3
4 /**
5 * Provides various methods needed for formatting dates and times. This
6 * implementation implments the [Discordian calendar][1], mainly for testing with
7 * something very different from the usual Gregorian calendar.
8 *
9 * Being intended mainly for testing, niceties like i18n and better
10 * configurability have been omitted.
11 *
12 * [1]: https://en.wikipedia.org/wiki/Discordian_calendar
13 *
14 * @class
15 * @extends mw.widgets.datetime.DateTimeFormatter
16 *
17 * @constructor
18 * @param {Object} [config] Configuration options
19 */
20 mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) {
21 config = $.extend( {}, config );
22
23 // Parent constructor
24 mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].call( this, config );
25 };
26
27 /* Setup */
28
29 OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
30
31 /* Static */
32
33 /**
34 * @inheritdoc
35 */
36 mw.widgets.datetime.DiscordianDateTimeFormatter.static.formats = {
37 '@time': '${hour|0}:${minute|0}:${second|0}',
38 '@date': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#}',
39 '@datetime': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
40 '@default': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
41 };
42
43 /* Methods */
44
45 /**
46 * @inheritdoc
47 *
48 * Additional fields implemented here are:
49 * - ${year|#}: Year as a number
50 * - ${season|#}: Season as a number
51 * - ${season|full}: Season as a string
52 * - ${day|#}: Day of the month as a number
53 * - ${day|0}: Day of the month as a number with leading 0
54 * - ${dow|full}: Day of the week as a string
55 * - ${hour|#}: Hour as a number
56 * - ${hour|0}: Hour as a number with leading 0
57 * - ${minute|#}: Minute as a number
58 * - ${minute|0}: Minute as a number with leading 0
59 * - ${second|#}: Second as a number
60 * - ${second|0}: Second as a number with leading 0
61 * - ${millisecond|#}: Millisecond as a number
62 * - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
63 */
64 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
65 var spec = null;
66
67 switch ( tag + '|' + params[ 0 ] ) {
68 case 'year|#':
69 spec = {
70 component: 'Year',
71 calendarComponent: true,
72 type: 'number',
73 size: 4,
74 zeropad: false
75 };
76 break;
77
78 case 'season|#':
79 spec = {
80 component: 'Season',
81 calendarComponent: true,
82 type: 'number',
83 size: 1,
84 intercalarySize: { 1: 0 },
85 zeropad: false
86 };
87 break;
88
89 case 'season|full':
90 spec = {
91 component: 'Season',
92 calendarComponent: true,
93 type: 'string',
94 intercalarySize: { 1: 0 },
95 values: {
96 1: 'Chaos',
97 2: 'Discord',
98 3: 'Confusion',
99 4: 'Bureaucracy',
100 5: 'The Aftermath'
101 }
102 };
103 break;
104
105 case 'dow|full':
106 spec = {
107 component: 'DOW',
108 calendarComponent: true,
109 editable: false,
110 type: 'string',
111 intercalarySize: { 1: 0 },
112 values: {
113 '-1': 'N/A',
114 0: 'Sweetmorn',
115 1: 'Boomtime',
116 2: 'Pungenday',
117 3: 'Prickle-Prickle',
118 4: 'Setting Orange'
119 }
120 };
121 break;
122
123 case 'day|#':
124 case 'day|0':
125 spec = {
126 component: 'Day',
127 calendarComponent: true,
128 type: 'string',
129 size: 2,
130 intercalarySize: { 1: 13 },
131 zeropad: params[ 0 ] === '0',
132 formatValue: function ( v ) {
133 if ( v === 'tib' ) {
134 return 'St. Tib\'s Day';
135 }
136 return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
137 },
138 parseValue: function ( v ) {
139 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
140 return 'tib';
141 }
142 return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
143 }
144 };
145 break;
146
147 case 'hour|#':
148 case 'hour|0':
149 case 'minute|#':
150 case 'minute|0':
151 case 'second|#':
152 case 'second|0':
153 spec = {
154 component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
155 calendarComponent: false,
156 type: 'number',
157 size: 2,
158 zeropad: params[ 0 ] === '0'
159 };
160 break;
161
162 case 'millisecond|#':
163 case 'millisecond|0':
164 spec = {
165 component: 'Millisecond',
166 calendarComponent: false,
167 type: 'number',
168 size: 3,
169 zeropad: params[ 0 ] === '0'
170 };
171 break;
172
173 default:
174 return mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
175 }
176
177 if ( spec ) {
178 if ( spec.editable === undefined ) {
179 spec.editable = true;
180 }
181 if ( spec.component !== 'Day' ) {
182 spec.formatValue = this.formatSpecValue;
183 spec.parseValue = this.parseSpecValue;
184 }
185 if ( spec.values ) {
186 spec.size = Math.max.apply(
187 // eslint-disable-next-line jquery/no-map-util
188 null, $.map( spec.values, function ( v ) { return v.length; } )
189 );
190 }
191 }
192
193 return spec;
194 };
195
196 /**
197 * Get components from a Date object
198 *
199 * Components are:
200 * - Year {number}
201 * - Season {number} 1-5
202 * - Day {number|string} 1-73 or 'tib'
203 * - DOW {number} 0-4, or -1 on St. Tib's Day
204 * - Hour {number} 0-23
205 * - Minute {number} 0-59
206 * - Second {number} 0-59
207 * - Millisecond {number} 0-999
208 * - intercalary {string} '1' on St. Tib's Day
209 *
210 * @param {Date|null} date
211 * @return {Object} Components
212 */
213 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
214 var ret, day, month,
215 monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
216
217 if ( !( date instanceof Date ) ) {
218 date = this.defaultDate;
219 }
220
221 if ( this.local ) {
222 day = date.getDate();
223 month = date.getMonth();
224 ret = {
225 Year: date.getFullYear() + 1166,
226 Hour: date.getHours(),
227 Minute: date.getMinutes(),
228 Second: date.getSeconds(),
229 Millisecond: date.getMilliseconds(),
230 zone: date.getTimezoneOffset()
231 };
232 } else {
233 day = date.getUTCDate();
234 month = date.getUTCMonth();
235 ret = {
236 Year: date.getUTCFullYear() + 1166,
237 Hour: date.getUTCHours(),
238 Minute: date.getUTCMinutes(),
239 Second: date.getUTCSeconds(),
240 Millisecond: date.getUTCMilliseconds(),
241 zone: 0
242 };
243 }
244
245 if ( month === 1 && day === 29 ) {
246 ret.Season = 1;
247 ret.Day = 'tib';
248 ret.DOW = -1;
249 ret.intercalary = '1';
250 } else {
251 day = monthDays[ month ] + day - 1;
252 ret.Season = Math.floor( day / 73 ) + 1;
253 ret.Day = ( day % 73 ) + 1;
254 ret.DOW = day % 5;
255 ret.intercalary = '';
256 }
257
258 return ret;
259 };
260
261 /**
262 * @inheritdoc
263 */
264 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
265 return this.getDateFromComponents(
266 this.adjustComponentInternal(
267 this.getComponentsFromDate( date ), component, delta, mode
268 )
269 );
270 };
271
272 /**
273 * Adjust the components directly
274 *
275 * @private
276 * @param {Object} components Modified in place
277 * @param {string} component
278 * @param {number} delta
279 * @param {string} mode
280 * @return {Object} components
281 */
282 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
283 var i, min, max, range, next, preTib, postTib, wasTib;
284
285 if ( delta === 0 ) {
286 return components;
287 }
288
289 switch ( component ) {
290 case 'Year':
291 min = 1166;
292 max = 11165;
293 next = null;
294 break;
295 case 'Season':
296 min = 1;
297 max = 5;
298 next = 'Year';
299 break;
300 case 'Week':
301 if ( components.Day === 'tib' ) {
302 components.Day = 59; // Could choose either one...
303 components.Season = 1;
304 }
305 min = 1;
306 max = 73;
307 next = 'Season';
308 break;
309 case 'Day':
310 min = 1;
311 max = 73;
312 next = 'Season';
313 break;
314 case 'Hour':
315 min = 0;
316 max = 23;
317 next = 'Day';
318 break;
319 case 'Minute':
320 min = 0;
321 max = 59;
322 next = 'Hour';
323 break;
324 case 'Second':
325 min = 0;
326 max = 59;
327 next = 'Minute';
328 break;
329 case 'Millisecond':
330 min = 0;
331 max = 999;
332 next = 'Second';
333 break;
334 default:
335 return components;
336 }
337
338 switch ( mode ) {
339 case 'overflow':
340 case 'clip':
341 case 'wrap':
342 }
343
344 if ( component === 'Day' ) {
345 i = Math.abs( delta );
346 delta = delta < 0 ? -1 : 1;
347 preTib = delta > 0 ? 59 : 60;
348 postTib = delta > 0 ? 60 : 59;
349 while ( i-- > 0 ) {
350 if ( components.Day === preTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
351 components.Day = 'tib';
352 } else if ( components.Day === 'tib' ) {
353 components.Day = postTib;
354 components.Season = 1;
355 } else {
356 components.Day += delta;
357 if ( components.Day < min ) {
358 switch ( mode ) {
359 case 'overflow':
360 components.Day = max;
361 this.adjustComponentInternal( components, 'Season', -1, mode );
362 break;
363 case 'wrap':
364 components.Day = max;
365 break;
366 case 'clip':
367 components.Day = min;
368 i = 0;
369 break;
370 }
371 }
372 if ( components.Day > max ) {
373 switch ( mode ) {
374 case 'overflow':
375 components.Day = min;
376 this.adjustComponentInternal( components, 'Season', 1, mode );
377 break;
378 case 'wrap':
379 components.Day = min;
380 break;
381 case 'clip':
382 components.Day = max;
383 i = 0;
384 break;
385 }
386 }
387 }
388 }
389 } else {
390 if ( component === 'Week' ) {
391 component = 'Day';
392 delta *= 5;
393 }
394 if ( components.Day === 'tib' ) {
395 // For sanity
396 components.Season = 1;
397 }
398 switch ( mode ) {
399 case 'overflow':
400 if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
401 components.Day = 59; // Could choose either one...
402 wasTib = true;
403 } else {
404 wasTib = false;
405 }
406 i = Math.abs( delta );
407 delta = delta < 0 ? -1 : 1;
408 while ( i-- > 0 ) {
409 components[ component ] += delta;
410 if ( components[ component ] < min ) {
411 components[ component ] = max;
412 components = this.adjustComponentInternal( components, next, -1, mode );
413 }
414 if ( components[ component ] > max ) {
415 components[ component ] = min;
416 components = this.adjustComponentInternal( components, next, 1, mode );
417 }
418 }
419 if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
420 components.Day = 'tib';
421 }
422 break;
423 case 'wrap':
424 range = max - min + 1;
425 components[ component ] += delta;
426 while ( components[ component ] < min ) {
427 components[ component ] += range;
428 }
429 while ( components[ component ] > max ) {
430 components[ component ] -= range;
431 }
432 break;
433 case 'clip':
434 components[ component ] += delta;
435 if ( components[ component ] < min ) {
436 components[ component ] = min;
437 }
438 if ( components[ component ] > max ) {
439 components[ component ] = max;
440 }
441 break;
442 }
443 if ( components.Day === 'tib' &&
444 ( components.Season !== 1 || !this.isLeapYear( components.Year ) )
445 ) {
446 components.Day = 59; // Could choose either one...
447 }
448 }
449
450 return components;
451 };
452
453 /**
454 * @inheritdoc
455 */
456 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
457 var month, day, days,
458 date = new Date(),
459 monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ];
460
461 components = $.extend( {}, this.getComponentsFromDate( null ), components );
462 if ( components.Day === 'tib' ) {
463 month = 1;
464 day = 29;
465 } else {
466 days = components.Season * 73 + components.Day - 74;
467 month = 0;
468 while ( days >= monthDays[ month + 1 ] ) {
469 month++;
470 }
471 day = days - monthDays[ month ] + 1;
472 }
473
474 if ( components.zone ) {
475 // Can't just use the constructor because that's stupid about ancient years.
476 date.setFullYear( components.Year - 1166, month, day );
477 date.setHours( components.Hour, components.Minute, components.Second, components.Millisecond );
478 } else {
479 // Date.UTC() is stupid about ancient years too.
480 date.setUTCFullYear( components.Year - 1166, month, day );
481 date.setUTCHours( components.Hour, components.Minute, components.Second, components.Millisecond );
482 }
483
484 return date;
485 };
486
487 /**
488 * Get whether the year is a leap year
489 *
490 * @private
491 * @param {number} year
492 * @return {boolean}
493 */
494 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
495 year -= 1166;
496 if ( year % 4 ) {
497 return false;
498 } else if ( year % 100 ) {
499 return true;
500 }
501 return ( year % 400 ) === 0;
502 };
503
504 /**
505 * @inheritdoc
506 */
507 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
508 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
509 };
510
511 /**
512 * @inheritdoc
513 */
514 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
515 var components1 = this.getComponentsFromDate( date1 ),
516 components2 = this.getComponentsFromDate( date2 );
517
518 return components1.Year === components2.Year && components1.Season === components2.Season;
519 };
520
521 /**
522 * @inheritdoc
523 */
524 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
525 var dt, components, season, i, row,
526 ret = {
527 dayComponent: 'Day',
528 weekComponent: 'Week',
529 monthComponent: 'Season'
530 },
531 seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
532 seasonStart = [ 0, -3, -1, -4, -2 ];
533
534 if ( !( date instanceof Date ) ) {
535 date = this.defaultDate;
536 }
537
538 components = this.getComponentsFromDate( date );
539 components.Day = 1;
540 season = components.Season;
541
542 ret.header = seasons[ season - 1 ] + ' ' + components.Year;
543
544 if ( seasonStart[ season - 1 ] ) {
545 this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
546 }
547
548 ret.rows = [];
549 do {
550 row = [];
551 for ( i = 0; i < 6; i++ ) {
552 dt = this.getDateFromComponents( components );
553 row[ i ] = {
554 display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
555 date: dt,
556 extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
557 };
558
559 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
560 if ( components.Day !== 'tib' && i === 3 ) {
561 row[ ++i ] = null;
562 }
563 }
564
565 ret.rows.push( row );
566 } while ( components.Season === season );
567
568 return ret;
569 };
570
571 }() );