1. スマホの目覚ましは「曜日」までしか知らない
平日は 6 時半、土曜は 8 時、日曜は鳴らさない。これをスマホの目覚ましで組むと、アラームが 2 つになる。ここまではいい。困るのは祝日の月曜で、たいていの目覚ましは曜日しか見ていないので 6 時半に鳴る。前の晩に手でオフにして、火曜の朝に戻し忘れる——誰でも一度はやっている。
カレンダーに入れても同じことが起きる。多くのカレンダーの「繰り返し」は 1 つの予定に時刻を 1 つしか持てず、曜日ごとに時刻を変えたければ予定を分ける(Google Calendar API の制約、「書けないスケジュール」の博物誌のカタログ 17)。分けた予定は、やはり祝日を知らない。
「曜日で時刻が違う」は、時刻の違う列を 2 本作って束ねると書ける。祝日はその後で足す。
2. 時刻の違う列を 2 本作って、束ねる
まず曜日だけで書く:
premise JP { calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon }
@JP
alarmWk = everyDay |> filter(d => not (weekday(d) == Sat or weekday(d) == Sun)) |> at(T06:30)
alarmSat = everyDay |> filter(d => weekday(d) == Sat) |> at(T08:00)
alarmWk | alarmSat
everyDay |> filter(d => …)で「毎日」から曜日で絞る。weekday(d)は日 d の曜日。at(T06:30)は、日の列に時刻を貼る(4 勤 4 休の回でも出勤 8 時に使った)。|は 2 本の列の和。日曜はどちらの列にも無いので、束ねても出てこない。
10 月の 2 週間ぶんを出す:
$ kairos list --from 2026-10-05 --to 2026-10-19 alarm.kairos 2026-10-05T06:30 2026-10-06T06:30 2026-10-07T06:30 2026-10-08T06:30 2026-10-09T06:30 2026-10-10T08:00 2026-10-12T06:30 2026-10-13T06:30 2026-10-14T06:30 2026-10-15T06:30 2026-10-16T06:30 2026-10-17T08:00
土曜の 8 時と日曜の空きは出ているが、10 月 12 日(月・スポーツの日)に 6 時半が立っている。スマホの目覚ましと同じ失敗を、ここまでの式も犯している。
3. 祝日は「土曜扱い」にする
祝日の表を前文(premise)に置き、「平日」を「土日でも祝日でもない日」に、「8 時の日」を「土曜か祝日。ただし日曜は除く」にする:
premise JP {
calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon
national = [2026-01-01, 2026-01-12, 2026-02-11, 2026-02-23, 2026-03-20, 2026-04-29, 2026-05-03..2026-05-06,
2026-07-20, 2026-08-11, 2026-09-21..2026-09-23, 2026-10-12, 2026-11-03, 2026-11-23] covering: 2026..2026
satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
bizDay = everyDay \ (satSun | national)
}
@JP
sat = everyDay |> filter(d => weekday(d) == Sat)
sun = everyDay |> filter(d => weekday(d) == Sun)
lateDay = (sat | national) \ sun
(bizDay |> at(T06:30)) | (lateDay |> at(T08:00))
national = [...] covering: 2026..2026が祝日の表。この表は 2026 年しか覆っていない、と 自分で言っている(祝日テーブルは黙って腐る)。\は差。everyDay \ (satSun | national)で「土日でも祝日でもない日」=bizDay。lateDay = (sat | national) \ sunは「土曜か祝日、ただし日曜は除く」。和と差で鳴らす日の集合を 先に作り、最後にatで時刻を貼る。
同じ 2 週間:
$ kairos list --from 2026-10-05 --to 2026-10-19 alarm.kairos 2026-10-05T06:30 2026-10-06T06:30 2026-10-07T06:30 2026-10-08T06:30 2026-10-09T06:30 2026-10-10T08:00 2026-10-12T08:00 2026-10-13T06:30 2026-10-14T06:30 2026-10-15T06:30 2026-10-16T06:30 2026-10-17T08:00 # 被覆サマリ # JP.national covering 2026-01-01..2026-12-31 残走路 74 日
10 月 12 日が 8 時になった。末尾の「残走路 74 日」は、表の終わり(12 月 31 日)まで、評価した範囲の末尾から数えてあと 74 日という意味である。表の寿命が出力に出る。
ゴールデンウィークで確かめる:
$ kairos list --from 2026-05-01 --to 2026-05-11 alarm.kairos 2026-05-01T06:30 2026-05-02T08:00 2026-05-04T08:00 2026-05-05T08:00 2026-05-06T08:00 2026-05-07T06:30 2026-05-08T06:30 2026-05-09T08:00
5 月 2 日(土)は 8 時、3 日は日曜なので無し、4・5・6 日(みどりの日・こどもの日・振替休日)は 8 時、7 日(木)から 6 時半に戻る。2026 年を通すと 313 回——6 時半が 244 回、8 時が 69 回(土曜 52 回と、平日に当たる祝日 17 回)。
4. 「次に鳴るのは」——next
目覚ましで知りたいのは、一覧より「次」である。next が、指定日以降の最初の点を返す:
$ kairos next -n 3 --from 2026-10-09 alarm.kairos 2026-10-09T06:30 2026-10-10T08:00 2026-10-12T08:00
金曜 6 時半、土曜 8 時、日曜を飛ばして、祝日の月曜 8 時。-n を付けなければ 1 点だけ出る。これを毎日 1 回引いて、時計なりカレンダーなりに渡せば、目覚ましの「予約」はできる(§6)。
5. 表が切れた日に、何が起きるか
会社の年末年始休業(12 月 29 日〜1 月 3 日)を表に足し、年をまたいで出してみる:
companyOff = [2026-12-29..2027-01-03] covering: 2026..2027 bizDay = everyDay \ (satSun | national | companyOff) … lateDay = (sat | national | companyOff) \ sun
$ kairos list --from 2026-12-28 --to 2027-01-05 alarm.kairos 2026-12-28T06:30 2026-12-29T08:00 2026-12-30T08:00 2026-12-31T08:00 # ⚠ 範囲外 2027-01-01..2027-01-05(JP.national covering 2026-01-01..2026-12-31)
12 月 29〜31 日は休業日なので 8 時。そして 1 月 1 日から先が無い。祝日の表が 2026 年までなので、2027 年 1 月 1 日が「平日」か「祝日」かを式は決められない。決められない日の目覚ましを、黙って 6 時半で鳴らすことも、黙って消すこともせず、「この区間は表の外」と言って止まる(黙りの既定の反対)。2027 年の祝日を表に足せば、同じ式がそのまま動く。
スマホの目覚ましは、来年の祝日を知らないまま 6 時半に鳴る。違いはそこにある。
6. 射程——鳴らすのは誰か
- 式が出すのは「鳴る時刻の列」まで。実際に音を出すのは時計・スマホ・カレンダー(実装系)の仕事で、 言語は関与しない。今できるのは、
nextで次の点を取って通知の仕組みに渡すこと(下)と、 Playground で列を目で確かめること。 - 日曜に鳴らさないのは、日曜の列を作らないことで表した。「鳴らさない日」を否定で書くより、 「鳴らす日」を和と差で組む方が、読み返したとき分かりやすい。
- 夏時間のある地域では、
at(T06:30)は壁時計の 6 時半である(切替日の扱いは 「1 日後」と「24 時間後」は別の量である)。日本では関係ない。
7. 動かす
通知まで通すなら、crontab からの引っ越しガイドの「予約型」がそのまま使える。毎日 0 時に next --json で当日の点を取り、at に登録する:
0 0 * * * cd ~/alarm && kairos next --json alarm.kairos | ./schedule-notify.sh
一覧を取って判定する「時計型」(月末最終営業日の自動実行まで通す)でもよい。どちらも「定義 1 ファイル・crontab 1 行」で、祝日の判断は定義の側にある。
まとめ
- 目覚ましもカレンダーも、繰り返しに持てる時刻は 1 つで、祝日を知らない
- Kairos では時刻の違う列を 2 本作って
|で束ねる。at(T06:30)が時刻を貼る - 祝日は表(
national … covering:)に置き、「平日」を差\、「8 時の日」を和と差で集合として作ってから時刻を貼る - 2026 年は 313 回(6:30 が 244、8:00 が 69)。次に鳴る点は
next - 表が切れた年は、黙って鳴らさず「表の外」と言う。来年の祝日を足せば式はそのまま
▶ Playground で定義を実行する——2026 年 10 月 5 日からの 2 週間が出る(10 月 12 日が 8 時)。コメントにある「曜日だけ」の行と入れ替えると、10 月 12 日が 6 時半に戻る。--from を 2026-05-01 にすればゴールデンウィークが見える。ブラウザ内で完結し、何も送信しない。
Kairos はスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント