# `SuperPi.Schedule.Cron`
[🔗](https://github.com/oleksandr/super-pi-beam/blob/v0.1.0/lib/super_pi/schedule/cron.ex#L1)

Cron expressions with time zones.

Parsing and field matching reuse `Oban.Cron.Expression`, which only answers "does this minute
match". `next_after/3` adds the missing "when is the next one": a walk over local calendar
days that skips a whole day when its month, day of month or weekday cannot match, then
lists the matching local times of the days that remain. Granularity is one minute.

Zone rules come from `Tzdata` (IANA data bundled with the package, no network at run time).

## Daylight saving time

The result is always an instant (UTC, whole minute), strictly after the argument.

- An expression that matches every hour (`*` in the hour field) follows the real clock. A
  local time that does not exist (spring forward) cannot fire, and a local time that occurs
  twice (fall back) fires twice. `0 * * * *` therefore fires once per real hour across
  both changes: no duplicate, no gap.
- An expression with fixed hours (`30 2 * * *`) fires once per local day. When the local
  time falls in a spring-forward gap, the run happens at the first minute after the gap
  (as Vixie cron does) instead of being skipped. When it occurs twice, only the first
  occurrence fires.

# `expr`

```elixir
@type expr() :: Oban.Cron.Expression.t()
```

# `matches?`

```elixir
@spec matches?(expr(), DateTime.t() | NaiveDateTime.t()) :: boolean()
```

Does the local wall-clock minute of `datetime` match? `datetime` is read as it is, so pass it
already shifted to the schedule's zone.

# `next_after`

```elixir
@spec next_after(expr() | String.t(), DateTime.t(), String.t()) ::
  {:ok, DateTime.t()} | {:error, :no_match | :invalid_timezone | String.t()}
```

The first matching minute strictly after `after_dt`, as a UTC `DateTime`, for the expression
read in `zone`. `{:error, :no_match}` when none exists within eight years (for example
`0 0 30 2 *`) and `{:error, :invalid_timezone}` for an unknown zone.

# `parse`

```elixir
@spec parse(String.t()) :: {:ok, expr()} | {:error, String.t()}
```

Parses a five-field expression or an `@daily`-style alias. `@reboot` is rejected.

# `valid_timezone?`

```elixir
@spec valid_timezone?(term()) :: boolean()
```

True when `zone` is a time zone of the bundled IANA database.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
