CodeIQ:遠い昔、はるか彼方の銀河系の カレンダー(初級編)

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

問題の概要

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

曜日 t, u, v, w, x, y, z
1年の日数 345日
1年の月数 11か月(A〜K月)
1か月の日数 A, B, D, F, H, I, K月は31日
C, E, G, J月は32日
閏年 なし

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

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

私のプログラム

Javaで解答しています。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;

/**
 *
 * @author kamio
 */
public class Main {

    public class Cal{
        // 定数
        final String[] Week = {"z", "t", "u", "v", "w", "x", "y"};
        final String[] Month = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"};
        final HashMap<String, Integer> RevMonth;
        final int LD=32;    // 長い月の日数
        final int SD=31;    // 短い月の日数
        //                   A, B, C, D, E, F, G, H, I, J, K
        final int[] Days = {SD,SD,LD,SD,LD,SD,LD,SD,SD,LD,SD};
        // 各月の1日が1年の何日目になるかを表す
        final int[] AddedDays;
        final int TotalDaysOfYear = 345;
        final int Epoc = 500;

        // 変数
        String  CalStr = "";    // カレンダーの文字列表現
        int     Year = 0;       // 年(500を引いた値)
        int     Mon = 0;        // 月(0始まり)
        int     Day = 1;        // 日(1始まり)
        int     Date = 0;       // 曜日(0始まり)

        public Cal(){
            RevMonth = new HashMap<>();
            for(int i=0; i<Month.length; i++){
                RevMonth.put(Month[i], i);
            }

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

        public void setString(String str){
            String[] splited = str.split("\\.");

            Year = Integer.parseInt(splited[0]) - Epoc;
            Mon = RevMonth.get(splited[1]);
            Day = Integer.parseInt(splited[2]);

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

        public int calcDate(int y, int m, int d){
            int days = TotalDaysOfYear * y + AddedDays[m] + d;
            return days % 7;
        }

        public String dateStr(){
            return Week[Date];
        }
    }

    public void printDate(String str){
        str = str.trim();

        Cal c = new Cal();
        c.setString(str);

        System.out.println(c.dateStr());
    }

    /**
     * @param args the command line arguments
     * @throws java.io.IOException
     */
    public static void main(String[] args) throws IOException {
        try (BufferedReader stdReader = new BufferedReader(new InputStreamReader(System.in))) {
            String line;
            Main cc = new Main();

            while ((line = stdReader.readLine()) != null) {
                cc.printDate(line);
            }
        }
    }

}

解説

取り立ててアルゴリズム、実装的に難しい部分はありません。
main()は自身のメンバメソッドprintDate()を呼んでいるだけですが、この関数もCalクラスのインスタンスを作成してCal#setString()で入力値を与え、Cal#dateStr()で曜日をもらって出力しているだけです。

Cのtm構造体を真似る

プログラムの本体はCalクラスです。これを作るにあたってはC言語のtm構造体を参考にしています。Year、Mon、Day、Dateの各メンバ変数の扱いはtm構造体と同じです。曜日を求めるために入力された年月日が紀元から何日目なのかを計算するのですが、それが容易になるためです。

コンストラクタ

コンストラクタではRevMonthとAddedDaysを計算しています。
これらは実際のところ定数なのでスタティックイニシャライザで初期化したかったのですが、インナークラスのためコンストラクタで初期化しています。

RevMonthは月を表す文字を数値に変換するためのものです。
AddedDaysはその月が年の何日目に当たるかをあらかじめ計算しておくためのものです。

setString()

入力された文字列をパースして年月日を数値化し、calcDate()で曜日を計算します。年については紀元(Epoc)を引いて紀元から何日目かを計算できるようにします。

calcDate()

年月日を元に入力値が紀元から何日目かを計算し、それを7(曜日数)で割ったあまりを求めます。あまりの値が曜日の要素番号に一致します。

dateStr()

曜日の数値に相当する文字を返します。
曜日の数値は曜日を表す文字の配列の要素番号に一致するのでそれを返すだけです。

雑感

問題を読んだ瞬間に、「あー、(Cのtime.hにある)tmとそれ関連の関数を問題のカレンダーにあわせて実装するだけだ」と思いました。実際その通りに実装しています。tm構造体を知っていたので年月日の値をどう扱えば良いかを考える必要がなく、容易でした。