rrule-rust
Version:
RRule implementation for browsers and Node.js written in Rust
478 lines (477 loc) • 15.1 kB
JavaScript
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,
};
}
}