UNPKG

rrule-rust

Version:

RRule implementation for browsers and Node.js written in Rust

478 lines (477 loc) 15.1 kB
import { RRule } from './rrule'; import { RRuleSet as Rust } from './lib'; import { DateTime, } from './datetime'; import { DtStart } from './dtstart'; import { ExDate } from './exdate'; import { RDate } from './rdate'; import { OperationCache } from './cache'; /** * Represents a set of recurrence rules (RRuleSet) according to RFC 5545. * * RRuleSet combines multiple recurrence components: * - DTSTART: The start date/time * - RRULE: Rules for generating occurrences * - EXRULE: Rules for excluding occurrences * - RDATE: Additional dates to include * - EXDATE: Specific dates to exclude * * @example * ```typescript * // Weekly meeting on Mondays, excluding holidays * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.local(2024, 1, 15, 9, 0, 0)), * rrules: [ * new RRule({ * frequency: Frequency.Weekly, * byWeekday: [Weekday.Monday] * }) * ], * exdates: [ * new ExDate([ * DateTime.date(2024, 1, 1), // New Year * DateTime.date(2024, 12, 25) // Christmas * ]) * ] * }); * * // Get first 10 occurrences * const occurrences = rruleSet.all(10); * ``` */ export class RRuleSet { constructor(optionsOrDtstart) { this._cache = new OperationCache({ disabled: false, }); if ('dtstart' in optionsOrDtstart) { this.dtstart = optionsOrDtstart.dtstart; this.rrules = optionsOrDtstart?.rrules ?? []; this.exrules = optionsOrDtstart?.exrules ?? []; this.exdates = optionsOrDtstart?.exdates ?? []; this.rdates = optionsOrDtstart?.rdates ?? []; } else { this.dtstart = optionsOrDtstart; this.rrules = []; this.exrules = []; this.exdates = []; this.rdates = []; } } /** * Provides access to cache control for this RRuleSet instance. * * By default, caching is enabled to optimize repeated calls to methods like `all()`, * `between()`, and iteration. Use this property to control caching behavior based on * your performance and memory requirements. * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.local(2024, 1, 1, 9, 0, 0)), * rrules: [new RRule({ frequency: Frequency.Daily })] * }); * * // Check if caching is disabled * console.log(rruleSet.cache.disabled); // false * * // Disable caching for memory-constrained environments * rruleSet.cache.disable(); * * // Clear cached data to free memory * rruleSet.cache.clear(); * ``` */ get cache() { return this._cache; } /** * Parses an RFC 5545 formatted string into an RRuleSet. * * @param str - RFC 5545 formatted string containing DTSTART, RRULE, etc. * @returns A new RRuleSet instance * * @example * ```typescript * const str = `DTSTART:20240115T090000 * RRULE:FREQ=WEEKLY;BYDAY=MO * EXDATE:20240101,20241225`; * const rruleSet = RRuleSet.fromString(str); * ``` */ static fromString(str) { return this.fromRust(Rust.parse(str)); } static fromPlain(plain) { return new RRuleSet({ dtstart: DtStart.fromPlain(plain.dtstart), rrules: plain.rrules.map((rrule) => RRule.fromPlain(rrule)), exrules: plain.exrules.map((rrule) => RRule.fromPlain(rrule)), exdates: plain.exdates.map((datetime) => ExDate.fromPlain(datetime)), rdates: plain.rdates.map((datetime) => RDate.fromPlain(datetime)), }); } /** * @internal */ static fromRust(rust) { const set = new RRuleSet({ dtstart: new DtStart({ value: DateTime.fromInt32Array(rust.dtstart), tzid: rust.tzid ?? undefined, }), rrules: rust.rrules.map((rrule) => RRule.fromRust(rrule)), exrules: rust.exrules.map((rrule) => RRule.fromRust(rrule)), exdates: rust.exdates.map((exdate) => ExDate.fromRust(exdate)), rdates: rust.rdates.map((rdate) => RDate.fromRust(rdate)), }); set.rust = rust; return set; } /** * Creates a new RRuleSet with a different start date/time. * * @param dtstart - The new start date/time * @returns A new RRuleSet instance * * @example * ```typescript * const rruleSet = new RRuleSet(new DtStart(DateTime.date(2024, 1, 15))); * const updated = rruleSet.setDtStart(new DtStart(DateTime.date(2024, 2, 1))); * ``` */ setDtStart(dtstart) { return new RRuleSet({ ...this.toOptions(), dtstart: dtstart, }); } /** * Creates a new RRuleSet with an additional recurrence rule. * * @param rrule - The recurrence rule to add * @returns A new RRuleSet instance * * @example * ```typescript * const rruleSet = new RRuleSet(new DtStart(DateTime.date(2024, 1, 15))); * const withRule = rruleSet.addRRule(new RRule(Frequency.Weekly)); * ``` */ addRRule(rrule) { const rrules = [...this.rrules, rrule]; return new RRuleSet({ ...this.toOptions(), rrules, }); } /** * Creates a new RRuleSet with a different set of recurrence rules. * * @param rrules - The new array of recurrence rules * @returns A new RRuleSet instance */ setRRules(rrules) { return new RRuleSet({ ...this.toOptions(), rrules, }); } /** * Creates a new RRuleSet with an additional exclusion rule. * * @param rrule - The exclusion rule to add * @returns A new RRuleSet instance * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 15)), * rrules: [new RRule(Frequency.Daily)] * }); * // Exclude weekends * const noWeekends = rruleSet.addExRule(new RRule({ * frequency: Frequency.Weekly, * byWeekday: [Weekday.Saturday, Weekday.Sunday] * })); * ``` */ addExRule(rrule) { const exrules = [...this.exrules, rrule]; return new RRuleSet({ ...this.toOptions(), exrules, }); } /** * Creates a new RRuleSet with a different set of exclusion rules. * * @param rrules - The new array of exclusion rules * @returns A new RRuleSet instance */ setExRules(rrules) { return new RRuleSet({ ...this.toOptions(), exrules: rrules, }); } /** * Creates a new RRuleSet with an additional exception date. * * @param exdate - The exception date(s) to add * @returns A new RRuleSet instance * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 15)), * rrules: [new RRule(Frequency.Daily)] * }); * // Exclude specific holidays * const withExceptions = rruleSet.addExDate(new ExDate([ * DateTime.date(2024, 1, 1), * DateTime.date(2024, 12, 25) * ])); * ``` */ addExDate(exdate) { return new RRuleSet({ ...this.toOptions(), exdates: [...this.exdates, exdate], }); } /** * Creates a new RRuleSet with a different set of exception dates. * * @param exdates - The new array of exception dates * @returns A new RRuleSet instance */ setExDates(exdates) { return new RRuleSet({ ...this.toOptions(), exdates: exdates, }); } /** * Creates a new RRuleSet with an additional recurrence date. * * @param datetime - The recurrence date(s) to add * @returns A new RRuleSet instance * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 15)), * rrules: [new RRule(Frequency.Weekly)] * }); * // Add extra dates not covered by the rule * const withExtras = rruleSet.addRDate(new RDate([ * DateTime.date(2024, 2, 14), // Valentine's Day special * DateTime.date(2024, 3, 17) // St. Patrick's Day special * ])); * ``` */ addRDate(datetime) { return new RRuleSet({ ...this.toOptions(), rdates: [...this.rdates, datetime], }); } /** * Creates a new RRuleSet with a different set of recurrence dates. * * @param datetimes - The new array of recurrence dates * @returns A new RRuleSet instance */ setRDates(datetimes) { return new RRuleSet({ ...this.toOptions(), rdates: datetimes, }); } /** * Returns all the occurrences of the recurrence set. * * @param limit - Optional maximum number of occurrences to return * @returns Array of date/time occurrences * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 15)), * rrules: [new RRule({ frequency: Frequency.Daily, count: 5 })] * }); * * // Get all occurrences (limited by count in rule) * const all = rruleSet.all(); * * // Get first 10 occurrences * const first10 = rruleSet.all(10); * ``` */ // TODO: add skip (?) all(limit) { return this._cache.getOrCompute(`all:${limit}`, () => DateTime.fromFlatInt32Array(this.toRust().all(limit))); } /** * Returns all occurrences between two dates. * * @param after - The lower bound date (exclusive by default) * @param before - The upper bound date (exclusive by default) * @param inclusive - Whether to include the boundary dates in results * @returns Array of date/time occurrences in the range * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 1)), * rrules: [new RRule(Frequency.Daily)] * }); * * // Get occurrences in January 2024 (exclusive) * const january = rruleSet.between( * DateTime.date(2024, 1, 1), * DateTime.date(2024, 2, 1) * ); * * // Get occurrences in January 2024 (inclusive) * const januaryInclusive = rruleSet.between( * DateTime.date(2024, 1, 1), * DateTime.date(2024, 1, 31), * true * ); * ``` */ between(after, before, inclusive) { return this._cache.getOrCompute(`between:${after.toString()},${before.toString()},${inclusive}`, () => DateTime.fromFlatInt32Array(this.toRust().between(after.toInt32Array(), before.toInt32Array(), inclusive))); } /** * Parses an RFC 5545 string and updates the RRuleSet. * * @param str - RFC 5545 formatted string * @returns A new RRuleSet instance parsed from the string * * @example * ```typescript * const rruleSet = new RRuleSet(new DtStart(DateTime.date(2024, 1, 15))); * const updated = rruleSet.setFromString(`DTSTART:20240201T090000 * RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR`); * ``` */ setFromString(str) { return RRuleSet.fromRust(this.toRust().setFromString(str)); } /** * @internal */ toRust() { this.rust ??= new Rust(this.dtstart.value.toInt32Array(), this.dtstart.tzid, undefined, this.rrules.map((rrule) => rrule.toRust()), this.exrules.map((rrule) => rrule.toRust()), this.exdates.map((exdate) => exdate.toRust()), this.rdates.map((rdate) => rdate.toRust())); return this.rust; } /** * Converts the RRuleSet to an RFC 5545 formatted string. * * @returns RFC 5545 formatted string representation * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 15)), * rrules: [new RRule({ frequency: Frequency.Weekly, byWeekday: [Weekday.Monday] })] * }); * console.log(rruleSet.toString()); * // DTSTART:20240115 * // RRULE:FREQ=WEEKLY;BYDAY=MO * ``` */ toString() { return this.toRust().toString(); } toPlain() { return { dtstart: this.dtstart.toPlain(), rrules: this.rrules.map((rrule) => rrule.toPlain()), exrules: this.exrules.map((rrule) => rrule.toPlain()), exdates: this.exdates.map((rrule) => rrule.toPlain()), rdates: this.rdates.map((rrule) => rrule.toPlain()), }; } /** * Returns an iterator for the recurrence set. * * This allows using RRuleSet with for-of loops and other iteration constructs. * * @returns An iterator over the occurrences * * @example * ```typescript * const rruleSet = new RRuleSet({ * dtstart: new DtStart(DateTime.date(2024, 1, 15)), * rrules: [new RRule({ frequency: Frequency.Daily, count: 5 })] * }); * * // Iterate over occurrences * for (const occurrence of rruleSet) { * console.log(occurrence.toString()); * } * * // Use with spread operator * const allOccurrences = [...rruleSet]; * ``` */ [Symbol.iterator]() { const cache = this._cache.getOrSet('iterator:data', { values: [], done: false, }); let cacheIndex = 0; let iterAndStore; const getIterAndStore = () => { return (iterAndStore ??= [ this.toRust().iterator(cache.values.length), new Int32Array(7), ]); }; return { next: () => { const cachedValue = cache.values[cacheIndex++]; if (cachedValue) { return { done: false, value: cachedValue, }; } else if (cache.done) { return { done: true, value: undefined, }; } const [iter, store] = getIterAndStore(); const next = iter.next(store); if (!next) { cache.done = true; return { done: true, value: undefined, }; } const value = DateTime.fromInt32Array(next === true ? store : next); cache.values.push(value); return { done: false, value, }; }, }; } toOptions() { return { dtstart: this.dtstart, rrules: this.rrules, exrules: this.exrules, exdates: this.exdates, rdates: this.rdates, }; } }