From a21e23cc3eb80ac88fe04ab1a34b9ce874b5080d Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Tue, 4 Nov 2025 12:09:40 +0700 Subject: [PATCH 1/6] walking on eggs --- src/cache.ts | 4 +- ...kiterresult.ts => callback-iter-result.ts} | 2 +- src/{dateutil.ts => date-util.ts} | 0 src/{datewithzone.ts => date-with-zone.ts} | 2 +- src/datetime.ts | 6 +- src/index.ts | 10 +- src/{iterinfo => iter-info}/easter.ts | 0 src/{iterinfo => iter-info}/index.ts | 12 +- .../monthinfo.ts => iter-info/month-info.ts} | 6 +- .../yearinfo.ts => iter-info/year-info.ts} | 16 +- src/{iterresult.ts => iter-result.ts} | 31 +-- src/{iterset.ts => iter-set.ts} | 10 +- src/iter/{poslist.ts => build-pos-list.ts} | 8 +- src/iter/index.ts | 25 +- ...optimiseOptions.ts => optimize-options.ts} | 16 +- src/nlp/i18n.ts | 2 +- src/nlp/index.ts | 6 +- src/nlp/{parsetext.ts => parse-text.ts} | 4 +- src/nlp/{totext.ts => to-text.ts} | 0 ...ptionstostring.ts => options-to-string.ts} | 8 +- src/{parseoptions.ts => parse-options.ts} | 14 +- src/{parsestring.ts => parse-string.ts} | 6 +- src/rrule.ts | 58 ++--- src/rruleset.ts | 16 +- src/rrulestr.ts | 10 +- src/types.ts | 38 +-- src/weekday.ts | 50 ++-- test/cache.test.ts | 8 +- test/dateutil.test.ts | 2 +- test/datewithzone.test.ts | 7 +- test/lib/utils.ts | 26 +- test/nlp.test.ts | 8 +- test/optionstostring.test.ts | 6 +- test/parseoptions.test.ts | 4 +- test/parsestring.test.ts | 151 ++++++------ test/rrule.test.ts | 23 +- test/rruleset.test.ts | 24 +- test/rrulestr.test.ts | 224 +++++++++--------- 38 files changed, 409 insertions(+), 434 deletions(-) rename src/{callbackiterresult.ts => callback-iter-result.ts} (91%) rename src/{dateutil.ts => date-util.ts} (100%) rename src/{datewithzone.ts => date-with-zone.ts} (92%) rename src/{iterinfo => iter-info}/easter.ts (100%) rename src/{iterinfo => iter-info}/index.ts (93%) rename src/{iterinfo/monthinfo.ts => iter-info/month-info.ts} (95%) rename src/{iterinfo/yearinfo.ts => iter-info/year-info.ts} (97%) rename src/{iterresult.ts => iter-result.ts} (72%) rename src/{iterset.ts => iter-set.ts} (91%) rename src/iter/{poslist.ts => build-pos-list.ts} (85%) rename src/iter/{optimiseOptions.ts => optimize-options.ts} (92%) rename src/nlp/{parsetext.ts => parse-text.ts} (99%) rename src/nlp/{totext.ts => to-text.ts} (100%) rename src/{optionstostring.ts => options-to-string.ts} (92%) rename src/{parseoptions.ts => parse-options.ts} (97%) rename src/{parsestring.ts => parse-string.ts} (97%) diff --git a/src/cache.ts b/src/cache.ts index e2867b1c..90e8172a 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,6 +1,6 @@ -import IterResult, { IterArgs } from './iterresult' -import { clone, cloneDates } from './dateutil' +import { clone, cloneDates } from './date-util' import { isArray } from './helpers' +import IterResult, { IterArgs } from './iter-result' export type CacheKeys = 'before' | 'after' | 'between' diff --git a/src/callbackiterresult.ts b/src/callback-iter-result.ts similarity index 91% rename from src/callbackiterresult.ts rename to src/callback-iter-result.ts index e375cc26..f7728b77 100644 --- a/src/callbackiterresult.ts +++ b/src/callback-iter-result.ts @@ -1,4 +1,4 @@ -import IterResult, { IterArgs } from './iterresult' +import IterResult, { IterArgs } from './iter-result' type Iterator = (d: Date, len: number) => boolean diff --git a/src/dateutil.ts b/src/date-util.ts similarity index 100% rename from src/dateutil.ts rename to src/date-util.ts diff --git a/src/datewithzone.ts b/src/date-with-zone.ts similarity index 92% rename from src/datewithzone.ts rename to src/date-with-zone.ts index e05fcc2e..3e3a7edf 100644 --- a/src/datewithzone.ts +++ b/src/date-with-zone.ts @@ -1,4 +1,4 @@ -import { dateInTimeZone, timeToUntilString } from './dateutil' +import { dateInTimeZone, timeToUntilString } from './date-util' export class DateWithZone { public date: Date diff --git a/src/datetime.ts b/src/datetime.ts index 421f2341..5682bf1c 100644 --- a/src/datetime.ts +++ b/src/datetime.ts @@ -1,6 +1,6 @@ -import { ParsedOptions, Frequency } from './types' -import { pymod, divmod, empty, includes } from './helpers' -import { getWeekday, MAXYEAR, monthRange } from './dateutil' +import { getWeekday, MAXYEAR, monthRange } from './date-util' +import { divmod, empty, includes, pymod } from './helpers' +import { Frequency, ParsedOptions } from './types' export class Time { public hour: number diff --git a/src/index.ts b/src/index.ts index 237a275f..4a14fe7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,12 +13,10 @@ * https://github.com/jakubroztocil/rrule/blob/master/LICENCE * */ - export { RRule } from './rrule' export { RRuleSet } from './rruleset' -export { rrulestr } from './rrulestr' -export { Frequency, ByWeekday, Options } from './types' -export { Weekday, WeekdayStr, ALL_WEEKDAYS } from './weekday' -export { RRuleStrOptions } from './rrulestr' -export { datetime } from './dateutil' +export { datetime } from './date-util' +export { rrulestr, RRuleStrOptions } from './rrulestr' +export { ByWeekday, Frequency, Options } from './types' +export { ALL_WEEKDAYS, Weekday, WeekdayStr } from './weekday' diff --git a/src/iterinfo/easter.ts b/src/iter-info/easter.ts similarity index 100% rename from src/iterinfo/easter.ts rename to src/iter-info/easter.ts diff --git a/src/iterinfo/index.ts b/src/iter-info/index.ts similarity index 93% rename from src/iterinfo/index.ts rename to src/iter-info/index.ts index 4cca2f5d..0acf36ab 100644 --- a/src/iterinfo/index.ts +++ b/src/iter-info/index.ts @@ -1,10 +1,10 @@ -import { notEmpty, repeat, range, isPresent } from '../helpers' -import { ParsedOptions, Frequency } from '../types' -import { YearInfo, rebuildYear } from './yearinfo' -import { rebuildMonth, MonthInfo } from './monthinfo' -import { easter } from './easter' +import { datetime, sort, toOrdinal } from '../date-util' import { Time } from '../datetime' -import { datetime, sort, toOrdinal } from '../dateutil' +import { isPresent, notEmpty, range, repeat } from '../helpers' +import { Frequency, ParsedOptions } from '../types' +import { easter } from './easter' +import { MonthInfo, rebuildMonth } from './month-info' +import { YearInfo, rebuildYear } from './year-info' export type DaySet = [(number | null)[], number, number] export type GetDayset = () => DaySet diff --git a/src/iterinfo/monthinfo.ts b/src/iter-info/month-info.ts similarity index 95% rename from src/iterinfo/monthinfo.ts rename to src/iter-info/month-info.ts index 2a2045b6..812cbdec 100644 --- a/src/iterinfo/monthinfo.ts +++ b/src/iter-info/month-info.ts @@ -1,8 +1,8 @@ -import { ParsedOptions } from '../types' +import { empty, pymod, repeat } from '../helpers' import { RRule } from '../rrule' -import { empty, repeat, pymod } from '../helpers' +import { ParsedOptions } from '../types' -export interface MonthInfo { +export type MonthInfo = { lastyear: number lastmonth: number nwdaymask: number[] diff --git a/src/iterinfo/yearinfo.ts b/src/iter-info/year-info.ts similarity index 97% rename from src/iterinfo/yearinfo.ts rename to src/iter-info/year-info.ts index a34d2070..daaae8a5 100644 --- a/src/iterinfo/yearinfo.ts +++ b/src/iter-info/year-info.ts @@ -1,19 +1,19 @@ -import { ParsedOptions } from '../types' -import { datetime, getWeekday, isLeapYear, toOrdinal } from '../dateutil' -import { empty, repeat, pymod, includes } from '../helpers' +import { datetime, getWeekday, isLeapYear, toOrdinal } from '../date-util' +import { empty, includes, pymod, repeat } from '../helpers' import { M365MASK, - MDAY365MASK, - NMDAY365MASK, - WDAYMASK, M365RANGE, M366MASK, + M366RANGE, + MDAY365MASK, MDAY366MASK, + NMDAY365MASK, NMDAY366MASK, - M366RANGE, + WDAYMASK, } from '../masks' +import { ParsedOptions } from '../types' -export interface YearInfo { +export type YearInfo = { yearlen: 365 | 366 nextyearlen: 365 | 366 yearordinal: number diff --git a/src/iterresult.ts b/src/iter-result.ts similarity index 72% rename from src/iterresult.ts rename to src/iter-result.ts index 87842a4a..4d896ca1 100644 --- a/src/iterresult.ts +++ b/src/iter-result.ts @@ -1,4 +1,4 @@ -import { QueryMethodTypes, IterResultType } from './types' +import { IterResultType, QueryMethodTypes } from './types' // ============================================================================= // Results @@ -17,10 +17,10 @@ export interface IterArgs { * This class helps us to emulate python's generators, sorta. */ export default class IterResult { - public readonly method: M - public readonly args: Partial - public readonly minDate: Date | null = null - public readonly maxDate: Date | null = null + public method: M + public args: Partial + public minDate: Date | null = null + public maxDate: Date | null = null public _result: Date[] = [] public total = 0 @@ -40,14 +40,6 @@ export default class IterResult { } } - /** - * Possibly adds a date into the result. - * - * @param {Date} date - the date isn't necessarly added to the result - * list (if it is too late/too early) - * @return {Boolean} true if it makes sense to continue the iteration - * false if we're done. - */ accept(date: Date) { ++this.total const tooEarly = this.minDate && date < this.minDate @@ -67,28 +59,19 @@ export default class IterResult { return this.add(date) } - /** - * - * @param {Date} date that is part of the result. - * @return {Boolean} whether we are interested in more values. - */ add(date: Date) { this._result.push(date) return true } - /** - * 'before' and 'after' return only one date, whereas 'all' - * and 'between' an array. - * - * @return {Date,Array?} - */ getValue(): IterResultType { const res = this._result + switch (this.method) { case 'all': case 'between': return res as IterResultType + case 'before': case 'after': default: diff --git a/src/iterset.ts b/src/iter-set.ts similarity index 91% rename from src/iterset.ts rename to src/iter-set.ts index aad22b88..aad6b38c 100644 --- a/src/iterset.ts +++ b/src/iter-set.ts @@ -1,9 +1,9 @@ -import IterResult from './iterresult' -import { RRule } from './rrule' -import { DateWithZone } from './datewithzone' +import { sort } from './date-util' +import { DateWithZone } from './date-with-zone' import { iter } from './iter' -import { sort } from './dateutil' -import { QueryMethodTypes, IterResultType } from './types' +import IterResult from './iter-result' +import { RRule } from './rrule' +import { IterResultType, QueryMethodTypes } from './types' export function iterSet( iterResult: IterResult, diff --git a/src/iter/poslist.ts b/src/iter/build-pos-list.ts similarity index 85% rename from src/iter/poslist.ts rename to src/iter/build-pos-list.ts index 9276a1c8..94dda2c3 100644 --- a/src/iter/poslist.ts +++ b/src/iter/build-pos-list.ts @@ -1,9 +1,9 @@ -import { combine, fromOrdinal, sort } from '../dateutil' -import Iterinfo from '../iterinfo/index' -import { pymod, isPresent, includes } from '../helpers' +import { combine, fromOrdinal, sort } from '../date-util' import { Time } from '../datetime' +import { includes, isPresent, pymod } from '../helpers' +import Iterinfo from '../iter-info/index' -export function buildPoslist( +export function buildPosList( bysetpos: number[], timeset: Time[], start: number, diff --git a/src/iter/index.ts b/src/iter/index.ts index 7e718665..f1a10962 100644 --- a/src/iter/index.ts +++ b/src/iter/index.ts @@ -1,19 +1,19 @@ -import IterResult from '../iterresult' +import { combine, fromOrdinal, MAXYEAR } from '../date-util' +import { DateWithZone } from '../date-with-zone' +import { DateTime, Time } from '../datetime' +import { includes, isPresent, notEmpty } from '../helpers' +import Iterinfo from '../iter-info/index' +import IterResult from '../iter-result' +import { buildTimeset } from '../parse-options' +import { RRule } from '../rrule' import { freqIsDailyOrGreater, Options, ParsedOptions, QueryMethodTypes, } from '../types' -import { combine, fromOrdinal, MAXYEAR } from '../dateutil' -import Iterinfo from '../iterinfo/index' -import { RRule } from '../rrule' -import { buildTimeset } from '../parseoptions' -import { includes, isPresent, notEmpty } from '../helpers' -import { DateWithZone } from '../datewithzone' -import { buildPoslist } from './poslist' -import { DateTime, Time } from '../datetime' -import { optimiseOptions } from './optimiseOptions' +import { buildPosList } from './build-pos-list' +import { optimizeOptions } from './optimize-options' export function iter( iterResult: IterResult, @@ -22,13 +22,14 @@ export function iter( exdateHash?: { [k: number]: boolean }, evalExdate?: (after: Date, before: Date) => void, ) { - parsedOptions = optimiseOptions( + parsedOptions = optimizeOptions( iterResult, parsedOptions, origOptions, exdateHash, evalExdate, ) + const { freq, dtstart, interval, until, bysetpos } = parsedOptions let count = parsedOptions.count @@ -53,7 +54,7 @@ export function iter( const filtered = removeFilteredDays(dayset, start, end, ii, parsedOptions) if (notEmpty(bysetpos)) { - const poslist = buildPoslist(bysetpos, timeset, start, end, ii, dayset) + const poslist = buildPosList(bysetpos, timeset, start, end, ii, dayset) for (let j = 0; j < poslist.length; j++) { const res = poslist[j] diff --git a/src/iter/optimiseOptions.ts b/src/iter/optimize-options.ts similarity index 92% rename from src/iter/optimiseOptions.ts rename to src/iter/optimize-options.ts index 93b617f0..be014029 100644 --- a/src/iter/optimiseOptions.ts +++ b/src/iter/optimize-options.ts @@ -1,11 +1,11 @@ -import { DateTime, DurationUnit } from 'luxon' +import { DateTime } from 'luxon' -import { Frequency, Options, ParsedOptions, QueryMethodTypes } from '../types' -import IterResult from '../iterresult' import { notEmpty } from '../helpers' +import IterResult from '../iter-result' +import { Frequency, Options, ParsedOptions, QueryMethodTypes } from '../types' import { Weekday } from '../weekday' -const UNIT_BY_FREQUENCY: Record> = { +const UNIT_BY_FREQUENCY = { [Frequency.YEARLY]: 'year', [Frequency.MONTHLY]: 'month', [Frequency.WEEKLY]: 'week', @@ -13,9 +13,9 @@ const UNIT_BY_FREQUENCY: Record> = { [Frequency.HOURLY]: 'hour', [Frequency.MINUTELY]: 'minute', [Frequency.SECONDLY]: 'second', -} +} as const -const optimize = ( +function optimize( frequency: Frequency, dtstart: Date, interval: number, @@ -24,7 +24,7 @@ const optimize = ( count?: number, exdateHash?: { [k: number]: boolean }, evalExdate?: (after: Date, before: Date) => void, -) => { +) { const frequencyUnit = UNIT_BY_FREQUENCY[frequency] const minDateTime = DateTime.fromJSDate(minDate ? minDate : maxDate, { zone: 'UTC', @@ -65,7 +65,7 @@ const optimize = ( } } -export function optimiseOptions( +export function optimizeOptions( iterResult: IterResult, parsedOptions: ParsedOptions, origOptions: Partial, diff --git a/src/nlp/i18n.ts b/src/nlp/i18n.ts index 1b2e3953..e91a0a84 100644 --- a/src/nlp/i18n.ts +++ b/src/nlp/i18n.ts @@ -2,7 +2,7 @@ // i18n // ============================================================================= -export interface Language { +export type Language = { dayNames: string[] monthNames: string[] tokens: { diff --git a/src/nlp/index.ts b/src/nlp/index.ts index f1084832..efb4bccd 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,8 +1,8 @@ -import ToText, { DateFormatter, GetText } from './totext' -import parseText from './parsetext' import { RRule } from '../rrule' import { Frequency } from '../types' import ENGLISH, { Language } from './i18n' +import parseText from './parse-text' +import ToText, { DateFormatter, GetText } from './to-text' /* ! * rrule.js - Library for working with recurrence rules for calendar dates. @@ -138,4 +138,4 @@ export interface Nlp { toText: typeof toText } -export { fromText, parseText, isFullyConvertible, toText } +export { fromText, isFullyConvertible, parseText, toText } diff --git a/src/nlp/parsetext.ts b/src/nlp/parse-text.ts similarity index 99% rename from src/nlp/parsetext.ts rename to src/nlp/parse-text.ts index b48560ea..c0c4722e 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parse-text.ts @@ -1,14 +1,14 @@ -import ENGLISH, { Language } from './i18n' import { RRule } from '../rrule' import { ByWeekday, Options } from '../types' import { WeekdayStr } from '../weekday' +import ENGLISH, { Language } from './i18n' // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp } + private rules: { [k: string]: RegExp } public text: string public symbol: string | null public value: RegExpExecArray | null diff --git a/src/nlp/totext.ts b/src/nlp/to-text.ts similarity index 100% rename from src/nlp/totext.ts rename to src/nlp/to-text.ts diff --git a/src/optionstostring.ts b/src/options-to-string.ts similarity index 92% rename from src/optionstostring.ts rename to src/options-to-string.ts index b444b58a..146cf30a 100644 --- a/src/optionstostring.ts +++ b/src/options-to-string.ts @@ -1,9 +1,9 @@ +import { timeToUntilString } from './date-util' +import { DateWithZone } from './date-with-zone' +import { includes, isArray, isNumber, isPresent, toArray } from './helpers' +import { DEFAULT_OPTIONS, RRule } from './rrule' import { Options } from './types' -import { RRule, DEFAULT_OPTIONS } from './rrule' -import { includes, isPresent, isArray, isNumber, toArray } from './helpers' import { Weekday } from './weekday' -import { timeToUntilString } from './dateutil' -import { DateWithZone } from './datewithzone' export function optionsToString(options: Partial) { const rrule: string[][] = [] diff --git a/src/parseoptions.ts b/src/parse-options.ts similarity index 97% rename from src/parseoptions.ts rename to src/parse-options.ts index 8be481d0..ac9a8bc1 100644 --- a/src/parseoptions.ts +++ b/src/parse-options.ts @@ -1,16 +1,16 @@ -import { Options, ParsedOptions, freqIsDailyOrGreater } from './types' +import { getWeekday, isDate, isValidDate } from './date-util' +import { Time } from './datetime' import { includes, - notEmpty, - isPresent, - isNumber, isArray, + isNumber, + isPresent, isWeekdayStr, + notEmpty, } from './helpers' -import { RRule, defaultKeys, DEFAULT_OPTIONS } from './rrule' -import { getWeekday, isDate, isValidDate } from './dateutil' +import { DEFAULT_OPTIONS, RRule, defaultKeys } from './rrule' +import { Options, ParsedOptions, freqIsDailyOrGreater } from './types' import { Weekday } from './weekday' -import { Time } from './datetime' export function initializeOptions(options: Partial) { const invalid: string[] = [] diff --git a/src/parsestring.ts b/src/parse-string.ts similarity index 97% rename from src/parsestring.ts rename to src/parse-string.ts index fd3a5588..d28c7a42 100644 --- a/src/parsestring.ts +++ b/src/parse-string.ts @@ -1,7 +1,7 @@ -import { Options, Frequency } from './types' -import { Weekday } from './weekday' -import { untilStringToDate } from './dateutil' +import { untilStringToDate } from './date-util' import { Days } from './rrule' +import { Frequency, Options } from './types' +import { Weekday } from './weekday' export function parseString(rfcString: string): Partial { const options = rfcString diff --git a/src/rrule.ts b/src/rrule.ts index 86816d44..226665f4 100644 --- a/src/rrule.ts +++ b/src/rrule.ts @@ -1,24 +1,24 @@ -import { isValidDate } from './dateutil' +import { isValidDate } from './date-util' -import IterResult, { IterArgs } from './iterresult' -import CallbackIterResult from './callbackiterresult' +import { Cache, CacheKeys } from './cache' +import CallbackIterResult from './callback-iter-result' +import IterResult, { IterArgs } from './iter-result' +import { iter } from './iter/index' import { Language } from './nlp/i18n' -import { fromText, parseText, toText, isFullyConvertible } from './nlp/index' -import { DateFormatter, GetText } from './nlp/totext' +import { fromText, isFullyConvertible, parseText, toText } from './nlp/index' +import { DateFormatter, GetText } from './nlp/to-text' +import { optionsToString } from './options-to-string' +import { initializeOptions, parseOptions } from './parse-options' +import { parseString } from './parse-string' import { - ParsedOptions, - Options, Frequency, + IterResultType, + Options, + ParsedOptions, QueryMethods, QueryMethodTypes, - IterResultType, } from './types' -import { parseOptions, initializeOptions } from './parseoptions' -import { parseString } from './parsestring' -import { optionsToString } from './optionstostring' -import { Cache, CacheKeys } from './cache' import { Weekday } from './weekday' -import { iter } from './iter/index' // ============================================================================= // RRule @@ -72,7 +72,7 @@ export class RRule implements QueryMethods { // RRule class 'constants' - static readonly FREQUENCIES: (keyof typeof Frequency)[] = [ + static FREQUENCIES: (keyof typeof Frequency)[] = [ 'YEARLY', 'MONTHLY', 'WEEKLY', @@ -82,21 +82,21 @@ export class RRule implements QueryMethods { 'SECONDLY', ] - static readonly YEARLY = Frequency.YEARLY - static readonly MONTHLY = Frequency.MONTHLY - static readonly WEEKLY = Frequency.WEEKLY - static readonly DAILY = Frequency.DAILY - static readonly HOURLY = Frequency.HOURLY - static readonly MINUTELY = Frequency.MINUTELY - static readonly SECONDLY = Frequency.SECONDLY - - static readonly MO = Days.MO - static readonly TU = Days.TU - static readonly WE = Days.WE - static readonly TH = Days.TH - static readonly FR = Days.FR - static readonly SA = Days.SA - static readonly SU = Days.SU + static YEARLY = Frequency.YEARLY + static MONTHLY = Frequency.MONTHLY + static WEEKLY = Frequency.WEEKLY + static DAILY = Frequency.DAILY + static HOURLY = Frequency.HOURLY + static MINUTELY = Frequency.MINUTELY + static SECONDLY = Frequency.SECONDLY + + static MO = Days.MO + static TU = Days.TU + static WE = Days.WE + static TH = Days.TH + static FR = Days.FR + static SA = Days.SA + static SU = Days.SU constructor(options: Partial = {}, noCache = false) { // RFC string diff --git a/src/rruleset.ts b/src/rruleset.ts index 327ac8a0..c7b6ea20 100644 --- a/src/rruleset.ts +++ b/src/rruleset.ts @@ -1,8 +1,8 @@ -import { sort, timeToUntilString } from './dateutil' +import { sort, timeToUntilString } from './date-util' import { includes } from './helpers' -import IterResult from './iterresult' -import { iterSet } from './iterset' -import { optionsToString } from './optionstostring' +import IterResult from './iter-result' +import { iterSet } from './iter-set' +import { optionsToString } from './options-to-string' import { RRule } from './rrule' import { rrulestr } from './rrulestr' import { IterResultType, QueryMethodTypes } from './types' @@ -27,10 +27,10 @@ function createGetterSetter(fieldName: string) { } export class RRuleSet extends RRule { - public readonly _rrule: RRule[] - public readonly _rdate: Date[] - public readonly _exrule: RRule[] - public readonly _exdate: Date[] + public _rrule: RRule[] + public _rdate: Date[] + public _exrule: RRule[] + public _exdate: Date[] private _dtstart?: Date | null | undefined private _tzid?: string diff --git a/src/rrulestr.ts b/src/rrulestr.ts index 2934180b..24fe3c89 100644 --- a/src/rrulestr.ts +++ b/src/rrulestr.ts @@ -1,9 +1,9 @@ +import { untilStringToDate } from './date-util' +import { includes, split } from './helpers' +import { parseDtstart, parseString } from './parse-string' import { RRule } from './rrule' import { RRuleSet } from './rruleset' -import { untilStringToDate } from './dateutil' -import { includes, split } from './helpers' import { Options } from './types' -import { parseString, parseDtstart } from './parsestring' export interface RRuleStrOptions { dtstart: Date | null @@ -27,13 +27,15 @@ const DEFAULT_OPTIONS: RRuleStrOptions = { tzid: null, } -export function parseInput(s: string, options: Partial) { +export function parseInput(s: string, options: Partial = {}) { const rrulevals: Partial[] = [] let rdatevals: Date[] = [] + const exrulevals: Partial[] = [] let exdatevals: Date[] = [] const parsedDtstart = parseDtstart(s) + const { dtstart } = parsedDtstart let { tzid } = parsedDtstart diff --git a/src/types.ts b/src/types.ts index 3fad1552..6b85f9ef 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import { Weekday, WeekdayStr } from './weekday' -export interface QueryMethods { +export type QueryMethods = { all(): Date[] between(after: Date, before: Date, inc: boolean): Date[] before(date: Date, inc: boolean): Date | null @@ -36,24 +36,24 @@ export function freqIsDailyOrGreater( export interface Options { freq: Frequency - dtstart: Date | null + dtstart?: Date interval: number - wkst: Weekday | number | null - count: number | null - until: Date | null - tzid: string | null - bysetpos: number | number[] | null - bymonth: number | number[] | null - bymonthday: number | number[] | null - bynmonthday: number[] | null - byyearday: number | number[] | null - byweekno: number | number[] | null - byweekday: ByWeekday | ByWeekday[] | null - bynweekday: number[][] | null - byhour: number | number[] | null - byminute: number | number[] | null - bysecond: number | number[] | null - byeaster: number | null + wkst?: Weekday | number + count?: number + until?: Date + tzid?: string + bysetpos?: number | number[] + bymonth?: number | number[] + bymonthday?: number | number[] + bynmonthday?: number[] + byyearday?: number | number[] + byweekno?: number | number[] + byweekday?: ByWeekday | ByWeekday[] + bynweekday?: number[][] + byhour?: number | number[] + byminute?: number | number[] + bysecond?: number | number[] + byeaster?: number } export interface ParsedOptions extends Options { @@ -71,4 +71,4 @@ export interface ParsedOptions extends Options { bysecond: number[] } -export type ByWeekday = WeekdayStr | number | Weekday +export type ByWeekday = Weekday | WeekdayStr | number diff --git a/src/weekday.ts b/src/weekday.ts index dc4d61c6..55799822 100644 --- a/src/weekday.ts +++ b/src/weekday.ts @@ -2,50 +2,42 @@ // Weekday // ============================================================================= -export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU' -export const ALL_WEEKDAYS: WeekdayStr[] = [ - 'MO', - 'TU', - 'WE', - 'TH', - 'FR', - 'SA', - 'SU', -] +export const ALL_WEEKDAYS = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] as const + +export type WeekdayStr = (typeof ALL_WEEKDAYS)[number] export class Weekday { - public readonly weekday: number - public readonly n?: number + public weekday: number + public n?: number + + static fromStr(str: WeekdayStr) { + return new Weekday(ALL_WEEKDAYS.indexOf(str)) + } constructor(weekday: number, n?: number) { - if (n === 0) throw new Error("Can't create weekday with n == 0") + if (n === 0) { + throw new Error("Can't create weekday with n == 0") + } + this.weekday = weekday this.n = n } - static fromStr(str: WeekdayStr): Weekday { - return new Weekday(ALL_WEEKDAYS.indexOf(str)) + equals(other: Weekday) { + return this.weekday === other.weekday && this.n === other.n } - // __call__ - Cannot call the object directly, do it through - // e.g. RRule.TH.nth(-1) instead, - nth(n: number) { - return this.n === n ? this : new Weekday(this.weekday, n) + getJsWeekday() { + return this.weekday === 6 ? 0 : this.weekday + 1 } - // __eq__ - equals(other: Weekday) { - return this.weekday === other.weekday && this.n === other.n + nth(n: number) { + return this.n === n ? this : new Weekday(this.weekday, n) } - // __repr__ toString() { - let s: string = ALL_WEEKDAYS[this.weekday] - if (this.n) s = (this.n > 0 ? '+' : '') + String(this.n) + s - return s - } + const count = this.n ? `${this.n > 0 ? '+' : ''}${this.n}` : '' - getJsWeekday() { - return this.weekday === 6 ? 0 : this.weekday + 1 + return `${count}${ALL_WEEKDAYS[this.weekday]}` } } diff --git a/test/cache.test.ts b/test/cache.test.ts index eb38522d..53754533 100644 --- a/test/cache.test.ts +++ b/test/cache.test.ts @@ -1,5 +1,5 @@ import { Cache } from '../src/cache' -import { IterArgs } from '../src/iterresult' +import { IterArgs } from '../src/iter-result' const dates = [ new Date('2021-01-01T00:00:00.000+00:00'), @@ -14,6 +14,7 @@ const dates = [ describe('Cache', () => { it('returns false for an empty cache', () => { const cache = new Cache() + const args: Partial = { after: new Date('2021-01-01T00:00:00.000+00:00'), before: new Date('2021-01-08T00:00:00.000+00:00'), @@ -25,6 +26,7 @@ describe('Cache', () => { it('returns an empty array for a cached but empty set', () => { const cache = new Cache() + const args: Partial = { after: new Date('2021-01-01T00:00:00.000+00:00'), before: new Date('2021-01-08T00:00:00.000+00:00'), @@ -38,6 +40,7 @@ describe('Cache', () => { it('returns cached entries if the "what" and the args both match', () => { const cache = new Cache() + const args: Partial = { after: new Date('2021-01-01T00:00:00.000+00:00'), before: new Date('2021-01-08T00:00:00.000+00:00'), @@ -51,6 +54,7 @@ describe('Cache', () => { it('does not return cached entries if the "what" matches but the args do not', () => { const cache = new Cache() + const args: Partial = { after: new Date('2021-01-01T00:00:00.000+00:00'), before: new Date('2021-01-08T00:00:00.000+00:00'), @@ -70,6 +74,7 @@ describe('Cache', () => { it('does not return cached entries if args match but the "what" does not', () => { const cache = new Cache() + const args: Partial = { after: new Date('2021-01-01T00:00:00.000+00:00'), before: new Date('2021-01-08T00:00:00.000+00:00'), @@ -83,6 +88,7 @@ describe('Cache', () => { it('reuses dates cached for the "all" method when querying using another method', () => { const cache = new Cache() + const args: Partial = { after: new Date('2021-01-04T00:00:00.000+00:00'), before: new Date('2021-01-06T00:00:00.000+00:00'), diff --git a/test/dateutil.test.ts b/test/dateutil.test.ts index b7787dd3..5f3f550c 100644 --- a/test/dateutil.test.ts +++ b/test/dateutil.test.ts @@ -1,4 +1,4 @@ -import { datetime, untilStringToDate } from '../src/dateutil' +import { datetime, untilStringToDate } from '../src/date-util' describe('untilStringToDate', () => { it('parses a date string', () => { diff --git a/test/datewithzone.test.ts b/test/datewithzone.test.ts index 954bfd57..740518bc 100644 --- a/test/datewithzone.test.ts +++ b/test/datewithzone.test.ts @@ -1,6 +1,7 @@ -import { DateWithZone } from '../src/datewithzone' -import { set as setMockDate, reset as resetMockDate } from 'mockdate' -import { datetime, expectedDate } from './lib/utils' +import { reset as resetMockDate, set as setMockDate } from 'mockdate' +import { datetime } from '../src/date-util' +import { DateWithZone } from '../src/date-with-zone' +import { expectedDate } from './lib/utils' describe('toString', () => { it('returns the date when no tzid is present', () => { diff --git a/test/lib/utils.ts b/test/lib/utils.ts index 902be8a0..b013b57c 100644 --- a/test/lib/utils.ts +++ b/test/lib/utils.ts @@ -1,12 +1,12 @@ -export { datetime } from '../../src/dateutil' -import { RRule, RRuleSet } from '../../src' -import { dateInTimeZone, datetime } from '../../src/dateutil' - -export const TEST_CTX = { - ALSO_TESTSTRING_FUNCTIONS: false, - ALSO_TESTNLP_FUNCTIONS: false, - ALSO_TESTBEFORE_AFTER_BETWEEN: false, - ALSO_TESTSUBSECOND_PRECISION: false, +import { dateInTimeZone, datetime } from '../../src/date-util' +import { RRule } from '../../src/rrule' +import { RRuleSet } from '../../src/rruleset' + +export const ALSO_TEST = { + STRING_FUNCTIONS: false, + NLP_FUNCTIONS: false, + BEFORE_AFTER_BETWEEN: false, + SUBSECOND_PRECISION: false, } export function isNumber(maybeNumber: any): maybeNumber is number { @@ -120,13 +120,13 @@ export const testRecurring = function ( // Additional tests using the expected dates // ========================================================== - if (TEST_CTX.ALSO_TESTSUBSECOND_PRECISION) { + if (ALSO_TEST.SUBSECOND_PRECISION) { expect(actualDates.map(extractTime)).toEqual( expectedDates.map(extractTime), ) } - if (TEST_CTX.ALSO_TESTSTRING_FUNCTIONS) { + if (ALSO_TEST.STRING_FUNCTIONS) { // Test toString()/fromString() const str = rule.toString() const rrule2 = RRule.fromString(str) @@ -138,7 +138,7 @@ export const testRecurring = function ( } if ( - TEST_CTX.ALSO_TESTNLP_FUNCTIONS && + ALSO_TEST.NLP_FUNCTIONS && rule.isFullyConvertibleToText && rule.isFullyConvertibleToText() ) { @@ -158,7 +158,7 @@ export const testRecurring = function ( expect(rrule3.toString()).toBe(str) } - if (method === 'all' && TEST_CTX.ALSO_TESTBEFORE_AFTER_BETWEEN) { + if (method === 'all' && ALSO_TEST.BEFORE_AFTER_BETWEEN) { // Test before, after, and between - use the expected dates. // create a clean copy of the rrule object to bypass caching rule = rule.clone() diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 152ff666..9385ceb9 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -1,7 +1,7 @@ -import { RRule } from '../src' -import { optionsToString } from '../src/optionstostring' -import { DateFormatter } from '../src/nlp/totext' -import { datetime } from './lib/utils' +import { datetime } from '../src/date-util' +import { DateFormatter } from '../src/nlp/to-text' +import { optionsToString } from '../src/options-to-string' +import { RRule } from '../src/rrule' const texts = [ ['Every day', 'RRULE:FREQ=DAILY'], diff --git a/test/optionstostring.test.ts b/test/optionstostring.test.ts index b502622e..aeba173f 100644 --- a/test/optionstostring.test.ts +++ b/test/optionstostring.test.ts @@ -1,7 +1,7 @@ -import { Options } from '../src/types' +import { datetime } from '../src/date-util' +import { optionsToString } from '../src/options-to-string' import { RRule } from '../src/rrule' -import { optionsToString } from '../src/optionstostring' -import { datetime } from './lib/utils' +import { Options } from '../src/types' describe('optionsToString', () => { it('serializes valid single lines of rrules', function () { diff --git a/test/parseoptions.test.ts b/test/parseoptions.test.ts index 487f164e..5366ec84 100644 --- a/test/parseoptions.test.ts +++ b/test/parseoptions.test.ts @@ -1,5 +1,5 @@ -import { parseOptions } from '../src/parseoptions' -import { RRule } from '../src' +import { parseOptions } from '../src/parse-options' +import { RRule } from '../src/rrule' describe('TZID', () => { it('leaves null when null', () => { diff --git a/test/parsestring.test.ts b/test/parsestring.test.ts index d96e86e7..48d13a19 100644 --- a/test/parsestring.test.ts +++ b/test/parsestring.test.ts @@ -1,91 +1,76 @@ +import { datetime } from '../src/date-util' +import { parseString } from '../src/parse-string' import { RRule } from '../src/rrule' -import { parseString } from '../src/parsestring' -import { Options, Frequency } from '../src/types' -import { datetime } from './lib/utils' +import { Frequency, Options } from '../src/types' describe('parseString', () => { - it('parses valid single lines of rrules', function () { - const expectations: [string, Partial][] = [ - [ - 'FREQ=WEEKLY;UNTIL=20100101T000000Z', - { freq: RRule.WEEKLY, until: datetime(2010, 1, 1, 0, 0, 0) }, - ], + it.each([ + [ + 'FREQ=WEEKLY;UNTIL=20100101T000000Z', + { freq: RRule.WEEKLY, until: datetime(2010, 1, 1, 0, 0, 0) }, + ], - // Parse also `date` but return `date-time` - [ - 'FREQ=WEEKLY;UNTIL=20100101', - { freq: RRule.WEEKLY, until: datetime(2010, 1, 1, 0, 0, 0) }, - ], - [ - 'DTSTART;TZID=America/New_York:19970902T090000', - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - tzid: 'America/New_York', - }, - ], - [ - 'RRULE:DTSTART;TZID=America/New_York:19970902T090000', - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - tzid: 'America/New_York', - }, - ], - ] + // Parse also `date` but return `date-time` + [ + 'FREQ=WEEKLY;UNTIL=20100101', + { freq: RRule.WEEKLY, until: datetime(2010, 1, 1, 0, 0, 0) }, + ], + [ + 'DTSTART;TZID=America/New_York:19970902T090000', + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + tzid: 'America/New_York', + }, + ], + [ + 'RRULE:DTSTART;TZID=America/New_York:19970902T090000', + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + tzid: 'America/New_York', + }, + ], + ])( + 'parses valid single lines of rrules', + function (input: string, expected: Partial) { + expect(parseString(input)).toEqual(expected) + }, + ) - expectations.forEach(function (item) { - const s = item[0] - const s2 = item[1] - // s - expect(parseString(s)).toEqual(s2) - }) + it.each([ + [ + 'DTSTART;TZID=America/New_York:19970902T090000\nRRULE:FREQ=WEEKLY;UNTIL=20100101T000000Z', + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + tzid: 'America/New_York', + freq: RRule.WEEKLY, + until: datetime(2010, 1, 1, 0, 0, 0), + }, + ], + [ + 'DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=YEARLY;COUNT=3\n', + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + freq: RRule.YEARLY, + count: 3, + }, + ], + ])('parses multiline rules', (input: string, expected: Partial) => { + expect(parseString(input)).toEqual(expected) }) - it('parses multiline rules', () => { - const expectations: [string, Partial][] = [ - [ - 'DTSTART;TZID=America/New_York:19970902T090000\nRRULE:FREQ=WEEKLY;UNTIL=20100101T000000Z', - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - tzid: 'America/New_York', - freq: RRule.WEEKLY, - until: datetime(2010, 1, 1, 0, 0, 0), - }, - ], - [ - 'DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=YEARLY;COUNT=3\n', - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - freq: RRule.YEARLY, - count: 3, - }, - ], - ] - - expectations.forEach(function (item) { - const s = item[0] - const s2 = item[1] - // s - expect(parseString(s)).toEqual(s2) - }) - }) - - it('parses legacy dtstart in rrule', () => { - const expectations: [string, Partial][] = [ - [ - 'RRULE:FREQ=WEEKLY;DTSTART;TZID=America/New_York:19970902T090000', - { - freq: Frequency.WEEKLY, - dtstart: datetime(1997, 9, 2, 9, 0, 0), - tzid: 'America/New_York', - }, - ], - ] - - expectations.forEach(function (item) { - const s = item[0] - const s2 = item[1] - // s - expect(parseString(s)).toEqual(s2) - }) - }) + it.each([ + [ + 'RRULE:FREQ=WEEKLY;DTSTART;TZID=America/New_York:19970902T090000', + { + freq: Frequency.WEEKLY, + dtstart: datetime(1997, 9, 2, 9, 0, 0), + tzid: 'America/New_York', + }, + ], + ])( + 'parses legacy dtstart in rrule', + (input: string, expected: Partial) => { + expect(parseString(input)).toEqual(expected) + }, + ) }) diff --git a/test/rrule.test.ts b/test/rrule.test.ts index c1c2b2d9..bee2ac81 100644 --- a/test/rrule.test.ts +++ b/test/rrule.test.ts @@ -1,30 +1,27 @@ -import { - parse, - datetime, - testRecurring, - expectedDate, - TEST_CTX, -} from './lib/utils' -import { RRule, rrulestr, Frequency } from '../src/index' -import { set as setMockDate, reset as resetMockDate } from 'mockdate' +import { reset as resetMockDate, set as setMockDate } from 'mockdate' +import { datetime } from '../src/date-util' +import { RRule } from '../src/rrule' +import { rrulestr } from '../src/rrulestr' +import { Frequency } from '../src/types' +import { ALSO_TEST, expectedDate, parse, testRecurring } from './lib/utils' describe('RRule', function () { beforeAll(() => { // Enable additional toString() / fromString() tests // for each testRecurring(). - TEST_CTX.ALSO_TESTSTRING_FUNCTIONS = true + ALSO_TEST.STRING_FUNCTIONS = true // Enable additional toText() / fromText() tests // for each testRecurring(). // Many of the tests fail because the conversion is only approximate, // but it gives an idea about how well or bad it converts. - TEST_CTX.ALSO_TESTNLP_FUNCTIONS = false + ALSO_TEST.NLP_FUNCTIONS = false // Thorough after()/before()/between() tests. // NOTE: can take a longer time. - TEST_CTX.ALSO_TESTBEFORE_AFTER_BETWEEN = true + ALSO_TEST.BEFORE_AFTER_BETWEEN = true - TEST_CTX.ALSO_TESTSUBSECOND_PRECISION = true + ALSO_TEST.SUBSECOND_PRECISION = true }) it('rrulestr https://github.com/jkbrzt/rrule/pull/164', function () { diff --git a/test/rruleset.test.ts b/test/rruleset.test.ts index 28e0c1ab..79405da4 100644 --- a/test/rruleset.test.ts +++ b/test/rruleset.test.ts @@ -1,30 +1,28 @@ -import { - parse, - datetime, - testRecurring, - expectedDate, - TEST_CTX, -} from './lib/utils' -import { RRule, RRuleSet, rrulestr, Frequency } from '../src' -import { set as setMockDate, reset as resetMockDate } from 'mockdate' +import { reset as resetMockDate, set as setMockDate } from 'mockdate' +import { datetime } from '../src/date-util' +import { RRule } from '../src/rrule' +import { RRuleSet } from '../src/rruleset' +import { rrulestr } from '../src/rrulestr' +import { Frequency } from '../src/types' +import { ALSO_TEST, expectedDate, parse, testRecurring } from './lib/utils' describe('RRuleSet', function () { beforeAll(() => { // Enable additional toString() / fromString() tests // for each testRecurring(). - TEST_CTX.ALSO_TESTSTRING_FUNCTIONS = false + ALSO_TEST.STRING_FUNCTIONS = false // Enable additional toText() / fromText() tests // for each testRecurring(). // Many of the tests fail because the conversion is only approximate, // but it gives an idea about how well or bad it converts. - TEST_CTX.ALSO_TESTNLP_FUNCTIONS = false + ALSO_TEST.NLP_FUNCTIONS = false // Thorough after()/before()/between() tests. // NOTE: can take a longer time. - TEST_CTX.ALSO_TESTBEFORE_AFTER_BETWEEN = false + ALSO_TEST.BEFORE_AFTER_BETWEEN = false - TEST_CTX.ALSO_TESTSUBSECOND_PRECISION = false + ALSO_TEST.SUBSECOND_PRECISION = false }) testRecurring( diff --git a/test/rrulestr.test.ts b/test/rrulestr.test.ts index 0b6698b8..fbb84722 100644 --- a/test/rrulestr.test.ts +++ b/test/rrulestr.test.ts @@ -1,50 +1,50 @@ -import { parse, datetime, testRecurring, TEST_CTX } from './lib/utils' -import { RRule, RRuleSet, rrulestr, Frequency } from '../src' -import { Days } from '../src/rrule' -import { parseInput } from '../src/rrulestr' +import { datetime } from '../src/date-util' +import { Days, RRule } from '../src/rrule' +import { RRuleSet } from '../src/rruleset' +import { parseInput, rrulestr } from '../src/rrulestr' +import { Frequency } from '../src/types' +import { ALSO_TEST, parse, testRecurring } from './lib/utils' describe('rrulestr', function () { beforeAll(() => { // Enable additional toString() / fromString() tests // for each testRecurring(). - TEST_CTX.ALSO_TESTSTRING_FUNCTIONS = false + ALSO_TEST.STRING_FUNCTIONS = false // Enable additional toText() / fromText() tests // for each testRecurring(). // Many of the tests fail because the conversion is only approximate, // but it gives an idea about how well or bad it converts. - TEST_CTX.ALSO_TESTNLP_FUNCTIONS = false + ALSO_TEST.NLP_FUNCTIONS = false // Thorough after()/before()/between() tests. // NOTE: can take a longer time. - TEST_CTX.ALSO_TESTBEFORE_AFTER_BETWEEN = true + ALSO_TEST.BEFORE_AFTER_BETWEEN = true - TEST_CTX.ALSO_TESTSUBSECOND_PRECISION = false + ALSO_TEST.SUBSECOND_PRECISION = false }) + const basicRule = [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=3', + ].join('\n') + it('parses an rrule', () => { - expect( - rrulestr('DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=YEARLY;COUNT=3\n'), - ).toBeInstanceOf(RRule) + expect(rrulestr(basicRule)).toBeInstanceOf(RRule) + }) + + it('parses an rruleset when forceset=true', () => { + expect(rrulestr(basicRule, { forceset: true })).toBeInstanceOf(RRuleSet) }) it('parses an rrule without frequency', () => { const rRuleString = 'DTSTART:19970902T090000Z' - const parsedRRuleSet = rrulestr(rRuleString, { - forceset: true, - }) as RRuleSet - expect(parsedRRuleSet.toString()).toBe(rRuleString) - const parsedRRule = rrulestr(rRuleString) as RRule + const parsedRRule = rrulestr(rRuleString) expect(parsedRRule.toString()).toBe(rRuleString) - }) - it('parses an rruleset when forceset=true', () => { - expect( - rrulestr('DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=YEARLY;COUNT=3\n', { - forceset: true, - }), - ).toBeInstanceOf(RRuleSet) + const parsedRRuleSet = rrulestr(rRuleString, { forceset: true }) + expect(parsedRRuleSet.toString()).toBe(rRuleString) }) it('parses an rruleset when there are multiple rrules', () => { @@ -57,19 +57,15 @@ describe('rrulestr', function () { ).toBeInstanceOf(RRuleSet) }) - testRecurring( - 'testStr', - rrulestr('DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=YEARLY;COUNT=3\n'), - [ - datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0), - ], - ) + testRecurring('testStr', rrulestr(basicRule), [ + datetime(1997, 9, 2, 9, 0), + datetime(1998, 9, 2, 9, 0), + datetime(1999, 9, 2, 9, 0), + ]) testRecurring( 'testStrCase', - rrulestr('dtstart:19970902T090000Z\n' + 'rrule:freq=yearly;count=3\n'), + rrulestr('dtstart:19970902T090000Z rrule:freq=yearly;count=3'), [ datetime(1997, 9, 2, 9, 0), datetime(1998, 9, 2, 9, 0), @@ -79,7 +75,7 @@ describe('rrulestr', function () { testRecurring( 'testStrSpaces', - rrulestr(' DTSTART:19970902T090000Z ' + ' RRULE:FREQ=YEARLY;COUNT=3 '), + rrulestr(' DTSTART:19970902T090000Z RRULE:FREQ=YEARLY;COUNT=3 '), [ datetime(1997, 9, 2, 9, 0), datetime(1998, 9, 2, 9, 0), @@ -90,7 +86,9 @@ describe('rrulestr', function () { testRecurring( 'testStrSpacesAndLines', rrulestr( - ' DTSTART:19970902T090000Z \n' + ' \n RRULE:FREQ=YEARLY;COUNT=3 \n', + [' DTSTART:19970902T090000Z ', '', ' RRULE:FREQ=YEARLY;COUNT=3 '].join( + '\n', + ), ), [ datetime(1997, 9, 2, 9, 0), @@ -101,7 +99,7 @@ describe('rrulestr', function () { testRecurring( 'testStrNoDTStart', - rrulestr('RRULE:FREQ=YEARLY;COUNT=3\n', { + rrulestr('RRULE:FREQ=YEARLY;COUNT=3', { dtstart: parse('19970902T090000'), }), [ @@ -113,7 +111,7 @@ describe('rrulestr', function () { testRecurring( 'testStrValueOnly', - rrulestr('FREQ=YEARLY;COUNT=3\n', { + rrulestr('FREQ=YEARLY;COUNT=3', { dtstart: parse('19970902T090000'), }), [ @@ -125,7 +123,7 @@ describe('rrulestr', function () { testRecurring( 'testStrUnfold', - rrulestr('FREQ=YEA\n RLY;COUNT=3\n', { + rrulestr(['FREQ=YEA', ' RLY;COUNT=3'].join('\n'), { unfold: true, dtstart: parse('19970902T090000'), }), @@ -139,9 +137,11 @@ describe('rrulestr', function () { testRecurring( 'testStrSet', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n' + - 'RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n', + [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU', + 'RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH', + ].join('\n'), ), [ datetime(1997, 9, 2, 9, 0), @@ -153,10 +153,12 @@ describe('rrulestr', function () { testRecurring( 'testStrSetDate', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU\n' + - 'RDATE:19970904T090000Z\n' + - 'RDATE:19970909T090000Z\n', + [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU', + 'RDATE:19970904T090000Z', + 'RDATE:19970909T090000Z', + ].join('\n'), ), [ datetime(1997, 9, 2, 9, 0), @@ -168,9 +170,11 @@ describe('rrulestr', function () { testRecurring( 'testStrSetExRule', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n' + - 'EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n', + [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH', + 'EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH', + ].join('\n'), ), [ datetime(1997, 9, 2, 9, 0), @@ -182,11 +186,13 @@ describe('rrulestr', function () { testRecurring( 'testStrSetExDate', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n' + - 'EXDATE:19970904T090000Z\n' + - 'EXDATE:19970911T090000Z\n' + - 'EXDATE:19970918T090000Z\n', + [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH', + 'EXDATE:19970904T090000Z', + 'EXDATE:19970911T090000Z', + 'EXDATE:19970918T090000Z', + ].join('\n'), ), [ datetime(1997, 9, 2, 9, 0), @@ -198,16 +204,18 @@ describe('rrulestr', function () { testRecurring( 'testStrSetDateAndExDate', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RDATE:19970902T090000Z\n' + - 'RDATE:19970904T090000Z\n' + - 'RDATE:19970909T090000Z\n' + - 'RDATE:19970911T090000Z\n' + - 'RDATE:19970916T090000Z\n' + - 'RDATE:19970918T090000Z\n' + - 'EXDATE:19970904T090000Z\n' + - 'EXDATE:19970911T090000Z\n' + - 'EXDATE:19970918T090000Z\n', + [ + 'DTSTART:19970902T090000Z', + 'RDATE:19970902T090000Z', + 'RDATE:19970904T090000Z', + 'RDATE:19970909T090000Z', + 'RDATE:19970911T090000Z', + 'RDATE:19970916T090000Z', + 'RDATE:19970918T090000Z', + 'EXDATE:19970904T090000Z', + 'EXDATE:19970911T090000Z', + 'EXDATE:19970918T090000Z', + ].join('\n'), ), [ datetime(1997, 9, 2, 9, 0), @@ -219,14 +227,16 @@ describe('rrulestr', function () { testRecurring( 'testStrSetDateAndExRule', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RDATE:19970902T090000Z\n' + - 'RDATE:19970904T090000Z\n' + - 'RDATE:19970909T090000Z\n' + - 'RDATE:19970911T090000Z\n' + - 'RDATE:19970916T090000Z\n' + - 'RDATE:19970918T090000Z\n' + - 'EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n', + [ + 'DTSTART:19970902T090000Z', + 'RDATE:19970902T090000Z', + 'RDATE:19970904T090000Z', + 'RDATE:19970909T090000Z', + 'RDATE:19970911T090000Z', + 'RDATE:19970916T090000Z', + 'RDATE:19970918T090000Z', + 'EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH', + ].join('\n'), ), [ datetime(1997, 9, 2, 9, 0), @@ -238,10 +248,10 @@ describe('rrulestr', function () { testRecurring( 'testStrKeywords', rrulestr( - 'DTSTART:19970902T030000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;' + - 'BYMONTH=3;byweekday=TH;BYMONTHDAY=3;' + - 'BYHOUR=3;BYMINUTE=3;BYSECOND=3\n', + [ + 'DTSTART:19970902T030000Z', + 'RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;BYMONTH=3;byweekday=TH;BYMONTHDAY=3;BYHOUR=3;BYMINUTE=3;BYSECOND=3', + ].join('\n'), ), [ datetime(2033, 3, 3, 3, 3, 3), @@ -253,8 +263,10 @@ describe('rrulestr', function () { testRecurring( 'testStrNWeekDay', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH\n', + [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH', + ].join('\n'), ), [ datetime(1997, 12, 25, 9, 0), @@ -266,8 +278,10 @@ describe('rrulestr', function () { testRecurring( 'testStrNWeekDayLarge', rrulestr( - 'DTSTART:19970902T090000Z\n' + - 'RRULE:FREQ=YEARLY;COUNT=3;BYDAY=13TU,-13TH\n', + [ + 'DTSTART:19970902T090000Z', + 'RRULE:FREQ=YEARLY;COUNT=3;BYDAY=13TU,-13TH', + ].join('\n'), ), [ datetime(1997, 10, 2, 9, 0), @@ -277,7 +291,9 @@ describe('rrulestr', function () { ) it('parses without TZID', () => { - const rrule = rrulestr('DTSTART:19970902T090000\nRRULE:FREQ=WEEKLY') + const rrule = rrulestr( + ['DTSTART:19970902T090000', 'RRULE:FREQ=WEEKLY'].join('\n'), + ) expect(rrule.origOptions).toMatchObject({ freq: Frequency.WEEKLY, @@ -287,8 +303,10 @@ describe('rrulestr', function () { it('parses TZID', () => { const rrule = rrulestr( - 'DTSTART;TZID=America/New_York:19970902T090000\n' + + [ + 'DTSTART;TZID=America/New_York:19970902T090000', 'RRULE:FREQ=DAILY;UNTIL=19980902T090000', + ].join('\n'), ) expect(rrule.origOptions).toMatchObject({ @@ -340,49 +358,43 @@ describe('rrulestr', function () { }) it('parses an RDATE with no TZID param', () => { - const rruleset = rrulestr( - 'DTSTART:20180719T111500Z\n' + - 'RRULE:FREQ=DAILY;INTERVAL=1\n' + - 'RDATE:20180720T111500Z\n' + - 'EXDATE:20180721T111500Z', - ) as RRuleSet - - expect(rruleset.valueOf()).toEqual([ + const input = [ 'DTSTART:20180719T111500Z', 'RRULE:FREQ=DAILY;INTERVAL=1', 'RDATE:20180720T111500Z', 'EXDATE:20180721T111500Z', - ]) + ] + + const rruleset = rrulestr(input.join('\n')) + expect(rruleset.valueOf()).toEqual(input) }) it('parses an RDATE with a TZID param', () => { - const rruleset = rrulestr( - 'DTSTART;TZID=America/Los_Angeles:20180719T111500\n' + - 'RRULE:FREQ=DAILY;INTERVAL=1\n' + - 'RDATE;TZID=America/Los_Angeles:20180720T111500\n' + - 'EXDATE;TZID=America/Los_Angeles:20180721T111500', - ) as RRuleSet - - expect(rruleset.valueOf()).toEqual([ + const input = [ 'DTSTART;TZID=America/Los_Angeles:20180719T111500', 'RRULE:FREQ=DAILY;INTERVAL=1', 'RDATE;TZID=America/Los_Angeles:20180720T111500', 'EXDATE;TZID=America/Los_Angeles:20180721T111500', - ]) + ] + + const rruleset = rrulestr(input.join('\n')) + expect(rruleset.valueOf()).toEqual(input) }) }) describe('parseInput', () => { it('parses an input into a structure', () => { const output = parseInput( - 'DTSTART;TZID=America/New_York:19970902T090000\n' + - 'RRULE:FREQ=DAILY;UNTIL=19980902T090000;INTERVAL=1\n' + - 'RDATE:19970902T090000Z\n' + - 'RDATE:19970904T090000Z\n' + - 'EXDATE:19970904T090000Z\n' + - 'EXRULE:FREQ=WEEKLY;INTERVAL=2\n', - {}, + [ + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;UNTIL=19980902T090000;INTERVAL=1', + 'RDATE:19970902T090000Z', + 'RDATE:19970904T090000Z', + 'EXDATE:19970904T090000Z', + 'EXRULE:FREQ=WEEKLY;INTERVAL=2', + ].join('\n'), ) + expect(output).toMatchObject({ dtstart: datetime(1997, 9, 2, 9, 0, 0), tzid: 'America/New_York', From b94d2432bf88d2f2044a08e893faa45eae519915 Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Tue, 4 Nov 2025 13:22:14 +0700 Subject: [PATCH 2/6] run tests in band --- package.json | 2 +- src/cache.ts | 5 +- src/date-util.ts | 90 ++++----------- src/datetime.ts | 14 +-- src/helpers.ts | 107 +++--------------- src/iter-info/index.ts | 6 +- src/iter-info/year-info.ts | 8 +- src/iter-result.ts | 2 +- src/iter/build-pos-list.ts | 6 +- src/iter/index.ts | 44 +++---- src/iter/optimize-options.ts | 28 +++-- src/nlp/index.ts | 2 +- src/nlp/to-text.ts | 12 +- src/options-to-string.ts | 10 +- src/parse-options.ts | 105 ++++++++--------- src/rrule.ts | 16 ++- src/rruleset.ts | 5 +- src/rrulestr.ts | 8 +- src/types.ts | 2 +- src/weekday.ts | 4 + test/helpers.test.ts | 77 ++----------- test/lib/utils.ts | 6 +- test/nlp.test.ts | 95 +++++++--------- test/options-to-string.test.ts | 40 +++++++ test/optionstostring.test.ts | 44 ------- ...eoptions.test.ts => parse-options.test.ts} | 0 ...rsestring.test.ts => parse-string.test.ts} | 0 27 files changed, 269 insertions(+), 469 deletions(-) create mode 100644 test/options-to-string.test.ts delete mode 100644 test/optionstostring.test.ts rename test/{parseoptions.test.ts => parse-options.test.ts} (100%) rename test/{parsestring.test.ts => parse-string.test.ts} (100%) diff --git a/package.json b/package.json index 124fb714..98410a34 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "tsc -p tsconfig.build.json", - "test": "nyc jest **/*.test.ts" + "test": "nyc jest **/*.test.ts --runInBand" }, "dependencies": { "luxon": "^3.7.2" diff --git a/src/cache.ts b/src/cache.ts index 90e8172a..fb74c6be 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,5 +1,4 @@ import { clone, cloneDates } from './date-util' -import { isArray } from './helpers' import IterResult, { IterArgs } from './iter-result' export type CacheKeys = 'before' | 'after' | 'between' @@ -75,7 +74,7 @@ export class Cache { const cachedObject = this[what] if (what === 'all') { cached = this.all as Date[] - } else if (isArray(cachedObject)) { + } else if (Array.isArray(cachedObject)) { // Let's see whether we've already called the // 'what' method with the same 'args' for (let i = 0; i < cachedObject.length; i++) { @@ -97,7 +96,7 @@ export class Cache { this._cacheAdd(what, cached, args) } - return isArray(cached) + return Array.isArray(cached) ? cloneDates(cached) : cached instanceof Date ? clone(cached) diff --git a/src/date-util.ts b/src/date-util.ts index b3b258ce..8be5a369 100644 --- a/src/date-util.ts +++ b/src/date-util.ts @@ -1,16 +1,8 @@ -import { padStart } from './helpers' import { Time } from './datetime' type Datelike = Pick -export const datetime = function ( - y: number, - m: number, - d: number, - h = 0, - i = 0, - s = 0, -) { +export function datetime(y: number, m: number, d: number, h = 0, i = 0, s = 0) { return new Date(Date.UTC(y, m - 1, d, h, i, s)) } @@ -26,11 +18,6 @@ export const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] */ export const ONE_DAY = 1000 * 60 * 60 * 24 -/** - * Number of milliseconds of one week - */ -export const ONE_WEEK = ONE_DAY * 7 - /** * @see: */ @@ -49,46 +36,18 @@ export const ORDINAL_BASE = datetime(1970, 1, 1) */ export const PY_WEEKDAYS = [6, 0, 1, 2, 3, 4, 5] -/** - * py_date.timetuple()[7] - */ -export const getYearDay = function (date: Date) { - const dateNoTime = new Date( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - ) - return ( - Math.ceil( - (dateNoTime.valueOf() - new Date(date.getUTCFullYear(), 0, 1).valueOf()) / - ONE_DAY, - ) + 1 - ) -} - -export const isLeapYear = function (year: number) { +export function isLeapYear(year: number) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 } -export const isDate = function (value: unknown): value is Date { - return value instanceof Date -} - -export const isValidDate = function (value: unknown): value is Date { - return isDate(value) && !isNaN(value.getTime()) -} - -/** - * @return {Number} the date's timezone offset in ms - */ -export const tzOffset = function (date: Date) { - return date.getTimezoneOffset() * 60 * 1000 +export function isValidDate(value: unknown): value is Date { + return value instanceof Date && !isNaN(value.getTime()) } /** * @see: */ -export const daysBetween = function (date1: Date, date2: Date) { +export function daysBetween(date1: Date, date2: Date) { // The number of milliseconds in one day // Convert both dates to milliseconds const date1ms = date1.getTime() @@ -104,18 +63,18 @@ export const daysBetween = function (date1: Date, date2: Date) { /** * @see: */ -export const toOrdinal = function (date: Date) { +export function toOrdinal(date: Date) { return daysBetween(date, ORDINAL_BASE) } /** * @see - */ -export const fromOrdinal = function (ordinal: number) { +export function fromOrdinal(ordinal: number) { return new Date(ORDINAL_BASE.getTime() + ordinal * ONE_DAY) } -export const getMonthDays = function (date: Date) { +export function getMonthDays(date: Date) { const month = date.getUTCMonth() return month === 1 && isLeapYear(date.getUTCFullYear()) ? 29 @@ -125,14 +84,14 @@ export const getMonthDays = function (date: Date) { /** * @return {Number} python-like weekday */ -export const getWeekday = function (date: Date) { +export function getWeekday(date: Date) { return PY_WEEKDAYS[date.getUTCDay()] } /** * @see: */ -export const monthRange = function (year: number, month: number) { +export function monthRange(year: number, month: number) { const date = datetime(year, month + 1, 1) return [getWeekday(date), getMonthDays(date)] } @@ -140,7 +99,7 @@ export const monthRange = function (year: number, month: number) { /** * @see: */ -export const combine = function (date: Date, time: Date | Time) { +export function combine(date: Date, time: Date | Time) { time = time || date return new Date( Date.UTC( @@ -155,12 +114,11 @@ export const combine = function (date: Date, time: Date | Time) { ) } -export const clone = function (date: Date | Time) { - const dolly = new Date(date.getTime()) - return dolly +export function clone(date: Date | Time) { + return new Date(date.getTime()) } -export const cloneDates = function (dates: Date[] | Time[]) { +export function cloneDates(dates: Date[] | Time[]) { const clones = [] for (let i = 0; i < dates.length; i++) { clones.push(clone(dates[i])) @@ -171,27 +129,27 @@ export const cloneDates = function (dates: Date[] | Time[]) { /** * Sorts an array of Date or Time objects */ -export const sort = function (dates: T[]) { +export function sort(dates: T[]) { dates.sort(function (a, b) { return a.getTime() - b.getTime() }) } -export const timeToUntilString = function (time: number, utc = true) { +export function timeToUntilString(time: number, utc = true) { const date = new Date(time) return [ - padStart(date.getUTCFullYear().toString(), 4, '0'), - padStart(date.getUTCMonth() + 1, 2, '0'), - padStart(date.getUTCDate(), 2, '0'), + `${date.getUTCFullYear()}`.padStart(4, '0'), + `${date.getUTCMonth() + 1}`.padStart(2, '0'), + `${date.getUTCDate()}`.padStart(2, '0'), 'T', - padStart(date.getUTCHours(), 2, '0'), - padStart(date.getUTCMinutes(), 2, '0'), - padStart(date.getUTCSeconds(), 2, '0'), + `${date.getUTCHours()}`.padStart(2, '0'), + `${date.getUTCMinutes()}`.padStart(2, '0'), + `${date.getUTCSeconds()}`.padStart(2, '0'), utc ? 'Z' : '', ].join('') } -export const untilStringToDate = function (until: string) { +export function untilStringToDate(until: string) { const re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/ const bits = re.exec(until) @@ -216,7 +174,7 @@ const dateTZtoISO8601 = function (date: Date, timeZone: string) { return dateStr.replace(' ', 'T') + 'Z' } -export const dateInTimeZone = function (date: Date, timeZone: string) { +export function dateInTimeZone(date: Date, timeZone: string) { const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone // Date constructor can only reliably parse dates in ISO8601 format const dateInLocalTZ = new Date(dateTZtoISO8601(date, localTimeZone)) diff --git a/src/datetime.ts b/src/datetime.ts index 5682bf1c..8922cc20 100644 --- a/src/datetime.ts +++ b/src/datetime.ts @@ -1,5 +1,5 @@ import { getWeekday, MAXYEAR, monthRange } from './date-util' -import { divmod, empty, includes, pymod } from './helpers' +import { divmod, empty, pymod } from './helpers' import { Frequency, ParsedOptions } from './types' export class Time { @@ -153,7 +153,7 @@ export class DateTime extends Time { this.addDaily(dayDiv) } - if (empty(byhour) || includes(byhour, this.hour)) break + if (empty(byhour) || byhour.includes(this.hour)) break } } @@ -178,8 +178,8 @@ export class DateTime extends Time { } if ( - (empty(byhour) || includes(byhour, this.hour)) && - (empty(byminute) || includes(byminute, this.minute)) + (empty(byhour) || byhour.includes(this.hour)) && + (empty(byminute) || byminute.includes(this.minute)) ) { break } @@ -211,9 +211,9 @@ export class DateTime extends Time { } if ( - (empty(byhour) || includes(byhour, this.hour)) && - (empty(byminute) || includes(byminute, this.minute)) && - (empty(bysecond) || includes(bysecond, this.second)) + (empty(byhour) || byhour.includes(this.hour)) && + (empty(byminute) || byminute.includes(this.minute)) && + (empty(bysecond) || bysecond.includes(this.second)) ) { break } diff --git a/src/helpers.ts b/src/helpers.ts index 14bd02bf..3db38065 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -2,88 +2,35 @@ // Helper functions // ============================================================================= -import { ALL_WEEKDAYS, WeekdayStr } from './weekday' - -export const isPresent = function ( - value?: T | null | undefined, -): value is T { +export function isDefined(value?: T | null | undefined): value is T { return value !== null && value !== undefined } -export const isNumber = function (value: unknown): value is number { +export function isNumber(value: unknown): value is number { return typeof value === 'number' } -export const isWeekdayStr = function (value: unknown): value is WeekdayStr { - return typeof value === 'string' && ALL_WEEKDAYS.includes(value as WeekdayStr) -} - -export const isArray = Array.isArray - -/** - * Simplified version of python's range() - */ -export const range = function (start: number, end: number = start): number[] { - if (arguments.length === 1) { +export function range(start: number, end?: number) { + if (end === undefined) { end = start start = 0 } - const rang = [] - for (let i = start; i < end; i++) rang.push(i) - return rang -} -export const clone = function (array: T[]): T[] { - return ([] as T[]).concat(array) + return Array.from({ length: end - start }).map((_, i) => i + start) } -export const repeat = function (value: T | T[], times: number): (T | T[])[] { - let i = 0 - const array: (T | T[])[] = [] - - if (isArray(value)) { - for (; i < times; i++) array[i] = ([] as T[]).concat(value) - } else { - for (; i < times; i++) array[i] = value - } - return array +export function repeat(value: T | T[], length: number) { + return Array.isArray(value) + ? Array.from({ length }, () => [...value]) + : Array.from({ length }, () => value) } -export const toArray = function (item: T | T[]): T[] { - if (isArray(item)) { - return item - } - - return [item] -} - -export function padStart( - item: string | number, - targetLength: number, - padString = ' ', -) { - const str = String(item) - targetLength = targetLength >> 0 - if (str.length > targetLength) { - return String(str) - } - - targetLength = targetLength - str.length - if (targetLength > padString.length) { - padString += repeat(padString, targetLength / padString.length) - } - - return padString.slice(0, targetLength) + String(str) +export function toArray(item: T | T[]) { + return Array.isArray(item) ? item : [item] } -/** - * Python like split - */ -export const split = function (str: string, sep: string, num: number) { - const splits = str.split(sep) - return num - ? splits.slice(0, num).concat([splits.slice(num).join(sep)]) - : splits +export function empty(obj: T | T[] | null | undefined) { + return !isDefined(obj) || (Array.isArray(obj) && obj.length === 0) } /** @@ -101,7 +48,7 @@ export const split = function (str: string, sep: string, num: number) { * @return {number} a % b where the result is between 0 and b (either 0 <= x < b * or b < x <= 0, depending on the sign of b). */ -export const pymod = function (a: number, b: number) { +export function pymod(a: number, b: number) { const r = a % b // If r and b differ in sign, add b to wrap the result to the correct sign. return r * b < 0 ? r + b : r @@ -110,30 +57,6 @@ export const pymod = function (a: number, b: number) { /** * @see: */ -export const divmod = function (a: number, b: number) { +export function divmod(a: number, b: number) { return { div: Math.floor(a / b), mod: pymod(a, b) } } - -export const empty = function (obj: T | T[] | null | undefined) { - return !isPresent(obj) || (Array.isArray(obj) && obj.length === 0) -} - -/** - * Python-like boolean - * - * @return {Boolean} value of an object/primitive, taking into account - * the fact that in Python an empty list's/tuple's - * boolean value is False, whereas in JS it's true - */ -export const notEmpty = function ( - obj: T | T[] | null | undefined, -): obj is T[] | T { - return !empty(obj) -} - -/** - * Return true if a value is in an array - */ -export const includes = function (arr: T[] | null | undefined, val: T) { - return notEmpty(arr) && arr.indexOf(val) !== -1 -} diff --git a/src/iter-info/index.ts b/src/iter-info/index.ts index 0acf36ab..3eac58cf 100644 --- a/src/iter-info/index.ts +++ b/src/iter-info/index.ts @@ -1,6 +1,6 @@ import { datetime, sort, toOrdinal } from '../date-util' import { Time } from '../datetime' -import { isPresent, notEmpty, range, repeat } from '../helpers' +import { empty, isDefined, range, repeat } from '../helpers' import { Frequency, ParsedOptions } from '../types' import { easter } from './easter' import { MonthInfo, rebuildMonth } from './month-info' @@ -29,7 +29,7 @@ export default class Iterinfo { } if ( - notEmpty(options.bynweekday) && + !empty(options.bynweekday) && (month !== this.lastmonth || year !== this.lastyear) ) { const { yearlen, mrange, wdaymask } = this.yearinfo @@ -43,7 +43,7 @@ export default class Iterinfo { ) } - if (isPresent(options.byeaster)) { + if (isDefined(options.byeaster)) { this.eastermask = easter(year, options.byeaster) } } diff --git a/src/iter-info/year-info.ts b/src/iter-info/year-info.ts index daaae8a5..58f43871 100644 --- a/src/iter-info/year-info.ts +++ b/src/iter-info/year-info.ts @@ -1,5 +1,5 @@ import { datetime, getWeekday, isLeapYear, toOrdinal } from '../date-util' -import { empty, includes, pymod, repeat } from '../helpers' +import { empty, pymod, repeat } from '../helpers' import { M365MASK, M365RANGE, @@ -93,7 +93,7 @@ export function rebuildYear(year: number, options: ParsedOptions) { } } - if (includes(options.byweekno, 1)) { + if (options.byweekno.includes(1)) { // Check week number 1 of next year as well // orig-TODO : Check -numweeks for next year. let i = no1wkst + numweeks * 7 @@ -117,7 +117,7 @@ export function rebuildYear(year: number, options: ParsedOptions) { // days from last year's last week number in // this year. let lnumweeks: number - if (!includes(options.byweekno, -1)) { + if (!options.byweekno.includes(-1)) { const lyearweekday = getWeekday(datetime(year - 1, 1, 1)) let lno1wkst = pymod(7 - lyearweekday.valueOf() + options.wkst, 7) @@ -136,7 +136,7 @@ export function rebuildYear(year: number, options: ParsedOptions) { lnumweeks = -1 } - if (includes(options.byweekno, lnumweeks)) { + if (options.byweekno.includes(lnumweeks)) { for (let i = 0; i < no1wkst; i++) result.wnomask[i] = 1 } } diff --git a/src/iter-result.ts b/src/iter-result.ts index 4d896ca1..c63e6d5c 100644 --- a/src/iter-result.ts +++ b/src/iter-result.ts @@ -4,7 +4,7 @@ import { IterResultType, QueryMethodTypes } from './types' // Results // ============================================================================= -export interface IterArgs { +export type IterArgs = { inc: boolean before: Date after: Date diff --git a/src/iter/build-pos-list.ts b/src/iter/build-pos-list.ts index 94dda2c3..4526acd8 100644 --- a/src/iter/build-pos-list.ts +++ b/src/iter/build-pos-list.ts @@ -1,6 +1,6 @@ import { combine, fromOrdinal, sort } from '../date-util' import { Time } from '../datetime' -import { includes, isPresent, pymod } from '../helpers' +import { isDefined, pymod } from '../helpers' import Iterinfo from '../iter-info/index' export function buildPosList( @@ -29,7 +29,7 @@ export function buildPosList( const tmp = [] for (let k = start; k < end; k++) { const val = dayset[k] - if (!isPresent(val)) continue + if (!isDefined(val)) continue tmp.push(val) } let i: number @@ -44,7 +44,7 @@ export function buildPosList( const res = combine(date, time) // XXX: can this ever be in the array? // - compare the actual date instead? - if (!includes(poslist, res)) poslist.push(res) + if (!poslist.includes(res)) poslist.push(res) } sort(poslist) diff --git a/src/iter/index.ts b/src/iter/index.ts index f1a10962..bc2b6b02 100644 --- a/src/iter/index.ts +++ b/src/iter/index.ts @@ -1,7 +1,7 @@ import { combine, fromOrdinal, MAXYEAR } from '../date-util' import { DateWithZone } from '../date-with-zone' import { DateTime, Time } from '../datetime' -import { includes, isPresent, notEmpty } from '../helpers' +import { empty, isDefined } from '../helpers' import Iterinfo from '../iter-info/index' import IterResult from '../iter-result' import { buildTimeset } from '../parse-options' @@ -53,7 +53,7 @@ export function iter( const filtered = removeFilteredDays(dayset, start, end, ii, parsedOptions) - if (notEmpty(bysetpos)) { + if (!empty(bysetpos)) { const poslist = buildPosList(bysetpos, timeset, start, end, ii, dayset) for (let j = 0; j < poslist.length; j++) { @@ -79,7 +79,7 @@ export function iter( } else { for (let j = start; j < end; j++) { const currentDay = dayset[j] - if (!isPresent(currentDay)) { + if (!isDefined(currentDay)) { continue } @@ -147,21 +147,21 @@ function isFiltered( } = options return ( - (notEmpty(bymonth) && !includes(bymonth, ii.mmask[currentDay])) || - (notEmpty(byweekno) && !ii.wnomask[currentDay]) || - (notEmpty(byweekday) && !includes(byweekday, ii.wdaymask[currentDay])) || - (notEmpty(ii.nwdaymask) && !ii.nwdaymask[currentDay]) || - (byeaster !== null && !includes(ii.eastermask, currentDay)) || - ((notEmpty(bymonthday) || notEmpty(bynmonthday)) && - !includes(bymonthday, ii.mdaymask[currentDay]) && - !includes(bynmonthday, ii.nmdaymask[currentDay])) || - (notEmpty(byyearday) && + (!empty(bymonth) && !bymonth.includes(ii.mmask[currentDay])) || + (!empty(byweekno) && !ii.wnomask[currentDay]) || + (!empty(byweekday) && !byweekday.includes(ii.wdaymask[currentDay])) || + (!empty(ii.nwdaymask) && !ii.nwdaymask[currentDay]) || + (byeaster !== null && !ii.eastermask.includes(currentDay)) || + ((!empty(bymonthday) || !empty(bynmonthday)) && + !bymonthday.includes(ii.mdaymask[currentDay]) && + !bynmonthday.includes(ii.nmdaymask[currentDay])) || + (!empty(byyearday) && ((currentDay < ii.yearlen && - !includes(byyearday, currentDay + 1) && - !includes(byyearday, -ii.yearlen + currentDay)) || + !byyearday.includes(currentDay + 1) && + !byyearday.includes(-ii.yearlen + currentDay)) || (currentDay >= ii.yearlen && - !includes(byyearday, currentDay + 1 - ii.yearlen) && - !includes(byyearday, -ii.nextyearlen + currentDay - ii.yearlen)))) + !byyearday.includes(currentDay + 1 - ii.yearlen) && + !byyearday.includes(-ii.nextyearlen + currentDay - ii.yearlen)))) ) } @@ -205,14 +205,14 @@ function makeTimeset( if ( (freq >= RRule.HOURLY && - notEmpty(byhour) && - !includes(byhour, counterDate.hour)) || + !empty(byhour) && + !byhour.includes(counterDate.hour)) || (freq >= RRule.MINUTELY && - notEmpty(byminute) && - !includes(byminute, counterDate.minute)) || + !empty(byminute) && + !byminute.includes(counterDate.minute)) || (freq >= RRule.SECONDLY && - notEmpty(bysecond) && - !includes(bysecond, counterDate.second)) + !empty(bysecond) && + !bysecond.includes(counterDate.second)) ) { return [] } diff --git a/src/iter/optimize-options.ts b/src/iter/optimize-options.ts index be014029..a5cb79fd 100644 --- a/src/iter/optimize-options.ts +++ b/src/iter/optimize-options.ts @@ -1,6 +1,6 @@ import { DateTime } from 'luxon' -import { notEmpty } from '../helpers' +import { empty } from '../helpers' import IterResult from '../iter-result' import { Frequency, Options, ParsedOptions, QueryMethodTypes } from '../types' import { Weekday } from '../weekday' @@ -97,20 +97,18 @@ export function optimizeOptions( (!minDate && !maxDate) || (minDate && minDate <= dtstart) || (maxDate && maxDate <= dtstart) || - notEmpty(bymonth) || - notEmpty(bysetpos) || - notEmpty(bymonthday) || - notEmpty(byyearday) || - notEmpty(byweekno) || - notEmpty(byhour) || - notEmpty(byminute) || - notEmpty(bysecond) || - notEmpty(bysecond) || - notEmpty(byeaster) || - (notEmpty(byweekday) && - (Array.isArray(byweekday) ? byweekday : [byweekday]).some( - (byweekdayInstance) => byweekdayInstance instanceof Weekday, - )) + !empty(bymonth) || + !empty(bysetpos) || + !empty(bymonthday) || + !empty(byyearday) || + !empty(byweekno) || + !empty(byhour) || + !empty(byminute) || + !empty(bysecond) || + !empty(bysecond) || + !empty(byeaster) || + (!empty(byweekday) && + [].concat(byweekday).some((w) => w instanceof Weekday)) ) { return parsedOptions } diff --git a/src/nlp/index.ts b/src/nlp/index.ts index efb4bccd..162d5b98 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -131,7 +131,7 @@ const toText = function ( const { isFullyConvertible } = ToText -export interface Nlp { +export type Nlp = { fromText: typeof fromText parseText: typeof parseText isFullyConvertible: typeof isFullyConvertible diff --git a/src/nlp/to-text.ts b/src/nlp/to-text.ts index a4f4266a..b49a0452 100644 --- a/src/nlp/to-text.ts +++ b/src/nlp/to-text.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from './i18n' +import { isDefined, isNumber } from '../helpers' import { RRule } from '../rrule' -import { Options, ByWeekday } from '../types' +import { ByWeekday, Options } from '../types' import { Weekday } from '../weekday' -import { isArray, isNumber, isPresent } from '../helpers' +import ENGLISH, { Language } from './i18n' // ============================================================================= // Helper functions @@ -81,8 +81,8 @@ export default class ToText { if (!this.bymonthday.length) this.bymonthday = null } - if (isPresent(this.origOptions.byweekday)) { - const byweekday = !isArray(this.origOptions.byweekday) + if (isDefined(this.origOptions.byweekday)) { + const byweekday = !Array.isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] : this.origOptions.byweekday const days = String(byweekday) @@ -466,7 +466,7 @@ export default class ToText { finalDelim?: string, delim = ',', ) { - if (!isArray(arr)) { + if (!Array.isArray(arr)) { arr = [arr] } const delimJoin = function ( diff --git a/src/options-to-string.ts b/src/options-to-string.ts index 146cf30a..083aaf77 100644 --- a/src/options-to-string.ts +++ b/src/options-to-string.ts @@ -1,6 +1,6 @@ import { timeToUntilString } from './date-util' import { DateWithZone } from './date-with-zone' -import { includes, isArray, isNumber, isPresent, toArray } from './helpers' +import { isDefined, isNumber, toArray } from './helpers' import { DEFAULT_OPTIONS, RRule } from './rrule' import { Options } from './types' import { Weekday } from './weekday' @@ -13,13 +13,13 @@ export function optionsToString(options: Partial) { for (let i = 0; i < keys.length; i++) { if (keys[i] === 'tzid') continue - if (!includes(defaultKeys, keys[i])) continue + if (!defaultKeys.includes(keys[i])) continue let key = keys[i].toUpperCase() const value = options[keys[i]] let outValue = '' - if (!isPresent(value) || (isArray(value) && !value.length)) continue + if (!isDefined(value) || (Array.isArray(value) && !value.length)) continue switch (key) { case 'FREQ': @@ -53,7 +53,7 @@ export function optionsToString(options: Partial) { return wday } - if (isArray(wday)) { + if (Array.isArray(wday)) { return new Weekday(wday[0], wday[1]) } @@ -71,7 +71,7 @@ export function optionsToString(options: Partial) { break default: - if (isArray(value)) { + if (Array.isArray(value)) { const strValues: string[] = [] for (let j = 0; j < value.length; j++) { strValues[j] = String(value[j]) diff --git a/src/parse-options.ts b/src/parse-options.ts index ac9a8bc1..fe612737 100644 --- a/src/parse-options.ts +++ b/src/parse-options.ts @@ -1,28 +1,18 @@ -import { getWeekday, isDate, isValidDate } from './date-util' +import { getWeekday, isValidDate } from './date-util' import { Time } from './datetime' -import { - includes, - isArray, - isNumber, - isPresent, - isWeekdayStr, - notEmpty, -} from './helpers' +import { empty, isDefined, isNumber } from './helpers' import { DEFAULT_OPTIONS, RRule, defaultKeys } from './rrule' import { Options, ParsedOptions, freqIsDailyOrGreater } from './types' -import { Weekday } from './weekday' +import { Weekday, isWeekdayStr } from './weekday' export function initializeOptions(options: Partial) { - const invalid: string[] = [] - const keys = Object.keys(options) as (keyof Options)[] - - // Shallow copy for options and origOptions and check for invalid - for (const key of keys) { - if (!includes(defaultKeys, key)) invalid.push(key) - if (isDate(options[key]) && !isValidDate(options[key])) { - invalid.push(key) - } - } + const invalid = Object.entries(options) + .flatMap(([key, value]) => + !(defaultKeys as string[]).includes(key) + || (value instanceof Date && !isValidDate(value)) + ? [key] + : [] + ) if (invalid.length) { throw new Error('Invalid options: ' + invalid.join(', ')) @@ -34,15 +24,19 @@ export function initializeOptions(options: Partial) { export function parseOptions(options: Partial) { const opts = { ...DEFAULT_OPTIONS, ...initializeOptions(options) } - if (isPresent(opts.byeaster)) opts.freq = RRule.YEARLY + if (isDefined(opts.byeaster)) { + opts.freq = RRule.YEARLY + } - if (!(isPresent(opts.freq) && RRule.FREQUENCIES[opts.freq])) { + if (!(isDefined(opts.freq) && RRule.FREQUENCIES[opts.freq])) { throw new Error(`Invalid frequency: ${opts.freq} ${options.freq}`) } - if (!opts.dtstart) opts.dtstart = new Date(new Date().setMilliseconds(0)) + if (!opts.dtstart) { + opts.dtstart = new Date(new Date().setMilliseconds(0)) + } - if (!isPresent(opts.wkst)) { + if (!isDefined(opts.wkst)) { opts.wkst = RRule.MO.weekday } else if (isNumber(opts.wkst)) { // cool, just keep it like that @@ -50,38 +44,39 @@ export function parseOptions(options: Partial) { opts.wkst = opts.wkst.weekday } - if (isPresent(opts.bysetpos)) { - if (isNumber(opts.bysetpos)) opts.bysetpos = [opts.bysetpos] + if (isDefined(opts.bysetpos)) { + if (isNumber(opts.bysetpos)) { + opts.bysetpos = [opts.bysetpos] + } - for (let i = 0; i < opts.bysetpos.length; i++) { - const v = opts.bysetpos[i] - if (v === 0 || !(v >= -366 && v <= 366)) { - throw new Error( - 'bysetpos must be between 1 and 366,' + ' or between -366 and -1', - ) + opts.bysetpos.forEach(v => { + if (v === 0 || Math.abs(v) >= 366) { + throw new Error('bysetpos must be between 1 and 366,' + ' or between -366 and -1') } - } + }) } if ( !( - Boolean(opts.byweekno as number) || - notEmpty(opts.byweekno as number[]) || - notEmpty(opts.byyearday as number[]) || + isDefined(opts.byweekday) || + isDefined(opts.byeaster) || + Boolean(opts.byweekno) || Boolean(opts.bymonthday) || - notEmpty(opts.bymonthday as number[]) || - isPresent(opts.byweekday) || - isPresent(opts.byeaster) + !empty(opts.byweekno) || + !empty(opts.byyearday) || + !empty(opts.bymonthday) ) ) { switch (opts.freq) { case RRule.YEARLY: - if (!opts.bymonth) opts.bymonth = opts.dtstart.getUTCMonth() + 1 + opts.bymonth ??= opts.dtstart.getUTCMonth() + 1 opts.bymonthday = opts.dtstart.getUTCDate() break + case RRule.MONTHLY: opts.bymonthday = opts.dtstart.getUTCDate() break + case RRule.WEEKLY: opts.byweekday = [getWeekday(opts.dtstart)] break @@ -89,24 +84,20 @@ export function parseOptions(options: Partial) { } // bymonth - if (isPresent(opts.bymonth) && !isArray(opts.bymonth)) { - opts.bymonth = [opts.bymonth] + if (isDefined(opts.bymonth)) { + opts.bymonth = [].concat(opts.bymonth) } // byyearday - if ( - isPresent(opts.byyearday) && - !isArray(opts.byyearday) && - isNumber(opts.byyearday) - ) { - opts.byyearday = [opts.byyearday] + if (isDefined(opts.byyearday)) { + opts.byyearday = [].concat(opts.byyearday) } // bymonthday - if (!isPresent(opts.bymonthday)) { + if (!isDefined(opts.bymonthday)) { opts.bymonthday = [] opts.bynmonthday = [] - } else if (isArray(opts.bymonthday)) { + } else if (Array.isArray(opts.bymonthday)) { const bymonthday = [] const bynmonthday = [] @@ -129,12 +120,12 @@ export function parseOptions(options: Partial) { } // byweekno - if (isPresent(opts.byweekno) && !isArray(opts.byweekno)) { + if (isDefined(opts.byweekno) && !Array.isArray(opts.byweekno)) { opts.byweekno = [opts.byweekno] } // byweekday / bynweekday - if (!isPresent(opts.byweekday)) { + if (!isDefined(opts.byweekday)) { opts.bynweekday = null } else if (isNumber(opts.byweekday)) { opts.byweekday = [opts.byweekday] @@ -171,19 +162,19 @@ export function parseOptions(options: Partial) { bynweekday.push([wday.weekday, wday.n]) } } - opts.byweekday = notEmpty(byweekday) ? byweekday : null - opts.bynweekday = notEmpty(bynweekday) ? bynweekday : null + opts.byweekday = !empty(byweekday) ? byweekday : null + opts.bynweekday = !empty(bynweekday) ? bynweekday : null } // byhour - if (!isPresent(opts.byhour)) { + if (!isDefined(opts.byhour)) { opts.byhour = opts.freq < RRule.HOURLY ? [opts.dtstart.getUTCHours()] : null } else if (isNumber(opts.byhour)) { opts.byhour = [opts.byhour] } // byminute - if (!isPresent(opts.byminute)) { + if (!isDefined(opts.byminute)) { opts.byminute = opts.freq < RRule.MINUTELY ? [opts.dtstart.getUTCMinutes()] : null } else if (isNumber(opts.byminute)) { @@ -191,7 +182,7 @@ export function parseOptions(options: Partial) { } // bysecond - if (!isPresent(opts.bysecond)) { + if (!isDefined(opts.bysecond)) { opts.bysecond = opts.freq < RRule.SECONDLY ? [opts.dtstart.getUTCSeconds()] : null } else if (isNumber(opts.bysecond)) { diff --git a/src/rrule.ts b/src/rrule.ts index 226665f4..f7553044 100644 --- a/src/rrule.ts +++ b/src/rrule.ts @@ -268,12 +268,16 @@ export class RRule implements QueryMethods { * Will convert all rules described in nlp:ToText * to text. */ - toText( - gettext?: GetText, - language?: Language, - dateFormatter?: DateFormatter, - ) { - return toText(this, gettext, language, dateFormatter) + toText({ + getText, + language, + dateFormatter, + }: { + getText?: GetText + language?: Language + dateFormatter?: DateFormatter + } = {}) { + return toText(this, getText, language, dateFormatter) } isFullyConvertibleToText() { diff --git a/src/rruleset.ts b/src/rruleset.ts index c7b6ea20..d7148df3 100644 --- a/src/rruleset.ts +++ b/src/rruleset.ts @@ -1,5 +1,4 @@ import { sort, timeToUntilString } from './date-util' -import { includes } from './helpers' import IterResult from './iter-result' import { iterSet } from './iter-set' import { optionsToString } from './options-to-string' @@ -201,7 +200,7 @@ function _addRule(rrule: RRule, collection: RRule[]) { throw new TypeError(String(rrule) + ' is not RRule instance') } - if (!includes(collection.map(String), String(rrule))) { + if (!collection.map(String).includes(String(rrule))) { collection.push(rrule) } } @@ -210,7 +209,7 @@ function _addDate(date: Date, collection: Date[]) { if (!(date instanceof Date)) { throw new TypeError(String(date) + ' is not Date instance') } - if (!includes(collection.map(Number), Number(date))) { + if (!collection.map(Number).includes(Number(date))) { collection.push(date) sort(collection) } diff --git a/src/rrulestr.ts b/src/rrulestr.ts index 24fe3c89..d17c86d2 100644 --- a/src/rrulestr.ts +++ b/src/rrulestr.ts @@ -1,11 +1,10 @@ import { untilStringToDate } from './date-util' -import { includes, split } from './helpers' import { parseDtstart, parseString } from './parse-string' import { RRule } from './rrule' import { RRuleSet } from './rruleset' import { Options } from './types' -export interface RRuleStrOptions { +export type RRuleStrOptions = { dtstart: Date | null cache: boolean unfold: boolean @@ -173,7 +172,7 @@ function initializeOptions(options: Partial) { ) as (keyof typeof DEFAULT_OPTIONS)[] keys.forEach(function (key) { - if (!includes(defaultKeys, key)) invalid.push(key) + if (!defaultKeys.includes(key)) invalid.push(key) }) if (invalid.length) { @@ -191,7 +190,8 @@ function extractName(line: string) { } } - const [name, value] = split(line, ':', 1) + const [name, value] = line.split(':', 2) + return { name, value, diff --git a/src/types.ts b/src/types.ts index 6b85f9ef..ca58bb4e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,7 +34,7 @@ export function freqIsDailyOrGreater( return freq < Frequency.HOURLY } -export interface Options { +export type Options = { freq: Frequency dtstart?: Date interval: number diff --git a/src/weekday.ts b/src/weekday.ts index 55799822..bccef540 100644 --- a/src/weekday.ts +++ b/src/weekday.ts @@ -6,6 +6,10 @@ export const ALL_WEEKDAYS = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] as const export type WeekdayStr = (typeof ALL_WEEKDAYS)[number] +export function isWeekdayStr(value: unknown): value is WeekdayStr { + return typeof value === 'string' && ALL_WEEKDAYS.includes(value as WeekdayStr) +} + export class Weekday { public weekday: number public n?: number diff --git a/test/helpers.test.ts b/test/helpers.test.ts index 2030ee86..caa58ebc 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -1,48 +1,21 @@ -import { - clone, - divmod, - empty, - includes, - isArray, - isPresent, - pymod, - range, - repeat, - split, -} from '../src/helpers' +import { divmod, empty, isDefined, pymod, range, repeat } from '../src/helpers' import { isNumber } from './lib/utils' -describe('isPresent', () => { +describe('isDefined', () => { it('is false if object is null', () => { - expect(isPresent(null)).toBe(false) + expect(isDefined(null)).toBe(false) }) it('is false if object is undefined', () => { - expect(isPresent(undefined)).toBe(false) + expect(isDefined(undefined)).toBe(false) }) - it('is true if object is non-null and not undefined', () => { - expect(isPresent(0)).toBe(true) - expect(isPresent('')).toBe(true) - expect(isPresent('foo')).toBe(true) - expect(isPresent(123)).toBe(true) - expect(isPresent([])).toBe(true) - }) -}) - -describe('isArray', () => { - it('is true if it is an array', () => { - expect(isArray([])).toBe(true) - expect(isArray([1])).toBe(true) - expect(isArray(['foo'])).toBe(true) - }) - - it('is false if it is empty', () => { - expect(isArray('foo')).toBe(false) - expect(isArray(null)).toBe(false) - expect(isArray(0)).toBe(false) - expect(isArray(undefined)).toBe(false) - }) + it.each([0, '', 'foo', 123, []])( + 'is true if object is non-null and not undefined', + (val) => { + expect(isDefined(val)).toBe(true) + }, + ) }) describe('isNumber', () => { @@ -75,18 +48,6 @@ describe('empty', () => { }) }) -describe('includes', () => { - it('is true if the object is found', () => { - expect(includes(['foo'], 'foo')).toBe(true) - expect(includes([0], 0)).toBe(true) - }) - - it('is false if the object is not found', () => { - expect(includes(['foo'], 'bar')).toBe(false) - expect(includes([0], 1)).toBe(false) - }) -}) - describe('pymod', () => { it('returns the wrapped result', () => { expect(pymod(1, 8)).toBe(1) @@ -103,24 +64,6 @@ describe('divmod', () => { }) }) -describe('split', () => { - it('splits on the separator', () => { - expect(split('one-two-three', '-', 0)).toEqual(['one', 'two', 'three']) - }) - - it('only splits the specified number when nonzero', () => { - expect(split('one-two-three', '-', 1)).toEqual(['one', 'two-three']) - }) -}) - -describe('clone', () => { - it('copies an array', () => { - const a = ['a', 'b', 'c'] - expect(clone(a)).not.toBe(a) - expect(clone(a)).toEqual(a) - }) -}) - describe('range', () => { it('generates a range', () => { expect(range(3, 7)).toEqual([3, 4, 5, 6]) diff --git a/test/lib/utils.ts b/test/lib/utils.ts index b013b57c..7a9b8342 100644 --- a/test/lib/utils.ts +++ b/test/lib/utils.ts @@ -38,7 +38,7 @@ const extractTime = function (date: Date) { /** * dateutil.parser.parse */ -export const parse = function (str: string) { +export function parse(str: string) { const parts = str.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/) const [, y, m, d, h, i, s] = parts const year = Number(y) @@ -50,13 +50,13 @@ export const parse = function (str: string) { return datetime(year, month, day, hour, minute, second) } -interface TestRecurring { +type TestRecurring = { (m: string, testObj: unknown, expectedDates: Date | Date[]): void only: (...args: unknown[]) => void skip: (...args: unknown[]) => void } -interface TestObj { +type TestObj = { rrule: RRule method: 'all' | 'between' | 'before' | 'after' args: unknown[] diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 9385ceb9..1e8cf6a4 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -3,68 +3,56 @@ import { DateFormatter } from '../src/nlp/to-text' import { optionsToString } from '../src/options-to-string' import { RRule } from '../src/rrule' -const texts = [ - ['Every day', 'RRULE:FREQ=DAILY'], - ['Every day at 10, 12 and 17', 'RRULE:FREQ=DAILY;BYHOUR=10,12,17'], +const fromTexts = [ + ['RRULE:FREQ=DAILY', 'Every day'], + ['RRULE:FREQ=DAILY;BYHOUR=10,12,17', 'Every day at 10, 12 and 17'], [ - 'Every week on Sunday at 10, 12 and 17', 'RRULE:FREQ=WEEKLY;BYDAY=SU;BYHOUR=10,12,17', + 'Every week on Sunday at 10, 12 and 17', ], - ['Every week', 'RRULE:FREQ=WEEKLY'], - ['Every hour', 'RRULE:FREQ=HOURLY'], - ['Every 4 hours', 'RRULE:INTERVAL=4;FREQ=HOURLY'], - ['Every week on Tuesday', 'RRULE:FREQ=WEEKLY;BYDAY=TU'], - ['Every week on Monday, Wednesday', 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE'], - ['Every weekday', 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR'], - ['Every 2 weeks', 'RRULE:INTERVAL=2;FREQ=WEEKLY'], - ['Every month', 'RRULE:FREQ=MONTHLY'], - ['Every 6 months', 'RRULE:INTERVAL=6;FREQ=MONTHLY'], - ['Every year', 'RRULE:FREQ=YEARLY'], - ['Every year on the 1st Friday', 'RRULE:FREQ=YEARLY;BYDAY=+1FR'], - ['Every year on the 13th Friday', 'RRULE:FREQ=YEARLY;BYDAY=+13FR'], - ['Every month on the 4th', 'RRULE:FREQ=MONTHLY;BYMONTHDAY=4'], - ['Every month on the 4th last', 'RRULE:FREQ=MONTHLY;BYMONTHDAY=-4'], - ['Every month on the 3rd Tuesday', 'RRULE:FREQ=MONTHLY;BYDAY=+3TU'], - ['Every month on the 3rd last Tuesday', 'RRULE:FREQ=MONTHLY;BYDAY=-3TU'], - ['Every month on the last Monday', 'RRULE:FREQ=MONTHLY;BYDAY=-1MO'], - ['Every month on the 2nd last Friday', 'RRULE:FREQ=MONTHLY;BYDAY=-2FR'], + ['RRULE:FREQ=WEEKLY', 'Every week'], + ['RRULE:FREQ=HOURLY', 'Every hour'], + ['RRULE:INTERVAL=4;FREQ=HOURLY', 'Every 4 hours'], + ['RRULE:FREQ=WEEKLY;BYDAY=TU', 'Every week on Tuesday'], + ['RRULE:FREQ=WEEKLY;BYDAY=MO,WE', 'Every week on Monday, Wednesday'], + ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', 'Every weekday'], + ['RRULE:INTERVAL=2;FREQ=WEEKLY', 'Every 2 weeks'], + ['RRULE:FREQ=MONTHLY', 'Every month'], + ['RRULE:INTERVAL=6;FREQ=MONTHLY', 'Every 6 months'], + ['RRULE:FREQ=YEARLY', 'Every year'], + ['RRULE:FREQ=YEARLY;BYDAY=+1FR', 'Every year on the 1st Friday'], + ['RRULE:FREQ=YEARLY;BYDAY=+13FR', 'Every year on the 13th Friday'], + ['RRULE:FREQ=MONTHLY;BYMONTHDAY=4', 'Every month on the 4th'], + ['RRULE:FREQ=MONTHLY;BYMONTHDAY=-4', 'Every month on the 4th last'], + ['RRULE:FREQ=MONTHLY;BYDAY=+3TU', 'Every month on the 3rd Tuesday'], + ['RRULE:FREQ=MONTHLY;BYDAY=-3TU', 'Every month on the 3rd last Tuesday'], + ['RRULE:FREQ=MONTHLY;BYDAY=-1MO', 'Every month on the last Monday'], + ['RRULE:FREQ=MONTHLY;BYDAY=-2FR', 'Every month on the 2nd last Friday'], // ['Every week until January 1, 2007', 'RRULE:FREQ=WEEKLY;UNTIL=20070101T080000Z'], - ['Every week for 20 times', 'RRULE:FREQ=WEEKLY;COUNT=20'], + ['RRULE:FREQ=WEEKLY;COUNT=20', 'Every week for 20 times'], ] const toTexts = [ - ...texts, + ...fromTexts, [ - 'Every week on monday', 'DTSTART;TZID=America/New_York:20220601T000000\nRRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO', + 'Every week on monday', ], ] describe('NLP', () => { - it('fromText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(RRule.fromText(text).toString()).toBe(str) - }) + it.each(fromTexts)('parseText()', function (rule, text) { + expect(optionsToString(RRule.parseText(text))).toBe(rule) }) - it('toText()', function () { - toTexts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(RRule.fromString(str).toText().toLowerCase()).toBe( - text.toLowerCase(), - ) - }) + it.each(fromTexts)('fromText()', function (rule, text) { + expect(RRule.fromText(text).toString()).toBe(rule) }) - it('parseText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(optionsToString(RRule.parseText(text))).toBe(str) - }) + it.each(toTexts)('toText()', function (rule, text) { + expect(RRule.fromString(rule).toText().toLowerCase()).toBe( + text.toLowerCase(), + ) }) it('permits integers in byweekday (#153)', () => { @@ -78,13 +66,12 @@ describe('NLP', () => { }) it('sorts monthdays correctly (#101)', () => { - const options = { freq: 2, bymonthday: [3, 10, 17, 24] } - const rule = new RRule(options) + const rule = new RRule({ freq: 2, bymonthday: [3, 10, 17, 24] }) expect(rule.toText()).toBe('every week on the 3rd, 10th, 17th and 24th') }) it('shows correct text for every day', () => { - const options = { + const rule = new RRule({ freq: RRule.WEEKLY, byweekday: [ RRule.MO, @@ -95,20 +82,18 @@ describe('NLP', () => { RRule.SA, RRule.SU, ], - } - const rule = new RRule(options) + }) + expect(rule.toText()).toBe('every day') }) it('shows correct text for every minute', () => { - const options = { freq: RRule.MINUTELY } - const rule = new RRule(options) + const rule = new RRule({ freq: RRule.MINUTELY }) expect(rule.toText()).toBe('every minute') }) it('shows correct text for every (plural) minutes', () => { - const options = { freq: RRule.MINUTELY, interval: 2 } - const rule = new RRule(options) + const rule = new RRule({ freq: RRule.MINUTELY, interval: 2 }) expect(rule.toText()).toBe('every 2 minutes') }) @@ -130,7 +115,7 @@ describe('NLP', () => { const dateFormatter: DateFormatter = (year, month, day) => `${day}. ${month}, ${year}` - expect(rrule.toText(undefined, undefined, dateFormatter)).toBe( + expect(rrule.toText({ dateFormatter })).toBe( 'every week until 10. November, 2012', ) }) diff --git a/test/options-to-string.test.ts b/test/options-to-string.test.ts new file mode 100644 index 00000000..82c2b313 --- /dev/null +++ b/test/options-to-string.test.ts @@ -0,0 +1,40 @@ +import { datetime } from '../src/date-util' +import { optionsToString } from '../src/options-to-string' +import { RRule } from '../src/rrule' +import { Options } from '../src/types' + +describe('optionsToString', () => { + it.each([ + [ + { freq: RRule.WEEKLY, until: datetime(2010, 1, 1, 0, 0, 0) }, + 'RRULE:FREQ=WEEKLY;UNTIL=20100101T000000Z', + ], + [ + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + tzid: 'America/New_York', + }, + 'DTSTART;TZID=America/New_York:19970902T090000', + ], + [ + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + freq: RRule.WEEKLY, + }, + 'DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=WEEKLY', + ], + [ + { + dtstart: datetime(1997, 9, 2, 9, 0, 0), + tzid: 'America/New_York', + freq: RRule.WEEKLY, + }, + 'DTSTART;TZID=America/New_York:19970902T090000\n' + 'RRULE:FREQ=WEEKLY', + ], + ])( + 'serializes valid single lines of rrules', + function (options: Partial, expected: string) { + expect(optionsToString(options)).toEqual(expected) + }, + ) +}) diff --git a/test/optionstostring.test.ts b/test/optionstostring.test.ts deleted file mode 100644 index aeba173f..00000000 --- a/test/optionstostring.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { datetime } from '../src/date-util' -import { optionsToString } from '../src/options-to-string' -import { RRule } from '../src/rrule' -import { Options } from '../src/types' - -describe('optionsToString', () => { - it('serializes valid single lines of rrules', function () { - const expectations: [Partial, string][] = [ - [ - { freq: RRule.WEEKLY, until: datetime(2010, 1, 1, 0, 0, 0) }, - 'RRULE:FREQ=WEEKLY;UNTIL=20100101T000000Z', - ], - [ - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - tzid: 'America/New_York', - }, - 'DTSTART;TZID=America/New_York:19970902T090000', - ], - [ - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - freq: RRule.WEEKLY, - }, - 'DTSTART:19970902T090000Z\n' + 'RRULE:FREQ=WEEKLY', - ], - [ - { - dtstart: datetime(1997, 9, 2, 9, 0, 0), - tzid: 'America/New_York', - freq: RRule.WEEKLY, - }, - 'DTSTART;TZID=America/New_York:19970902T090000\n' + 'RRULE:FREQ=WEEKLY', - ], - ] - - expectations.forEach(function (item) { - const s = item[0] - const s2 = item[1] - // JSON.stringify(s) - expect(optionsToString(s)).toEqual(s2) - }) - }) -}) diff --git a/test/parseoptions.test.ts b/test/parse-options.test.ts similarity index 100% rename from test/parseoptions.test.ts rename to test/parse-options.test.ts diff --git a/test/parsestring.test.ts b/test/parse-string.test.ts similarity index 100% rename from test/parsestring.test.ts rename to test/parse-string.test.ts From 9fd8d4182049a36b8a6e1edffb8d5c8dc941818e Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Tue, 4 Nov 2025 16:25:38 +0700 Subject: [PATCH 3/6] date-fns finally? --- package.json | 8 +- pnpm-lock.yaml | 20 +++++ src/cache.ts | 6 +- src/date-util.ts | 161 +++++++++++++++------------------------ src/date-with-zone.ts | 4 +- src/datetime.ts | 4 +- src/iter/index.ts | 4 +- src/options-to-string.ts | 4 +- src/parse-options.ts | 19 ++--- src/rruleset.ts | 4 +- 10 files changed, 113 insertions(+), 121 deletions(-) diff --git a/package.json b/package.json index 98410a34..3f4fc578 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "tsc -p tsconfig.build.json", - "test": "nyc jest **/*.test.ts --runInBand" + "test": "jest **/*.test.ts --runInBand" }, "dependencies": { "luxon": "^3.7.2" @@ -34,6 +34,8 @@ "@types/mockdate": "^3.0.0", "@types/node": "^24.10.0", "coverage": "^0.4.1", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "jest": "^29.7.0", "mockdate": "^3.0.5", "nyc": "^15.1.0", @@ -55,5 +57,9 @@ "html" ], "all": true + }, + "peerDependencies": { + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc674dce..ac5c1c2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,12 @@ importers: coverage: specifier: ^0.4.1 version: 0.4.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + date-fns-tz: + specifier: ^3.2.0 + version: 3.2.0(date-fns@4.1.0) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@24.10.0)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3)) @@ -744,6 +750,14 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2758,6 +2772,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + date-fns-tz@3.2.0(date-fns@4.1.0): + dependencies: + date-fns: 4.1.0 + + date-fns@4.1.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/src/cache.ts b/src/cache.ts index fb74c6be..dac92117 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,4 +1,4 @@ -import { clone, cloneDates } from './date-util' +import { clone } from './date-util' import IterResult, { IterArgs } from './iter-result' export type CacheKeys = 'before' | 'after' | 'between' @@ -37,7 +37,7 @@ export class Cache { args?: Partial, ) { if (value) { - value = value instanceof Date ? clone(value) : cloneDates(value) + value = value instanceof Date ? clone(value) : value.map(clone) } if (what === 'all') { @@ -97,7 +97,7 @@ export class Cache { } return Array.isArray(cached) - ? cloneDates(cached) + ? cached.map(clone) : cached instanceof Date ? clone(cached) : cached diff --git a/src/date-util.ts b/src/date-util.ts index 8be5a369..1b6216bf 100644 --- a/src/date-util.ts +++ b/src/date-util.ts @@ -1,106 +1,89 @@ +import { differenceInDays } from 'date-fns' import { Time } from './datetime' type Datelike = Pick -export function datetime(y: number, m: number, d: number, h = 0, i = 0, s = 0) { - return new Date(Date.UTC(y, m - 1, d, h, i, s)) +export function datetime( + y: number, + m: number, + d: number, + h = 0, + i = 0, + s = 0, + ms = 0, +) { + return new Date(Date.UTC(y, m - 1, d, h, i, s, ms)) } -/** - * General date-related utilities. - * Also handles several incompatibilities between JavaScript and Python - * - */ -export const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - -/** - * Number of milliseconds of one day - */ -export const ONE_DAY = 1000 * 60 * 60 * 24 - -/** - * @see: - */ -export const MAXYEAR = 9999 - -/** - * Python uses 1-Jan-1 as the base for calculating ordinals but we don't - * want to confuse the JS engine with milliseconds > Number.MAX_NUMBER, - * therefore we use 1-Jan-1970 instead - */ -export const ORDINAL_BASE = datetime(1970, 1, 1) - -/** - * Python: MO-SU: 0 - 6 - * JS: SU-SAT 0 - 6 - */ -export const PY_WEEKDAYS = [6, 0, 1, 2, 3, 4, 5] - -export function isLeapYear(year: number) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 +export function sort(dates: T[]) { + dates.sort((a, b) => a.getTime() - b.getTime()) } export function isValidDate(value: unknown): value is Date { return value instanceof Date && !isNaN(value.getTime()) } -/** - * @see: - */ -export function daysBetween(date1: Date, date2: Date) { - // The number of milliseconds in one day - // Convert both dates to milliseconds - const date1ms = date1.getTime() - const date2ms = date2.getTime() +export function clone(date: Date | Time) { + return new Date(date.getTime()) +} - // Calculate the difference in milliseconds - const differencems = date1ms - date2ms +// --- - // Convert back to days and return - return Math.round(differencems / ONE_DAY) -} +export const MAX_YEAR = 9999 -/** - * @see: - */ -export function toOrdinal(date: Date) { - return daysBetween(date, ORDINAL_BASE) +export function isLeapYear(year: number) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 } -/** - * @see - - */ -export function fromOrdinal(ordinal: number) { - return new Date(ORDINAL_BASE.getTime() + ordinal * ONE_DAY) -} +// --- -export function getMonthDays(date: Date) { +const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +// faster than date-fns +export function getDaysInMonth(date: Date) { const month = date.getUTCMonth() return month === 1 && isLeapYear(date.getUTCFullYear()) ? 29 : MONTH_DAYS[month] } -/** - * @return {Number} python-like weekday - */ +// http://docs.python.org/library/calendar.html#calendar.monthrange +export function monthRange(year: number, month: number) { + const date = new Date(Date.UTC(year, month, 1)) + return [getWeekday(date), getDaysInMonth(date)] +} + +// --- + +const ONE_DAY = 1000 * 60 * 60 * 24 + +// Python: MO-SU: 0 - 6 vs JS: SU-SAT 0 - 6 +const PY_WEEKDAYS = [6, 0, 1, 2, 3, 4, 5] + export function getWeekday(date: Date) { return PY_WEEKDAYS[date.getUTCDay()] } +// --- + /** - * @see: + * Python uses 1-Jan-1 as the base for calculating ordinals but we don't + * want to confuse the JS engine with milliseconds > Number.MAX_NUMBER, + * therefore we use 1-Jan-1970 instead */ -export function monthRange(year: number, month: number) { - const date = datetime(year, month + 1, 1) - return [getWeekday(date), getMonthDays(date)] +export const ORDINAL_BASE = datetime(1970, 1, 1) + +export function fromOrdinal(ordinal: number) { + return new Date(ORDINAL_BASE.getTime() + ordinal * ONE_DAY) } -/** - * @see: - */ -export function combine(date: Date, time: Date | Time) { - time = time || date +export function toOrdinal(date: Date) { + return differenceInDays(date, ORDINAL_BASE) +} + +// --- + +export function combine(date: Date, time: Date | Time = date) { return new Date( Date.UTC( date.getUTCFullYear(), @@ -114,28 +97,9 @@ export function combine(date: Date, time: Date | Time) { ) } -export function clone(date: Date | Time) { - return new Date(date.getTime()) -} - -export function cloneDates(dates: Date[] | Time[]) { - const clones = [] - for (let i = 0; i < dates.length; i++) { - clones.push(clone(dates[i])) - } - return clones -} +// --- -/** - * Sorts an array of Date or Time objects - */ -export function sort(dates: T[]) { - dates.sort(function (a, b) { - return a.getTime() - b.getTime() - }) -} - -export function timeToUntilString(time: number, utc = true) { +export function untilTimeToString(time: number, utc = true) { const date = new Date(time) return [ `${date.getUTCFullYear()}`.padStart(4, '0'), @@ -167,19 +131,20 @@ export function untilStringToDate(until: string) { ) } -const dateTZtoISO8601 = function (date: Date, timeZone: string) { - // date format for sv-SE is almost ISO8601 - const dateStr = date.toLocaleString('sv-SE', { timeZone }) - // '2023-02-07 10:41:36' - return dateStr.replace(' ', 'T') + 'Z' +// --- + +// date format for sv-SE is almost ISO8601 +function dateTZtoISO8601(date: Date, timeZone: string) { + return `${date.toLocaleString('sv-SE', { timeZone }).replace(' ', 'T')}Z` } export function dateInTimeZone(date: Date, timeZone: string) { const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone + // Date constructor can only reliably parse dates in ISO8601 format const dateInLocalTZ = new Date(dateTZtoISO8601(date, localTimeZone)) const dateInTargetTZ = new Date(dateTZtoISO8601(date, timeZone ?? 'UTC')) - const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime() + const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime() return new Date(date.getTime() - tzOffset) } diff --git a/src/date-with-zone.ts b/src/date-with-zone.ts index 3e3a7edf..e4fdb317 100644 --- a/src/date-with-zone.ts +++ b/src/date-with-zone.ts @@ -1,4 +1,4 @@ -import { dateInTimeZone, timeToUntilString } from './date-util' +import { dateInTimeZone, untilTimeToString } from './date-util' export class DateWithZone { public date: Date @@ -17,7 +17,7 @@ export class DateWithZone { } public toString() { - const datestr = timeToUntilString(this.date.getTime(), this.isUTC) + const datestr = untilTimeToString(this.date.getTime(), this.isUTC) if (!this.isUTC) { return `;TZID=${this.tzid}:${datestr}` } diff --git a/src/datetime.ts b/src/datetime.ts index 8922cc20..a72bb906 100644 --- a/src/datetime.ts +++ b/src/datetime.ts @@ -1,4 +1,4 @@ -import { getWeekday, MAXYEAR, monthRange } from './date-util' +import { getWeekday, MAX_YEAR, monthRange } from './date-util' import { divmod, empty, pymod } from './helpers' import { Frequency, ParsedOptions } from './types' @@ -236,7 +236,7 @@ export class DateTime extends Time { if (this.month === 13) { this.month = 1 ++this.year - if (this.year > MAXYEAR) { + if (this.year > MAX_YEAR) { return } } diff --git a/src/iter/index.ts b/src/iter/index.ts index bc2b6b02..a785faf4 100644 --- a/src/iter/index.ts +++ b/src/iter/index.ts @@ -1,4 +1,4 @@ -import { combine, fromOrdinal, MAXYEAR } from '../date-util' +import { combine, fromOrdinal, MAX_YEAR } from '../date-util' import { DateWithZone } from '../date-with-zone' import { DateTime, Time } from '../datetime' import { empty, isDefined } from '../helpers' @@ -114,7 +114,7 @@ export function iter( // Handle frequency and interval counterDate.add(parsedOptions, filtered) - if (counterDate.year > MAXYEAR) { + if (counterDate.year > MAX_YEAR) { return emitResult(iterResult) } diff --git a/src/options-to-string.ts b/src/options-to-string.ts index 083aaf77..2f6f38a7 100644 --- a/src/options-to-string.ts +++ b/src/options-to-string.ts @@ -1,4 +1,4 @@ -import { timeToUntilString } from './date-util' +import { untilTimeToString } from './date-util' import { DateWithZone } from './date-with-zone' import { isDefined, isNumber, toArray } from './helpers' import { DEFAULT_OPTIONS, RRule } from './rrule' @@ -67,7 +67,7 @@ export function optionsToString(options: Partial) { break case 'UNTIL': - outValue = timeToUntilString(value as number, !options.tzid) + outValue = untilTimeToString(value as number, !options.tzid) break default: diff --git a/src/parse-options.ts b/src/parse-options.ts index fe612737..6bb6773b 100644 --- a/src/parse-options.ts +++ b/src/parse-options.ts @@ -6,13 +6,12 @@ import { Options, ParsedOptions, freqIsDailyOrGreater } from './types' import { Weekday, isWeekdayStr } from './weekday' export function initializeOptions(options: Partial) { - const invalid = Object.entries(options) - .flatMap(([key, value]) => - !(defaultKeys as string[]).includes(key) - || (value instanceof Date && !isValidDate(value)) - ? [key] - : [] - ) + const invalid = Object.entries(options).flatMap(([key, value]) => + !(defaultKeys as string[]).includes(key) || + (value instanceof Date && !isValidDate(value)) + ? [key] + : [], + ) if (invalid.length) { throw new Error('Invalid options: ' + invalid.join(', ')) @@ -49,9 +48,11 @@ export function parseOptions(options: Partial) { opts.bysetpos = [opts.bysetpos] } - opts.bysetpos.forEach(v => { + opts.bysetpos.forEach((v) => { if (v === 0 || Math.abs(v) >= 366) { - throw new Error('bysetpos must be between 1 and 366,' + ' or between -366 and -1') + throw new Error( + 'bysetpos must be between 1 and 366,' + ' or between -366 and -1', + ) } }) } diff --git a/src/rruleset.ts b/src/rruleset.ts index d7148df3..69f8871b 100644 --- a/src/rruleset.ts +++ b/src/rruleset.ts @@ -1,4 +1,4 @@ -import { sort, timeToUntilString } from './date-util' +import { sort, untilTimeToString } from './date-util' import IterResult from './iter-result' import { iterSet } from './iter-set' import { optionsToString } from './options-to-string' @@ -224,7 +224,7 @@ function rdatesToString( const header = isUTC ? `${param}:` : `${param};TZID=${tzid}:` const dateString = rdates - .map((rdate) => timeToUntilString(rdate.valueOf(), isUTC)) + .map((rdate) => untilTimeToString(rdate.valueOf(), isUTC)) .join(',') return `${header}${dateString}` From 380d6af4f7dfb38778c1c7a5bc83fce2fb5626fb Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Tue, 4 Nov 2025 17:01:17 +0700 Subject: [PATCH 4/6] removed luxon --- package.json | 4 +- pnpm-lock.yaml | 12 ---- src/date-util.ts | 3 +- src/iter-set.ts | 4 +- src/iter/index.ts | 19 +----- src/iter/optimize-options.ts | 129 ----------------------------------- src/rrule.ts | 2 +- 7 files changed, 7 insertions(+), 166 deletions(-) delete mode 100644 src/iter/optimize-options.ts diff --git a/package.json b/package.json index 3f4fc578..ead562e4 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@types/node": "^24.10.0", "coverage": "^0.4.1", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", "jest": "^29.7.0", "mockdate": "^3.0.5", "nyc": "^15.1.0", @@ -59,7 +58,6 @@ "all": true }, "peerDependencies": { - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0" + "date-fns": "^4.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac5c1c2a..0adf97fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,9 +30,6 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 - date-fns-tz: - specifier: ^3.2.0 - version: 3.2.0(date-fns@4.1.0) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@24.10.0)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3)) @@ -750,11 +747,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - date-fns-tz@3.2.0: - resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} - peerDependencies: - date-fns: ^3.0.0 || ^4.0.0 - date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -2772,10 +2764,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - date-fns-tz@3.2.0(date-fns@4.1.0): - dependencies: - date-fns: 4.1.0 - date-fns@4.1.0: {} debug@4.4.3: diff --git a/src/date-util.ts b/src/date-util.ts index 1b6216bf..8c70847e 100644 --- a/src/date-util.ts +++ b/src/date-util.ts @@ -113,8 +113,9 @@ export function untilTimeToString(time: number, utc = true) { ].join('') } +const re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/ + export function untilStringToDate(until: string) { - const re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/ const bits = re.exec(until) if (!bits) throw new Error(`Invalid UNTIL value: ${until}`) diff --git a/src/iter-set.ts b/src/iter-set.ts index aad6b38c..d67c105c 100644 --- a/src/iter-set.ts +++ b/src/iter-set.ts @@ -59,8 +59,8 @@ export function iterSet( if (!iterResult.accept(new Date(zonedDate.getTime()))) break } - _rrule.forEach(function (rrule) { - iter(iterResult, rrule.options, rrule.origOptions, _exdateHash, evalExdate) + _rrule.forEach((rrule) => { + iter(iterResult, rrule.options) }) const res = iterResult._result diff --git a/src/iter/index.ts b/src/iter/index.ts index a785faf4..6e876572 100644 --- a/src/iter/index.ts +++ b/src/iter/index.ts @@ -6,30 +6,13 @@ import Iterinfo from '../iter-info/index' import IterResult from '../iter-result' import { buildTimeset } from '../parse-options' import { RRule } from '../rrule' -import { - freqIsDailyOrGreater, - Options, - ParsedOptions, - QueryMethodTypes, -} from '../types' +import { freqIsDailyOrGreater, ParsedOptions, QueryMethodTypes } from '../types' import { buildPosList } from './build-pos-list' -import { optimizeOptions } from './optimize-options' export function iter( iterResult: IterResult, parsedOptions: ParsedOptions, - origOptions: Partial, - exdateHash?: { [k: number]: boolean }, - evalExdate?: (after: Date, before: Date) => void, ) { - parsedOptions = optimizeOptions( - iterResult, - parsedOptions, - origOptions, - exdateHash, - evalExdate, - ) - const { freq, dtstart, interval, until, bysetpos } = parsedOptions let count = parsedOptions.count diff --git a/src/iter/optimize-options.ts b/src/iter/optimize-options.ts deleted file mode 100644 index a5cb79fd..00000000 --- a/src/iter/optimize-options.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { DateTime } from 'luxon' - -import { empty } from '../helpers' -import IterResult from '../iter-result' -import { Frequency, Options, ParsedOptions, QueryMethodTypes } from '../types' -import { Weekday } from '../weekday' - -const UNIT_BY_FREQUENCY = { - [Frequency.YEARLY]: 'year', - [Frequency.MONTHLY]: 'month', - [Frequency.WEEKLY]: 'week', - [Frequency.DAILY]: 'day', - [Frequency.HOURLY]: 'hour', - [Frequency.MINUTELY]: 'minute', - [Frequency.SECONDLY]: 'second', -} as const - -function optimize( - frequency: Frequency, - dtstart: Date, - interval: number, - minDate?: Date, - maxDate?: Date, - count?: number, - exdateHash?: { [k: number]: boolean }, - evalExdate?: (after: Date, before: Date) => void, -) { - const frequencyUnit = UNIT_BY_FREQUENCY[frequency] - const minDateTime = DateTime.fromJSDate(minDate ? minDate : maxDate, { - zone: 'UTC', - }) - const dtstartDateTime = DateTime.fromJSDate(dtstart, { zone: 'UTC' }) - - const diff = Math.abs( - dtstartDateTime.diff(minDateTime, frequencyUnit).get(frequencyUnit), - ) - const intervalsInDiff = Math.floor(diff / interval) - - let optimisedDtstart = dtstartDateTime.plus({ - [frequencyUnit]: interval * intervalsInDiff, - }) - - let decrementCountFor = intervalsInDiff - - if (evalExdate) { - evalExdate( - optimisedDtstart.minus({ millisecond: 1 }).toJSDate(), - optimisedDtstart.plus({ millisecond: 1 }).toJSDate(), - ) - } - - while (exdateHash?.[optimisedDtstart.toMillis()]) { - optimisedDtstart = optimisedDtstart.minus({ [frequencyUnit]: interval }) - decrementCountFor-- - } - - if (count !== undefined) { - count = count - decrementCountFor - count = count < 0 ? 0 : count - } - - return { - dtstart: optimisedDtstart.toJSDate(), - count, - } -} - -export function optimizeOptions( - iterResult: IterResult, - parsedOptions: ParsedOptions, - origOptions: Partial, - exdateHash?: { [k: number]: boolean }, - evalExdate?: (after: Date, before: Date) => void, -) { - const { - freq, - count, - bymonth, - bysetpos, - bymonthday, - byyearday, - byweekno, - byhour, - byminute, - bysecond, - byweekday, - byeaster, - interval = 1, - } = origOptions - const { minDate, maxDate, method, args } = iterResult - const { dtstart } = parsedOptions - const { skipOptimisation } = args - - if ( - skipOptimisation || - method === 'all' || - (!minDate && !maxDate) || - (minDate && minDate <= dtstart) || - (maxDate && maxDate <= dtstart) || - !empty(bymonth) || - !empty(bysetpos) || - !empty(bymonthday) || - !empty(byyearday) || - !empty(byweekno) || - !empty(byhour) || - !empty(byminute) || - !empty(bysecond) || - !empty(bysecond) || - !empty(byeaster) || - (!empty(byweekday) && - [].concat(byweekday).some((w) => w instanceof Weekday)) - ) { - return parsedOptions - } - - return { - ...parsedOptions, - ...optimize( - freq, - dtstart, - interval, - minDate, - maxDate, - count, - exdateHash, - evalExdate, - ), - } -} diff --git a/src/rrule.ts b/src/rrule.ts index f7553044..3a6dc9a6 100644 --- a/src/rrule.ts +++ b/src/rrule.ts @@ -127,7 +127,7 @@ export class RRule implements QueryMethods { protected _iter( iterResult: IterResult, ): IterResultType { - return iter(iterResult, this.options, this.origOptions) + return iter(iterResult, this.options) } private _cacheGet(what: CacheKeys | 'all', args?: Partial) { From ba8c08c3409cf4a66413317997c7d570636e1ada Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Tue, 4 Nov 2025 17:35:04 +0700 Subject: [PATCH 5/6] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ead562e4..c748f12d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@taskworld/rrule", - "version": "3.0.0", + "version": "3.1.0", "description": "JavaScript library for working with recurrence rules for calendar dates.", "repository": "git://github.com/taskworld/rrule.git", "author": "Jakub Roztocil, Lars Schöning, David Golightly, and Taskworld", From 26115f9e6fd3e886ffb163d7f94b86cef3d7a89f Mon Sep 17 00:00:00 2001 From: Julien Barbay Date: Tue, 4 Nov 2025 17:38:08 +0700 Subject: [PATCH 6/6] hop --- package.json | 6 +----- pnpm-lock.yaml | 8 -------- src/date-util.ts | 6 ++++-- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index c748f12d..22bb63e9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "tsc -p tsconfig.build.json", - "test": "jest **/*.test.ts --runInBand" + "test": "jest **/*.test.ts" }, "dependencies": { "luxon": "^3.7.2" @@ -34,7 +34,6 @@ "@types/mockdate": "^3.0.0", "@types/node": "^24.10.0", "coverage": "^0.4.1", - "date-fns": "^4.1.0", "jest": "^29.7.0", "mockdate": "^3.0.5", "nyc": "^15.1.0", @@ -56,8 +55,5 @@ "html" ], "all": true - }, - "peerDependencies": { - "date-fns": "^4.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0adf97fd..fc674dce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,9 +27,6 @@ importers: coverage: specifier: ^0.4.1 version: 0.4.1 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@24.10.0)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3)) @@ -747,9 +744,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2764,8 +2758,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - date-fns@4.1.0: {} - debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/src/date-util.ts b/src/date-util.ts index 8c70847e..73a70882 100644 --- a/src/date-util.ts +++ b/src/date-util.ts @@ -1,4 +1,3 @@ -import { differenceInDays } from 'date-fns' import { Time } from './datetime' type Datelike = Pick @@ -39,7 +38,6 @@ export function isLeapYear(year: number) { const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] -// faster than date-fns export function getDaysInMonth(date: Date) { const month = date.getUTCMonth() return month === 1 && isLeapYear(date.getUTCFullYear()) @@ -64,6 +62,10 @@ export function getWeekday(date: Date) { return PY_WEEKDAYS[date.getUTCDay()] } +export function differenceInDays(date1: Date, date2: Date) { + return Math.round((date1.getTime() - date2.getTime()) / ONE_DAY) +} + // --- /**