平日は 6 時半、土曜と祝日は 8 時——曜日で時刻が変わる予定を、1 本の式で書く

Kairos

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 で公開しています。

コメント

タイトルとURLをコピーしました