packages feed

aws-eventbridge-cron 0.1.0.1 → 0.1.1.0

raw patch · 6 files changed

+429/−1 lines, 6 filesdep +tzdep +tzdataPVP ok

version bump matches the API change (PVP)

Dependencies added: tz, tzdata

API changes (from Hackage documentation)

+ AWS.EventBridge.Schedule: Schedule :: CronExprT -> TZ -> TZLabel -> Schedule
+ AWS.EventBridge.Schedule: [scheduleExpr] :: Schedule -> CronExprT
+ AWS.EventBridge.Schedule: [scheduleZoneLabel] :: Schedule -> TZLabel
+ AWS.EventBridge.Schedule: [scheduleZone] :: Schedule -> TZ
+ AWS.EventBridge.Schedule: data Schedule
+ AWS.EventBridge.Schedule: nextRunTimesLocal :: Schedule -> LocalTime -> Int -> Either String [LocalTime]
+ AWS.EventBridge.Schedule: nextRunTimesLocalFromUTC :: Schedule -> UTCTime -> Int -> Either String [LocalTime]
+ AWS.EventBridge.Schedule: nextRunTimesLocalFromZoned :: Schedule -> ZonedTime -> Int -> Either String [LocalTime]
+ AWS.EventBridge.Schedule: nextRunTimesUTC :: Schedule -> UTCTime -> Int -> Either String [UTCTime]
+ AWS.EventBridge.Schedule: nextRunTimesUTCFromLocal :: Schedule -> LocalTime -> Int -> Either String [UTCTime]
+ AWS.EventBridge.Schedule: nextRunTimesUTCFromZoned :: Schedule -> ZonedTime -> Int -> Either String [UTCTime]
+ AWS.EventBridge.Schedule: nextRunTimesZoned :: Schedule -> ZonedTime -> Int -> Either String [ZonedTime]
+ AWS.EventBridge.Schedule: nextRunTimesZonedFromLocal :: Schedule -> LocalTime -> Int -> Either String [ZonedTime]
+ AWS.EventBridge.Schedule: nextRunTimesZonedFromUTC :: Schedule -> UTCTime -> Int -> Either String [ZonedTime]
+ AWS.EventBridge.Schedule: parseCronTextWithZone :: TZLabel -> Text -> Either String Schedule
+ AWS.EventBridge.Schedule: scheduleFromExpr :: TZLabel -> CronExprT -> Schedule
+ AWS.EventBridge.Schedule: scheduleFromText :: TZLabel -> Text -> Either String Schedule

Files

CHANGELOG.md view
@@ -5,6 +5,18 @@ The format is based on Keep a Changelog and this project adheres to Semantic Versioning. +## [0.1.1.0] - 2025-11-20+### Added+- `AWS.EventBridge.Schedule` module that pairs schedules with an IANA timezone+	(via the `tz`/`tzdata` packages) and exposes the full matrix of+	`nextRunTimes` variants for UTC, local, and zoned outputs.+- Smart constructors such as `scheduleFromText` plus helper variants+	(`nextRunTimesUTCFromLocal`, `nextRunTimesZonedFromUTC`, etc.) documented with+	Haddock examples.+- Property-style regression tests in `ScheduleSpec` covering cron/one-time rate+	conversions, DST transitions, and timezone-dependent outputs.+- README examples describing timezone-aware workflows and feature list updates.+ ## [0.1.0.1] - 2025-11-17 ### Added - `ScheduleKind`, `scheduleKind`, and `isRecurring` helpers for classifying parsed schedules without re-parsing downstream.
README.md view
@@ -16,6 +16,9 @@ - Schedule introspection helpers: `scheduleKind` returns a `ScheduleKind`, and   `isRecurring` distinguishes recurring (`cron`/`rate`) expressions from   `at(...)` one-time schedules.+- Timezone-aware helpers: `AWS.EventBridge.Schedule` pairs expressions with an+  IANA timezone (via the `tz`/`tzdata` packages) and exposes `nextRunTimes`+  variants for UTC, local, or fully-zoned outputs. - Extensive property-based test suite that mirrors the behaviour documented by   AWS. @@ -71,6 +74,65 @@ isRecurringExample = isRecurring <$> parseCronText "at(2025-11-16T09:30:00)" -- Right False ```++### Timezone-Aware Schedules++EventBridge rules can set a schedule timezone. Use `AWS.EventBridge.Schedule` to+bind an expression to a `TZLabel` so you can request run times in UTC, as local+wall-clock values, or as `ZonedTime`s tagged with the appropriate offset.++```haskell+import AWS.EventBridge.Schedule+import Data.Time (UTCTime(..), LocalTime, ZonedTime, fromGregorian)+import Data.Time.Zones.All (TZLabel(..))++baseUTC :: UTCTime+baseUTC = UTCTime (fromGregorian 2025 11 1) 0++zonedSchedule :: Either String Schedule+zonedSchedule = scheduleFromText America__New_York "cron(0 9 * NOV ? 2025)"++utcRuns :: Either String [UTCTime]+utcRuns = nextRunTimesUTC <$> zonedSchedule <*> pure baseUTC <*> pure 2+-- Right [2025-11-01 13:00:00 UTC,2025-11-02 14:00:00 UTC]++localRuns :: Either String [LocalTime]+localRuns = nextRunTimesLocalFromUTC <$> zonedSchedule <*> pure baseUTC <*> pure 2+-- Right [2025-11-01 09:00:00,2025-11-02 09:00:00]++zonedRuns :: Either String [ZonedTime]+zonedRuns = nextRunTimesZonedFromUTC <$> zonedSchedule <*> pure baseUTC <*> pure 2+-- Right [2025-11-01 09:00:00 EDT,2025-11-02 09:00:00 EST]+```++Every combination of base input (`UTCTime`, `LocalTime`, `ZonedTime`) and output+form is available, so you can normalize timestamps at the edges of your system+and avoid comparing values that silently belong to different timezones.++### API Overview++1. Parse using `parseCronText` (UTC) or `scheduleFromText` (timezone-aware).+2. Wrap with `scheduleFromExpr`/`scheduleFromText` if you need timezone metadata.+3. Choose an evaluation helper based on the base input you have and the output+  you need. Prefer the primary trio (`nextRunTimesUTC`, `nextRunTimesLocal`,+  `nextRunTimesZoned`) and reach for the conversion helpers when you want to+  avoid manual conversions.++| Base input  | Output      | Function                           |+|-------------|-------------|------------------------------------|+| `UTCTime`   | `UTCTime`   | `nextRunTimesUTC`                  |+| `LocalTime` | `UTCTime`   | `nextRunTimesUTCFromLocal`         |+| `ZonedTime` | `UTCTime`   | `nextRunTimesUTCFromZoned`         |+| `UTCTime`   | `LocalTime` | `nextRunTimesLocalFromUTC`         |+| `LocalTime` | `LocalTime` | `nextRunTimesLocal`                |+| `ZonedTime` | `LocalTime` | `nextRunTimesLocalFromZoned`       |+| `UTCTime`   | `ZonedTime` | `nextRunTimesZonedFromUTC`         |+| `LocalTime` | `ZonedTime` | `nextRunTimesZonedFromLocal`       |+| `ZonedTime` | `ZonedTime` | `nextRunTimesZoned`                |++Use the conversion helpers when you already have a base timestamp in a specific+representation and want the library to handle the translation for you (for+example, UI-provided `LocalTime` that needs to be compared against UTC events).  ### Error Reporting 
aws-eventbridge-cron.cabal view
@@ -1,7 +1,7 @@ cabal-version:      3.0  name:               aws-eventbridge-cron-version:            0.1.0.1+version:            0.1.1.0 synopsis:           AWS EventBridge cron, rate, and one-time parser with scheduler description:   Parse AWS EventBridge cron, rate, and one-time expressions and compute@@ -33,6 +33,7 @@ library   exposed-modules:       AWS.EventBridge.Cron+    , AWS.EventBridge.Schedule   other-modules:       AWS.EventBridge.DayOfMonth     , AWS.EventBridge.DayOfWeek@@ -53,6 +54,8 @@     , megaparsec     , text     , time+    , tz+    , tzdata  test-suite aws-eventbridge-cron-test   type: exitcode-stdio-1.0@@ -69,6 +72,8 @@     , AWS.EventBridge.YearsSpec     , AWS.EventBridge.RateSpec     , AWS.EventBridge.CronSpec+    , AWS.EventBridge.Schedule+    , AWS.EventBridge.ScheduleSpec     , AWS.EventBridge.OneTimeSpec     , TestSupport     , AWS.EventBridge.Cron@@ -93,3 +98,5 @@     , tasty-quickcheck >=0.10 && <0.11     , text     , time+    , tz+    , tzdata
+ src/AWS/EventBridge/Schedule.hs view
@@ -0,0 +1,264 @@+{-# LANGUAGE RecordWildCards #-}++-- | Zone-aware helpers built on top of 'AWS.EventBridge.Cron'.+--+-- ## Choosing an entry point+--+-- 1. Parse the expression: use 'parseCronText' (UTC-only) or 'scheduleFromText'+--    when the rule already specifies an IANA timezone.+-- 2. Wrap the expression with 'scheduleFromExpr' or 'scheduleFromText' to carry+--    timezone metadata.+-- 3. Evaluate upcoming runs via the primary trio:+--+--    * 'nextRunTimesUTC'   – keep everything in UTC and compare against other+--      absolute timestamps.+--    * 'nextRunTimesLocal' – receive wall-clock values in the schedule's zone.+--    * 'nextRunTimesZoned' – like 'nextRunTimesLocal' but tagged with the+--      `TimeZone` used at each occurrence (captures DST changes).+--+-- Conversion helpers (the `*FromUTC`, `*FromLocal`, and `*FromZoned` variants)+-- simply preprocess the base value before delegating to one of the three+-- primary functions. They are handy when your caller already has a specific+-- representation and you want to avoid manual conversions.+--+-- ### Base/Input vs Output quick reference+--+-- | Base type  | Desired output | Function                         |+-- |------------|----------------|----------------------------------|+-- | 'UTCTime'  | 'UTCTime'      | 'nextRunTimesUTC'                |+-- | 'LocalTime'| 'UTCTime'      | 'nextRunTimesUTCFromLocal'       |+-- | 'ZonedTime'| 'UTCTime'      | 'nextRunTimesUTCFromZoned'       |+-- | 'UTCTime'  | 'LocalTime'    | 'nextRunTimesLocalFromUTC'       |+-- | 'LocalTime'| 'LocalTime'    | 'nextRunTimesLocal'              |+-- | 'ZonedTime'| 'LocalTime'    | 'nextRunTimesLocalFromZoned'     |+-- | 'UTCTime'  | 'ZonedTime'    | 'nextRunTimesZonedFromUTC'       |+-- | 'LocalTime'| 'ZonedTime'    | 'nextRunTimesZonedFromLocal'     |+-- | 'ZonedTime'| 'ZonedTime'    | 'nextRunTimesZoned'              |+module AWS.EventBridge.Schedule+  ( -- * Schedule construction+    Schedule(..)+  , scheduleFromExpr+  , scheduleFromText+  , parseCronTextWithZone+    -- * Primary evaluation helpers+  , nextRunTimesUTC+  , nextRunTimesLocal+  , nextRunTimesZoned+    -- * Conversion helpers+  , nextRunTimesUTCFromLocal+  , nextRunTimesUTCFromZoned+  , nextRunTimesLocalFromUTC+  , nextRunTimesLocalFromZoned+  , nextRunTimesZonedFromUTC+  , nextRunTimesZonedFromLocal+  ) where++import AWS.EventBridge.Cron+  ( CronExprT+  , ScheduleKind(..)+  , nextRunTimes+  , parseCronText+  , scheduleKind+  )+import Data.Text (Text)+import Data.Time+  ( LocalTime(..)+  , ZonedTime(..)+  , UTCTime(..)+  , timeOfDayToTime+  , timeToTimeOfDay+  , zonedTimeToUTC+  )+import Data.Time.Zones+  ( TZ+  , localTimeToUTCTZ+  , timeZoneForUTCTime+  , utcToLocalTimeTZ+  )+import Data.Time.Zones.All (TZLabel(..), tzByLabel)++-- | Scheduling expression paired with its IANA timezone.+--+-- Use 'scheduleFromText' when you need to parse the expression and bind the+-- timezone in one step, or 'scheduleFromExpr' if you already have a+-- 'CronExprT'.+data Schedule = Schedule+  { scheduleExpr :: CronExprT+  , scheduleZone :: TZ+  , scheduleZoneLabel :: TZLabel+  }++-- | Construct a 'Schedule' from an existing 'CronExprT' and timezone label.+--+-- @tz@ ships with bindings for the entire IANA database through 'TZLabel'. The+-- resulting schedule can be fed directly into the zone-aware 'nextRunTimes*'+-- helpers below.+scheduleFromExpr :: TZLabel -> CronExprT -> Schedule+scheduleFromExpr label expr = Schedule+  { scheduleExpr = expr+  , scheduleZone = tzByLabel label+  , scheduleZoneLabel = label+  }++-- | Parse an EventBridge expression and attach a timezone in the same step.+--+-- >>> :{+-- let base = read "2025-11-16 03:30:00 UTC" :: UTCTime+-- in case scheduleFromText Asia__Kolkata "cron(0 9 ? NOV SUN 2025)" of+--      Left err -> Left err+--      Right sched -> nextRunTimesUTC sched base 1+-- :}+-- Right [2025-11-16 03:30:00 UTC]+scheduleFromText :: TZLabel -> Text -> Either String Schedule+scheduleFromText label input =+  fmap (scheduleFromExpr label) (parseCronText input)++-- | Backwards-compatible alias for 'scheduleFromText'.+parseCronTextWithZone :: TZLabel -> Text -> Either String Schedule+parseCronTextWithZone = scheduleFromText++-- | Local-time primary helper.+--+-- Evaluate run times in the schedule's local timezone, starting from a local+-- base time that is already expressed in the schedule's zone. Choose this when+-- you want to present results exactly as the rule owner configured them.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 08:00:00" :: LocalTime+-- >>> nextRunTimesLocal sched base 1+-- Right [2025-11-01 09:00:00]+nextRunTimesLocal :: Schedule -> LocalTime -> Int -> Either String [LocalTime]+nextRunTimesLocal = scheduleLocalOccurrences++-- | Conversion helper.+--+-- Evaluate run times in the schedule's local timezone using a UTC base. Ideal+-- when upstream systems give you absolute timestamps (e.g. database clocks) but+-- UI clients expect the local wall clock.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 12:00:00 UTC" :: UTCTime+-- >>> nextRunTimesLocalFromUTC sched base 1+-- Right [2025-11-01 09:00:00]+nextRunTimesLocalFromUTC :: Schedule -> UTCTime -> Int -> Either String [LocalTime]+nextRunTimesLocalFromUTC schedule base = scheduleLocalOccurrences schedule (utcToLocalTimeTZ (scheduleZone schedule) base)++-- | Conversion helper.+--+-- Evaluate run times in the schedule's local timezone using an arbitrary+-- 'ZonedTime' base (the offset on the input is ignored; only the instant+-- matters). Handy when you get user input such as "2025-11-01 09:00 EDT" and+-- want to keep working in local values.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 09:00:00-04:00" :: ZonedTime+-- >>> nextRunTimesLocalFromZoned sched base 2+-- Right [2025-11-01 09:00:00,2025-11-02 09:00:00]+nextRunTimesLocalFromZoned :: Schedule -> ZonedTime -> Int -> Either String [LocalTime]+nextRunTimesLocalFromZoned schedule base = scheduleLocalOccurrences schedule (utcToLocalTimeTZ (scheduleZone schedule) (zonedTimeToUTC base))++-- | UTC primary helper.+--+-- Evaluate run times in UTC while accepting a UTC base. This is the simplest+-- option when the rest of your system already speaks UTC.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 12:30:00 UTC" :: UTCTime+-- >>> nextRunTimesUTC sched base 1+-- Right [2025-11-01 13:00:00 UTC]+nextRunTimesUTC :: Schedule -> UTCTime -> Int -> Either String [UTCTime]+nextRunTimesUTC schedule base limit =+  fmap (map (localToUTC schedule)) (nextRunTimesLocalFromUTC schedule base limit)++-- | Conversion helper.+--+-- Evaluate run times in UTC with a local base. Use this when your caller has+-- local wall time but downstream systems expect UTC instants.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 08:00:00" :: LocalTime+-- >>> nextRunTimesUTCFromLocal sched base 1+-- Right [2025-11-01 13:00:00 UTC]+nextRunTimesUTCFromLocal :: Schedule -> LocalTime -> Int -> Either String [UTCTime]+nextRunTimesUTCFromLocal schedule base limit =+  fmap (map (localToUTC schedule)) (nextRunTimesLocal schedule base limit)++-- | Conversion helper.+--+-- Evaluate run times in UTC with a 'ZonedTime' base. Helpful when upstream APIs+-- hand you zoned timestamps and you want to compare the schedule against other+-- UTC data.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 09:00:00-04:00" :: ZonedTime+-- >>> nextRunTimesUTCFromZoned sched base 1+-- Right [2025-11-01 13:00:00 UTC]+nextRunTimesUTCFromZoned :: Schedule -> ZonedTime -> Int -> Either String [UTCTime]+nextRunTimesUTCFromZoned schedule base limit =+  fmap (map (localToUTC schedule)) (nextRunTimesLocalFromZoned schedule base limit)++-- | Zoned-time primary helper.+--+-- Evaluate run times as 'ZonedTime' values using a zoned base. This preserves+-- the original offset used for the base and propagates DST changes into the+-- outputs, which is useful for logs or API responses that must mention the+-- effective offset explicitly.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 09:00:00-04:00" :: ZonedTime+-- >>> nextRunTimesZoned sched base 2+-- Right [2025-11-01 09:00:00-04:00,2025-11-02 09:00:00-05:00]+nextRunTimesZoned :: Schedule -> ZonedTime -> Int -> Either String [ZonedTime]+nextRunTimesZoned schedule base limit =+  fmap (map (localToZoned schedule)) (nextRunTimesLocalFromZoned schedule base limit)++-- | Conversion helper.+--+-- Evaluate run times as 'ZonedTime' values from a UTC base.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 12:30:00 UTC" :: UTCTime+-- >>> nextRunTimesZonedFromUTC sched base 2+-- Right [2025-11-01 09:00:00-04:00,2025-11-02 09:00:00-05:00]+nextRunTimesZonedFromUTC :: Schedule -> UTCTime -> Int -> Either String [ZonedTime]+nextRunTimesZonedFromUTC schedule base limit =+  fmap (map (localToZoned schedule)) (nextRunTimesLocalFromUTC schedule base limit)++-- | Conversion helper.+--+-- Evaluate run times as 'ZonedTime' values from a local base.+--+-- >>> let Right sched = scheduleFromText America__New_York "cron(0 9 * * ? *)"+-- >>> let base = read "2025-11-01 09:00:00" :: LocalTime+-- >>> nextRunTimesZonedFromLocal sched base 1+-- Right [2025-11-01 09:00:00-04:00]+nextRunTimesZonedFromLocal :: Schedule -> LocalTime -> Int -> Either String [ZonedTime]+nextRunTimesZonedFromLocal schedule base limit =+  fmap (map (localToZoned schedule)) (nextRunTimesLocal schedule base limit)++scheduleLocalOccurrences :: Schedule -> LocalTime -> Int -> Either String [LocalTime]+scheduleLocalOccurrences sched@Schedule{..} base limit =+  case scheduleKind scheduleExpr of+    RateSchedule -> do+      let baseUtc = localToUTC sched base+      timesUtc <- nextRunTimes scheduleExpr baseUtc limit+      pure (map (utcToLocalTimeTZ scheduleZone) timesUtc)+    _ ->+      fmap (map utcToLocalNaive) (nextRunTimes scheduleExpr (localToNaiveUTC base) limit)++localToNaiveUTC :: LocalTime -> UTCTime+localToNaiveUTC (LocalTime day tod) = UTCTime day (timeOfDayToTime tod)++utcToLocalNaive :: UTCTime -> LocalTime+utcToLocalNaive (UTCTime day diff) = LocalTime day (timeToTimeOfDay diff)++localToUTC :: Schedule -> LocalTime -> UTCTime+localToUTC Schedule{..} = localTimeToUTCTZ scheduleZone++localToZoned :: Schedule -> LocalTime -> ZonedTime+localToZoned schedule local =+  let utcVal = localToUTC schedule local+      zone = scheduleZone schedule+      localWall = utcToLocalTimeTZ zone utcVal+      tzInfo = timeZoneForUTCTime zone utcVal+   in ZonedTime localWall tzInfo
+ test/AWS/EventBridge/ScheduleSpec.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE OverloadedStrings #-}++module AWS.EventBridge.ScheduleSpec (tests) where++import AWS.EventBridge.Schedule+import AWS.EventBridge.Cron (parseCronText)+import Data.Time+  ( LocalTime+  , UTCTime(..)+  , ZonedTime+  , addUTCTime+  , fromGregorian+  , secondsToDiffTime+  )+import Data.Time.Zones.All (TZLabel(..))+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit++tests :: TestTree+tests = testGroup "schedule"+  [ testCase "cron nextRunTimesUTC respects timezone" $ do+      sched <- either assertFailure pure (scheduleFromText Asia__Kolkata "cron(0 9 * * ? *)")+      let base = UTCTime (fromGregorian 2025 11 16) 0+          expected = UTCTime (fromGregorian 2025 11 16) (secondsToDiffTime (3 * 3600 + 30 * 60))+      nextRunTimesUTC sched base 1 @?= Right [expected]++    , testCase "cron zoned outputs reflect DST" $ do+      sched <- either assertFailure pure (scheduleFromText America__New_York "cron(0 9 * * ? *)")+      let base = read "2025-11-01 09:00:00-04:00" :: ZonedTime+          expected =+            [ "2025-11-01 09:00:00 EDT"+            , "2025-11-02 09:00:00 EST"+            ]++      fmap (map show) (nextRunTimesZoned sched base 2) @?= Right expected++  , testCase "rate expressions remain absolute" $ do+      expr <- either assertFailure pure (parseCronText "rate(1 hour)")+      let sched = scheduleFromExpr Asia__Tokyo expr+          base = UTCTime (fromGregorian 2025 11 1) 0+          expected = take 2 (iterate (addUTCTime 3600) base)+      nextRunTimesUTC sched base 2 @?= Right expected++    , testCase "local helper trims times before base" $ do+      sched <- either assertFailure pure (scheduleFromText Asia__Kolkata "cron(0 9 * * ? *)")+      let baseLocal = read "2025-11-16 08:30:00" :: LocalTime+          expected =+            [ read "2025-11-16 09:00:00" :: LocalTime+            , read "2025-11-17 09:00:00"+            ]+      nextRunTimesLocal sched baseLocal 2 @?= Right expected++    , testCase "local-from-UTC matches local helper" $ do+      sched <- either assertFailure pure (scheduleFromText Asia__Kolkata "cron(0 9 * * ? *)")+      let baseUtc = UTCTime (fromGregorian 2025 11 16) (secondsToDiffTime (3 * 3600 + 30 * 60))+      nextRunTimesLocalFromUTC sched baseUtc 1+        @?= nextRunTimesLocal sched (read "2025-11-16 09:00:00" :: LocalTime) 1++    , testCase "UTC-from-local converts to absolute" $ do+      sched <- either assertFailure pure (scheduleFromText Asia__Kolkata "cron(0 9 * * ? *)")+      let baseLocal = read "2025-11-16 09:00:00" :: LocalTime+          expected = UTCTime (fromGregorian 2025 11 16) (secondsToDiffTime (3 * 3600 + 30 * 60))+      nextRunTimesUTCFromLocal sched baseLocal 1 @?= Right [expected]++    , testCase "zoned-from-UTC emits offsets" $ do+      sched <- either assertFailure pure (scheduleFromText America__New_York "cron(0 9 * * ? *)")+      let baseUtc = UTCTime (fromGregorian 2025 11 1) (secondsToDiffTime (12 * 3600))+          expected =+            [ "2025-11-01 09:00:00 EDT"+            , "2025-11-02 09:00:00 EST"+            ]+      fmap (map show) (nextRunTimesZonedFromUTC sched baseUtc 2) @?= Right expected++    , testCase "parseCronTextWithZone aliases scheduleFromText" $ do+      let expr = "cron(0 0 1 1 ? 2025)"+      schedA <- either assertFailure pure (parseCronTextWithZone Asia__Kolkata expr)+      schedB <- either assertFailure pure (scheduleFromText Asia__Kolkata expr)+      scheduleZoneLabel schedA @?= scheduleZoneLabel schedB+      nextRunTimesUTC schedA (UTCTime (fromGregorian 2025 1 1) 0) 1+        @?= nextRunTimesUTC schedB (UTCTime (fromGregorian 2025 1 1) 0) 1+  ]
test/Main.hs view
@@ -9,6 +9,7 @@ import qualified AWS.EventBridge.RateSpec as RateSpec import qualified AWS.EventBridge.OneTimeSpec as OneTimeSpec import qualified AWS.EventBridge.CronSpec as CronSpec+import qualified AWS.EventBridge.ScheduleSpec as ScheduleSpec import Test.Tasty (defaultMain, testGroup)  main :: IO ()@@ -24,4 +25,5 @@       , RateSpec.tests       , OneTimeSpec.tests       , CronSpec.tests+      , ScheduleSpec.tests       ]