前回(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 | - | - | - | - | - |
25年10月6日
25年10月6日
25年9月29日
25年9月29日
20年8月24日
20年8月24日
16年2月14日
16年2月14日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
25年7月20日
20年8月16日
20年8月16日
20年8月16日
20年8月16日
24年6月17日
24年6月17日
wiki
Linux JM
Java API
2002年
2003年
2004年
2005年
2006年
2007年
2008年
2009年
2010年
2011年
2012年
2013年
2014年
2015年
2016年
2017年
2018年
2019年
2020年
2021年
2022年
2023年
2024年
2025年
過去日記について
アクセス統計
サーバ一覧
サイトの情報合計:
本日: