npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@verifyhash/cron-dialect

v0.1.0

Published

Zero-dependency, multi-dialect cron parser: Unix 5-field, Quartz 6/7-field, and AWS EventBridge — with the field-count and day-of-week (0/7=Sun vs 1=Sun) and ? no-specific-value differences parsed correctly instead of silently mis-read.

Readme

cron-dialect

A small, zero-dependency cron parser that actually knows the difference between the cron dialects people paste into it. The popular explainers (Crontab Guru and friends) understand only classic Unix 5-field cron and will silently mis-read a 6- or 7-field Quartz / Spring / AWS EventBridge expression — the fields shift, and day-of-week is numbered differently, so "every Monday" quietly becomes "every Sunday". This library parses each dialect on its own terms and refuses to guess.

This package is the parser core, plus two modules that build on the normalized model: translate (cron → English) and schedule (nextRuns, upcoming fire times). A UI lives in a later module.

Install

npm install @verifyhash/cron-dialect

Package name: @verifyhash/cron-dialect is a placeholder scope shown for publish-readiness. The final published scope and name are the owner's call and may change before the first release.

Zero runtime dependencies. Ships as CommonJS; a hand-written index.d.ts gives TypeScript callers full types out of the box.

// Single entry point — everything public is on the main module:
const {
  parseCron, CronParseError,   // parse.js
  translate, explain,          // translate.js
  nextRuns,                    // schedule.js
  fromEnglish, ACCEPTED_FORMS, // english.js
} = require('@verifyhash/cron-dialect');

parseCron('0 12 * * 1', 'unix5').fields.dayOfWeek.dayNames; // ['MON']
translate('0 12 * * 1', 'unix5', 'quartz6').expr;           // '0 0 12 ? * 2'
explain('*/15 * * * *', 'unix5');                           // 'Minute 0, 15, 30, 45, of every hour, every day.  [unix5]'
nextRuns('*/15 * * * *', 'unix5', { fromISO: '2026-03-08T09:07:30Z', count: 1 }); // ['2026-03-08T09:15:00Z']
fromEnglish('every 15 minutes', 'quartz6').expr;           // '0 */15 * * * ?'

The individual modules are also reachable as subpaths (require('@verifyhash/cron-dialect/schedule'), .../english, .../translate, .../parse) if you only want one.

Who it's for

  • Anyone writing a cron builder/validator/explainer that must support more than vanilla Unix cron.
  • Developers debugging why a Quartz or AWS schedule "runs on the wrong day" — usually the 1=Sunday vs 0/7=Sunday mismatch this library makes explicit.
  • Library authors who want a strict, pure (no DOM, no network, no filesystem) function they can run anywhere Node runs.

The dialects, and exactly how they differ

| dialect | fields | order | day-of-week | ? token | |------------------|:------:|-----------------------------------------|---------------|-----------| | unix5 | 5 | min hour dom month dow | 0–7, 0 & 7 = Sun | illegal | | quartz6 | 6 | sec min hour dom month dow | 1–7, 1 = Sun | required on exactly one of dom/dow | | quartz7 | 6 or 7 | sec min hour dom month dow [year] | 1–7, 1 = Sun | required on exactly one of dom/dow | | awsEventBridge | 6 | min hour dom month dow year | 1–7, 1 = Sun | required on exactly one of dom/dow |

The two differences that bite people most:

  • Day-of-week numbering. ... 1 means Monday in unix5 but Sunday in every Quartz/AWS dialect. The parser resolves both to a canonical days array (0 = Sunday … 6 = Saturday) plus a dayNames array, so the real weekday is unambiguous no matter which dialect you fed it.
  • The ? "no specific value" token. Quartz and AWS require a ? on exactly one of day-of-month / day-of-week (you cannot pin both). Classic Unix cron has no such token at all, so ? there is a hard error rather than a silently ignored character.

Example

const { parseCron } = require('cron-dialect');

// Same numeric "1", two different weekdays:
parseCron('0 12 * * 1', 'unix5').fields.dayOfWeek.dayNames;          // ['MON']
parseCron('0 12 ? * 1 *', 'awsEventBridge').fields.dayOfWeek.dayNames; // ['SUN']

// Quartz noon-every-day, with its leading seconds field and the required ?:
parseCron('0 0 12 * * ?', 'quartz6').fields.hour.values;            // [12]

// Honest failures instead of guesses:
parseCron('0 0 12 * * ?', 'unix5');   // throws: unix5 has no "?" token
parseCron('* * * * 8', 'unix5');      // throws: dow 8 out of range (0-7)

The normalized model

parseCron(expr, dialect) returns:

{
  dialect: 'quartz6',
  expression: '0 0 12 * * ?',
  fields: {
    second:     { field, raw, all, any, values, min, max },
    minute:     { ... },
    hour:       { ... },
    dayOfMonth: { ... },
    month:      { ... },
    dayOfWeek:  { ..., days, dayNames },   // days/dayNames only on this field
    year:       { ... }                    // only when the dialect has a year
  }
}

Per field:

  • values — the concrete integers the field matches, sorted, in that dialect's native numbering (so a Quartz Sunday is 1, a Unix Sunday is 0 and possibly 7).
  • alltrue when the field was a bare *.
  • anytrue when the field was ? (values is then empty).
  • days / dayNames — day-of-week only: the canonical weekday set (0 = Sunday) and its three-letter names, unifying the numbering schemes.
  • min / max — the field's inclusive native range.

Supported field syntax: *, single values, ranges a-b, lists a,b,c, steps */n, a/n (from a to the max, by n), and a-b/n. Month and day-of-week accept the usual three-letter names (JAN, MON, …), case-insensitive.

API

Every export listed here is reachable from the main module (require('@verifyhash/cron-dialect')); the "module" column is the file that also exposes it as a subpath import.

| export | module | signature | what it does / returns | |--------|--------|-----------|------------------------| | parseCron | parse.js (main) | parseCron(expr, dialect) | Parses and normalizes expr for the given dialect into the field model documented above. Throws CronParseError on anything invalid (wrong field count, out-of-range value, a token illegal in that dialect, a malformed step, an unknown dialect) — it never silently accepts garbage. | | translate | translate.js | translate(expr, from, to) | Re-emits expr from the from dialect into the to dialect, returning { expr, warnings }. warnings is an array of { code, message } objects, one per lossy or gotcha-prone conversion (injected seconds, an injected ?, the 1=Sun↔0/7=Sun day-of-week renumbering, a dropped year field). Throws CronParseError if the source expr does not parse. | | explain | translate.js | explain(expr, dialect) | Returns a plain-English string describing the schedule, e.g. "At 12:00, on Monday. [unix5]". The reading is dialect-correct: it resolves the weekday from the canonical dayNames, so a Quartz 2 and a Unix 1 both read as "Monday" — the numbering gotcha is neutralized in the wording itself. Throws CronParseError on an invalid expr. | | nextRuns | schedule.js | nextRuns(expr, dialect, opts) | Returns an array of the next opts.count upcoming fire times as UTC ISO-8601 instant strings. Honors the Quartz seconds field and the dialect day-of-week numbering (it re-uses parseCron, never re-derives). Timezone-aware and DST-deterministic (see below). Throws CronParseError on an invalid expr; returns [] honestly when a finite (year-pinned) expression has no future fires. | | fromEnglish | english.js | fromEnglish(phrase, dialect) | The reverse direction: turns a constrained English phrase into a cron expression that is correct for dialect. Returns { expr, reading } on success or { error, nearest } on an unrecognized/ambiguous phrase — it returns errors as values (never throws) and never emits a guessed expression. | | CronParseError | parse.js (main) | (Error subclass) | The error type thrown by parseCron, translate, explain, and nextRuns on invalid input. Exported so callers can catch (e) { if (e instanceof CronParseError) … }. | | ACCEPTED_FORMS | english.js | (string array) | The complete list of English grammar forms fromEnglish accepts (['every minute', 'every hour', … , 'on day N of the month']) — handy for building an autocomplete or a help panel without hard-coding the list.

explain vs translate warnings. explain bakes the gotcha-awareness into its wording (it always reads the real weekday, whatever the dialect number). The machine-readable list of what changed in a conversion lives on translate(...).warnings — that is the array you surface to a user as inline "heads-up, this differs" notices.

fromEnglish(phrase, dialect)

The mirror of explain (which goes cron → English). It reads a small, fixed grammar and emits a cron expression that is correct for the requested dialect — the seconds field, the trailing year field, the ? token, and the 0/7=Sun vs 1=Sun day-of-week numbering are all inherited from translate, never re-derived. It is deliberately strict: anything outside the grammar is an explicit error, never a guessed expression.

const { fromEnglish } = require('cron-dialect/english');

fromEnglish('every 15 minutes', 'unix5');
// { expr: '*/15 * * * *',  reading: 'Minute 0, 15, 30, 45, ...  [unix5]' }

// Same phrase, dialect-correct output — note the seconds field and the ?:
fromEnglish('every 15 minutes', 'quartz6');        // { expr: '0 */15 * * * ?',  reading: ... }
fromEnglish('every 15 minutes', 'awsEventBridge'); // { expr: '*/15 * * * ? *',  reading: ... }

// The 1=Sunday shift is applied automatically for Quartz/AWS:
fromEnglish('at 9am on weekdays', 'unix5');        // { expr: '0 9 * * 1-5', ... }        Mon-Fri = 1-5
fromEnglish('at 9am on weekdays', 'quartz6');      // { expr: '0 0 9 ? * 2,3,4,5,6', ... } Mon-Fri = 2-6

// Honest failure — NEVER a bogus expr:
fromEnglish('sometimes in the afternoon', 'unix5');
// { error: 'could not parse "sometimes in the afternoon"', nearest: 'at HH:MM (e.g. "at 3:00pm")' }

Accepted grammar (the complete list — case-insensitive; a trailing period is ignored). An at HH:MM time may be combined with any day form, in either word order (at 9am on weekdays == on weekdays at 9am).

| phrase | meaning | example emitted (unix5) | |--------|---------|-------------------------| | every minute | every minute | * * * * * | | every hour | top of every hour | 0 * * * * | | every day | daily at midnight | 0 0 * * * | | every N minutes | minute step */N (N 1–59) | */15 * * * * | | every N hours | hour step */N (N 1–23) | 0 */5 * * * | | at HH:MM | daily at that time; optional am/pm, or midnight / noon | 0 9 * * * | | daily at HH:MM | same as at HH:MM | 30 14 * * * | | weekly at HH:MM | every Sunday at that time | 0 9 * * 0 | | monthly at HH:MM | day 1 of the month at that time | 15 18 1 * * | | on weekdays | Monday–Friday | 0 0 * * 1-5 | | on weekends | Saturday & Sunday | 0 0 * * 0,6 | | on <day names> | comma / and-separated names (Monday, Wed and Fri) | 0 0 * * 1,3,5 | | on day N of the month | day-of-month N (N 1–31) | 0 0 N * * |

Day forms default to midnight (00:00) when no at time is given. Frequency forms (every N minutes / every N hours / every minute / every hour) set their own minute/hour and cannot be combined with an at time — doing so is an error, not a silent guess.

Return contract.

  • Success{ expr, reading }, where reading is explain(expr, dialect) (the dialect-correct human reading of the emitted expression).
  • Unparseable or ambiguous{ error, nearest }. error is a plain-English message; nearest is the closest accepted grammar form (a hint, honestly not a promise it means the same thing). On failure no expr is ever returned — the function never emits a bogus expression to cover an input it didn't understand. fromEnglish returns these errors as values (it does not throw).

nextRuns(expr, dialect, { fromISO, count = 5, tz = 'UTC' })

Returns an array of the next count fire times, as UTC ISO-8601 instant strings (2026-03-08T09:15:00Z, whole seconds). It does not re-parse fields — it calls parseCron(expr, dialect) and walks the normalized model, so Quartz seconds and the 1=Sun vs 0/7=Sun day-of-week numbering are honored exactly as the parser normalizes them.

const { nextRuns } = require('cron-dialect/schedule');

nextRuns('*/15 * * * *', 'unix5', { fromISO: '2026-03-08T09:07:30Z', count: 3 });
// [ '2026-03-08T09:15:00Z', '2026-03-08T09:30:00Z', '2026-03-08T09:45:00Z' ]

// Seconds field (Quartz) fires at :30, not :00:
nextRuns('30 0 12 * * ?', 'quartz6', { fromISO: '2026-07-21T00:00:00Z', count: 1 });
// [ '2026-07-21T12:00:30Z' ]

// Same numeric "1", different weekday -> different dates:
nextRuns('0 12 * * 1',   'unix5',   { fromISO: '2026-07-21T00:00:00Z', count: 1 }); // Monday
// [ '2026-07-27T12:00:00Z' ]
nextRuns('0 0 12 ? * 1', 'quartz6', { fromISO: '2026-07-21T00:00:00Z', count: 1 }); // Sunday
// [ '2026-07-26T12:00:00Z' ]
  • fromISO — the instant to start from; only fires strictly after it are returned. Pass a fixed value for deterministic output (it defaults to the host clock only when omitted).
  • tz — an IANA timezone name (default UTC). Matching is against wall-clock time in that zone; the returned instants are the corresponding UTC moments. E.g. 0 12 * * * in America/New_York returns ...T16:00:00Z in summer (EDT, UTC-4).
  • An unparseable expression throws the parser's CronParseError (never a silent []). A finite year set entirely in the past returns [] honestly (the expression parses; there are simply no future fires).

Timezone / DST rule (pinned & deterministic). Fire times are matched in wall-clock time, then converted to UTC using the zone's offset at that instant. Across a DST transition:

  • Spring-forward gap (a wall time that never happens, e.g. 02:30 when 02:00→03:00 is skipped): that fire is omitted — not shifted to 03:30, not fired at the boundary.
  • Fall-back overlap (a wall time that happens twice, e.g. 01:30 when 02:00→01:00 repeats): the fire happens once, at the first (earlier, still-daylight) occurrence, and is not duplicated.

Given a fixed fromISO, nextRuns is fully deterministic and never reads the host timezone. A UTC (or any non-DST) zone is never affected by the rule above.

Honest limits

  • The Quartz L (last), W (nearest weekday), and # (nth weekday-of-month) tokens are not yet supported and currently raise a CronParseError rather than being parsed. They are planned; until then the library errors honestly instead of mis-parsing them.
  • Reverse ranges (5-1) are rejected rather than interpreted as wrap-around.

Errors

On anything invalid — wrong field count for the dialect, an out-of-range value, a token illegal in that dialect (? in Unix, a missing/duplicate ? in Quartz/AWS), a malformed step, or an unknown dialect — parseCron throws a CronParseError (also exported). It never silently accepts garbage.

How to run the tests

cd tools/cron-dialect
npm test

That single command runs four files of hand-verified golden vectors, in order:

  • parse-vectors — each dialect, the day-of-week numbering differences, the ? rules, and every honest-error case.
  • translate-vectors — cross-dialect translate (including the injected seconds/? and the day-of-week renumbering warnings) and explain's plain-English readings.
  • schedule-vectorsnextRuns: quarter-hour and month-boundary walks, the Quartz seconds field, the 1=Sun vs 0=Sun date-difference case, timezone matching, and the spring-forward/fall-back DST rules.
  • english-vectorsfromEnglish: every accepted grammar form across the dialects, and the honest { error, nearest } failures for unparseable input.

Each file prints ok — N assertions passed and exits non-zero on any mismatch, so npm test fails fast on the first bad file. No network, no filesystem, no dependencies — pure Node, runnable offline.

License

MIT.