{"id":49394712,"url":"https://github.com/rrulenet/recurrence","last_synced_at":"2026-04-28T15:02:51.194Z","repository":{"id":353627264,"uuid":"1217752394","full_name":"rrulenet/recurrence","owner":"rrulenet","description":"Temporal-first recurrence API for rules, composed schedules, and RFC 5545 parsing.","archived":false,"fork":false,"pushed_at":"2026-04-24T17:48:37.000Z","size":59,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-24T19:29:42.611Z","etag":null,"topics":["calendar","deno","icalendar","recurrence","rfc5545","rrule","scheduling","temporal","typescript"],"latest_commit_sha":null,"homepage":"https://rrule.net","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rrulenet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-22T07:23:37.000Z","updated_at":"2026-04-24T17:48:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rrulenet/recurrence","commit_stats":null,"previous_names":["rrulenet/recurrence"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/rrulenet/recurrence","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrulenet%2Frecurrence","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrulenet%2Frecurrence/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrulenet%2Frecurrence/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrulenet%2Frecurrence/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rrulenet","download_url":"https://codeload.github.com/rrulenet/recurrence/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrulenet%2Frecurrence/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32385943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T14:34:11.604Z","status":"ssl_error","status_checked_at":"2026-04-28T14:32:37.009Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["calendar","deno","icalendar","recurrence","rfc5545","rrule","scheduling","temporal","typescript"],"created_at":"2026-04-28T15:02:45.843Z","updated_at":"2026-04-28T15:02:51.179Z","avatar_url":"https://github.com/rrulenet.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://rrule.net\"\u003e\n    \u003cimg src=\"./assets/avatar.svg\" alt=\"rrule.net\" width=\"96\" height=\"96\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003e@rrulenet/recurrence\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  Temporal-first recurrence API for rules, composed schedules, and RFC 5545 parsing.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://rrule.net\"\u003errule.net\u003c/a\u003e •\n  \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal\"\u003eTemporal API\u003c/a\u003e •\n  \u003cstrong\u003e@rrulenet ecosystem\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ccode\u003e@rrulenet/rrule\u003c/code\u003e ·\n  \u003ccode\u003e@rrulenet/recurrence\u003c/code\u003e ·\n  \u003ccode\u003e@rrulenet/core\u003c/code\u003e ·\n  \u003ccode\u003e@rrulenet/cli\u003c/code\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@rrulenet/recurrence\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/%40rrulenet%2Frecurrence\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://jsr.io/@rrulenet/recurrence\"\u003e\u003cimg src=\"https://img.shields.io/jsr/v/%40rrulenet%2Frecurrence\" alt=\"JSR version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://rrulenet.github.io/recurrence/coverage.json\"\u003e\u003cimg src=\"https://img.shields.io/endpoint?url=https://rrulenet.github.io/recurrence/coverage.json\" alt=\"Coverage\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-MIT-2563EB\" alt=\"MIT License\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003csub\u003e\u003cstrong\u003e@rrulenet/rrule\u003c/strong\u003e: classic API · \u003cstrong\u003e@rrulenet/recurrence\u003c/strong\u003e: Temporal-first API · \u003cstrong\u003e@rrulenet/core\u003c/strong\u003e: engine · \u003cstrong\u003e@rrulenet/cli\u003c/strong\u003e: workflows\u003c/sub\u003e\n\u003c/p\u003e\n\n`@rrulenet/recurrence` is the Temporal-first package in the `@rrulenet` ecosystem. It is designed for applications that want one recurrence type, direct support for `Temporal.Instant` and `Temporal.ZonedDateTime`, first-class set algebra, and RFC 5545 parsing and serialization where possible.\n\nUse `@rrulenet/recurrence` when your application boundary is already Temporal-oriented. Use `@rrulenet/rrule` when you want the classic `rrule.js`-style API.\n\n## Table of Contents\n\n- [Install](#install)\n- [Getting Started](#getting-started)\n- [Why Recurrence](#why-recurrence)\n- [API](#api)\n  - [Recurrence](#recurrence)\n  - [Recurrence.parse(options) and parse(options)](#recurrenceparseoptions-and-parseoptions)\n  - [Recurrence.rule(options) and rule(options)](#recurrenceruleoptions-and-ruleoptions)\n  - [Recurrence.dates(values, options?)](#recurrencedatesvalues-options)\n  - [Query Methods](#query-methods)\n  - [Algebra Methods](#algebra-methods)\n  - [Presentation and Serialization](#presentation-and-serialization)\n- [Constructor Input Shape](#constructor-input-shape)\n- [Examples](#examples)\n- [Error Handling](#error-handling)\n- [Development](#development)\n\n## Install\n\n```bash\nnpm install @rrulenet/recurrence\n```\n\nIf your runtime does not yet provide the [Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), install a polyfill in your application:\n\n```bash\nnpm install temporal-polyfill\n```\n\n```bash\nnpm install @js-temporal/polyfill\n```\n\nPolyfill projects:\n- [`temporal-polyfill`](https://github.com/fullcalendar/temporal-polyfill)\n- [`@js-temporal/polyfill`](https://github.com/js-temporal/temporal-polyfill)\n\n## Getting Started\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = Recurrence.rule({\n  freq: 'DAILY',\n  count: 3,\n  byHour: [9],\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00+01:00[Europe/Paris]'),\n});\n\nconsole.log(recurrence.all().map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-02T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-03T09:00:00+01:00[Europe/Paris]'\n// ]\n```\n\n## Why Recurrence\n\n`@rrulenet/recurrence` revolves around a single public type: `Recurrence`.\n\nThat matters because many real schedules are not just one rule. For example:\n\n```text\nEvery weekday at 9:00 and weekends at 10:00\n```\n\nThis package models that directly as one recurrence expression instead of forcing a split between \"rule\" and \"rule set\". That makes the API easier to compose, easier to test, and better suited to programmatic generation by applications, CLIs, or agents.\n\n## API\n\n```ts\nimport {\n  Recurrence,\n  parse,\n  rule,\n  TEMPORAL_ERROR_CODES,\n  TemporalApiError,\n} from '@rrulenet/recurrence';\n\nimport type {\n  RecurrenceJson,\n  RecurrenceJsonEntry,\n  RecurrenceJsonRuleInput,\n} from '@rrulenet/recurrence';\n```\n\nMain exports:\n- `Recurrence`\n- `parse(options)`\n- `rule(options)`\n- `TEMPORAL_ERROR_CODES`\n- `TemporalApiError`\n\nType exports:\n- `RecurrenceJson`\n- `RecurrenceJsonEntry`\n- `RecurrenceJsonRuleInput`\n\n### `Recurrence`\n\n`Recurrence` is the central type. It supports:\n- construction from a canonical object shape\n- parsing RFC strings\n- creating simple rules\n- creating explicit date-only recurrences\n- querying occurrences\n- combining, intersecting, and subtracting recurrence expressions\n- text and RFC serialization\n\n### `Recurrence.parse(options)` and `parse(options)`\n\nParse an RFC 5545 recurrence string into a `Recurrence`.\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = Recurrence.parse({\n  rruleString: 'RRULE:FREQ=DAILY;COUNT=2;BYHOUR=9',\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00+01:00[Europe/Paris]'),\n});\n\nconsole.log(recurrence.all().map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-02T09:00:00+01:00[Europe/Paris]'\n// ]\n// The parsed recurrence yields ZonedDateTime values in the resolved timezone.\n```\n\nNotes:\n- `rruleString` is required\n- `start` can be a `Date`, `Temporal.Instant`, or `Temporal.ZonedDateTime`\n- if `start` is a `Temporal.ZonedDateTime`, its timezone is inferred automatically\n- inline `DTSTART` information inside the string remains authoritative\n\nThe top-level `parse(options)` export is a convenience alias for `Recurrence.parse(options)`.\n\n### `Recurrence.rule(options)` and `rule(options)`\n\nCreate a simple rule and get back a `Recurrence`.\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { rule } from '@rrulenet/recurrence';\n\nconst recurrence = rule({\n  freq: 'WEEKLY',\n  byDay: ['MO', 'WE', 'FR'],\n  byHour: [9],\n  count: 5,\n  start: Temporal.Instant.from('2025-01-01T08:00:00Z'),\n  tzid: 'Europe/Paris',\n});\n\nconsole.log(recurrence.first()?.toString());\n// '2025-01-01T09:00:00+01:00[Europe/Paris]'\n\nconsole.log(recurrence.take(3).map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-03T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-06T09:00:00+01:00[Europe/Paris]'\n// ]\n```\n\nThis is sugar for creating a `Recurrence` with one included rule. Use it when your schedule is a single rule and you want the shortest entry point.\n\n### `Recurrence.dates(values, options?)`\n\nCreate a `Recurrence` from explicit dates only.\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = Recurrence.dates([\n  Temporal.Instant.from('2025-05-01T09:00:00Z'),\n  Temporal.Instant.from('2025-05-08T09:00:00Z'),\n], {\n  tzid: 'Europe/Paris',\n});\n\nconsole.log(recurrence.all().map((value) =\u003e value.toString()));\n// [\n//   '2025-05-01T11:00:00+02:00[Europe/Paris]',\n//   '2025-05-08T11:00:00+02:00[Europe/Paris]'\n// ]\n```\n\nThis is useful for holidays, one-off exceptions, or explicit include/exclude lists.\n\n### Query Methods\n\nAll query methods return `Temporal.ZonedDateTime` values.\n\n#### `recurrence.all(iterator?)`\n\n```js\nconst values = recurrence.all();\nconsole.log(values.map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-03T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-06T09:00:00+01:00[Europe/Paris]',\n//   ...\n// ]\n```\n\nYou can also provide an iterator to stop early:\n\n```js\nconst firstThree = recurrence.all((value, index) =\u003e index \u003c 3);\nconsole.log(firstThree.map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-03T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-06T09:00:00+01:00[Europe/Paris]'\n// ]\n```\n\n#### `recurrence.between(after, before, inc = false, iterator?)`\n\n```js\nconst values = recurrence.between(\n  Temporal.Instant.from('2025-01-01T00:00:00Z'),\n  Temporal.Instant.from('2025-01-31T23:59:59Z'),\n  true,\n);\n\nconsole.log(values.map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-03T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-06T09:00:00+01:00[Europe/Paris]',\n//   ...\n// ]\n```\n\n#### `recurrence.after(date, inc = false)`\n\n```js\nconst next = recurrence.after(Temporal.Instant.from('2025-01-15T12:00:00Z'));\n// First occurrence strictly after the boundary by default\n```\n\n#### `recurrence.before(date, inc = false)`\n\n```js\nconst previous = recurrence.before(Temporal.Instant.from('2025-01-15T12:00:00Z'));\n// Last occurrence strictly before the boundary by default\n```\n\n#### `recurrence.first()`\n\nReturn the first occurrence, or `null` if none exists.\n\n```js\nconst first = recurrence.first();\n// Shortcut for the first occurrence in the series\n```\n\n#### `recurrence.take(count)`\n\nReturn the first `count` occurrences.\n\n```js\nconst preview = recurrence.take(5);\n\nconsole.log(preview.map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-03T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-06T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-08T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-10T09:00:00+01:00[Europe/Paris]'\n// ]\n```\n\n#### `recurrence.takeAfter(date, count, inc = false)`\n\nReturn the next `count` occurrences after a boundary.\n\n```js\nconst upcoming = recurrence.takeAfter(\n  Temporal.Instant.from('2025-01-05T12:00:00Z'),\n  3,\n);\n\nconsole.log(upcoming.map((value) =\u003e value.toString()));\n// [\n//   '2025-01-06T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-08T09:00:00+01:00[Europe/Paris]',\n//   '2025-01-10T09:00:00+01:00[Europe/Paris]'\n// ]\n```\n\nThis is useful for product APIs, queues, and dashboards that need the next N occurrences from a moving boundary without manually looping over `after()`.\n\n#### `recurrence.count(limit?)`\n\nCount occurrences. On open-ended recurrences, pass a limit to keep the query bounded.\n\n```js\nconst exact = recurrence.count();\nconst bounded = recurrence.count(10);\n\nconsole.log(exact);\n// 5\n\nconsole.log(bounded);\n// 5\n```\n\n#### `recurrence.hasAny()`, `recurrence.isEmpty()`\n\nQuick presence checks.\n\n```js\nif (recurrence.hasAny()) {\n  console.log('Schedule has at least one occurrence');\n}\n```\n\n#### `recurrence.hasAnyBetween(after, before, inc = false)`\n\nCheck whether at least one occurrence exists in a range.\n\n```js\nconst activeThisWeek = recurrence.hasAnyBetween(\n  Temporal.Instant.from('2025-01-01T00:00:00Z'),\n  Temporal.Instant.from('2025-01-07T23:59:59Z'),\n  true,\n);\n// Boolean check without materializing the full matching slice\n```\n\n#### `recurrence.occursAt(date)`\n\nCheck whether the recurrence contains an occurrence at an exact instant.\n\n```js\nconst occurs = recurrence.occursAt(Temporal.Instant.from('2025-01-03T09:00:00Z'));\n// Exact instant membership check\n```\n\n### Algebra Methods\n\n#### `new Recurrence(input)`\n\nThe constructor accepts the canonical composed shape:\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = new Recurrence({\n  start: Temporal.ZonedDateTime.from('2026-01-01T09:00:00+01:00[Europe/Paris]'),\n  include: [\n    {\n      rule: {\n        freq: 'WEEKLY',\n        byDay: ['MO', 'TU', 'WE', 'TH', 'FR'],\n        byHour: [9],\n      },\n    },\n    {\n      rule: {\n        freq: 'WEEKLY',\n        byDay: ['SA', 'SU'],\n        byHour: [10],\n      },\n    },\n  ],\n  exclude: [\n    {\n      dates: [Temporal.Instant.from('2026-01-03T09:00:00Z')],\n    },\n  ],\n});\n```\n\nThis is the most expressive entry point. Use it when you want one recurrence object that includes multiple rules, explicit dates, exclusions, or both.\n\n#### `Recurrence.union(...recurrences)` and `recurrence.union(...recurrences)`\n\nCombine multiple recurrence expressions.\n\n```js\nconst weekdays = Recurrence.rule({\n  freq: 'WEEKLY',\n  byDay: ['MO', 'TU', 'WE', 'TH', 'FR'],\n  byHour: [9],\n  start: Temporal.ZonedDateTime.from('2026-01-01T09:00:00+01:00[Europe/Paris]'),\n});\n\nconst weekends = Recurrence.rule({\n  freq: 'WEEKLY',\n  byDay: ['SA', 'SU'],\n  byHour: [10],\n  start: Temporal.ZonedDateTime.from('2026-01-01T09:00:00+01:00[Europe/Paris]'),\n});\n\nconst combined = Recurrence.union(weekdays, weekends);\n\nconsole.log(combined.toText());\n// every week on weekday at 9 AM CET and every week on Saturday and Sunday at 10 AM CET\n```\n\nThis is a first-class algebraic composition. It is more general than the flat constructor shape.\n\n#### `Recurrence.intersection(...recurrences)` and `recurrence.intersection(...recurrences)`\n\nKeep only the occurrences shared by multiple recurrence expressions.\n\n```js\nconst daily = Recurrence.rule({\n  freq: 'DAILY',\n  count: 7,\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n});\n\nconst weekdays = Recurrence.rule({\n  freq: 'WEEKLY',\n  byDay: ['MO', 'TU', 'WE', 'TH', 'FR'],\n  byHour: [9],\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n});\n\nconst weekdayDaily = Recurrence.intersection(daily, weekdays);\n\nconsole.log(weekdayDaily.all().map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+00:00[UTC]',\n//   '2025-01-02T09:00:00+00:00[UTC]',\n//   '2025-01-03T09:00:00+00:00[UTC]',\n//   '2025-01-06T09:00:00+00:00[UTC]',\n//   '2025-01-07T09:00:00+00:00[UTC]'\n// ]\n```\n\nThis is useful when a schedule is best expressed as the overlap between broader recurrence expressions.\n\n#### `Recurrence.difference(include, exclude)` and `recurrence.difference(exclude)`\n\nSubtract one recurrence from another.\n\n```js\nconst businessDays = Recurrence.rule({\n  freq: 'DAILY',\n  count: 10,\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n});\n\nconst weekends = Recurrence.rule({\n  freq: 'WEEKLY',\n  byDay: ['SA', 'SU'],\n  byHour: [9],\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n});\n\nconst weekdaysOnly = businessDays.difference(weekends);\n\nconsole.log(weekdaysOnly.take(3).map((value) =\u003e value.toString()));\n// [\n//   '2025-01-01T09:00:00+00:00[UTC]',\n//   '2025-01-02T09:00:00+00:00[UTC]',\n//   '2025-01-03T09:00:00+00:00[UTC]'\n// ]\n```\n\n#### `recurrence.includingDates(values)` and `recurrence.excludingDates(values)`\n\nReturn a new `Recurrence` with extra dates included or excluded.\n\n```js\nconst adjusted = recurrence\n  .includingDates([Temporal.Instant.from('2025-01-05T09:00:00Z')])\n  .excludingDates([Temporal.Instant.from('2025-01-02T09:00:00Z')]);\n\nconsole.log(adjusted.all().map((value) =\u003e value.toString()));\n// The extra date is added and the excluded date is removed, without mutating `recurrence`\n```\n\nThese methods are immutable: they do not mutate the original recurrence.\n\n### Presentation and Serialization\n\n#### `recurrence.toString()`\n\nSerialize a flat recurrence to RFC-compatible lines.\n\n```js\nconst recurrence = new Recurrence({\n  start: Temporal.ZonedDateTime.from('1997-09-02T09:00:00-04:00[America/New_York]'),\n  include: [\n    {\n      rule: {\n        freq: 'DAILY',\n        count: 5,\n      },\n    },\n  ],\n});\n\nconsole.log(recurrence.toString());\n// DTSTART;TZID=America/New_York:19970902T090000\n// RRULE:FREQ=DAILY;COUNT=5\n// Flat RFC-compatible representation\n```\n\nFor algebraic expressions such as `Recurrence.union(...)` and `Recurrence.difference(...)`, `toString()` throws `TEMPORAL_UNSERIALIZABLE_EXPRESSION` when there is no flat RFC representation.\n\n#### `recurrence.toText(options?)`\n\nDescribe a recurrence in natural language.\n\n```js\nconsole.log(recurrence.toText());\n// every week on weekday at 9 AM, every week on Saturday and Sunday at 10 AM\n```\n\n#### `recurrence.isFullyConvertibleToText(options?)`\n\nCheck whether the full recurrence expression can be rendered completely as text.\n\n```js\nif (recurrence.isFullyConvertibleToText()) {\n  console.log(recurrence.toText());\n}\n```\n\n#### `recurrence.toInput()`\n\nReturn the flat constructor shape for flat recurrences.\n\n```js\nconst input = recurrence.toInput();\n```\n\nLike `toString()`, this throws for non-flat algebraic expressions.\n\n#### `recurrence.toJSON()`\n\nReturn a stable public JSON shape. Flat recurrences serialize as flat input-like objects; algebraic recurrences serialize as structural expressions.\n\n```js\nconst json = recurrence.toJSON();\n\nconsole.log(json);\n// {\n//   kind: 'input',\n//   start: '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   tzid: 'Europe/Paris',\n//   include: [\n//     {\n//       rule: {\n//         freq: 'WEEKLY',\n//         start: '2025-01-01T08:00:00Z',\n//         tzid: 'Europe/Paris',\n//         interval: 1,\n//         count: 5,\n//         byDay: ['MO', 'WE', 'FR'],\n//         byHour: [9],\n//       },\n//     },\n//   ],\n//   exclude: [],\n// }\n```\n\nThis is the recommended representation for inspection, snapshots, transport, and structural equality checks.\n\n#### `Recurrence.fromJSON(json)`\n\nRebuild a `Recurrence` from a value previously produced by `toJSON()`.\n\n```js\nconst recurrence = rule({\n  freq: 'WEEKLY',\n  byDay: ['MO', 'WE', 'FR'],\n  byHour: [9],\n  count: 5,\n  start: Temporal.Instant.from('2025-01-01T08:00:00Z'),\n  tzid: 'Europe/Paris',\n});\n\nconst saved = recurrence.toJSON();\nconsole.log(saved);\n// {\n//   kind: 'input',\n//   start: '2025-01-01T09:00:00+01:00[Europe/Paris]',\n//   tzid: 'Europe/Paris',\n//   include: [\n//     {\n//       rule: {\n//         freq: 'WEEKLY',\n//         start: '2025-01-01T08:00:00Z',\n//         tzid: 'Europe/Paris',\n//         interval: 1,\n//         count: 5,\n//         byDay: ['MO', 'WE', 'FR'],\n//         byHour: [9],\n//       },\n//     },\n//   ],\n//   exclude: [],\n// }\n\nconst rebuilt = Recurrence.fromJSON(saved);\nconsole.log(rebuilt instanceof Recurrence);\n// true\n```\n\nThis supports both flat input-shaped recurrences and algebraic expressions such as unions, intersections, and differences.\n\n#### `Recurrence.isJSON(value)` and `Recurrence.validateJSON(value)`\n\nValidate a persisted or received JSON value before rebuilding a `Recurrence`.\n\n```js\nconst saved = recurrence.toJSON();\n\nconsole.log(Recurrence.isJSON(saved));\n// true\n\nconst validation = Recurrence.validateJSON(saved);\nconsole.log(validation);\n// { ok: true }\n\nif (Recurrence.isJSON(saved)) {\n  const rebuilt = Recurrence.fromJSON(saved);\n  console.log(rebuilt.equals(recurrence));\n  // true\n}\n```\n\nUse `isJSON()` when you want TypeScript narrowing. Use `validateJSON()` when an API boundary needs a non-throwing result that can expose the validation error.\n\n#### `recurrence.clone()`\n\nCreate a new `Recurrence` with the same public structure.\n\n```js\nconst copy = recurrence.clone();\n// Independent Recurrence instance with the same public structure\n```\n\n#### `recurrence.equals(other)`\n\nCheck structural equality through the public JSON representation.\n\n```js\nif (recurrence.equals(otherRecurrence)) {\n  console.log('Same recurrence shape');\n}\n// Structural equality based on the public JSON representation\n```\n\n#### `recurrence.normalize()` and `recurrence.flatten()`\n\nNormalize nested unions and intersections into a simpler structural form.\n\n```js\nconst normalized = Recurrence.union(\n  Recurrence.union(a, b),\n  c,\n).normalize();\n// Nested unions/intersections are flattened into a simpler structural form\n```\n\nThis is useful when recurrence expressions are assembled programmatically and you want a more stable shape for inspection or comparison.\n\n## Constructor Input Shape\n\n```ts\ntype RecurrenceInput = {\n  start?: Date | Temporal.Instant | Temporal.ZonedDateTime | null;\n  tzid?: string | null;\n  include: RecurrenceEntry[];\n  exclude?: RecurrenceEntry[];\n};\n\ntype RecurrenceEntry =\n  | { rule: RecurrenceRuleInput }\n  | { dates: (Date | Temporal.Instant | Temporal.ZonedDateTime)[] };\n```\n\nRule fields supported by `RecurrenceRuleInput` include:\n- `freq`\n- `start`\n- `tzid`\n- `interval`\n- `count`\n- `until`\n- `wkst`\n- `bySetPos`\n- `byMonth`\n- `byMonthDay`\n- `byYearDay`\n- `byWeekNo`\n- `byDay`\n- `byHour`\n- `byMinute`\n- `bySecond`\n- `byEaster`\n- `rscale`\n- `skip`\n\nAccepted date inputs:\n- `Date`\n- `Temporal.Instant`\n- `Temporal.ZonedDateTime`\n\n## Examples\n\n### Weekdays at 9:00, weekends at 10:00\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = new Recurrence({\n  start: Temporal.ZonedDateTime.from('2026-01-01T09:00:00+01:00[Europe/Paris]'),\n  include: [\n    {\n      rule: {\n        freq: 'WEEKLY',\n        byDay: ['MO', 'TU', 'WE', 'TH', 'FR'],\n        byHour: [9],\n      },\n    },\n    {\n      rule: {\n        freq: 'WEEKLY',\n        byDay: ['SA', 'SU'],\n        byHour: [10],\n      },\n    },\n  ],\n});\n```\n\n### Parse an RFC string, then add explicit exceptions\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = Recurrence.parse({\n  rruleString: 'RRULE:FREQ=DAILY;COUNT=5;BYHOUR=9',\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n}).excludingDates([\n  Temporal.Instant.from('2025-01-03T09:00:00Z'),\n]);\n```\n\n### Build a holiday calendar from explicit dates\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst holidays = Recurrence.dates([\n  Temporal.ZonedDateTime.from('2025-05-01T00:00:00+02:00[Europe/Paris]'),\n  Temporal.ZonedDateTime.from('2025-05-08T00:00:00+02:00[Europe/Paris]'),\n]);\n```\n\n### Intersect two broader schedules\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst everyDay = Recurrence.rule({\n  freq: 'DAILY',\n  count: 10,\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n});\n\nconst weekdays = Recurrence.rule({\n  freq: 'WEEKLY',\n  byDay: ['MO', 'TU', 'WE', 'TH', 'FR'],\n  byHour: [9],\n  start: Temporal.ZonedDateTime.from('2025-01-01T09:00:00Z[UTC]'),\n});\n\nconst weekdayOccurrences = Recurrence.intersection(everyDay, weekdays);\n```\n\n### Snapshot a recurrence for inspection or transport\n\n```js\nimport { Temporal } from 'temporal-polyfill';\nimport { Recurrence } from '@rrulenet/recurrence';\n\nconst recurrence = new Recurrence({\n  start: Temporal.ZonedDateTime.from('2026-01-01T09:00:00+01:00[Europe/Paris]'),\n  include: [\n    {\n      rule: {\n        freq: 'WEEKLY',\n        byDay: ['MO', 'TU', 'WE', 'TH', 'FR'],\n        byHour: [9],\n      },\n    },\n    {\n      rule: {\n        freq: 'WEEKLY',\n        byDay: ['SA', 'SU'],\n        byHour: [10],\n      },\n    },\n  ],\n});\n\nconst snapshot = recurrence.toJSON();\n// Plain JSON value that can be stored in a DB or sent over the network\n\nconst rebuilt = Recurrence.fromJSON(snapshot);\n// Full Recurrence instance rebuilt from the saved JSON\n\nconst stable = recurrence.normalize().toJSON();\n// Useful when a program has built nested unions/intersections and you want a simpler saved shape\n\nconsole.log(snapshot);\n// {\n//   kind: 'input',\n//   start: '2026-01-01T09:00:00+01:00[Europe/Paris]',\n//   tzid: 'Europe/Paris',\n//   include: [\n//     { rule: { freq: 'WEEKLY', start: '2026-01-01T08:00:00Z', tzid: 'Europe/Paris', interval: 1, count: null, byDay: ['MO', 'TU', 'WE', 'TH', 'FR'], byHour: [9] } },\n//     { rule: { freq: 'WEEKLY', start: '2026-01-01T08:00:00Z', tzid: 'Europe/Paris', interval: 1, count: null, byDay: ['SA', 'SU'], byHour: [10] } },\n//   ],\n//   exclude: [],\n// }\n\nconsole.log(rebuilt instanceof Recurrence);\n// true\n```\n\n## Error Handling\n\nPublic API errors are thrown as `TemporalApiError`.\n\n```js\nimport { TEMPORAL_ERROR_CODES, TemporalApiError, Recurrence } from '@rrulenet/recurrence';\n\ntry {\n  Recurrence.parse({ rruleString: '   ' });\n} catch (error) {\n  if (error instanceof TemporalApiError) {\n    console.log(error.code === TEMPORAL_ERROR_CODES.INVALID_RRULE_STRING);\n  }\n}\n```\n\nAvailable error codes:\n- `TEMPORAL_INVALID_OPTIONS`\n- `TEMPORAL_INVALID_RRULE_STRING`\n- `TEMPORAL_INVALID_DATE`\n- `TEMPORAL_INVALID_TZID`\n- `TEMPORAL_UNSUPPORTED_INPUT`\n- `TEMPORAL_TZID_CONTRADICTION`\n- `TEMPORAL_CONFLICTING_ZONED_DATETIMES`\n- `TEMPORAL_INVALID_COLLECTION_ELEMENT`\n- `TEMPORAL_INVALID_ENTRY`\n- `TEMPORAL_UNSERIALIZABLE_EXPRESSION`\n\n## Development\n\n```bash\nnpm install\nnpm test\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frrulenet%2Frecurrence","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frrulenet%2Frecurrence","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frrulenet%2Frecurrence/lists"}