@heinlein-video/rrule
Version:
rrule fork. Includes the src/ folder for typescript sourceMaps
505 lines (434 loc) • 13.2 kB
text/typescript
import ENGLISH, { Language } from './i18n'
import { RRule } from '../rrule'
import { Options, ByWeekday } from '../types'
import { Weekday } from '../weekday'
import { isArray, isNumber, isPresent } from '../helpers'
// =============================================================================
// Helper functions
// =============================================================================
/**
* Return true if a value is in an array
*/
const contains = function (arr: string[], val: string) {
return arr.indexOf(val) !== -1
}
// =============================================================================
// ToText
// =============================================================================
export type GetText = (id: string | number | Weekday) => string
const defaultGetText: GetText = (id) => id.toString()
export type DateFormatter = (year: number, month: string, day: number) => string
const defaultDateFormatter: DateFormatter = (
year: number,
month: string,
day: number
) => `${month} ${day}, ${year}`
/**
*
* @param {RRule} rrule
* Optional:
* @param {Function} gettext function
* @param {Object} language definition
* @constructor
*/
export default class ToText {
static IMPLEMENTED: string[][]
private rrule: RRule
private text: string[]
private gettext: GetText
private dateFormatter: DateFormatter
private language: Language
private options: Partial<Options>
private origOptions: Partial<Options>
private bymonthday: Options['bymonthday'] | null
private byweekday: {
allWeeks: ByWeekday[] | null
someWeeks: ByWeekday[] | null
isWeekdays: boolean
isEveryDay: boolean
} | null
constructor(
rrule: RRule,
gettext: GetText = defaultGetText,
language: Language = ENGLISH,
dateFormatter: DateFormatter = defaultDateFormatter
) {
this.text = []
this.language = language || ENGLISH
this.gettext = gettext
this.dateFormatter = dateFormatter
this.rrule = rrule
this.options = rrule.options
this.origOptions = rrule.origOptions
if (this.origOptions.bymonthday) {
const bymonthday = ([] as number[]).concat(this.options.bymonthday)
const bynmonthday = ([] as number[]).concat(this.options.bynmonthday)
bymonthday.sort((a, b) => a - b)
bynmonthday.sort((a, b) => b - a)
// 1, 2, 3, .., -5, -4, -3, ..
this.bymonthday = bymonthday.concat(bynmonthday)
if (!this.bymonthday.length) this.bymonthday = null
}
if (isPresent(this.origOptions.byweekday)) {
const byweekday = !isArray(this.origOptions.byweekday)
? [this.origOptions.byweekday]
: this.origOptions.byweekday
const days = String(byweekday)
this.byweekday = {
allWeeks: byweekday.filter(function (weekday: Weekday) {
return !weekday.n
}),
someWeeks: byweekday.filter(function (weekday: Weekday) {
return Boolean(weekday.n)
}),
isWeekdays:
days.indexOf('MO') !== -1 &&
days.indexOf('TU') !== -1 &&
days.indexOf('WE') !== -1 &&
days.indexOf('TH') !== -1 &&
days.indexOf('FR') !== -1 &&
days.indexOf('SA') === -1 &&
days.indexOf('SU') === -1,
isEveryDay:
days.indexOf('MO') !== -1 &&
days.indexOf('TU') !== -1 &&
days.indexOf('WE') !== -1 &&
days.indexOf('TH') !== -1 &&
days.indexOf('FR') !== -1 &&
days.indexOf('SA') !== -1 &&
days.indexOf('SU') !== -1,
}
const sortWeekDays = function (a: Weekday, b: Weekday) {
return a.weekday - b.weekday
}
this.byweekday.allWeeks.sort(sortWeekDays)
this.byweekday.someWeeks.sort(sortWeekDays)
if (!this.byweekday.allWeeks.length) this.byweekday.allWeeks = null
if (!this.byweekday.someWeeks.length) this.byweekday.someWeeks = null
} else {
this.byweekday = null
}
}
/**
* Test whether the rrule can be fully converted to text.
*
* @param {RRule} rrule
* @return {Boolean}
*/
static isFullyConvertible(rrule: RRule) {
const canConvert = true
if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false
if (rrule.origOptions.until && rrule.origOptions.count) return false
for (const key in rrule.origOptions) {
if (contains(['dtstart', 'wkst', 'freq'], key)) return true
if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false
}
return canConvert
}
isFullyConvertible() {
return ToText.isFullyConvertible(this.rrule)
}
/**
* Perform the conversion. Only some of the frequencies are supported.
* If some of the rrule's options aren't supported, they'll
* be omitted from the output an "(~ approximate)" will be appended.
*
* @return {*}
*/
toString() {
const gettext = this.gettext
if (!(this.options.freq in ToText.IMPLEMENTED)) {
return gettext('RRule error: Unable to fully convert this rrule to text')
}
this.text = [gettext('every')]
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this[RRule.FREQUENCIES[this.options.freq]]()
if (this.options.until) {
this.add(gettext('until'))
const until = this.options.until
this.add(
this.dateFormatter(
until.getUTCFullYear(),
this.language.monthNames[until.getUTCMonth()],
until.getUTCDate()
)
)
} else if (this.options.count) {
this.add(gettext('for'))
.add(this.options.count.toString())
.add(
this.plural(this.options.count) ? gettext('times') : gettext('time')
)
}
if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)'))
return this.text.join('')
}
HOURLY() {
const gettext = this.gettext
if (this.options.interval !== 1) this.add(this.options.interval.toString())
this.add(
this.plural(this.options.interval) ? gettext('hours') : gettext('hour')
)
}
MINUTELY() {
const gettext = this.gettext
if (this.options.interval !== 1) this.add(this.options.interval.toString())
this.add(
this.plural(this.options.interval)
? gettext('minutes')
: gettext('minute')
)
}
DAILY() {
const gettext = this.gettext
if (this.options.interval !== 1) this.add(this.options.interval.toString())
if (this.byweekday && this.byweekday.isWeekdays) {
this.add(
this.plural(this.options.interval)
? gettext('weekdays')
: gettext('weekday')
)
} else {
this.add(
this.plural(this.options.interval) ? gettext('days') : gettext('day')
)
}
if (this.origOptions.bymonth) {
this.add(gettext('in'))
this._bymonth()
}
if (this.bymonthday) {
this._bymonthday()
} else if (this.byweekday) {
this._byweekday()
} else if (this.origOptions.byhour) {
this._byhour()
}
}
WEEKLY() {
const gettext = this.gettext
if (this.options.interval !== 1) {
this.add(this.options.interval.toString()).add(
this.plural(this.options.interval) ? gettext('weeks') : gettext('week')
)
}
if (this.byweekday && this.byweekday.isWeekdays) {
if (this.options.interval === 1) {
this.add(
this.plural(this.options.interval)
? gettext('weekdays')
: gettext('weekday')
)
} else {
this.add(gettext('on')).add(gettext('weekdays'))
}
} else if (this.byweekday && this.byweekday.isEveryDay) {
this.add(
this.plural(this.options.interval) ? gettext('days') : gettext('day')
)
} else {
if (this.options.interval === 1) this.add(gettext('week'))
if (this.origOptions.bymonth) {
this.add(gettext('in'))
this._bymonth()
}
if (this.bymonthday) {
this._bymonthday()
} else if (this.byweekday) {
this._byweekday()
}
}
}
MONTHLY() {
const gettext = this.gettext
if (this.origOptions.bymonth) {
if (this.options.interval !== 1) {
this.add(this.options.interval.toString()).add(gettext('months'))
if (this.plural(this.options.interval)) this.add(gettext('in'))
} else {
// this.add(gettext('MONTH'))
}
this._bymonth()
} else {
if (this.options.interval !== 1) {
this.add(this.options.interval.toString())
}
this.add(
this.plural(this.options.interval)
? gettext('months')
: gettext('month')
)
}
if (this.bymonthday) {
this._bymonthday()
} else if (this.byweekday && this.byweekday.isWeekdays) {
this.add(gettext('on')).add(gettext('weekdays'))
} else if (this.byweekday) {
this._byweekday()
}
}
YEARLY() {
const gettext = this.gettext
if (this.origOptions.bymonth) {
if (this.options.interval !== 1) {
this.add(this.options.interval.toString())
this.add(gettext('years'))
} else {
// this.add(gettext('YEAR'))
}
this._bymonth()
} else {
if (this.options.interval !== 1) {
this.add(this.options.interval.toString())
}
this.add(
this.plural(this.options.interval) ? gettext('years') : gettext('year')
)
}
if (this.bymonthday) {
this._bymonthday()
} else if (this.byweekday) {
this._byweekday()
}
if (this.options.byyearday) {
this.add(gettext('on the'))
.add(this.list(this.options.byyearday, this.nth, gettext('and')))
.add(gettext('day'))
}
if (this.options.byweekno) {
this.add(gettext('in'))
.add(
this.plural((this.options.byweekno as number[]).length)
? gettext('weeks')
: gettext('week')
)
.add(this.list(this.options.byweekno, undefined, gettext('and')))
}
}
private _bymonthday() {
const gettext = this.gettext
if (this.byweekday && this.byweekday.allWeeks) {
this.add(gettext('on'))
.add(
this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or'))
)
.add(gettext('the'))
.add(this.list(this.bymonthday, this.nth, gettext('or')))
} else {
this.add(gettext('on the')).add(
this.list(this.bymonthday, this.nth, gettext('and'))
)
}
// this.add(gettext('DAY'))
}
private _byweekday() {
const gettext = this.gettext
if (this.byweekday.allWeeks && !this.byweekday.isWeekdays) {
this.add(gettext('on')).add(
this.list(this.byweekday.allWeeks, this.weekdaytext)
)
}
if (this.byweekday.someWeeks) {
if (this.byweekday.allWeeks) this.add(gettext('and'))
this.add(gettext('on the')).add(
this.list(this.byweekday.someWeeks, this.weekdaytext, gettext('and'))
)
}
}
private _byhour() {
const gettext = this.gettext
this.add(gettext('at')).add(
this.list(this.origOptions.byhour, undefined, gettext('and'))
)
}
private _bymonth() {
this.add(
this.list(this.options.bymonth, this.monthtext, this.gettext('and'))
)
}
nth(n: number | string) {
n = parseInt(n.toString(), 10)
let nth: string
const gettext = this.gettext
if (n === -1) return gettext('last')
const npos = Math.abs(n)
switch (npos) {
case 1:
case 21:
case 31:
nth = npos + gettext('st')
break
case 2:
case 22:
nth = npos + gettext('nd')
break
case 3:
case 23:
nth = npos + gettext('rd')
break
default:
nth = npos + gettext('th')
}
return n < 0 ? nth + ' ' + gettext('last') : nth
}
monthtext(m: number) {
return this.language.monthNames[m - 1]
}
weekdaytext(wday: Weekday | number) {
const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday()
return (
((wday as Weekday).n ? this.nth((wday as Weekday).n) + ' ' : '') +
this.language.dayNames[weekday]
)
}
plural(n: number) {
return n % 100 !== 1
}
add(s: string) {
this.text.push(' ')
this.text.push(s)
return this
}
list(
arr: ByWeekday | ByWeekday[],
callback?: GetText,
finalDelim?: string,
delim = ','
) {
if (!isArray(arr)) {
arr = [arr]
}
const delimJoin = function (
array: string[],
delimiter: string,
finalDelimiter: string
) {
let list = ''
for (let i = 0; i < array.length; i++) {
if (i !== 0) {
if (i === array.length - 1) {
list += ' ' + finalDelimiter + ' '
} else {
list += delimiter + ' '
}
}
list += array[i]
}
return list
}
callback =
callback ||
function (o) {
return o.toString()
}
const realCallback = (arg: ByWeekday) => {
return callback && callback.call(this, arg)
}
if (finalDelim) {
return delimJoin(arr.map(realCallback), delim, finalDelim)
} else {
return arr.map(realCallback).join(delim + ' ')
}
}
}