packages feed

hodatime-1.0.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, monthl, month, year, next, previous, dayOfWeek, DayNth(..), CalendarDate)
import Data.HodaTime.Calendar.Coptic (calendarDate, fromNthDay, fromWeekDate, Coptic, Month(..), DayOfWeek(..))
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, lensProps, 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 = (get day x, succ . fromEnum $ month x, get 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)

lensProps :: TestTree
lensProps = testGroup "Lens"
  [
     QC.testProperty "dayOfWeek . next n dow $ date == dow" testNextDoW
    ,QC.testProperty "next n (dayOfWeek date) date == modify (+ n * 7) day date" $ testDirection next (+)
    ,QC.testProperty "previous n (dayOfWeek date) date == modify (- n * 7) day date" $ testDirection previous $ flip (-)
  ]
  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 == modify (adjust $ n * 7) day 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 -> get day d >= 1 && get 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 -> get 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 <$> (modify (+1) monthl <$> calendarDate 1 Thout 1716)) @?= Just (1, 2, 1716)
    ,testCase "1 Mesori + 1 month == 1 PiKogiEnavot (12th -> 13th month)" $ (ymd <$> (modify (+1) monthl <$> 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)