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 +4/−0
- LICENSE.markdown +21/−0
- README.markdown +9/−0
- saturn.cabal +93/−0
- source/library/Saturn.hs +64/−0
- source/library/Saturn/Unstable/Extra/Int.hs +10/−0
- source/library/Saturn/Unstable/Extra/Ord.hs +4/−0
- source/library/Saturn/Unstable/Extra/Parsec.hs +16/−0
- source/library/Saturn/Unstable/Extra/Time.hs +14/−0
- source/library/Saturn/Unstable/Extra/Tuple.hs +9/−0
- source/library/Saturn/Unstable/Match.hs +89/−0
- source/library/Saturn/Unstable/Parse.hs +28/−0
- source/library/Saturn/Unstable/Render.hs +21/−0
- source/library/Saturn/Unstable/Type/Day.hs +36/−0
- source/library/Saturn/Unstable/Type/Element.hs +32/−0
- source/library/Saturn/Unstable/Type/Field.hs +56/−0
- source/library/Saturn/Unstable/Type/Hour.hs +36/−0
- source/library/Saturn/Unstable/Type/Minute.hs +36/−0
- source/library/Saturn/Unstable/Type/Month.hs +36/−0
- source/library/Saturn/Unstable/Type/Number.hs +33/−0
- source/library/Saturn/Unstable/Type/Range.hs +45/−0
- source/library/Saturn/Unstable/Type/Schedule.hs +43/−0
- source/library/Saturn/Unstable/Type/Weekday.hs +36/−0
- source/library/Saturn/Unstable/Type/Wildcard.hs +22/−0
- source/test-suite/Main.hs +19/−0
- source/test-suite/Saturn/Unstable/Extra/IntSpec.hs +23/−0
- source/test-suite/Saturn/Unstable/Extra/OrdSpec.hs +22/−0
- source/test-suite/Saturn/Unstable/Extra/ParsecSpec.hs +35/−0
- source/test-suite/Saturn/Unstable/Extra/TimeSpec.hs +29/−0
- source/test-suite/Saturn/Unstable/Extra/TupleSpec.hs +20/−0
- source/test-suite/SaturnSpec.hs +929/−0
+ 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++[](https://github.com/tfausak/saturn/actions/workflows/workflow.yaml)+[](https://hackage.haskell.org/package/saturn)+[](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