2026年10月9日金曜日

復活祭の簡易計算

私とほぼ同年代の天文学者 堀源一郎さん(1930-2023)が提案 した簡易 月齢計算法がある. 以下「鬼鬼西」という.

私はこれを活用して, 復活祭の日取りを「簡易」計算することがよくある. こ れとは別に, 正式の復活祭の日取り(Computus Paschalis)は, 教会暦によ りその年のgolden number(黄金数), epact(歳首月齢), paschal moon(春 分満月),と次々に求めて計算するのだが, 鬼鬼西で計算するのとかなりよく同じ になる. しかし本当にいつも一致するかという一抹の不安もあり, この2つ の計算法がどの程度一致するかを調べたのが今回のブログの話題である。

これは私の自作の予定表の表紙にある数表である. 上は曜日を, 下は月齢 を計算するためのものである. 中央の12個の数は, 月定数. 左か ら1月,2月, ..., 12月に対応する. 両端のは年定数. 左は今年の, 右は来年のためのもの だ.


私としては, この表, これらの定数はは諳じていて, 自家薬籠中の物に なってはいるが, 毎年 予定表の定位置にに鎮座している.

例えは, 今年の3月21日の月齢は, (11(今年の年定数)+0(3月定数)+21(知りたい日))=32. 30を 越えているから30を減じて2になる. 1日後の22日は3, 2日後の23日は4, ...n日後 の21+n日は2+nだから, 月齢が14になるnは12で, つまり3月33日, 4月2日である.

4月2日の曜日は(1(今年の年定数)+8(4月定数)+2(知りたい日)=11. 7を超えているから 7を引いて4, つまり木曜である. この後の最初の日曜は, 3(金), 4(土), 5(日)だから, 今年 の復活祭は4月5日であった.

一方, 教会暦による方は, 2026年の黄金数が13; 次にExtended Table of Epactsで, 2000年台 で黄金数が13に対するepactを見ると11; さらにGregorian Calendar of Epactsで3月 を見ると, 11は20日のところにある. これがnew moonの日(月齢1)なので, これに13を 足してfull moonは, 3月33日, つまり4月2日と得られる. よって復活祭は4月5日.

今年は鬼鬼西の満月も, 教会暦の万でも同じ日になった. これをプログラムで計算できる ようにし, (教会暦の方はTAOCP演習問題1.3.2-14ののアルゴリズムを使った)2000年から2049年まで 両者(満月の日)を描いたのが, 次の図である.


左端に年を示す. 上端は春分満月の起きうる日付で, 左端が3月21日; 右端が4月18日である.

毎年のの鬼鬼西の満月を緑の四角, 教会暦の満月を赤の四角, 両者が同じ日の時は灰色の 四角で表示する. それぞれの年に 7日ごとに置いた短かく黒い太線は, 土曜と日曜の境を 示す. 右の欄外は, 教会暦の満月から計算した復活祭の日付けで, さらにそに右の星印 は, その年は, 鬼鬼西と教会暦から計算した復活祭の日取りが違う, つまり一抹の不安 が的中することを示す.

両計算の不一致は, この50年間に7回見えるが, それは次の場合である.
簡易計算の満月が3月21日になる. 2000,2019,2038
簡易計算の満月が日曜になる. 2018,2022,2042,2049
2025年, 2045年など, 日曜でも教会暦の方も日曜になるから, 両者は同じに なり, 復活祭も同日になるから, 簡易計算の満月が日曜ならすぐに駄目と いうわけでもない.

今のところはこのような結論で, 鬼鬼西も結構頑張っていることが分った. さらに 詳細な検討は, またの機会にしよう.

2026年8月24日月曜日

クィックソート

今年の3月5日に, オックスフォード大学の教授だったTony Hoareが死去し た. TonyとはIFIPの会議で何回か会ったが, 最後の方は, 1997年の春, 私 がオックスフォードへ行った時に大学で会い, 2000年の秋, Tonyが京都賞 で来日した時, 東京で会った.

私は昨年の秋頃の情報処理学会の雑誌に, 計算機科学はもう終ったという 話を書いて物議を醸したが, その余韻も収まらぬうちに, 計算機科学者の 大物のTony死去の通知を受け取った.

Tony Hoareといえば, クィックソートを提案したことで知られる. クィッ クソートは計算機科学の主要な話題の1つであったが, もう大学の講義で 語られることはないかも知れない. 今回のブログは,クィックソートをお さらいしよう.

クィックソートを一言でいえば, ある数列をいきなりソートするのではな く, 数列をその中央値(アルゴリズムではその値をピボットという)より小 さいものの列と大きいものの列に2分割し, それぞれをソートしてからそ の結果を連結するものである. 厳密な中央値は, ソートしてみないと分ら ないから, この説明は矛盾しているが, 実際には適当な要素を中央値であ ることを祈って使う.

長さがnの数列をソートする手間がnの二乗だとすると, 2分割した数列の ソートの手間は, 片側で4分のn二乗になり, 両方では2分のn二乗になって 効率的になるというのが趣旨である.

ソートすべき数列がピボットで大小に分割され, そのそれぞれをソートす ることで進行する様子を示したのが次の図である. 一番上は最初のソート される数列で, 左端の6をピボットにして分割したのが2段目である. ピボッ トより小さいものが左に集まり, 大きいのが右に集る. このそれぞれはも うピボットを越えて反対側にいくことはない. ピボットになった数も最 終の位置に収まった. 大小それぞれの部分列をさらに再帰的にソートする のだが, 再帰アルゴリズムでは停止条件が必要で, 今の場合はもうソート する必要のない, 長さが1以下の数列になったときである.

クィックソートでは, この分割法とピボットの選び方がキーポイントであ るが, ピボットを決めるためにプログラムがごたごたしたのでは, プログ ラムを美しくという観点からは望ましくないので, 例題としては, 運を天 に任せて数列の左端の要素をとることにする.

この方針だと, ソートすべき数列lsについて, ピボットは(car ls)で, 残 り(cdr ls)をピボットより小さい(等しいのも)のグループと, より大きい のグループにする. その安易は方法は, 例えばschemeではfilter関数を使 い,

(let ((p (car ls)) (qs (cdr ls)))
  (filter (lambda (a) (<= a p))) qs) 
  (filter (lambda (a) (< p a))) qs))
とすればよいが, こんなところでfilterのような大型命令を使うのには, ある種のうしろめたさが残る. 大型命令を使うならSchemeでは最初から
							  
(sort '(3 4 1 2 5 0) <)
と書けばよい.

ということで, 分割のプログラム part を書いてみる.
(define (part ls)
  (define (swap i j) (let ((t (list-ref ls i)))
    (list-set! ls i (list-ref ls j)) 
    (list-set! ls j t)))
  (let ((p (car ls)) (l 1) (r (- (length ls) 1)))
    (while (<= l r)
      (while (and (<= l r) (<=  (list-ref ls l) p))
         (set! l (+ l 1)))
      (while (and (<= l r) (>  (list-ref ls r) p))
         (set! r (- r 1)))
      (if (< l r) (swap l r)))
    (swap 0 r) 
    (list (take r ls) (list-ref ls r) 
          (drop (+ r 1) ls))))
いくつかの数列でやってみる. 上からランダムな数列, ピボットが最小値, ピボットが最大値, 数列の長さが1, 数列の長さが2の例である.
(part '(3 4 1 2 5 0)) => ((2 0 1) 3 (5 4))
(part '(0 1 2 3 4 5)) => (() 0 (1 2 3 4 5))
(part '(5 4 3 2 1 0)) => ((0 4 3 2 1) 5 ())
(part '(0)) => (() 0 ())
(part '(0 1)) => (() 0 (1))
(part '(1 0)) => ((0) 1 ())			
lsは分割する数列. (swap i j)はそのi番目要素とj番目要素を交換するルー チン.

letに入って, pはピボット. lとrは数列を探索為るポインタの初期化. l はピボットを除いた数列の左端から右方へ, rを超えない範囲でピボット に等しいかピボットより大きい数を探す. rは反対に数列の右端から左方 へ, lを超えない範囲でピボットより小さい数を探す.

プログラムの結果は, 小さいか等しいもののリスト, ピボット, 大きいも ののリストの3つ組で示す.

この夫々について, lとrの動きを示したのが次の図である.

それぞれの図で上はピボットの値と, 最初のlとrの位置, 次は左右からの 最初の探索が終わった時のlとrの位置. 最後が分割の終を示す. 緑と赤の 矢印で, 小さい部分列と大きい部分列の範囲を示す, 分割プログラムが出来たら, クィックソートは簡単だ. 分割プログラムが 受け取った数列の長さが1以下ならそれを返す. そうでなければ,小さいか 等しいもののリストを分割プログラムでソートしたもの,ピボットのリス ト, 大きいもののリストを分割プログラムでソートしたものを結合したも のを返すように書けばよい.
(define (quicksort ls)
  (define (qsort ls)
    (define (swap i j) (let ((t (list-ref ls i)))
      (list-set! ls i (list-ref ls j))
      (list-set! ls j t)))
   (if (<= (length ls) 1) ls
    (let ((p (car ls)) (l 1)
          (r (- (length ls) 1)))
     (while (<= l r)
       (while (and (<= l r) (<=(list-ref ls l) p))
         (set! l (+ l 1)))
       (while (and (<= l r) (> (list-ref ls r) p))
         (set! r (- r 1)))
       (if (< l r) (swap l r)))
     (swap 0 r) 
     (append (qsort (take r ls)) (list p)
       (qsort (drop (+ r 1) ls))))))
  (qsort ls))
アルゴリズムの解説を見ると, ピボットを中央値に近付けるべく, 数列か ら3個所の値を取ってその中央値を使うとか, ソートする数列の長さがあ る程度短くなると, 通常のソートに切り換えるとか書いてあるが, プログ ラムを美しくするという立場からは,そういう些細なことは採用したくな い.

ところで, 上のquicksortでは, qsortがソートすべき数列の部分列lsを受 け取り,その部分をソートして返す構造である. これに対し, quicksortが 受け取った数列全体lsに対し, leftを左端, rightを右端とするその一部 に直接交換を繰り返してソートする方式も考えられる. 下のプログラムは, 下請けのqsortは, その左右の限界を受け取り, 大域的な数列lsの数を交 換するようになっている.

ソートする数列の数が, 直接交換されて揃うのを想像するには, この方が よいように思うがどうだろうか.
(define (quicksort ls)
  (define (qsort left right)
   (define (swap i j) (let ((t (list-ref ls i)))
     (list-set! ls i (list-ref ls j))
     (list-set! ls j t)))
   (if (>  (- right left) 0)
    (let ((p (list-ref ls left)) (l (+ left 1))
          (r right))
     (while (<=  l r)
      (while (and (<= l r) (<= (list-ref ls l) p))
        (set! l (+ l 1)))
      (while (and (<= l r) (> (list-ref ls r) p))
        (set! r (- r 1)))
      (if (< l r) (swap l r)))
     (swap left r) 
     (qsort left (- r 1)) (qsort (+ r 1) right))))
  (qsort 0 (- (length ls) 1)) ls)
この2通りのquicksortの方式についての私のイメージはこうである. 始め のqsort ls方は, ある家庭で洗濯物を袋に入れて洗濯屋に依頼する. その 洗濯屋は洗濯物をこれこれこれとそれそれそれに分けて別々の袋に入れ, 下請けの洗濯屋に洗濯を依頼する. やがて両方の下請けから洗濯が出来上っ て来ると, それらを合わせて依頼した家庭に屆ける.

それに対して, qsort left rightの方は, 棚の上に多くの盆栽を並べて楽 しんでいる御隠居が, 盆栽の手入れを盆栽屋に依頼する. 盆栽屋はこれこ れこれの盆栽とそれそれそれの盆栽に分類し, それぞれを下請けの盆栽屋 に手入れを依頼する. やがて両方の下請けの盆栽屋から手入れが終わった と報告を受けると, 始めの盆栽屋は御隠居に手入れは終りましたと報告す る.

書いてみると, あまり違いがないような気もするが, 読まれた方の印象は どうであろうか.

2026年7月30日木曜日

満月の高さ

子供の頃から, 夏の月は低く, 冬の月は高いと知っていた. 太陽について いえば, 夏の太陽は高く, 冬の太陽は低い. なぜ反対なのか. 私は小学校 高学年のころ, 原田三夫「子供の天文学」が愛読書で, 天文学の基本的な ことは知っていたが, 月の高さについてはよく分らなかった. 直感的にか 出任せにかさかしらにかは知らないが, 理由は「太陽を見るのは昼で, 月 を見るのは夜でだから」と説明していた.

それから何十年の経った今から3年前の学士会会報(No.962, 2023-V)に, 国立天文台(当時, 現在は京都産業大学神山宇宙科学研究所長)の渡部潤一 さんが「中秋の名月」なる一文を寄稿されていて, それに下のような図が あった. (この図は学士会会報の図を見ながら, 私がPostScriptで描き直 した.) 図ではしっかりと冬至の満月は高く, 夏至の満月は低いと説明さ れている. そうなると, 私もこのことを確信したくなった. これまできち んと調べなかったのは, 月の軌道である白道が, 太陽の軌道である黄道と 交差していることから, 月の位置は捉え処がなく, どう動くか調べようも ないだろうと思っていたところ, 確信を以って「夏至の満月は低い」とあっ たので, 自分でも調べてみる気分になった.

手初めに, 天球に於ける月の軌道を描くことにした. 月の位置の赤経赤緯 の表は, 理科年表や天文年鑑にあるが, 紙に印刷してあるから取り出して データとして使うことは出来ない. しかしネット上には国立天文台の 「暦象年表」のページがあり, そこから2026年の毎日の太陽と月の赤経赤 緯を取り込むことが出来る. これを利用して, 太陽と月の軌道を図に描く ことにした.

暦象年表とは, 理科年表の暦部のことである. 私も詳しいことは覚えてい ないが, 終戦の頃やその後の数年は理科年表は刊行されず, 暦象年表が市 販された. 理科年表は暦部の他は毎年殆んど同じ内容なので, 必要なのは 暦象年表だけである. しかし数年経つと, 理科年表が刊行されるようにな り, 暦象年表は書店から姿を消した.

ところがある時, どこかの天文台で, 思いがけなく暦象年表を見付た. 一 般への市販はなくなったが, 天文屋さんのためには出版されていたらしい. そうこうするうちに, 暦象年表はインターネットで見られるようになっ た. ありがたいことである. (有料だが, 理科年表もインターネットで見 ることが出来る.)

この図は時間とは無関係に, 黄道(赤の曲線)と白道(青の曲線)の天球にお ける位置の動きを示す. 赤線の右端は, 黄道が赤経0hの時(春分点の時)の 赤緯は0°で, 右から1/4は, 黄道が赤経6hの時(夏至の時)の赤緯は +23.5°であるのように見る. 白道についても同じ. ただ白道の線は, 14本 くらいが殆んど重なっている.

曲線上にある点は, 暦の各月の満月の日に於ける赤経と赤緯の位置を示す. 5が2回あるのは, 5月に満月が2回あったからだ. これを見ると, 同じ暦 の月の満月は太陽と丁度12時間ずれていることも分る.

これで判明したのは, 交差角が5°とはいえ, 白道は殆んど黄道と一致して いることだった.

この図からの結論をいえば, 太陽と新月の月は, その赤経と赤緯は同じで, 満月の月の赤経は12hずれ, 赤緯は南北反対だが, 絶対値は同じというこ とである. 従って夏至の満月は, 太陽が天の赤道の上, 23.5°の高いとこ ろで南中するのに対し, 月は天の赤道の下, 23.5°低いところで南中する. 冬至はその反対である. 目出度し目出度し.

今回の検討は, 私にもうひとつの宿題の解答も与えた. それは1960年かそ の少し後だっと思うが, 物理学科の何年か後輩の佐々木不可止君がスェー デンの大学の留学を終えて帰国し, 私のところへ来て雑談していた時のこ とだ. 佐々木君はこういった. 「スェーデンのような高緯度の所では, 月 が北の空に出る.」この話はそれだけで, 私は北極圏では白夜に太陽が沈 まないように, 月も地平線に沿って回るからだろうと考えたが, それ以上 はまたいつか考えようという程度であった.

このブログの最初にあった図は, 東京あたりの北緯35°くらいを想定して 描いてある. それより赤道に近い方では, 地軸がもっと寝て, 満月の軌道 はもっと起きる. 反対に極に近くを考えると, 地軸は立って, 満月の軌道 は水平になり, 春分・秋分の満月の軌道は水平線のところに来る. 冬至の 満月は地上23.5°のあたりで東→南→西→北→と周回す るようになるから, 北の空を通ることもあるわけだ.

佐々木君は大学院が終わると, 北海道大学に就職し, 理学部化学科の教授 だった. 北大を定年で退職し, 他の私大に移ったが, 間も無く病没し た.

佐々木君は学部生の頃にも高橋研に出入りし, パラメトロン回路にも興味 をもっていた. 佐々木君の考えた閾値の論理回路の驚くべき式は, Knuth がThe Art of Computer Programming, 第4A巻の7.1.15項の「対称的Boole 関数」に紹介した.

2026年7月11日土曜日

マージソート

昨今のことは存ぜぬが, かつての計算機科学科での必須の科目は, 計算機 のアーキテクチャとアルゴリズムであった. しかし最近のパソコンの中は 当時のアーキテクチャとは掛け離れて複雑になり, プログラムを書いて計 算するよりも, 情報端末として使われている. アルゴリズムを勉強しても 使う機会はない.

ところで突然マージソートのプログラムを書くことになった. マージソート に関しては大体のことは理解している心算りであったが, TAOCPの5.2.4項 を眺めたら, 意外と巧妙なことをやっているらしい. しかし, TAOCPのプ ログラムは例によって自然言語で書いてあり, 読む気にもならない. 私と しては, 再帰プログラムでさっさと書けるのではないかと思い立ち, やっ てみたのがこのブログである.

とりさえずマージソートの核心部分のおさらいから. 下の図にはA, B, Cの 3つの図がある, 似たような絵はTAOCPにあり, データの数値もそこから 借用した.

まずAを見よう. 左方のs0とs1は既にソート済みの数列である. どちらも 左の小さい値のある方が先頭である. これをマージして右方の数列t0を作る. まず先頭の503と87を比べ, 小さい方, 87をs1から外して, それまで空の列で あったt0に移す. s1は512が先頭になる. 再び先頭同士を比べ, 小さい方, 503をt0へ移す, 今度は703と512を比べ, 512がt0へ移る. 次はs1の 677がt1へ行き, s1は空になる. このように比べている数列の一方が空に なったら, もう一方の残りの数列をすべてt0へ移す. するとs0, s1は共に 空になり, すべての数はソートされてt0へ移動した. マージソートは元の 数列のソートされたと見える部分列を2つずつマージし, 最後は1つの数列に なるまで繰り返す.

その手順の最初が図Bで, 最後が図Cである. まずBから. 上の列が与えられた ソートすべきデータ列である. 下の列は部分的にマージされた列で, 元の数列と同じ 長さの領域を別に用意する.

マージされる2つの数列は, 上の数列の両端から, s0とs1で示すように内側に向って 段々大きくなるように並んでいると考える. ソートされた列の終は次の数値が今の数値より小さく なるところである. 図では縦線で示した. 上の数列の左からをs0, 右からをs1とし, これをマージして用意した領域の左からt0として詰める.

s0とs1とでt0が出来たら, 続いてs0の内側のs2と, s1の内側のs3をマージし, t0を作った領域の, 今度は右端からt1として詰めていく. こうして行くとそのうち, 比べる べき数が同じ場所になり, tの領域も中程に1個所, 空地が残った状態になり, その残った数を空地に移すとこの回のマージは終了する.

次はこうして得られたtの数列を出発側にし, また同じようなマージを繰り返す. その 内, 図Cのように, 同じ数(この図では中程の908)を比べる状態になり, tの 領域の唯一の空地が右端に残り, そこに908を移動してソートは終了する.

後はこれを正直にプログラムにすることだ. Schemeで書いたのがこれである. 関数名がalgorithm524nなのは, TAOCPのAlgorithm524Nを書き換えたから である. 引数のrはソートすべき数列.

すぐにtwowaymergesortというサブルーチンがある. これは図BやCの1回のsからtへの マージである. 引数のrsはs(source)の数列. その長さがnで, その長さのt(target)の 数列の場所. Schemeではリストの要素を取るのに, list-refのように長い関数名を 書かなくてはならないので, 数列rsの添字iを取り出す関数rや, sのi番目をtのl 番目に移動するmoveなど, よく使う関数(手続き)を用意する. (MIT Scheme にはどういう訳かlist-set!がないので, 自分で定義して使っている.)

let文は次に使うsの左の場所の添字i, 右の添字j, tの左の添字k, 右の添字lを 初期化する.

次にtoleftとtorightがあるが, 新しい領域の左にあるtへ移すか右にあるtの 移すかで使い分ける. その夫々にfromleftとfromrightがある. これは 比較の一方が空になった後, もう一方の残りをどちらのsから移動するか で使い分ける.

(define (algorithm524n r)
 (define (twowaymergesort rs)
 (let* ((n (length rs))(rt (make-list n '())))
  (define (r i) (list-ref rs i))
  (define (move l i) (list-set! rt l (r i)))
  (define (less? a b) (let ((rsa (r a)) (rsb (r b)))
    (< rsa rsb)))
  (let ((i 0) (j (- n 1)) (k 0) (l (- n 1)))
    (define (toleft)
      (define (fromright) 
        (move k j)(set! j (- j 1))(set! k (+ k 1))
        (if (less? j (+ j 1))(toright)(fromright)))
      (define (fromleft)
        (move k i)(set! i (+ i 1))(set! k (+ k 1))
        (if (less? (- i 1) i)(fromleft)(toright)))
      (if (= i j) (begin (move k i)
        (if (< k (- n 1))(twowaymergesort rt) rt))
        (if (less? i j) 
          (begin (move k i)
            (set! i (+ i 1)) (set! k (+  k 1))
	    (if (less? (- i 1) i)
	      (toleft) (fromright)))
	  (begin (move k j)
	    (set! j (- j 1)) (set! k (+ k 1))
	    (if (less? j (+ j 1))
	      (fromleft) (toleft))))))
    (define (toright) 
      (define (fromright) 
        (move l j)(set! j (- j 1))(set! l (- l 1))
        (if (less? j (+ j 1))(toleft)(fromright)))
      (define (fromleft) 
        (move l i)(set! i (+ i 1))(set! l (- l 1))
        (if (less? (- i 1) i)(fromleft)(toleft)))
          (if (= i j) (begin (move l i)
	    (twowaymergesort rt))
      (if (less? i j)
	  (begin (move l i)
	    (set! i (+ i 1)) (set! l (- l 1))
	    (if (less? (- i 1) i)
	       (toright) (fromright)))
	  (begin (move l j)
	    (set! j (- j 1)) (set! l (- l 1))
	    (if (less? j (+ j 1))
	      (fromleft) (toright))))))
    (toleft))))
(twowaymergesort r))

(algorithm524n
  '(503  87 512  61 908 170 897 275
    653 426 154 509 612 677 765 703))
TAOCPには, merge sortは1945年頃, John von Neumannが提案したと 書いてある.

2026年3月27日金曜日

干支の計算

「今年はうま年だ」のように, なに年かが気になるのは, 現代人では正月 の頃だけであろう. また「初午」, 「土用の丑の日」, 「三の酉」のよう に日付けにも十二支が対応していることも常識であろう. 十二支は曜日の ように, 正確に順番に対応して来たので, 利用できる場面が多い.

歴史上の事件ではその年の干支を付けて, 「壬申の乱」とか「辛亥革命」 とか, 干支で識別されることがある.

しかし最近は「今年の干支はひのえうま」や, 「庚申(こうしん)の日」と か十干の方まで気にする人は絶滅危惧種かも知れない.

絶滅寸前かも知れないが, 私は時々「この前の甲子(きのえね)は何年だった か」のように, 年の干支が知りたくなることがある. また青木信仰「時と 暦」によると, 日本書紀には漏刻(水を使った時計)の使用開始が671年の「夏 四月丁卯朔辛卯」と書いてあるそうで, これも日付けにしてみたい.

干支は表Aの十干(天干ともいう)と, 表Bの十二支(地支ともいう)を順に組 み合わせたものである. 10と12は互いに素ではなく, 最大公約数が2, 最 小公倍数が60なので, 組合せは表Cの様に60個になる.

十干の方は「こう, おつ, へい, てい」位までは音読みで知っている人は いるであろう. 十二支は訓で読むと「ね, うし, とら, ..., いぬ, い」で, 大 体の人は知っているに違いない.

百人一首には喜撰法師の「わが庵は都のたつみしかぞ住む 世をうぢ山と 人はいふなり」があり, それをもじって「わが庵は都のたつ, み, うま, ひつじ, さる, とり, いぬ, い, ね,うし, とら, う」と本歌取りした人 がいた.

しかし干支の組合せは音読みの方が普通なので, この機会に表の音読み を覚えのはどうか. 次の音読みは旧かな遣いで書いてあり, 実際には 「甲」, 「庚」は「こう」,「丑」は「ちゅう」, 「卯」は「ぼう」と読 む.

さらに十干や十二支の番号も知っていると役立つ. 十干は「こう, おつ」, 「へい, てい」の ように2つずつ組にして覚えると, その組が「き, ひ, つち, か, みづ」に対応する. また十二支は「し, ちう, いん, ばう」, 「しん, し, ご, び」, 「しん, ゆう, じゅつ, がい」 と4つずつにすると, それぞれの組の先頭の年がうるう年になるので都合がよい.

さてこれからが表題の「干支の計算」である. 表Cの干支の一覧が 手元にあり, 今年の干支が「丙午」であることを知っていれば, その前の「甲子」を知るには, 2026年の「丙午」 がn=42で, 「甲子」はn=0だから, 2026-42=1984と判る.

漏刻の使用開始は, 朔(4月1日)の干支, 丁卯がn=3で, 開始日の 干支, 辛卯がn=27だから, 1+(27-3)=25日と判る.

しかし, 表Cのような一覧表なしで, 干支の番号(0<=n<60)を知りたい時 はどうするか. もちろん, ここでは十干の番号(0<=s<10)と十二支の番号 (0<=b<12)は判っているとする. つまり「丙」のs=2, 「午」のb=6 から「丙午」のn=42を知る方法の話である.

下の図を見て欲しい. 縦に3つの図がある. 上の図は, 横軸のnに対して, 赤で示すs, 青で示すbの対応を示す. 赤線, 青線の左端の塗り潰しの 丸は閉区間, 白抜きの丸は開区間を示す. つまりn=10 の時のsは一番左の赤線の値ではなく, その次の赤線の値, 0であることを 示す.

中の図は, s-bを示す. 10<=n<12でs-bが負になっているが, これを 12だけ上に移動すると10になり, 12<=n<20の線と一緒になって, 10<=n<20の区間でs-bの値が2になる. 他の負の値の場所も同様になる. それが下の図である. これで判明したことは, sとbからnを作るに は, この値の2を5倍してsを足せばよい. Schemeで書くと
(define (nval s b)
  (let ((d (- s b)))
    (+ s (* 5 (if (< d 0) (+ d 12) d))))))
丙午の場合(s=2, b=6)なら
  (nval 2 6) => 42
Schemeの関数(modulo n d)は, 0<=r<dの剰余を返すから
(define (nval s b)
  (+ s (* 5 (modulo (- s b) 12))))
と書ける.

Schemeでなくても, Pythonのように負の分子に正の剰余を返す言語なら
def nval(s,b):
    return s+5*((s-b)%12)
でもこの方式だと, 丁卯(s=3, b=3)と辛卯 (s=7, b=3)の差は, それぞれのnを 計算してから差をとることになる. sとbのそれぞれの 差からnの差が得られると嬉しい. そこでそれぞれの差分dsとdbの差とnの差の 関係を作ってみると, 次の表のようであり, これは何と, sと bからnを得るのと同じであった. 考えてみるとまぁ そうなるのは当然である.
ds=7-3=4, db=3-3=0 だから
4+5*((4-0)%12)=24
である.

これで今回の話題は完了した. 以下は蛇足である.

いつか書いたかも知れないが, グレゴリオ暦の曜日を知るには, その日のユリウス日JDに 1を足して7で割った剰余をとると, (日曜を0とする)曜日の番号を得る. この方式が干支に 使えないかとやってみると, JDから1を引いて10で割った 剰余は(甲を0とする)十干の番号になる. さらにJDに1を足して12で割った剰余は(子を0 とする)十二支の番号になる. 2025年12月21日は日曜で甲子であった. その日のJDは 2461031. それを7, 10, 12で割った剰余は6, 1, 11である. JDから干支を 計算するには, s=(JD-1)mod 10. b=(JD+1)mod 12から, このブログの式 n=s+(s-b+12)mod 12 *5で 計算してもよいが, JDを60で割った剰余が11なので, JD-11を60で割るのが 簡単である.

整理すると
曜日 (JD+1)%7
十干 (JD-1)%10
十二支 (JD+1)%12
干支 (JD-11)%60
だけ覚えればよい. 偶然だが足したり引いたるがすべて1なのが覚え易い. 私のiPhone には, 個人用電卓があり, JDを計算する機能もあるから, この情報は貴重である.

干支とは関係ないが, 暦の最重要な計算は復活祭である. puzzle and paradoxにある アルゴリズムでは, 今年は4月5日. ということは, ConwayのいうDoomsdayは今年は 土曜日だ. この話題はまたいつか.