# Cron

Backoff and spacing schedules recur relative to *when the last run finished*.
Some jobs instead need to fire at specific points on the calendar — "every
weekday at 9:00", "at 4 AM on the 8th through 14th of each month". The `Cron`
module describes those points using [UNIX cron expressions](https://en.wikipedia.org/wiki/Cron),
and `Schedule.cron` turns one into a [`Schedule`](https://effect.plants.sh/scheduling/schedule/) you can
drive with [`Effect.repeat`](https://effect.plants.sh/scheduling/repetition-and-retry/).

```ts
import { Cron, Effect, Schedule } from "effect"

// "second minute hour day-of-month month day-of-week"
// → at 09:00:00, Monday through Friday.
const businessDaysAt9 = Schedule.cron("0 0 9 * * 1-5", "Europe/Rome")

const sendDigest = Effect.gen(function* () {
  yield* Effect.log("sending the daily digest")
}).pipe(Effect.repeat(businessDaysAt9))
```

`Schedule.cron` accepts the expression as a string (with an optional time zone)
or a pre-built `Cron` value. The resulting schedule's `Output` is a
`Duration` — the gap it slept until the next matching instant — and its `Error`
channel is `Cron.CronParseError`, since an invalid expression surfaces as a
typed failure rather than a thrown exception.
**Six fields, seconds first:** Effect's cron expressions lead with a **seconds** field, so a full expression
  has six parts: `seconds minutes hours day-of-month month day-of-week`. `"0 0 9
  * * 1-5"` means "at second 0, minute 0, hour 9, any day, any month, on weekdays
  1–5". A five-field expression (omitting seconds) is also accepted — seconds
  default to `0`. Each field accepts numbers, ranges (`8-14`), lists (`1,15`),
  steps (`*/15`), and `*` for "any".

## Building a Cron without parsing

When you'd rather not hand-write an expression, `Cron.make` builds a `Cron` from
explicit numeric constraints. An empty (or, for `seconds`/`tz`, omitted) field
means "no constraint" — i.e. "match any value", exactly like `*`.

```ts
import { Cron, DateTime } from "effect"

// 04:00 on the 8th–14th of every month, in the Europe/Rome time zone.
const cron = Cron.make({
  seconds: [0],
  minutes: [0],
  hours: [4],
  days: [8, 9, 10, 11, 12, 13, 14],
  months: [], // any month
  weekdays: [], // any weekday
  tz: DateTime.zoneMakeNamedUnsafe("Europe/Rome")
})
```

## Parsing expressions safely

`Cron.parse` validates an expression and returns a
[`Result`](https://effect.plants.sh/data-types/result/) — `Result.Success` with the `Cron`, or
`Result.Failure` with a `CronParseError`. Use it when the expression comes from
configuration or user input and might be malformed.

```ts
import { Cron, Result } from "effect"

const parsed = Cron.parse("0 0 4 8-14 * *", "UTC")

if (Result.isSuccess(parsed)) {
  // parsed.success is a Cron
} else {
  // parsed.failure is a CronParseError with a .message
}
```

If you control the expression and want to fail fast on a typo, `Cron.parseUnsafe`
returns the `Cron` directly and throws on invalid input — convenient for
module-level constants.

### Cron field syntax

Each of the six fields accepts the same grammar:

| Form     | Example  | Meaning                                    |
| -------- | -------- | ------------------------------------------ |
| Any      | `*`      | every value in the field's range           |
| Single   | `9`      | exactly 9                                  |
| Range    | `8-14`   | 8 through 14 inclusive                      |
| List     | `1,15`   | 1 and 15                                   |
| Step     | `*/15`   | every 15th value (0, 15, 30, …)            |
| Range+step | `0-30/10` | every 10th value in 0–30 (0, 10, 20, 30) |

The field ranges are: seconds `0-59`, minutes `0-59`, hours `0-23`, day-of-month
`1-31`, month `1-12`, weekday `0-6` (`0` is Sunday). The month and weekday fields
also accept three-letter aliases — `JAN`–`DEC` and `SUN`–`SAT` (case-insensitive).

```ts
import { Cron } from "effect"

// Every 15 minutes, business hours, weekdays — using aliases.
Cron.parseUnsafe("0 */15 9-17 * * MON-FRI")
```
**Day-of-month and weekday together:** When *both* the day-of-month and weekday fields are constrained, a date matches
  if **either** one matches — this is standard cron behavior. If only one is
  constrained, only that field is consulted.

## Querying a Cron directly

A `Cron` is useful even outside a `Schedule`. These helpers take a
`DateTime.Input` (a `Date`, epoch millis, or `DateTime`):

```ts
import { Cron } from "effect"

const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

// Does this instant satisfy the schedule?
Cron.match(cron, new Date("2025-01-08T04:00:00Z")) // => true

// When does it next fire on or after a given instant? (returns a Date)
Cron.next(cron, new Date("2025-01-01T00:00:00Z")) // => 2025-01-08T04:00:00.000Z

// Iterate matching instants lazily.
const it = Cron.sequence(cron, new Date("2025-01-01T00:00:00Z"))
it.next().value // => 2025-01-08T04:00:00.000Z
it.next().value // => 2025-01-09T04:00:00.000Z
```
**Cron jobs and overlap:** `Schedule.cron` waits until the next matching instant, runs the effect, then
  waits again. If a run lasts past the following trigger, that occurrence is not
  queued up — the schedule simply targets the next future match. For overlapping
  or long-running jobs, fork each run as a child fiber (see
  [Concurrency](https://effect.plants.sh/concurrency/)) so the cron loop stays responsive.

## Combining cron with other schedules

Because `Schedule.cron` returns an ordinary `Schedule`, it composes like any
other. Cap a cron job to a fixed number of runs, or tap its output for logging:

```ts
import { Duration, Effect, Schedule } from "effect"

const limited = Schedule.cron("0 0 9 * * 1-5", "Europe/Rome").pipe(
  Schedule.both(Schedule.recurs(10)), // run at most 10 times
  Schedule.tapOutput((slept: Duration.Duration) =>
    Effect.logDebug(`woke after ${Duration.toMillis(slept)}ms`)
  )
)
```

To verify cron-driven code without waiting for the calendar, advance simulated
time with `TestClock` — see [Clock](https://effect.plants.sh/scheduling/clock/).

## Reference

Everything the `Cron` module exports. A `Cron` value is immutable, hashable, and
implements [`Equal`](https://effect.plants.sh/traits/equal-and-hash/), so two crons with the same field
restrictions compare equal by value.

### make

Builds a `Cron` from explicit field constraints. `minutes`, `hours`, `days`,
`months`, and `weekdays` are required iterables of numbers; `seconds` (defaults
to `[0]`) and `tz` (a `DateTime.TimeZone`) are optional. An empty iterable for a
field means "match any value" — the same as `*`. Values may be passed in any
order; they are sorted and de-duplicated internally.

```ts
import { Cron, DateTime } from "effect"

// 09:30 every Monday, in New York local time.
const cron = Cron.make({
  seconds: [0],
  minutes: [30],
  hours: [9],
  days: [], // any day-of-month
  months: [], // any month
  weekdays: [1], // Monday
  tz: DateTime.zoneMakeNamedUnsafe("America/New_York")
})

Cron.match(cron, new Date("2025-06-02T13:30:00Z")) // => true (Mon 09:30 EDT)
```

### parse

Parses a five- or six-field cron expression into a
`Result<Cron, CronParseError>`. The optional second argument is a time zone,
either a `DateTime.TimeZone` or an IANA string like `"Europe/Rome"`. A five-field
expression has seconds prepended as `0` automatically.

```ts
import { Cron, Result } from "effect"

Cron.parse("0 0 4 8-14 * *", "UTC")
// => Result.Success(Cron { hours: {4}, days: {8..14}, tz: Some(UTC) })

Cron.parse("nope")
// => Result.Failure(CronParseError: "Invalid number of segments in cron expression")
```

### parseUnsafe

Like `parse`, but returns the `Cron` directly and throws on invalid input. Use it
for trusted, literal expressions (e.g. module-level constants).

```ts
import { Cron } from "effect"

Cron.parseUnsafe("0 0 9 * * *", "America/New_York")
// => Cron { hours: {9}, tz: Some(America/New_York) }

// Cron.parseUnsafe("invalid") // throws CronParseError
```

### isCron

Type guard that narrows an `unknown` value to `Cron`.

```ts
import { Cron } from "effect"

const cron = Cron.parseUnsafe("0 0 9 * * *")

Cron.isCron(cron) // => true
Cron.isCron({}) // => false
Cron.isCron("0 0 9 * * *") // => false
```

### isCronParseError

Type guard that narrows an `unknown` value to `CronParseError` — handy when
inspecting a caught defect or a generic failure channel.

```ts
import { Cron, Result } from "effect"

const result = Cron.parse("totally invalid")

if (Result.isFailure(result)) {
  Cron.isCronParseError(result.failure) // => true
}

Cron.isCronParseError(new Error("nope")) // => false
```

### match

Returns `true` when a given instant satisfies the schedule. Seconds, minutes,
hours, and months are matched directly; the day-of-month/weekday pair follows
cron's inclusive rule (either may match when both are constrained). The instant
is interpreted in the cron's time zone, if any.

```ts
import { Cron } from "effect"

const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

Cron.match(cron, new Date("2025-01-08T04:00:00Z")) // => true
Cron.match(cron, new Date("2025-01-08T05:00:00Z")) // => false (wrong hour)
Cron.match(cron, new Date("2025-01-07T04:00:00Z")) // => false (wrong day)
```

### next

Returns the next matching `Date` strictly *after* the supplied instant (or after
"now" when omitted). Because the search is strict, passing a matching instant
yields the *following* occurrence.

```ts
import { Cron } from "effect"

const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

Cron.next(cron, new Date("2025-01-01T00:00:00Z"))
// => 2025-01-08T04:00:00.000Z

Cron.next(cron, new Date("2025-01-08T04:00:00Z")) // already a match
// => 2025-01-09T04:00:00.000Z
```

### prev

Returns the previous matching `Date` strictly *before* the supplied instant (or
before "now" when omitted). The search is strict, mirroring `next`.

```ts
import { Cron } from "effect"

const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

Cron.prev(cron, new Date("2025-01-20T00:00:00Z"))
// => 2025-01-14T04:00:00.000Z
```

### sequence

Returns an infinite `IterableIterator<Date>` of matching instants, each strictly
after the previous. Combine with `Array.from({ length })` or a `for…of` with a
break to take a finite slice.

```ts
import { Cron } from "effect"

const cron = Cron.parseUnsafe("0 0 9 * * 1-5") // 09:00, weekdays

const iterator = Cron.sequence(cron, new Date("2025-01-01T00:00:00Z"))
const next3 = Array.from({ length: 3 }, () => iterator.next().value)
// => [
//   2025-01-01T09:00:00.000Z,  // Wed
//   2025-01-02T09:00:00.000Z,  // Thu
//   2025-01-03T09:00:00.000Z   // Fri
// ]
```

### Equivalence

An [`Equivalence`](https://effect.plants.sh/traits/equivalence/) over `Cron` that compares the six
field restrictions (seconds, minutes, hours, days, months, weekdays). It does
**not** compare the optional time zone, and ordering of the input values is
irrelevant.

```ts
import { Cron } from "effect"

const a = Cron.parseUnsafe("0 0,30 9 1,15 * 1-5")
const b = Cron.parseUnsafe("0 30,0 9 15,1 * 1-5") // values reordered

Cron.Equivalence(a, b) // => true
```

### equals

Checks whether two `Cron` values have the same field restrictions (ignoring time
zone). Available in both data-last (curried) and data-first forms.

```ts
import { Cron } from "effect"

const a = Cron.parseUnsafe("0 0 9 1,15 * 1-5")
const b = Cron.parseUnsafe("0 0 9 1,15 * 1-5")

Cron.equals(a, b) // => true
Cron.equals(a)(b) // => true (curried)
```

### CronParseError

The tagged error (`_tag: "CronParseError"`) produced when parsing fails. It
carries a human-readable `message` and the offending `input` string. `Cron.parse`
returns it in the failure channel of its `Result`, and `Schedule.cron` surfaces
the very same type in its `Error` channel — so a malformed expression there is a
typed failure you can [`Effect.catch`](https://effect.plants.sh/error-management/expected-errors/),
never a thrown exception.

```ts
import { Cron, Result } from "effect"

const result = Cron.parse("0 99 9 * * *") // minute out of range

if (Result.isFailure(result)) {
  const error = result.failure
  error._tag // => "CronParseError"
  error.message // => "Expected a value between 0 and 59"
  error.input // => "99"
}
```

```ts
import { Cron, Effect, Schedule } from "effect"

// The CronParseError reaches the effect's error channel, not a throw.
const program = Effect.void.pipe(
  Effect.repeat(Schedule.cron("not a cron")),
  Effect.catchTag("CronParseError", (e: Cron.CronParseError) =>
    Effect.logError(`bad cron: ${e.message}`)
  )
)
```

### Cron (type)

The model interface. A `Cron` exposes six `ReadonlySet<number>` fields plus an
optional time zone. Empty sets denote unconstrained fields (the `*` case), so
inspect schedules through the public helpers rather than assuming every allowed
value is stored.

```ts
import { Cron } from "effect"

declare const cron: Cron.Cron
cron.seconds // ReadonlySet<number>
cron.minutes // ReadonlySet<number>
cron.hours // ReadonlySet<number>
cron.days // ReadonlySet<number>  (day-of-month, 1–31)
cron.months // ReadonlySet<number>  (1–12)
cron.weekdays // ReadonlySet<number>  (0–6, Sunday = 0)
cron.tz // Option.Option<DateTime.TimeZone>
```
**Time zones:** A cron's `tz` is a `DateTime.TimeZone`. `next`/`prev`/`match` account for
  daylight-saving transitions in that zone. See
  [DateTime &amp; Duration](https://effect.plants.sh/data-types/datetime-and-duration/) for building and
  inspecting time zones.