diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 23a2291c..7cf0f750 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -14,9 +14,9 @@ jobs: - run: npm install -g codecov nyc - run: yarn install - run: yarn build - - run: TZ=America/Vancouver yarn test - - run: TZ=America/Los_Angeles yarn test - - run: TZ=Africa/Nairobi yarn test - - run: TZ=Pacific/Kiritimati yarn test - - run: TZ=Asia/Tokyo yarn test-ci + - run: LANG=en_CA TZ=America/Vancouver yarn test + - run: LANG=en_US TZ=America/Los_Angeles yarn test + - run: LANG=sw_KE TZ=Africa/Nairobi yarn test + - run: LANG=en_KI TZ=Pacific/Kiritimati yarn test + - run: LANG=jp_JP TZ=Asia/Tokyo yarn test-ci - run: nyc report --reporter=json && codecov -t ${{ secrets.CODECOV_REPO_TOKEN }} -f coverage/*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index fed80fa4..ebfafd9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ### Changelog +- 2.7.2 (2023-02-10) + + - Bugfixes: + - Fix rezonedDate ([#523](https://github.com/jakubroztocil/rrule/issues/523)) + - Export datetime ([#551](https://github.com/jakubroztocil/rrule/issues/551)) + - Fixes types for `before()` and `after()` ([#560](https://github.com/jakubroztocil/rrule/issues/560)) + - Update README (https://github.com/jakubroztocil/rrule/pull/543) + - 2.7.1 (2022-07-10) - Internal: diff --git a/README.md b/README.md index 45e9139f..428dd9c9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![js-standard-style][js-standard-image]][js-standard-url] [![Downloads][downloads-image]][downloads-url] [![Gitter][gitter-image]][gitter-url] -[![codecov.io](http://codecov.io/github/jakubroztocil/rrule/coverage.svg?branch=master)](http://codecov.io/github/jakubroztocil/rrule?branch=master) +[![codecov.io](http://codecov.io/github/jkbrzt/rrule/coverage.svg?branch=master)](http://codecov.io/github/jkbrzt/rrule?branch=master) rrule.js supports recurrence rules as defined in the [iCalendar RFC](https://tools.ietf.org/html/rfc5545), with a few important @@ -21,7 +21,7 @@ to natural language. ### Quick Start -- [Demo app](http://jakubroztocil.github.io/rrule/) +- [Demo app](http://jkbrzt.github.io/rrule/) - # For contributors and maintainers: the code for the demo app is only on `gh-pages` branch #### Client Side @@ -32,8 +32,8 @@ $ yarn add rrule Alternatively, download manually: -- [rrule.min.js](https://jakubroztocil.github.io/rrule/dist/es5/rrule.min.js) (bundled, minified) -- [rrule.js](https://jakubroztocil.github.io/rrule/dist/es5/rrule.js) (bundled, not minified) +- [rrule.min.js](https://jkbrzt.github.io/rrule/dist/es5/rrule.min.js) (bundled, minified) +- [rrule.js](https://jkbrzt.github.io/rrule/dist/es5/rrule.js) (bundled, not minified) ```html @@ -175,9 +175,9 @@ Dates in JavaScript are tricky. `RRule` tries to support as much flexibility as By default, `RRule` deals in ["floating" times or UTC timezones](https://tools.ietf.org/html/rfc5545#section-3.2.19). If you want results in a specific timezone, `RRule` also provides [timezone support](#timezone-support). Either way, JavaScript's built-in "timezone" offset tends to just get in the way, so this library simply doesn't use it at all. All times are returned with zero offset, as though it didn't exist in JavaScript. -**The bottom line is the returned "UTC" dates are always meant to be interpreted as dates in your local timezone. This may mean you have to do additional conversion to get the "correct" local time with offset applied.** +**THE BOTTOM LINE: Returned "UTC" dates are always meant to be interpreted as dates in your local timezone. This may mean you have to do additional conversion to get the "correct" local time with offset applied.** -For this reason, it is highly recommended to use timestamps in UTC eg. `new Date(Date.UTC(...))`. Returned dates will likewise be in UTC (except on Chrome, which always returns dates with a timezone offset). It's recommended to use the provided `datetime` helper, which +For this reason, it is highly recommended to use timestamps in UTC eg. `new Date(Date.UTC(...))`. Returned dates will likewise be in UTC (except on Chrome, which always returns dates with a timezone offset). It's recommended to use the provided `datetime()` helper, which creates dates in the correct format using a 1-based month. For example: @@ -202,7 +202,7 @@ date.getUTCDate() // --> 1 date.getUTCHours() // --> 18 ``` -If you want to get the same times in true UTC, you may do so eg. using [Luxon](https://moment.github.io/luxon/#/): +If you want to get the same times in true UTC, you may do so (e.g., using [Luxon](https://moment.github.io/luxon/#/)): ```ts rule.all().map(date => @@ -282,7 +282,7 @@ iCalendar RFC. Only `freq` is required. Option Description - + freq @@ -491,7 +491,7 @@ rule.all(function (date, i) { ##### `RRule.prototype.between(after, before, inc=false [, iterator])` Returns all the occurrences of the rrule between `after` and `before`. -The inc keyword defines what happens if `after` and/or `before` are +The `inc` keyword defines what happens if `after` and/or `before` are themselves occurrences. With `inc == true`, they will be included in the list, if they are found in the recurrence set. @@ -582,8 +582,8 @@ var rule = new RRule(options) #### Natural Language Text Methods -These methods provide an incomplete support for text–`RRule` and -`RRule`–text conversion. You should test them with your input to see +These methods provide an incomplete support for text→`RRule` and +`RRule`→text conversion. You should test them with your input to see whether the result is acceptable. ##### `RRule.prototype.toText([gettext, [language]])` @@ -634,7 +634,7 @@ var rule = new RRule(options) new RRuleSet([(noCache = false)]) ``` -The RRuleSet instance allows more complex recurrence setups, mixing multiple +The `RRuleSet` instance allows more complex recurrence setups, mixing multiple rules, dates, exclusion rules, and exclusion dates. Default `noCache` argument is `false`, caching of results will be enabled, @@ -642,30 +642,30 @@ improving performance of multiple queries considerably. ##### `RRuleSet.prototype.rrule(rrule)` -Include the given rrule instance in the recurrence set generation. +Include the given `rrule` instance in the recurrence set generation. ##### `RRuleSet.prototype.rdate(dt)` -Include the given datetime instance in the recurrence set generation. +Include the given datetime instance `dt` in the recurrence set generation. ##### `RRuleSet.prototype.exrule(rrule)` -Include the given rrule instance in the recurrence set exclusion list. Dates +Include the given `rrule` instance in the recurrence set exclusion list. Dates which are part of the given recurrence rules will not be generated, even if -some inclusive rrule or rdate matches them. NOTE: EXRULE has been (deprecated +some inclusive rrule or rdate matches them. **NOTE:** `EXRULE` has been (deprecated in RFC 5545)[https://icalendar.org/iCalendar-RFC-5545/a-3-deprecated-features.html] -and does not support a DTSTART property. +and does not support a `DTSTART` property. ##### `RRuleSet.prototype.exdate(dt)` -Include the given datetime instance in the recurrence set exclusion list. Dates -included that way will not be generated, even if some inclusive rrule or -rdate matches them. +Include the given datetime instance `dt` in the recurrence set exclusion list. Dates +included that way will not be generated, even if some inclusive `rrule` or +`rdate` matches them. ##### `RRuleSet.prototype.tzid(tz?)` Sets or overrides the timezone identifier. Useful if there are no rrules in this -RRuleSet and thus no DTSTART. +`RRuleSet` and thus no `DTSTART`. ##### `RRuleSet.prototype.all([iterator])` @@ -709,36 +709,57 @@ rrulestr(rruleStr[, options]) The `rrulestr` function is a parser for RFC-like syntaxes. The string passed as parameter may be a multiple line string, a single line string, or just the -RRULE property value. +`RRULE` property value. Additionally, it accepts the following keyword arguments: -`cache` -If True, the rruleset or rrule created instance will cache its results. -Default is not to cache. - -`dtstart` -If given, it must be a datetime instance that will be used when no DTSTART -property is found in the parsed string. If it is not given, and the property -is not found, datetime.now() will be used instead. - -`unfold` -If set to True, lines will be unfolded following the RFC specification. It -defaults to False, meaning that spaces before every line will be stripped. - -`forceset` -If set to True a rruleset instance will be returned, even if only a single rule -is found. The default is to return an rrule if possible, and an rruleset if necessary. +
-`compatible` -If set to True, the parser will operate in RFC-compatible mode. Right now it -means that unfold will be turned on, and if a DTSTART is found, it will be -considered the first recurrence instance, as documented in the RFC. +
cache
+
+If true, the rruleset or rrule created instance +will cache its results. +Default is not to cache. +
+ +
dtstart
+
+If given, it must be a datetime instance that will be used when no +DTSTART property is found in the parsed string. +If it is not given, and the property is not found, +datetime.now() will be used instead. +
+ +
unfold
+
+If set to true, lines will be unfolded following the RFC specification. +It defaults to false, meaning that spaces before every line will be stripped. +
+ +
forceset
+
+If set to true, an rruleset instance will be returned, +even if only a single rule is found. +The default is to return an rrule if possible, and +an rruleset if necessary. +
+ +
compatible
+
+If set to true, the parser will operate in RFC-compatible mode. +Right now it means that unfold will be turned on, and if a DTSTART is found, +it will be considered the first recurrence instance, as documented in the RFC. +
+ +
tzid
+
+If given, it must be a string that will be used when no TZID +property is found in the parsed string. +If it is not given, and the property is not found, 'UTC' will +be used by default. +
-`tzid` -If given, it must be a string that will be used when no `TZID` property is found -in the parsed string. If it is not given, and the property is not found, `'UTC'` -will be used by default. +
--- @@ -771,8 +792,8 @@ rruleSet.rrule( rruleSet.rdate(start) ``` -- Unlike documented in the RFC, every keyword is valid on every frequency (the - RFC documents that `byweekno` is only valid on yearly frequencies, for example). +- Unlike documented in the RFC, every keyword is valid on every frequency. (The + RFC documents that `byweekno` is only valid on yearly frequencies, for example.) ### Development @@ -798,21 +819,21 @@ $ yarn build #### Authors -- [Jakub Roztocil](http://roztocil.co/) - ([@jakubroztocil](http://twitter.com/jakubroztocil)) +- [Jakub Roztocil](http://roztocil.co) + ([@jkbrzt](http://twitter.com/jkbrzt)) - Lars Schöning ([@lyschoening](http://twitter.com/lyschoening)) - David Golightly ([@davigoli](http://twitter.com/davigoli)) Python `dateutil` is written by [Gustavo -Niemeyer](http://niemeyer.net/). +Niemeyer](http://niemeyer.net). -See [LICENCE](https://github.com/jakubroztocil/rrule/blob/master/LICENCE) for +See [LICENCE](https://github.com/jkbrzt/rrule/blob/master/LICENCE) for more details. [npm-url]: https://npmjs.org/package/rrule [npm-image]: http://img.shields.io/npm/v/rrule.svg -[ci-url]: https://github.com/jakubroztocil/rrule/actions -[ci-image]: https://github.com/jakubroztocil/rrule/workflows/Node%20CI/badge.svg +[ci-url]: https://github.com/jkbrzt/rrule/actions +[ci-image]: https://github.com/jkbrzt/rrule/workflows/Node%20CI/badge.svg [downloads-url]: https://npmjs.org/package/rrule [downloads-image]: http://img.shields.io/npm/dm/rrule.svg?style=flat-square [js-standard-url]: https://github.com/feross/standard @@ -822,4 +843,4 @@ more details. #### Related projects -- https://rrules.com/ — RESTful API to get back occurrences of RRULEs that conform to RFC 5545. +- https://rrules.com — RESTful API to get back occurrences of RRULEs that conform to RFC 5545. diff --git a/package.json b/package.json index 6a92e46c..e6d15dab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rrule", - "version": "2.7.1", + "version": "2.7.2", "description": "JavaScript library for working with recurrence rules for calendar dates.", "homepage": "http://jakubroztocil.github.io/rrule/", "license": "BSD-3-Clause", diff --git a/src/dateutil.ts b/src/dateutil.ts index 4ebc94be..b2cd176a 100644 --- a/src/dateutil.ts +++ b/src/dateutil.ts @@ -203,3 +203,20 @@ export const untilStringToDate = function (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' +} + +export const dateInTimeZone = function (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) +} diff --git a/src/datewithzone.ts b/src/datewithzone.ts index 9bcd50cb..e05fcc2e 100644 --- a/src/datewithzone.ts +++ b/src/datewithzone.ts @@ -1,4 +1,4 @@ -import { timeToUntilString } from './dateutil' +import { dateInTimeZone, timeToUntilString } from './dateutil' export class DateWithZone { public date: Date @@ -34,15 +34,6 @@ export class DateWithZone { return this.date } - const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone - const dateInLocalTZ = new Date( - this.date.toLocaleString(undefined, { timeZone: localTimeZone }) - ) - const dateInTargetTZ = new Date( - this.date.toLocaleString(undefined, { timeZone: this.tzid ?? 'UTC' }) - ) - const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime() - - return new Date(this.date.getTime() - tzOffset) + return dateInTimeZone(this.date, this.tzid) } } diff --git a/src/index.ts b/src/index.ts index b1452f1b..237a275f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,6 @@ export { RRuleSet } from './rruleset' export { rrulestr } from './rrulestr' export { Frequency, ByWeekday, Options } from './types' -export { Weekday, WeekdayStr } from './weekday' +export { Weekday, WeekdayStr, ALL_WEEKDAYS } from './weekday' export { RRuleStrOptions } from './rrulestr' export { datetime } from './dateutil' diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index e3495650..fbaeccf9 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -126,6 +126,7 @@ export default function parseText(text: string, language: Language = ENGLISH) { options.freq = RRule.WEEKLY options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] ttr.nextSymbol() + AT() F() break @@ -133,6 +134,7 @@ export default function parseText(text: string, language: Language = ENGLISH) { options.freq = RRule.WEEKLY if (ttr.nextSymbol()) { ON() + AT() F() } break @@ -198,6 +200,7 @@ export default function parseText(text: string, language: Language = ENGLISH) { options.byweekday.push(RRule[wkd] as ByWeekday) ttr.nextSymbol() } + AT() MDAYs() F() break diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index cbb614a1..0a2859cd 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -281,6 +281,10 @@ export default class ToText { } else if (this.byweekday) { this._byweekday() } + + if (this.origOptions.byhour) { + this._byhour() + } } } diff --git a/src/rrule.ts b/src/rrule.ts index 3d59de6e..cab2d1dd 100644 --- a/src/rrule.ts +++ b/src/rrule.ts @@ -204,7 +204,7 @@ export class RRule implements QueryMethods { * * @return Date or null */ - before(dt: Date, inc = false): Date { + before(dt: Date, inc = false): Date | null { if (!isValidDate(dt)) { throw new Error('Invalid date passed in to RRule.before') } @@ -214,7 +214,7 @@ export class RRule implements QueryMethods { result = this._iter(new IterResult('before', args)) this._cacheAdd('before', result, args) } - return result as Date + return result as Date | null } /** @@ -224,7 +224,7 @@ export class RRule implements QueryMethods { * * @return Date or null */ - after(dt: Date, inc = false): Date { + after(dt: Date, inc = false): Date | null { if (!isValidDate(dt)) { throw new Error('Invalid date passed in to RRule.after') } @@ -234,7 +234,7 @@ export class RRule implements QueryMethods { result = this._iter(new IterResult('after', args)) this._cacheAdd('after', result, args) } - return result as Date + return result as Date | null } /** diff --git a/src/types.ts b/src/types.ts index e540563c..81785142 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,8 +3,8 @@ import { Weekday, WeekdayStr } from './weekday' export interface QueryMethods { all(): Date[] between(after: Date, before: Date, inc: boolean): Date[] - before(date: Date, inc: boolean): Date - after(date: Date, inc: boolean): Date + before(date: Date, inc: boolean): Date | null + after(date: Date, inc: boolean): Date | null } export type QueryMethodTypes = keyof QueryMethods diff --git a/test/lib/utils.ts b/test/lib/utils.ts index 5610b384..f37eb088 100644 --- a/test/lib/utils.ts +++ b/test/lib/utils.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { ExclusiveTestFunction, TestFunction } from 'mocha' export { datetime } from '../../src/dateutil' -import { datetime } from '../../src/dateutil' +import { dateInTimeZone, datetime } from '../../src/dateutil' import { RRule, RRuleSet } from '../../src' const assertDatesEqual = function ( @@ -239,16 +239,5 @@ export function expectedDate( currentLocalDate: Date, targetZone: string ): Date { - // get the tzoffset between the client tz and the target tz - const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone - const dateInLocalTZ = new Date( - startDate.toLocaleString(undefined, { timeZone: localTimeZone }) - ) - const dateInTargetTZ = new Date( - startDate.toLocaleString(undefined, { timeZone: targetZone }) - ) - const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime() - - return new Date(startDate.getTime() - tzOffset) - // return new Date(new Date(startDate.getTime() + tzOffset).toLocaleDateString(undefined, { timeZone: localTimeZone })) + return dateInTimeZone(startDate, targetZone) } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index c64e7b44..dc64c41c 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -7,6 +7,10 @@ import { datetime } from './lib/utils' const texts = [ ['Every day', 'RRULE:FREQ=DAILY'], ['Every day at 10, 12 and 17', 'RRULE:FREQ=DAILY;BYHOUR=10,12,17'], + [ + 'Every week on Sunday at 10, 12 and 17', + 'RRULE:FREQ=WEEKLY;BYDAY=SU;BYHOUR=10,12,17', + ], ['Every week', 'RRULE:FREQ=WEEKLY'], ['Every hour', 'RRULE:FREQ=HOURLY'], ['Every 4 hours', 'RRULE:INTERVAL=4;FREQ=HOURLY'], diff --git a/test/rrule.test.ts b/test/rrule.test.ts index d6e1d146..d5f70762 100644 --- a/test/rrule.test.ts +++ b/test/rrule.test.ts @@ -158,6 +158,27 @@ describe('RRule', function () { ] ) + testRecurring( + 'testBetweenWithTZ', + { + rrule: new RRule({ + freq: RRule.WEEKLY, + dtstart: parse('20220613T090000'), + byweekday: [RRule.TU], + tzid: 'Europe/London', + }), + method: 'between', + args: [parse('20220613T093000'), parse('20220716T083000')], + }, + [ + expectedDate(datetime(2022, 6, 14, 9, 0), undefined, 'Europe/London'), + expectedDate(datetime(2022, 6, 21, 9, 0), undefined, 'Europe/London'), + expectedDate(datetime(2022, 6, 28, 9, 0), undefined, 'Europe/London'), + expectedDate(datetime(2022, 7, 5, 9, 0), undefined, 'Europe/London'), + expectedDate(datetime(2022, 7, 12, 9, 0), undefined, 'Europe/London'), + ] + ) + testRecurring( 'testYearly', new RRule({