万年カレンダーを作る(上)



はじめに

 もともとは、新暦(グレゴリオ暦)・旧暦(和暦)・ユリウス暦(ロシア暦)・干支・曜日の変換表を作ろうといろんな文献からこつこつと対応関係を集めていたのだが、ある程度データが集まり始めると生来の凝り性から完全なものを作り上げたくなってきた。しかしデータの元ネタをネット上に頼っているとどうしても抜けが生じてしまう。抜けを埋めるためには計算で補完する必要がある。はじめのうちは手計算だったのだが、やがてはパラメーターを入力として変換するプログラムとなり、ついにはデータファイルを入力に一括して変換するプログラムにまで成長した。せっかくなので、ある程度たまったプログラムを解説かたがた公開してみることにする。

 わかるかぎりのバグは潰したはずだが、プログラムの常で絶対にバグがないとは断定しかねる。もしこのプログラムを使用したことでなんらかの問題が生じてもその責には応じかねるのであらかじめご了承願いたい。この論文中で紹介するプログラムはすべて Java で記述している。とはいえ、このプログラムを書くために付け焼き刃で勉強したのでオブジェクト指向のお作法などには全く従っていない。手本にはしないほうが身のためであるとお断りしておく。またほとんどの場合メソッドだけを抜き出して例示しており、そのままでは動かない。適宜上位メソッドを用意して使用されたい。また Java の導入・使用法についても、ほかに適当なサイトがいくらもあるのでそちらを参照していただきたい。

グレゴリオ暦

 グレゴリオ暦(Gregorian Calendar)については細かい説明は不要と思う。現在日本をはじめ主要国で用いられている暦で、事実上の標準暦である。1年は365日、4年に一度(西暦で4で割り切れる年)閏年があって366日となるが、100で割り切れる年には閏年とせず、ただし400で割り切れる年は閏年とする。つまり400年間に97回閏年を置く、という置閏法である。

ユリウス通日

 「ユリウス通日」(Julian Date)と「ユリウス暦」(Julian Calendar)は名前は似ているがまったく別のものである。ユリウス通日(またはユリウス日)は天文学の世界で主に用いられており、紀元前4713年1月1日をユリウス通日1とし、以後順次日数を数えて現代に至っている。例えば2005年1月1日はユリウス通日では2453372日となる。グレゴリオ暦の次にまずユリウス通日の話をしたのには理由がある。さまざまな暦の間の変換を行なう際にはユリウス通日を仲立ちにするのが一番わかりやすいからだ。どんな暦で表現しようとも、ある一日のユリウス通日は常に同じである。つまり、グレゴリオ暦→ユリウス通日の変換プログラムと、ユリウス通日→ユリウス暦の変換プログラムを用意すれば、これを組み合わせるだけでグレゴリオ暦→ユリウス暦の変換プログラムが実現できる。別の暦法に拡張する場合でも、その暦法とユリウス通日の変換プログラムを作ればいいのだ。このやり方でプログラムを作っていくことになる。
 まず必要になるのは、グレゴリオ暦からユリウス通日に変換するプログラムだ。ここで用意した fromDate2JD() メソッドがそれである。

long fromDate2JD( long y, long m, long d )
 public static long fromDate2JD( long y, long m, long d ) {

    y = y + 4000;
    if( m < 3 ) {
        m += 12;
        y--;
        }

    long wy400q, wy400r, wy100q, wy100r, wy4q, wy4r, wm;

    wy400q = y / 400;
    wy400r = y % 400;

    wy100q = wy400r / 100;
    wy100r = wy400r % 100;

    wy4q = wy100r / 4;
    wy4r = wy100r % 4;

    wm = 0;
    if( m == 3 ) wm = 0;
    if( m == 4 ) wm = 31;
    if( m == 5 ) wm = 61;
    if( m == 6 ) wm = 92;
    if( m == 7 ) wm = 122;
    if( m == 8 ) wm = 153;
    if( m == 9 ) wm = 184;
    if( m == 10 ) wm = 214;
    if( m == 11 ) wm = 245;
    if( m == 12 ) wm = 275;
    if( m == 13 ) wm = 306;
    if( m == 14 ) wm = 337;

    long wy400 = wy400q * 146097;
    long wy100 = wy100q * 36524;
    long wy4 = wy4q * 1461;
    long wy1 = wy4r * 365;

    long jd = wy400 + wy100 + wy4 + wy1 + wm + d + 260149;

    return( jd );
    }
入力は年、月、日でいずれも数値型。実用性と計算の容易性のかねあいで紀元前4000年以降に限定している。それ以前の日付を入力した際の動作は未定義である。注意しなければいけないのは、紀元前の年は負の値となるが、紀元前1年はゼロ、紀元前100年は−99といったように調整して入力する必要があるということだ。これは西暦1年の前の年はゼロ年ではなく前1年であることに起因する。この調整をメソッド内にとりこむという考え方もあるだろうが、ここでは当メソッド外の入出力の時点で調整することとし、メソッド内にはとりこんでいない。出力はユリウス通日である。入力の数値チェックは行なっていないので、現実にはありえない日付(−5月とか32日とか)を入力してもそのまま計算してそれなりの数値を出力する。もちろん結果は使えるものではない。
 このメソッドの中では、1年を3月に始まり2月(14月)に終わる仮想の1年に変換して計算させている。これはこの種の暦変換プログラムではよく使われる手法で、閏日が2月末に置かれることからこの閏日を仮想の年末に置いて計算を単純にしようというものである。紀元前4000年に基準日を置き、そこから400年、100年、4年、1年、日といった単位がそれぞれのいくつずつあるかを計算し、日数を掛け合わせて積み上げたものを基準日のユリウス通日に加えて算出したのがこのユリウス通日である。
 つづいて用意すべきものはユリウス通日からグレゴリオ暦に戻すためのものだが、これは「グレゴリオ暦→ユリウス通日」とは少しく異なった配慮は必要になる。グレゴリオ暦は年、月、日といった数値の組み合わせであり、ユリウス通日のようなひとつの数値にはまとまらないからである。そこで用意したのは、次のようなメソッド群である。
long getGregCalY( long j )
 public static long getGregCalY( long j ) {

    long wy = j - 260149;
    long wy400q = wy / 146097;
    long wy400r = wy % 146097;
    if( wy400r == 0 ) { wy400q--; wy400r += 146097; }

    long wy100q = wy400r / 36524;
    long wy100r = wy400r % 36524;
    if( wy100r == 0 ) { wy100q--; wy100r += 36524; }

    long wy4q = wy100r / 1461;
    long wy4r = wy100r % 1461;
    if( wy4r == 0 ) { wy4q--; wy4r += 1461; }

    long wy1q = wy4r / 365;
    long wy1r = wy4r % 365;
    if( wy1r == 0 || wy1q == 4 ) wy1q--;

    long y = wy400q * 400 + wy100q * 100 + wy4q * 4 + wy1q - 4000;
    return( y );
    }
long getGregCalJD( long j )
 public static long getGregCalJD( long j ) {

    long wy = j - 260149;
    long wy400q = wy / 146097;
    long wy400r = wy % 146097;
    if( wy400r == 0 ) { wy400q--; wy400r += 146097; }

    long wy100q = wy400r / 36524;
    long wy100r = wy400r % 36524;
    if( wy100r == 0 ) { wy100q--; wy100r += 36524; }

    long wy4q = wy100r / 1461;
    long wy4r = wy100r % 1461;
    if( wy4r == 0 ) { wy4q--; wy4r += 1461; }

    long wy1q = wy4r / 365;
    long wy1r = wy4r % 365;
    if( wy1r == 0 || wy1q == 4 ) wy1r += 365;

    return( wy1r );
    }
long getMonth( long jdy )
 public static long getMonth( long jdy ) {

    long m = 0;

    if( jdy <= 31 )               m = 3;
    if( jdy <= 61 && jdy > 31 )   m = 4;
    if( jdy <= 92 && jdy > 61 )   m = 5;
    if( jdy <= 122 && jdy > 92 )  m = 6;
    if( jdy <= 153 && jdy > 122 ) m = 7;
    if( jdy <= 184 && jdy > 153 ) m = 8;
    if( jdy <= 214 && jdy > 184 ) m = 9;
    if( jdy <= 245 && jdy > 214 ) m = 10;
    if( jdy <= 275 && jdy > 245 ) m = 11;
    if( jdy <= 306 && jdy > 275 ) m = 12;
    if( jdy <= 337 && jdy > 306 ) m = 13;
    if( jdy > 337 )               m = 14;

    return( m );
    }
long getDate( long jdy, long m )
 public static long getDate( long jdy, long m ) {

    long d = 0;

    if( m == 3 )   d = jdy;
    if( m == 4 )   d = jdy - 31;
    if( m == 5 )   d = jdy - 61;
    if( m == 6 )   d = jdy - 92;
    if( m == 7 )   d = jdy - 122;
    if( m == 8 )   d = jdy - 153;
    if( m == 9 )   d = jdy - 184;
    if( m == 10 )  d = jdy - 214;
    if( m == 11 )  d = jdy - 245;
    if( m == 12 )  d = jdy - 275;
    if( m == 13 )  d = jdy - 306;
    if( m == 14 )  d = jdy - 337;

    return( d );
    }
ここで j はユリウス通日、jdy は一年のうちの第何日に当たるかをしめす数値(コンピューターの世界でユリウス日といえばもっぱらこちらである)で getGregCalJD() で得られる。この jdy をもとに月と日を計算させている。このとき、年に関して fromDate2JD() と同様の配慮が必要になる点では同じだが、月に対して fromDate2JD() では内部に隠れていた配慮が表に現れてくる。すなわち、一年を3月から14月までの仮想の一年として取り扱う必要があるのである。その値によって現実の暦にあてはめるためには年の数値の調整も必要になる。これらの調整を含んだユリウス通日からグレゴリオ暦への変換を行なうメソッドが次のものである。
String printGregCalendar( long j )
 public static String printGregCalendar( long j ) {

    long y = getGregCalY( j );
    long wd = getGregCalJD( j );
    long m = getMonth( wd );
    long d = getDate( wd, m );

    if( m > 12 ) { m -= 12; y++; }
    if( y <= 0 ) y--;
    StringBuffer sb = new StringBuffer();
    sb.append("Calendar date (Gregorian) is: ");
    if( y < 1000 && y > 99 )  sb.append(" ");
    if( y < 100 && y > 9 )    sb.append("  ");
    if( y < 10 && y > 0 )     sb.append("   ");
    if( y < 0 && y > -10 )    sb.append("  ");
    if( y < -9 && y > -100 )  sb.append(" ");
    sb.append(y);
    sb.append(".");
    if( m < 10 ) sb.append(" ");
    sb.append(m);
    sb.append(".");
    if( d < 10 ) sb.append(" ");
    sb.append(d);

    String s = sb.toString();
    return(s);
    }
この内部では getGregCalY()、getGregCalJD()、getMonth()、getDate() を部品として使用している。

ユリウス暦

 ユリウス暦はローマのユリウス・カエサルが制定したと伝えられる暦で、1年は365日、4年に一度の閏年は366日とする暦である。単純な割りには精度の高い暦だが、現行のグレゴリオ暦と比べると400年で3日のずれが生じる。16世紀に入ると年初とされた春分が本来の3月21日から10日以上もずれてきており、教会の儀式にも差し支えることから当時のローマ法王であるグレゴリウス13世が現在のグレゴリオ暦を制定し、ユリウス暦1582年10月4日の翌日をグレゴリオ暦10月15日と改めた。しかし制定したのがカソリックの首長であるローマ法王であったため、これと敵対するプロテスタントのイギリスでグレゴリオ暦が採用されたのは18世紀、ロシア正教のロシアでは革命後の20世紀になってからのことであった。大国ロシアで比較的最近まで用いられていたことからロシア暦と呼ばれることもある。
 ユリウス暦からユリウス通日に変換するプログラムの考え方はグレゴリオ暦のものと同じである。むしろ置閏法が単純であるだけにアルゴリズムも単純になった。

long fromJDate2JD( long y, long m, long d )
 public static long fromJDate2JD( long y, long m, long d ) {

    y = y + 4000;
    if( m < 3 ) {
        m += 12;
        y--;
        }

    long wy4q, wy4r, wm;

    wy4q = y / 4;
    wy4r = y % 4;

    wm = 0;
    if( m == 3 ) wm = 0;
    if( m == 4 ) wm = 31;
    if( m == 5 ) wm = 61;
    if( m == 6 ) wm = 92;
    if( m == 7 ) wm = 122;
    if( m == 8 ) wm = 153;
    if( m == 9 ) wm = 184;
    if( m == 10 ) wm = 214;
    if( m == 11 ) wm = 245;
    if( m == 12 ) wm = 275;
    if( m == 13 ) wm = 306;
    if( m == 14 ) wm = 337;

    long wy4 = wy4q * 1461;
    long wy1 = wy4r * 365;

    long jd = wy4 + wy1 + wm + d + 260117;

    return( jd );
    }
入力に関する注意点、年に関する取り扱いなどすべてグレゴリオ暦と同様である。ユリウス通日からユリウス暦に変換するプログラムも同様に、次のメソッド群からなる。
long getJuliCalY( long j )
 public static long getJuliCalY( long j ) {

    long wy = j - 260117;
    long wy4q = wy / 1461;
    long wy4r = wy % 1461;
    if( wy4r == 0 ) {
        wy4q--;
        wy4r += 1461;
        }

    long wy1q = wy4r / 365;
    long wy1r = wy4r % 365;
    if( wy1r == 0 || wy1q == 4 )
        wy1q--;

    long y = wy4q * 4 + wy1q - 4000;
    return( y );
    }
long getJuliCalJD( long j )
 public static long getJuliCalJD( long j ) {

    long wy = j - 260117;
    long wy4q = wy / 1461;
    long wy4r = wy % 1461;
    if( wy4r == 0 ) {
        wy4q--;
        wy4r += 1461;
        }

    long wy1q = wy4r / 365;
    long wy1r = wy4r % 365;
    if( wy1r == 0 || wy1q == 4 )
        wy1r += 365;

    return( wy1r );
    }
long getMonth( long jdy )
long getDate( long jdy, long m )
getMonth() メソッド、getDate() メソッドは同一である。グレゴリオ暦とユリウス暦では年単位の置閏法が異なるだけで、年内の月の配置に差はないからだ。与えられたユリウス通日に該当するユリウス暦日を表示するメソッドは以下である。
String printJuliCalendar( long j )
 public static String printJuliCalendar( long j ) {

    long y = getJuliCalY( j );
    long wd = getJuliCalJD( j );
    long m = getMonth( wd );
    long d = getDate( wd, m );

    if( m > 12 ) { m -= 12; y++; }
    if( y <= 0 ) y--;

    StringBuffer sb = new StringBuffer();
    sb.append("Calendar date (Julian) is:    ");
    if( y < 1000 && y > 99 )  sb.append(" ");
    if( y < 100 && y > 9 )    sb.append("  ");
    if( y < 10 && y > 0 )     sb.append("   ");
    if( y < 0 && y > -10 )    sb.append("  ");
    if( y < -9 && y > -100 )  sb.append(" ");
    sb.append(y);
    sb.append(".");
    if( m < 10 ) sb.append(" ");
    sb.append(m);
    sb.append(".");
    if( d < 10 ) sb.append(" ");
    sb.append(d);

    String s = sb.toString();
    return(s);
    }
上記の部品を利用している点も、また同じである。

七曜

 七曜(月火水木金土日)はもともと古代バビロニアに起源を持ち、西方ではユダヤ教と結びつきやがてキリスト教世界で一般的に用いられることになった。東方ではインドで仏典に取り入れられ、中国を経て空海が日本に伝えたという。以後もっぱら寺院内で用いられ明治まで一般にあまり普及しなかったが、日本で記録され続けていた七曜は西洋で用いられていたものとぴったり一致した。
 ある日付から七曜を導き出すには、ユリウス通日を媒介にすれば簡単に求まる。ユリウス通日を7で割った剰余を曜日に配列すればいいだけである。
long retWeekday( long j )
 public static long retWeekday( long j ) {

    long wd = (j+2)%7;
    if( wd == 0 ) wd = 7;

    return( wd );
    }
入力はユリウス通日、出力は1〜7で、それぞれ日曜ないし土曜に相当する。この変換をわかりやすくするためのメソッドも用意した。内部で部品として retWeekday() メソッドを使用している。
String printWeekday( long j )
 public static String printWeekday( long j ) {

    long wd = retWeekday( j );

    StringBuffer sb = new StringBuffer();

    if ( wd == 1)    sb.append("Weekday:   Sun");
    if ( wd == 2)    sb.append("Weekday:   Mon");
    if ( wd == 3)    sb.append("Weekday:   Tue");
    if ( wd == 4)    sb.append("Weekday:   Wed");
    if ( wd == 5)    sb.append("Weekday:   Thu");
    if ( wd == 6)    sb.append("Weekday:   Fri");
    if ( wd == 7)    sb.append("Weekday:   Sat");

    String s = sb.toString();
    return(s);
    }

干支

 干支(えと)というと年に関係するものと思われる方が多いだろうが、もともとは日にも配したものである。干は幹、支は枝を意味するが十干十二支という言葉の通り干が10種類(甲乙丙丁戊己庚辛壬癸)、支が12種類(子丑寅卯辰巳午未申酉戌亥)でその最小公倍数である60通りで一巡する。甲丑とか乙子といった組み合わせはあり得ない。
 干支も、七曜と同様にユリウス通日を媒介にして求めることができる。干、支を別々に求めて組み合わせればよい。
long retKanshiK( long j )
 public static long retKanshiK( long j ) {

    long kan = j%10;
    if (kan == 0) kan = 10;

    return( kan );
    }
long retKanshiS( long j )
 public static long retKanshiS( long j ) {

    long shi = (j+2)%12;
    if (shi == 0) shi = 12;

    return( shi );
    }
いずれも七曜と同じく入力はユリウス通日、出力は retKanshiK() は干の1〜10で「甲」から順番に配列する。retKanshiS() は支の1〜12で「子」から配列する。応用として、この干支の数値を入力として文字列として干支を出力するものが次のメソッドになる。
String retJKanshi( long k, long s )
 public static String retJKanshi( long k, long s ) {

    StringBuffer sb = new StringBuffer(3);

    if( k == 1 )   sb.append("甲");
    if( k == 2 )   sb.append("乙");
    if( k == 3 )   sb.append("丙");
    if( k == 4 )   sb.append("丁");
    if( k == 5 )   sb.append("戊");
    if( k == 6 )   sb.append("己");
    if( k == 7 )   sb.append("庚");
    if( k == 8 )   sb.append("辛");
    if( k == 9 )   sb.append("壬");
    if( k == 10)   sb.append("癸");

    if( s == 1 )   sb.append("子");
    if( s == 2 )   sb.append("丑");
    if( s == 3 )   sb.append("寅");
    if( s == 4 )   sb.append("卯");
    if( s == 5 )   sb.append("辰");
    if( s == 6 )   sb.append("巳");
    if( s == 7 )   sb.append("午");
    if( s == 8 )   sb.append("未");
    if( s == 9 )   sb.append("申");
    if( s == 10)   sb.append("酉");
    if( s == 11)   sb.append("戌");
    if( s == 12)   sb.append("亥");

    String st = sb.toString();

    return( st );
    }
さらに応用して、旧暦日付と干支から新暦日付に変換するプログラムが次に掲げる eto2d.java である。
eto2d.java
class eto2d {
 public static void main(String args[]) {
    long year = Integer.parseInt(args[0]);
    long month = Integer.parseInt(args[1]);
    long day = Integer.parseInt(args[2]);
    long kan = Integer.parseInt(args[3]);
    long shi = Integer.parseInt(args[4]);

    if( year < 0 )  year++;

    long ijd = fromDate2JD( year, month, day );

    long ojd = nextKanshi( ijd, kan, shi );

    System.out.println("");
    System.out.println("Julian date is:               " + ojd);
    System.out.println("");

    System.out.println(printJuliCalendar( ojd ));
    System.out.println(printGregCalendar( ojd ));
    System.out.println(printWeekday( ojd ));
    System.out.println(printJKanshi( ojd ));
    }

 public static long nextKanshi( long j, long k, long s ) {

    long wk, ws;
    if( k == 10 )  wk = 0;
    else           wk = k;
    if( s == 12 )  ws = 0;
    else           ws = s;

    long p = 0, q;
    for( q = 1 ; q <= 60 ; q++)
        if( (q%10 == wk) && (q%12 == ws) )
            p = q;

    long w = ((j-10)/60) * 60 + p + 10;

    if( w < j )   w += 60;

    return( w );
    }

 public static String printJKanshi( long j ) {

    long kan = retKanshiK( j );
    long shi = retKanshiS( j );

    String kanshi = retJKanshi( kan, shi );

    StringBuffer sb = new StringBuffer();
    sb.append("Kan-shi:   ");
    sb.append(kanshi);

    String s = sb.toString();
    return(s);
    }
}
例えば続日本後紀などに「承和九年秋七月癸巳朔乙卯」とあった場合、承和九年は西暦842年にあたり、癸巳が七月一日であるから乙卯は七月二十三日になる。これをユリウス暦(およびグレゴリオ暦)に換算する。グレゴリオ暦の7月23日に対し、旧暦の七月二十三日はこれより必ず遅れ、その日数はだいたい20日から50日の範囲になる(詳しくは「下」に譲る)。干支は60日周期で一巡するから、この範囲に含まれる該当する干支は一つだけに決まり、かつ最初に合致する日がその日であるはずだ。つまり、西暦842年7月23日(この日の干支は辛未)の次に乙卯になる日が「承和九年秋七月癸巳朔乙卯」となる。入力パラメーターは順に(西暦)年、月、日、干(1〜10)、支(1〜12)である。この例では次のような入力となる。
>java eto2d 842 7 23 2 4
結果は次の通りで、ユリウス暦で842年9月1日に該当することがわかる。
>java eto2d 842 7 23 2 4

Julian date is:               2028842

Calendar date (Julian) is:     842. 9. 1
Calendar date (Gregorian) is:  842. 9. 5
Weekday:   Fri
Kan-shi:   乙卯

 なお、現在むしろ一般的なほうの年に配する干支のために次のメソッドを用意した。
long retYKanshiK( long y )
 public static long retYKanshiK( long y ) {

    long kan = (y + 4197) % 10;
    if (kan == 0) kan = 10;

    return( kan );
    }
long retYKanshiS( long y )
 public static long retYKanshiS( long y ) {

    long shi = (y + 4197) % 12;
    if (shi == 0) shi = 12;

    return( shi );
    }
こちらの入力は西暦年(紀元前に関する注意は同前)で、出力は日に対するものと同様であるから、同じく retJKanshi() を用いて文字列を得ることができる。

(上)の終わりに

 今回、グレゴリオ暦とユリウス暦、ユリウス通日と七曜、干支にしか触れなかったのは、純粋に Java だけで実装できる範囲にとどめたからである。これからさらに旧暦に拡張するためには Java に加えて追加の機能が必要になる。これについては改めて「下」として述べることにしたい。



TOPページへ

Copyright © Hiroshi Nishida (Misohito), 2005.