( function () {
/**
* @classdesc DateTimeFormatter for the Discordian calendar.
*
* Provides various methods needed for formatting dates and times. This
* implementation implements the [Discordian calendar](https://en.wikipedia.org/wiki/Discordian_calendar),
* mainly for testing with something very different from the usual Gregorian
* calendar.
*
* Being intended mainly for testing, niceties like i18n and better
* configurability have been omitted.
*
* @class
* @extends mw.widgets.datetime.DateTimeFormatter
*
* @constructor
* @description Create an instance of `mw.widgets.datetime.DiscordianDateTimeFormatter`.
* @param {Object} [config] Configuration options
*/
mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) {
config = Object.assign( {}, config );
// Parent constructor
mw.widgets.datetime.DiscordianDateTimeFormatter.super.call( this, config );
};
/* Setup */
OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
/* Static */
/**
* Default format specifications.
*
* See the `format` parameter in {@link mw.widgets.datetime.DateTimeFormatter}.
*
* @memberof mw.widgets.datetime.DiscordianDateTimeFormatter
* @type {Object.<string,string>}
* @name formats
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.static.formats = {
'@time': '${hour|0}:${minute|0}:${second|0}',
'@date': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#}',
'@datetime': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
'@default': '$!{dow|full}${not-intercalary|1|, }${season|full}${not-intercalary|1| }${day|#}, ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
};
/* Methods */
/**
* Turn a tag into a field specification object.
*
* Additional fields implemented here are:
* - ${year|#}: Year as a number
* - ${season|#}: Season as a number
* - ${season|full}: Season as a string
* - ${day|#}: Day of the month as a number
* - ${day|0}: Day of the month as a number with leading 0
* - ${dow|full}: Day of the week as a string
* - ${hour|#}: Hour as a number
* - ${hour|0}: Hour as a number with leading 0
* - ${minute|#}: Minute as a number
* - ${minute|0}: Minute as a number with leading 0
* - ${second|#}: Second as a number
* - ${second|0}: Second as a number with leading 0
* - ${millisecond|#}: Millisecond as a number
* - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
*
* @protected
* @param {string} tag
* @param {string[]} params
* @return {FieldSpecificationObject} Field specification object, or null if the tag+params are unrecognized.
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
let spec = null;
switch ( tag + '|' + params[ 0 ] ) {
case 'year|#':
spec = {
component: 'Year',
calendarComponent: true,
type: 'number',
size: 4,
zeropad: false
};
break;
case 'season|#':
spec = {
component: 'Season',
calendarComponent: true,
type: 'number',
size: 1,
intercalarySize: { 1: 0 },
zeropad: false
};
break;
case 'season|full':
spec = {
component: 'Season',
calendarComponent: true,
type: 'string',
intercalarySize: { 1: 0 },
values: {
1: 'Chaos',
2: 'Discord',
3: 'Confusion',
4: 'Bureaucracy',
5: 'The Aftermath'
}
};
break;
case 'dow|full':
spec = {
component: 'DOW',
calendarComponent: true,
editable: false,
type: 'string',
intercalarySize: { 1: 0 },
values: {
'-1': 'N/A',
0: 'Sweetmorn',
1: 'Boomtime',
2: 'Pungenday',
3: 'Prickle-Prickle',
4: 'Setting Orange'
}
};
break;
case 'day|#':
case 'day|0':
spec = {
component: 'Day',
calendarComponent: true,
type: 'string',
size: 2,
intercalarySize: { 1: 13 },
zeropad: params[ 0 ] === '0',
formatValue: function ( v ) {
if ( v === 'tib' ) {
return 'St. Tib\'s Day';
}
return mw.widgets.datetime.DateTimeFormatter.prototype.formatSpecValue.call( this, v );
},
parseValue: function ( v ) {
if ( /^\s*(st.?\s*)?tib('?s)?(\s*day)?\s*$/i.test( v ) ) {
return 'tib';
}
return mw.widgets.datetime.DateTimeFormatter.prototype.parseSpecValue.call( this, v );
}
};
break;
case 'hour|#':
case 'hour|0':
case 'minute|#':
case 'minute|0':
case 'second|#':
case 'second|0':
spec = {
component: tag.charAt( 0 ).toUpperCase() + tag.slice( 1 ),
calendarComponent: false,
type: 'number',
size: 2,
zeropad: params[ 0 ] === '0'
};
break;
case 'millisecond|#':
case 'millisecond|0':
spec = {
component: 'Millisecond',
calendarComponent: false,
type: 'number',
size: 3,
zeropad: params[ 0 ] === '0'
};
break;
default:
return mw.widgets.datetime.DiscordianDateTimeFormatter.super.prototype.getFieldForTag.call( this, tag, params );
}
if ( spec ) {
if ( spec.editable === undefined ) {
spec.editable = true;
}
if ( spec.component !== 'Day' ) {
spec.formatValue = this.formatSpecValue;
spec.parseValue = this.parseSpecValue;
}
if ( spec.values ) {
spec.size = Math.max.apply(
// eslint-disable-next-line no-jquery/no-map-util
null, $.map( spec.values, ( v ) => v.length )
);
}
}
return spec;
};
/**
* Get components from a Date object.
*
* Components are:
* - Year {number}
* - Season {number} 1-5
* - Day {number|string} 1-73 or 'tib'
* - DOW {number} 0-4, or -1 on St. Tib's Day
* - Hour {number} 0-23
* - Minute {number} 0-59
* - Second {number} 0-59
* - Millisecond {number} 0-999
* - intercalary {string} '1' on St. Tib's Day
*
* @param {Date|null} date
* @return {Object} Components
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
let ret, day, month;
const monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
if ( !( date instanceof Date ) ) {
date = this.defaultDate;
}
if ( this.local ) {
day = date.getDate();
month = date.getMonth();
ret = {
Year: date.getFullYear() + 1166,
Hour: date.getHours(),
Minute: date.getMinutes(),
Second: date.getSeconds(),
Millisecond: date.getMilliseconds(),
zone: date.getTimezoneOffset()
};
} else {
day = date.getUTCDate();
month = date.getUTCMonth();
ret = {
Year: date.getUTCFullYear() + 1166,
Hour: date.getUTCHours(),
Minute: date.getUTCMinutes(),
Second: date.getUTCSeconds(),
Millisecond: date.getUTCMilliseconds(),
zone: 0
};
}
if ( month === 1 && day === 29 ) {
ret.Season = 1;
ret.Day = 'tib';
ret.DOW = -1;
ret.intercalary = '1';
} else {
day = monthDays[ month ] + day - 1;
ret.Season = Math.floor( day / 73 ) + 1;
ret.Day = ( day % 73 ) + 1;
ret.DOW = day % 5;
ret.intercalary = '';
}
return ret;
};
/**
* @inheritdoc
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
return this.getDateFromComponents(
this.adjustComponentInternal(
this.getComponentsFromDate( date ), component, delta, mode
)
);
};
/**
* Adjust the components directly.
*
* @private
* @param {Object} components Modified in place
* @param {string} component
* @param {number} delta
* @param {string} mode
* @return {Object} components
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.adjustComponentInternal = function ( components, component, delta, mode ) {
let i, min, max, range, next, preTib, postTib, wasTib;
if ( delta === 0 ) {
return components;
}
switch ( component ) {
case 'Year':
min = 1166;
max = 11165;
next = null;
break;
case 'Season':
min = 1;
max = 5;
next = 'Year';
break;
case 'Week':
if ( components.Day === 'tib' ) {
components.Day = 59; // Could choose either one...
components.Season = 1;
}
min = 1;
max = 73;
next = 'Season';
break;
case 'Day':
min = 1;
max = 73;
next = 'Season';
break;
case 'Hour':
min = 0;
max = 23;
next = 'Day';
break;
case 'Minute':
min = 0;
max = 59;
next = 'Hour';
break;
case 'Second':
min = 0;
max = 59;
next = 'Minute';
break;
case 'Millisecond':
min = 0;
max = 999;
next = 'Second';
break;
default:
return components;
}
switch ( mode ) {
case 'overflow':
case 'clip':
case 'wrap':
}
if ( component === 'Day' ) {
i = Math.abs( delta );
delta = delta < 0 ? -1 : 1;
preTib = delta > 0 ? 59 : 60;
postTib = delta > 0 ? 60 : 59;
while ( i-- > 0 ) {
if ( components.Day === preTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
components.Day = 'tib';
} else if ( components.Day === 'tib' ) {
components.Day = postTib;
components.Season = 1;
} else {
components.Day += delta;
if ( components.Day < min ) {
switch ( mode ) {
case 'overflow':
components.Day = max;
this.adjustComponentInternal( components, 'Season', -1, mode );
break;
case 'wrap':
components.Day = max;
break;
case 'clip':
components.Day = min;
i = 0;
break;
}
}
if ( components.Day > max ) {
switch ( mode ) {
case 'overflow':
components.Day = min;
this.adjustComponentInternal( components, 'Season', 1, mode );
break;
case 'wrap':
components.Day = min;
break;
case 'clip':
components.Day = max;
i = 0;
break;
}
}
}
}
} else {
if ( component === 'Week' ) {
component = 'Day';
delta *= 5;
}
if ( components.Day === 'tib' ) {
components.Season = 1;
}
switch ( mode ) {
case 'overflow':
if ( components.Day === 'tib' && ( component === 'Season' || component === 'Year' ) ) {
components.Day = 59; // Could choose either one...
wasTib = true;
} else {
wasTib = false;
}
i = Math.abs( delta );
delta = delta < 0 ? -1 : 1;
while ( i-- > 0 ) {
components[ component ] += delta;
if ( components[ component ] < min ) {
components[ component ] = max;
components = this.adjustComponentInternal( components, next, -1, mode );
}
if ( components[ component ] > max ) {
components[ component ] = min;
components = this.adjustComponentInternal( components, next, 1, mode );
}
}
if ( wasTib && components.Season === 1 && this.isLeapYear( components.Year ) ) {
components.Day = 'tib';
}
break;
case 'wrap':
range = max - min + 1;
components[ component ] += delta;
while ( components[ component ] < min ) {
components[ component ] += range;
}
while ( components[ component ] > max ) {
components[ component ] -= range;
}
break;
case 'clip':
components[ component ] += delta;
if ( components[ component ] < min ) {
components[ component ] = min;
}
if ( components[ component ] > max ) {
components[ component ] = max;
}
break;
}
if ( components.Day === 'tib' &&
( components.Season !== 1 || !this.isLeapYear( components.Year ) )
) {
components.Day = 59; // Could choose either one...
}
}
return components;
};
/**
* @inheritdoc
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
let month, day;
const
date = new Date(),
monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ];
components = Object.assign( {}, this.getComponentsFromDate( null ), components );
if ( components.Day === 'tib' ) {
month = 1;
day = 29;
} else {
const days = components.Season * 73 + components.Day - 74;
month = 0;
while ( days >= monthDays[ month + 1 ] ) {
month++;
}
day = days - monthDays[ month ] + 1;
}
if ( components.zone ) {
// Can't just use the constructor because that's stupid about ancient years.
date.setFullYear( components.Year - 1166, month, day );
date.setHours( components.Hour, components.Minute, components.Second, components.Millisecond );
} else {
// Date.UTC() is stupid about ancient years too.
date.setUTCFullYear( components.Year - 1166, month, day );
date.setUTCHours( components.Hour, components.Minute, components.Second, components.Millisecond );
}
return date;
};
/**
* Get whether the year is a leap year.
*
* @private
* @param {number} year
* @return {boolean}
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.isLeapYear = function ( year ) {
year -= 1166;
if ( year % 4 ) {
return false;
} else if ( year % 100 ) {
return true;
}
return ( year % 400 ) === 0;
};
/**
* @inheritdoc
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarHeadings = function () {
return [ 'SM', 'BT', 'PD', 'PP', null, 'SO' ];
};
/**
* @inheritdoc
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
const components1 = this.getComponentsFromDate( date1 ),
components2 = this.getComponentsFromDate( date2 );
return components1.Year === components2.Year && components1.Season === components2.Season;
};
/**
* @inheritdoc
*/
mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
const
ret = {
dayComponent: 'Day',
weekComponent: 'Week',
monthComponent: 'Season'
},
seasons = [ 'Chaos', 'Discord', 'Confusion', 'Bureaucracy', 'The Aftermath' ],
seasonStart = [ 0, -3, -1, -4, -2 ];
if ( !( date instanceof Date ) ) {
date = this.defaultDate;
}
const components = this.getComponentsFromDate( date );
components.Day = 1;
const season = components.Season;
ret.header = seasons[ season - 1 ] + ' ' + components.Year;
if ( seasonStart[ season - 1 ] ) {
this.adjustComponentInternal( components, 'Day', seasonStart[ season - 1 ], 'overflow' );
}
ret.rows = [];
do {
const row = [];
for ( let i = 0; i < 6; i++ ) {
const dt = this.getDateFromComponents( components );
row[ i ] = {
display: components.Day === 'tib' ? 'Tib' : String( components.Day ),
date: dt,
extra: components.Season < season ? 'prev' : components.Season > season ? 'next' : null
};
this.adjustComponentInternal( components, 'Day', 1, 'overflow' );
if ( components.Day !== 'tib' && i === 3 ) {
row[ ++i ] = null;
}
}
ret.rows.push( row );
} while ( components.Season === season );
return ret;
};
}() );