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 extends OccurrenceGenerator implements IScheduleLike, IDataContainer { /** * 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 { return !!(object && typeof object === 'object' && (object as any)[SCHEDULE_ID]); } readonly rrules: ReadonlyArray> = []; readonly exrules: ReadonlyArray> = []; readonly rdates!: Dates; readonly exdates!: Dates; pipe: (...operatorFns: OperatorFnOutput[]) => OccurrenceStream = 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; /** * 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 | Rule>; exrules?: ReadonlyArray | Rule>; rdates?: ReadonlyArray> | Dates; exdates?: ReadonlyArray> | Dates; 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, { 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>, 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(...this.rrules), subtract(...this.exrules), add(this.rdates), subtract(this.exdates), unique(), ]; 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 = {}): OccurrenceIterator | Dates]> { return new OccurrenceIterator(this, this.normalizeOccurrencesArgs(args)); } collections(args: ICollectionsArgs = {}): CollectionIterator | Dates]> { return new CollectionIterator(this, this.normalizeCollectionsArgs(args)); } add(prop: 'rrule' | 'exrule', value: Rule): Schedule; add(prop: 'rdate' | 'exdate', value: DateInput): Schedule; add(prop: 'rdate' | 'exdate' | 'rrule' | 'exrule', value: Rule | DateInput) { 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); break; case 'exrule': exrules.push(value as Rule); break; case 'rdate': rdates = this.rdates.add(value as DateInput); break; case 'exdate': exdates = this.exdates.add(value as DateInput); break; } return new Schedule({ dateAdapter: this.dateAdapter, timezone: this.timezone, data: this.data, rrules, exrules, rdates, exdates, }); } remove(prop: 'rrule' | 'exrule', value: Rule): Schedule; remove(prop: 'rdate' | 'exdate', value: DateInput): Schedule; remove(prop: 'rdate' | 'exdate' | 'rrule' | 'exrule', value: Rule | DateInput) { 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); break; case 'exdate': exdates = this.exdates.remove(value as DateInput); 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; set(prop: 'rrules' | 'exrules', value: Rule[]): Schedule; set(prop: 'rdates' | 'exdates', value: Dates): Schedule; set( prop: 'timezone' | 'rrules' | 'exrules' | 'rdates' | 'exdates', value: string | null | Rule[] | Dates, 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[]; break; case 'exrules': exrules = value as Rule[]; break; case 'rdates': rdates = value as Dates; break; case 'exdates': exdates = value as Dates; break; } return new Schedule({ dateAdapter: this.dateAdapter, timezone, data: this.data, rrules, exrules, rdates, exdates, }); } /** @internal use occurrences() instead */ *_run(args: IRunArgs = {}): IterableIterator { 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++; } } }