カレンダーを表示するプログラムを作成してテストする

Luaでカレンダーを表示するプログラムを作成してみます。その後で作成したプログラムのテストについて考えます。まずは、calコマンドでカレンダー表示の確認。

$ cal 3 2012
      3月 2012
日 月 火 水 木 金 土
             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 31

カレンダー表示で解決すべき問題は、閏年と曜日の計算だ。閏年かどうかの判定は、簡単な判定式がある。曜日はどうしたら求まるだろう。調べると、ツェラーの公式というのがあるようだ。

ツェラーの公式を使えば、その日が何曜日なのか求めることが出来る。計算自体も単純なので素直にプログラムを書いてみた。

-- ツェラーの公式により曜日を計算する
-- http://ja.wikipedia.org/wiki/%E3%83%84%E3%82%A7%E3%83%A9%E3%83%BC%E3%81%AE%E5%85%AC%E5%BC%8F
function zeller(y, m, q)
   local j = math.floor(y / 100)
   local k = y % 100
   
   -- j: 年の千の位と百の位(たとえば2310年ならば23)
   -- k: 年の下2桁(たとえば2310年ならば10)
   -- m: 月
   -- q: 日

   -- 月が1月, 2月の場合はそれぞれ前年の13月, 14月とする
   -- (たとえば、2007年1月1日なら2006年13月1日とする)
   if m == 1 or m == 2 then
      m = 12 + m
      k = k - 1
      if k < 0 then
         j = j - 1
         k = 99
      end
   end

   -- hが0なら土曜日、1なら日曜、2なら月曜、3なら火曜、4なら水曜、5なら木曜、6なら金曜である。
   local h = q +
             math.floor((m+1)*26/10) +
             k +
             math.floor(k/4) +
             math.floor(j/4) +
             5*j
   return ({ Sa, Su, Mo, Tu, We, Th, Fr })[(h % 7) + 1]
end

いくつかテストしてみる。

$ lua calendar.lua 3 2012
      3月 2012
日 月 火 水 木 金 土
             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 31

$ lua calendar.lua 2 2012
      2月 2012
日 月 火 水 木 金 土
          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

問題なく動作している(っぽい)。

上のプログラムのテストコードを書きたい。だけど、どうやってテストしたら良いだろうか。プログラムをLuaからPythonに書き直してテストする方法が思い浮かんだ。Pythonにはdatetimeモジュールがあり、そのモジュールを使えば曜日を求めることが出来るから、自分のコードと値を比較して正しいかチェックできる。

Pythonなどの(曜日計算が可能なライブラリがある)言語を使う以外には、どのようなテスト方法が考えられるだろうか。冒頭でcalコマンドによる出力結果を見たけど、calコマンドの出力結果と比較することでもテストは可能じゃないかな。今回はそれでテストしてみよう。

テストを実行するまでの流れ。

  • calコマンドでテストデータを生成。テストデータは1980年から2012年までとする(範囲は適当)。
  • 自作プログラムも同様に1980年から2012年までのカレンダーを出力する
  • 最後に二つの出力結果をdiffで比較する。

テストデータの生成と比較は以下のような感じで行った。月と年をパラメータ化してスクリプトで自動生成。

$ cal 3 2001 > ./cal/3-2001.txt
$ lua calendar.lua 2 2001 > ./my/3-2001.txt
$ diff ./cal/3-2001.txt ./my/3-2001.txt

diffによる比較を行い、テストをパスしたのを確認した。……けど、これで安心していいのだろうか?より良いテスト方法や他にテストすべき項目はあるだろうか。

いくつか気になったところ:

  • diffでの比較のために、calの出力に合わせる作業が面倒だった。実際に改行が足りない箇所があってスクリプトを修正したりした。ツェラーの公式のテストとは何の関係のない部分。
  • 上のスクリプト(calendar.lua)では、曜日計算はその月の1日しか行っていない。他の日付でもテストすべき。

ソースコード:

-- calendar.lua
-- カレンダーを出力

Su = "Sunday"
Mo = "Monday"
Tu = "Tuesday"
We = "Wednesday"
Th = "Thursday"
Fr = "Friday"
Sa = "Saturday"

-- ツェラーの公式により曜日を計算する
-- http://ja.wikipedia.org/wiki/%E3%83%84%E3%82%A7%E3%83%A9%E3%83%BC%E3%81%AE%E5%85%AC%E5%BC%8F
function zeller(y, m, q)
   local j = math.floor(y / 100)
   local k = y % 100
   
   -- j: 年の千の位と百の位(たとえば2310年ならば23)
   -- k: 年の下2桁(たとえば2310年ならば10)
   -- m: 月
   -- q: 日

   -- 月が1月, 2月の場合はそれぞれ前年の13月, 14月とする
   -- (たとえば、2007年1月1日なら2006年13月1日とする)
   if m == 1 or m == 2 then
      m = 12 + m
      k = k - 1
      if k < 0 then
         j = j - 1
         k = 99
      end
   end

   -- hが0なら土曜日、1なら日曜、2なら月曜、3なら火曜、4なら水曜、5なら木曜、6なら金曜である。
   local h = q +
             math.floor((m+1)*26/10) +
             k +
             math.floor(k/4) +
             math.floor(j/4) +
             5*j
   return ({ Sa, Su, Mo, Tu, We, Th, Fr })[(h % 7) + 1]
end

function toJa(weekday)
   return ({ [Su]="日", [Mo]="月", [Tu]="火", [We]="水", [Th]="木", [Fr]="金", [Sa]="土" })[weekday]
end

function isLeap(y)
   return y % 4 == 0 and (y % 400 == 0 or y % 100 ~= 0)
end

function days(y, m)
   local ds = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
   if m == 2 and isLeap(y) then
      return 29
   end
   return ds[m]
end

function calendar(y, m)
   local weekday = zeller(y, m, 1)
   local space = ({ [Su]=0, [Mo]=1, [Tu]=2, [We]=3, [Th]=4, [Fr]=5, [Sa]=6 })[weekday]

   local tab = {}
   for i = 1, space do tab[#tab+1] = "  " end
   for i = 1, days(y, m) do tab[#tab+1] = string.format("%2d", i) end

   io.write(string.format("     %2d月 %d\n", m, y))
   io.write("日 月 火 水 木 金 土\n")
   local n = 0
   local ln = 0
   for i = 1, #tab do
      if i > 1 then
         if n % 7 == 0 then
            io.write("\n")
            ln = ln + 1
         else
            io.write(" ")
         end
      end
        
      io.write(tab[i])
      n = n + 1
   end
   if 6-ln > 0 then
      for i = 1, 6-ln do io.write("\n") end
   end
end

function main()
   local date = os.date("*t")
   local m = tonumber(arg[1] or date.month)
   local y = tonumber(arg[2] or date.year)
   calendar(y, m)
end

main()