# `CalendarEvent`

A calendar event, as a calendar app reads one: a title, and its days or its times.

Use it to send an invitation or an `.ics` entry. For when something repeats on its own, use [`RecurrenceRule`](/docs/types/type-recurrence-rule.md).

## Shape

| Field | Type | Required? |
|---|---|---|
| `summary` | `Text` | Yes |
| `first_day` | [`CalendarDate`](/docs/types/type-calendar-date.md) | No |
| `last_day` | [`CalendarDate`](/docs/types/type-calendar-date.md) | No |
| `start` | [`LocalDateTime`](/docs/types/type-local-date-time.md) | No |
| `end` | [`LocalDateTime`](/docs/types/type-local-date-time.md) | No |
| `zone` | [`Zone`](/docs/types/type-zone.md) | No |
| `location` | `Text` | No |
| `description` | `Text` | No |
| `recurrence` | [`RecurrenceRule`](/docs/types/type-recurrence-rule.md) | No |

Write every field when you build one, and give a field that isn't required as `none`.

Its parts keep these rules, and a value that breaks one fails with the rule's name:

- `days_or_times`: first_day and last_day with no start, end or zone, or start, end and zone with no days
- `last_day_not_before_first`: last_day >= first_day
- `end_after_start`: end > start
- `recurrence_from_start`: recurrence starts on first_day, or at start in zone

## Example

```jadpo
CalendarEvent {
    summary: "Team lunch"
    first_day: "2026-12-18"
    last_day: "2026-12-18"
    start: none
    end: none
    zone: none
    location: none
    description: none
    recurrence: none
}
```

`text.parse(raw, as: CalendarEvent)` reads one from text that comes from outside. Text that isn't one at all fails `malformed`.

## Good to know

- An event has either days (`first_day` and `last_day`) or times (`start`, `end` and `zone`), never both.
- `last_day` is included, even though calendar files write the day after.
- `==` tells you whether two are the same. `<` and `list.sort_by` don't take it, so sort by one of its parts instead.

## In JSON

An object of its parts, in a request, a response or `json.write`:

```json
{"summary":"Christmas Day","first_day":"2026-12-25","last_day":"2026-12-25","start":null,"end":null,"zone":null,"location":null,"description":null,"recurrence":null}
{"summary":"Half-term walk","first_day":"2026-10-24","last_day":"2026-10-26","start":null,"end":null,"zone":null,"location":"Keswick, Cumbria","description":"Boots; maps\nFlask","recurrence":null}
{"summary":"Wedding anniversary","first_day":"2024-02-29","last_day":"2024-02-29","start":null,"end":null,"zone":null,"location":null,"description":null,"recurrence":"DTSTART;VALUE=DATE:20240229\nRRULE:FREQ=YEARLY"}
```

When you read one, a field that isn't required may be left out, or sent as `null`.

## Details

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

`CalendarEvent` 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: `BEGIN:VEVENT\nDTSTART;VALUE=DATE:20261225\nDTEND;VALUE=DATE:20261226\nSUMMARY:Christmas Day\nEND:VEVENT`, `BEGIN:VEVENT\nDTSTART;VALUE=DATE:20261024\nDTEND;VALUE=DATE:20261027\nSUMMARY:Half-term walk\nLOCATION:Keswick\, Cumbria\nDESCRIPTION:Boots\; maps\nFlask\nEND:VEVENT` and `BEGIN:VEVENT\nDTSTART;VALUE=DATE:20240229\nDTEND;VALUE=DATE:20240301\nSUMMARY:Wedding anniversary\nRRULE:FREQ=YEARLY\nEND:VEVENT`.

### How it is stored

It is stored in 9 columns, each named for the field, `_` and its suffix. This is `BEGIN:VEVENT\nDTSTART;VALUE=DATE:20240229\nDTEND;VALUE=DATE:20240301\nSUMMARY:Wedding anniversary\nRRULE:FREQ=YEARLY\nEND:VEVENT`:

| Column | Type | Holds |
|---|---|---|
| `<field>_summary` | `text` | `'Wedding anniversary'` |
| `<field>_first_day` | `date` | `'2024-02-29'::date` |
| `<field>_last_day` | `date` | `'2024-02-29'::date` |
| `<field>_start` | `timestamp` | `NULL::timestamp` |
| `<field>_end` | `timestamp` | `NULL::timestamp` |
| `<field>_zone` | `text` | `NULL::text` |
| `<field>_location` | `text` | `NULL::text` |
| `<field>_description` | `text` | `NULL::text` |
| `<field>_recurrence` | `text` | `E'DTSTART;VALUE=DATE:20240229\nRRULE:FREQ=YEARLY'` |

### How it is shown to people

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

With `locale: Locale.en_gb`:

| Value | Shown |
|---|---|
| `"BEGIN:VEVENT\nDTSTART;VALUE=DATE:20261225\nDTEND;VALUE=DATE:20261226\nSUMMARY:Christmas Day\nEND:VEVENT"` | `"BEGIN:VEVENT\nDTSTART;VALUE=DATE:20261225\nDTEND;VALUE=DATE:20261226\nSUMMARY:Christmas Day\nEND:VEVENT"` |
| `"BEGIN:VEVENT\nDTSTART;VALUE=DATE:20261024\nDTEND;VALUE=DATE:20261027\nSUMMARY:Half-term walk\nLOCATION:Keswick\\, Cumbria\nDESCRIPTION:Boots\\; maps\\nFlask\nEND:VEVENT"` | `"BEGIN:VEVENT\nDTSTART;VALUE=DATE:20261024\nDTEND;VALUE=DATE:20261027\nSUMMARY:Half-term walk\nLOCATION:Keswick\\, Cumbria\nDESCRIPTION:Boots\\; maps\\nFlask\nEND:VEVENT"` |
| `"BEGIN:VEVENT\nDTSTART;VALUE=DATE:20240229\nDTEND;VALUE=DATE:20240301\nSUMMARY:Wedding anniversary\nRRULE:FREQ=YEARLY\nEND:VEVENT"` | `"BEGIN:VEVENT\nDTSTART;VALUE=DATE:20240229\nDTEND;VALUE=DATE:20240301\nSUMMARY:Wedding anniversary\nRRULE:FREQ=YEARLY\nEND:VEVENT"` |
| `"BEGIN:VEVENT\nDTSTART;TZID=Europe/London:20261024T220000\nDTEND;TZID=Europe/London:20261025T020000\nSUMMARY:Night shift\nRRULE:FREQ=WEEKLY;COUNT=4\nEND:VEVENT"` | `"BEGIN:VEVENT\nDTSTART;TZID=Europe/London:20261024T220000\nDTEND;TZID=Europe/London:20261025T020000\nSUMMARY:Night shift\nRRULE:FREQ=WEEKLY;COUNT=4\nEND:VEVENT"` |

### Operations

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>
