diff --git a/src/dateutil.ts b/src/dateutil.ts index 9ed09b18..ecf1e874 100644 --- a/src/dateutil.ts +++ b/src/dateutil.ts @@ -161,7 +161,7 @@ export namespace dateutil { }) } - export const timeToUntilString = function (time: number, utc = true) { + export const toRfc5545DateTime = function (time: number, utc = true) { const date = new Date(time) return [ padStart(date.getUTCFullYear().toString(), 4, '0'), @@ -175,11 +175,20 @@ export namespace dateutil { ].join('') } - export const untilStringToDate = function (until: string) { + export const toRfc5545Date = function (time: number) { + const date = new Date(time) + return [ + padStart(date.getUTCFullYear().toString(), 4, '0'), + padStart(date.getUTCMonth() + 1, 2, '0'), + padStart(date.getUTCDate(), 2, '0') + ].join('') + } + + export const fromRfc5545DateTime = function (dt: string) { const re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/ - const bits = re.exec(until) + const bits = re.exec(dt) - if (!bits) throw new Error(`Invalid UNTIL value: ${until}`) + if (!bits) throw new Error(`Invalid date-time value: ${dt}`) return new Date( Date.UTC( @@ -193,6 +202,21 @@ export namespace dateutil { ) } + export const fromRfc5545Date = function (dt: string) { + const re = /^(\d{4})(\d{2})(\d{2})$/ + const bits = re.exec(dt) + + if (!bits) throw new Error(`Invalid date value: ${dt}`) + + return new Date( + Date.UTC( + parseInt(bits[1], 10), + parseInt(bits[2], 10) - 1, + parseInt(bits[3], 10) + ) + ) + } + } export default dateutil diff --git a/src/datewithzone.ts b/src/datewithzone.ts index 8ae3ed09..80247f60 100644 --- a/src/datewithzone.ts +++ b/src/datewithzone.ts @@ -15,7 +15,7 @@ export class DateWithZone { } public toString () { - const datestr = dateutil.timeToUntilString(this.date.getTime(), this.isUTC) + const datestr = dateutil.toRfc5545DateTime(this.date.getTime(), this.isUTC) if (!this.isUTC) { return `;TZID=${this.tzid}:${datestr}` } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index f58e269d..98d5e3a6 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -129,7 +129,7 @@ export default class ToText { if (rrule.origOptions.until && rrule.origOptions.count) return false for (let key in rrule.origOptions) { - if (contains(['dtstart', 'wkst', 'freq'], key)) return true + if (contains(['dtstart', 'dtend', 'wkst', 'freq'], key)) return true if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false } diff --git a/src/optionstostring.ts b/src/optionstostring.ts index 6321712e..7657f3fc 100644 --- a/src/optionstostring.ts +++ b/src/optionstostring.ts @@ -1,4 +1,4 @@ -import { Options } from './types' +import { Options, DateTimeProperty, DateTimeValue } from './types' import RRule, { DEFAULT_OPTIONS } from './rrule' import { includes, isPresent, isArray, isNumber, toArray } from './helpers' import { Weekday } from './weekday' @@ -8,6 +8,7 @@ import { DateWithZone } from './datewithzone' export function optionsToString (options: Partial) { let rrule: string[][] = [] let dtstart: string = '' + let dtend: string = '' const keys: (keyof Options)[] = Object.keys(options) as (keyof Options)[] const defaultKeys = Object.keys(DEFAULT_OPTIONS) @@ -56,14 +57,37 @@ export function optionsToString (options: Partial) { return new Weekday(wday) }).toString() - break + case 'DTSTART': - dtstart = buildDtstart(value, options.tzid) + dtstart = formatDateTime(value, options, DateTimeProperty.START) + break + + case 'DTEND': + dtend = formatDateTime(value, options, DateTimeProperty.END) + break + + case 'DTVALUE': break case 'UNTIL': - outValue = dateutil.timeToUntilString(value, !options.tzid) + /** + * From [RFC 5545](https://tools.ietf.org/html/rfc5545): + * + * 3.3.10. Recurrence Rule + * + * The value of the UNTIL rule part MUST have the same value type as the + * "DTSTART" property. Furthermore, if the "DTSTART" property is specified as + * a date with local time, then the UNTIL rule part MUST also be specified as + * a date with local time. If the "DTSTART" property is specified as a date + * with UTC time or a date with local time and time zone reference, then the + * UNTIL rule part MUST be specified as a date with UTC time. + */ + if (options.dtvalue === DateTimeValue.DATE) { + outValue = dateutil.toRfc5545Date(value) + } else { + outValue = dateutil.toRfc5545DateTime(value, !!options.tzid) + } break default: @@ -89,13 +113,23 @@ export function optionsToString (options: Partial) { ruleString = `RRULE:${rules}` } - return [ dtstart, ruleString ].filter(x => !!x).join('\n') + return [ dtstart, dtend, ruleString ].filter(x => !!x).join('\n') } -function buildDtstart (dtstart?: number, tzid?: string | null) { - if (!dtstart) { +function formatDateTime (dt?: number, options: Partial = {}, prop = DateTimeProperty.START) { + if (!dt) { return '' } - - return 'DTSTART' + new DateWithZone(new Date(dtstart), tzid).toString() + let prefix = prop.toString() + if (options.dtvalue) { + prefix += ';VALUE=' + options.dtvalue.toString() + } + if (!options.tzid) { + if (options.dtvalue === DateTimeValue.DATE) { + return prefix + ':' + dateutil.toRfc5545Date(dt) + } else { + return prefix + ':' + dateutil.toRfc5545DateTime(dt, false) + } + } + return prefix + new DateWithZone(new Date(dt), options.tzid).toString() } diff --git a/src/parsestring.ts b/src/parsestring.ts index b1b38e53..86f1691a 100644 --- a/src/parsestring.ts +++ b/src/parsestring.ts @@ -1,28 +1,94 @@ -import { Options, Frequency } from './types' +import { Options, Frequency, DateTimeProperty, DateTimeValue } from './types' import { Weekday } from './weekday' import dateutil from './dateutil' import { Days } from './rrule' export function parseString (rfcString: string): Partial { const options = rfcString.split('\n').map(parseLine).filter(x => x !== null) - return { ...options[0], ...options[1] } + /** + * From [RFC 5545](https://tools.ietf.org/html/rfc5545): + * + * 3.8.2.2. Date-Time End ("DTEND") + * + * The value type of this property MUST be the same as the "DTSTART" property, and its + * value MUST be later in time than the value of the "DTSTART" property. Furthermore, + * this property MUST be specified as a date with local time if and only if the + * "DTSTART" property is also specified as a date with local time. + */ + return options.reduce((acc: Partial, cur: Partial) => { + let existing + if (cur.dtstart) { + if (acc.dtstart) { + throw new Error('Invalid rule: DTSTART must occur only once') + } + if (acc.dtend && acc.dtend.valueOf() <= cur.dtstart.valueOf()) { + throw new Error('Invalid rule: DTEND must be later than DTSTART') + } + existing = acc.dtend + } + if (cur.dtend) { + if (acc.dtend) { + throw new Error('Invalid rule: DTEND must occur only once') + } + if (acc.dtstart && acc.dtstart.valueOf() >= cur.dtend.valueOf()) { + throw new Error('Invalid rule: DTEND must be later than DTSTART') + } + existing = acc.dtstart + } + if (existing && acc.dtvalue !== cur.dtvalue) { + // Different value types. + throw new Error('Invalid rule: DTSTART and DTEND must have the same value type') + } else if (existing && acc.tzid !== cur.tzid) { + // Different timezones. + throw new Error('Invalid rule: DTSTART and DTEND must have the same timezone') + } + return Object.assign(acc, cur) + }, {}) || {} } -export function parseDtstart (line: string) { +export function parseDateTime (line: string, prop = DateTimeProperty.START): Partial { const options: Partial = {} - const dtstartWithZone = /DTSTART(?:;TZID=([^:=]+?))?(?::|=)([^;\s]+)/i.exec(line) + const dtWithZone = new RegExp( + `${prop}(?:;TZID=([^:=]+?))?(?:;VALUE=(DATE|DATE-TIME))?(?::|=)([^;\\s]+)`, 'i' + ).exec(line) - if (!dtstartWithZone) { + if (!dtWithZone) { return options } - const [ _, tzid, dtstart ] = dtstartWithZone + const [ _, tzid, dtvalue, dt ] = dtWithZone if (tzid) { + if (dt.endsWith('Z')) { + throw new Error(`Invalid UTC date-time value with timezone: ${line}`) + } options.tzid = tzid + } else if (dt.endsWith('Z')) { + options.tzid = 'UTC' } - options.dtstart = dateutil.untilStringToDate(dtstart) + + if (dtvalue === DateTimeValue.DATE) { + if (prop === DateTimeProperty.START) { + options.dtstart = dateutil.fromRfc5545Date(dt) + } else { + options.dtend = dateutil.fromRfc5545Date(dt) + } + options.dtvalue = DateTimeValue.DATE + if (options.tzid) { + throw new Error(`Invalid date value with timezone: ${line}`) + } + } else { // Default value type is DATE-TIME + if (prop === DateTimeProperty.START) { + options.dtstart = dateutil.fromRfc5545DateTime(dt) + } else { + options.dtend = dateutil.fromRfc5545DateTime(dt) + } + if (dtvalue) { + options.dtvalue = DateTimeValue.DATE_TIME + } + } + return options } @@ -41,7 +107,9 @@ function parseLine (rfcString: string) { case 'EXRULE': return parseRrule(rfcString) case 'DTSTART': - return parseDtstart(rfcString) + return parseDateTime(rfcString, DateTimeProperty.START) + case 'DTEND': + return parseDateTime(rfcString, DateTimeProperty.END) default: throw new Error(`Unsupported RFC prop ${key} in ${rfcString}`) } @@ -49,7 +117,7 @@ function parseLine (rfcString: string) { function parseRrule (line: string) { const strippedLine = line.replace(/^RRULE:/i, '') - const options = parseDtstart(strippedLine) + const options = parseDateTime(strippedLine) const attrs = line.replace(/^(?:RRULE|EXRULE):/i, '').split(';') @@ -84,12 +152,15 @@ function parseRrule (line: string) { case 'DTSTART': case 'TZID': // for backwards compatibility - const dtstart = parseDtstart(line) - options.tzid = dtstart.tzid - options.dtstart = dtstart.dtstart + const parsed = parseDateTime(line) + options.tzid = parsed.tzid + options.dtstart = parsed.dtstart + if (parsed.dtvalue) { + options.dtvalue = parsed.dtvalue + } break case 'UNTIL': - options.until = dateutil.untilStringToDate(value) + options.until = dateutil.fromRfc5545DateTime(value) break case 'BYEASTER': options.byeaster = Number(value) diff --git a/src/rrule.ts b/src/rrule.ts index ccbac06c..475c3105 100644 --- a/src/rrule.ts +++ b/src/rrule.ts @@ -5,7 +5,7 @@ import CallbackIterResult from './callbackiterresult' import { Language } from './nlp/i18n' import { Nlp } from './nlp/index' import { DateFormatter, GetText } from './nlp/totext' -import { ParsedOptions, Options, Frequency, QueryMethods, QueryMethodTypes, IterResultType } from './types' +import { ParsedOptions, Options, DateTimeValue, Frequency, QueryMethods, QueryMethodTypes, IterResultType } from './types' import { parseOptions, initializeOptions } from './parseoptions' import { parseString } from './parsestring' import { optionsToString } from './optionstostring' @@ -43,6 +43,8 @@ export const Days = { export const DEFAULT_OPTIONS: Options = { freq: Frequency.YEARLY, dtstart: null, + dtend: null, + dtvalue: DateTimeValue.DATE_TIME, interval: 1, wkst: Days.MO, count: null, @@ -124,6 +126,17 @@ export default class RRule implements QueryMethods { static parseString = parseString static fromString (str: string) { + /* From [RFC 5545](https://tools.ietf.org/html/rfc5545): + * + * 3.3.10. Recurrence Rule + * + * The BYSECOND, BYMINUTE, and BYHOUR rule parts MUST NOT be specified when the + * associated "DTSTART" property has a DATE value type. These rule parts MUST be + * ignored in RECUR value that violate the above requirement (e.g., generated by + * applications that pre-date this revision of iCalendar). + * + * TODO: ^^^ + */ return new RRule(RRule.parseString(str) || undefined) } diff --git a/src/rruleset.ts b/src/rruleset.ts index 1e548fb9..b4c96c3c 100644 --- a/src/rruleset.ts +++ b/src/rruleset.ts @@ -3,7 +3,7 @@ import dateutil from './dateutil' import { includes } from './helpers' import IterResult from './iterresult' import { iterSet } from './iterset' -import { QueryMethodTypes, IterResultType } from './types' +import { DateTimeProperty, QueryMethodTypes, IterResultType } from './types' import { rrulestr } from './rrulestr' import { optionsToString } from './optionstostring' @@ -33,6 +33,7 @@ export default class RRuleSet extends RRule { public readonly _exdate: Date[] private _dtstart?: Date | null | undefined + private _dtend?: Date | null | undefined private _tzid?: string /** @@ -51,6 +52,7 @@ export default class RRuleSet extends RRule { } dtstart = createGetterSetter.apply(this, ['dtstart']) + dtend = createGetterSetter.apply(this, ['dtend']) tzid = createGetterSetter.apply(this, ['tzid']) _iter (iterResult: IterResult): IterResultType { @@ -140,7 +142,10 @@ export default class RRuleSet extends RRule { let result: string[] = [] if (!this._rrule.length && this._dtstart) { - result = result.concat(optionsToString({ dtstart: this._dtstart })) + result = result.concat(optionsToString({ dtstart: this._dtstart, tzid: this._tzid })) + } + if (!this._rrule.length && this._dtend) { + result = result.concat(optionsToString({ dtend: this._dtend, tzid: this._tzid })) } this._rrule.forEach(function (rrule) { @@ -220,7 +225,7 @@ function rdatesToString (param: string, rdates: Date[], tzid: string | undefined const header = isUTC ? `${param}:` : `${param};TZID=${tzid}:` const dateString = rdates - .map(rdate => dateutil.timeToUntilString(rdate.valueOf(), isUTC)) + .map(rdate => dateutil.toRfc5545DateTime(rdate.valueOf(), isUTC)) .join(',') return `${header}${dateString}` diff --git a/src/rrulestr.ts b/src/rrulestr.ts index 967d20a1..9495fc12 100644 --- a/src/rrulestr.ts +++ b/src/rrulestr.ts @@ -2,11 +2,13 @@ import RRule from './rrule' import RRuleSet from './rruleset' import dateutil from './dateutil' import { includes, split } from './helpers' -import { Options } from './types' -import { parseString, parseDtstart } from './parsestring' +import { Options, DateTimeProperty, DateTimeValue } from './types' +import { parseString, parseDateTime } from './parsestring' export interface RRuleStrOptions { dtstart: Date | null + dtend: Date | null + dtvalue: DateTimeValue | null cache: boolean unfold: boolean forceset: boolean @@ -20,6 +22,8 @@ export interface RRuleStrOptions { */ const DEFAULT_OPTIONS: RRuleStrOptions = { dtstart: null, + dtend: null, + dtvalue: null, cache: false, unfold: false, forceset: false, @@ -33,7 +37,8 @@ export function parseInput (s: string, options: Partial) { let exrulevals: Partial[] = [] let exdatevals: Date[] = [] - let { dtstart, tzid } = parseDtstart(s) + let { dtstart, dtvalue, tzid } = parseDateTime(s) + let dtend: Date | null = null const lines = splitIntoLines(s, options.unfold) @@ -73,6 +78,26 @@ export function parseInput (s: string, options: Partial) { case 'DTSTART': break + case 'DTEND': + let parsed: Partial = parseDateTime(s, DateTimeProperty.END) + if (parsed.dtend) { + if (dtend) { + throw new Error('Invalid rule: DTEND must occur only once') + } + if (dtstart && dtstart.valueOf() >= parsed.dtend.valueOf()) { + throw new Error('Invalid rule: DTEND must be later than DTSTART') + } + if (dtstart && dtvalue !== parsed.dtvalue) { + // Different value types. + throw new Error('Invalid rule: DTSTART and DTEND must have the same value type') + } else if (dtstart && tzid !== parsed.tzid) { + // Different timezones. + throw new Error('Invalid rule: DTSTART and DTEND must have the same timezone') + } + dtend = parsed.dtend + } + break + default: throw new Error('unsupported property: ' + name) } @@ -80,6 +105,8 @@ export function parseInput (s: string, options: Partial) { return { dtstart, + dtend, + dtvalue, tzid, rrulevals, rdatevals, @@ -95,6 +122,8 @@ function buildRule (s: string, options: Partial) { exrulevals, exdatevals, dtstart, + dtend, + dtvalue, tzid } = parseInput(s, options) @@ -115,12 +144,13 @@ function buildRule (s: string, options: Partial) { const rset = new RRuleSet(noCache) rset.dtstart(dtstart) + rset.dtend(dtend) rset.tzid(tzid || undefined) rrulevals.forEach(val => { rset.rrule( new RRule( - groomRruleOptions(val, dtstart, tzid), + groomRruleOptions(val, dtstart, dtend, dtvalue, tzid), noCache ) ) @@ -133,7 +163,7 @@ function buildRule (s: string, options: Partial) { exrulevals.forEach(val => { rset.exrule( new RRule( - groomRruleOptions(val, dtstart, tzid), + groomRruleOptions(val, dtstart, dtend, dtvalue, tzid), noCache ) ) @@ -151,6 +181,8 @@ function buildRule (s: string, options: Partial) { return new RRule(groomRruleOptions( val, val.dtstart || options.dtstart || dtstart, + val.dtend || options.dtend || dtend, + val.dtvalue || options.dtvalue || dtvalue, val.tzid || options.tzid || tzid ), noCache) } @@ -162,10 +194,12 @@ export function rrulestr ( return buildRule(s, initializeOptions(options)) } -function groomRruleOptions (val: Partial, dtstart?: Date | null, tzid?: string | null) { +function groomRruleOptions (val: Partial, dtstart?: Date | null, dtend?: Date | null, dtvalue?: DateTimeValue | null, tzid?: string | null) { return { ...val, dtstart, + dtend, + dtvalue, tzid } } @@ -256,5 +290,5 @@ function parseRDate (rdateval: string, parms: string[]) { return rdateval .split(',') - .map(datestr => dateutil.untilStringToDate(datestr)) + .map(datestr => dateutil.fromRfc5545DateTime(datestr)) } diff --git a/src/types.ts b/src/types.ts index bbf1d85a..25e77730 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,16 @@ export interface QueryMethods { export type QueryMethodTypes = keyof QueryMethods export type IterResultType = M extends 'all' | 'between' ? Date[] : (Date | null) +export enum DateTimeProperty { + START = 'DTSTART', + END = 'DTEND' +} + +export enum DateTimeValue { + DATE = 'DATE', + DATE_TIME = 'DATE-TIME' +} + export enum Frequency { YEARLY = 0, MONTHLY = 1, @@ -27,6 +37,8 @@ export function freqIsDailyOrGreater (freq: Frequency): freq is Frequency.YEARLY export interface Options { freq: Frequency dtstart: Date | null + dtend: Date | null + dtvalue: DateTimeValue | null interval: number wkst: Weekday | number | null count: number | null @@ -48,6 +60,7 @@ export interface Options { export interface ParsedOptions extends Options { dtstart: Date + dtend: Date wkst: number bysetpos: number[] bymonth: number[] diff --git a/test/dateutil.test.ts b/test/dateutil.test.ts index d6bd196a..7f4b31fa 100644 --- a/test/dateutil.test.ts +++ b/test/dateutil.test.ts @@ -1,9 +1,16 @@ import { dateutil } from '../src/dateutil' import { expect } from 'chai' -describe('untilStringToDate', () => { - it('parses a date string', () => { - const date = dateutil.untilStringToDate('19970902T090000') +describe('fromRfc5545DateTime', () => { + it('parses a date-time string', () => { + const date = dateutil.fromRfc5545DateTime('19970902T090000') expect(date.getTime()).to.equal(new Date(Date.UTC(1997, 8, 2, 9, 0, 0, 0)).getTime()) }) }) + +describe('fromRfc5545Date', () => { + it('parses a date string', () => { + const date = dateutil.fromRfc5545Date('19970902') + expect(date.getTime()).to.equal(new Date(Date.UTC(1997, 8, 2, 0, 0, 0, 0)).getTime()) + }) +}) diff --git a/test/optionstostring.test.ts b/test/optionstostring.test.ts index 4eb5e94e..e4d25e6c 100644 --- a/test/optionstostring.test.ts +++ b/test/optionstostring.test.ts @@ -6,10 +6,17 @@ import { expect } from "chai"; describe('optionsToString', () => { it('serializes valid single lines of rrules', function () { const expectations: ([ Partial, string ][]) = [ - [{ freq: RRule.WEEKLY, until: new Date(Date.UTC(2010, 0, 1, 0, 0, 0)) }, 'RRULE:FREQ=WEEKLY;UNTIL=20100101T000000Z' ], + [{ freq: RRule.WEEKLY, until: new Date(Date.UTC(2010, 0, 1, 0, 0, 0)) }, 'RRULE:FREQ=WEEKLY;UNTIL=20100101T000000' ], + [{ freq: RRule.WEEKLY, until: new Date(Date.UTC(2010, 0, 1, 0, 0, 0)), tzid: 'UTC' }, 'RRULE:FREQ=WEEKLY;UNTIL=20100101T000000Z' ], + [{ dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)) }, 'DTSTART:19970902T090000' ], [{ dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), tzid: 'America/New_York' }, 'DTSTART;TZID=America/New_York:19970902T090000' ], [ { dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), freq: RRule.WEEKLY }, + 'DTSTART:19970902T090000\n' + + 'RRULE:FREQ=WEEKLY' + ], + [ + { dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), tzid: 'UTC', freq: RRule.WEEKLY }, 'DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=WEEKLY' ], @@ -17,6 +24,12 @@ describe('optionsToString', () => { { dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), tzid: 'America/New_York', freq: RRule.WEEKLY }, 'DTSTART;TZID=America/New_York:19970902T090000\n' + 'RRULE:FREQ=WEEKLY' + ], + [ + { dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), dtend: new Date(Date.UTC(1997, 8, 3, 9, 0, 0)), tzid: 'UTC', freq: RRule.WEEKLY }, + 'DTSTART:19970902T090000Z\n' + + 'DTEND:19970903T090000Z\n' + + 'RRULE:FREQ=WEEKLY' ] ] diff --git a/test/parsestring.test.ts b/test/parsestring.test.ts index ade2397e..5f1394c2 100644 --- a/test/parsestring.test.ts +++ b/test/parsestring.test.ts @@ -37,9 +37,32 @@ describe('parseString', () => { 'RRULE:FREQ=YEARLY;COUNT=3\n', { dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), + tzid: 'UTC', freq: RRule.YEARLY, count: 3 } + ], + [ + 'DTSTART:19970902T090000Z\n' + + 'DTEND:19970903T090000Z\n' + + 'RRULE:FREQ=WEEKLY\n', + { + dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), + dtend: new Date(Date.UTC(1997, 8, 3, 9, 0, 0)), + tzid: 'UTC', + freq: RRule.WEEKLY + } + ], + [ + 'RRULE:FREQ=WEEKLY\n' + + 'DTEND:19970903T090000Z\n' + + 'DTSTART:19970902T090000Z\n', + { + dtstart: new Date(Date.UTC(1997, 8, 2, 9, 0, 0)), + dtend: new Date(Date.UTC(1997, 8, 3, 9, 0, 0)), + tzid: 'UTC', + freq: RRule.WEEKLY + } ] ] diff --git a/test/rrule.test.ts b/test/rrule.test.ts index dbf90411..ce1ca589 100644 --- a/test/rrule.test.ts +++ b/test/rrule.test.ts @@ -2,6 +2,7 @@ import { parse, datetime, testRecurring, expectedDate } from './lib/utils' import { expect } from 'chai' import { RRule, rrulestr, Frequency } from '../src/index' import { DateTime } from 'luxon' +import { DateWithZone } from '../src/datewithzone' import { set as setMockDate, reset as resetMockDate } from 'mockdate' import { optionsToString } from '../src/optionstostring'; @@ -27,8 +28,8 @@ describe('RRule', function () { const s2 = rrulestr(s1).toString() expect(s1).equals(s2, s1 + ' => ' + s2) }) - - it('rrulestr itteration not infinite when interval 0', function () { + + it('rrulestr iteration not infinite when interval 0', function () { ['FREQ=YEARLY;INTERVAL=0;BYSETPOS=1;BYDAY=MO', 'FREQ=MONTHLY;INTERVAL=0;BYSETPOS=1;BYDAY=MO', 'FREQ=DAILY;INTERVAL=0;BYSETPOS=1;BYDAY=MO', @@ -3504,6 +3505,7 @@ describe('RRule', function () { [6, RRule.SU].forEach(function (wkst) { const rr = new RRule({ dtstart: new Date(Date.UTC(2017, 9, 17, 0, 30, 0, 0)), + tzid: 'UTC', until: new Date(Date.UTC(2017, 11, 22, 1, 30, 0, 0)), freq: RRule.MONTHLY, interval: 1, @@ -3642,7 +3644,7 @@ describe('RRule', function () { const ruleString = rrule.toString() const rrule2 = RRule.fromString(ruleString) - expect(ruleString).to.equal('DTSTART:09900101T000000Z\nRRULE:COUNT=1') + expect(ruleString).to.equal('DTSTART:09900101T000000\nRRULE:COUNT=1') expect(rrule2.count()).to.equal(1) expect(rrule2.all()).to.deep.equal([ new Date(Date.UTC(990, 0, 1, 0, 0, 0)) diff --git a/test/rruleset.test.ts b/test/rruleset.test.ts index 2fe7f0da..317aa19d 100644 --- a/test/rruleset.test.ts +++ b/test/rruleset.test.ts @@ -366,7 +366,8 @@ describe('RRuleSet', function () { set.rrule(new RRule({ freq: RRule.YEARLY, count: 2, - dtstart: parse('19600101T090000') + dtstart: parse('19600101T090000'), + tzid: 'UTC' })) expect(set.valueOf()).to.deep.equal([ @@ -381,7 +382,8 @@ describe('RRuleSet', function () { set.rrule(new RRule({ freq: RRule.YEARLY, count: 2, - dtstart: parse('19600101T090000') + dtstart: parse('19600101T090000'), + tzid: 'UTC' })) set.rrule(new RRule({ @@ -649,6 +651,21 @@ describe('RRuleSet', function () { expectRecurrence([original, legacy]).toBeUpdatedWithEndDate([ 'DTSTART;TZID=America/New_York:20171201T080000', + 'RRULE:FREQ=WEEKLY;UNTIL=20171224T235959Z', + ].join('\n')) + }) + + it('handles rule with floating time', () => { + const legacy = [ + 'RRULE:DTSTART=20171201T080000;FREQ=WEEKLY', + ] + const original = [ + 'DTSTART:20171201T080000', + 'RRULE:FREQ=WEEKLY', + ] + + expectRecurrence([original, legacy]).toBeUpdatedWithEndDate([ + 'DTSTART:20171201T080000', 'RRULE:FREQ=WEEKLY;UNTIL=20171224T235959', ].join('\n')) }) @@ -785,4 +802,4 @@ describe('RRuleSet', function () { expect(set.exdates()).eql([dt]); }); }); -}); \ No newline at end of file +}); diff --git a/test/rrulestr.test.ts b/test/rrulestr.test.ts index 1ae2d511..88d17af8 100644 --- a/test/rrulestr.test.ts +++ b/test/rrulestr.test.ts @@ -317,7 +317,7 @@ describe('rrulestr', function () { it('parses a DTSTART with a TZID inside an RRULE', () => { const rrule = rrulestr( - 'RRULE:UNTIL=19990404T110000Z;DTSTART;TZID=America/New_York:19990104T110000Z;FREQ=WEEKLY;BYDAY=TU,WE' + 'RRULE:UNTIL=19990404T110000Z;DTSTART;TZID=America/New_York:19990104T110000;FREQ=WEEKLY;BYDAY=TU,WE' ) expect(rrule.options).to.deep.include({