# ical — iCalendar (.ics / RFC 5545) adapter system

iCalendar (.ics / RFC 5545) format pack, pure JS with zero dependencies: build VEVENT and VCALENDAR text (createEvent, createCalendar) with summary/location/organizer/attendees/categories, parse .ics...


Pure-JS, **zero-dependency** capability pack for the **iCalendar file format** and calendar
scheduling. It builds and parses `.ics` text (VCALENDAR / VEVENT / VALARM), constructs and expands
RFC 5545 **RRULE** recurrence, and derives **free/busy** and **availability** from a set of events.

Node built-ins only (`node:crypto` for UID generation). Deterministic: all recurrence math runs in
**UTC** so results never drift with the host timezone. Output `.ics` uses CRLF line endings, 75-octet
line folding, and RFC 5545 TEXT escaping.

## Tools

| Tool | Input (one args object) | Output |
|---|---|---|
| `createEvent` | `{ event: { start, end\|duration, summary, location, organizer, attendees, categories, allDay, rrule, alarms, uid } }` (or the fields at top level) | `{ ics, uid, start, end, allDay, lineCount }` — a `VEVENT` block |
| `createCalendar` | `{ events: [...], prodId?, name?, method?, timeZone? }` | `{ ics, eventCount, uids, bytes }` — a full `VCALENDAR` |
| `parse` | `{ ics }` | `{ calendar, events[], eventCount }` — events with ISO + epoch dates |
| `rrule` | `{ options: { freq, interval, count, until, byDay, byMonthDay, byMonth, byHour, byMinute, bySetPos, weekStart } }` | `{ rrule, line, parts, summary }` |
| `expandRecurrence` | `{ rrule, dtStart, rangeStart?, rangeEnd?, limit? }` | `{ occurrences[], count, truncated }` |
| `freeBusy` | `{ events: [...], rangeStart?, rangeEnd? }` | `{ busy[], blockCount, totalBusyMs }` (merged busy blocks) |
| `availability` | `{ rangeStart, rangeEnd, events?\|busy?, workingHours?, slotMinutes?, minMinutes? }` | `{ slots[], slotCount, totalFreeMs }` (open/bookable slots) |
| `addAlarm` | `{ ics?, alarm: { action, trigger\|minutesBefore, description } }` | `{ ics }` with a `VALARM` injected (or a standalone `valarm`) |
| `toDataUri` | `{ ics, filename? }` | `{ dataUri, filename, bytes }` — `data:text/calendar;…;base64,…` |
| `duration` | `{ iso }` or `{ ms }` | `{ iso, ms }` — ISO 8601 ⇄ milliseconds |
| `validate` | `{ ics }` | `{ valid, errors[], warnings[], eventCount }` |

Dates accept **ISO strings**, **epoch ms**, or **ICS date strings** (`YYYYMMDD`,
`YYYYMMDDTHHMMSS`, `YYYYMMDDTHHMMSSZ`) interchangeably. Durations use ISO 8601 (`PT1H30M`, `P1D`).

## Usage

```js
import ical from './index.js';

// 1. A weekly stand-up, every Mon/Wed/Fri at 09:00, 12 times
const rr = ical.adapters.rrule({ options: { freq: 'WEEKLY', byDay: ['MO', 'WE', 'FR'], count: 12 } });
// rr.rrule === "FREQ=WEEKLY;COUNT=12;BYDAY=MO,WE,FR"

const dates = ical.adapters.expandRecurrence({
  rrule: rr.rrule,
  dtStart: '2026-01-05T09:00:00Z',
  rangeEnd: '2026-03-01T00:00:00Z',
});
// dates.occurrences → [{ iso, ms, ics }, ...]

// 2. Build a downloadable calendar file
const cal = ical.adapters.createCalendar({
  name: 'Team',
  events: [
    { start: '2026-01-05T09:00:00Z', duration: 'PT30M', summary: 'Stand-up', rrule: rr.rrule,
      alarms: [{ action: 'DISPLAY', minutesBefore: 10 }] },
    { start: '2026-01-06', allDay: true, summary: 'Company holiday' },
  ],
});
const link = ical.adapters.toDataUri({ ics: cal.ics, filename: 'team.ics' });

// 3. Find bookable 30-min slots inside Mon–Fri 9–5, avoiding existing meetings
const slots = ical.adapters.availability({
  rangeStart: '2026-01-05T00:00:00Z',
  rangeEnd:   '2026-01-06T00:00:00Z',
  events: [{ start: '2026-01-05T10:00:00Z', end: '2026-01-05T11:00:00Z', summary: 'Sync' }],
  workingHours: { startHour: 9, endHour: 17, days: ['MO','TU','WE','TH','FR'] },
  slotMinutes: 30,
});

// 4. Round-trip: parse an .ics back to structured events
const parsed = ical.adapters.parse({ ics: cal.ics });
```

## DRY boundary (respected — do not cross)

This pack owns exactly **one** thing: the **iCalendar (.ics) format + its RRULE recurrence +
free/busy/availability**. It deliberately does **not** overlap its neighbours:

- **`calendar`** renders a single date across 24 world calendar *systems* (Gregorian, Hebrew, Islamic
  Hijri, Maya Long Count, …). That is calendar-system *conversion*, not file authoring. `ical` never
  converts calendar systems.
- **`cron`** parses human intervals (`'5m'`, `'2h'`) and fires schedule *actions* (ping/print/call).
  That is job scheduling. `ical` produces RFC 5545 **RRULE** recurrence for calendar events, not cron
  specs — and never executes actions.
- **`datetime`** does date *math* (durations between dates, business-day counting, timezone
  wall-clock, locale formatting, leap-year predicates). `ical` calls no date library and only does the
  minimal UTC arithmetic needed to enumerate recurrence occurrences and build slots.

If you need world-calendar conversion, interval-firing, or general date math, use those packs and pass
their results in.


---
Source: shared/engines/adapters/domain/ical/README.md
Canonical: https://docs.leumas.tech/p/adapters/domain/ical
