packages feed

tztime-0.1.0.0: test/Test/Data/Time/TZTime.hs

-- SPDX-FileCopyrightText: 2022 Serokell <https://serokell.io/>
--
-- SPDX-License-Identifier: MPL-2.0

{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

module Test.Data.Time.TZTime where

import Control.Arrow ((>>>))
import Data.Function ((&))
import Data.Time (DayOfWeek(..), LocalTime(..), TimeOfDay(..), TimeZone(..))
import Data.Time.Calendar.Compat (pattern YearMonthDay)
import Data.Time.TZInfo as TZI
import Data.Time.TZTime as TZT
import Data.Time.TZTime.Internal (TZTime(UnsafeTZTime))
import Test.Tasty (TestTree)
import Test.Tasty.HUnit
import Test.Utils

unit_atStartOfDay_at_gaps :: Assertion
unit_atStartOfDay_at_gaps = do
  -- On this date in Havana, at 23:59, the clocks are turned forward 1 hour,
  -- which means midnight never happens.
  --
  -- `atStartOfDay` should return the 01:00, the earliest time possible for that day.
  --
  -- See: https://www.timeanddate.com/time/zone/cuba/havana?year=2022
  let tz = TZI.fromLabel TZI.America__Havana
  let tzt = unsafeFromLocalTime tz (LocalTime (YearMonthDay 2022 3 13) (TimeOfDay 10 45 0))
  let result = atStartOfDay tzt

  result @?=
    UnsafeTZTime
      (LocalTime (YearMonthDay 2022 3 13) (TimeOfDay 1 0 0))
      tz
      (TimeZone (-4 * 60) True "CDT")

test_atStartOfDay_at_overlaps :: [TestTree]
test_atStartOfDay_at_overlaps =
  [ testCase "America/Havana" do
      -- On this date in Havana, at 01:00, the clocks are turned backwards 1 hour,
      -- which means midnight happens twice.
      --
      -- `atStartOfDay` should return the earliest of the two midnights.
      --
      -- See: https://www.timeanddate.com/time/zone/cuba/havana?year=2022
      let tz = TZI.fromLabel TZI.America__Havana
      let tzt = unsafeFromLocalTime tz (LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 10 45 0))
      let result = atStartOfDay tzt

      result @?=
        UnsafeTZTime
          (LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 0 0 0))
          tz
          (TimeZone (-4 * 60) True "CDT")

      -- Sanity checks
      -- Make sure this really is an overlap
      assertBool "Expected `result` to land on an overlap" $
        (result & toUTC) < (result & atLatestOffset & toUTC)
      -- This really is the earliest offset
      result @?= atEarliestOffset result

  , testCase "America/St_Johns" do
      -- Up until 2010, in America/St_Johns, the clocks were turned backwards
      -- 1 hour in autumn at 00:01 back to 23:01 the previous day, meaning the **date**
      -- would be moved backwards.
      --
      -- See:
      --   * https://davecturner.github.io/2017/12/27/timezone-curiosities.html
      --   * https://www.timeanddate.com/time/zone/canada/st-johns?year=2009
      let tz = TZI.fromLabel TZI.America__St_Johns
      let tzt = unsafeFromLocalTime tz (LocalTime (YearMonthDay 2009 11 1) (TimeOfDay 10 45 0))
      let result = atStartOfDay tzt

      result @?=
        UnsafeTZTime
          (LocalTime (YearMonthDay 2009 11 1) (TimeOfDay 0 0 0))
          tz
          (TimeZone (-2 * 60 - 30) True "NDT")

      -- Sanity checks
      -- Make sure this really is an overlap
      assertBool "Expected `result` to land on an overlap" $
        (result & toUTC) < (result & atLatestOffset & toUTC)
      -- This really is the earliest offset
      result @?= atEarliestOffset result
  ]

unit_addTime_and_diffTZTime_operate_on_the_universal_time_line :: Assertion
unit_addTime_and_diffTZTime_operate_on_the_universal_time_line = do
  -- At 01:59:59 on 2022-11-06, the America/Winnipeg time zone switches
  -- from CDT (UTC-5) to CST (UTC-6).
  -- In other words, the clocks are turned back 1 hour, back to 01:00:00.
  let local = LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 0 30 0)
  let tz = TZI.fromLabel TZI.America__Winnipeg
  let tzt = unsafeFromLocalTime tz local

  addTime (hours 1) tzt @?=
    UnsafeTZTime (LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 1 30 0)) tz cdt

  addTime (hours 2) tzt @?=
    UnsafeTZTime (LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 1 30 0)) tz cst

  addTime (hours 3) tzt @?=
    UnsafeTZTime (LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 2 30 0)) tz cst

  diffTZTime (addTime (hours 1) tzt) tzt @?= hours 1
  diffTZTime (addTime (hours 2) tzt) tzt @?= hours 2
  diffTZTime (addTime (hours 3) tzt) tzt @?= hours 3

unit_composing_modifiers :: Assertion
unit_composing_modifiers = do
  let tz = TZI.fromLabel TZI.America__Havana
  let tzt = unsafeFromLocalTime tz (LocalTime (YearMonthDay 2022 11 6) (TimeOfDay 10 45 0))

  let result =
        tzt
          & modifyLocal (
              addCalendarClip (calendarMonths 2 <> calendarDays 3)
              >>> atFirstDayOfWeekOnAfter Wednesday
              >>> atMidnight
            )
          & atEarliestOffset
          & addTime (minutes 1 + seconds 20)

  result @?= unsafeFromLocalTime tz
    (LocalTime (YearMonthDay 2023 1 11) (TimeOfDay 0 1 20))

unit_addCalendar_zero_equals_id :: Assertion
unit_addCalendar_zero_equals_id = do
  -- With `modifyLocal`, adding 0 days should be equivalent
  -- to the identity function.
  let tz = TZI.fromLabel TZI.America__Winnipeg
  let mkLocalTime hh mm ss = LocalTime (YearMonthDay 2022 11 6) (TimeOfDay hh mm ss)

  check $ unsafeFromLocalTime tz (mkLocalTime 0 0 0)
  check $ unsafeFromLocalTime tz (mkLocalTime 2 0 0)
  check $ fromLocalTimeStrict tz (mkLocalTime 1 0 0) & \(Left (TZOverlap _ earliest _)) -> earliest
  check $ fromLocalTimeStrict tz (mkLocalTime 1 0 0) & \(Left (TZOverlap _ _ latest)) -> latest
  check $ fromLocalTimeStrict tz (mkLocalTime 1 30 0) & \(Left (TZOverlap _ earliest _)) -> earliest
  check $ fromLocalTimeStrict tz (mkLocalTime 1 30 0) & \(Left (TZOverlap _ _ latest)) -> latest
  check $ fromLocalTimeStrict tz (mkLocalTime 1 59 0) & \(Left (TZOverlap _ earliest _)) -> earliest
  check $ fromLocalTimeStrict tz (mkLocalTime 1 59 0) & \(Left (TZOverlap _ _ latest)) -> latest

  where
    check :: TZTime -> Assertion
    check tzt = do
      modifyLocal (addCalendarClip (calendarDays 0)) tzt @?= tzt