# `RecurrenceRule`

When something repeats, in the calendar standard's own text.

Use it for repeating events, such as every Monday at 09:30. `temporal.occurrences` gives the next ones. For a shop's weekly hours, use [`OpeningHours`](/docs/types/type-opening-hours.md).

## Shape

One value, held as text in one set form, with no parts: `DTSTART;VALUE=DATE:20240229
RRULE:FREQ=YEARLY`.

## Example

```jadpo
RecurrenceRule("DTSTART;TZID=Europe/London:20261026T093000\nRRULE:FREQ=WEEKLY;BYDAY=MO")
```

`RecurrenceRule(raw)` checks the text, and text that isn't one breaks the `format` rule.

`text.parse(raw, as: RecurrenceRule)` reads one from text that comes from outside. Text that isn't one at all fails `malformed`, such as `FREQ=WEEKLY;BYDAY=MO`.

## Good to know

- It holds its first time on one line and its rule on the next, with the zone in the first, so daylight saving is kept right.
- `==` tells you whether two are the same. `<` and `list.sort_by` don't take it.

## In JSON

A string, in a request, a response or `json.write`:

```json
"DTSTART;TZID=Europe/London:20261026T093000\nRRULE:FREQ=WEEKLY;BYDAY=MO"
"DTSTART;TZID=America/New_York:20260310T090000\nRRULE:FREQ=MONTHLY;BYDAY=2TU;COUNT=12"
"DTSTART;TZID=Europe/London:20260130T170000\nRRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z"
```

## Details

<details>
<summary>Text form, storage, how it is shown, and operations</summary>

`RecurrenceRule` is in the group Time and calendar, and has been in Jadpo since 0.1.0.

### Text form

`text.from` writes one standard text form, the same form `text.parse` reads back: `DTSTART;TZID=Europe/London:20261026T093000\nRRULE:FREQ=WEEKLY;BYDAY=MO`, `DTSTART;TZID=America/New_York:20260310T090000\nRRULE:FREQ=MONTHLY;BYDAY=2TU;COUNT=12` and `DTSTART;TZID=Europe/London:20260130T170000\nRRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z`.

### How it is stored

It is stored in one column named for the field, of type `text`. `DTSTART;TZID=Europe/London:20260130T170000\nRRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z` is stored as `E'DTSTART;TZID=Europe/London:20260130T170000\nRRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z'`.

### How it is shown to people

`text.format(value, locale:)` shows it to people in their locale.

With `locale: Locale.en_gb`:

| Value | Shown |
|---|---|
| `"DTSTART;TZID=Europe/London:20261026T093000\nRRULE:FREQ=WEEKLY;BYDAY=MO"` | `"DTSTART;TZID=Europe/London:20261026T093000\nRRULE:FREQ=WEEKLY;BYDAY=MO"` |
| `"DTSTART;TZID=America/New_York:20260310T090000\nRRULE:FREQ=MONTHLY;BYDAY=2TU;COUNT=12"` | `"DTSTART;TZID=America/New_York:20260310T090000\nRRULE:FREQ=MONTHLY;BYDAY=2TU;COUNT=12"` |
| `"DTSTART;TZID=Europe/London:20260130T170000\nRRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z"` | `"DTSTART;TZID=Europe/London:20260130T170000\nRRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z"` |
| `"DTSTART;VALUE=DATE:20240229\nRRULE:FREQ=YEARLY"` | `"DTSTART;VALUE=DATE:20240229\nRRULE:FREQ=YEARLY"` |

### Operations

- `temporal.occurrences(rule: RecurrenceRule, from: T, count: Int) -> List<ZonedTime>`: The next `count` occurrences of `rule` at or after `from`, in its zone (RFC 5545): fewer when the rule ends first; `count` is from 1 to 1000, so an unbounded rule is never expanded without a bound. From a date, the days they fall on, `List<CalendarDate>`, which a rule on dates gives; from an `Instant`, a timed rule's local times.

It also takes `text.parse`, `text.from`, `text.format`, `json.read`, `json.write` and `==`. [How every standard type behaves](/docs/every-standard-type-meets-one-contract.md) says how they work.

</details>
