hodatime-1.1.0.0: tests/HodaTime/Calendar/CopticTest.hs
module HodaTime.Calendar.CopticTest
(
copticTests
)
where
import Test.Tasty
import Test.Tasty.QuickCheck as QC
import Test.Tasty.HUnit
import Data.Maybe (fromJust)
import Data.Time.Calendar (Day, fromGregorian)
import qualified Data.Time.Calendar.Julian as J
import Data.Time.Clock (UTCTime(..))
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import HodaTime.Util
import Data.HodaTime.CalendarDate (day, month, year, next, previous, dayOfWeek, DayNth(..), CalendarDate)
import Data.HodaTime.Calendar.Coptic (calendarDate, fromNthDay, fromWeekDate, Coptic, Month(..), DayOfWeek(..))
import Data.HodaTime.Period (applyPeriod, days, months)
import Data.HodaTime.Instant (fromSecondsSinceUnixEpoch)
import Data.HodaTime.TimeZone (utc)
import Data.HodaTime.ZonedDateTime (fromInstant, ZonedDateTime)
import qualified Data.HodaTime.ZonedDateTime as Z
copticTests :: TestTree
copticTests = testGroup "Coptic Tests" [qcProps, unitTests]
qcProps :: TestTree
qcProps = testGroup "(checked by QuickCheck)" [roundTripProps, periodProps, nthDayProps]
unitTests :: TestTree
unitTests = testGroup "Unit tests" [structureUnits, crossCalendarUnits]
-- | Decode a Coptic date to (day, 1-based month, year) for explicit expected-value assertions.
ymd :: CalendarDate Coptic -> (Int, Int, Int)
ymd x = (day x, succ . fromEnum $ month x, year x)
-- | Data.Time has no Coptic calendar, so we verify the construct -> decode bijection directly.
roundTripProps :: TestTree
roundTripProps = testGroup "Constructor"
[
QC.testProperty "construct -> decode round-trips" testRoundTrip
]
where
testRoundTrip (RandomCopticDate y m d) = (ymd <$> calendarDate d m y) == Just (d, succ (fromEnum m), y)
periodProps :: TestTree
periodProps = testGroup "Period"
[
QC.testProperty "dayOfWeek . next n dow $ date == dow" testNextDoW
,QC.testProperty "next n (dayOfWeek date) date == a positive day period" $ testDirection next id
,QC.testProperty "previous n (dayOfWeek date) date == a negative day period" $ testDirection previous negate
]
where
epochDay = fromJust $ calendarDate 1 Thout 1716
testNextDoW dow (Positive n) = (dayOfWeek . next n dow $ epochDay) == dow
testDirection dir adjust (Positive n) = dir n (dayOfWeek epochDay) epochDay == applyPeriod (days (adjust (n * 7))) epochDay
-- | 'fromNthDay' and 'fromWeekDate' are the generic constructors instantiated for Coptic. We skip the short thirteenth
-- month (which has fewer than 7 days, so a given weekday may not occur) to keep the properties total.
nthDayProps :: TestTree
nthDayProps = testGroup "fromNthDay / fromWeekDate"
[
QC.testProperty "fromNthDay First dow is the first such weekday (day 1..7)" testFirst
,QC.testProperty "fromNthDay Last dow is the last such weekday (final week)" testLast
,QC.testProperty "fromWeekDate lands on the requested day-of-week" testWeekDoW
]
where
testFirst dow (RandomCopticDate y m _)
| m == PiKogiEnavot = True
| otherwise = let r = fromNthDay First dow m y in (dayOfWeek <$> r) == Just dow && maybe False (\d -> day d >= 1 && day d <= 7) r
testLast dow (RandomCopticDate y m _)
| m == PiKogiEnavot = True
| otherwise = let r = fromNthDay Last dow m y in (dayOfWeek <$> r) == Just dow && maybe False (\d -> day d >= 24) r
testWeekDoW dow (RandomCopticDate y _ _) = maybe True ((== dow) . dayOfWeek) (fromWeekDate 1 dow y)
structureUnits :: TestTree
structureUnits = testGroup "Structure"
[
testCase "30 Thout is valid" $ (ymd <$> calendarDate 30 Thout 1716) @?= Just (30, 1, 1716)
,testCase "31 Thout is invalid (months are 30 days)" $ calendarDate 31 Thout 1716 @?= Nothing
,testCase "5 PiKogiEnavot is valid in a non-leap year (1732)" $ (ymd <$> calendarDate 5 PiKogiEnavot 1732) @?= Just (5, 13, 1732)
,testCase "6 PiKogiEnavot is valid in a leap year (1731, 1731 mod 4 == 3)" $ (ymd <$> calendarDate 6 PiKogiEnavot 1731) @?= Just (6, 13, 1731)
,testCase "6 PiKogiEnavot is invalid in a non-leap year (1732)" $ calendarDate 6 PiKogiEnavot 1732 @?= Nothing
,testCase "1 Thout + 1 month == 1 Paopi" $ (ymd . applyPeriod (months 1) <$> calendarDate 1 Thout 1716) @?= Just (1, 2, 1716)
,testCase "1 Mesori + 1 month == 1 PiKogiEnavot (12th -> 13th month)" $ (ymd . applyPeriod (months 1) <$> calendarDate 1 Mesori 1716) @?= Just (1, 13, 1716)
]
-- | The strongest checks: the same absolute day, anchored via Data.Time, must decode to the expected Coptic date.
-- The epoch (Coptic 1.Thout.1 = Julian 29.Aug.284) is pinned via 'Data.Time.Calendar.Julian', and two well-known
-- Nayrouz (Coptic new year) dates are pinned via the Gregorian calendar.
crossCalendarUnits :: TestTree
crossCalendarUnits = testGroup "Cross-calendar (Instant)"
[
testCase "epoch: Julian 29.Aug.284 is Coptic 1 Thout 1" $ copticOfDay (J.fromJulian 284 8 29) >>= (@?= (1, 1, 1))
,testCase "Nayrouz 1738 = 11.Sep.2021 (Gregorian)" $ copticOfDay (fromGregorian 2021 9 11) >>= (@?= (1738, 1, 1))
,testCase "Nayrouz 1716 = 12.Sep.1999 (Gregorian, pre-leap)" $ copticOfDay (fromGregorian 1999 9 12) >>= (@?= (1716, 1, 1))
]
-- | View the UTC midnight of a Data.Time 'Day' as a Coptic date, returning (year, 1-based month, day).
copticOfDay :: Day -> IO (Int, Int, Int)
copticOfDay dt = do
tz <- utc
let secs = round (utcTimeToPOSIXSeconds (UTCTime dt 0))
zdt = fromInstant (fromSecondsSinceUnixEpoch secs) tz :: ZonedDateTime Coptic
return (Z.year zdt, succ (fromEnum (Z.month zdt)), Z.day zdt)