Skip to content
This repository was archived by the owner on Nov 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -23,7 +23,7 @@
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc -p tsconfig.build.json",
"test": "nyc jest **/*.test.ts"
"test": "jest **/*.test.ts"
},
"dependencies": {
"luxon": "^3.7.2"
Expand Down
13 changes: 6 additions & 7 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import IterResult, { IterArgs } from './iterresult'
import { clone, cloneDates } from './dateutil'
import { isArray } from './helpers'
import { clone } from './date-util'
import IterResult, { IterArgs } from './iter-result'

export type CacheKeys = 'before' | 'after' | 'between'

Expand Down Expand Up @@ -38,7 +37,7 @@ export class Cache {
args?: Partial<IterArgs>,
) {
if (value) {
value = value instanceof Date ? clone(value) : cloneDates(value)
value = value instanceof Date ? clone(value) : value.map(clone)
}

if (what === 'all') {
Expand Down Expand Up @@ -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++) {
Expand All @@ -97,8 +96,8 @@ export class Cache {
this._cacheAdd(what, cached, args)
}

return isArray(cached)
? cloneDates(cached)
return Array.isArray(cached)
? cached.map(clone)
: cached instanceof Date
? clone(cached)
: cached
Expand Down
2 changes: 1 addition & 1 deletion src/callbackiterresult.ts → src/callback-iter-result.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import IterResult, { IterArgs } from './iterresult'
import IterResult, { IterArgs } from './iter-result'

type Iterator = (d: Date, len: number) => boolean

Expand Down
153 changes: 153 additions & 0 deletions src/date-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Time } from './datetime'

type Datelike = Pick<Date, 'getTime'>

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))
}

export function sort<T extends Datelike>(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())
}

export function clone(date: Date | Time) {
return new Date(date.getTime())
}

// ---

export const MAX_YEAR = 9999

export function isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}

// ---

const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

export function getDaysInMonth(date: Date) {
const month = date.getUTCMonth()
return month === 1 && isLeapYear(date.getUTCFullYear())
? 29
: MONTH_DAYS[month]
}

// 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()]
}

export function differenceInDays(date1: Date, date2: Date) {
return Math.round((date1.getTime() - date2.getTime()) / ONE_DAY)
}

// ---

/**
* 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)

export function fromOrdinal(ordinal: number) {
return new Date(ORDINAL_BASE.getTime() + ordinal * ONE_DAY)
}

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(),
date.getUTCMonth(),
date.getUTCDate(),
time.getHours(),
time.getMinutes(),
time.getSeconds(),
time.getMilliseconds(),
),
)
}

// ---

export function untilTimeToString(time: number, utc = true) {
const date = new Date(time)
return [
`${date.getUTCFullYear()}`.padStart(4, '0'),
`${date.getUTCMonth() + 1}`.padStart(2, '0'),
`${date.getUTCDate()}`.padStart(2, '0'),
'T',
`${date.getUTCHours()}`.padStart(2, '0'),
`${date.getUTCMinutes()}`.padStart(2, '0'),
`${date.getUTCSeconds()}`.padStart(2, '0'),
utc ? 'Z' : '',
].join('')
}

const re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/

export function untilStringToDate(until: string) {
const bits = re.exec(until)

if (!bits) throw new Error(`Invalid UNTIL value: ${until}`)

return new Date(
Date.UTC(
parseInt(bits[1], 10),
parseInt(bits[2], 10) - 1,
parseInt(bits[3], 10),
parseInt(bits[5], 10) || 0,
parseInt(bits[6], 10) || 0,
parseInt(bits[7], 10) || 0,
),
)
}

// ---

// 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()
return new Date(date.getTime() - tzOffset)
}
4 changes: 2 additions & 2 deletions src/datewithzone.ts → src/date-with-zone.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dateInTimeZone, timeToUntilString } from './dateutil'
import { dateInTimeZone, untilTimeToString } from './date-util'

export class DateWithZone {
public date: Date
Expand All @@ -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}`
}
Expand Down
20 changes: 10 additions & 10 deletions src/datetime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ParsedOptions, Frequency } from './types'
import { pymod, divmod, empty, includes } from './helpers'
import { getWeekday, MAXYEAR, monthRange } from './dateutil'
import { getWeekday, MAX_YEAR, monthRange } from './date-util'
import { divmod, empty, pymod } from './helpers'
import { Frequency, ParsedOptions } from './types'

export class Time {
public hour: number
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
}
Expand Down
Loading
Loading