CodeIQ:私好みのカレンダー

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「私好みのカレンダー」(CodeIQ)を参照してください。

問題の概要

標準入力からYYYYMM形式で年月が与えられます。
それに対してその月の日曜日始まりのカレンダーと月曜日始まりのカレンダーを出力せよ、という問題です。

私のプログラム

Pythonで解答しています。

import sys
import fileinput
import itertools
import time
from datetime import date

DAYS = {
	1:31,
	2:28,
	3:31,
	4:30,
	5:31,
	6:30,
	7:31,
	8:31,
	9:30,
	10:31,
	11:30,
	12:31,
}

# dateオブジェクトから閏年を判断する
# 2月で閏年なら1、それ以外は0を返す
def isLeepYear(d):
	if d.month == 2:
		y = d.year
		return (1 // (y % 4 + 1)) * (1 - 1 // (y % 100 + 1)) + (1 // (y % 400 + 1))
	else:
		return 0

# 文字列のリスト化したカレンダーをフォーマットして表示する
# wd 最初の日の曜日の位置(日曜が最初なら日曜が0、月曜が最初なら月曜が0)
# ed その月の最後の日
def printCalender(wd, ed):
	cal = []

	for i in range(wd+ed):
		if i < wd:
			cal.append("  ")	# 問題のどこにスペース2個と書いてある?
		else:
			cal.append("%02d" % (i-wd+1))

	for i, n in enumerate(cal):
		sys.stdout.write(n)
		if (i != len(cal)-1) and (i%7 != 6):
			sys.stdout.write(",")
		else:
			sys.stdout.write("\n")

# 月曜日始まりのカレンダーを表示する
def printCalbeginMon(d):
	wd = d.weekday()	# 1日の曜日
	ed = DAYS[d.month] + isLeepYear(d)
	printCalender(wd, ed)

# 日曜日始まりのカレンダーを表示する
def printCalBeginSun(d):
	wd = d.isoweekday()
	if wd == 7:
		wd = 0
	ed = DAYS[d.month] + isLeepYear(d)
	printCalender(wd, ed)

#==============================================================================
# main
#------------------------------------------------------------------------------
Year = 0	# 年
Month = 0	# 月

for line in fileinput.input():
	if not line.strip():
		continue

	Year = int(line.strip()[0:4])
	Month = int(line.strip()[4:6])

	begin = date(Year, Month, 1)
	printCalBeginSun(begin)
	printCalbeginMon(begin)

解説

全部自力でやろうとしたらそこそこ大変ですが、大体の言語にはカレンダーを扱う機能があるのでそれを知っていれば簡単です。

基本的な考え方

カレンダーを出力するために必要な要件は次の3つです。
  1. 当該年月の日数を知る
  2. 当該年月の1日の曜日を知る
  3. 閏年で2月なら日数を+1する
    1. 1は表引きにすれば良いので問題はありません。
      3は調べれば計算式がわかるのでそれを実装するだけです。
      2は自力でやろうとすると面倒なので言語のカレンダー機能を使用します。

1日の曜日を決める

printCalbeginMon()ではdatetime#weekday()をprintCalBeginSun()ではdatetime#isoweekday()を使用して求めています(printCalbeginMon()のbeginが小文字なのはtypoです)。
weekday()は月曜日が0、日曜日が6を返しますので月曜日はじまりのカレンダーにはこの値をそのまま使用します(この時Pythonでは月曜日はじまりになっているのに驚きました)。
isoweekday()は月曜日が1で日曜日が7を返すので7を0に変換して使用します。

閏年

isLeepYear()で計算しています。4の倍数の年は閏年、100の倍数なら閏年ではない、400の倍数は閏年の通りに計算しています。引数に年月をもらって、2月以外は常に0を返します。これによってprintCalender()では常にisLeepYear()を呼び出せば良くなるためコードが単純になります。
しかし、関数名がイマイチです。年だけ渡した場合に閏年かどうかを判断するわけではないので関数名と処理が一致していません。

printCalender()

印字する本体です。
引数wdの説明がわかりにくいですが、その月の1日の曜日がカレンダーの最初の曜日から相対的にいくつ離れているかを渡すということです。日曜日始まりで1日が水曜日なら3、月曜日はじまりなら2を渡しなさいということです。
edは最後の日(=何日あるか)です。

最初に印字するカレンダーを文字列で作成しています。1回のループで処理することもできますがif文が多くなってコードが読みにくくなるので2回に分けました。この時、1日の場所まで空欄で埋めてあるので印字時には単純に7回ごとに改行すれば済みます。
印字は7で割った余りが6でなければ「,」、6なら改行を出力します。それから最後の日の後も改行なのでそれをチェックします。

雑感

プログラム的にはどうこうなかったのですが私はこの問題に2回ジェクトされました。
理由は問題を読んだ限り1日までの部分にスペースを2個入れるというのが読み取れなかったため1回目はスペースを入れなかったため、2回目はHTMLの表示上2つの連続したスペースは1つにしかならないためそれに合わせてスペースを1個しか入れなかったためです(2017/3/27現在表示が修正されているようです)。
頭にきたのでコメント(39行目)に文句を書いています。
仕様が不明確な問題が時々ありますが、こういう部分は明確に記述してほしいと思います。