Merge "DateTimeInputWidget: Only show calendar when focusing date components, not...
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets.datetime / DiscordianDateTimeFormatter.js
1 ( function ( $, mw ) {
2
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 );
21
22 // Parent constructor
23 mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].call( this, config );
24 };
25
26 /* Setup */
27
28 OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
29
30 /* Static */
31
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 };
41
42 /* Methods */
43
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;
65
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;
76
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;
87
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;
103
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;
121
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;
145
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;
160
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;
171
172 default:
173 return mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
174 }
175
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 }
190
191 return spec;
192 };
193
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 ];
214
215 if ( !( date instanceof Date ) ) {
216 date = this.defaultDate;
217 }
218
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 }
242
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 }
255
256 return ret;
257 };
258
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 };
269
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;
282
283 if ( delta === 0 ) {
284 return components;
285 }
286
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 }
335
336 switch ( mode ) {
337 case 'overflow':
338 case 'clip':
339 case 'wrap':
340 }
341
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 }
447
448 return components;
449 };
450
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 ];
458
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 }
471
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 }
481
482 return date;
483 };
484
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 };
501
502 /**
503 * @inheritdoc
504 */
505 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
506 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
507 };
508
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 );
515
516 return components1.Year === components2.Year && components1.Season === components2.Season;
517 };
518
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 ];
531
532 if ( !( date instanceof Date ) ) {
533 date = this.defaultDate;
534 }
535
536 components = this.getComponentsFromDate( date );
537 components.Day = 1;
538 season = components.Season;
539
540 ret.header = seasons[ season - 1 ] + ' ' + components.Year;
541
542 if ( seasonStart[ season - 1 ] ) {
543 this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
544 }
545
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 };
556
557 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
558 if ( components.Day !== 'tib' && i === 3 ) {
559 row[ ++i ] = null;
560 }
561 }
562
563 ret.rows.push( row );
564 } while ( components.Season === season );
565
566 return ret;
567 };
568
569 }( jQuery, mediaWiki ) );