<div align="center">

# croniq

**Parse cron expressions, compute the next/previous run times, and describe them in plain English — in ~2 KB, with zero dependencies.**

[![npm version](https://img.shields.io/npm/v/@didrod2539/croniq.svg?color=success)](https://www.npmjs.com/package/@didrod2539/croniq)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@didrod2539/croniq?label=gzip)](https://bundlephobia.com/package/@didrod2539/croniq)
[![CI](https://github.com/didrod205/croniq/actions/workflows/ci.yml/badge.svg)](https://github.com/didrod205/croniq/actions/workflows/ci.yml)
[![types](https://img.shields.io/npm/types/@didrod2539/croniq.svg)](https://www.npmjs.com/package/@didrod2539/croniq)
[![license](https://img.shields.io/npm/l/@didrod2539/croniq.svg)](./LICENSE)

</div>

"When does `0 9 * * 1-5` run next?" is a question you should never answer by
hand — and definitely shouldn't trust an LLM to guess. It's exact calendar
arithmetic (ranges, steps, month lengths, leap years, the day-of-month/day-of-week
union rule). **croniq** does it deterministically, and tells you in English.

```ts
import { parse } from "@didrod2539/croniq";

const cron = parse("0 9 * * 1-5");      // 09:00, weekdays
cron.next();                            // → Date of the next weekday 09:00
cron.describe();                        // "At 09:00, Monday through Friday"
cron.matches(new Date());               // is right now a run time?
```

---

## Why croniq?

- 🎯 **Correct, not approximate.** Leap days, month lengths, and the Vixie-cron day-of-month **OR** day-of-week rule are all handled. Verified against known schedules in tests.
- ⚡ **Fast.** Field-jumping (not minute-by-minute scanning), so sparse schedules like `0 0 29 2 *` resolve in microseconds.
- 🗣️ **Human-readable.** `describe()` turns an expression into a sentence — great for dashboards and confirmations.
- 🧩 **Full syntax.** 5 fields or 6 (with seconds), ranges `1-5`, lists `1,3,5`, steps `*/15`, names `JAN`/`MON`, `?`, and macros `@daily`/`@hourly`/`@weekly`/…
- 🕒 **UTC or local.** One flag.
- 🪶 **~2 KB gzipped, zero dependencies.** Node 18+, Deno, Bun, Workers and the browser.

## Install

```bash
npm install @didrod2539/croniq
# or: pnpm add @didrod2539/croniq  /  yarn add @didrod2539/croniq
```

> Published under the `@didrod2539` npm scope (the unscoped name `croniq` was
> blocked by npm for being too close to `cron`). The import name matches the
> package name; everything else is identical.

Ships ESM **and** CommonJS:

```ts
import { parse, isValid } from "@didrod2539/croniq";        // ESM / TypeScript
const { parse, isValid } = require("@didrod2539/croniq");   // CommonJS
```

## CLI

```bash
npx @didrod2539/croniq "0 9 * * 1"             # describe + the next runs
npx @didrod2539/croniq describe "*/15 * * * *" # "Every 15 minutes"
npx @didrod2539/croniq next "0 0 1 * *" --iso -n 5
npx @didrod2539/croniq valid "61 * * * *"      # exit 1 if invalid
```

The bundled command is **`croniq`**. Commands: `describe`, `next`, `prev`,
`valid`. Options: `-n/--count`, `--utc`, `--iso`. `valid` sets the exit code.

## Usage

### Next & previous runs

```ts
const cron = parse("*/15 * * * *");       // every 15 minutes

cron.next();                              // next run after now
cron.next(new Date("2026-03-15T12:07Z")); // → 2026-03-15T12:15:00Z
cron.prev();                              // previous run before now
cron.nextN(5);                            // next five runs (Date[])
```

### Validate

```ts
isValid("0 9 * * 1-5"); // true
isValid("99 * * * *");  // false
```

### Describe

```ts
parse("0 9 * * 1-5").describe();   // "At 09:00, Monday through Friday"
parse("*/15 * * * *").describe();  // "Every 15 minutes"
parse("@hourly").describe();       // "At 0 minutes past every hour"
parse("0 0 1 1 *").describe();     // "At 00:00, on day-of-month 1, in January"
```

### UTC vs local

```ts
parse("0 0 * * *", { utc: true }).next();  // midnights in UTC
parse("0 0 * * *").next();                 // midnights in the host's local time
```

### Seconds field

A 6-field expression adds a leading seconds column:

```ts
parse("*/30 * * * * *").next(); // every 30 seconds
```

## Supported syntax

| Field         | Values            | Allowed                       |
| ------------- | ----------------- | ----------------------------- |
| Second (opt.) | 0–59              | `* , - /`                     |
| Minute        | 0–59              | `* , - /`                     |
| Hour          | 0–23              | `* , - /`                     |
| Day of month  | 1–31              | `* , - / ?`                   |
| Month         | 1–12 or `JAN`–`DEC` | `* , - /`                   |
| Day of week   | 0–7 or `SUN`–`SAT` (0 & 7 = Sunday) | `* , - / ?` |

Macros: `@yearly` / `@annually`, `@monthly`, `@weekly`, `@daily` / `@midnight`,
`@hourly`. When both day-of-month and day-of-week are restricted, a date matches
if **either** matches (standard cron behavior).

> `L`, `W`, and `#` modifiers aren't supported yet — [open an issue](https://github.com/didrod205/croniq/issues) if you need them.

## API

| Member | Description |
| ------ | ----------- |
| `parse(expr, opts?)` | Parse into a `Cron` (throws on invalid input). |
| `isValid(expr, opts?)` | `true`/`false` without throwing. |
| `cron.next(from?)` / `cron.prev(from?)` | Next/previous run `Date`. |
| `cron.nextN(n, from?)` / `cron.prevN(n, from?)` | Arrays of run times. |
| `cron.matches(date?)` | Does the date satisfy the expression? |
| `cron.describe()` | Plain-English description. |
| `cron.minutes` / `.hours` / `.daysOfMonth` / `.months` / `.daysOfWeek` / `.seconds` | Parsed value arrays. |

## Comparison

|                          | `croniq` | hand-rolled date math | heavier cron libs |
| ------------------------ | :------: | :-------------------: | :---------------: |
| next / prev computation  |    ✅    |          ⚠️           |        ✅         |
| Plain-English describe   |    ✅    |          ❌           |        ⚠️ (separate lib) |
| Validation               |    ✅    |          ❌           |        ✅         |
| Zero dependencies        |    ✅    |          ✅           |        ⚠️         |
| ~2 KB gzipped            |    ✅    |          —            |        ❌         |

## Contributing

Contributions are very welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md)
and our [Code of Conduct](./CODE_OF_CONDUCT.md).

```bash
git clone https://github.com/didrod205/croniq.git
cd croniq
npm install
npm test
```

## 💖 Sponsor

`croniq` is free and MIT-licensed, built and maintained in spare time. If it
saved you from debugging a misfiring schedule, please consider supporting it —
every bit helps keep the project healthy.

- ⭐ **Star this repo** — the simplest, free way to help others discover it.
- 🍋 **[Sponsor via Lemon Squeezy](https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010)** — one-time or recurring support.

> Sponsoring? Open an issue and we'll add your name/logo here. Thank you! 🙏

## License

[MIT](./LICENSE) © croniq contributors
