# Converting to and from Elixir date/time types

Tempo treats every value as a **bounded interval on the time line**, never as an instant (see [When to use Tempo](when-to-use-tempo.md) for why). The moment you bring a stdlib `Date`, `Time`, `NaiveDateTime`, or `DateTime` into Tempo, it stops being a point and becomes a span.

This guide answers the two questions that follow from that:

* **At what resolution** does a converted value land?
* **What interval** does a "point in time" actually become — and why does the width matter?

## Setup — required for every example

Every example uses the `~o` sigil from `Tempo.Sigils`. Bring it into scope first:

```elixir
import Tempo.Sigils
```

## 1. Stdlib → Tempo: the resolution you get

`Tempo.from_elixir/2` is the unified gateway (there are also type-specific `from_date/1`, `from_time/1`, `from_naive_date_time/1`, `from_date_time/1`).

The rule is: **resolution follows the type's declared precision, not the magnitude of its fields.** Elixir's `Time`/`NaiveDateTime`/`DateTime` are second-granular by construction, so `09:00:00` is a *fully specified second* — not an under-specified hour. A zero component is still a specified component.

| Elixir value | Inferred resolution |
|---|---|
| `~D[2022-07-04]` | `:day` (a `Date` has no time axis) |
| `~T[14:30:00]` | `:second` |
| `~N[2022-07-04 14:30:00]` | `:second` |
| `~U[2022-07-04 14:30:00Z]` | `:second` |
| `~U[2022-07-04 14:30:00.000000Z]` | `:microsecond` (sub-second precision present) |

```elixir
Tempo.from_elixir(~U[2022-07-04 14:30:00Z]) |> Tempo.resolution()
#=> {:second, 1}
```

To deliberately widen to a coarser span, pass `:resolution`:

```elixir
# Treat a midnight value as the whole day, not the first second of it.
Tempo.from_elixir(~N[2022-07-04 00:00:00], resolution: :day)
#=> ~o"2022Y7M4D"
```

> *"Read a calendar value at the precision it was written; widen to a whole day only when you ask for it."*

## 2. A point in time **is** an interval

Every Tempo value materialises to an explicit half-open `[from, to)` span via `Tempo.to_interval/1`. The width is **one unit at the value's resolution** — so the resolution from §1 decides the span:

| Converted from | Interval | Width |
|---|---|---|
| `~D[2022-07-04]` (day) | `[2022-07-04, 2022-07-05)` | 1 day |
| `~U[…14:30:45Z]` (second) | `[…14:30:45, …14:30:46)` | 1 second |
| `~U[…14:30:45.123456Z]` (µs) | `[…45.123456, …45.123457)` | 1 microsecond |

```elixir
{:ok, interval} = Tempo.from_elixir(~U[2022-07-04 14:30:45Z]) |> Tempo.to_interval()
Tempo.Interval.duration(interval)
#=> ~o"PT1S"
```

```elixir
{:ok, day} = Tempo.from_elixir(~D[2022-07-04]) |> Tempo.to_interval()
Tempo.Interval.duration(day)
#=> ~o"PT86400S"   # one whole day
```

The upper bound is **exclusive** (`[from, to)`), which is what makes the spans tile the time line cleanly — `[2022-07-04, 2022-07-05)` followed by `[2022-07-05, 2022-07-06)` is exactly `[2022-07-04, 2022-07-06)` with no gap or overlap. See [Enumeration semantics](enumeration-semantics.md) for how this drives iteration, and `Tempo.to_interval/2` for the full materialisation contract.

> *"A day is the span from this midnight to the next; a timestamp is the span from this second to the next."*

Two notes:

* **Sub-second spans and `duration/1`.** `duration/1` reports whole seconds, so a one-microsecond interval reads as `~o"PT0S"`. The span is still one microsecond wide — inspect the endpoints (`from/1`, `to/1`) when you need sub-second extent.

* **A bare `Time` materialises non-anchored.** `~T[14:30:00]` becomes `T14H30M0S/T14H30M1S` — a one-second span on the *time-of-day* axis with no date. Operations that need an absolute position (duration in wall-clock seconds, cross-zone comparison) require anchoring it to a date first.

* **A pure time-of-day group materialises non-anchored too.** A grouped time value with no date — for example the first quarter-hour `[hour: 16, minute: {:group, 1..15}]` — becomes the relative span `[16:01, 16:16)` on the time-of-day axis. The same anchoring caveat applies. A group whose upper bound would carry off the end of the day (`23:45..`, with no date to carry into) or any group with a date component (`5G10DU`) needs anchoring first and returns a `Tempo.MaterialisationError` until then.

## 3. Why the width matters

This is the whole point of the interval model, and the place stdlib intuition trips up. Because each "instant" is really a half-open one-unit span, **two timestamps one second apart do not overlap — they meet at the shared boundary.**

```elixir
earlier = ~o"2022Y7M4DT14H30M45S"   # the span [45s, 46s)
later   = ~o"2022Y7M4DT14H30M46S"   # the span [46s, 47s)

Tempo.relation(earlier, later)
#=> :meets

Tempo.overlaps?(earlier, later)
#=> false
```

Identical timestamps are `:equals`; consecutive ones `:meets`; only a genuine span covering shared time `:overlaps`. That precision is exactly what lets free/busy scans, coalescing, and set operations be unambiguous — there is never a "do these touch?" grey area.

> *"9:30:45 and 9:30:46 don't clash — they're back-to-back."*

And because a plain `DateTime`/`NaiveDateTime` now materialises (it infers to second resolution, and a second is a one-second span), converted timestamps drop straight into the set-algebra API:

```elixir
{:ok, busy} = Tempo.union(~o"2022Y7M4DT14H30M45S", ~o"2022Y7M4DT14H30M46S")
#=> two adjacent one-second spans, ready for difference/intersection/…
```

## 4. Tempo → stdlib: projecting back out

Going the other way, you choose how much of the interval/zone information to keep:

| Function | Result | Zone handling |
|---|---|---|
| `to_date/1` | `Date` | dropped (wall-clock date) |
| `to_time/1` | `Time` | dropped (wall-clock time-of-day) |
| `to_naive_date_time/1` | `NaiveDateTime` | **dropped** (wall-clock reading) |
| `to_date_time/1` | `DateTime` | **preserved** (lossless inverse) |

`to_naive_date_time/1` keeps the wall-clock numbers and discards the offset — exactly like the stdlib `DateTime.to_naive/1`. It does **not** shift to UTC:

```elixir
# Paris is UTC+2 in summer; the wall reading is 10:30, not 08:30.
paris = Tempo.from_elixir(DateTime.new!(~D[2022-06-15], ~T[10:30:00], "Europe/Paris"))
Tempo.to_naive_date_time(paris)
#=> {:ok, ~N[2022-06-15 10:30:00.000000]}
```

When the zone matters, `to_date_time/1` is the lossless inverse of `from_elixir/2` on a `DateTime` — it preserves the named zone and re-derives the offset from the time-zone database:

```elixir
Tempo.to_date_time(paris)
#=> {:ok, #DateTime<2022-06-15 10:30:00.000000+02:00 CEST Europe/Paris>}
```

If you want UTC *wall* time rather than the local reading, normalise explicitly first:

```elixir
{:ok, utc} = Tempo.shift_zone(paris, "Etc/UTC")
Tempo.to_naive_date_time(utc)
#=> {:ok, ~N[2022-06-15 08:30:00.000000]}
```

Two caveats:

* **Resolution must reach the target.** A value coarser than a full datetime (e.g. `~o"2022-11"`) cannot fill a `NaiveDateTime`/`DateTime` and returns `{:error, _}`. Project to the type the value's resolution supports (`to_date/1` for a day, `to_time/1` for a time-of-day).

* **Microsecond precision normalises to 6.** A second-resolution value round-trips through `{0, 0}` → `{0, 6}` microseconds; the instant, zone, and wall reading are identical, only the precision tag widens.

## 5. Where to convert

Convert at the **edges**, compute in the **middle**:

* **Edge layer** (HTTP, DB, log parsing): stdlib types. Parse Unix timestamps and ISO strings into `Date`/`DateTime`.
* **Domain layer** (scheduling, availability, recurrence, set operations): Tempo. Convert in with `from_elixir/2` when the next operation is interval-shaped.
* **Display layer**: `Tempo.to_string/2` for locale-aware output, or the `to_*` projections above to hand a stdlib value back to non-Tempo code.

See [When to use Tempo](when-to-use-tempo.md) for the full decision tree.

## Cheat sheet

```elixir
# Stdlib → Tempo  (resolution = the type's precision; override with :resolution)
Tempo.from_date(~D[2026-06-15])                  # :day
Tempo.from_time(~T[14:30:00])                    # :second (time-of-day, non-anchored)
Tempo.from_naive_date_time(~N[2026-06-15 14:30:00])
Tempo.from_date_time(~U[2026-06-15 14:30:00Z])   # :second, zoned
Tempo.from_elixir(value, resolution: :day)       # unified gateway + explicit widen

# Point → interval  (every value is a span of one unit at its resolution)
Tempo.to_interval(tempo)                          # {:ok, %Interval{}} half-open [from, to)

# Tempo → Stdlib
Tempo.to_date(tempo)                              # zone dropped
Tempo.to_time(tempo)                              # zone dropped
Tempo.to_naive_date_time(tempo)                   # zone dropped (wall-clock, not UTC)
Tempo.to_date_time(tempo)                         # zone preserved (lossless)
Tempo.shift_zone(tempo, "Etc/UTC")                # normalise to UTC first if needed
```
