packages feed

time-1.16.0.1: test/main/Test/Calendar/Easter.hs

module Test.Calendar.Easter (
    testEaster,
) where

import Data.Foldable
import Data.Time.Calendar
import Data.Time.Calendar.Easter
import Test.Tasty
import Test.Tasty.HUnit
import Text.Read

easterDatesFile :: FilePath
easterDatesFile = "test/main/Test/Calendar/EasterData/easter500.txt"

allEasterDatesFile :: FilePath
allEasterDatesFile = "test/main/Test/Calendar/EasterData/easter-reference-1900-2099.csv"

parseField :: Read a => String -> IO a
parseField field =
    case readMaybe field of
        Just value -> return value
        Nothing -> fail $ "invalid integer field: " ++ show field

parseEasterDates :: [String] -> IO [Day]
parseEasterDates [] = return []
parseEasterDates (monthText : dayText : yearText : rest) = do
    month <- parseField monthText
    day <- parseField dayText
    year <- parseField yearText
    easterDay <-
        maybe (fail $ "invalid date: " ++ unwords [yearText, monthText, dayText]) return $
            fromGregorianValid year month day
    (easterDay :) <$> parseEasterDates rest
parseEasterDates fields = fail $ "easter500.txt: expected month/day/year triples, leftover fields: " ++ show fields

getEasterDates :: IO [Day]
getEasterDates = do
    contents <- readFile easterDatesFile
    parseEasterDates $ words contents

splitCommas :: String -> [String]
splitCommas [] = [""]
splitCommas (',' : rest) = "" : splitCommas rest
splitCommas (char : rest) =
    case splitCommas rest of
        [] -> [[char]]
        field : fields -> (char : field) : fields

testGregorianEaster :: TestTree
testGregorianEaster =
    testCase "Gregorian Easter" $ do
        easterDates <- getEasterDates
        for_ easterDates $ \expectedDate@(YearMonthDay y _ _) -> do
            let
                foundDate = gregorianEaster y
            assertEqual "Gregorian Easter" expectedDate foundDate

getAllEasterDates :: IO [(Day, Day, Day, Day)]
getAllEasterDates = do
    contents <- readFile allEasterDatesFile
    parseAllEasterDates $ lines contents

parseAllEasterDates :: [String] -> IO [(Day, Day, Day, Day)]
parseAllEasterDates [] = fail $ allEasterDatesFile ++ ": empty file"
parseAllEasterDates (header : rows) = do
    let
        expectedHeader =
            [ "year"
            , "golden_number"
            , "gregorian_pfm"
            , "gregorian_easter"
            , "orthodox_pfm_gregorian"
            , "orthodox_easter_gregorian"
            ]
    if splitCommas header == expectedHeader
        then traverse parseAllEasterDateLine $ zip [2 :: Int ..] rows
        else fail $ allEasterDatesFile ++ ": unexpected header: " ++ show header

parseAllEasterDateLine :: (Int, String) -> IO (Day, Day, Day, Day)
parseAllEasterDateLine (lineNumber, line) =
    case splitCommas line of
        [yearText, _, gregorianMoonText, gregorianEasterText, orthodoxMoonText, orthodoxEasterText] -> do
            year <- parseField yearText
            gregorianMoon <- parseDayField lineNumber "gregorian_pfm" gregorianMoonText
            gregorianEasterDay <- parseDayField lineNumber "gregorian_easter" gregorianEasterText
            orthodoxMoon <- parseDayField lineNumber "orthodox_pfm_gregorian" orthodoxMoonText
            orthodoxEasterDay <- parseDayField lineNumber "orthodox_easter_gregorian" orthodoxEasterText
            let
                YearMonthDay gregorianMoonYear _ _ = gregorianMoon
            if gregorianMoonYear == year
                then return (gregorianMoon, gregorianEasterDay, orthodoxMoon, orthodoxEasterDay)
                else fail $ allEasterDatesFile ++ ":" ++ show lineNumber ++ ": row year does not match Gregorian Paschal moon"
        fields -> fail $ allEasterDatesFile ++ ":" ++ show lineNumber ++ ": expected 6 CSV fields, found " ++ show (length fields)

parseDayField :: Int -> String -> String -> IO Day
parseDayField lineNumber fieldName field =
    case splitDate field of
        [yearText, monthText, dayText] -> do
            year <- parseField yearText
            month <- parseField monthText
            day <- parseField dayText
            maybe
                (fail $ allEasterDatesFile ++ ":" ++ show lineNumber ++ ": invalid " ++ fieldName ++ ": " ++ show field)
                return
                $ fromGregorianValid year month day
        _ -> fail $ allEasterDatesFile ++ ":" ++ show lineNumber ++ ": invalid " ++ fieldName ++ ": " ++ show field

splitDate :: String -> [String]
splitDate [] = [""]
splitDate ('-' : rest) = "" : splitDate rest
splitDate (char : rest) =
    case splitDate rest of
        [] -> [[char]]
        field : fields -> (char : field) : fields

testAllEasters :: TestTree
testAllEasters =
    testCase "all Easters" $ do
        allEasterDates <- getAllEasterDates
        for_ allEasterDates $ \(expectedGM@(YearMonthDay y _ _), expectedGE, expectedOM, expectedOE) -> do
            assertEqual "Gregorian Paschal moon" expectedGM $ gregorianPaschalMoon y
            assertEqual "Gregorian Easter" expectedGE $ gregorianEaster y
            assertEqual "Orthodox Paschal moon" expectedOM $ orthodoxPaschalMoon y
            assertEqual "Orthodox Easter" expectedOE $ orthodoxEaster y

testEaster :: TestTree
testEaster = testGroup "testEaster" [testGregorianEaster, testAllEasters]