# Scheduling with Tempo

```elixir
Mix.install([
  {:ex_tempo, "~> 2.0"},
  {:tz, "~> 0.28"}
])

Calendar.put_time_zone_database(Tz.TimeZoneDatabase)
```

## Setup

Run this cell next: every example below depends on it. It imports `Tempo.Sigils`, which provides the `~o` sigil the examples use to write a Tempo value as ISO 8601 text — `~o"2026-06-15"` is the whole day of 15 June 2026. The import brings in only `sigil_o/2`, no other functions.

It needs a cell of its own because Livebook compiles a whole cell before running it, so the setup cell above can't import a module that its own `Mix.install/2` has yet to fetch.

```elixir
import Tempo.Sigils
```

## Free time is set algebra

Scheduling questions are set-theory questions in disguise. *When is Alice free?* is the workday **minus** her meetings. *When are Alice and Bob both free?* is the **intersection** of their free time. No cursors, no sorting by start time — just the algebra:

```elixir
work = ~o"2026-06-15T09:00:00/2026-06-15T17:00:00"

alice_busy =
  Tempo.IntervalSet.new!([
    ~o"2026-06-15T10:00:00/2026-06-15T11:00:00",
    ~o"2026-06-15T14:00:00/2026-06-15T15:00:00"
  ])

bob_busy =
  Tempo.IntervalSet.new!([
    ~o"2026-06-15T09:30:00/2026-06-15T10:30:00",
    ~o"2026-06-15T16:00:00/2026-06-15T17:00:00"
  ])

{:ok, alice_free} = Tempo.difference(work, alice_busy)
{:ok, bob_free} = Tempo.difference(work, bob_busy)
{:ok, mutual} = Tempo.intersection(alice_free, bob_free)
mutual
```

> *"Alice's free time is the workday **minus** her meetings; Bob's is the same. **Mutual** free time is the **intersection** of theirs."*

## From regions to bookable slots

Free time gives the *regions* where a meeting could go. A booking page offers discrete slots — cut the regions into hours with `slots/3`, which leaves out a window too short to hold one:

```elixir
{:ok, slots} = Tempo.IntervalSet.slots(mutual, ~o"PT1H")
Tempo.IntervalSet.members(slots)
```

## When will it be done? — shifting through free time

The dual question — given a task and a busy calendar, *when does it finish?* — is `Tempo.shift/3` with `skipping:`. The duration is consumed from free time only; busy spans cost nothing to cross:

```elixir
meeting = ~o"2026-06-15T10:00/2026-06-15T11:00"
Tempo.shift(~o"2026-06-15T09:30", ~o"PT1H", skipping: meeting)
```

> *"One hour of work starting **09:30**, skipping the **10:00 meeting**, finishes at **11:30** — thirty minutes before, thirty after."*

The busy set can be unbounded. The weekend days of a span with no end are a lazy, territory-aware set, and the walk consumes only as much of it as the shift needs:

```elixir
{:ok, weekends} = Tempo.select(~o"2026-06-18/..", Tempo.weekends(:US))
Tempo.shift(~o"2026-06-18T16:00", ~o"P3D", skipping: weekends)
```

> *"Three days of processing starting **Thursday 16:00**, skipping **every weekend**, finishes **Tuesday 16:00**."*

## Workdays

Business-day arithmetic is the same idea with the calendar's own vocabulary. Which days count as the weekend is territory-aware CLDR data — Saudi Arabia weekends on Friday–Saturday:

```elixir
{Tempo.add_workdays(~o"2026-06-18", 3), Tempo.count_workdays(~o"2026-06", :US)}
```

June 19 2026 is a Friday — a workday in the United States, the weekend in Saudi Arabia:

```elixir
{Tempo.weekend?(~o"2026-06-19"), Tempo.weekend?(~o"2026-06-19", :SA)}
```

## Recurrence — RFC 5545 RRULE

Recurring events parse from standard RRULE strings into the same interval model, so occurrences are just another set to do algebra on:

```elixir
{:ok, standup} = Tempo.RRule.parse("FREQ=WEEKLY;BYDAY=TU;COUNT=4", from: ~o"2026-06-01")
{:ok, tuesdays} = Tempo.to_interval(standup)
tuesdays
```

An unbounded recurrence (`FREQ=WEEKLY` with no `COUNT`/`UNTIL`) refuses to list its occurrences without a `:within` window — supply one and it expands exactly as far as you asked:

```elixir
{:ok, weekly} = Tempo.RRule.parse("FREQ=WEEKLY;BYDAY=FR", from: ~o"2026-06-01")
{:ok, fridays} = Tempo.to_interval(weekly, within: ~o"2026-07")
Tempo.IntervalSet.count(fridays)
```

## Putting it together

Occurrences are just another set, so every predicate applies. *Does June 9 fall on a standup day? Which standups land in the first half of the month?*

```elixir
{Tempo.overlaps?(~o"2026-06-09", tuesdays), Tempo.IntervalSet.covered?(tuesdays, ~o"2026-06-10")}
```

```elixir
{:ok, early_june} = Tempo.members_overlapping(tuesdays, ~o"2026-06-01/2026-06-15")
early_june
```

## Where next

* The [scheduling guide](https://hexdocs.pm/ex_tempo/scheduling.html) adds floating-vs-zoned events, zone-rule-proof future dates, and bounded recurrence. Critical-path scheduling is in the companion package `tempo_network`.
* The [workdays and weekends](https://hexdocs.pm/ex_tempo/workdays-and-weekends.html) and [holidays](https://hexdocs.pm/ex_tempo/holidays.html) cookbooks cover observed-holiday rules.
* The [iCal integration guide](https://hexdocs.pm/ex_tempo/ical-integration.html) imports real calendars into this same algebra.
