CodeIQ:遠い昔、はるか彼方の銀河系の カレンダー(ややリアル編)

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「遠い昔、はるか彼方の銀河系の カレンダー(ややリアル編)」(CodeIQ)を参照してください。

問題の概要

次のようなカレンダーがあります。

曜日 t, u, v, w, x, y, z
1年の日数 345日または344日
1年の月数 11か月(A〜K月)
1か月の日数 ■平年の場合
A, B, D, F, H, I, K月は31日
C, E, G, J月は32日

■うるう年の場合
A, B, D, E, F, H, I, K月は31日
C, G, J月は32日

※ グレゴリオ暦と異なり、うるう年の方が一日短い
閏年 20で割り切れる年はうるう年
ラゲ暦年が80で割り切れる年は平年
ラゲ暦年が4000で割り切れる年はうるう年

紀元は1600年A月1日で曜日はt曜日になります。

【入力と出力】
「1600.A.1」のような形式で年月日が与えられます。
曜日を出力してください。

私のプログラム

C++で解答しています。

#include <stdio.h>
#include <string>
#include <map>

using namespace std;

class RageCal {
private:
	const static char Week[7];
	const static char Month[11];
	const static int LD;
	const static int SD;
	const static int Days[11];
	const static int TotalDaysOfYear;
	const static int Epoc;

	map< char, int > RevMonth;
	int AddedDays[11];

	int Year;	// 年(紀元年を引いた値)
	int Mon;	// 月(0始まり)
	int Day;	// 日(1始まり)
	int Date;	// 曜日(0始まり)
public:
	RageCal();
	~RageCal();
	void setString(char* str);
	int calcDate(int y, int m, int d);
	char dateChar();
	int addLeepYear(int y, int m);
};

const char RageCal::Week[] = {'z', 't', 'u', 'v', 'w', 'x', 'y'};
const char RageCal::Month[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'};
const int RageCal::LD = 32;
const int RageCal::SD = 31;
const int RageCal::Days[] = {SD,SD,LD,SD,LD,SD,LD,SD,SD,LD,SD};
const int RageCal::TotalDaysOfYear = 345;
const int RageCal::Epoc = 1600;

RageCal::RageCal(){
	Year = 0;
	Mon = 0;
	Day = 1;
	Date = 0;

	for(int i=0; i<sizeof(Month); i++){
		RevMonth.insert(map< char, int >::value_type(Month[i], i));
	}

	for(int i=0; i<11; i++){
		if(i== 0){
			AddedDays[i] = 0;
		}
		else{
			AddedDays[i] = AddedDays[i-1] + Days[i-1];
		}
	}
}

RageCal::~RageCal(){
}

void RageCal::setString(char* str){
	char m;
	sscanf(str, "%d.%c.%d", &Year, &m, &Day);

	Year -= Epoc;
	Mon = RevMonth[m];

	Date = calcDate(Year, Mon, Day);
}

int RageCal::calcDate(int y, int m, int d){
	unsigned long long days = (unsigned long long)TotalDaysOfYear * y + AddedDays[m] + d + addLeepYear(y, m);
	return days % 7;
}

char RageCal::dateChar(){
	return Week[Date];
}

int RageCal::addLeepYear(int y, int m){
	int ry = RageCal::Epoc + y -1;	// A月はまだ閏月を迎えていない
	int ret = (ry/20 - ry/80 + ry/4000) - (1599/20 - 1599/80);

	if(m>4){
		ry += 1;	// F月以降は閏月を含む
		if(ry%4000 == 0){ ret += 1; }
		else if(ry%80 == 0){ret += 0; }
		else if(ry%20 == 0){ret += 1; }
		else{ ret += 0; }
	}

	return -1 * ret;
}

int main(int argc, char* argv[]){
	char str[128];
	while( fgets(str, sizeof(str), stdin) != NULL ){
		RageCal rc;
		rc.setString(str);
		printf("%c\n", rc.dateChar());
	}
	return 0;
}

解説

基本的には初級編と同じなので細かい説明は省略します。
初級編はJavaだったのに上級編はC++ですが基本的には同じです。コンストラクタ、setString()、dateStr()は同じことをしています。

addLeepYear()

入力値までに何回閏年による日数の減少があるかを計算する関数で、この問題のキモです。
基本的には仕様に示された年ごとに閏年が来るのでその分を計算します。ただし、F月以降にならないと1日増えないのでそれを考慮し、閏年の計算の基準になるのは年ですが年初は閏年による日数の変動を受けていないのでそれを考慮して1少なく計算します(84行目)。 85行目の計算が年初時点で何回の閏年を経過したかの計算になります。
(87〜93行目)でF月以降だったらさらに当年の分を考慮します。

calcDate()

年月日を元に入力値が紀元から何日目かを計算し、それを7(曜日数)で割ったあまりを求めます。あまりの値が曜日の要素番号に一致します。
この時、addLeepYear()の結果を加えて日数が正しくなるようにします。

雑感

初級編からやったので難しくありませんでした。
当然Javaでもできましたし、性能が問題になるようなプログラムでもありませんが、こっちをC++でやったのは(多分)C++上級のスキル認定が欲しかったからだと思います。
C++でクラスを作ったのは久しぶりで、メンバの初期化を宣言時にはできないことを忘れいていて(よく忘れますが)、「あれ、なんでコンパイラに怒られるんだ?」となりました。