1 /* eslint-disable no-restricted-properties */
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.
9 * Being intended mainly for testing, niceties like i18n and better
10 * configurability have been omitted.
12 * [1]: https://en.wikipedia.org/wiki/Discordian_calendar
15 * @extends mw.widgets.datetime.DateTimeFormatter
18 * @param {Object} [config] Configuration options
20 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
= function MwWidgetsDatetimeDiscordianDateTimeFormatter( config
) {
21 config
= $.extend( {}, config
);
24 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
[ 'super' ].call( this, config
);
29 OO
.inheritClass( mw
.widgets
.datetime
.DiscordianDateTimeFormatter
, mw
.widgets
.datetime
.DateTimeFormatter
);
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}'
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
64 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.getFieldForTag = function ( tag
, params
) {
67 switch ( tag
+ '|' + params
[ 0 ] ) {
71 calendarComponent
: true,
81 calendarComponent
: true,
84 intercalarySize
: { 1: 0 },
92 calendarComponent
: true,
94 intercalarySize
: { 1: 0 },
108 calendarComponent
: true,
111 intercalarySize
: { 1: 0 },
117 3: 'Prickle-Prickle',
127 calendarComponent
: true,
130 intercalarySize
: { 1: 13 },
131 zeropad
: params
[ 0 ] === '0',
132 formatValue: function ( v
) {
134 return 'St. Tib\'s Day';
136 return mw
.widgets
.datetime
.DateTimeFormatter
.prototype.formatSpecValue
.call( this, v
);
138 parseValue: function ( v
) {
139 if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v
) ) {
142 return mw
.widgets
.datetime
.DateTimeFormatter
.prototype.parseSpecValue
.call( this, v
);
154 component
: tag
.charAt( 0 ).toUpperCase() + tag
.slice( 1 ),
155 calendarComponent
: false,
158 zeropad
: params
[ 0 ] === '0'
162 case 'millisecond|#':
163 case 'millisecond|0':
165 component
: 'Millisecond',
166 calendarComponent
: false,
169 zeropad
: params
[ 0 ] === '0'
174 return mw
.widgets
.datetime
.DiscordianDateTimeFormatter
[ 'super' ].prototype.getFieldForTag
.call( this, tag
, params
);
178 if ( spec
.editable
=== undefined ) {
179 spec
.editable
= true;
181 if ( spec
.component
!== 'Day' ) {
182 spec
.formatValue
= this.formatSpecValue
;
183 spec
.parseValue
= this.parseSpecValue
;
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
; } )
197 * Get components from a Date object
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
210 * @param {Date|null} date
211 * @return {Object} Components
213 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.getComponentsFromDate = function ( date
) {
215 monthDays
= [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
217 if ( !( date
instanceof Date
) ) {
218 date
= this.defaultDate
;
222 day
= date
.getDate();
223 month
= date
.getMonth();
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()
233 day
= date
.getUTCDate();
234 month
= date
.getUTCMonth();
236 Year
: date
.getUTCFullYear() + 1166,
237 Hour
: date
.getUTCHours(),
238 Minute
: date
.getUTCMinutes(),
239 Second
: date
.getUTCSeconds(),
240 Millisecond
: date
.getUTCMilliseconds(),
245 if ( month
=== 1 && day
=== 29 ) {
249 ret
.intercalary
= '1';
251 day
= monthDays
[ month
] + day
- 1;
252 ret
.Season
= Math
.floor( day
/ 73 ) + 1;
253 ret
.Day
= ( day
% 73 ) + 1;
255 ret
.intercalary
= '';
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
273 * Adjust the components directly
276 * @param {Object} components Modified in place
277 * @param {string} component
278 * @param {number} delta
279 * @param {string} mode
280 * @return {Object} components
282 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.adjustComponentInternal = function ( components
, component
, delta
, mode
) {
283 var i
, min
, max
, range
, next
, preTib
, postTib
, wasTib
;
289 switch ( component
) {
301 if ( components
.Day
=== 'tib' ) {
302 components
.Day
= 59; // Could choose either one...
303 components
.Season
= 1;
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;
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;
356 components
.Day
+= delta
;
357 if ( components
.Day
< min
) {
360 components
.Day
= max
;
361 this.adjustComponentInternal( components
, 'Season', -1, mode
);
364 components
.Day
= max
;
367 components
.Day
= min
;
372 if ( components
.Day
> max
) {
375 components
.Day
= min
;
376 this.adjustComponentInternal( components
, 'Season', 1, mode
);
379 components
.Day
= min
;
382 components
.Day
= max
;
390 if ( component
=== 'Week' ) {
394 if ( components
.Day
=== 'tib' ) {
396 components
.Season
= 1;
400 if ( components
.Day
=== 'tib' && ( component
=== 'Season' || component
=== 'Year' ) ) {
401 components
.Day
= 59; // Could choose either one...
406 i
= Math
.abs( delta
);
407 delta
= delta
< 0 ? -1 : 1;
409 components
[ component
] += delta
;
410 if ( components
[ component
] < min
) {
411 components
[ component
] = max
;
412 components
= this.adjustComponentInternal( components
, next
, -1, mode
);
414 if ( components
[ component
] > max
) {
415 components
[ component
] = min
;
416 components
= this.adjustComponentInternal( components
, next
, 1, mode
);
419 if ( wasTib
&& components
.Season
=== 1 && this.isLeapYear( components
.Year
) ) {
420 components
.Day
= 'tib';
424 range
= max
- min
+ 1;
425 components
[ component
] += delta
;
426 while ( components
[ component
] < min
) {
427 components
[ component
] += range
;
429 while ( components
[ component
] > max
) {
430 components
[ component
] -= range
;
434 components
[ component
] += delta
;
435 if ( components
[ component
] < min
) {
436 components
[ component
] = min
;
438 if ( components
[ component
] > max
) {
439 components
[ component
] = max
;
443 if ( components
.Day
=== 'tib' &&
444 ( components
.Season
!== 1 || !this.isLeapYear( components
.Year
) )
446 components
.Day
= 59; // Could choose either one...
456 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.getDateFromComponents = function ( components
) {
457 var month
, day
, days
,
459 monthDays
= [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ];
461 components
= $.extend( {}, this.getComponentsFromDate( null ), components
);
462 if ( components
.Day
=== 'tib' ) {
466 days
= components
.Season
* 73 + components
.Day
- 74;
468 while ( days
>= monthDays
[ month
+ 1 ] ) {
471 day
= days
- monthDays
[ month
] + 1;
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
);
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
);
488 * Get whether the year is a leap year
491 * @param {number} year
494 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.isLeapYear = function ( year
) {
498 } else if ( year
% 100 ) {
501 return ( year
% 400 ) === 0;
507 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.getCalendarHeadings = function () {
508 return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
514 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.sameCalendarGrid = function ( date1
, date2
) {
515 var components1
= this.getComponentsFromDate( date1
),
516 components2
= this.getComponentsFromDate( date2
);
518 return components1
.Year
=== components2
.Year
&& components1
.Season
=== components2
.Season
;
524 mw
.widgets
.datetime
.DiscordianDateTimeFormatter
.prototype.getCalendarData = function ( date
) {
525 var dt
, components
, season
, i
, row
,
528 weekComponent
: 'Week',
529 monthComponent
: 'Season'
531 seasons
= [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
532 seasonStart
= [ 0, -3, -1, -4, -2 ];
534 if ( !( date
instanceof Date
) ) {
535 date
= this.defaultDate
;
538 components
= this.getComponentsFromDate( date
);
540 season
= components
.Season
;
542 ret
.header
= seasons
[ season
- 1 ] + ' ' + components
.Year
;
544 if ( seasonStart
[ season
- 1 ] ) {
545 this.adjustComponentInternal( components
, 'Day', seasonStart
[ season
- 1 ], 'overflow' );
551 for ( i
= 0; i
< 6; i
++ ) {
552 dt
= this.getDateFromComponents( components
);
554 display
: components
.Day
=== 'tib' ? 'Tib' : String( components
.Day
),
556 extra
: components
.Season
< season
? 'prev' : components
.Season
> season
? 'next' : null
559 this.adjustComponentInternal( components
, 'Day', 1, 'overflow' );
560 if ( components
.Day
!== 'tib' && i
=== 3 ) {
565 ret
.rows
.push( row
);
566 } while ( components
.Season
=== season
);