karsh-cron

Language
TypeScript
Stars
0
Forks
0
Updated
License
MIT

Topics: crontypescript

Open on GitHub ↗

README

karsh-cron

Bir cron ifadesini okur, bozuksa nerede ve neden bozuk olduğunu söyler, seçilen saat diliminde sonraki çalışmaları yaz saati kurallarıyla hesaplar ve ifadeyi Türkçe ya da İngilizce bir cümleye çevirir. Bağımlılığı yok, dört dosya.

Reads a cron expression, says where and why it is broken, works out the next runs in a given time zone with daylight saving handled, and turns the expression into a Turkish or English sentence. No dependencies, four files.


Türkçe

Ne işe yarar

30 9 * * 1-5 yazan alanın yanında "Hafta içi her gün saat 09:30'da" yazmasını sağlar. İfade bozuksa hangi alanın hangi parçasının neden kabul edilmediğini söyler. Ve "peki gerçekten ne zaman çalışacak?" sorusuna, saatin ileri alındığı geceyi de hesaba katarak cevap verir.

Üç iş, dört dosya, üçü de tek başına kullanılabilir:

  • parse — beş alan ya da başında saniyeyle altı alan; ay ve gün adları; liste, aralık, adım ve açık adım (5/15 = 5'ten itibaren 15'te bir); @daily gibi kısayollar. Quartz'a özgü L, W, # ve yıl alanı genel bir hata yerine adıyla reddedilir — yapıştırılan bir ifadenin kabul edilmemesinin en sık sebebi budur ve kullanıcı bunu bilmeyi hak eder.
  • next — sonraki çalışmalar, IANA saat diliminde ve klasik cron'un (Linux'ta cronie) yaz saati kurallarıyla: saat ileri alınırken kaybolan saatte sabit saatli iş, saatin sıçradığı anda çalışır; geri alınırken tekrar eden saatte yalnızca ilk turda çalışır.
  • describe + turkish — ifadenin ne yaptığı, cümle olarak. Türkçe çevrilmedi, yazıldı: ek sayının okunuşuna göre seçilir (15'te ama 30'da, 13:40'ta ama 17:30'da), gün aralıkları "-dan … -a" alır, listeler "ve" ile biter.

Kurulum

Paket derlenmiş dosya taşımaz; TypeScript kaynağı olduğu gibi yayımlanır (exports doğrudan src/index.ts'i gösterir), yani bir paketleyici (Vite, esbuild, webpack, Next) kullanan projede olduğu gibi çalışır. Next'te transpilePackages: ["karsh-cron"] gerekir; düz node TypeScript kaynağını çalıştıramaz. Build adımı yoktur.

npm i github:HasanKarsi/karsh-cron

Node 18 ve üzeri. Saat dilimi farkları çalışma ortamının kendi tz veritabanından, Intl.DateTimeFormat üzerinden gelir — pakette saat dilimi tablosu taşınmaz.

Kullanım

1. Oku ve cümleye çevir

import { parseCron, describeSchedule } from "karsh-cron";

const result = parseCron("30 9 * * 1-5");
if (result.ok) {
  describeSchedule(result.schedule, "tr"); // "Hafta içi her gün saat 09:30'da"
  describeSchedule(result.schedule, "en"); // "At 09:30 on weekdays"
}

2. Sonraki çalışmalar — yaz saati dahil

29 Mart 2026 gecesi Berlin'de saat 02:30 hiç yaşanmaz. Sabit saatli bir iş cronie'de atlanmaz, saatin sıçradığı anda çalışır; shift bunu söyler:

import { parseCron, nextRuns } from "karsh-cron";

const result = parseCron("30 2 * * *");
if (!result.ok) throw new Error(result.error.code);

const runs = nextRuns(result.schedule, "Europe/Berlin", Date.parse("2026-03-28T12:00:00Z"), 3);
// runs[0] -> { at: 2026-03-29T01:00:00Z, offset: 7200, shift: { kind: "gap", … } }
// runs[1] -> { at: 2026-03-30T00:30:00Z, offset: 7200, shift: null }

3. Hatayı göster, notu göster

import { parseCron, scheduleNotes, describeSchedule } from "karsh-cron";

parseCron("0 9 22-2 * *");
// { ok: false, error: { code: "reversed", field: "dayOfMonth", token: "22-2", fix: "22-31,1-2" } }

parseCron("0 9 * * MON#2");
// { ok: false, error: { code: "quartz", feature: "#", token: "MON#2" } }

const every7 = parseCron("*/7 * * * *");
if (every7.ok) scheduleNotes(every7.schedule);
// [{ code: "stepWrap", field: "minute", token: "*/7", step: 7, last: 56, first: 0, gap: 4 }]
// "Her 7 dakikada bir" değil: sayaç her saat başı sıfırlanır, son aralık 4 dakikadır.

const friday13 = parseCron("0 0 13 * 5");
if (friday13.ok) describeSchedule(friday13.schedule, "tr");
// "Hem her ayın 13'ünde hem her Cuma, saat 00:00'da" — Vixie cron'un VEYA kuralı,
// yani "ayın 13'üne denk gelen Cuma" değil.

Hiçbir fonksiyon throw etmez; parseCron sonucu ok ile ayrılan bir nesnedir. Bozuk girdi bir istisna değil, gösterilecek bir sonuçtur.

API

İmza Ne döner
parseCron(input) { ok: true, schedule } ya da { ok: false, error }. Hata kodlu: fieldCount, outOfRange, reversed, quartz, unknownMacro, zeroStep…
normalizeExpression(input) Boşlukları tekleştirilmiş ifade — "0 9 * * *" ile "0 9 * * *" tek anahtar olur.
nextRuns(schedule, zone, fromMs, count) fromMs'ten sonraki en çok count çalışma (öntanımlı 5); her biri { at, wall, offset, shift }. Elli yıllık ufukta çalışma yoksa daha az, hatta boş dizi.
isValidZone(zone) · zoneOffset(zone, epochSeconds) Saat dilimi adı tanınıyor mu; o andaki UTC farkı (saniye).
splitWall(wall) · nextWallMatch(schedule, from, lastYear) Duvar saati aritmetiği: saniyeyi tarih parçalarına ayırır, alanların izin verdiği bir sonraki duvar saatini bulur.
describeSchedule(schedule, lang) Cümle. lang: "tr" ya da "en".
describeField(field, lang) · fieldValues(field, lang) Alan alan tablo için: bir alanın anlamı ("Pazartesi–Cuma") ve açılımı ("Pzt, Sal, Çar, Per, Cum").
dayHalves(schedule, lang) İki gün alanı ayrı ayrı — VEYA kuralını açıklayan notu yazmak için.
scheduleNotes(schedule) Beklenmeyen davranışlar: dayOr, stepWrap, stepSingle, shortMonths, leapOnly, never.
compatNotes(schedule) Aynı ifadenin n8n'de (Schedule Trigger'ın kullandığı cron paketi) ve crontab'da anlamının değiştiği yerler.
partValues(name, part) Tek bir virgül parçasının açıldığı sayılar.
withEnding(n, kase) · numberEnding(n, kase) · clockEnding(hour, minute, second, kase) Türkçe hal ekleri: 15'te, 30'da, 13:40'ta. kase: loc, abl, dat, poss, possLoc, possAbl, possDat.
MACROS · FIELD_ORDER · FIELD_BOUNDS Kısayol tablosu (ve hangisini kimin bildiği), alan sırası, alan sınırları.

Neden kütüphane değil, elle yazıldı

Hazır cron paketlerinin çoğu "bir sonraki çalışma"yı verir ama iki soruya yazılı bir cevap vermez: saat ileri alındığında kaybolan saatte ne oluyor, ve iki gün alanı birlikte kısıtlandığında hangisi geçerli. Bu paket ikisini de klasik cron'un kaynağındaki davranışa (cronie, src/cron.c) göre uygular ve sonucu shift ile dayOr olarak gösterir; çünkü bu kod bir arayüz için yazıldı ve arayüz, kullanıcıya "şurada bir sürpriz var" demek zorundaydı.

Aynı sebeple ayrıştırıcı istisna atmaz: throw edilen bir hata ekrana basılamaz, { code: "reversed", field, token, fix } basılabilir. Türkçe cümle de öyle: rakamla yazılmış bir sayıya doğru eki koyan hazır bir şey yoktu — ek rakamdan değil sayının okunuşundan çıkar ("on beş" → 15'te, "otuz" → 30'da), o yüzden sayıyı okuyan küçük bir parça da buraya girdi.

Sonuç: dört dosya, sıfır bağımlılık, tarayıcıda ve Node'da aynı davranış.

Lisans

MIT — bkz. LICENSE.


English

What it is for

So that the field holding 30 9 * * 1-5 can have "At 09:30 on weekdays" beside it; so that a broken expression says which part of which field was not accepted and why; and so that "when does it actually run?" is answered with the night the clocks move taken into account.

Three jobs, four files, each usable on its own:

  • parse — five fields, or six with seconds in front; month and weekday names; lists, ranges, steps and the open step (5/15 = from 5, every 15); shorthands such as @daily. Quartz's own L, W, # and year field are refused by name rather than with a generic error — that is the usual reason a pasted expression fails, and the person pasting deserves to know.
  • next — the next runs, in an IANA time zone, with classic cron's (cronie's) daylight saving rules: in the hour spring forward removes, a fixed-time job runs at the moment the clock jumps; in the hour that happens twice, it runs on the first pass only.
  • describe + turkish — what the schedule does, as a sentence. The Turkish is written rather than translated: the case ending follows how the number is spoken (15'te but 30'da, 13:40'ta but 17:30'da).

Install

No build step and no compiled files: the package ships TypeScript source and exports points straight at src/index.ts, so it works as-is in any project built with a bundler (Vite, esbuild, webpack, Next). On Next, add transpilePackages: ["karsh-cron"]; plain node cannot run TypeScript source.

npm i github:HasanKarsi/karsh-cron

Node 18 or newer. Offsets come from the runtime's own tz database through Intl.DateTimeFormat; no zone table ships with the package.

Usage

1. Read it, then say it

import { parseCron, describeSchedule } from "karsh-cron";

const result = parseCron("30 9 * * 1-5");
if (result.ok) {
  describeSchedule(result.schedule, "en"); // "At 09:30 on weekdays"
  describeSchedule(result.schedule, "tr"); // "Hafta içi her gün saat 09:30'da"
}

2. Next runs, daylight saving included

On 29 March 2026 the clock in Berlin never reads 02:30. A fixed-time job is not skipped by cronie — it runs the moment the clock jumps, and shift says so:

import { parseCron, nextRuns } from "karsh-cron";

const result = parseCron("30 2 * * *");
if (!result.ok) throw new Error(result.error.code);

const runs = nextRuns(result.schedule, "Europe/Berlin", Date.parse("2026-03-28T12:00:00Z"), 3);
// runs[0] -> { at: 2026-03-29T01:00:00Z, offset: 7200, shift: { kind: "gap", … } }
// runs[1] -> { at: 2026-03-30T00:30:00Z, offset: 7200, shift: null }

3. Show the error, show the note

import { parseCron, scheduleNotes, describeSchedule } from "karsh-cron";

parseCron("0 9 22-2 * *");
// { ok: false, error: { code: "reversed", field: "dayOfMonth", token: "22-2", fix: "22-31,1-2" } }

parseCron("0 9 * * MON#2");
// { ok: false, error: { code: "quartz", feature: "#", token: "MON#2" } }

const every7 = parseCron("*/7 * * * *");
if (every7.ok) scheduleNotes(every7.schedule);
// [{ code: "stepWrap", field: "minute", token: "*/7", step: 7, last: 56, first: 0, gap: 4 }]
// Not "every 7 minutes": the count restarts each hour, so the last gap is 4 minutes.

const friday13 = parseCron("0 0 13 * 5");
if (friday13.ok) describeSchedule(friday13.schedule, "en");
// "On the 13th of every month and every Friday, at 00:00" — Vixie cron's OR rule,
// not "Friday the 13th".

Nothing here throws. parseCron returns a result discriminated by ok: bad input is something to display, not an exception to catch.

API

Signature Returns
parseCron(input) { ok: true, schedule } or { ok: false, error }, the error coded: fieldCount, outOfRange, reversed, quartz, unknownMacro, zeroStep…
normalizeExpression(input) The expression with whitespace collapsed, so two spellings are one key.
nextRuns(schedule, zone, fromMs, count) At most count runs (default 5) strictly after fromMs, each { at, wall, offset, shift }. Fewer, possibly none, when there is no run within fifty years.
isValidZone(zone) · zoneOffset(zone, epochSeconds) Whether the zone name is known; the UTC offset in seconds at that instant.
splitWall(wall) · nextWallMatch(schedule, from, lastYear) Wall-clock arithmetic: seconds into date parts, and the next wall-clock second the fields allow.
describeSchedule(schedule, lang) The sentence. lang is "tr" or "en".
describeField(field, lang) · fieldValues(field, lang) For a field-by-field table: what a field means ("Monday–Friday") and what it expands to ("Mon, Tue, Wed, Thu, Fri").
dayHalves(schedule, lang) The two day fields separately, for the note that explains the OR rule.
scheduleNotes(schedule) What a schedule does that people do not expect: dayOr, stepWrap, stepSingle, shortMonths, leapOnly, never.
compatNotes(schedule) Where the same expression means something else in n8n (the cron package behind its Schedule Trigger) or in a Linux crontab.
partValues(name, part) The numbers one comma-separated item expands to.
withEnding(n, case) · numberEnding(n, case) · clockEnding(hour, minute, second, case) Turkish case endings for numbers written as digits: 15'te, 30'da, 13:40'ta. Cases: loc, abl, dat, poss, possLoc, possAbl, possDat.
MACROS · FIELD_ORDER · FIELD_BOUNDS The shorthand table (and who understands each), field order, field bounds.

Why this is written out rather than pulled in

Most cron packages give you the next run but do not write down an answer to two questions: what happens in the hour spring forward removes, and which of the two day fields wins when both are restricted. This one follows classic cron's own source (cronie, src/cron.c) for both and reports the answer as shift and dayOr — because it was written for an interface, and the interface had to be able to say "there is a surprise here".

For the same reason the parser does not throw: you cannot render a thrown error, but you can render { code: "reversed", field, token, fix }. And the Turkish sentence: nothing existing puts the right suffix on a number written in digits, because the suffix follows how the number is spoken rather than how it is written ("on beş" → 15'te, "otuz" → 30'da).

The result: four files, zero dependencies, identical behaviour in the browser and in Node.

License

MIT — see LICENSE.

← All repos