Add parameter to API modules to apply change tags to log entries
[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 type: 'number',
71 size: 4,
72 zeropad: false
73 };
74 break;
75
76 case 'season|#':
77 spec = {
78 component: 'Season',
79 type: 'number',
80 size: 1,
81 intercalarySize: { 1: 0 },
82 zeropad: false
83 };
84 break;
85
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;
100
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;
117
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;
140
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;
154
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;
164
165 default:
166 return mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
167 }
168
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 }
183
184 return spec;
185 };
186
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 ];
207
208 if ( !( date instanceof Date ) ) {
209 date = this.defaultDate;
210 }
211
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 }
235
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 }
248
249 return ret;
250 };
251
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 };
262
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;
275
276 if ( delta === 0 ) {
277 return components;
278 }
279
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 }
328
329 switch ( mode ) {
330 case 'overflow':
331 case 'clip':
332 case 'wrap':
333 }
334
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 }
440
441 return components;
442 };
443
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 ];
451
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 }
464
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 }
474
475 return date;
476 };
477
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 };
494
495 /**
496 * @inheritdoc
497 */
498 mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
499 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
500 };
501
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 );
508
509 return components1.Year === components2.Year && components1.Season === components2.Season;
510 };
511
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 ];
524
525 if ( !( date instanceof Date ) ) {
526 date = this.defaultDate;
527 }
528
529 components = this.getComponentsFromDate( date );
530 components.Day = 1;
531 season = components.Season;
532
533 ret.header = seasons[ season - 1 ] + ' ' + components.Year;
534
535 if ( seasonStart[ season - 1 ] ) {
536 this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
537 }
538
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 };
549
550 this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
551 if ( components.Day !== 'tib' && i === 3 ) {
552 row[ ++i ] = null;
553 }
554 }
555
556 ret.rows.push( row );
557 } while ( components.Season === season );
558
559 return ret;
560 };
561
562 }( jQuery, mediaWiki ) );