ラベル Fixed Day Number の投稿を表示しています。 すべての投稿を表示
ラベル Fixed Day Number の投稿を表示しています。 すべての投稿を表示

2024年7月2日火曜日

Fixed Day Number

前回のブログのアルゴリズムに, 年初から前月末までの総日数を計算する式:
(367 * month - 362) // 12
があった. monthを1から12まで変えて値を計算すると.
[0, 31, 61, 92, 122, 153, 183, 214, 245, 275, 306, 336]
が得られる. 演算子「//」で小数点以下を落しているが, その落された値を調べると, ( (4777 - 367 * month) // 12 の図も右に示す.)

のような図になる. どちらも2月は30日あるとする.

まず左の図に注目する. 1月から2月は下がり, 2月から3月は上がり, 4月へは下がり, ... 8月と9月へは下がり, ... つまり大の月の後は下がり, 小の月の後は上がるらしい.

計算して見ると下がる量は0.4167, 上がる量は0.5833である.

もう一度式を見ると, monthが1増えると, 367 / 12 つまり 30.5833 増える. これと30及び31の差であった. これで納得できるが, Calendrical Calculation にはこういう説明があった.
(7 * m - 2) // 12 + 30 * (m - 1)
この第2項 30 * (m - 1) はすべて小の月として前月末までの総日数.

第1項はm = 1, 2, ..., 12について,
0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6
  31 30 31 30 31 30 31 31 30 31 30
である(上の行), 下の行は1月から11月までの日数で, こうして見ると, 大の月の 上で上の行の値は1増える. つまり小の月とした時の誤差を得ている.

そこで上の第1項と第2項を足すと, (367 * m - 362) // 12になるのであった.

2月を30日にしたという発想がよかったのだ. 一方, 年末から計算する方の式はこう導く.

欲しい値は1月, 2月, ..., 12月で, 2月も30日とするから
[367, 336, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31]
である. 30の倍数の列
[360, 330, 300, 270, 240, 210, 180, 150, 120, 90, 60, 30]
に誤差の列
  [7, 6, 6, 5, 5, 4, 4, 3, 2, 2, 1, 1]
  
を足したものだ. これは7から年初からの計算の誤差を引いたのもだ. つまり,
7-(7 * m - 2) // 12
でこれを(13 - m) * 30 に足す. これを一纏めにしたい.

まず下の計算の1行目のように, 床関数を天井関数にする. そして1を足して床関数に戻す. しかしmの値によっては, 整数になることがあり, 1を足すと失敗する. 従って, 2行目のように, 1の代りに 11/12を足してみる. 3行目はそれに30日の倍数を足したところ. 最後に分数の外に ある部分を分子に足す. すると(4777-367m)//12が得られる.

どうだろうか.

2024年7月1日月曜日

Fixed Day Number

私の好きな本に, Edward M. ReingoldとNachum DershowitzのCalendrical Caluculation がある. 随分昔に購入したもので, 手元にあるのは2004年のThird Printing.

要するに暦に関するアルゴリズムが満載で面白い. この本では暦の変換の基準は, Fixed Day Numberといい, Gregorian暦が過去に遡って使われたとして, その1年1月1日(の正子)を 第1日とするものである. いちいちfixed day numberといわず, R.D.と いうらしい. (Rata Die)

似たようのものに, -4712年1月1日正午から起算するユリウス日があるが, これは途中でJulian暦 からGregolian暦になるので変換が面倒である.

Calendrical Caluculationにある, Gregorian暦の年月日からそのR.Dを計算する アルゴリズムを見てみよう. 本書のアルゴリズムはCommon Lispで記述されているが, 今日はPythonに翻訳する.
def fixeddaynumber(year, month, day):
    return (365 * (year - 1) + (year - 1) // 4
            - ((year - 1) // 100) + (year - 1) // 400
            + (367 * month - 362) // 12
            + (0 if (month <= 2) else
               (-1 if leap_year (year) else -2))
            + day)
     
最初の365 * (year - 1)は前年までがすべて平年であったとしての, 前年の 最後の日までの総日数である. しかし, 4年に一度閏年があるから, その分を足す. + (year - 1) // 4. Gregorian暦では, 100で整除される年は平年に するのであったからそれを引く. - ((year - 1) // 100). しかし400で 整除されるなら閏年にするからそれを足す. + (year - 1) // 400. 前年末までの総日数はこれで計算出来た.

次は前月末までの日数を計算する. 目標としては, 1月なら0. 2月なら31, 3月は 平年なら59, 閏年なら60, ... 閏年の補正は後でやることにし, 本書にあるアルゴリズム は(367 * month - 362) // 12というものだ. monthを 1から12まで変えてこの値を計算すると:
print(list(map (lambda m:(367*m-362)//12, range(1,13))))
[0, 31, 61, 92, 122, 153, 183, 214, 245, 275, 306, 336]
    
3月からは, 平年なら2, 閏年なら1多いことがわかる. 従ってこの関数の month番目を使い, monthが1, 2ならこの値そのまま, 3以降はこの値から 平年は2, 閏年は1を引いて使う.

これで前年末までと前月末までの総日数が分ったから, 後はdayを足せばよい.

閏年の補正に本書ではこういう式を使う.
def leap_year (year):
    return (year % 4) == 0 & ((year % 400)
                              in [100, 200, 300])
    
これで1年1月1日を計算すると:
print (fixeddaynumber (1, 1, 1))
1
 
なるほど. また, おととし, 開業150年と話題になった鉄道開通の日, 1872年10月14日(明治5年には日本は まだ旧暦を使っていたが, これは太陽暦に換算してある)のR.D.を計算してみる.

各々の項の値は682915, 467, -18, 4, 275, -1, 14で, R.D.は683656 である.

ところで私は何度も暦の計算のプログラムを書いたが, その年の終までの日数を計算し, その月の前までの日数を引いて日を足すのが好きだ. 上のアルゴリズムに year - 1が4回もあるのが気に食わないのである.

それで普通は年末から今月0日までの日数を引くことになるが, 大体はそこは定数の 表を利用する. しかし, Calendricalの本にあるような式が使えないかと 考えた. Calendricalの式と同様, 1,2月は特別扱いにしてもよいとして, 定数など を変えてテストした. 欲しい値は:
[[365, 366], [334, 335], 306, 275, 245, 214,
 184, 153, 122, 92, 61, 31]
    
である. 左端が1月と2月で, かっこ内は左が平年, 右が閏年. 右端が12月. その結果, (4777 - 367 * m) // 12がよいらしかった. つまり
print(list(map (lambda m:(4777 - 367 * m) // 12,
                range(1,13))))
[367, 336, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31]
    
1月, 2月は平年は2を引き, 閏年は1を引く.

ここで使う定数4777は非常にクリティカルで, 4776や 4778に変えてみると, 前者は7月が小さく, 後者は2月が大きい.
print(list(map (lambda m:(4776 - 367 * m) // 12,
                range(1,13))))
[367, 336, 306, 275, 245, 214, 183, 153, 122, 92, 61, 31]

print(list(map (lambda m:(4778 - 367 * m) // 12, 
                range(1,13))))
[367, 337, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31]

これを利用して書いたfixed day number関数は次の通り:
def fixeddaynumber2(year, month, day):
    return (365 * year + year // 4 - (year // 100)
            + year // 400 - (4777 - 367 * month) // 12
            + (0 if (month > 2) else
               (-1 (year % 400 == 0)
                if (year % 100 == 0) else
                (year % 4 == 0) -2))
            + day)
こちらで鉄道記念日を計算したのと, 本年6月30日の値の7で割った剰余とを見ると:
print (fixeddaynumber2 (1872, 10, 14))
683656

print ((fixeddaynumber2 (2024, 6, 30)) % 7)
0       
だから, R.D.を7で割った剰余が曜日になるわけだ. つまり, R.D.1の日, 1年1月1日は月曜であった.

年末から月始めまでの日数を今回のように式で計算すると, 実引数に0月とか13月が与え られても計算してしまう恐れがある. 表なら範囲外としてエラーに出来る. それを 心配したとしても, 日の値に範囲外が与えられる可能性はあるのだから, まぁ我慢することに しよう.