# cron-explain-ja

[![npm](https://img.shields.io/npm/v/cron-explain-ja.svg)](https://www.npmjs.com/package/cron-explain-ja)
[![CI](https://github.com/buko106/cron-explain-ja/actions/workflows/ci.yml/badge.svg)](https://github.com/buko106/cron-explain-ja/actions/workflows/ci.yml)

cron 式と日本語を相互変換するライブラリ / CLI。ランタイム依存パッケージはありません。

ブラウザから試せる[デモページ](https://www.buko106.tokyo/cron-explain-ja/)があります。

**cron 式は UTC のサーバーで動くもの、日本語は `tz`（既定 `Asia/Tokyo`）の時刻**として
扱います。「日本語で書いた予定を UTC のサーバーの crontab に貼る」「UTC の crontab を
日本時間で読む」ためのタイムゾーン変換が入ります。

```ts
parse("毎日午後1時").expression; // '0 4 * * *'   JST 13:00 → UTC 04:00
explain("0 4 * * *"); //            '毎日午後1時'  UTC 04:00 → JST 13:00
```

変換したくない場合は `tz: "UTC"`（CLI では `--tz UTC`）を指定します。

## CLI

インストールせずに npx から実行できます。パッケージ名は `cron-explain-ja`、
コマンド名は `cron-ja` です。

```bash
$ npx cron-explain-ja explain "0 4 * * 1-5"
平日の午後1時

$ npx cron-explain-ja parse "毎日午後1時"
0 4 * * *
```

繰り返し使うならインストールして `cron-ja` で呼びます（以下の例はこの形で書きます）。

```bash
npm i -g cron-explain-ja      # プロジェクトに入れるなら npm i -D cron-explain-ja
```

### コマンド

```
cron-ja <command> [args] [options]

Commands:
  explain   <expr>      cron式を日本語にする
  parse     <text>      日本語をcron式にする
  validate  <expr>      cron式を検証する
  next      <expr>      次回の実行日時を表示する
  (省略)    <input>     入力を自動判定して explain または parse
```

引数を省略して標準入力をパイプすると、1 行ずつ処理します。

```bash
$ cron-ja parse "毎日午後1時"
0 4 * * *

$ cron-ja explain "0 4 * * 1-5"
平日の午後1時

$ cron-ja explain "0 4 * * 1-5" --detailed
平日の午後1時

  UTC 0 4 * * 1-5  →  Asia/Tokyo 0 13 * * 1-5

  分      0       0分
  時      13      午後1時
  日      *       毎日
  月      *       毎月
  曜日    1-5     平日

次回:
  2026-09-07 (月) 13:00
  2026-09-08 (火) 13:00
  2026-09-09 (水) 13:00

$ cron-ja explain "0 9 * * 1-5" --tz UTC     # 変換しない
平日の午前9時

$ cron-ja parse "毎日"
0 0 * * *
warn: 「毎日」は何時ですか？ → '9' としました（confidence: 0.6）
      --default-hour で変更できます

$ cron-ja validate "0 25 * * *"
error: 時 フィールドの値 25 は範囲外です (0-23)
  0 25 * * *
    ^^

$ cron-ja explain "0 9-17 * * 1-5"           # 変換できない式
error: Asia/Tokyo（時差 +9:00）では日付をまたぐ時刻とまたがない時刻が混ざるため、cron 式に書き換えられません

$ crontab -l | grep -v '^#' | cut -d' ' -f1-5 | cron-ja explain --tz UTC
平日の午前9時
毎日午前3時
15分ごと
```

### オプション

すべてのコマンドで使えます。

| オプション | 説明 |
| --- | --- |
| `--json` | JSON で出力する（複数行入力では JSONL） |
| `-q`, `--quiet` | 結果のみ出力する |
| `--no-color` | 色を無効化する（`NO_COLOR` 環境変数でも可） |
| `-h`, `--help` | ヘルプを表示する |
| `-v`, `--version` | バージョンを表示する |

`explain`:

| オプション | 既定 | 説明 |
| --- | --- | --- |
| `--style <casual\|formal>` | `casual` | `formal` は分を 2 桁で必ず表示する |
| `--hour <12h\|24h>` | `12h` | `24h` は「15時」形式 |
| `--seconds` | — | 6 フィールド（秒付き）として解釈する |
| `--tz <zone>` | `Asia/Tokyo` | 日本語側のタイムゾーン。IANA 名か `local` |
| `--show-tz` | — | 文末にタイムゾーン名を併記する |
| `--detailed` | — | フィールド別の内訳と次回 3 回を表示する |

`parse`:

| オプション | 既定 | 説明 |
| --- | --- | --- |
| `--tz <zone>` | `Asia/Tokyo` | 日本語を読む壁時計のゾーン。出力は常に UTC |
| `--strict` | — | 曖昧なら失敗する（exit 3） |
| `--default-hour <n>` | `9` | 時刻が読み取れないときに使う時 |
| `--allow-extensions` | — | `L` / `#` / `W` の使用を許可する |
| `-i`, `--interactive` | — | 曖昧な点を対話で確認する |

`validate`:

| オプション | 既定 | 説明 |
| --- | --- | --- |
| `--seconds` | — | 6 フィールド（秒付き）として解釈する |

`next`:

| オプション | 既定 | 説明 |
| --- | --- | --- |
| `--seconds` | — | 6 フィールド（秒付き）として解釈する |
| `--tz <zone>` | `Asia/Tokyo` | 表示に使うゾーン。式は常に UTC として数える |
| `-n`, `--count <n>` | `3` | 表示件数 |
| `--from <iso-datetime>` | — | 起点の日時（ISO 8601） |
| `--format <human\|iso\|unix>` | `human` | 出力形式 |

`--tz` は `explain` と `parse` では変換に、`next` では表示に使います。
`--from` にタイムゾーンを書かなかった場合（`2026-06-14T02:00` など）は `--tz` の壁時計として
読みます（`Z` やオフセットを書けばそのとおりに解釈します）。

```bash
$ cron-ja next "0 4 * * *" -n 2              # 表示は Asia/Tokyo
2026-09-07 (月) 13:00
2026-09-08 (火) 13:00

$ cron-ja next "0 4 * * *" --tz UTC --format iso -n 1
2026-09-07T04:00:00Z
```

結果は stdout、note / warn は stderr に出るため、`$(cron-ja parse "...")` で結果だけを
受け取れます。`--json`（複数行入力では JSONL）でスクリプトから扱えます。

終了コード: `0` 成功 / `1` 内部エラー / `2` 入力エラー / `3` 曖昧（`--strict` 時）。

## ライブラリ

```bash
npm i cron-explain-ja
```

```ts
import { explain, parse } from "cron-explain-ja";

explain("0 9 * * 1-5", { tz: "UTC" }); // '平日の午前9時'
parse("平日の朝9時", { tz: "UTC" }).expression; // '0 9 * * 1-5'
```

### `explain(expression, options?): string`

cron 式を 1 文の日本語にします。不正な式は `CronSyntaxError` を投げます。

以下の例は既定の `tz`（`Asia/Tokyo`）で動かしたものです。入力の cron 式は UTC なので、
出力の時刻は 9 時間進んでいます。

```ts
explain("*/15 * * * *"); // '15分ごと'
explain("0 0 1 * *"); // '毎月1日の午前9時'
explain("0 */3 * * *"); // '3時間ごと（毎時0分）'
explain("0 0-8 * * 1-5"); // '平日の午前9時から午後5時まで毎時0分'
explain("0 0 * * *", { hour: "24h" }); // '毎日9時'
explain("0 0 * * *", { style: "formal" }); // '毎日午前9時00分'
explain("0 4 * * *"); // '毎日午後1時'（UTC 04:00 を JST で読む）
explain("0 9 * * *", { tz: "UTC" }); // '毎日午前9時'（変換しない）
explain("0 4 * * *", { showTimeZone: true }); // '毎日午後1時（Asia/Tokyo）'
```

| オプション | 既定 | 説明 |
| --- | --- | --- |
| `style` | `'casual'` | `'formal'` は分を 2 桁で必ず表示する |
| `hour` | `'12h'` | `'24h'` は「15時」形式 |
| `seconds` | `false` | 6 フィールド（秒付き）として解釈する |
| `tz` | `'Asia/Tokyo'` | cron 式(UTC)を読み替えるゾーン。IANA 名か `'local'` |
| `showTimeZone` | `false` | 末尾に `（zone）` を付ける |
| `collapseWeekdays` | `true` | `1-5` を「平日」、`0,6` を「週末」に畳む |

`L` / `#` / `W` は Quartz 拡張として解釈しますが、**曜日番号は Unix cron の 0 = 日曜**に
従います。Quartz は 1 = 日曜なので、Quartz 向けに書かれた式をそのまま渡すと曜日が 1 つ
ずれます。7 も日曜なので、`0-7` は「日曜から日曜」ではなく全曜日を指します。

```ts
explain("0 1 * * 6L"); // '最終土曜日の午前10時'（Quartz の意味では最終金曜日）
```

マクロは `@daily /usr/bin/foo` のようにコマンドが続いていても解釈します（crontab の行を
そのまま渡せます）。ただし `@reboot` は起動時に一度だけ実行される指定で日時を持たないため、
`CronSyntaxError` を投げます。

### `explainDetailed(expression, options?): Explanation`

フィールド別の内訳、正規化済みの式、注意書き、次回 3 回を返します。

```ts
const detail = explainDetailed("0 0 * * 1-5");
detail.text; // '平日の午前9時'
detail.expression; // '0 0 * * 1-5'（入力(UTC)を正規化。JAN は数値に、単独の 7 は 0 になる）
detail.localExpression; // '0 9 * * 1-5'（tz の壁時計。text と fields はこちらの説明）
detail.fields.dayOfWeek; // { raw: '1-5', kind: 'range', values: [1,2,3,4,5], text: '平日' }
detail.next; // [Date, Date, Date]
```

### `parse(text, options?): ParseResult`

日本語を cron 式にします。解釈が一意でないときは黙って決めず、`confidence`
（0.0–1.0）と `ambiguities` で返します。

日本語は `tz`（既定 `Asia/Tokyo`）の壁時計として読み、返る `expression` は UTC です。

```ts
parse("平日の朝9時"); // { expression: '0 0 * * 1-5', confidence: 1, ... }
parse("毎時9分と39分"); // { expression: '9,39 * * * *', confidence: 1, ... }
parse("毎月10日から20日までの午後3時"); // { expression: '0 6 10-20 * *', confidence: 1, ... }
parse("3か月ごとの1日の午前9時"); // { expression: '0 0 1 */3 *', confidence: 1, ... }
parse("休日の朝9時"); // { expression: '0 0 * * 0,6', confidence: 0.8, ... }
parse("毎晩9時"); // { expression: '0 12 * * *', confidence: 1, ... }
parse("毎晩6時"); // { expression: '0 9 * * *', confidence: 0.9, ... }（午後6時と読む）
parse("毎日"); // { expression: '0 0 * * *', confidence: 0.6, ambiguities: [{ field: 'hour', ... }] }
parse("こんにちは"); // { expression: null, confidence: 0, ... }
```

cron の曜日では厳密に表せない語は、近い曜日に寄せたうえで理由を `notes` に書き、
`confidence` を下げます。「休日」は祝日も指しますが cron には祝日が無いため、週末（`0,6`）
として解釈します（祝日には実行されません）。

「朝」「夜」などの時間帯の語と一緒に書かれた 12 時までの数字は、その語が指す時間帯に近い方の
読みを採ります。「夜6時」は午前 6 時ではなく午後 6 時（18 時）です。語の指す時間帯から離れた
ぶんは `notes` に書いて `confidence` を下げます。「毎晩」「毎朝」は「毎日」＋時間帯の語として
読みます。

| オプション | 既定 | 説明 |
| --- | --- | --- |
| `strict` | `false` | 曖昧なら `ParseAmbiguityError` を投げる |
| `defaultHour` | `9` | 時刻が読み取れないときに使う時 |
| `timeOfDay` | — | 「朝」などの既定の時を上書きする |
| `allowExtensions` | `false` | `L` / `#` / `W` の使用で減点しない |
| `tz` | `'Asia/Tokyo'` | 日本語を読む壁時計のゾーン。IANA 名か `'local'`。出力は常に UTC |

### `validate(expression, options?): ValidationResult`

構文エラーを投げずに返し、実行されない日付なども警告します。

```ts
validate("0 25 * * *").errors; // [{ field: 'hour', message: '…範囲外です (0-23)', position: 2 }]
validate("0 0 30 2 *").warnings; // ['2月30日は存在しないため、このジョブは実行されません']
```

### `next(expression, options?): Date[]`

次回の実行日時を返します。**式は UTC として解釈します**（実行環境の `TZ` には依存しません）。
返るのは絶対時刻なので、どのタイムゾーンで表示するかは呼び出し側の裁量です。
日と曜日の同時指定は標準 cron と同じく OR 条件、`L` / `#` / `W` を含む式は空配列を返します。

```ts
next("0 9 * * 1-5", { count: 5, from: new Date() });
```

## タイムゾーン

`tz` には `'Asia/Tokyo'` `'America/New_York'` のような IANA のゾーン名か、実行環境の
タイムゾーンを指す `'local'` を渡せます。`'UTC'` を渡すと変換は起きません。

```ts
parse("毎日午後1時").expression; // '0 4 * * *'
parse("毎日午後1時", { tz: "UTC" }).expression; // '0 13 * * *'
explain("0 20 * * 1"); // '毎週火曜日の午前5時'（UTC 月曜 20:00 = JST 火曜 05:00）
```

日をまたぐときは曜日や日も一緒にずれます。`parse` の `localExpression`、`explainDetailed`
の `localExpression` に、変換前後のもう一方の式が入ります。

### 変換できない式

cron 式は「フィールドごとに独立した値の集合」しか表せないため、ずらした結果がその形に
収まらないことがあります。近い式を黙って返すと半年ずれた予定になるので、
`CronTimeZoneError` を投げます。

| 場面 | 例（`Asia/Tokyo`） |
| --- | --- |
| 日をまたぐ時刻とまたがない時刻が混ざる | `0 9-17 * * 1-5`（18:00〜翌02:00 になる） |
| 日が 1 日ずれて月をまたぐ | `0 20 31 * *`、`0 20 * 1 *` |
| 日がずれて `L` / `#` / `W` の意味が変わる | `0 20 L * *` |
| 分の繰り上がりが時刻によって変わる | `0,30 4 * * *` を `+5:45` のゾーンへ |
| ゾーンに夏時間がある | `America/New_York` |

夏時間のあるゾーンは、冬と夏で時刻が変わるため 1 つの cron 式に落ちません。書き換えた式は
crontab に貼られたあと何年も動くので、オフセットが変わらないことは**今年と翌年**にわたって
確かめます。変換せずに読みたい場合は `tz: "UTC"` を指定してください。

## 開発

開発用ツールチェーン（tsdown / vitest 5）は **Node 22.18 以上**を必要とします。
公開されるパッケージ自体は `engines` のとおり Node 18.3 以上で動作し、CI の
`runtime` ジョブがビルド成果物を Node 18 / 20 で実行して検証しています。

```bash
npm ci
npm run lint && npm run typecheck && npm test && npm run build
```

詳細な設計は [DESIGN.md](https://github.com/buko106/cron-explain-ja/blob/main/DESIGN.md) を
参照してください（npm のパッケージには同梱していません）。

## リリース

バージョンは [release-please](https://github.com/googleapis/release-please) が
[Conventional Commits](https://www.conventionalcommits.org/ja/v1.0.0/) から決めます。
main に載るコミットメッセージ（PR は squash merge なので PR のタイトル）に種別を付けます。

| 種別 | 上がるバージョン |
|---|---|
| `fix:` / `perf:` | patch |
| `feat:` | minor |
| `feat!:`、または本文に `BREAKING CHANGE:` | major |
| `docs:` `build:` `ci:` `test:` `refactor:` `chore:` | 上がらない（CHANGELOG にも載らない） |

main にマージすると Release ワークフローがリリース PR（`chore(main): release X.Y.Z`）を
作ります。この PR は package.json の version と CHANGELOG.md を書き換えるだけで、
マージするとタグ・GitHub Release・npm への publish がまとめて走ります。npm への publish は
Trusted Publishing（OIDC）で行うので、npm のトークンは保管していません。

## 互換性

[Semantic Versioning](https://semver.org/lang/ja/) に従います。1.0.0 以降、次のものを
公開 API として扱います。

- `explain` / `explainDetailed` / `parse` / `validate` / `next` の引数とオプション
- `Explanation` / `ParseResult` / `ValidationResult` が返すフィールドの型と意味
- `CronSyntaxError` / `CronTimeZoneError` / `ParseAmbiguityError` の型
- CLI のサブコマンド、オプション名、終了コード、stdout に出る内容

次のものは対象外です。変わっても major は上げません。

| 対象外 | 理由 |
| --- | --- |
| 出力される日本語の言い回し | 不自然な説明を直すのは patch。文面に依存するなら自分でスナップショットを取ること |
| stderr の note / warn の文面 | 同上 |
| `ParseResult.tokens`（`Token` / `TokenType`） | デバッグ用。パーサを改良すると種別が増減する |
| `FieldAST` の `kind` の顔ぶれ | Quartz の `LW` など未対応の構文を後から足せるようにしておく。`switch` を書くなら `default` を用意すること |
| `confidence` の具体的な値 | 減点の重みは実例を見て調整する。閾値で使うなら余裕を持たせること |

## ライセンス

[MIT](./LICENSE)
