@heinlein-video/rrule
Version:
rrule fork. Includes the src/ folder for typescript sourceMaps
252 lines (208 loc) • 5.53 kB
text/typescript
import { RRule } from './rrule'
import { RRuleSet } from './rruleset'
import { untilStringToDate } from './dateutil'
import { includes, split } from './helpers'
import { Options } from './types'
import { parseString, parseDtstart } from './parsestring'
export interface RRuleStrOptions {
dtstart: Date | null
cache: boolean
unfold: boolean
forceset: boolean
compatible: boolean
tzid: string | null
}
/**
* RRuleStr
* To parse a set of rrule strings
*/
const DEFAULT_OPTIONS: RRuleStrOptions = {
dtstart: null,
cache: false,
unfold: false,
forceset: false,
compatible: false,
tzid: null,
}
export function parseInput(s: string, options: Partial<RRuleStrOptions>) {
const rrulevals: Partial<Options>[] = []
let rdatevals: Date[] = []
const exrulevals: Partial<Options>[] = []
let exdatevals: Date[] = []
const parsedDtstart = parseDtstart(s)
const { dtstart } = parsedDtstart
let { tzid } = parsedDtstart
const lines = splitIntoLines(s, options.unfold)
lines.forEach((line) => {
if (!line) return
const { name, parms, value } = breakDownLine(line)
switch (name.toUpperCase()) {
case 'RRULE':
if (parms.length) {
throw new Error(`unsupported RRULE parm: ${parms.join(',')}`)
}
rrulevals.push(parseString(line))
break
case 'RDATE':
const [, rdateTzid] = /RDATE(?:;TZID=([^:=]+))?/i.exec(line) ?? []
if (rdateTzid && !tzid) {
tzid = rdateTzid
}
rdatevals = rdatevals.concat(parseRDate(value, parms))
break
case 'EXRULE':
if (parms.length) {
throw new Error(`unsupported EXRULE parm: ${parms.join(',')}`)
}
exrulevals.push(parseString(value))
break
case 'EXDATE':
exdatevals = exdatevals.concat(parseRDate(value, parms))
break
case 'DTSTART':
break
default:
throw new Error('unsupported property: ' + name)
}
})
return {
dtstart,
tzid,
rrulevals,
rdatevals,
exrulevals,
exdatevals,
}
}
function buildRule(s: string, options: Partial<RRuleStrOptions>) {
const { rrulevals, rdatevals, exrulevals, exdatevals, dtstart, tzid } =
parseInput(s, options)
const noCache = options.cache === false
if (options.compatible) {
options.forceset = true
options.unfold = true
}
if (
options.forceset ||
rrulevals.length > 1 ||
rdatevals.length ||
exrulevals.length ||
exdatevals.length
) {
const rset = new RRuleSet(noCache)
rset.dtstart(dtstart)
rset.tzid(tzid || undefined)
rrulevals.forEach((val) => {
rset.rrule(new RRule(groomRruleOptions(val, dtstart, tzid), noCache))
})
rdatevals.forEach((date) => {
rset.rdate(date)
})
exrulevals.forEach((val) => {
rset.exrule(new RRule(groomRruleOptions(val, dtstart, tzid), noCache))
})
exdatevals.forEach((date) => {
rset.exdate(date)
})
if (options.compatible && options.dtstart) rset.rdate(dtstart)
return rset
}
const val = rrulevals[0] || {}
return new RRule(
groomRruleOptions(
val,
val.dtstart || options.dtstart || dtstart,
val.tzid || options.tzid || tzid
),
noCache
)
}
export function rrulestr(
s: string,
options: Partial<RRuleStrOptions> = {}
): RRule | RRuleSet {
return buildRule(s, initializeOptions(options))
}
function groomRruleOptions(
val: Partial<Options>,
dtstart?: Date | null,
tzid?: string | null
) {
return {
...val,
dtstart,
tzid,
}
}
function initializeOptions(options: Partial<RRuleStrOptions>) {
const invalid: string[] = []
const keys = Object.keys(options) as (keyof typeof options)[]
const defaultKeys = Object.keys(
DEFAULT_OPTIONS
) as (keyof typeof DEFAULT_OPTIONS)[]
keys.forEach(function (key) {
if (!includes(defaultKeys, key)) invalid.push(key)
})
if (invalid.length) {
throw new Error('Invalid options: ' + invalid.join(', '))
}
return { ...DEFAULT_OPTIONS, ...options }
}
function extractName(line: string) {
if (line.indexOf(':') === -1) {
return {
name: 'RRULE',
value: line,
}
}
const [name, value] = split(line, ':', 1)
return {
name,
value,
}
}
function breakDownLine(line: string) {
const { name, value } = extractName(line)
const parms = name.split(';')
if (!parms) throw new Error('empty property name')
return {
name: parms[0].toUpperCase(),
parms: parms.slice(1),
value,
}
}
function splitIntoLines(s: string, unfold = false) {
s = s && s.trim()
if (!s) throw new Error('Invalid empty string')
// More info about 'unfold' option
// Go head to http://www.ietf.org/rfc/rfc2445.txt
if (!unfold) {
return s.split(/\s/)
}
const lines = s.split('\n')
let i = 0
while (i < lines.length) {
// TODO
const line = (lines[i] = lines[i].replace(/\s+$/g, ''))
if (!line) {
lines.splice(i, 1)
} else if (i > 0 && line[0] === ' ') {
lines[i - 1] += line.slice(1)
lines.splice(i, 1)
} else {
i += 1
}
}
return lines
}
function validateDateParm(parms: string[]) {
parms.forEach((parm) => {
if (!/(VALUE=DATE(-TIME)?)|(TZID=)/.test(parm)) {
throw new Error('unsupported RDATE/EXDATE parm: ' + parm)
}
})
}
function parseRDate(rdateval: string, parms: string[]) {
validateDateParm(parms)
return rdateval.split(',').map((datestr) => untilStringToDate(datestr))
}