packages feed

saturn (empty) → 0.1.0.0

raw patch · 31 files changed

+1870/−0 lines, 31 filesdep +QuickCheckdep +basedep +containers

Dependencies added: QuickCheck, base, containers, hspec, parsec, saturn, text, time

Files

+ CHANGELOG.markdown view
@@ -0,0 +1,4 @@+# Change log++Saturn follows the [Package Versioning Policy](https://pvp.haskell.org). You+can find release notes [on GitHub](https://github.com/tfausak/saturn/releases).
+ LICENSE.markdown view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2023 Taylor Fausak++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.markdown view
@@ -0,0 +1,9 @@+# Saturn++[![Workflow](https://github.com/tfausak/saturn/actions/workflows/workflow.yaml/badge.svg)](https://github.com/tfausak/saturn/actions/workflows/workflow.yaml)+[![Hackage](https://img.shields.io/hackage/v/saturn)](https://hackage.haskell.org/package/saturn)+[![Stackage](https://www.stackage.org/package/saturn/badge/nightly?label=stackage)](https://www.stackage.org/package/saturn)++:ringed_planet: Handle POSIX cron schedules.++See the documentation on Hackage: <https://hackage.haskell.org/package/saturn>.
+ saturn.cabal view
@@ -0,0 +1,93 @@+cabal-version:   2.2+name:            saturn+version:         0.1.0.0+synopsis:        Handle POSIX cron schedules.+description:     Saturn handles POSIX cron schedules.+build-type:      Simple+category:        Cron+extra-doc-files:+  CHANGELOG.markdown+  README.markdown++maintainer:      Taylor Fausak+license:         MIT+license-file:    LICENSE.markdown++source-repository head+  location: https://github.com/tfausak/saturn+  type:     git++flag pedantic+  default: False+  manual:  True++common library+  build-depends:+    , base ^>=4.16.4 || ^>=4.17.0 || ^>=4.18.0+    , parsec ^>=3.1.15+    , time ^>=1.11.1 || ^>=1.12.2++  default-extensions: FlexibleContexts+  default-language:   Haskell2010+  ghc-options:+    -Weverything -Wno-all-missed-specialisations -Wno-implicit-prelude+    -Wno-missed-specialisations -Wno-missing-deriving-strategies+    -Wno-missing-export-lists -Wno-missing-kind-signatures+    -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module+    -Wno-safe -Wno-unsafe++  if flag(pedantic)+    ghc-options: -Werror++common executable+  import:        library+  build-depends: saturn+  ghc-options:   -rtsopts -threaded++library+  import:          library+  build-depends:+    , containers ^>=0.6.5+    , text ^>=1.2.5 || ^>=2.0.2++  exposed-modules:+    Saturn+    Saturn.Unstable.Extra.Int+    Saturn.Unstable.Extra.Ord+    Saturn.Unstable.Extra.Parsec+    Saturn.Unstable.Extra.Time+    Saturn.Unstable.Extra.Tuple+    Saturn.Unstable.Match+    Saturn.Unstable.Parse+    Saturn.Unstable.Render+    Saturn.Unstable.Type.Day+    Saturn.Unstable.Type.Element+    Saturn.Unstable.Type.Field+    Saturn.Unstable.Type.Hour+    Saturn.Unstable.Type.Minute+    Saturn.Unstable.Type.Month+    Saturn.Unstable.Type.Number+    Saturn.Unstable.Type.Range+    Saturn.Unstable.Type.Schedule+    Saturn.Unstable.Type.Weekday+    Saturn.Unstable.Type.Wildcard++  hs-source-dirs:  source/library++test-suite saturn-test-suite+  import:         executable+  build-depends:+    , hspec ^>=2.11.2+    , QuickCheck ^>=2.14.3++  hs-source-dirs: source/test-suite+  main-is:        Main.hs+  other-modules:+    Saturn.Unstable.Extra.IntSpec+    Saturn.Unstable.Extra.OrdSpec+    Saturn.Unstable.Extra.ParsecSpec+    Saturn.Unstable.Extra.TimeSpec+    Saturn.Unstable.Extra.TupleSpec+    SaturnSpec++  type:           exitcode-stdio-1.0
+ source/library/Saturn.hs view
@@ -0,0 +1,64 @@+-- | Saturn handles POSIX cron schedules, which are defined here:+-- <https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07>.+--+-- A cron schedule is specified with five fields, each separated with at least+-- one space. Each field can either be a wildcard (represented by an asterisk),+-- or it can be one or more elements separated by commas. Each element can+-- either be a number or a range, which is two numbers separated by a hyphen.+--+-- In order, the fields represent:+--+-- 1. Minute, between 0 and 59.+-- 2. Hour, between 0 and 23.+-- 3. Day, between 1 and 31.+-- 4. Month, between 1 and 12.+-- 5. Weekday, between 0 and 6 where 0 represents Sunday.+--+-- Here is a more graphical representation of the fields:+--+-- >  +--------- Minute+-- >  | +------- Hour+-- >  | | +----- Day+-- >  | | | +--- Month+-- >  | | | | +- Weekday+-- >  | | | | |+-- > "* * * * *"+--+-- To get started, use 'Parse.fromText' to parse a 'Schedule.Schedule'. Then+-- use 'Render.toText' to render it again. To see if the 'Schedule.Schedule'+-- matches a certain time, use 'Match.isMatch'. To get the next time that+-- matches a schedule, use 'Match.nextMatch'.+--+-- >>> :set -XOverloadedStrings+-- >>> let Right schedule = fromText "* * * * *"+-- >>> toText schedule+-- "* * * * *"+-- >>> let utcTime = Data.Time.UTCTime (Data.Time.fromGregorian 1970 1 1) 0+-- >>> isMatch utcTime schedule+-- True+-- >>> nextMatch utcTime schedule+-- Just 1970-01-01 00:01:00 UTC+module Saturn+  ( -- * Types+    Schedule.Schedule,++    -- * Parsing+    Parse.fromText,+    Parse.fromLazyText,+    Parse.fromString,++    -- * Rendering+    Render.toText,+    Render.toLazyText,+    Render.toString,++    -- * Matching+    Match.isMatch,+    Match.nextMatch,+  )+where++import qualified Saturn.Unstable.Match as Match+import qualified Saturn.Unstable.Parse as Parse+import qualified Saturn.Unstable.Render as Render+import qualified Saturn.Unstable.Type.Schedule as Schedule
+ source/library/Saturn/Unstable/Extra/Int.hs view
@@ -0,0 +1,10 @@+module Saturn.Unstable.Extra.Int where++import qualified Data.Bits as Bits+import qualified Data.Word as Word++fromWord8 :: Word.Word8 -> Int+fromWord8 = fromIntegral++toWord8 :: Int -> Maybe Word.Word8+toWord8 = Bits.toIntegralSized
+ source/library/Saturn/Unstable/Extra/Ord.hs view
@@ -0,0 +1,4 @@+module Saturn.Unstable.Extra.Ord where++within :: (Ord a) => (a, a) -> a -> Bool+within (lo, hi) x = lo <= x && x <= hi
+ source/library/Saturn/Unstable/Extra/Parsec.hs view
@@ -0,0 +1,16 @@+module Saturn.Unstable.Extra.Parsec where++import qualified Data.List.NonEmpty as NonEmpty+import qualified Text.Parsec as Parsec++either ::+  Parsec.ParsecT s u m a ->+  Parsec.ParsecT s u m b ->+  Parsec.ParsecT s u m (Either a b)+either l r = fmap Left l Parsec.<|> fmap Right r++sepByNE ::+  Parsec.ParsecT s u m a ->+  Parsec.ParsecT s u m sep ->+  Parsec.ParsecT s u m (NonEmpty.NonEmpty a)+sepByNE p s = (NonEmpty.:|) <$> p <*> Parsec.many (s *> p)
+ source/library/Saturn/Unstable/Extra/Time.hs view
@@ -0,0 +1,14 @@+module Saturn.Unstable.Extra.Time where++import qualified Data.Time as Time+import qualified Data.Word as Word++dayOfWeekToWord8 :: Time.DayOfWeek -> Word.Word8+dayOfWeekToWord8 dayOfWeek = case dayOfWeek of+  Time.Sunday -> 0+  Time.Monday -> 1+  Time.Tuesday -> 2+  Time.Wednesday -> 3+  Time.Thursday -> 4+  Time.Friday -> 5+  Time.Saturday -> 6
+ source/library/Saturn/Unstable/Extra/Tuple.hs view
@@ -0,0 +1,9 @@+module Saturn.Unstable.Extra.Tuple where++import qualified Data.Bifunctor as Bifunctor++mapBoth :: (a -> b) -> (a, a) -> (b, b)+mapBoth f = Bifunctor.bimap f f++toSequence :: (Enum a) => (a, a) -> [a]+toSequence = uncurry enumFromTo
+ source/library/Saturn/Unstable/Match.hs view
@@ -0,0 +1,89 @@+module Saturn.Unstable.Match where++import qualified Control.Monad as Monad+import qualified Data.List as List+import qualified Data.Maybe as Maybe+import qualified Data.Set as Set+import qualified Data.Time as Time+import qualified Saturn.Unstable.Extra.Int as Int+import qualified Saturn.Unstable.Extra.Time as Time+import qualified Saturn.Unstable.Type.Day as Day+import qualified Saturn.Unstable.Type.Field as Field+import qualified Saturn.Unstable.Type.Hour as Hour+import qualified Saturn.Unstable.Type.Minute as Minute+import qualified Saturn.Unstable.Type.Month as Month+import qualified Saturn.Unstable.Type.Schedule as Schedule+import qualified Saturn.Unstable.Type.Weekday as Weekday+import qualified Saturn.Unstable.Type.Wildcard as Wildcard++-- | Returns 'True' if the given 'Time.UTCTime' matches the given+-- 'Schedule.Schedule', otherwise returns 'False'.+isMatch :: Time.UTCTime -> Schedule.Schedule -> Bool+isMatch utcTime schedule = Maybe.fromMaybe False $ do+  let time = Time.pastMidnight $ Time.utctDayTime utcTime+  minute <- Int.toWord8 $ Time.todMin time+  Monad.guard . Minute.isMatch minute $ Schedule.minute schedule++  hour <- Int.toWord8 $ Time.todHour time+  Monad.guard . Hour.isMatch hour $ Schedule.hour schedule++  let date = Time.utctDay utcTime+  let (_, monthOfYear, dayOfMonth) = Time.toGregorian date+  month <- Int.toWord8 monthOfYear+  Monad.guard . Month.isMatch month $ Schedule.month schedule++  day <- Int.toWord8 dayOfMonth+  let dayMatches = Day.isMatch day $ Schedule.day schedule+  let weekday = Time.dayOfWeekToWord8 $ Time.dayOfWeek date+  let weekdayMatches = Weekday.isMatch weekday $ Schedule.weekday schedule+  Monad.guard $+    if dayIsWildcard schedule || weekdayIsWildcard schedule+      then dayMatches && weekdayMatches+      else dayMatches || weekdayMatches++  pure True++dayIsWildcard :: Schedule.Schedule -> Bool+dayIsWildcard = Field.isWildcard . Day.toField . Schedule.day++weekdayIsWildcard :: Schedule.Schedule -> Bool+weekdayIsWildcard = Field.isWildcard . Weekday.toField . Schedule.weekday++-- | Looks for the first time after the given 'Time.UTCTime' that matches the+-- given 'Schedule.Schedule'. Returns 'Nothing' if the 'Schedule.Schedule' only+-- matches dates that cannot happen, like February 30th.+nextMatch :: Time.UTCTime -> Schedule.Schedule -> Maybe Time.UTCTime+nextMatch utcTime schedule =+  if dayIsWildcard schedule || weekdayIsWildcard schedule+    then incompleteNextMatch utcTime schedule+    else do+      let wildcard = Field.fromEither . Left $ Wildcard.fromUnit ()+      day <- Day.fromField wildcard+      weekday <- Weekday.fromField wildcard+      Maybe.listToMaybe . List.sort $+        Maybe.catMaybes+          [ incompleteNextMatch utcTime schedule {Schedule.day = day},+            incompleteNextMatch utcTime schedule {Schedule.weekday = weekday}+          ]++incompleteNextMatch :: Time.UTCTime -> Schedule.Schedule -> Maybe Time.UTCTime+incompleteNextMatch utcTime schedule = Maybe.listToMaybe $ do+  let oldDate = Time.utctDay utcTime+  let (oldYear, _, _) = Time.toGregorian oldDate+  year <- [oldYear .. oldYear + 8]+  month <- fmap Int.fromWord8 . Set.toAscList . Month.expand $ Schedule.month schedule+  day <- fmap Int.fromWord8 . Set.toAscList . Day.expand $ Schedule.day schedule+  date <- Maybe.maybeToList $ Time.fromGregorianValid year month day+  Monad.guard $ date >= oldDate+  Monad.guard+    . Set.member (Time.dayOfWeekToWord8 $ Time.dayOfWeek date)+    . Weekday.expand+    $ Schedule.weekday schedule+  hour <- fmap Int.fromWord8 . Set.toAscList . Hour.expand $ Schedule.hour schedule+  minute <- fmap Int.fromWord8 . Set.toAscList . Minute.expand $ Schedule.minute schedule+  time <-+    fmap Time.sinceMidnight+      . Maybe.maybeToList+      $ Time.makeTimeOfDayValid hour minute 0+  Monad.when (date == oldDate) . Monad.guard $ time > Time.utctDayTime utcTime+  pure Time.UTCTime {Time.utctDay = date, Time.utctDayTime = time}
+ source/library/Saturn/Unstable/Parse.hs view
@@ -0,0 +1,28 @@+module Saturn.Unstable.Parse where++import qualified Data.Text as Text+import qualified Data.Text.Lazy as LazyText+import qualified Saturn.Unstable.Type.Schedule as Schedule+import qualified Text.Parsec as Parsec++-- | Parses a lazy 'LazyText.Text' value into a 'Schedule.Schedule'. See+-- 'fromText' for details.+fromLazyText :: LazyText.Text -> Either Parsec.ParseError Schedule.Schedule+fromLazyText = Parsec.parse parsec ""++-- | Parses a 'String' into a 'Schedule.Schedule'. See 'fromText' for details.+fromString :: String -> Either Parsec.ParseError Schedule.Schedule+fromString = Parsec.parse parsec ""++-- | Parses a strict 'Text.Text' value into a 'Schedule.Schedule'. The input+-- should have five fields, each separated by at least one space. Leading and+-- trailing spaces are allowed.+fromText :: Text.Text -> Either Parsec.ParseError Schedule.Schedule+fromText = Parsec.parse parsec ""++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Schedule.Schedule+parsec =+  Parsec.skipMany (Parsec.char ' ')+    *> Schedule.parsec+    <* Parsec.skipMany (Parsec.char ' ')+    <* Parsec.eof
+ source/library/Saturn/Unstable/Render.hs view
@@ -0,0 +1,21 @@+module Saturn.Unstable.Render where++import qualified Data.Text as Text+import qualified Data.Text.Lazy as LazyText+import qualified Data.Text.Lazy.Builder as Builder+import qualified Saturn.Unstable.Type.Schedule as Schedule++-- | Renders a 'Schedule.Schedule' into a lazy 'LazyText.Text' value. See+-- 'toText' for details.+toLazyText :: Schedule.Schedule -> LazyText.Text+toLazyText = Builder.toLazyText . Schedule.toBuilder++-- | Renders a 'Schedule.Schedule' into a 'String' value. See 'toText' for+-- details.+toString :: Schedule.Schedule -> String+toString = LazyText.unpack . toLazyText++-- | Renders a 'Schedule.Schedule' into a strict 'Text.Text' value. The output+-- will have five fields, each separated by a single space.+toText :: Schedule.Schedule -> Text.Text+toText = LazyText.toStrict . toLazyText
+ source/library/Saturn/Unstable/Type/Day.hs view
@@ -0,0 +1,36 @@+module Saturn.Unstable.Type.Day where++import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Field as Field+import qualified Text.Parsec as Parsec++newtype Day+  = Day Field.Field+  deriving (Eq, Show)++bounds :: (Word.Word8, Word.Word8)+bounds = (1, 31)++fromField :: Field.Field -> Maybe Day+fromField field =+  if Field.isValid bounds field then Just $ Day field else Nothing++toField :: Day -> Field.Field+toField = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Day+parsec = do+  field <- Field.parsec+  maybe (fail "invalid Day") pure $ fromField field++toBuilder :: Day -> Builder.Builder+toBuilder = Field.toBuilder . toField++expand :: Day -> Set.Set Word.Word8+expand = Field.expand bounds . toField++isMatch :: Word.Word8 -> Day -> Bool+isMatch word8 = Set.member word8 . expand
+ source/library/Saturn/Unstable/Type/Element.hs view
@@ -0,0 +1,32 @@+module Saturn.Unstable.Type.Element where++import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Extra.Parsec as Parsec+import qualified Saturn.Unstable.Type.Number as Number+import qualified Saturn.Unstable.Type.Range as Range+import qualified Text.Parsec as Parsec++newtype Element+  = Element (Either Range.Range Number.Number)+  deriving (Eq, Show)++fromEither :: Either Range.Range Number.Number -> Element+fromEither = Coerce.coerce++toEither :: Element -> Either Range.Range Number.Number+toEither = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Element+parsec = fromEither <$> Parsec.either (Parsec.try Range.parsec) Number.parsec++toBuilder :: Element -> Builder.Builder+toBuilder = either Range.toBuilder Number.toBuilder . toEither++isValid :: (Word.Word8, Word.Word8) -> Element -> Bool+isValid tuple = either (Range.isValid tuple) (Number.isValid tuple) . toEither++expand :: Element -> Set.Set Word.Word8+expand = either Range.expand (Set.singleton . Number.toWord8) . toEither
+ source/library/Saturn/Unstable/Type/Field.hs view
@@ -0,0 +1,56 @@+module Saturn.Unstable.Type.Field where++import qualified Data.Coerce as Coerce+import qualified Data.Either as Either+import qualified Data.Foldable as Foldable+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Extra.Parsec as Parsec+import qualified Saturn.Unstable.Extra.Tuple as Tuple+import qualified Saturn.Unstable.Type.Element as Element+import qualified Saturn.Unstable.Type.Wildcard as Wildcard+import qualified Text.Parsec as Parsec++newtype Field+  = Field (Either Wildcard.Wildcard (NonEmpty.NonEmpty Element.Element))+  deriving (Eq, Show)++fromEither ::+  Either Wildcard.Wildcard (NonEmpty.NonEmpty Element.Element) -> Field+fromEither = Coerce.coerce++toEither ::+  Field -> Either Wildcard.Wildcard (NonEmpty.NonEmpty Element.Element)+toEither = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Field+parsec =+  fromEither+    <$> Parsec.either+      Wildcard.parsec+      (Parsec.sepByNE Element.parsec $ Parsec.char ',')++toBuilder :: Field -> Builder.Builder+toBuilder =+  either+    Wildcard.toBuilder+    ( Foldable.fold+        . NonEmpty.intersperse (Builder.singleton ',')+        . fmap Element.toBuilder+    )+    . toEither++isValid :: (Word.Word8, Word.Word8) -> Field -> Bool+isValid tuple = either (const True) (all $ Element.isValid tuple) . toEither++expand :: (Word.Word8, Word.Word8) -> Field -> Set.Set Word.Word8+expand tuple =+  either+    (const . Set.fromList $ Tuple.toSequence tuple)+    (Set.unions . fmap Element.expand)+    . toEither++isWildcard :: Field -> Bool+isWildcard = Either.isLeft . toEither
+ source/library/Saturn/Unstable/Type/Hour.hs view
@@ -0,0 +1,36 @@+module Saturn.Unstable.Type.Hour where++import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Field as Field+import qualified Text.Parsec as Parsec++newtype Hour+  = Hour Field.Field+  deriving (Eq, Show)++bounds :: (Word.Word8, Word.Word8)+bounds = (0, 23)++fromField :: Field.Field -> Maybe Hour+fromField field =+  if Field.isValid bounds field then Just $ Hour field else Nothing++toField :: Hour -> Field.Field+toField = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Hour+parsec = do+  field <- Field.parsec+  maybe (fail "invalid Hour") pure $ fromField field++toBuilder :: Hour -> Builder.Builder+toBuilder = Field.toBuilder . toField++expand :: Hour -> Set.Set Word.Word8+expand = Field.expand bounds . toField++isMatch :: Word.Word8 -> Hour -> Bool+isMatch word8 = Set.member word8 . expand
+ source/library/Saturn/Unstable/Type/Minute.hs view
@@ -0,0 +1,36 @@+module Saturn.Unstable.Type.Minute where++import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Field as Field+import qualified Text.Parsec as Parsec++newtype Minute+  = Minute Field.Field+  deriving (Eq, Show)++bounds :: (Word.Word8, Word.Word8)+bounds = (0, 59)++fromField :: Field.Field -> Maybe Minute+fromField field =+  if Field.isValid bounds field then Just $ Minute field else Nothing++toField :: Minute -> Field.Field+toField = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Minute+parsec = do+  field <- Field.parsec+  maybe (fail "invalid Minute") pure $ fromField field++toBuilder :: Minute -> Builder.Builder+toBuilder = Field.toBuilder . toField++expand :: Minute -> Set.Set Word.Word8+expand = Field.expand bounds . toField++isMatch :: Word.Word8 -> Minute -> Bool+isMatch word8 = Set.member word8 . expand
+ source/library/Saturn/Unstable/Type/Month.hs view
@@ -0,0 +1,36 @@+module Saturn.Unstable.Type.Month where++import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Field as Field+import qualified Text.Parsec as Parsec++newtype Month+  = Month Field.Field+  deriving (Eq, Show)++bounds :: (Word.Word8, Word.Word8)+bounds = (1, 12)++fromField :: Field.Field -> Maybe Month+fromField field =+  if Field.isValid bounds field then Just $ Month field else Nothing++toField :: Month -> Field.Field+toField = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Month+parsec = do+  field <- Field.parsec+  maybe (fail "invalid Month") pure $ fromField field++toBuilder :: Month -> Builder.Builder+toBuilder = Field.toBuilder . toField++expand :: Month -> Set.Set Word.Word8+expand = Field.expand bounds . toField++isMatch :: Word.Word8 -> Month -> Bool+isMatch word8 = Set.member word8 . expand
+ source/library/Saturn/Unstable/Type/Number.hs view
@@ -0,0 +1,33 @@+module Saturn.Unstable.Type.Number where++import qualified Data.Bits as Bits+import qualified Data.Coerce as Coerce+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Text.Lazy.Builder.Int as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Extra.Ord as Ord+import qualified Text.Parsec as Parsec+import qualified Text.Read as Read++newtype Number+  = Number Word.Word8+  deriving (Eq, Show)++fromWord8 :: Word.Word8 -> Number+fromWord8 = Coerce.coerce++toWord8 :: Number -> Word.Word8+toWord8 = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Number+parsec = do+  string <- Parsec.many1 Parsec.digit+  integer <- maybe (fail "invalidNumber") pure $ Read.readMaybe string+  maybe (fail "invalidNumber") (pure . fromWord8) $+    Bits.toIntegralSized (integer :: Integer)++toBuilder :: Number -> Builder.Builder+toBuilder = Builder.decimal . toWord8++isValid :: (Word.Word8, Word.Word8) -> Number -> Bool+isValid tuple = Ord.within tuple . toWord8
+ source/library/Saturn/Unstable/Type/Range.hs view
@@ -0,0 +1,45 @@+module Saturn.Unstable.Type.Range where++import qualified Control.Monad as Monad+import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Extra.Tuple as Tuple+import qualified Saturn.Unstable.Type.Number as Number+import qualified Text.Parsec as Parsec++newtype Range+  = Range (Number.Number, Number.Number)+  deriving (Eq, Show)++fromTuple :: (Number.Number, Number.Number) -> Maybe Range+fromTuple (lo, hi) =+  if Number.toWord8 lo > Number.toWord8 hi+    then Nothing+    else Just $ Range (lo, hi)++toTuple :: Range -> (Number.Number, Number.Number)+toTuple = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Range+parsec = do+  lo <- Number.parsec+  Monad.void $ Parsec.char '-'+  hi <- Number.parsec+  maybe (fail "invalid Range") pure $ fromTuple (lo, hi)++toBuilder :: Range -> Builder.Builder+toBuilder range =+  let (lo, hi) = toTuple range+   in Number.toBuilder lo <> Builder.singleton '-' <> Number.toBuilder hi++isValid :: (Word.Word8, Word.Word8) -> Range -> Bool+isValid tuple = uncurry (&&) . Tuple.mapBoth (Number.isValid tuple) . toTuple++expand :: Range -> Set.Set Word.Word8+expand =+  Set.fromList+    . Tuple.toSequence+    . Tuple.mapBoth Number.toWord8+    . toTuple
+ source/library/Saturn/Unstable/Type/Schedule.hs view
@@ -0,0 +1,43 @@+module Saturn.Unstable.Type.Schedule where++import qualified Data.Text.Lazy.Builder as Builder+import qualified Saturn.Unstable.Type.Day as Day+import qualified Saturn.Unstable.Type.Hour as Hour+import qualified Saturn.Unstable.Type.Minute as Minute+import qualified Saturn.Unstable.Type.Month as Month+import qualified Saturn.Unstable.Type.Weekday as Weekday+import qualified Text.Parsec as Parsec++data Schedule = Schedule+  { minute :: Minute.Minute,+    hour :: Hour.Hour,+    day :: Day.Day,+    month :: Month.Month,+    weekday :: Weekday.Weekday+  }+  deriving (Eq, Show)++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Schedule+parsec =+  Schedule+    <$> Minute.parsec+    <* Parsec.skipMany1 (Parsec.char ' ')+    <*> Hour.parsec+    <* Parsec.skipMany1 (Parsec.char ' ')+    <*> Day.parsec+    <* Parsec.skipMany1 (Parsec.char ' ')+    <*> Month.parsec+    <* Parsec.skipMany1 (Parsec.char ' ')+    <*> Weekday.parsec++toBuilder :: Schedule -> Builder.Builder+toBuilder schedule =+  Minute.toBuilder (minute schedule)+    <> Builder.singleton ' '+    <> Hour.toBuilder (hour schedule)+    <> Builder.singleton ' '+    <> Day.toBuilder (day schedule)+    <> Builder.singleton ' '+    <> Month.toBuilder (month schedule)+    <> Builder.singleton ' '+    <> Weekday.toBuilder (weekday schedule)
+ source/library/Saturn/Unstable/Type/Weekday.hs view
@@ -0,0 +1,36 @@+module Saturn.Unstable.Type.Weekday where++import qualified Data.Coerce as Coerce+import qualified Data.Set as Set+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Field as Field+import qualified Text.Parsec as Parsec++newtype Weekday+  = Weekday Field.Field+  deriving (Eq, Show)++bounds :: (Word.Word8, Word.Word8)+bounds = (0, 6)++fromField :: Field.Field -> Maybe Weekday+fromField field =+  if Field.isValid bounds field then Just $ Weekday field else Nothing++toField :: Weekday -> Field.Field+toField = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Weekday+parsec = do+  field <- Field.parsec+  maybe (fail "invalid Weekday") pure $ fromField field++toBuilder :: Weekday -> Builder.Builder+toBuilder = Field.toBuilder . toField++expand :: Weekday -> Set.Set Word.Word8+expand = Field.expand bounds . toField++isMatch :: Word.Word8 -> Weekday -> Bool+isMatch word8 = Set.member word8 . expand
+ source/library/Saturn/Unstable/Type/Wildcard.hs view
@@ -0,0 +1,22 @@+module Saturn.Unstable.Type.Wildcard where++import qualified Control.Monad as Monad+import qualified Data.Coerce as Coerce+import qualified Data.Text.Lazy.Builder as Builder+import qualified Text.Parsec as Parsec++newtype Wildcard+  = Wildcard ()+  deriving (Eq, Show)++fromUnit :: () -> Wildcard+fromUnit = Coerce.coerce++toUnit :: Wildcard -> ()+toUnit = Coerce.coerce++parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Wildcard+parsec = fmap fromUnit . Monad.void $ Parsec.char '*'++toBuilder :: Wildcard -> Builder.Builder+toBuilder = const $ Builder.singleton '*'
+ source/test-suite/Main.hs view
@@ -0,0 +1,19 @@+import qualified Saturn.Unstable.Extra.IntSpec+import qualified Saturn.Unstable.Extra.OrdSpec+import qualified Saturn.Unstable.Extra.ParsecSpec+import qualified Saturn.Unstable.Extra.TimeSpec+import qualified Saturn.Unstable.Extra.TupleSpec+import qualified SaturnSpec+import qualified Test.Hspec as Hspec++main :: IO ()+main = Hspec.hspec $ Hspec.parallel spec++spec :: Hspec.Spec+spec = do+  Saturn.Unstable.Extra.IntSpec.spec+  Saturn.Unstable.Extra.OrdSpec.spec+  Saturn.Unstable.Extra.ParsecSpec.spec+  Saturn.Unstable.Extra.TimeSpec.spec+  Saturn.Unstable.Extra.TupleSpec.spec+  SaturnSpec.spec
+ source/test-suite/Saturn/Unstable/Extra/IntSpec.hs view
@@ -0,0 +1,23 @@+module Saturn.Unstable.Extra.IntSpec where++import qualified Saturn.Unstable.Extra.Int as Int+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Extra.Int" $ do+  Hspec.describe "fromWord8" $ do+    Hspec.it "works" $ do+      Int.fromWord8 0 `Hspec.shouldBe` 0++  Hspec.describe "toWord8" $ do+    Hspec.it "succeeds with minimum" $ do+      Int.toWord8 0 `Hspec.shouldBe` Just 0++    Hspec.it "succeeds with maximum" $ do+      Int.toWord8 255 `Hspec.shouldBe` Just 255++    Hspec.it "fails below minimum" $ do+      Int.toWord8 (-1) `Hspec.shouldBe` Nothing++    Hspec.it "fails above maximum" $ do+      Int.toWord8 256 `Hspec.shouldBe` Nothing
+ source/test-suite/Saturn/Unstable/Extra/OrdSpec.hs view
@@ -0,0 +1,22 @@+module Saturn.Unstable.Extra.OrdSpec where++import qualified Saturn.Unstable.Extra.Ord as Ord+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Extra.Ord" $ do+  Hspec.describe "within" $ do+    Hspec.it "fails below lower bound" $ do+      'a' `Hspec.shouldNotSatisfy` Ord.within ('b', 'd')++    Hspec.it "succeeds at lower bound" $ do+      'b' `Hspec.shouldSatisfy` Ord.within ('b', 'd')++    Hspec.it "succeeds within bounds" $ do+      'c' `Hspec.shouldSatisfy` Ord.within ('b', 'd')++    Hspec.it "succeeds at upper bound" $ do+      'd' `Hspec.shouldSatisfy` Ord.within ('b', 'd')++    Hspec.it "fails above upper bound" $ do+      'e' `Hspec.shouldNotSatisfy` Ord.within ('b', 'd')
+ source/test-suite/Saturn/Unstable/Extra/ParsecSpec.hs view
@@ -0,0 +1,35 @@+module Saturn.Unstable.Extra.ParsecSpec where++import qualified Data.Either as Either+import qualified Data.List.NonEmpty as NonEmpty+import qualified Saturn.Unstable.Extra.Parsec as Parsec+import qualified Test.Hspec as Hspec+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Extra.Parsec" $ do+  Hspec.describe "either" $ do+    let parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m (Either Char Char)+        parsec = Parsec.either (Parsec.char 'a') (Parsec.char 'b')++    Hspec.it "succeeds with left" $ do+      Parsec.parse parsec "" "a" `Hspec.shouldBe` Right (Left 'a')++    Hspec.it "succeeds with right" $ do+      Parsec.parse parsec "" "b" `Hspec.shouldBe` Right (Right 'b')++    Hspec.it "fails with neither" $ do+      Parsec.parse parsec "" "c" `Hspec.shouldSatisfy` Either.isLeft++  Hspec.describe "sepByNE" $ do+    let parsec :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m (NonEmpty.NonEmpty Char)+        parsec = Parsec.sepByNE (Parsec.char 'a') (Parsec.char ' ')++    Hspec.it "succeeds with one" $ do+      Parsec.parse parsec "" "a" `Hspec.shouldBe` Right ('a' NonEmpty.:| [])++    Hspec.it "succeeds with many" $ do+      Parsec.parse parsec "" "a a" `Hspec.shouldBe` Right ('a' NonEmpty.:| "a")++    Hspec.it "fails with none" $ do+      Parsec.parse parsec "" "" `Hspec.shouldSatisfy` Either.isLeft
+ source/test-suite/Saturn/Unstable/Extra/TimeSpec.hs view
@@ -0,0 +1,29 @@+module Saturn.Unstable.Extra.TimeSpec where++import qualified Data.Time as Time+import qualified Saturn.Unstable.Extra.Time as Time+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Extra.Time" $ do+  Hspec.describe "dayOfWeekToWord8" $ do+    Hspec.it "works with Sunday" $ do+      Time.dayOfWeekToWord8 Time.Sunday `Hspec.shouldBe` 0++    Hspec.it "works with Monday" $ do+      Time.dayOfWeekToWord8 Time.Monday `Hspec.shouldBe` 1++    Hspec.it "works with Tuesday" $ do+      Time.dayOfWeekToWord8 Time.Tuesday `Hspec.shouldBe` 2++    Hspec.it "works with Wednesday" $ do+      Time.dayOfWeekToWord8 Time.Wednesday `Hspec.shouldBe` 3++    Hspec.it "works with Thursday" $ do+      Time.dayOfWeekToWord8 Time.Thursday `Hspec.shouldBe` 4++    Hspec.it "works with Friday" $ do+      Time.dayOfWeekToWord8 Time.Friday `Hspec.shouldBe` 5++    Hspec.it "works with Saturday" $ do+      Time.dayOfWeekToWord8 Time.Saturday `Hspec.shouldBe` 6
+ source/test-suite/Saturn/Unstable/Extra/TupleSpec.hs view
@@ -0,0 +1,20 @@+module Saturn.Unstable.Extra.TupleSpec where++import qualified Saturn.Unstable.Extra.Tuple as Tuple+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Extra.Tuple" $ do+  Hspec.describe "mapBoth" $ do+    Hspec.it "works" $ do+      Tuple.mapBoth succ ('A', 'a') `Hspec.shouldBe` ('B', 'b')++  Hspec.describe "toSequence" $ do+    Hspec.it "works" $ do+      Tuple.toSequence ('b', 'd') `Hspec.shouldBe` "bcd"++    Hspec.it "works with singleton" $ do+      Tuple.toSequence ('a', 'a') `Hspec.shouldBe` "a"++    Hspec.it "works with empty" $ do+      Tuple.toSequence ('b', 'a') `Hspec.shouldBe` ""
+ source/test-suite/SaturnSpec.hs view
@@ -0,0 +1,929 @@+module SaturnSpec where++import qualified Data.Either as Either+import qualified Data.Fixed as Fixed+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Maybe as Maybe+import qualified Data.Time as Time+import qualified Data.Time.Calendar.WeekDate as Time+import qualified Data.Word as Word+import qualified Saturn+import qualified Saturn.Unstable.Extra.Tuple as Tuple+import qualified Saturn.Unstable.Type.Day as Day+import qualified Saturn.Unstable.Type.Element as Element+import qualified Saturn.Unstable.Type.Field as Field+import qualified Saturn.Unstable.Type.Hour as Hour+import qualified Saturn.Unstable.Type.Minute as Minute+import qualified Saturn.Unstable.Type.Month as Month+import qualified Saturn.Unstable.Type.Number as Number+import qualified Saturn.Unstable.Type.Range as Range+import qualified Saturn.Unstable.Type.Schedule as Schedule+import qualified Saturn.Unstable.Type.Weekday as Weekday+import qualified Saturn.Unstable.Type.Wildcard as Wildcard+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck++spec :: Hspec.Spec+spec = Hspec.describe "Saturn" $ do+  Hspec.describe "round trips" $ do+    Hspec.it "through string"+      . QuickCheck.forAllShrink arbitrarySchedule shrinkSchedule+      $ \schedule ->+        Saturn.fromString (Saturn.toString schedule) `Hspec.shouldBe` Right schedule++    Hspec.it "through strict text"+      . QuickCheck.forAllShrink arbitrarySchedule shrinkSchedule+      $ \schedule ->+        Saturn.fromText (Saturn.toText schedule) `Hspec.shouldBe` Right schedule++    Hspec.it "through lazy text"+      . QuickCheck.forAllShrink arbitrarySchedule shrinkSchedule+      $ \schedule ->+        Saturn.fromLazyText (Saturn.toLazyText schedule) `Hspec.shouldBe` Right schedule++  Hspec.describe "fromString" $ do+    Hspec.it "accepts wildcards" $ do+      schedule <- newSchedule [] [] [] [] []+      Saturn.fromString "* * * * *" `Hspec.shouldBe` Right schedule++    Hspec.it "accepts extra spaces" $ do+      schedule <- newSchedule [] [] [] [] []+      Saturn.fromString "  *  *  *  *  *  " `Hspec.shouldBe` Right schedule++    Hspec.it "accepts numbers" $ do+      schedule <- newSchedule [[4]] [[3]] [[2]] [[1]] [[0]]+      Saturn.fromString "4 3 2 1 0" `Hspec.shouldBe` Right schedule++    Hspec.it "accepts ranges" $ do+      schedule <- newSchedule [[8, 9]] [[6, 7]] [[4, 5]] [[2, 3]] [[0, 1]]+      Saturn.fromString "8-9 6-7 4-5 2-3 0-1" `Hspec.shouldBe` Right schedule++    Hspec.it "accepts choices" $ do+      schedule <- newSchedule [[8], [9]] [[6], [7]] [[4], [5]] [[2], [3]] [[0], [1]]+      Saturn.fromString "8,9 6,7 4,5 2,3 0,1" `Hspec.shouldBe` Right schedule++    Hspec.describe "minute" $ do+      Hspec.it "accepts a number" $ do+        schedule <- newSchedule [[0]] [] [] [] []+        Saturn.fromString "0 * * * *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a range" $ do+        schedule <- newSchedule [[0, 1]] [] [] [] []+        Saturn.fromString "0-1 * * * *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a choice" $ do+        schedule <- newSchedule [[0], [1]] [] [] [] []+        Saturn.fromString "0,1 * * * *" `Hspec.shouldBe` Right schedule++      Hspec.it "rejects two wildcards" $ do+        Saturn.fromString "*,* * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a number" $ do+        Saturn.fromString "*,0 * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a range" $ do+        Saturn.fromString "*,0-0 * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number" $ do+        Saturn.fromString "60 * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number as part of a choice" $ do+        Saturn.fromString "0,60 * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds range" $ do+        Saturn.fromString "60-61 * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a half out of bounds range" $ do+        Saturn.fromString "0-60 * * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a backwards range" $ do+        Saturn.fromString "1-0 * * * *" `Hspec.shouldSatisfy` Either.isLeft++    Hspec.describe "hour" $ do+      Hspec.it "accepts a number" $ do+        schedule <- newSchedule [] [[0]] [] [] []+        Saturn.fromString "* 0 * * *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a range" $ do+        schedule <- newSchedule [] [[0, 1]] [] [] []+        Saturn.fromString "* 0-1 * * *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a choice" $ do+        schedule <- newSchedule [] [[0], [1]] [] [] []+        Saturn.fromString "* 0,1 * * *" `Hspec.shouldBe` Right schedule++      Hspec.it "rejects two wildcards" $ do+        Saturn.fromString "* *,* * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a number" $ do+        Saturn.fromString "* *,0 * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a range" $ do+        Saturn.fromString "* *,0-0 * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number" $ do+        Saturn.fromString "* 24 * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number as part of a choice" $ do+        Saturn.fromString "* 0,24 * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds range" $ do+        Saturn.fromString "* 24-25 * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a half out of bounds range" $ do+        Saturn.fromString "* 0-24 * * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a backwards range" $ do+        Saturn.fromString "* 1-0 * * *" `Hspec.shouldSatisfy` Either.isLeft++    Hspec.describe "day" $ do+      Hspec.it "accepts a number" $ do+        schedule <- newSchedule [] [] [[1]] [] []+        Saturn.fromString "* * 1 * *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a range" $ do+        schedule <- newSchedule [] [] [[1, 2]] [] []+        Saturn.fromString "* * 1-2 * *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a choice" $ do+        schedule <- newSchedule [] [] [[1], [2]] [] []+        Saturn.fromString "* * 1,2 * *" `Hspec.shouldBe` Right schedule++      Hspec.it "rejects two wildcards" $ do+        Saturn.fromString "* * *,* * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a number" $ do+        Saturn.fromString "* * *,1 * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a range" $ do+        Saturn.fromString "* * *,1-1 * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number" $ do+        Saturn.fromString "* * 32 * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number as part of a choice" $ do+        Saturn.fromString "* * 1,32 * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds range" $ do+        Saturn.fromString "* * 32-33 * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a half out of bounds range" $ do+        Saturn.fromString "* * 1-32 * *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a backwards range" $ do+        Saturn.fromString "* * 2-1 * *" `Hspec.shouldSatisfy` Either.isLeft++    Hspec.describe "month" $ do+      Hspec.it "accepts a number" $ do+        schedule <- newSchedule [] [] [] [[1]] []+        Saturn.fromString "* * * 1 *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a range" $ do+        schedule <- newSchedule [] [] [] [[1, 2]] []+        Saturn.fromString "* * * 1-2 *" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a choice" $ do+        schedule <- newSchedule [] [] [] [[1], [2]] []+        Saturn.fromString "* * * 1,2 *" `Hspec.shouldBe` Right schedule++      Hspec.it "rejects two wildcards" $ do+        Saturn.fromString "* * * *,* *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a number" $ do+        Saturn.fromString "* * * *,1 *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a range" $ do+        Saturn.fromString "* * * *,1-1 *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number" $ do+        Saturn.fromString "* * * 13 *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number as part of a choice" $ do+        Saturn.fromString "* * * 1,13 *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds range" $ do+        Saturn.fromString "* * * 13-14 *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a half out of bounds range" $ do+        Saturn.fromString "* * * 1-13 *" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a backwards range" $ do+        Saturn.fromString "* * * 2-1 *" `Hspec.shouldSatisfy` Either.isLeft++    Hspec.describe "weekday" $ do+      Hspec.it "accepts a number" $ do+        schedule <- newSchedule [] [] [] [] [[0]]+        Saturn.fromString "* * * * 0" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a range" $ do+        schedule <- newSchedule [] [] [] [] [[0, 1]]+        Saturn.fromString "* * * * 0-1" `Hspec.shouldBe` Right schedule++      Hspec.it "accepts a choice" $ do+        schedule <- newSchedule [] [] [] [] [[0], [1]]+        Saturn.fromString "* * * * 0,1" `Hspec.shouldBe` Right schedule++      Hspec.it "rejects two wildcards" $ do+        Saturn.fromString "* * * * *,*" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a number" $ do+        Saturn.fromString "* * * * *,0" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a wildcard and a range" $ do+        Saturn.fromString "* * * * *,0-0" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number" $ do+        Saturn.fromString "* * * * 7" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds number as part of a choice" $ do+        Saturn.fromString "* * * * 0,7" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects an out of bounds range" $ do+        Saturn.fromString "* * * * 7-8" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a half out of bounds range" $ do+        Saturn.fromString "* * * * 0-7" `Hspec.shouldSatisfy` Either.isLeft++      Hspec.it "rejects a backwards range" $ do+        Saturn.fromString "* * * * 1-0" `Hspec.shouldSatisfy` Either.isLeft++  Hspec.describe "toString" $ do+    Hspec.it "works with wildcards" $ do+      schedule <- newSchedule [] [] [] [] []+      Saturn.toString schedule `Hspec.shouldBe` "* * * * *"++    Hspec.it "works with numbers" $ do+      schedule <- newSchedule [[4]] [[3]] [[2]] [[1]] [[0]]+      Saturn.toString schedule `Hspec.shouldBe` "4 3 2 1 0"++    Hspec.it "works with ranges" $ do+      schedule <- newSchedule [[8, 9]] [[6, 7]] [[4, 5]] [[2, 3]] [[0, 1]]+      Saturn.toString schedule `Hspec.shouldBe` "8-9 6-7 4-5 2-3 0-1"++    Hspec.it "works with choices" $ do+      schedule <- newSchedule [[8], [9]] [[6], [7]] [[4], [5]] [[2], [3]] [[0], [1]]+      Saturn.toString schedule `Hspec.shouldBe` "8,9 6,7 4,5 2,3 0,1"++  Hspec.describe "isMatch" $ do+    Hspec.it "is always true with all wildcards"+      . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+      $ \utcTime -> do+        schedule <- newSchedule [] [] [] [] []+        schedule `Hspec.shouldSatisfy` Saturn.isMatch utcTime++    Hspec.it "is true when day or weekday matches" $ do+      s <- newSchedule [] [] [[5]] [] [[5]]+      t1 <- newUtcTime 1970 1 5 0 0 0+      s `Hspec.shouldSatisfy` Saturn.isMatch t1+      t2 <- newUtcTime 1970 1 2 0 0 0+      s `Hspec.shouldSatisfy` Saturn.isMatch t2++    Hspec.describe "minute" $ do+      Hspec.it "is always true when a number matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [[5]] [] [] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withMinute 5 utcTime)++      Hspec.it "is always true when a range matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [[4, 5]] [] [] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withMinute 5 utcTime)++      Hspec.it "is always true when a choice matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [[4], [5]] [] [] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withMinute 5 utcTime)++      Hspec.it "is true when a number matches" $ do+        t <- newUtcTime 1970 1 1 0 5 0+        s <- newSchedule [[5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a number does not match" $ do+        t <- newUtcTime 1970 1 1 0 6 0+        s <- newSchedule [[5]] [] [] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a range matches" $ do+        t <- newUtcTime 1970 1 1 0 5 0+        s <- newSchedule [[4, 5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a range does not match" $ do+        t <- newUtcTime 1970 1 1 0 6 0+        s <- newSchedule [[4, 5]] [] [] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a choice matches" $ do+        t <- newUtcTime 1970 1 1 0 5 0+        s <- newSchedule [[4], [5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a choice does not match" $ do+        t <- newUtcTime 1970 1 1 0 6 0+        s <- newSchedule [[4], [5]] [] [] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "accepts any second" $ do+        t <- newUtcTime 1970 1 1 0 5 6+        s <- newSchedule [[5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any hour" $ do+        t <- newUtcTime 1970 1 1 6 5 0+        s <- newSchedule [[5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any day" $ do+        t <- newUtcTime 1970 1 6 0 5 0+        s <- newSchedule [[5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any month" $ do+        t <- newUtcTime 1970 6 1 0 5 0+        s <- newSchedule [[5]] [] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++    Hspec.describe "hour" $ do+      Hspec.it "is always true when a number matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [[5]] [] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withHour 5 utcTime)++      Hspec.it "is always true when a range matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [[4, 5]] [] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withHour 5 utcTime)++      Hspec.it "is always true when a choice matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [[4], [5]] [] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withHour 5 utcTime)++      Hspec.it "is true when a number matches" $ do+        t <- newUtcTime 1970 1 1 5 0 0+        s <- newSchedule [] [[5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a number does not match" $ do+        t <- newUtcTime 1970 1 1 6 0 0+        s <- newSchedule [] [[5]] [] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a range matches" $ do+        t <- newUtcTime 1970 1 1 5 0 0+        s <- newSchedule [] [[4, 5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a range does not match" $ do+        t <- newUtcTime 1970 1 1 6 0 0+        s <- newSchedule [] [[4, 5]] [] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a choice matches" $ do+        t <- newUtcTime 1970 1 1 5 0 0+        s <- newSchedule [] [[4], [5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a choice does not match" $ do+        t <- newUtcTime 1970 1 1 6 0 0+        s <- newSchedule [] [[4], [5]] [] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "accepts any second" $ do+        t <- newUtcTime 1970 1 1 5 0 6+        s <- newSchedule [] [[5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any minute" $ do+        t <- newUtcTime 1970 1 1 5 6 0+        s <- newSchedule [] [[5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any day" $ do+        t <- newUtcTime 1970 1 6 5 0 0+        s <- newSchedule [] [[5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any month" $ do+        t <- newUtcTime 1970 6 1 5 0 0+        s <- newSchedule [] [[5]] [] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++    Hspec.describe "day" $ do+      Hspec.it "is always true when a number matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [[5]] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withDayOfMonth 5 utcTime)++      Hspec.it "is always true when a range matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [[4, 5]] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withDayOfMonth 5 utcTime)++      Hspec.it "is always true when a choice matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [[4], [5]] [] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withDayOfMonth 5 utcTime)++      Hspec.it "is true when a number matches" $ do+        t <- newUtcTime 1970 1 5 0 0 0+        s <- newSchedule [] [] [[5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a number does not match" $ do+        t <- newUtcTime 1970 1 6 0 0 0+        s <- newSchedule [] [] [[5]] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a range matches" $ do+        t <- newUtcTime 1970 1 5 0 0 0+        s <- newSchedule [] [] [[4, 5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a range does not match" $ do+        t <- newUtcTime 1970 1 6 0 0 0+        s <- newSchedule [] [] [[4, 5]] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a choice matches" $ do+        t <- newUtcTime 1970 1 5 0 0 0+        s <- newSchedule [] [] [[4], [5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a choice does not match" $ do+        t <- newUtcTime 1970 1 6 0 0 0+        s <- newSchedule [] [] [[4], [5]] [] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "accepts any second" $ do+        t <- newUtcTime 1970 1 5 0 0 6+        s <- newSchedule [] [] [[5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any minute" $ do+        t <- newUtcTime 1970 1 5 0 6 0+        s <- newSchedule [] [] [[5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any hour" $ do+        t <- newUtcTime 1970 1 5 6 0 0+        s <- newSchedule [] [] [[5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any month" $ do+        t <- newUtcTime 1970 6 5 0 0 0+        s <- newSchedule [] [] [[5]] [] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++    Hspec.describe "month" $ do+      Hspec.it "is always true when a number matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [] [[5]] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withMonthOfYear 5 utcTime)++      Hspec.it "is always true when a range matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [] [[4, 5]] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withMonthOfYear 5 utcTime)++      Hspec.it "is always true when a choice matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [] [[4], [5]] []+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withMonthOfYear 5 utcTime)++      Hspec.it "is true when a number matches" $ do+        t <- newUtcTime 1970 5 1 0 0 0+        s <- newSchedule [] [] [] [[5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a number does not match" $ do+        t <- newUtcTime 1970 6 1 0 0 0+        s <- newSchedule [] [] [] [[5]] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a range matches" $ do+        t <- newUtcTime 1970 5 1 0 0 0+        s <- newSchedule [] [] [] [[4, 5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a range does not match" $ do+        t <- newUtcTime 1970 6 1 0 0 0+        s <- newSchedule [] [] [] [[4, 5]] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a choice matches" $ do+        t <- newUtcTime 1970 5 1 0 0 0+        s <- newSchedule [] [] [] [[4], [5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a choice does not match" $ do+        t <- newUtcTime 1970 6 1 0 0 0+        s <- newSchedule [] [] [] [[4], [5]] []+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "accepts any second" $ do+        t <- newUtcTime 1970 5 1 0 0 6+        s <- newSchedule [] [] [] [[5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any minute" $ do+        t <- newUtcTime 1970 5 1 0 6 0+        s <- newSchedule [] [] [] [[5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any hour" $ do+        t <- newUtcTime 1970 5 1 6 0 0+        s <- newSchedule [] [] [] [[5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any day" $ do+        t <- newUtcTime 1970 5 6 0 0 0+        s <- newSchedule [] [] [] [[5]] []+        s `Hspec.shouldSatisfy` Saturn.isMatch t++    Hspec.describe "weekday" $ do+      Hspec.it "is always true when a number matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [] [] [[5]]+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withDayOfWeek Time.Friday utcTime)++      Hspec.it "is always true when a range matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [] [] [[4, 5]]+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withDayOfWeek Time.Friday utcTime)++      Hspec.it "is always true when a choice matches"+        . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+        $ \utcTime -> do+          schedule <- newSchedule [] [] [] [] [[4], [5]]+          schedule `Hspec.shouldSatisfy` Saturn.isMatch (withDayOfWeek Time.Friday utcTime)++      Hspec.it "is true when a number matches" $ do+        t <- newUtcTime 1970 1 2 0 0 0+        s <- newSchedule [] [] [] [] [[5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a number does not match" $ do+        t <- newUtcTime 1970 1 3 0 0 0+        s <- newSchedule [] [] [] [] [[5]]+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a range matches" $ do+        t <- newUtcTime 1970 1 2 0 0 0+        s <- newSchedule [] [] [] [] [[4, 5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a range does not match" $ do+        t <- newUtcTime 1970 1 3 0 0 0+        s <- newSchedule [] [] [] [] [[4, 5]]+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "is true when a choice matches" $ do+        t <- newUtcTime 1970 1 2 0 0 0+        s <- newSchedule [] [] [] [] [[4], [5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "is false when a choice does not match" $ do+        t <- newUtcTime 1970 1 3 0 0 0+        s <- newSchedule [] [] [] [] [[4], [5]]+        s `Hspec.shouldNotSatisfy` Saturn.isMatch t++      Hspec.it "accepts any second" $ do+        t <- newUtcTime 1970 1 2 0 0 6+        s <- newSchedule [] [] [] [] [[5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any minute" $ do+        t <- newUtcTime 1970 1 2 0 6 0+        s <- newSchedule [] [] [] [] [[5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any hour" $ do+        t <- newUtcTime 1970 1 2 6 0 0+        s <- newSchedule [] [] [] [] [[5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++      Hspec.it "accepts any day" $ do+        t <- newUtcTime 1970 1 9 0 0 0+        s <- newSchedule [] [] [] [] [[5]]+        s `Hspec.shouldSatisfy` Saturn.isMatch t++  Hspec.describe "nextMatch" $ do+    Hspec.it "succeeds with a leap day" $ do+      s <- newSchedule [[0]] [[0]] [[29]] [[2]] []+      t1 <- newUtcTime 1970 1 1 0 0 0+      t2 <- newUtcTime 1972 2 29 0 0 0+      Saturn.nextMatch t1 s `Hspec.shouldBe` Just t2++    Hspec.it "succeeds with the next leap day" $ do+      s <- newSchedule [[0]] [[0]] [[29]] [[2]] []+      t1 <- newUtcTime 1972 2 29 0 0 0+      t2 <- newUtcTime 1976 2 29 0 0 0+      Saturn.nextMatch t1 s `Hspec.shouldBe` Just t2++    Hspec.it "succeeds with the furthest leap day" $ do+      s <- newSchedule [[0]] [[0]] [[29]] [[2]] []+      t1 <- newUtcTime 1896 2 29 0 0 0+      t2 <- newUtcTime 1904 2 29 0 0 0+      Saturn.nextMatch t1 s `Hspec.shouldBe` Just t2++    Hspec.it "fails with an impossible date" $ do+      s <- newSchedule [[0]] [[0]] [[30]] [[2]] []+      t <- newUtcTime 1970 1 1 0 0 0+      Saturn.nextMatch t s `Hspec.shouldBe` Nothing++    Hspec.it "is always in the future"+      . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+      $ \t1 -> do+        schedule <- newSchedule [] [] [] [] []+        t2 <- maybe (fail "impossible") pure $ Saturn.nextMatch t1 schedule+        t2 `Hspec.shouldSatisfy` (>= t1)++    Hspec.it "always matches"+      . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+      $ \t1 -> do+        schedule <- newSchedule [] [] [] [] []+        t2 <- maybe (fail "impossible") pure $ Saturn.nextMatch t1 schedule+        schedule `Hspec.shouldSatisfy` Saturn.isMatch t2++withMinute :: Int -> Time.UTCTime -> Time.UTCTime+withMinute minute = overTimeOfDay $ \timeOfDay -> timeOfDay {Time.todMin = minute}++withHour :: Int -> Time.UTCTime -> Time.UTCTime+withHour hour = overTimeOfDay $ \timeOfDay -> timeOfDay {Time.todHour = hour}++overTimeOfDay :: (Time.TimeOfDay -> Time.TimeOfDay) -> Time.UTCTime -> Time.UTCTime+overTimeOfDay f utcTime =+  utcTime+    { Time.utctDayTime =+        Time.sinceMidnight . f . Time.pastMidnight $ Time.utctDayTime utcTime+    }++withDayOfMonth :: Time.DayOfMonth -> Time.UTCTime -> Time.UTCTime+withDayOfMonth dayOfMonth = overDay $ \day ->+  let (year, monthOfYear, _) = Time.toGregorian day+   in Time.fromGregorian year monthOfYear dayOfMonth++withMonthOfYear :: Time.MonthOfYear -> Time.UTCTime -> Time.UTCTime+withMonthOfYear monthOfYear = overDay $ \day ->+  let (year, _, dayOfMonth) = Time.toGregorian day+   in Time.fromGregorian year monthOfYear dayOfMonth++withDayOfWeek :: Time.DayOfWeek -> Time.UTCTime -> Time.UTCTime+withDayOfWeek dayOfWeek = overDay $ \day ->+  let fwt = Time.FirstWholeWeek+      dow = Time.Sunday+      (year, weekOfYear, _) = Time.toWeekCalendar fwt dow day+   in Time.fromWeekCalendar fwt dow year weekOfYear dayOfWeek++overDay :: (Time.Day -> Time.Day) -> Time.UTCTime -> Time.UTCTime+overDay f utcTime = utcTime {Time.utctDay = f $ Time.utctDay utcTime}++newUtcTime ::+  (MonadFail m) =>+  Time.Year ->+  Time.MonthOfYear ->+  Time.DayOfMonth ->+  Int ->+  Int ->+  Fixed.Pico ->+  m Time.UTCTime+newUtcTime year monthOfYear dayOfMonth hour minute second = do+  day <-+    maybe (fail "invalid Day") pure $+      Time.fromGregorianValid year monthOfYear dayOfMonth+  timeOfDay <-+    maybe (fail "invalid TimeOfDay") pure $+      Time.makeTimeOfDayValid hour minute second+  pure+    Time.UTCTime+      { Time.utctDay = day,+        Time.utctDayTime = Time.sinceMidnight timeOfDay+      }++arbitraryUtcTime :: QuickCheck.Gen Time.UTCTime+arbitraryUtcTime =+  Time.UTCTime+    <$> fmap Time.ModifiedJulianDay QuickCheck.arbitrary+    <*> fmap Time.picosecondsToDiffTime (QuickCheck.chooseInteger (0, 86400000000000000 - 1))++shrinkUtcTime :: Time.UTCTime -> [Time.UTCTime]+shrinkUtcTime =+  QuickCheck.shrinkMap+    ( \(d, t) ->+        Time.UTCTime+          { Time.utctDay = Time.ModifiedJulianDay d,+            Time.utctDayTime = Time.picosecondsToDiffTime t+          }+    )+    ( \x ->+        ( Time.toModifiedJulianDay $ Time.utctDay x,+          Time.diffTimeToPicoseconds $ Time.utctDayTime x+        )+    )++newSchedule ::+  (MonadFail m) =>+  [[Word.Word8]] ->+  [[Word.Word8]] ->+  [[Word.Word8]] ->+  [[Word.Word8]] ->+  [[Word.Word8]] ->+  m Schedule.Schedule+newSchedule minutes hours days months weekdays =+  Schedule.Schedule+    <$> newFieldWith "Minute" Minute.fromField minutes+    <*> newFieldWith "Hour" Hour.fromField hours+    <*> newFieldWith "Day" Day.fromField days+    <*> newFieldWith "Month" Month.fromField months+    <*> newFieldWith "Weekday" Weekday.fromField weekdays++newFieldWith ::+  (MonadFail m) =>+  String ->+  (Field.Field -> Maybe a) ->+  [[Word.Word8]] ->+  m a+newFieldWith name fromField xs = do+  field <- newField xs+  maybe (fail $ "invalid " <> name <> ": " <> show xs) pure $ fromField field++newField :: (MonadFail m) => [[Word.Word8]] -> m Field.Field+newField =+  fmap+    ( Field.fromEither+        . maybe (Left $ Wildcard.fromUnit ()) Right+        . NonEmpty.nonEmpty+    )+    . mapM newElement++newElement :: (MonadFail m) => [Word.Word8] -> m Element.Element+newElement xs = case xs of+  [x] -> pure . Element.fromEither . Right $ Number.fromWord8 x+  [x, y] -> Element.fromEither . Left <$> newRange (x, y)+  _ -> fail $ "invalid Element: " <> show xs++newRange :: (MonadFail m) => (Word.Word8, Word.Word8) -> m Range.Range+newRange tuple =+  maybe (fail $ "invalid Range: " <> show tuple) pure+    . Range.fromTuple+    $ Tuple.mapBoth Number.fromWord8 tuple++arbitrarySchedule :: QuickCheck.Gen Schedule.Schedule+arbitrarySchedule =+  Schedule.Schedule+    <$> arbitraryMinute+    <*> arbitraryHour+    <*> arbitraryDay+    <*> arbitraryMonth+    <*> arbitraryWeekday++arbitraryMinute :: QuickCheck.Gen Minute.Minute+arbitraryMinute = QuickCheck.suchThatMap arbitraryField Minute.fromField++arbitraryHour :: QuickCheck.Gen Hour.Hour+arbitraryHour = QuickCheck.suchThatMap arbitraryField Hour.fromField++arbitraryDay :: QuickCheck.Gen Day.Day+arbitraryDay = QuickCheck.suchThatMap arbitraryField Day.fromField++arbitraryMonth :: QuickCheck.Gen Month.Month+arbitraryMonth = QuickCheck.suchThatMap arbitraryField Month.fromField++arbitraryWeekday :: QuickCheck.Gen Weekday.Weekday+arbitraryWeekday = QuickCheck.suchThatMap arbitraryField Weekday.fromField++arbitraryField :: QuickCheck.Gen Field.Field+arbitraryField =+  Field.fromEither+    <$> QuickCheck.liftArbitrary2+      arbitraryWildcard+      (arbitraryNonEmpty arbitraryElement)++arbitraryWildcard :: QuickCheck.Gen Wildcard.Wildcard+arbitraryWildcard = pure $ Wildcard.fromUnit ()++arbitraryNonEmpty :: QuickCheck.Gen a -> QuickCheck.Gen (NonEmpty.NonEmpty a)+arbitraryNonEmpty g = (NonEmpty.:|) <$> g <*> QuickCheck.listOf g++arbitraryElement :: QuickCheck.Gen Element.Element+arbitraryElement =+  Element.fromEither+    <$> QuickCheck.liftArbitrary2+      arbitraryRange+      arbitraryNumber++arbitraryRange :: QuickCheck.Gen Range.Range+arbitraryRange =+  QuickCheck.suchThatMap+    (QuickCheck.liftArbitrary2 arbitraryNumber arbitraryNumber)+    Range.fromTuple++arbitraryNumber :: QuickCheck.Gen Number.Number+arbitraryNumber = Number.fromWord8 <$> QuickCheck.arbitrary++shrinkSchedule :: Schedule.Schedule -> [Schedule.Schedule]+shrinkSchedule schedule =+  ( \(minute, hour, day, month, weekday) ->+      Schedule.Schedule+        { Schedule.minute = minute,+          Schedule.hour = hour,+          Schedule.day = day,+          Schedule.month = month,+          Schedule.weekday = weekday+        }+  )+    <$> liftShrink5+      shrinkMinute+      shrinkHour+      shrinkDay+      shrinkMonth+      shrinkWeekday+      ( Schedule.minute schedule,+        Schedule.hour schedule,+        Schedule.day schedule,+        Schedule.month schedule,+        Schedule.weekday schedule+      )++liftShrink5 ::+  (t1 -> [t1]) ->+  (t2 -> [t2]) ->+  (t3 -> [t3]) ->+  (t4 -> [t4]) ->+  (t5 -> [t5]) ->+  (t1, t2, t3, t4, t5) ->+  [(t1, t2, t3, t4, t5)]+liftShrink5 f1 f2 f3 f4 f5 (x1, x2, x3, x4, x5) =+  (\((y1, y2), (y3, (y4, y5))) -> (y1, y2, y3, y4, y5))+    <$> QuickCheck.liftShrink2+      (QuickCheck.liftShrink2 f1 f2)+      (QuickCheck.liftShrink2 f3 (QuickCheck.liftShrink2 f4 f5))+      ((x1, x2), (x3, (x4, x5)))++shrinkMinute :: Minute.Minute -> [Minute.Minute]+shrinkMinute = Maybe.mapMaybe Minute.fromField . shrinkField . Minute.toField++shrinkHour :: Hour.Hour -> [Hour.Hour]+shrinkHour = Maybe.mapMaybe Hour.fromField . shrinkField . Hour.toField++shrinkDay :: Day.Day -> [Day.Day]+shrinkDay = Maybe.mapMaybe Day.fromField . shrinkField . Day.toField++shrinkMonth :: Month.Month -> [Month.Month]+shrinkMonth = Maybe.mapMaybe Month.fromField . shrinkField . Month.toField++shrinkWeekday :: Weekday.Weekday -> [Weekday.Weekday]+shrinkWeekday = Maybe.mapMaybe Weekday.fromField . shrinkField . Weekday.toField++shrinkField :: Field.Field -> [Field.Field]+shrinkField field =+  let xs = case Field.toEither field of+        Left _ -> []+        Right _ -> [Field.fromEither . Left $ Wildcard.fromUnit ()]+   in mappend xs+        . fmap Field.fromEither+        . QuickCheck.liftShrink2 shrinkWildcard (shrinkNonEmpty shrinkElement)+        $ Field.toEither field++shrinkWildcard :: Wildcard.Wildcard -> [Wildcard.Wildcard]+shrinkWildcard = fmap Wildcard.fromUnit . QuickCheck.shrink . Wildcard.toUnit++shrinkNonEmpty :: (a -> [a]) -> NonEmpty.NonEmpty a -> [NonEmpty.NonEmpty a]+shrinkNonEmpty f = Maybe.mapMaybe (NonEmpty.nonEmpty . f) . NonEmpty.toList++shrinkElement :: Element.Element -> [Element.Element]+shrinkElement element =+  let xs = case Element.toEither element of+        Left range ->+          let (lo, hi) = Range.toTuple range+           in fmap (Element.fromEither . Right) [lo, hi]+        Right _ -> []+   in mappend xs+        . fmap Element.fromEither+        . QuickCheck.liftShrink2 shrinkRange shrinkNumber+        $ Element.toEither element++shrinkRange :: Range.Range -> [Range.Range]+shrinkRange =+  Maybe.mapMaybe Range.fromTuple+    . QuickCheck.liftShrink2 shrinkNumber shrinkNumber+    . Range.toTuple++shrinkNumber :: Number.Number -> [Number.Number]+shrinkNumber = fmap Number.fromWord8 . QuickCheck.shrink . Number.toWord8