Haskellによる西暦(Day)⇔和暦変換、和暦の比較(定義ファイル読み込みバージョン)

定義ファイルで元号名、元号が使用開始された西暦年月日、最終西暦年月日を定義ファイルから読み込みます。
代数的データ型のアクセサを使うと可読性が悪くなるので、パターンマッチにより取り出す方法を使うようになりました。

module Main where

import Data.List
import Data.Time.Calendar
import System.IO.Unsafe
import qualified System.IO.UTF8 as U

import Test.HUnit


newtype GName   = GName  String deriving (Eq,Read,Show)
newtype Gfirst  = Gfirst String deriving (Eq,Read,Show)
newtype GfstDay = GfstDay  Day  deriving (Eq,Show)
newtype Glast   = Glast  String deriving (Eq,Read,Show)
newtype GlstDay = GlstDay  Day  deriving (Eq,Show)

-- Day型はreadで読み込めないのでString型で読み込み、readDayで変換しています。
readDay :: String -> Day
readDay [y1,y2,y3,y4,_,m1,m2,_,d1,d2] = fromGregorian (read [y1,y2,y3,y4]) (read [m1,m2]) (read [d1,d2])
readDay _                             = error "Wareki readDay: "

data GengoS = GengoS GName Gfirst  Glast   deriving (Eq,Read,Show)
data Gengo  = Gengo  GName GfstDay GlstDay deriving (Eq,Show)

gengoSToGengo :: GengoS -> Gengo
gengoSToGengo (GengoS gName (Gfirst firstSTR) (Glast lastSTR))= 
  Gengo gName (GfstDay $ readDay firstSTR) (GlstDay $ readDay lastSTR)

data Wareki = Wareki GName Integer Int Int deriving (Eq,Show)

instance Ord Wareki where
  compare w1 w2 = compare (fromWarekiToDay w1) (fromWarekiToDay w2)

-- (read :: String -> [GengoS])はread関数の型を限定。文字列を引数とし、
-- [GengoS]型を返す。
warekiDefRead :: IO [GengoS]
warekiDefRead = return.(read :: String -> [GengoS]) =<< U.readFile "wareki.ini"

gengoList :: [Gengo]
gengoList = map gengoSToGengo $ unsafePerformIO warekiDefRead

-- =============================================================================
-- 西暦から和暦への変換
-- ============================================================================
fromDayToWareki :: Day -> Maybe Wareki
fromDayToWareki day =
  case find betweenFirstAndLast gengoList of
    Just (Gengo gName (GfstDay firstDay) _) -> 
       let (gfirstYear,_,_) = toGregorian firstDay
       in  Just (Wareki gName (adYear - gfirstYear + 1) adMonth adDay)
    Nothing -> Nothing
  where
    betweenFirstAndLast (Gengo _ (GfstDay first) (GlstDay lst)) = (first <= day) && (day <= lst)
    (adYear,adMonth,adDay) = toGregorian day

-- ===========================================================================
-- 和暦から西暦への変換
-- =============================================================================
fromWarekiToDay :: Wareki -> Maybe Day
fromWarekiToDay (Wareki _     0     _      _   ) = Nothing
fromWarekiToDay (Wareki wName wYear wMonth wDay) =
    case find eqGengo gengoList of
      Just (Gengo _ (GfstDay firstDay) (GlstDay lastDy)) ->
         let (gfirstYear,_,_) = toGregorian firstDay
         in  case fromGregorianValid (gfirstYear + wYear - 1) wMonth wDay of
              Just newDay -> if newDay <= lastDy then (Just newDay) else Nothing
              Nothing     -> Nothing
      Nothing     -> Nothing
    where
        eqGengo (Gengo gName _ _) = wName == gName

warekiTest :: Test
warekiTest = "Wareki" ~:
  test [ fromDayToWareki (fromGregorian 1912  7 29) ~?= Just (Wareki (GName "明治") 45  7 29),
         fromDayToWareki (fromGregorian 1912  7 30) ~?= Just (Wareki (GName "大正")  1  7 30),
         fromDayToWareki (fromGregorian 1926 12 24) ~?= Just (Wareki (GName "大正") 15 12 24),
         fromDayToWareki (fromGregorian 1926 12 25) ~?= Just (Wareki (GName "昭和")  1 12 25),
         fromDayToWareki (fromGregorian 1989  1  7) ~?= Just (Wareki (GName "昭和") 64  1 7),
         fromDayToWareki (fromGregorian 1989  1  8) ~?= Just (Wareki (GName "平成")  1  1 8),
         (fromDayToWareki (fromGregorian 1926 12 24) >>= fromWarekiToDay) ~?= Just (fromGregorian 1926 12 24),
         ((Wareki (GName "昭和")  1 12 25) == (Wareki (GName "昭和")  1 12 25)) ~?= True,
         ((Wareki (GName "平成")  1 1  10)  > (Wareki (GName "昭和") 56 12 25)) ~?= True
        ]


toDayTest :: Test
toDayTest = "toDay" ~:
    test [ fromWarekiToDay (Wareki (GName "明治") 45  7 29) ~?= Just (fromGregorian 1912 7 29),
           fromWarekiToDay (Wareki (GName "大正")  1  7 30) ~?= Just (fromGregorian 1912 7 30),
           fromWarekiToDay (Wareki (GName "大正") 15 12 24) ~?= Just (fromGregorian 1926 12 24),
           fromWarekiToDay (Wareki (GName "昭和")  1 12 25) ~?= Just (fromGregorian 1926 12 25),
           fromWarekiToDay (Wareki (GName "平成")  1  1 8)  ~?= Just (fromGregorian 1989  1  8),
           -- 大正16年は存在しない。
           fromWarekiToDay (Wareki (GName "大正") 16 12 24) ~?= Nothing ,
           -- 大正15年12月25日は昭和なので存在しない。
           fromWarekiToDay (Wareki (GName "大正") 15 12 25) ~?= Nothing ,
           -- 年号の範囲内の異常な値は存在しない。
           fromWarekiToDay (Wareki (GName "大正")  1 13 25) ~?= Nothing,
           fromWarekiToDay (Wareki (GName "大正")  1 13 33) ~?= Nothing,
           (fromWarekiToDay (Wareki (GName "明治") 45 7 29) >>= fromDayToWareki) ~?= Just (Wareki (GName "明治") 45 7 29) ]

doTest :: IO Counts
doTest = do
  runTestTT warekiTest
  runTestTT toDayTest

main :: IO()
main = print =<< doTest

年号定義を追加すれば良い。

[ GengoS (GName "明治") (Gfirst "1868-10-23") (Glast "1912-07-29"),
  GengoS (GName "大正") (Gfirst "1912-07-30") (Glast "1926-12-24"),
  GengoS (GName "昭和") (Gfirst "1926-12-25") (Glast "1989-01-07"),
  GengoS (GName "平成") (Gfirst "1989-01-08") (Glast "2100-12-31") ]