前回(2009年11月16日の日記参照)は経過日から月日への計算を行いました。残るは年の計算です。
前回も紹介したとおり、グレゴリオ暦の平年、閏年には下記の法則があります。
4年周期は平年、平年、平年、閏年のパターンです。
100年周期は4年周期を25回繰り返すだけですが、最後の100年目だけは閏年ではありません。
400年周期は100年周期を4回繰り返すだけですが、最後の400年目だけは閏年になります。400年以上を扱うルールはありませんので、以降400年周期で同じパターンが続きます。
図示すると下記のようになります。オレンジの四角が閏年を表しています。
各周期の日数の計算式は下記の通りです。
パターンがわかってしまえば、与えられた経過日にこのパターンがいくつ含まれているか?を計算するのみです。
では実際に経過日から年を計算してみます。しつこいですがパターンは下記4つです。経過日にこれらのパターンがいくつ含まれているか調べます。
なぜ上記パターンの数を求めれば年数が出るのか?がわかる人は次の章を飛ばして読んでください。
ここでは桁の概念についてと、年への換算にどう使うか?を補足させていただきます。
私たちにおなじみの10進数は1234のように書きますが、これってどういう意味でしょうか?
10進数の1桁目は10^0がいくつ含まれているか、2桁目は10^1がいくつ含まれているかを示します。n桁目は10^(n-1) がいくつ含まれているか?を示します。
ですから10進数で1234は(1 * 10^3 + 2 * 10^2 + 3 * 10^1 + 4 * 10^0 = 1234)という数(10進数表記)を表します。
当たり前?でもこれは m進数でも同様で、n桁目はm^(n-1) がいくつ含まれているか?を示します。
ですから8進数の1234は(1 * 8^3 + 2 * 8^2 + 3 * 8^1 + 4 * 8^0 = 668)という数(10進数表記)を表します。
経過日から年を求める場合も、これと似た考え方ができます。各パターンの数を桁と見なして(400年の桁、100年の桁、4年の桁、1年の桁)、10進数へと換算してやればよいのです。
各パターンの数(400年、100年、4年、1年)が1, 2, 3, 3であれば(1 * 400 + 2 * 100 + 3 * 4 + 3 * 1 = 615)という年(10進数表記)を表します。
このように各パターンの数を求めてあげることで、年数が計算可能なのです。
パターンAの数は(経過日 / 146097)です。余りの経過日は400年未満のどこか(0年3月1日〜399年2月29日)を表します。これはパターンBの計算に回します。
パターンBの数は(経過日 / 36524)です。余りの経過日は100年未満のどこか(0年3月1日〜99年2月28日)を表します。余りは同様にパターンCの計算に回します。
しかし146096日(399年2月29日)の場合に3となるべきところが4になり間違えてしまうため、特別に3とします。その際の余り経過日は99年2月29日ですから36525 - 1 = 36524日(開始が0日のため1引く)となります。
パターンCの数は(経過日 / 1461)です。余りの経過日は4年未満のどこか(0年3月1日〜3年2月29日)を表します。余りは同様にパターンDの計算に回します。
パターンDの数は(経過日 / 365)です。余りの経過日は1年未満のどこか(0年3月1日〜0年2月28日)を表します。ここの余りは経過日から月日を求める計算に回します。
しかし1460日(3年2月29日)の場合に3となるべきところが4になり間違えてしまうため、特別に3とします。その際の余り経過日は0年2月29日ですから366 - 1 = 365日(開始が0日のため1引く)となります。
実装する関数の仕様は下記の通りです。
この処理をC言語で書くと下記のようになります。
int date_to_year(int date, int *year, int *mod)
{
int a400, a100, a4, a;
int m400, m100, m4, m;
if (date < 0) {
return -1;
}
a400 = date / 146097;
m400 = date % 146097;
if (m400 == 146096) {
a100 = 3;
m100 = 36524;
} else {
a100 = m400 / 36524;
m100 = m400 % 36524;
}
a4 = m100 / 1461;
m4 = m100 % 1461;
if (m4 == 1460) {
a = 3;
m = 365;
} else {
a = m4 / 365;
m = m4 % 365;
}
if (year) {
*year = a400 * 400 + a100 * 100 + a4 * 4 + a;
}
if (mod) {
*mod = m;
}
return 0;
}
これで経過日から年への変換は完成ですが、まだテストが終わっていません。
月日の変換のときは、入力に対する答えがたかだか366通りと少なかったため、目でチェックしました。しかし今回は400年つまり146097日もの数をチェックしなければなりません。こんな数を目で一々チェックしていたら頭がおかしくなります。
テストの方法についてはまた次回に。
ある時点からの経過日(0基点)を年月日にする、なんていかにも学校の授業で出てきそうなアルゴリズムですが、世間では需要がないのか、名前が付いているようなアルゴリズムがありません。
練習問題にちょうど良かったので、いっちょ考えてみました。鈴木アルゴリズム完成!なーんて言えれば良かったんですけど、大した物はできませんでした。
その代わりといってはなんですが、考え方を細かくメモっていきたいと思います。今日は月日の変換、後日に年の変換を扱います。
文章をうだうだ読むのが面倒くせえ!って人のために、実装例も載せる予定です。ライセンスを特に明示していなければ、修正BSDライセンスを適用します。
年月日のルールを決めるのは暦です。現在採用されている暦の主流は1582年頃に採用が始まった「グレゴリオ歴」です。下記の性質があります。
閏年は暦と地球の季節がずれないようにする仕組み(※1)です。もっと簡単に言うと、平年より1日長い年のことです。平年/閏年は下記のルールで決まります。
変なルールに見えるかもしれませんが、人類が苦労して「時間とはなんぞや?暦とはなんぞや?」を定義してきた証なのでしょう。
(※1)地球の公転周期(1年)は自転周期(1日)の365倍と1/4日くらいです。単純に暦を1年 = 365日としてしまうと4年で1日分、季節と暦がずれてしまいます。その差を補正するために閏年が作られました。
初めに経過日→月日の変換を考えます。その際に邪魔なのが年によって長さが変わる2月です。
例として「1/1から60日後は何月何日か?」を考えてみます。平年(2月が28日間)ならば答えは3/2、閏年(2月が29日間)ならば答えは3/1です。2月の存在によって、経過日と月の対応関係がずれてしまうのです。
年によって計算方法を変えるのは面倒です。以下のように経過日の基準点を変えて一通りの計算方法で、経過日→月日を計算できるようにします。
つまり3月を一年の始まり(正確には3/1を経過日0日とする)とし、来年の2月を当年の最終月14月として考えます。こうすると経過日と各月の対応関係が固定されるため、2月の長さが変化しても計算に影響が出ません(後ほどまた説明します)
では実際に経過日から月日を計算してみます。使うのは下記のテーブル(※2)です。
月 | 各月1日の経過日 | 月の長さ |
---|---|---|
3 | 0 | 31日 |
4 | 31 | 30日 |
5 | 61 | 31日 |
6 | 92 | 30日 |
7 | 122 | 31日 |
8 | 153 | 31日 |
9 | 184 | 30日 |
10 | 214 | 31日 |
11 | 245 | 30日 |
12 | 275 | 31日 |
13 | 306 | 31日 |
14 | 337 | 28 or 29日 |
テーブルの2列目は「その月の1日を表す経過日」を表しています。調べたい経過日と、2列目で大小比較すれば、調べたい経過日が何月なのかがわかります。何月なのかわかれば何日か?も簡単に分かります。
例として100を考えましょう。
まず92(6/1)< 100 < 122(7/1)から6月であることがわかります。
さらに100 - 92(6/1)= 8ですから6/1から8日後、つまり6/9だとわかります。
この計算方法の利点は、各月の1日に対応する経過日だけ考えれば計算できることです。さきほど2月の長さは気にしなくて良いと言った理由はここにあります。
2月は最終月のため、長さが変化しても各月の1日に対応する経過日は変化しません。というより各月の1日に対応する経過日を変化させないようにするために、わざわざ2月を最後に回したのです。
実装する関数の仕様は下記の通りです。
この処理をC言語で書くと下記のようになります。
int date_to_month(int date, int *month, int *days)
{
static int m_date[] = {
0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337,
};
int m, d;
int i;
if (date < 0 || 365 < date) {
return -1;
}
for (i = 0; i < 12; i++) {
if (date < m_date[i]) {
break;
}
}
m = i + 2;
d = date - m_date[i - 1] + 1;
if (month) {
*month = m;
}
if (days) {
*days = d;
}
return 0;
}
負の値や、1年(最長366日)以上の日数に対しては正常に動作しません。エラー処理として366以上の値や負の値を渡したときにエラーを返すこととします。
きちんと変換できるか、異常値に対してエラーを返すかどうか、-3から369までの経過日を与えてテストします。
int main()
{
int m, d;
int result, i;
for (i = -3; i < 370; i++) {
result = date_to_month(i, &m, &d);
if (result == -1) {
printf("date:%3d -> error\n", i);
continue;
}
printf("date:%3d -> %2d/%2d\n", i, m, d);
}
return 0;
}
実行結果は下記の通りです。適当に端折ってあります。
date: -3 -> error date: -2 -> error date: -1 -> error date: 0 -> 3/ 1 date: 1 -> 3/ 2 ...(略)... date:100 -> 6/ 9 date:101 -> 6/10 date:102 -> 6/11 ...(略)... date:364 -> 14/28 date:365 -> 14/29 date:366 -> error date:367 -> error date:368 -> error date:369 -> error
この関数は月のパラメータに13月やら14月を返します。しかし後ほど翌年の1月、2月へ変換してつじつまを合わせますので、ここでは何も変換しません。
ある月の1日を表す経過日を求めるには、下記の漸化式を用います。
(ある月の1日を表す経過日) = (前月の1日を表す経過日) + (前月の長さ)
要はテーブルの2列目(各月1日の経過日)と3列目(月の長さ)を足すと、次の月の1日を表す経過日が計算できるということです。
経過日から月日へ変換できたところで、また今度。次は経過日から年への変換を書く予定です。
以前からmixiアプリのサンシャイン牧場というゲームをやっています。ゲームを一言で言えば「重い」です。とにかく重い。重すぎる。
その他にも下記のような特徴があります。
どうもまともな部分がないな…。
そんなサンシャイン牧場ですが、今日は新たな一面を見せてくれました。
畑は真っ白、牧場は無人、名前は「nullさん」だとさ。なんとまあ…。
最近、有料アイテム制度を追加しているようですが、こんな状態のまま金取るの?無神経すぎる。
< | 2009 | > | ||||
<< | < | 11 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | - | - | - | - | - |
合計:
本日: