haskellで勉強がてらcalコマンドを書いてみた。

haskellの勉強がてらcalコマンドを書いてみた。
(おそらくHaskellをよく知っている方が見たら、下手の極みだろうけれど。見るに耐えない場合指摘いただけると非常に助かります。)

実行結果

import Data.Char
import System

main = do
  args <- getArgs
  let y = head args
  let m = last args
  let year = atoi y
  let month = atoi m
  putStrLn ("          " ++ y ++ "/" ++ m)
  putStrLn "Sun Mon Tue Wed Thr Fri Sat"
  let cal = transCalStr $ getcal year month
  putStr $ unlines cal

-- 数字 -> 数値への変換を行う
atoi :: String -> Int
atoi s = read s :: Int

-- 日付の数値配列 -> カレンダー文字列への変換を行う
transCalStr :: [Int] -> [String]
transCalStr xs = map unwords $ splitInterval 7 $ map intToCalDigit xs

-- 数値 -> カレンダー形式への変換
-- カレンダー形式とは、3文字分の文字列で数字に空白付与したもの
-- 例:1 -> "  1"   21 -> " 21"  0-> "   " (0は存在しない日付を意味する)
intToCalDigit :: Int -> String
intToCalDigit n
  | n == 0 = replicate 3 ' '
  | 1 <= n && n <= 9 = (replicate 2 ' ') ++ [intToDigit n]
  | otherwise = " " ++ itostr n

-- 数値 -> 数字文字列
itostr :: Int -> String
itostr 0 = ""
itostr n = itostr (div n 10) ++ [intToDigit (mod n 10)]

-- 指定された年月の日付数値配列を取得。
-- 月初めの存在しない日付は0詰めされる。
getcal :: Int -> Int -> [Int]
getcal y m = let
               days = getDayList y m
               start = zeller y m 1
             in
               addprev days start 0

-- 指定された日付の曜日を算出する(ツェラーの公式)
-- ( yy2 + [ yy2 / 4] + [ yy1 / 4 ] + [(13 * mm + 8 ) / 5 ] - ( 2 * yy1 ) + dd ) mod 7 
zeller :: Int -> Int -> Int -> Int
zeller y m d 
  | ((m == 1) || (m == 2)) = _zeller (y - 1) (m + 12) d
  | otherwise = _zeller y m d

_zeller :: Int -> Int -> Int -> Int
_zeller y m d = let
                   uppery = div y 100
                   lowery = mod y 100
                   y1 = lowery 
                   y2 = div lowery 4
                   y3 = div uppery 4
                   y4 = uppery * 2
                   m1 = div (13 * m + 8) 5
                   d1 = d
                 in mod (y1 + y2 + y3 + m1 - y4 + d1) 7

-- 与えられたリストを第一引数で指定した個数分リストに分割してリストのリスト
-- を作成する
splitInterval :: Int -> [a] -> [[a]]
splitInterval invl xs = init $ _splitInterval invl xs

_splitInterval :: Int -> [a] -> [[a]]
_splitInterval invl [] = [[]]
_splitInterval invl xs = let
                           divide = splitAt invl xs
                         in
                           [(fst divide)] ++ (_splitInterval invl (snd divide))

-- 第一引数のリストの先頭に第三引数の値を第二引数個分付与したリストを返す
addprev :: [a] -> Int -> a -> [a]
addprev xs num elem = (replicate num elem) ++ xs

-- 与えられた年月の日付リストを返す
getDayList :: Int -> Int -> [Int]
getDayList y m
  | m == 2 && isLeap y = [1..29]
  | m == 2 && (isLeap y == False) = [1..28]
  | m == 4 = [1..30]
  | m == 6 = [1..30]
  | m == 9 = [1..30]
  | m == 11 = [1..30]
  | otherwise = [1..31]

-- 指定した年が閏年か調べる
isLeap :: Int -> Bool
isLeap y 
  | mod y 400 == 0 = True
  | mod y 100 == 0 = False
  | mod y 4 == 0 = True
  | otherwise = False

do式の中のletではin記述してはいけないと初めて知った。
少し苦労したが、色々書いてみて覚えていこう。

次はネットワークにアクセスしてTwitterとかはてなブックマークへのアクセスを試みてみたい。