きっかけ
ポートフォリオ 1 件目の Cron TZ Viewer は「cron 式 → 次回実行時刻」を各 TZ で並べるツールでした。使ってるうちに気づいたのは、多くの人が困っているのはその前の段階だということ:
「毎週月曜 9:00 に実行したい。cron でどう書く?」
これを手打ちで書けるのは cron に慣れている人だけで、「あれ、曜日は左から何番目だっけ」「日曜は 0 それとも 7?」と毎回調べ直します。
構造(UI)から cron 式を組み立てる方向のツールが欲しい。というわけで姉妹作を作りました。入力と出力を逆にしただけ、と思いきや、こっちは cron パーサよりもずっと書きやすいという発見付き。
作ったもの
Cron Builder — https://sen.ltd/portfolio/cron-builder/
5 フィールド(分 / 時 / 日 / 月 / 曜日)それぞれに 6 モード:
-
毎回 (
*) -
〜ごと (
*/5) -
特定の値 (
30) -
リスト (
0,15,30,45) -
範囲 (
9-17) -
範囲+間隔 (
9-17/2)
入力を変えるたびに、cron 式と日本語/英語の説明が即時更新されます。出来上がったら "[次回実行時刻を見る →]" ボタンで Cron TZ Viewer に渡して実際の発火時刻を確認。
vanilla JS + HTML + CSS、ゼロ依存、ビルドツール不要。ロジックは 160 行(大半は日英の label テーブル)。node --test で 14 ケース。
パースより構築のほうが圧倒的に簡単
cron のパーサを書くと、*/5, 1-10, 1,3,5, 1-20/3, *, 数字 の 6 パターンを文字列から判別する必要があります。正規表現のオンパレードで、エラーメッセージも多い。
構築の場合、ユーザーが明示的にモードを選ぶので、switch 1 発で済みます:
export function buildField(config) {
if (!config || typeof config !== 'object') return '*'
switch (config.mode) {
case 'any': return '*'
case 'every': return `*/${config.step}`
case 'value': return String(config.value)
case 'list': return config.values.join(',')
case 'range': return `${config.from}-${config.to}`
case 'rangeStep':return `${config.from}-${config.to}/${config.step}`
default: return '*'
}
}
入力の型が構造化されているので、「これは範囲なのか毎回なのか」を推測する必要がない。パーサの仕事が全部 UI 側のモード選択に移ったことで、ロジックがここまで短くなる。
これは双方向ツールを書くと気づく構造で、「入力が自然言語なら難しい、入力が構造化 AST なら簡単」という、パーサ vs ジェネレータの対称性の話。同じ問題を逆から解くと、だいぶ景色が違います。
buildCron は buildField を 5 回呼ぶだけ
トップレベル関数はただの合成:
export function buildCron({ minute, hour, dom, month, dow }) {
return [
buildField(minute),
buildField(hour),
buildField(dom),
buildField(month),
buildField(dow),
].join(' ')
}
引数オブジェクトの destructuring で 5 つのフィールドを受け取って、それぞれ buildField して space で結合。cron 式がフィールド独立な設計になっているから、ここまで素直に書ける。
これが例えば AWS Events のスケジュール式のように「日と曜日のどちらかしか使えない」というクセがあると、もうちょっと条件分岐が要ります。crontab は純粋な直積なのが救い。
説明テキストはフィールド × モードの二次元テーブル
cron 式の隣に「毎週月曜 9:00」みたいな日本語説明を出すと、組み立てた直後にフィードバックが得られるので安心感が違います。この説明を生成するのは、フィールドとモードの二次元テーブルを引くだけ:
const LABELS = {
ja: {
minute: {
any: '毎分',
every: (n) => `${n}分ごと`,
value: (n) => `${n}分に 1 回`,
list: (vals) => `${vals.join(', ')}分`,
range: (a, b) => `${a}〜${b}分の間`,
rangeStep: (a, b, s) => `${a}〜${b}分の間、${s}分ごと`,
},
hour: { /* ... */ },
dom: { /* ... */ },
month: { /* ... */ },
dow: { /* ... */ },
},
en: { /* ... */ },
}
export function explainField(config, field, lang = 'ja') {
const L = LABELS[lang]
switch (config.mode) {
case 'any': return L[field].any
case 'every': return L[field].every(config.step)
case 'value': return L[field].value(config.value)
case 'list': return L[field].list(config.values)
case 'range': return L[field].range(config.from, config.to)
case 'rangeStep': return L[field].rangeStep(config.from, config.to, config.step)
}
}
月は 1月 2月 みたいに名前付きで出すので、value モードの実装だけ専用:
const MONTH_JA = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
month: {
value: (n) => `${MONTH_JA[n - 1]}`,
// ...
}
曜日も同じ (月, 火, 水...)。cron の数字をそのまま見せるのでなく人間語に変換するのは、入門者が cron を覚える道具としての価値を底上げします。
姉妹ツール連動
Builder で組み立てた式を、Viewer 側に URL クエリで渡せる:
const viewerUrl = new URL('https://sen.ltd/portfolio/cron-tz-viewer/')
viewerUrl.searchParams.set('cron', cronString)
window.open(viewerUrl.toString(), '_blank')
Cron TZ Viewer 側は entry 001 の時点で ?cron= クエリをサポートしていたので、何の改修もなしにリンク連動が動きます。URL クエリベースでツールを繋ぐのは、静的サイト同士の軽い協業として強力。状態の共有に JSON API もサーバも要らない。
テスト
node --test で 14 ケース。入出力の対が主:
test('every 5 minutes', () => {
assert.equal(
buildCron({
minute: { mode: 'every', step: 5 },
hour: { mode: 'any' },
dom: { mode: 'any' },
month: { mode: 'any' },
dow: { mode: 'any' },
}),
'*/5 * * * *'
)
})
test('weekdays 9 to 17', () => {
assert.equal(
buildCron({
minute: { mode: 'value', value: 0 },
hour: { mode: 'range', from: 9, to: 17 },
dom: { mode: 'any' },
month: { mode: 'any' },
dow: { mode: 'range', from: 1, to: 5 },
}),
'0 9-17 * * 1-5'
)
})
test('explain range in ja', () => {
assert.equal(explainField({ mode: 'range', from: 9, to: 17 }, 'hour', 'ja'), '9〜17時の間')
})
おわりに
SEN 合同会社の ポートフォリオシリーズ 100+ の 12 件目です。シリーズ 1 件目の Cron TZ Viewer と合わせて「cron 双方向ツールセット」として機能します。
- 📦 レポジトリ: https://github.com/sen-ltd/cron-builder
- 🌐 ライブデモ: https://sen.ltd/portfolio/cron-builder/
- 🏢 会社: https://sen.ltd/
バグ報告・改善案、歓迎です。
