はじめに:なぜ「Cron表現解析」が業務で効くのか
業務システムで「毎日3時に」「5分ごとに」「平日の9時だけ」といったスケジュールを柔軟に設定したいとき、
文字列1本でスケジュールを表現できるのが「Cron表現(Cron式)」です。
C# でこれを扱えるようになると、
「設定ファイルに書かれた Cron を読み取って、次の実行時刻を計算する」
「ユーザーが入力した Cron が正しいかチェックする」
といった“実務っぽい”処理を自前で組めるようになります。
ここでは、まず Cron の基本形をざっくり押さえたうえで、
「自作の超シンプル Cron パーサ」と「次の実行時刻を求めるロジック」を、
初心者向けにかみ砕いて説明していきます。
Cron表現の基本:5フィールドをざっくり理解する
標準的な5フィールド形式
よく使われる標準的な Cron は、スペース区切りの5フィールドです。
分 時 日 月 曜日
* * * * *
それぞれの意味は次のとおりです。
分:0〜59
時:0〜23
日:1〜31
月:1〜12(JAN〜DEC の略称を許す実装も多い)
曜日:0〜7(0と7が日曜、1が月曜…)
そして、各フィールドには「特定の数字」だけでなく、*(全部)、1-5(範囲)、*/5(ステップ)、1,3,5(列挙)などが書けます。
ここでは、初心者向けにまず「数字」と「*」と「カンマ(,)」だけを扱うシンプル版から始めます。
最小限のCronパーサを作る(数字・*・カンマだけ)
方針:文字列 → 5つの「許可値セット」に分解する
「Cron表現解析」の一番のコアは、
文字列を「各フィールドごとの“許可される値の集合”」に変換することです。
例えば、
"0 9 * * 1,2,3,4,5"
なら、
分:{0}
時:{9}
日:{1〜31全部}
月:{1〜12全部}
曜日:{1,2,3,4,5}
というイメージです。
これを C# のクラスに落とし込んでみます。
public class SimpleCronExpression
{
public HashSet<int> Minutes { get; }
public HashSet<int> Hours { get; }
public HashSet<int> Days { get; }
public HashSet<int> Months { get; }
public HashSet<int> DayOfWeeks { get; }
private SimpleCronExpression(
HashSet<int> minutes,
HashSet<int> hours,
HashSet<int> days,
HashSet<int> months,
HashSet<int> dayOfWeeks)
{
Minutes = minutes;
Hours = hours;
Days = days;
Months = months;
DayOfWeeks = dayOfWeeks;
}
public static SimpleCronExpression Parse(string cron)
{
if (cron == null) throw new ArgumentNullException(nameof(cron));
var parts = cron.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 5)
throw new FormatException("Cron は 5 フィールドである必要があります。");
var minutes = ParseField(parts[0], 0, 59);
var hours = ParseField(parts[1], 0, 23);
var days = ParseField(parts[2], 1, 31);
var months = ParseField(parts[3], 1, 12);
var dayOfWeeks = ParseField(parts[4], 0, 7); // 0,7=日曜
return new SimpleCronExpression(minutes, hours, days, months, dayOfWeeks);
}
private static HashSet<int> ParseField(string field, int min, int max)
{
var set = new HashSet<int>();
if (field == "*")
{
for (int i = min; i <= max; i++)
{
set.Add(i);
}
return set;
}
var tokens = field.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens)
{
if (!int.TryParse(token, out int value))
throw new FormatException($"数値に変換できません: {token}");
if (value < min || value > max)
throw new FormatException($"値が範囲外です: {value} (許容: {min}〜{max})");
set.Add(value);
}
if (set.Count == 0)
throw new FormatException("フィールドが空です。");
return set;
}
}
C#ここでの重要ポイントは、
「Cron文字列を“そのまま解釈し続ける”のではなく、一度“構造化された形(許可値セット)”に変換する」
という発想です。
一度この形にしてしまえば、
「ある DateTime が Cron にマッチするか?」
「次にマッチする時刻は?」
といった判定が、かなり書きやすくなります。
DateTime が Cron にマッチするか判定する
IsMatch(DateTime) を実装する
先ほどの SimpleCronExpression に、
「この日時が Cron の条件を満たしているか?」を判定するメソッドを足してみます。
public bool IsMatch(DateTime dt)
{
int minute = dt.Minute;
int hour = dt.Hour;
int day = dt.Day;
int month = dt.Month;
int dow = (int)dt.DayOfWeek; // 0:日曜, 1:月曜, ...
if (dow == 0)
{
DayOfWeeks.Add(7); // 0 と 7 を両方サポートしたい場合の工夫
}
return Minutes.Contains(minute)
&& Hours.Contains(hour)
&& Days.Contains(day)
&& Months.Contains(month)
&& DayOfWeeks.Contains(dow);
}
C#使い方の例です。
var cron = SimpleCronExpression.Parse("0 9 * * 1,2,3,4,5"); // 平日9:00
DateTime dt1 = new DateTime(2026, 2, 18, 9, 0, 0); // 水曜
DateTime dt2 = new DateTime(2026, 2, 18, 10, 0, 0);
DateTime dt3 = new DateTime(2026, 2, 22, 9, 0, 0); // 日曜
Console.WriteLine(cron.IsMatch(dt1)); // true
Console.WriteLine(cron.IsMatch(dt2)); // false
Console.WriteLine(cron.IsMatch(dt3)); // false
C#ここでの重要ポイントは、
「Cron の各フィールドと DateTime の各要素を、1対1で比較しているだけ」
というシンプルさです。
Cron の“魔法感”が薄れて、
「ただの条件集合なんだな」と分かってくるはずです。
次の実行時刻を求める:GetNextOccurrence
方針:今から1分ずつ進めて、最初にマッチした時刻を返す
実務で一番使いたくなるのは、
「この Cron に従うと、今から見て次に実行すべき時刻はいつか?」
という関数です。
まずは、分解能を「1分」と割り切って、
「今の時刻の1分後から順に、マッチするまで進める」という素直な実装をしてみます。
public DateTime GetNextOccurrence(DateTime from)
{
DateTime current = from.AddMinutes(1).AddSeconds(-from.Second).AddMilliseconds(-from.Millisecond);
for (int i = 0; i < 60 * 24 * 366; i++)
{
if (IsMatch(current))
{
return current;
}
current = current.AddMinutes(1);
}
throw new InvalidOperationException("1年以内にマッチする時刻が見つかりませんでした。");
}
C#使い方の例です。
var cron = SimpleCronExpression.Parse("0 9 * * 1,2,3,4,5"); // 平日9:00
DateTime now = new DateTime(2026, 2, 18, 8, 30, 0); // 水曜 8:30
DateTime next = cron.GetNextOccurrence(now);
Console.WriteLine(next); // 2026/02/18 9:00:00
C#この実装は決して効率的ではありませんが、
「Cron の条件に合う時刻を、時間軸上で“前から順に探しているだけ”」という構造が、
とても分かりやすいはずです。
実務で本気のスケジューラを作るなら、
フィールドごとに飛び先を計算してもっと効率化しますが、
まずはこの“総当たり1分刻み”版で、動くイメージを掴むのが良いです。
実務での現実的な選択肢:ライブラリを使う
NCrontab / Cronos などの利用
ここまで「自作パーサ」を書いてきましたが、
実務では、Cron の仕様は思った以上に奥が深く、*/5 や 1-10/2、JAN や MON-FRI、? など、
実装ごとの拡張も含めるとかなり複雑になります。
そのため、実際のプロダクトでは
- NCrontab
- Cronos
といった既存ライブラリを使うのが現実的です。
ただし、「中で何をやっているか」の感覚を持たずにライブラリだけ使うと、
意図しないスケジュールを組んでしまったときに原因が追えません。
だからこそ、ここまでのような「超シンプル版」を一度自分で書いてみて、
Cron は「文字列 → 許可値セット → DateTime とのマッチ判定 → 次の時刻探索」
という流れで動いている
というイメージを持っておくと、ライブラリの挙動も理解しやすくなります。
まとめ:「Cron表現解析ユーティリティ」は“文字列のスケジュールを時間軸に落とす橋”
Cron 表現解析の本質は、
「コンパクトな文字列で書かれたスケジュール」を、
「具体的な日時(DateTime)」に落とし込む橋渡しです。
ここで押さえておきたいポイントを、言葉でまとめるとこうなります。
Cron は「分・時・日・月・曜日」の5フィールドで、各フィールドは“許可される値の集合”だと捉える。
文字列を一度「許可値セット」にパースしてしまえば、DateTime がマッチするかどうかの判定は単純な集合チェックになる。
「次の実行時刻」は、“今から未来に向かって時間を進め、最初にマッチした時刻”として定義できる。
まずは 1分刻みの総当たりで実装してみて、動くイメージを掴んでから効率化やライブラリ利用を考える。
ここまで理解できれば、
設定ファイルや管理画面に書かれた Cron を、
自分の C# コードの中で“ちゃんと解釈して動かせる”ところまで、
一気に近づけます。
