diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..5c338d6da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,49 @@ +--- +name: Bug Report +about: Report a bug in @offload-project/rrule +title: "[Bug]: " +labels: bug +assignees: '' +--- + +### Description + +A clear and concise description of the bug. + +### Steps to Reproduce + +Provide a minimal code sample reproducing the issue. Include the exact RRULE string and any `Date` values you used (timestamps in UTC are easiest to compare). + +```ts +import { datetime, RRule, rrulestr } from '@offload-project/rrule' + +// e.g. constructing a rule, calling .all(), parsing an RFC string, etc. +``` + +1. Build rule with '...' +2. Call '...' +3. See the unexpected output / error. + +### Expected Output + +What dates / string / behavior did you expect? + +### Actual Output + +What did you actually get? Paste returned dates, thrown errors, or stack traces. + +``` +// Paste output / error here +``` + +### Environment + +- `@offload-project/rrule` version: [e.g., 2.9.0] +- Node version: [e.g., 20.19, 22.x] +- Runtime: [Node / Bun / browser — include browser name + version if applicable] +- Operating system: [e.g., macOS 14, Ubuntu 22.04, Windows 11] +- Local timezone: [run `date` in your terminal and paste the timezone, e.g., `PDT`, `Europe/Berlin`] + +### Additional Context + +Anything else that might help — TZID being used, whether you saw the bug only after DST, related packages, etc. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..b32538f86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,22 @@ +blank_issues_enabled: false +issue_templates: + - name: "Bug Report" + description: "Report a bug." + title: "[Bug]: " + labels: ["bug"] + body: "./ISSUE_TEMPLATE/bug_report.md" + - name: "Feature Request" + description: "Propose a new feature or improvement." + title: "[Feature]: " + labels: ["enhancement"] + body: "./ISSUE_TEMPLATE/feature_request.md" + - name: "Documentation" + description: "Suggest updates or additions to documentation." + title: "[Docs]: " + labels: ["documentation"] + body: "./ISSUE_TEMPLATE/documentation_request.md" + - name: "General Report" + description: "Provide general feedback or inquiries." + title: "[General]: " + labels: ["general"] + body: "./ISSUE_TEMPLATE/general_report.md" diff --git a/.github/ISSUE_TEMPLATE/documentation_request.md b/.github/ISSUE_TEMPLATE/documentation_request.md new file mode 100644 index 000000000..e058d80d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_request.md @@ -0,0 +1,27 @@ +--- +name: Documentation +about: Suggest updates or additions to documentation +title: "[Docs]: " +labels: documentation +assignees: '' +--- + +### Affected Documentation + +Which file or section needs updating? (e.g., `README.md`, API reference, timezone support, natural-language text methods, `CHANGELOG.md`) + +### What Is Missing or Incorrect? + +Describe what's unclear, outdated, or absent. + +### Suggested Changes + +Provide a detailed description — or a draft — of the proposed wording or examples. + +### Why Is This Needed? + +Explain who benefits and how (e.g., new adopters, users migrating from `rrule`, contributors). + +### Additional Context + +Links to related issues, PRs, RFC references, or external resources. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..aecb8fe1e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,36 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement for @offload-project/rrule +title: "[Feature]: " +labels: enhancement +assignees: '' +--- + +### Feature Description + +What feature would you like to see in the library? + +### Why Is This Needed? + +Describe the problem this solves or the use case it enables. Is it related to recurrence parsing, occurrence retrieval, natural-language text, timezone handling, RFC compliance, etc.? + +### Suggested API / Implementation + +If you have an idea of how this should look, share a code sketch. + +```ts +// e.g. proposed method signature or usage example +import { RRule } from '@offload-project/rrule' +``` + +### Alternatives Considered + +Are there workarounds you've tried, or alternative approaches you've ruled out? + +### Backwards Compatibility + +Would this change break existing API surface (`RRule`, `RRuleSet`, `rrulestr`, `validate`, exported types, RFC string output)? + +### Additional Context + +Add any relevant links — RFC sections, python-dateutil behavior, related issues, etc. diff --git a/.github/ISSUE_TEMPLATE/general_report.md b/.github/ISSUE_TEMPLATE/general_report.md new file mode 100644 index 000000000..41c579631 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general_report.md @@ -0,0 +1,29 @@ +--- +name: General Report +about: Provide general feedback, questions, or inquiries about @offload-project/rrule +title: "[General]: " +labels: general +assignees: '' +--- + +### Topic + +What is this about? (e.g., question, feedback, discussion, support inquiry) + +### Details + +Describe your question, feedback, or inquiry clearly. Include code samples if relevant. + +```ts +// Optional: code context +``` + +### Environment (if applicable) + +- `@offload-project/rrule` version: [e.g., 2.9.0] +- Node version: [e.g., 20.19, 22.x] +- Runtime: [Node / Bun / browser] + +### Additional Context + +Add any other information that might help us respond. diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index cc75e8a7c..000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,15 +0,0 @@ -### Reporting an issue - -Thank you for taking an interest in `rrule`! Please include the following in -your report: - -- [ ] Verify that you've looked through existing issues for duplicates before - creating a new one -- [ ] Code sample reproducing the issue. Be sure to include all input values you - are using such as the exact RRule string and dates. -- [ ] Expected output -- [ ] Actual output -- [ ] The version of `rrule` you are using -- [ ] Your operating system -- [ ] Your local timezone (run `$ date` from the command line - of the machine showing the bug) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e3e6cb68e..d186faa80 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,40 @@ ---- +# Description -### Thanks for contributing to `rrule`! +Please include a summary of the changes and the related issue. Include the motivation and context, and list any package dependencies impacted by this change. -To submit a pull request, please verify that you have done the following: +Fixes # (issue) -- [ ] Merged in or rebased on the latest `master` commit -- [ ] Linked to an existing bug or issue describing the bug or feature you're - addressing -- [ ] Written one or more tests showing that your change works as advertised +## Type of change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Deprecation (marks existing API as deprecated) +- [ ] Refactor / internal change (no functional impact) +- [ ] Documentation update +- [ ] CI / tooling change + +## How Has This Been Tested? + +Describe the tests you ran to verify your changes (Jest tests, manual checks in a host app, etc.). Provide instructions so reviewers can reproduce. + +- [ ] `bun run test` passes +- [ ] `bun run lint` passes +- [ ] `bun run build` produces a clean `dist/` +- [ ] Added or updated tests covering the change +- [ ] Added a changeset (`bun run change`) if this is a user-facing change + +**Test Configuration**: + +- Node version(s): [e.g., 20.19, 22.x] +- Runtime: [Node / Bun / browser] +- Local timezone: [e.g., `PDT`, `Europe/Berlin`] + +## Checklist + +- [ ] My code follows the style guidelines of this project (Biome) +- [ ] I have performed a self-review of my code +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing tests pass locally with my changes +- [ ] I have updated the README / docs where relevant +- [ ] Any breaking changes are clearly called out above diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..35b0fe9e7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Code of Conduct + +## Our pledge + +We want this project to be a welcoming, respectful space for everyone who wants to contribute — regardless of background, identity, or experience level. + +## Expected behavior + +- Be kind and patient. Assume good faith. +- Critique code and ideas, not people. +- Welcome questions, including basic ones. +- Acknowledge differing viewpoints and accept constructive feedback gracefully. +- Take responsibility for mistakes; learn and move on. + +## Unacceptable behavior + +- Personal attacks, insults, or demeaning language. +- Harassment of any kind, in public or private channels. +- Discriminatory language or behavior. +- Publishing others' private information without consent. +- Disruptive behavior that derails productive discussion. +- Any conduct a reasonable person would consider inappropriate in a professional setting. + +## Scope + +This Code of Conduct applies to all project spaces — issues, pull requests, discussions, commit messages, and any other communication tied to the project. It also applies when an individual is representing the project in public spaces. + +## Enforcement + +If you experience or witness behavior that violates this Code of Conduct, please report it via [GitHub Security Advisories](https://github.com/offload-project/rrule/security/advisories/new) (which supports private reporting) or by opening a private channel with a maintainer. + +Maintainers will review reports promptly and respond with appropriate action, which may include: + +- A private warning +- A temporary ban from project spaces +- A permanent ban from project spaces + +Reports will be handled with discretion. The reporter's identity will be kept confidential unless they choose otherwise. + +## Maintainer responsibilities + +Maintainers are responsible for clarifying standards, responding to reports fairly, and applying enforcement consistently. Maintainers who do not uphold this Code of Conduct in good faith may face consequences as determined by other maintainers. + +## Attribution + +This Code of Conduct is inspired by the [Contributor Covenant](https://www.contributor-covenant.org/), adapted to be concise for this project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..a6ee89a17 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,165 @@ +# Contributing to rrule + +Thanks for your interest in contributing! This document outlines the process and standards for contributing to `@offload-project/rrule`. + +## Code of Conduct + +By participating in this project, you agree to treat fellow contributors with respect. Be kind, assume good intent, and keep discussions focused on the work. See the full [Code of Conduct](CODE_OF_CONDUCT.md). + +## Ways to Contribute + +- Reporting bugs via the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) template +- Proposing new features via the [Feature Request](.github/ISSUE_TEMPLATE/feature_request.md) template +- Improving documentation (`README.md`, `CHANGELOG.md`) +- Fixing bugs or implementing features through pull requests +- Reviewing open pull requests + +Before opening a large PR, please open an issue first to discuss the approach. + +## Requirements + +- Node **20.19+** +- [Bun](https://bun.sh/) (the project uses `bun.lock` and Bun-based scripts) + +## Getting Set Up + +1. Fork the repository on GitHub and clone your fork: + + ```bash + git clone git@github.com:/rrule.git + cd rrule + ``` + +2. Install dependencies: + + ```bash + bun i + ``` + +3. Create a feature branch off `main`: + + ```bash + git checkout -b feat/short-description + ``` + +## Development Workflow + +### Running the Test Suite + +```bash +bun run test +``` + +Tests are written with [Jest](https://jestjs.io/) and live under `test/`. New behavior should be covered by tests; bug fixes should include a regression test. + +### Linting and Formatting + +This project uses [Biome](https://biomejs.dev/) for linting and formatting. + +```bash +bun run lint # check only +bun run check # apply fixes +bun run format # format only +``` + +A Husky `pre-commit` hook runs `lint-staged` — PRs must pass Biome checks. + +### Building + +```bash +bun run build +``` + +This runs `tsc` for type-checking and then bundles via Vite into `dist/`. + +### Local Demo + +```bash +bun run dev +``` + +Starts the Vite demo app at the URL printed in the terminal. + +## Changesets + +User-facing changes need a changeset so the release workflow can pick them up. Run: + +```bash +bun run change +``` + +…and follow the prompts. The generated Markdown file under `.changeset/` should be committed with your PR. + +Tooling-only / docs-only changes that should **not** trigger a release can skip the changeset. + +## Commit Messages + +We use [Conventional Commits](https://www.conventionalcommits.org/). + +Format: `(): ` + +Common types used in this repo: + +| Type | Use for | +| ----------- | -------------------------------------------------- | +| `feat` | New user-facing functionality | +| `fix` | Bug fixes | +| `deprecate` | Marking existing API as deprecated | +| `refactor` | Internal change with no behavior difference | +| `test` | Adding or updating tests | +| `docs` | Documentation only | +| `chore` | Tooling, dependency bumps, repo housekeeping | +| `ci` | Changes to GitHub Actions workflows | + +Examples (from this project's history): + +- `feat: rrule and ruleset validate` +- `chore: drop version back, add changeset, add build` + +Breaking changes: add `!` after the type (e.g., `feat!: rename RRule.parseString`) and explain the migration path in the PR body. + +## Pull Requests + +1. Make sure your branch is up to date with `main`. +2. Run the full local check before pushing: + + ```bash + bun run lint && bun run test && bun run build + ``` + +3. Push your branch and open a PR against `main` using the [PR template](.github/pull_request_template.md). +4. Fill in: + - What changed and why + - Type of change (bug fix, feature, breaking, deprecation, etc.) + - How it was tested (Node version, runtime, timezone) + - Whether a changeset was added +5. Keep PRs focused. One logical change per PR makes review faster and bisection easier. +6. CI must pass before review: + - `test.yml` — Jest test suite + - `release.yml` — release pipeline (validated on `main`) +7. Address review feedback in additional commits rather than force-pushing while review is active. + +## Adding or Changing Features + +When working on this library, keep these areas in mind: + +- **RFC compliance** — `rrule` implements [RFC 5545](https://tools.ietf.org/html/rfc5545). Document any intentional deviations in the README's "Differences From iCalendar RFC" section. +- **Public API** — `RRule`, `RRuleSet`, `rrulestr`, `validate`, `datetime`, and the exported types are part of the public contract. Renames or signature changes are breaking; deprecate first when possible. +- **Timezones** — date handling is the trickiest part of this library. Read the [Timezone Support](README.md#timezone-support) section of the README before changing anything that touches `tzid`, `dtstart`, or occurrence generation. +- **Natural-language text** — additions to `toText` / `fromText` should keep the existing language structure (see `src/nlp/`) and add tests in `test/`. +- **Caching** — `RRule` and `RRuleSet` cache results unless `noCache` is set. New occurrence-retrieval methods should respect this. + +## Documentation + +If your change affects public API, configuration, or usage, update: + +- `README.md` — quick start, API reference, examples +- `CHANGELOG.md` — generated from changesets at release time; you only need to write the changeset + +## Reporting Security Issues + +Please do **not** open a public issue for security vulnerabilities. See [SECURITY.md](SECURITY.md) for the private reporting process. + +## License + +By contributing, you agree that your contributions will be licensed under the [BSD-3-Clause License](LICENSE.md) that covers this project. diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/README.md b/README.md index 4357793e3..d1103edab 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,71 @@ -# rrule.js - -**Library for working with recurrence rules for calendar dates.** - -

- version - GitHub Actions Release Workflow Status - GitHub Actions Test Workflow Status +

+ Latest Version + GitHub Tests Action Status + GitHub Release Action Status

-rrule.js supports recurrence rules as defined in the [iCalendar RFC](https://tools.ietf.org/html/rfc5545), with a few -important [differences](#differences-from-icalendar-rfc). It is a partial port of the `rrule` module from the excellent -[python-dateutil](http://labix.org/python-dateutil/) library. On top of that, it supports parsing and serialization of -recurrence rules from and to natural language. - ---- - -### Requirements - -Node 20.19+ - -### Quick Start - -- [Demo app](http://offload-project.github.io/rrule/) - -#### Client Side - -```bash -$ bun i @offload-project/rrule -``` - -#### Server Side +# rrule.js -Includes TypeScript types. +A JavaScript / TypeScript library for working with recurrence rules for calendar dates, as defined in the [iCalendar RFC (RFC 5545)](https://tools.ietf.org/html/rfc5545). A partial port of the `rrule` module from [python-dateutil](http://labix.org/python-dateutil/), with parsing and serialization to and from natural language on top. + +## Features + +- **RFC 5545 recurrence rules** — parse, serialize, and iterate `RRULE` strings and option objects +- **`RRuleSet`** — combine multiple `RRULE`, `RDATE`, `EXRULE`, and `EXDATE` entries into a single recurrence set +- **Occurrence retrieval** — `.all()`, `.between()`, `.before()`, `.after()`, with optional iterator-based early termination +- **Natural-language text** — `toText()` / `fromText()` for human-friendly rule descriptions +- **Timezone support** — `TZID` parameter handling via the [Intl API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) +- **Validation** — `validate()` returns a structured result without throwing +- **Result caching** — enabled by default; configurable via `noCache` +- **Bundled TypeScript types** — works out of the box in TS projects + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) + - [RRule](#rrule) + - [RRuleSet](#rruleset) + - [rrulestr](#rrulestr) + - [validate](#validate) +- [Important: Use UTC dates](#important-use-utc-dates) +- [Timezone Support](#timezone-support) +- [API](#api) + - [`RRule` Constructor](#rrule-constructor) + - [Instance properties](#instance-properties) + - [Occurrence Retrieval Methods](#occurrence-retrieval-methods) + - [iCalendar RFC String Methods](#icalendar-rfc-string-methods) + - [Natural Language Text Methods](#natural-language-text-methods) + - [`RRuleSet` Constructor](#rruleset-constructor) + - [`rrulestr` Function](#rrulestr-function) + - [`validate` Function](#validate-function) +- [Differences From iCalendar RFC](#differences-from-icalendar-rfc) +- [Demo App](#demo-app) +- [Authors](#authors) +- [Contributing](#contributing) +- [Security](#security) +- [License](#license) + +## Requirements + +- Node **20.19+** + +## Installation ```bash -$ bun i @offload-project/rrule +bun i @offload-project/rrule # or -$ npm install @offload-project/rrule +npm install @offload-project/rrule ``` -#### Usage +TypeScript types are bundled — no separate `@types` install needed. -**RRule:** +## Quick Start + +### RRule ```js -import { datetime, RRule, RRuleSet, rrulestr, validate } from 'rrule' +import { datetime, RRule, RRuleSet, rrulestr, validate } from '@offload-project/rrule' // Create a rule: const rule = new RRule({ @@ -83,7 +104,7 @@ rule.toText() // "every 5 weeks on Monday, Friday until December 31, 2012, starting February 1, 2012" ``` -**RRuleSet:** +### RRuleSet ```js const rruleSet = new RRuleSet() @@ -143,7 +164,7 @@ rruleSet.toString() // "DTSTART:20120201T023000Z\nRRULE:FREQ=MONTHLY;COUNT=5\nRDATE:20120701T023000Z,20120702T023000Z\nEXRULE:FREQ=MONTHLY;COUNT=2\nEXDATE:20120601T023000Z" ``` -**rrulestr:** +### rrulestr ```js // Parse a RRule string, return a RRule object @@ -160,7 +181,7 @@ rrulestr( ) ``` -**validate:** +### validate ```js import { validate } from '@offload-project/rrule' @@ -173,21 +194,21 @@ validate('RRULE:FREQ=BOGUS') // { valid: false, error: { message: 'Invalid frequency: ...', cause: Error } } ``` -### Important: Use UTC dates +## Important: Use UTC dates -Dates in JavaScript are tricky. `RRule` tries to support as much flexibility as possible without adding any large +Dates in JavaScript are tricky. `RRule` tries to support as much flexibility as possible without adding any large required 3rd party dependencies, but that means we also have some special rules. -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. +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: Returned "UTC" dates are always meant to be interpreted as dates in your local timezone. This may +**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 +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: @@ -230,13 +251,11 @@ rule.all().map(date => For more examples see [python-dateutil](http://labix.org/python-dateutil/) documentation. ---- - -### Timezone Support +## Timezone Support -Rrule also supports use of the `TZID` parameter in the [RFC](https://tools.ietf.org/html/rfc5545#section-3.2.19) using +Rrule also supports use of the `TZID` parameter in the [RFC](https://tools.ietf.org/html/rfc5545#section-3.2.19) using the [Intl API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). -Support matrix for the Intl API applies. If you need to support additional environments, please consider using a +Support matrix for the Intl API applies. If you need to support additional environments, please consider using a [polyfill](https://formatjs.io/docs/polyfills/). Example with `TZID`: @@ -252,7 +271,7 @@ new RRule({ // which is the time in Los Angeles when it's 2018-02-01T10:30:00 in Tokyo. ``` -Whether or not you use the `TZID` param, make sure to only use JS `Date` objects that are represented in UTC to avoid +Whether or not you use the `TZID` param, make sure to only use JS `Date` objects that are represented in UTC to avoid unexpected timezone offsets being applied, for example: ```js @@ -273,15 +292,15 @@ new RRule({ // ['2018-02-01T10:30:00.000Z', '2018-03-01T10:30:00.000Z'] ``` -### API +## API -#### `RRule` Constructor +### `RRule` Constructor ```js new RRule(options[, noCache=false]) ``` -The `options` argument mostly corresponds to the properties defined for `RRULE` in the iCalendar RFC. Only `freq` is +The `options` argument mostly corresponds to the properties defined for `RRULE` in the iCalendar RFC. Only `freq` is required. @@ -319,7 +338,7 @@ required. @@ -340,7 +359,7 @@ required. @@ -348,88 +367,88 @@ required. @@ -437,20 +456,18 @@ required.
interval - The interval between each freq iteration. For example, when using RRule.YEARLY, an interval of + The interval between each freq iteration. For example, when using RRule.YEARLY, an interval of 2 means once every two years, but with RRule.HOURLY, it means once every two hours. The default interval is 1. @@ -328,8 +347,8 @@ required.
wkst - The week start day. Must be one of the RRule.MO, RRule.TU, RRule.WE - constants, or an integer, specifying the first day of the week. This will affect recurrences based on weekly + The week start day. Must be one of the RRule.MO, RRule.TU, RRule.WE + constants, or an integer, specifying the first day of the week. This will affect recurrences based on weekly periods. The default week start is RRule.MO.
until - If given, this must be a Date instance, that will specify the limit of the recurrence. If a + If given, this must be a Date instance, that will specify the limit of the recurrence. If a recurrence instance happens to be the same as the Date instance given in the until argument, this will be the last occurrence.
tzid - If given, this must be a IANA string recognized by the Intl API. See discussion under + If given, this must be a IANA string recognized by the Intl API. See discussion under Timezone support.
bysetpos - If given, it must be either an integer, or an array of integers, positive or negative. Each given integer - will specify an occurrence number, corresponding to the nth occurrence of the rule inside the frequency + If given, it must be either an integer, or an array of integers, positive or negative. Each given integer + will specify an occurrence number, corresponding to the nth occurrence of the rule inside the frequency period. For example, a bysetpos of -1 if combined with a RRule.MONTHLY - frequency, and a byweekday of (RRule.MO, RRule.TU, RRule.WE, + frequency, and a byweekday of (RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR), will result in the last work day of every month.
bymonth - If given, it must be either an integer, or an array of integers, meaning the months to apply the recurrence + If given, it must be either an integer, or an array of integers, meaning the months to apply the recurrence to.
bymonthday - If given, it must be either an integer, or an array of integers, meaning the month days to apply the + If given, it must be either an integer, or an array of integers, meaning the month days to apply the recurrence to.
byyearday - If given, it must be either an integer, or an array of integers, meaning the year days to apply the + If given, it must be either an integer, or an array of integers, meaning the year days to apply the recurrence to.
byweekno - If given, it must be either an integer, or an array of integers, meaning the week numbers to apply the - recurrence to. Week numbers have the meaning described in ISO8601, that is, the first week of the year is + If given, it must be either an integer, or an array of integers, meaning the week numbers to apply the + recurrence to. Week numbers have the meaning described in ISO8601, that is, the first week of the year is that containing at least four days of the new year.
byweekday - If given, it must be either an integer (0 == RRule.MO), an array of integers, one of the + If given, it must be either an integer (0 == RRule.MO), an array of integers, one of the weekday constants (RRule.MO, RRule.TU, etc), or an array of these constants. When - given, these variables will define the weekdays where the recurrence will be applied. It's also possible to - use an argument n for the weekday instances, which will mean the nth occurrence of this weekday in the - period. For example, with RRule.MONTHLY, or with RRule.YEARLY and - BYMONTH, using RRule.FR.nth(+1) or RRule.FR.nth(-1) in + given, these variables will define the weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth occurrence of this weekday in the + period. For example, with RRule.MONTHLY, or with RRule.YEARLY and + BYMONTH, using RRule.FR.nth(+1) or RRule.FR.nth(-1) in byweekday will specify the first or last friday of the month where the recurrence happens. - Notice that the RFC documentation, this is specified as BYDAY, but was renamed to avoid the + Notice that the RFC documentation, this is specified as BYDAY, but was renamed to avoid the ambiguity of that argument.
byhour - If given, it must be either an integer, or an array of integers, meaning the hours to apply the recurrence + If given, it must be either an integer, or an array of integers, meaning the hours to apply the recurrence to.
byminute - If given, it must be either an integer, or an array of integers, meaning the minutes to apply the recurrence + If given, it must be either an integer, or an array of integers, meaning the minutes to apply the recurrence to.
bysecond - If given, it must be either an integer, or an array of integers, meaning the seconds to apply the recurrence + If given, it must be either an integer, or an array of integers, meaning the seconds to apply the recurrence to.
byeaster - This is an extension to the RFC specification which the Python implementation provides. The value is a + This is an extension to the RFC specification which the Python implementation provides. The value is a number of days relative to Easter Sunday. For example, byeaster: 0 is Easter itself, byeaster: 1 is the day after Easter, and byeaster: -2 is Good Friday.
-`noCache`: Set to `true` to disable caching of results. If you use the same rrule instance multiple times, enabling +`noCache`: Set to `true` to disable caching of results. If you use the same rrule instance multiple times, enabling caching will improve the performance considerably. Enabled by default. See also [python-dateutil](http://labix.org/python-dateutil/) documentation. ---- - -#### Instance properties +### Instance properties
rule.options
Processed options applied to the rule. Includes default options (such as wkst). Currently, - rule.options.byweekday isn't equal to rule.origOptions.byweekday (which is an + rule.options.byweekday isn't equal to rule.origOptions.byweekday (which is an inconsistency).
rule.origOptions
@@ -459,19 +476,17 @@ See also [python-dateutil](http://labix.org/python-dateutil/) documentation.
---- - -#### Occurrence Retrieval Methods +### Occurrence Retrieval Methods -##### `RRule.prototype.all([iterator])` +#### `RRule.prototype.all([iterator])` -Returns all dates matching the rule. It is a replacement for the iterator protocol this class implements in the Python +Returns all dates matching the rule. It is a replacement for the iterator protocol this class implements in the Python version. -As rules without `until` or `count` represent infinite date series, you can optionally pass `iterator`, which is a -function that is called for each date matched by the rule. It gets two parameters `date` (the `Date` instance being -added), and `i` (zero-indexed position of `date` in the result). Dates are being added to the result as long as the -iterator returns `true`. If a `false`-y value is returned, `date` isn't added to the result and the iteration is +As rules without `until` or `count` represent infinite date series, you can optionally pass `iterator`, which is a +function that is called for each date matched by the rule. It gets two parameters `date` (the `Date` instance being +added), and `i` (zero-indexed position of `date` in the result). Dates are being added to the result as long as the +iterator returns `true`. If a `false`-y value is returned, `date` isn't added to the result and the iteration is interrupted (possibly prematurely). ```js @@ -487,10 +502,10 @@ rule.all((date, i) => i < 2) ['2012-02-01T10:30:00.000Z', '2012-05-01T10:30:00.000Z'] ``` -##### `RRule.prototype.between(after, before, inc=false [, iterator])` +#### `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 themselves occurrences. With `inc == true`, they +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. Optional `iterator` has the same function as it has with `RRule.prototype.all()`. @@ -500,23 +515,21 @@ rule.between(datetime(2012, 8, 1), datetime(2012, 9, 1)) ['2012-08-27T10:30:00.000Z', '2012-08-31T10:30:00.000Z'] ``` -##### `RRule.prototype.before(dt, inc=false)` +#### `RRule.prototype.before(dt, inc=false)` -Returns the last recurrence before the given `Date` instance. The `inc` argument defines what happens if `dt` is an +Returns the last recurrence before the given `Date` instance. The `inc` argument defines what happens if `dt` is an occurrence. With `inc == true`, if `dt` itself is an occurrence, it will be returned. -##### `RRule.prototype.after(dt, inc=false)` +#### `RRule.prototype.after(dt, inc=false)` -Returns the first recurrence after the given `Date` instance. The `inc` argument defines what happens if `dt` is an +Returns the first recurrence after the given `Date` instance. The `inc` argument defines what happens if `dt` is an occurrence. With `inc == true`, if `dt` itself is an occurrence, it will be returned. See also [python-dateutil](http://labix.org/python-dateutil/) documentation. ---- - -#### iCalendar RFC String Methods +### iCalendar RFC String Methods -##### `RRule.prototype.toString()` +#### `RRule.prototype.toString()` Returns a string representation of the rule as per the iCalendar RFC. Only properties explicitly specified in `options` are included: @@ -529,7 +542,7 @@ rule.toString() == RRule.optionsToString(rule.origOptions) // true ``` -##### `RRule.optionsToString(options)` +#### `RRule.optionsToString(options)` Converts `options` to iCalendar RFC `RRULE` string: @@ -547,7 +560,7 @@ RRule.optionsToString({ // "DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;" ``` -##### `RRule.fromString(rfcString)` +#### `RRule.fromString(rfcString)` Constructs an `RRule` instance from a complete `rfcString`: @@ -560,7 +573,7 @@ const rule = new RRule( ) ``` -##### `RRule.parseString(rfcString)` +#### `RRule.parseString(rfcString)` Only parse RFC string and return `options`. @@ -570,17 +583,15 @@ options.dtstart = datetime(2000, 2, 1) const rule = new RRule(options) ``` ---- +### Natural Language Text Methods -#### Natural Language Text Methods - -These methods provide an incomplete support for text-to-`RRule` and `RRule`-to-text conversion. You should test them +These methods provide an incomplete support for text-to-`RRule` and `RRule`-to-text conversion. You should test them with your input to see whether the result is acceptable. -##### `RRule.prototype.toText([gettext, [language, [dateFormatter]]])` +#### `RRule.prototype.toText([gettext, [language, [dateFormatter]]])` -Returns a textual representation of `rule`. The `gettext` callback, if provided, will be called for each text token and -its return value used instead. The optional `language` argument is a language definition to be used (defaults to +Returns a textual representation of `rule`. The `gettext` callback, if provided, will be called for each text token and +its return value used instead. The optional `language` argument is a language definition to be used (defaults to `ENGLISH`). The optional `dateFormatter` callback controls how dates (for `until` and `starting` clauses) are formatted. If `dtstart` is explicitly set in the rule options, it will be included as a ", starting [date]" suffix in the output. @@ -603,11 +614,11 @@ rule2.toText() // "every 2 months on the 15th, starting February 18, 2026" ``` -##### `RRule.prototype.isFullyConvertibleToText()` +#### `RRule.prototype.isFullyConvertibleToText()` Provides a hint on whether all the options the rule has can be converted to text. -##### `RRule.fromText(text[, language])` +#### `RRule.fromText(text[, language])` Constructs an `RRule` instance from `text`. @@ -615,7 +626,7 @@ Constructs an `RRule` instance from `text`. const rule = RRule.fromText('every day for 3 times') ``` -##### `RRule.parseText(text[, language])` +#### `RRule.parseText(text[, language])` Parse `text` into `options`: @@ -626,90 +637,86 @@ options.dtstart = datetime(2000, 2, 1) const rule = new RRule(options) ``` ---- - -#### `RRuleSet` Constructor +### `RRuleSet` Constructor ```js new RRuleSet([(noCache = false)]) ``` -The `RRuleSet` instance allows more complex recurrence setups, mixing multiple rules, dates, exclusion rules, and +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, improving performance of multiple queries +Default `noCache` argument is `false`, caching of results will be enabled, improving performance of multiple queries considerably. -##### `RRuleSet.prototype.rrule(rrule)` +#### `RRuleSet.prototype.rrule(rrule)` Include the given `rrule` instance in the recurrence set generation. -##### `RRuleSet.prototype.rdate(dt)` +#### `RRuleSet.prototype.rdate(dt)` Include the given datetime instance `dt` in the recurrence set generation. -##### `RRuleSet.prototype.exrule(rrule)` +#### `RRuleSet.prototype.exrule(rrule)` -Include the given `rrule` instance in the recurrence set exclusion list. Dates which are part of the given recurrence +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 in RFC 5545](https://icalendar.org/iCalendar-RFC-5545/a-3-deprecated-features.html) and does not support a `DTSTART` property. -##### `RRuleSet.prototype.exdate(dt)` +#### `RRuleSet.prototype.exdate(dt)` -Include the given datetime instance `dt` in the recurrence set exclusion list. Dates included that way will not be +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.dtstart(dt?)` +#### `RRuleSet.prototype.dtstart(dt?)` Sets or gets the start date for the recurrence set. -##### `RRuleSet.prototype.tzid(tz?)` +#### `RRuleSet.prototype.tzid(tz?)` Sets or gets the timezone identifier. Useful if there are no rrules in this `RRuleSet` and thus no `DTSTART`. -##### `RRuleSet.prototype.all([iterator])` +#### `RRuleSet.prototype.all([iterator])` Same as `RRule.prototype.all`. -##### `RRuleSet.prototype.between(after, before, inc=false [, iterator])` +#### `RRuleSet.prototype.between(after, before, inc=false [, iterator])` Same as `RRule.prototype.between`. -##### `RRuleSet.prototype.before(dt, inc=false)` +#### `RRuleSet.prototype.before(dt, inc=false)` Same as `RRule.prototype.before`. -##### `RRuleSet.prototype.after(dt, inc=false)` +#### `RRuleSet.prototype.after(dt, inc=false)` Same as `RRule.prototype.after`. -##### `RRuleSet.prototype.rrules()` +#### `RRuleSet.prototype.rrules()` Get list of included rrules in this recurrence set. -##### `RRuleSet.prototype.exrules()` +#### `RRuleSet.prototype.exrules()` Get list of excluded rrules in this recurrence set. -##### `RRuleSet.prototype.rdates()` +#### `RRuleSet.prototype.rdates()` Get list of included datetimes in this recurrence set. -##### `RRuleSet.prototype.exdates()` +#### `RRuleSet.prototype.exdates()` Get list of excluded datetimes in this recurrence set. ---- - -#### `rrulestr` Function +### `rrulestr` Function ```js rrulestr(rruleStr[, options]) ``` -The `rrulestr` function is a parser for RFC-like syntaxes. The string passed as parameter may be a multiple line string, +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. Additionally, it accepts the following keyword arguments: @@ -723,30 +730,30 @@ Additionally, it accepts the following keyword arguments:
dtstart
- If given, it must be a datetime instance that will be used when no DTSTART property is found in the + 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 + 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. @@ -754,9 +761,7 @@ Additionally, it accepts the following keyword arguments:
---- - -#### `validate` Function +### `validate` Function ```js validate(rruleStr[, options]) @@ -808,17 +813,15 @@ interface ValidationError { type ValidationResult = ValidationSuccess | ValidationError ``` ---- +## Differences From iCalendar RFC -### Differences From iCalendar RFC - -- `RRule` has no `byday` keyword. The equivalent keyword has been replaced by the `byweekday` keyword, to remove the +- `RRule` has no `byday` keyword. The equivalent keyword has been replaced by the `byweekday` keyword, to remove the ambiguity present in the original keyword. -- Unlike documented in the RFC, the starting datetime, `dtstart`, is not the first recurrence instance, unless it does - fit in the specified rules. - This is in part due to this project being a port of - [python-dateutil](https://labix.org/python-dateutil#head-a65103993a21b717f6702063f3717e6e75b4ba66), which has the - same non-compliant functionality. Note that you can get the original behavior by using a `RRuleSet` and adding the +- Unlike documented in the RFC, the starting datetime, `dtstart`, is not the first recurrence instance, unless it does + fit in the specified rules. + This is in part due to this project being a port of + [python-dateutil](https://labix.org/python-dateutil#head-a65103993a21b717f6702063f3717e6e75b4ba66), which has the + same non-compliant functionality. Note that you can get the original behavior by using a `RRuleSet` and adding the `dtstart` as an `rdate`. ```js @@ -838,39 +841,33 @@ rruleSet.rrule( rruleSet.rdate(start) ``` -- Unlike documented in the RFC, every keyword is valid on every frequency. (The RFC documents that `byweekno` is only +- 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 +## Demo App -rrule.js is implemented in TypeScript. It uses [Biome](https://biomejs.dev/) for linting and formatting. +- [Live demo](http://offload-project.github.io/rrule/) -To run the code, checkout this repository and run: +## Authors -``` -$ bun i -``` +- [Shavonn Brown](http://www.offloadproject.com) +- [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)) -To run the tests, run: +Python `dateutil` is written by [Gustavo Niemeyer](http://niemeyer.net). -``` -$ bun run test -``` +## Contributing -To build files for distribution, run: +Contributions are welcome! Please see the documents below before getting started. -``` -$ bun run build -``` +- [Contributing Guide](CONTRIBUTING.md) — setup, workflow, commit conventions, and PR process +- [Code of Conduct](CODE_OF_CONDUCT.md) — expectations for participation in this project -#### Authors +## Security -- [Shavonn Brown](http://www.offloadproject.com) -- [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)) +- [Security Policy](SECURITY.md) — how to report a vulnerability privately -Python `dateutil` is written by [Gustavo Niemeyer](http://niemeyer.net). +## License -See [LICENSE](https://github.com/offload-project/rrule/blob/main/LICENSE) for more details. +BSD-3-Clause. Please see [License File](LICENSE.md) for more information. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cdbbd74fd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,59 @@ +# Security Policy + +## Supported versions + +Security fixes are applied to the latest minor release line. Older minor versions may receive fixes for critical issues at the maintainers' discretion — when in doubt, please upgrade. + +| Version | Supported | +| ------------- | ---------------------- | +| `2.9.x` | ✅ | +| `2.x` (older) | ⚠️ critical fixes only | +| `< 2.0` | ❌ (please upgrade) | + +## Reporting a vulnerability + +**Please do not open a public GitHub issue for security reports.** + +Use [GitHub Security Advisories](https://github.com/offload-project/rrule/security/advisories/new) to report privately. This lets us discuss, fix, and coordinate disclosure before details become public. + +When reporting, please include: + +- A description of the issue and its potential impact. +- Steps to reproduce, or a minimal proof-of-concept (RRULE string, code snippet, input that triggers the issue). +- Affected `@offload-project/rrule` version(s), Node version, and runtime (Node / Bun / browser). +- Any suggested fix or mitigation (optional). + +## Response expectations + +- **Acknowledgement:** within 5 business days. +- **Initial assessment:** within 10 business days. +- **Fix timeline:** depends on severity. Critical issues get prioritized; lower-severity issues may be batched into the next regular release. + +We'll keep you updated on progress and credit you in the advisory unless you'd prefer to stay anonymous. + +## Scope + +Things in scope for this project: + +- Vulnerabilities in any code published under `@offload-project/rrule` (the `RRule`, `RRuleSet`, `rrulestr`, `validate`, NLP, and parsing modules). +- Parser issues that allow crafted RRULE / RDATE / EXDATE input to cause unbounded resource consumption (CPU, memory) — e.g., catastrophic backtracking, runaway occurrence generation that cannot be aborted. +- Prototype-pollution or unsafe property-access issues in options parsing. +- Incorrect occurrence generation that could be exploited downstream (e.g., scheduling logic relying on `rrule` skipping or producing dates it shouldn't). +- Cross-site scripting risk in any HTML or text output produced by the library (e.g., `toText`, demo app under `index.html`). + +Things **not** in scope (please report upstream or with the relevant project): + +- Vulnerabilities in Node, Bun, Vite, Biome, Jest, or other dev/runtime dependencies — please file with the respective project. +- Application-level misconfiguration in a consuming app (e.g., trusting unvalidated user input as an RRULE string without rate-limiting, or rendering `toText` output without escaping in a context that requires it). +- Timezone or DST quirks that result from documented behavior in the [Timezone Support](README.md#timezone-support) section of the README. If you believe the documented behavior is itself the vulnerability, please explain. +- Issues caused by user-supplied implementations of the library's extension points (custom `gettext`, custom `dateFormatter`, custom `Language` objects). + +## Disclosure + +Once a fix is published, we will: + +1. Publish a GitHub Security Advisory with details and credit. +2. Tag a patch release. +3. Update the changelog with a brief mention (without exploit details prior to the disclosure window). + +Thanks for helping keep the project and its users safe.