X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/resources/src/mediawiki.widgets.datetime/DiscordianDateTimeFormatter.js diff --git a/resources/src/mediawiki.widgets.datetime/DiscordianDateTimeFormatter.js b/resources/src/mediawiki.widgets.datetime/DiscordianDateTimeFormatter.js new file mode 100644 index 00000000..6db2d062 --- /dev/null +++ b/resources/src/mediawiki.widgets.datetime/DiscordianDateTimeFormatter.js @@ -0,0 +1,569 @@ +( function ( $, mw ) { + + /** + * Provides various methods needed for formatting dates and times. This + * implementation implments the [Discordian calendar][1], 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. + * + * [1]: https://en.wikipedia.org/wiki/Discordian_calendar + * + * @class + * @extends mw.widgets.datetime.DateTimeFormatter + * + * @constructor + * @param {Object} [config] Configuration options + */ + mw.widgets.datetime.DiscordianDateTimeFormatter = function MwWidgetsDatetimeDiscordianDateTimeFormatter( config ) { + config = $.extend( {}, config ); + + // Parent constructor + mw.widgets.datetime.DiscordianDateTimeFormatter[ 'super' ].call( this, config ); + }; + + /* Setup */ + + OO.inheritClass( mw.widgets.datetime.DiscordianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter ); + + /* Static */ + + /** + * @inheritdoc + */ + 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 */ + + /** + * @inheritdoc + * + * 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 + */ + mw.widgets.datetime.DiscordianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) { + var 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( + null, $.map( spec.values, function ( v ) { return 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 ) { + var ret, day, month, + 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 ) { + var 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' ) { + // For sanity + 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 ) { + var month, day, days, + date = new Date(), + monthDays = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ]; + + components = $.extend( {}, this.getComponentsFromDate( null ), components ); + if ( components.Day === 'tib' ) { + month = 1; + day = 29; + } else { + 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 ) { + var 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 ) { + var dt, components, season, i, row, + 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; + } + + components = this.getComponentsFromDate( date ); + components.Day = 1; + 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 { + row = []; + for ( i = 0; i < 6; i++ ) { + 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; + }; + +}( jQuery, mediaWiki ) );