@rschedule/rschedule
Version:
A typescript library for working with recurring dates and events.
415 lines (375 loc) • 13.9 kB
text/typescript
import { DateAdapter } from '../date-adapter';
import { DateTime } from '../date-time';
import { Dates } from '../dates';
import { IDataContainer, IRunArgs, IScheduleLike, OccurrenceGenerator } from '../interfaces';
import {
CollectionIterator,
ICollectionsArgs,
IOccurrencesArgs,
OccurrenceIterator,
} from '../iterators';
import { add, OccurrenceStream, OperatorFnOutput, pipeFn, subtract, unique } from '../operators';
import { RScheduleConfig } from '../rschedule-config';
import { IProvidedRuleOptions, Rule } from '../rule';
import { DateInput } from '../utilities';
const SCHEDULE_ID = Symbol.for('35d5d3f8-8924-43d2-b100-48e04b0cf500');
export class Schedule<T extends typeof DateAdapter, D = any> extends OccurrenceGenerator<T>
implements IScheduleLike<T>, IDataContainer<D> {
/**
* Similar to `Array.isArray`, `isSchedule` provides a surefire method
* of determining if an object is a `Schedule` by checking against the
* global symbol registry.
*/
static isSchedule(object: unknown): object is Schedule<any> {
return !!(object && typeof object === 'object' && (object as any)[SCHEDULE_ID]);
}
readonly rrules: ReadonlyArray<Rule<T>> = [];
readonly exrules: ReadonlyArray<Rule<T>> = [];
readonly rdates!: Dates<T>;
readonly exdates!: Dates<T>;
pipe: (...operatorFns: OperatorFnOutput<T>[]) => OccurrenceStream<T> = pipeFn(this);
/**
* Convenience property for holding arbitrary data. Accessible on individual DateAdapters
* generated by this `Schedule` object via the `DateAdapter#generators` property. Unlike
* the rest of the `Schedule` object, the data property is mutable.
*/
data!: D;
readonly isInfinite: boolean;
readonly hasDuration: boolean;
protected readonly [SCHEDULE_ID] = true;
private readonly occurrenceStream: OccurrenceStream<T>;
/**
* Create a new Schedule object with the specified options.
*
* The order of precidence for rrules, rdates, exrules, and exdates is:
*
* 1. rrules are included
* 2. exrules are excluded
* 3. rdates are included
* 4. exdates are excluded
*
* ### Options
*
* - **timezone**: the timezone that yielded occurrences should be in.
* - **data**: arbitrary data you can associate with this Schedule. This
* is the only mutable property of `Schedule` objects.
* - **dateAdapter**: the DateAdapter class that should be used for this Schedule.
* - **maxDuration**: currently unused.
* - **rrules**: rules specifying when occurrences happen. See the "Rule Config"
* section below.
* - **rdates**: individual dates that should be _included_ in the schedule.
* - **exdates**: individual dates that should be _excluded_ from the schedule.
* - **exrules**: rules specifying when occurrences shouldn't happen. See the
* "Rule Config" section below.
*
* ### Rule Config
*
* - #### frequency
*
* The frequency rule part identifies the type of recurrence rule. Valid values
* include `"SECONDLY"`, `"MINUTELY"`, `"HOURLY"`, `"DAILY"`, `"WEEKLY"`,
* `"MONTHLY"`, or `"YEARLY"`.
*
* - #### start
*
* The start of the rule (not necessarily the first occurrence).
* Either a `DateAdapter` instance, date object, or `DateTime` object.
* The type of date object depends on the `DateAdapter` class used for this
* `Rule`.
*
* - #### end?
*
* The end of the rule (not necessarily the last occurrence).
* Either a `DateAdapter` instance, date object, or `DateTime` object.
* The type of date object depends on the `DateAdapter` class used for this
* `Rule`.
*
* - #### duration?
*
* A length of time expressed in milliseconds.
*
* - #### interval?
*
* The interval rule part contains a positive integer representing at
* which intervals the recurrence rule repeats. The default value is
* `1`, meaning every second for a SECONDLY rule, every minute for a
* MINUTELY rule, every hour for an HOURLY rule, every day for a
* DAILY rule, every week for a WEEKLY rule, every month for a
* MONTHLY rule, and every year for a YEARLY rule. For example,
* within a DAILY rule, a value of `8` means every eight days.
*
* - #### count?
*
* The count rule part defines the number of occurrences at which to
* range-bound the recurrence. `count` and `end` are both two different
* ways of specifying how a recurrence completes.
*
* - #### weekStart?
*
* The weekStart rule part specifies the day on which the workweek starts.
* Valid values are `"MO"`, `"TU"`, `"WE"`, `"TH"`, `"FR"`, `"SA"`, and `"SU"`.
* This is significant when a WEEKLY rule has an interval greater than 1,
* and a `byDayOfWeek` rule part is specified. The
* default value is `"MO"`.
*
* - #### bySecondOfMinute?
*
* The bySecondOfMinute rule part expects an array of seconds
* within a minute. Valid values are 0 to 60.
*
* - #### byMinuteOfHour?
*
* The byMinuteOfHour rule part expects an array of minutes within an hour.
* Valid values are 0 to 59.
*
* - #### byHourOfDay?
*
* The byHourOfDay rule part expects an array of hours of the day.
* Valid values are 0 to 23.
*
* - #### byDayOfWeek?
*
* *note: the byDayOfWeek rule part is kinda complex. Blame the ICAL spec.*
*
* The byDayOfWeek rule part expects an array. Each array entry can
* be a day of the week (`"SU"`, `"MO"` , `"TU"`, `"WE"`, `"TH"`,
* `"FR"`, `"SA"`). If the rule's `frequency` is either MONTHLY or YEARLY,
* Any entry can also be a tuple where the first value of the tuple is a
* day of the week and the second value is an positive/negative integer
* (e.g. `["SU", 1]`). In this case, the number indicates the nth occurrence of
* the specified day within the MONTHLY or YEARLY rule.
*
* The behavior of byDayOfWeek changes depending on the `frequency`
* of the rule.
*
* Within a MONTHLY rule, `["MO", 1]` represents the first Monday
* within the month, whereas `["MO", -1]` represents the last Monday
* of the month.
*
* Within a YEARLY rule, the numeric value in a byDayOfWeek tuple entry
* corresponds to an offset within the month when the byMonthOfYear rule part is
* present, and corresponds to an offset within the year otherwise.
*
* Regardless of rule `frequency`, if a byDayOfWeek entry is a string
* (rather than a tuple), it means "all of these days" within the specified
* frequency (e.g. within a MONTHLY rule, `"MO"` represents all Mondays within
* the month).
*
* - #### byDayOfMonth?
*
* The byDayOfMonth rule part expects an array of days
* of the month. Valid values are 1 to 31 or -31 to -1.
*
* For example, -10 represents the tenth to the last day of the month.
* The byDayOfMonth rule part *must not* be specified when the rule's
* `frequency` is set to WEEKLY.
*
* - #### byMonthOfYear?
*
* The byMonthOfYear rule part expects an array of months
* of the year. Valid values are 1 to 12.
*
*/
constructor(
options: {
dateAdapter?: T;
timezone?: string | null;
data?: D;
rrules?: ReadonlyArray<IProvidedRuleOptions<T> | Rule<T>>;
exrules?: ReadonlyArray<IProvidedRuleOptions<T> | Rule<T>>;
rdates?: ReadonlyArray<DateInput<T>> | Dates<T>;
exdates?: ReadonlyArray<DateInput<T>> | Dates<T>;
maxDuration?: number;
} = {},
) {
super(options);
this.data = options.data as D;
for (const prop of ['rrules', 'exrules'] as ['rrules', 'exrules']) {
const arg = options[prop];
if (arg) {
this[prop] = arg.map(ruleArgs => {
if (Rule.isRule(ruleArgs)) {
return ruleArgs.set('timezone', this.timezone);
} else {
return new Rule(ruleArgs as IProvidedRuleOptions<T>, {
dateAdapter: this.dateAdapter as any,
timezone: this.timezone,
});
}
});
}
}
for (const prop of ['rdates', 'exdates'] as ['rdates', 'exdates']) {
const arg = options[prop];
if (arg) {
this[prop] = Dates.isDates(arg)
? arg.set('timezone', this.timezone)
: new Dates({
dates: arg as ReadonlyArray<DateInput<T>>,
dateAdapter: this.dateAdapter as any,
timezone: this.timezone,
});
} else {
this[prop] = new Dates({
dateAdapter: this.dateAdapter as any,
timezone: this.timezone,
});
}
}
this.hasDuration =
this.rrules.every(rule => rule.hasDuration) &&
this.exrules.every(rule => rule.hasDuration) &&
this.rdates.hasDuration &&
this.exdates.hasDuration;
this.isInfinite = this.rrules.some(rule => rule.isInfinite);
const operators = [
add<T>(...this.rrules),
subtract<T>(...this.exrules),
add<T>(this.rdates),
subtract<T>(this.exdates),
unique<T>(),
];
this.occurrenceStream = new OccurrenceStream({
operators,
dateAdapter: this.dateAdapter,
timezone: this.timezone,
});
// for some reason, setting `rrules` / `rdates` / etc with `for` loops is
// removing the `SCHEDULE_ID` property from the constructed object...
// need to set it
this[SCHEDULE_ID] = true;
}
occurrences(args: IOccurrencesArgs<T> = {}): OccurrenceIterator<T, [this, Rule<T> | Dates<T>]> {
return new OccurrenceIterator(this, this.normalizeOccurrencesArgs(args));
}
collections(args: ICollectionsArgs<T> = {}): CollectionIterator<T, [this, Rule<T> | Dates<T>]> {
return new CollectionIterator(this, this.normalizeCollectionsArgs(args));
}
add(prop: 'rrule' | 'exrule', value: Rule<T, unknown>): Schedule<T, D>;
add(prop: 'rdate' | 'exdate', value: DateInput<T>): Schedule<T, D>;
add(prop: 'rdate' | 'exdate' | 'rrule' | 'exrule', value: Rule<T, unknown> | DateInput<T>) {
const rrules = this.rrules.slice();
const exrules = this.exrules.slice();
let rdates = this.rdates;
let exdates = this.exdates;
switch (prop) {
case 'rrule':
rrules.push(value as Rule<T>);
break;
case 'exrule':
exrules.push(value as Rule<T>);
break;
case 'rdate':
rdates = this.rdates.add(value as DateInput<T>);
break;
case 'exdate':
exdates = this.exdates.add(value as DateInput<T>);
break;
}
return new Schedule({
dateAdapter: this.dateAdapter,
timezone: this.timezone,
data: this.data,
rrules,
exrules,
rdates,
exdates,
});
}
remove(prop: 'rrule' | 'exrule', value: Rule<T, unknown>): Schedule<T, D>;
remove(prop: 'rdate' | 'exdate', value: DateInput<T>): Schedule<T, D>;
remove(prop: 'rdate' | 'exdate' | 'rrule' | 'exrule', value: Rule<T, unknown> | DateInput<T>) {
let rrules = this.rrules;
let exrules = this.exrules;
let rdates = this.rdates;
let exdates = this.exdates;
switch (prop) {
case 'rrule':
rrules = rrules.filter(rule => rule !== value);
break;
case 'exrule':
exrules = exrules.filter(rule => rule !== value);
break;
case 'rdate':
rdates = this.rdates.remove(value as DateInput<T>);
break;
case 'exdate':
exdates = this.exdates.remove(value as DateInput<T>);
break;
}
return new Schedule({
dateAdapter: this.dateAdapter,
timezone: this.timezone,
data: this.data,
rrules,
exrules,
rdates,
exdates,
});
}
set(
prop: 'timezone',
value: string | null,
options?: { keepLocalTime?: boolean },
): Schedule<T, D>;
set(prop: 'rrules' | 'exrules', value: Rule<T, unknown>[]): Schedule<T, D>;
set(prop: 'rdates' | 'exdates', value: Dates<T, unknown>): Schedule<T, D>;
set(
prop: 'timezone' | 'rrules' | 'exrules' | 'rdates' | 'exdates',
value: string | null | Rule<T, unknown>[] | Dates<T, unknown>,
options: { keepLocalTime?: boolean } = {},
) {
let timezone = this.timezone;
let rrules = this.rrules;
let exrules = this.exrules;
let rdates = this.rdates;
let exdates = this.exdates;
switch (prop) {
case 'timezone':
if (value === this.timezone && !options.keepLocalTime) return this;
else if (options.keepLocalTime) {
rrules = rrules.map(rule => rule.set('timezone', value as string | null, options));
exrules = exrules.map(rule => rule.set('timezone', value as string | null, options));
rdates = rdates.set('timezone', value as string | null, options);
exdates = exdates.set('timezone', value as string | null, options);
}
timezone = value as string | null;
break;
case 'rrules':
rrules = value as Rule<T>[];
break;
case 'exrules':
exrules = value as Rule<T>[];
break;
case 'rdates':
rdates = value as Dates<T>;
break;
case 'exdates':
exdates = value as Dates<T>;
break;
}
return new Schedule({
dateAdapter: this.dateAdapter,
timezone,
data: this.data,
rrules,
exrules,
rdates,
exdates,
});
}
/** @internal use occurrences() instead */
*_run(args: IRunArgs = {}): IterableIterator<DateTime> {
const count = args.take;
delete args.take;
const iterator = this.occurrenceStream._run(args);
let date = iterator.next().value;
let index = 0;
while (date && (count === undefined || count > index)) {
date.generators.unshift(this);
const yieldArgs = yield this.normalizeRunOutput(date);
date = iterator.next(yieldArgs).value;
index++;
}
}
}