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);@dailygibi 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'teama30'da,13:40'taama17: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 ownL,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'tebut30'da,13:40'tabut17: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.