saturn 0.1.0.1 → 0.2.0.0
raw patch · 68 files changed
+2050/−1746 lines, 68 filesdep ~basedep ~parsecdep ~text
Dependency ranges changed: base, parsec, text, time
Files
- saturn.cabal +46/−22
- source/libraries/saturn/Saturn.hs +64/−0
- source/libraries/spec/Saturn/Unstable/Extra/IntSpec.hs +23/−0
- source/libraries/spec/Saturn/Unstable/Extra/OrdSpec.hs +22/−0
- source/libraries/spec/Saturn/Unstable/Extra/ParsecSpec.hs +37/−0
- source/libraries/spec/Saturn/Unstable/Extra/TimeSpec.hs +29/−0
- source/libraries/spec/Saturn/Unstable/Extra/TupleSpec.hs +20/−0
- source/libraries/spec/Saturn/Unstable/MatchSpec.hs +484/−0
- source/libraries/spec/Saturn/Unstable/ParseSpec.hs +214/−0
- source/libraries/spec/Saturn/Unstable/RenderSpec.hs +24/−0
- source/libraries/spec/Saturn/Unstable/Type/DaySpec.hs +29/−0
- source/libraries/spec/Saturn/Unstable/Type/ElementSpec.hs +45/−0
- source/libraries/spec/Saturn/Unstable/Type/FieldSpec.hs +53/−0
- source/libraries/spec/Saturn/Unstable/Type/HourSpec.hs +29/−0
- source/libraries/spec/Saturn/Unstable/Type/MinuteSpec.hs +29/−0
- source/libraries/spec/Saturn/Unstable/Type/MonthSpec.hs +29/−0
- source/libraries/spec/Saturn/Unstable/Type/NumberSpec.hs +21/−0
- source/libraries/spec/Saturn/Unstable/Type/RangeSpec.hs +38/−0
- source/libraries/spec/Saturn/Unstable/Type/ScheduleSpec.hs +85/−0
- source/libraries/spec/Saturn/Unstable/Type/WeekdaySpec.hs +29/−0
- source/libraries/spec/Saturn/Unstable/Type/WildcardSpec.hs +21/−0
- source/libraries/spec/SaturnSpec.hs +25/−0
- source/libraries/unstable/Saturn/Unstable/Extra/Int.hs +10/−0
- source/libraries/unstable/Saturn/Unstable/Extra/Ord.hs +4/−0
- source/libraries/unstable/Saturn/Unstable/Extra/Parsec.hs +16/−0
- source/libraries/unstable/Saturn/Unstable/Extra/Time.hs +14/−0
- source/libraries/unstable/Saturn/Unstable/Extra/Tuple.hs +9/−0
- source/libraries/unstable/Saturn/Unstable/Match.hs +89/−0
- source/libraries/unstable/Saturn/Unstable/Parse.hs +30/−0
- source/libraries/unstable/Saturn/Unstable/Render.hs +21/−0
- source/libraries/unstable/Saturn/Unstable/Type/Day.hs +38/−0
- source/libraries/unstable/Saturn/Unstable/Type/Element.hs +34/−0
- source/libraries/unstable/Saturn/Unstable/Type/Field.hs +58/−0
- source/libraries/unstable/Saturn/Unstable/Type/Hour.hs +38/−0
- source/libraries/unstable/Saturn/Unstable/Type/Minute.hs +38/−0
- source/libraries/unstable/Saturn/Unstable/Type/Month.hs +38/−0
- source/libraries/unstable/Saturn/Unstable/Type/Number.hs +35/−0
- source/libraries/unstable/Saturn/Unstable/Type/Range.hs +47/−0
- source/libraries/unstable/Saturn/Unstable/Type/Schedule.hs +45/−0
- source/libraries/unstable/Saturn/Unstable/Type/Weekday.hs +38/−0
- source/libraries/unstable/Saturn/Unstable/Type/Wildcard.hs +24/−0
- source/library/Saturn.hs +0/−64
- source/library/Saturn/Unstable/Extra/Int.hs +0/−10
- source/library/Saturn/Unstable/Extra/Ord.hs +0/−4
- source/library/Saturn/Unstable/Extra/Parsec.hs +0/−16
- source/library/Saturn/Unstable/Extra/Time.hs +0/−14
- source/library/Saturn/Unstable/Extra/Tuple.hs +0/−9
- source/library/Saturn/Unstable/Match.hs +0/−89
- source/library/Saturn/Unstable/Parse.hs +0/−28
- source/library/Saturn/Unstable/Render.hs +0/−21
- source/library/Saturn/Unstable/Type/Day.hs +0/−36
- source/library/Saturn/Unstable/Type/Element.hs +0/−32
- source/library/Saturn/Unstable/Type/Field.hs +0/−56
- source/library/Saturn/Unstable/Type/Hour.hs +0/−36
- source/library/Saturn/Unstable/Type/Minute.hs +0/−36
- source/library/Saturn/Unstable/Type/Month.hs +0/−36
- source/library/Saturn/Unstable/Type/Number.hs +0/−33
- source/library/Saturn/Unstable/Type/Range.hs +0/−45
- source/library/Saturn/Unstable/Type/Schedule.hs +0/−43
- source/library/Saturn/Unstable/Type/Weekday.hs +0/−36
- source/library/Saturn/Unstable/Type/Wildcard.hs +0/−22
- source/test-suite/Main.hs +28/−0
- source/test-suite/Saturn/Unstable/Extra/IntSpec.hs +0/−23
- source/test-suite/Saturn/Unstable/Extra/OrdSpec.hs +0/−22
- source/test-suite/Saturn/Unstable/Extra/ParsecSpec.hs +0/−35
- source/test-suite/Saturn/Unstable/Extra/TimeSpec.hs +0/−29
- source/test-suite/Saturn/Unstable/Extra/TupleSpec.hs +0/−20
- source/test-suite/SaturnSpec.hs +0/−929
saturn.cabal view
@@ -1,6 +1,6 @@-cabal-version: 2.2+cabal-version: 3.0 name: saturn-version: 0.1.0.1+version: 0.2.0.0 synopsis: Handle POSIX cron schedules. description: Saturn handles POSIX cron schedules. build-type: Simple@@ -22,13 +22,8 @@ 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+ build-depends: base ^>=4.16.0 || ^>=4.17.0 || ^>=4.18.0+ default-language: Haskell2010 ghc-options: -Weverything -Wno-all-missed-specialisations -Wno-implicit-prelude -Wno-missed-specialisations -Wno-missing-deriving-strategies@@ -39,19 +34,15 @@ if flag(pedantic) ghc-options: -Werror -common executable- import: library- build-depends: saturn- ghc-options: -rtsopts -threaded--library+library unstable import: library build-depends: , containers ^>=0.6.5+ , parsec ^>=3.1.14 , text ^>=1.2.5 || ^>=2.0.2+ , time ^>=1.11.1 || ^>=1.12.2 exposed-modules:- Saturn Saturn.Unstable.Extra.Int Saturn.Unstable.Extra.Ord Saturn.Unstable.Extra.Parsec@@ -72,22 +63,55 @@ Saturn.Unstable.Type.Weekday Saturn.Unstable.Type.Wildcard - hs-source-dirs: source/library+ hs-source-dirs: source/libraries/unstable -test-suite saturn-test-suite- import: executable+library+ import: library+ build-depends: saturn:unstable+ exposed-modules: Saturn+ hs-source-dirs: source/libraries/saturn++library spec+ import: library build-depends: , hspec ^>=2.11.2+ , parsec , QuickCheck ^>=2.14.3+ , saturn:unstable+ , text+ , time - hs-source-dirs: source/test-suite- main-is: Main.hs- other-modules:+ exposed-modules: Saturn.Unstable.Extra.IntSpec Saturn.Unstable.Extra.OrdSpec Saturn.Unstable.Extra.ParsecSpec Saturn.Unstable.Extra.TimeSpec Saturn.Unstable.Extra.TupleSpec+ Saturn.Unstable.MatchSpec+ Saturn.Unstable.ParseSpec+ Saturn.Unstable.RenderSpec+ Saturn.Unstable.Type.DaySpec+ Saturn.Unstable.Type.ElementSpec+ Saturn.Unstable.Type.FieldSpec+ Saturn.Unstable.Type.HourSpec+ Saturn.Unstable.Type.MinuteSpec+ Saturn.Unstable.Type.MonthSpec+ Saturn.Unstable.Type.NumberSpec+ Saturn.Unstable.Type.RangeSpec+ Saturn.Unstable.Type.ScheduleSpec+ Saturn.Unstable.Type.WeekdaySpec+ Saturn.Unstable.Type.WildcardSpec SaturnSpec + hs-source-dirs: source/libraries/spec++test-suite saturn-test-suite+ import: library+ build-depends:+ , hspec ^>=2.11.2+ , saturn:spec++ ghc-options: -rtsopts -threaded+ hs-source-dirs: source/test-suite+ main-is: Main.hs type: exitcode-stdio-1.0
+ source/libraries/saturn/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/libraries/spec/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/libraries/spec/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/libraries/spec/Saturn/Unstable/Extra/ParsecSpec.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/spec/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/libraries/spec/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/libraries/spec/Saturn/Unstable/MatchSpec.hs view
@@ -0,0 +1,484 @@+module Saturn.Unstable.MatchSpec where++import qualified Data.Fixed as Fixed+import qualified Data.Time as Time+import qualified Data.Time.Calendar.WeekDate as Time+import qualified Saturn.Unstable.Match as Match+import qualified Saturn.Unstable.Type.ScheduleSpec as ScheduleSpec+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Match" $ do+ Hspec.describe "isMatch" $ do+ Hspec.it "is always true with all wildcards"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch utcTime++ Hspec.it "is true when day or weekday matches" $ do+ s <- ScheduleSpec.new [] [] [[5]] [] [[5]]+ t1 <- newUtcTime 1970 1 5 0 0 0+ s `Hspec.shouldSatisfy` Match.isMatch t1+ t2 <- newUtcTime 1970 1 2 0 0 0+ s `Hspec.shouldSatisfy` Match.isMatch t2++ Hspec.describe "minute" $ do+ Hspec.it "is always true when a number matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [[5]] [] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withMinute 5 utcTime)++ Hspec.it "is always true when a range matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [[4, 5]] [] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withMinute 5 utcTime)++ Hspec.it "is always true when a choice matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [[4], [5]] [] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withMinute 5 utcTime)++ Hspec.it "is true when a number matches" $ do+ t <- newUtcTime 1970 1 1 0 5 0+ s <- ScheduleSpec.new [[5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a number does not match" $ do+ t <- newUtcTime 1970 1 1 0 6 0+ s <- ScheduleSpec.new [[5]] [] [] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a range matches" $ do+ t <- newUtcTime 1970 1 1 0 5 0+ s <- ScheduleSpec.new [[4, 5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a range does not match" $ do+ t <- newUtcTime 1970 1 1 0 6 0+ s <- ScheduleSpec.new [[4, 5]] [] [] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a choice matches" $ do+ t <- newUtcTime 1970 1 1 0 5 0+ s <- ScheduleSpec.new [[4], [5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a choice does not match" $ do+ t <- newUtcTime 1970 1 1 0 6 0+ s <- ScheduleSpec.new [[4], [5]] [] [] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "accepts any second" $ do+ t <- newUtcTime 1970 1 1 0 5 6+ s <- ScheduleSpec.new [[5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any hour" $ do+ t <- newUtcTime 1970 1 1 6 5 0+ s <- ScheduleSpec.new [[5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any day" $ do+ t <- newUtcTime 1970 1 6 0 5 0+ s <- ScheduleSpec.new [[5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any month" $ do+ t <- newUtcTime 1970 6 1 0 5 0+ s <- ScheduleSpec.new [[5]] [] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.describe "hour" $ do+ Hspec.it "is always true when a number matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [[5]] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withHour 5 utcTime)++ Hspec.it "is always true when a range matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [[4, 5]] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withHour 5 utcTime)++ Hspec.it "is always true when a choice matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [[4], [5]] [] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withHour 5 utcTime)++ Hspec.it "is true when a number matches" $ do+ t <- newUtcTime 1970 1 1 5 0 0+ s <- ScheduleSpec.new [] [[5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a number does not match" $ do+ t <- newUtcTime 1970 1 1 6 0 0+ s <- ScheduleSpec.new [] [[5]] [] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a range matches" $ do+ t <- newUtcTime 1970 1 1 5 0 0+ s <- ScheduleSpec.new [] [[4, 5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a range does not match" $ do+ t <- newUtcTime 1970 1 1 6 0 0+ s <- ScheduleSpec.new [] [[4, 5]] [] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a choice matches" $ do+ t <- newUtcTime 1970 1 1 5 0 0+ s <- ScheduleSpec.new [] [[4], [5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a choice does not match" $ do+ t <- newUtcTime 1970 1 1 6 0 0+ s <- ScheduleSpec.new [] [[4], [5]] [] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "accepts any second" $ do+ t <- newUtcTime 1970 1 1 5 0 6+ s <- ScheduleSpec.new [] [[5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any minute" $ do+ t <- newUtcTime 1970 1 1 5 6 0+ s <- ScheduleSpec.new [] [[5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any day" $ do+ t <- newUtcTime 1970 1 6 5 0 0+ s <- ScheduleSpec.new [] [[5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any month" $ do+ t <- newUtcTime 1970 6 1 5 0 0+ s <- ScheduleSpec.new [] [[5]] [] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.describe "day" $ do+ Hspec.it "is always true when a number matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [[5]] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withDayOfMonth 5 utcTime)++ Hspec.it "is always true when a range matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [[4, 5]] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withDayOfMonth 5 utcTime)++ Hspec.it "is always true when a choice matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [[4], [5]] [] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withDayOfMonth 5 utcTime)++ Hspec.it "is true when a number matches" $ do+ t <- newUtcTime 1970 1 5 0 0 0+ s <- ScheduleSpec.new [] [] [[5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a number does not match" $ do+ t <- newUtcTime 1970 1 6 0 0 0+ s <- ScheduleSpec.new [] [] [[5]] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a range matches" $ do+ t <- newUtcTime 1970 1 5 0 0 0+ s <- ScheduleSpec.new [] [] [[4, 5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a range does not match" $ do+ t <- newUtcTime 1970 1 6 0 0 0+ s <- ScheduleSpec.new [] [] [[4, 5]] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a choice matches" $ do+ t <- newUtcTime 1970 1 5 0 0 0+ s <- ScheduleSpec.new [] [] [[4], [5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a choice does not match" $ do+ t <- newUtcTime 1970 1 6 0 0 0+ s <- ScheduleSpec.new [] [] [[4], [5]] [] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "accepts any second" $ do+ t <- newUtcTime 1970 1 5 0 0 6+ s <- ScheduleSpec.new [] [] [[5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any minute" $ do+ t <- newUtcTime 1970 1 5 0 6 0+ s <- ScheduleSpec.new [] [] [[5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any hour" $ do+ t <- newUtcTime 1970 1 5 6 0 0+ s <- ScheduleSpec.new [] [] [[5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any month" $ do+ t <- newUtcTime 1970 6 5 0 0 0+ s <- ScheduleSpec.new [] [] [[5]] [] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.describe "month" $ do+ Hspec.it "is always true when a number matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [[5]] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withMonthOfYear 5 utcTime)++ Hspec.it "is always true when a range matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [[4, 5]] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withMonthOfYear 5 utcTime)++ Hspec.it "is always true when a choice matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [[4], [5]] []+ schedule `Hspec.shouldSatisfy` Match.isMatch (withMonthOfYear 5 utcTime)++ Hspec.it "is true when a number matches" $ do+ t <- newUtcTime 1970 5 1 0 0 0+ s <- ScheduleSpec.new [] [] [] [[5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a number does not match" $ do+ t <- newUtcTime 1970 6 1 0 0 0+ s <- ScheduleSpec.new [] [] [] [[5]] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a range matches" $ do+ t <- newUtcTime 1970 5 1 0 0 0+ s <- ScheduleSpec.new [] [] [] [[4, 5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a range does not match" $ do+ t <- newUtcTime 1970 6 1 0 0 0+ s <- ScheduleSpec.new [] [] [] [[4, 5]] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a choice matches" $ do+ t <- newUtcTime 1970 5 1 0 0 0+ s <- ScheduleSpec.new [] [] [] [[4], [5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a choice does not match" $ do+ t <- newUtcTime 1970 6 1 0 0 0+ s <- ScheduleSpec.new [] [] [] [[4], [5]] []+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "accepts any second" $ do+ t <- newUtcTime 1970 5 1 0 0 6+ s <- ScheduleSpec.new [] [] [] [[5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any minute" $ do+ t <- newUtcTime 1970 5 1 0 6 0+ s <- ScheduleSpec.new [] [] [] [[5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any hour" $ do+ t <- newUtcTime 1970 5 1 6 0 0+ s <- ScheduleSpec.new [] [] [] [[5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any day" $ do+ t <- newUtcTime 1970 5 6 0 0 0+ s <- ScheduleSpec.new [] [] [] [[5]] []+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.describe "weekday" $ do+ Hspec.it "is always true when a number matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [] [[5]]+ schedule `Hspec.shouldSatisfy` Match.isMatch (withDayOfWeek Time.Friday utcTime)++ Hspec.it "is always true when a range matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [] [[4, 5]]+ schedule `Hspec.shouldSatisfy` Match.isMatch (withDayOfWeek Time.Friday utcTime)++ Hspec.it "is always true when a choice matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \utcTime -> do+ schedule <- ScheduleSpec.new [] [] [] [] [[4], [5]]+ schedule `Hspec.shouldSatisfy` Match.isMatch (withDayOfWeek Time.Friday utcTime)++ Hspec.it "is true when a number matches" $ do+ t <- newUtcTime 1970 1 2 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a number does not match" $ do+ t <- newUtcTime 1970 1 3 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[5]]+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a range matches" $ do+ t <- newUtcTime 1970 1 2 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[4, 5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a range does not match" $ do+ t <- newUtcTime 1970 1 3 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[4, 5]]+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "is true when a choice matches" $ do+ t <- newUtcTime 1970 1 2 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[4], [5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "is false when a choice does not match" $ do+ t <- newUtcTime 1970 1 3 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[4], [5]]+ s `Hspec.shouldNotSatisfy` Match.isMatch t++ Hspec.it "accepts any second" $ do+ t <- newUtcTime 1970 1 2 0 0 6+ s <- ScheduleSpec.new [] [] [] [] [[5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any minute" $ do+ t <- newUtcTime 1970 1 2 0 6 0+ s <- ScheduleSpec.new [] [] [] [] [[5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any hour" $ do+ t <- newUtcTime 1970 1 2 6 0 0+ s <- ScheduleSpec.new [] [] [] [] [[5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.it "accepts any day" $ do+ t <- newUtcTime 1970 1 9 0 0 0+ s <- ScheduleSpec.new [] [] [] [] [[5]]+ s `Hspec.shouldSatisfy` Match.isMatch t++ Hspec.describe "nextMatch" $ do+ Hspec.it "succeeds with a leap day" $ do+ s <- ScheduleSpec.new [[0]] [[0]] [[29]] [[2]] []+ t1 <- newUtcTime 1970 1 1 0 0 0+ t2 <- newUtcTime 1972 2 29 0 0 0+ Match.nextMatch t1 s `Hspec.shouldBe` Just t2++ Hspec.it "succeeds with the next leap day" $ do+ s <- ScheduleSpec.new [[0]] [[0]] [[29]] [[2]] []+ t1 <- newUtcTime 1972 2 29 0 0 0+ t2 <- newUtcTime 1976 2 29 0 0 0+ Match.nextMatch t1 s `Hspec.shouldBe` Just t2++ Hspec.it "succeeds with the furthest leap day" $ do+ s <- ScheduleSpec.new [[0]] [[0]] [[29]] [[2]] []+ t1 <- newUtcTime 1896 2 29 0 0 0+ t2 <- newUtcTime 1904 2 29 0 0 0+ Match.nextMatch t1 s `Hspec.shouldBe` Just t2++ Hspec.it "fails with an impossible date" $ do+ s <- ScheduleSpec.new [[0]] [[0]] [[30]] [[2]] []+ t <- newUtcTime 1970 1 1 0 0 0+ Match.nextMatch t s `Hspec.shouldBe` Nothing++ Hspec.it "is always in the future"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \t1 -> do+ schedule <- ScheduleSpec.new [] [] [] [] []+ t2 <- maybe (fail "impossible") pure $ Match.nextMatch t1 schedule+ t2 `Hspec.shouldSatisfy` (>= t1)++ Hspec.it "always matches"+ . QuickCheck.forAllShrink arbitraryUtcTime shrinkUtcTime+ $ \t1 -> do+ schedule <- ScheduleSpec.new [] [] [] [] []+ t2 <- maybe (fail "impossible") pure $ Match.nextMatch t1 schedule+ schedule `Hspec.shouldSatisfy` Match.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+ )+ )
+ source/libraries/spec/Saturn/Unstable/ParseSpec.hs view
@@ -0,0 +1,214 @@+module Saturn.Unstable.ParseSpec where++import qualified Data.Either as Either+import qualified Saturn.Unstable.Parse as Parse+import qualified Saturn.Unstable.Type.ScheduleSpec as ScheduleSpec+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Parse" $ do+ Hspec.describe "fromString" $ do+ Hspec.it "accepts wildcards" $ do+ schedule <- ScheduleSpec.new [] [] [] [] []+ Parse.fromString "* * * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts extra spaces" $ do+ schedule <- ScheduleSpec.new [] [] [] [] []+ Parse.fromString " * * * * * " `Hspec.shouldBe` Right schedule++ Hspec.it "accepts numbers" $ do+ schedule <- ScheduleSpec.new [[4]] [[3]] [[2]] [[1]] [[0]]+ Parse.fromString "4 3 2 1 0" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts ranges" $ do+ schedule <- ScheduleSpec.new [[8, 9]] [[6, 7]] [[4, 5]] [[2, 3]] [[0, 1]]+ Parse.fromString "8-9 6-7 4-5 2-3 0-1" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts choices" $ do+ schedule <- ScheduleSpec.new [[8], [9]] [[6], [7]] [[4], [5]] [[2], [3]] [[0], [1]]+ Parse.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 <- ScheduleSpec.new [[0]] [] [] [] []+ Parse.fromString "0 * * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a range" $ do+ schedule <- ScheduleSpec.new [[0, 1]] [] [] [] []+ Parse.fromString "0-1 * * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a choice" $ do+ schedule <- ScheduleSpec.new [[0], [1]] [] [] [] []+ Parse.fromString "0,1 * * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "rejects two wildcards" $ do+ Parse.fromString "*,* * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a number" $ do+ Parse.fromString "*,0 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a range" $ do+ Parse.fromString "*,0-0 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number" $ do+ Parse.fromString "60 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number as part of a choice" $ do+ Parse.fromString "0,60 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds range" $ do+ Parse.fromString "60-61 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a half out of bounds range" $ do+ Parse.fromString "0-60 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a backwards range" $ do+ Parse.fromString "1-0 * * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.describe "hour" $ do+ Hspec.it "accepts a number" $ do+ schedule <- ScheduleSpec.new [] [[0]] [] [] []+ Parse.fromString "* 0 * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a range" $ do+ schedule <- ScheduleSpec.new [] [[0, 1]] [] [] []+ Parse.fromString "* 0-1 * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a choice" $ do+ schedule <- ScheduleSpec.new [] [[0], [1]] [] [] []+ Parse.fromString "* 0,1 * * *" `Hspec.shouldBe` Right schedule++ Hspec.it "rejects two wildcards" $ do+ Parse.fromString "* *,* * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a number" $ do+ Parse.fromString "* *,0 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a range" $ do+ Parse.fromString "* *,0-0 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number" $ do+ Parse.fromString "* 24 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number as part of a choice" $ do+ Parse.fromString "* 0,24 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds range" $ do+ Parse.fromString "* 24-25 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a half out of bounds range" $ do+ Parse.fromString "* 0-24 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a backwards range" $ do+ Parse.fromString "* 1-0 * * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.describe "day" $ do+ Hspec.it "accepts a number" $ do+ schedule <- ScheduleSpec.new [] [] [[1]] [] []+ Parse.fromString "* * 1 * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a range" $ do+ schedule <- ScheduleSpec.new [] [] [[1, 2]] [] []+ Parse.fromString "* * 1-2 * *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a choice" $ do+ schedule <- ScheduleSpec.new [] [] [[1], [2]] [] []+ Parse.fromString "* * 1,2 * *" `Hspec.shouldBe` Right schedule++ Hspec.it "rejects two wildcards" $ do+ Parse.fromString "* * *,* * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a number" $ do+ Parse.fromString "* * *,1 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a range" $ do+ Parse.fromString "* * *,1-1 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number" $ do+ Parse.fromString "* * 32 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number as part of a choice" $ do+ Parse.fromString "* * 1,32 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds range" $ do+ Parse.fromString "* * 32-33 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a half out of bounds range" $ do+ Parse.fromString "* * 1-32 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a backwards range" $ do+ Parse.fromString "* * 2-1 * *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.describe "month" $ do+ Hspec.it "accepts a number" $ do+ schedule <- ScheduleSpec.new [] [] [] [[1]] []+ Parse.fromString "* * * 1 *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a range" $ do+ schedule <- ScheduleSpec.new [] [] [] [[1, 2]] []+ Parse.fromString "* * * 1-2 *" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a choice" $ do+ schedule <- ScheduleSpec.new [] [] [] [[1], [2]] []+ Parse.fromString "* * * 1,2 *" `Hspec.shouldBe` Right schedule++ Hspec.it "rejects two wildcards" $ do+ Parse.fromString "* * * *,* *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a number" $ do+ Parse.fromString "* * * *,1 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a range" $ do+ Parse.fromString "* * * *,1-1 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number" $ do+ Parse.fromString "* * * 13 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number as part of a choice" $ do+ Parse.fromString "* * * 1,13 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds range" $ do+ Parse.fromString "* * * 13-14 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a half out of bounds range" $ do+ Parse.fromString "* * * 1-13 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a backwards range" $ do+ Parse.fromString "* * * 2-1 *" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.describe "weekday" $ do+ Hspec.it "accepts a number" $ do+ schedule <- ScheduleSpec.new [] [] [] [] [[0]]+ Parse.fromString "* * * * 0" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a range" $ do+ schedule <- ScheduleSpec.new [] [] [] [] [[0, 1]]+ Parse.fromString "* * * * 0-1" `Hspec.shouldBe` Right schedule++ Hspec.it "accepts a choice" $ do+ schedule <- ScheduleSpec.new [] [] [] [] [[0], [1]]+ Parse.fromString "* * * * 0,1" `Hspec.shouldBe` Right schedule++ Hspec.it "rejects two wildcards" $ do+ Parse.fromString "* * * * *,*" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a number" $ do+ Parse.fromString "* * * * *,0" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a wildcard and a range" $ do+ Parse.fromString "* * * * *,0-0" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number" $ do+ Parse.fromString "* * * * 7" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds number as part of a choice" $ do+ Parse.fromString "* * * * 0,7" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects an out of bounds range" $ do+ Parse.fromString "* * * * 7-8" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a half out of bounds range" $ do+ Parse.fromString "* * * * 0-7" `Hspec.shouldSatisfy` Either.isLeft++ Hspec.it "rejects a backwards range" $ do+ Parse.fromString "* * * * 1-0" `Hspec.shouldSatisfy` Either.isLeft
+ source/libraries/spec/Saturn/Unstable/RenderSpec.hs view
@@ -0,0 +1,24 @@+module Saturn.Unstable.RenderSpec where++import qualified Saturn.Unstable.Render as Render+import qualified Saturn.Unstable.Type.ScheduleSpec as ScheduleSpec+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Render" $ do+ Hspec.describe "toString" $ do+ Hspec.it "works with wildcards" $ do+ schedule <- ScheduleSpec.new [] [] [] [] []+ Render.toString schedule `Hspec.shouldBe` "* * * * *"++ Hspec.it "works with numbers" $ do+ schedule <- ScheduleSpec.new [[4]] [[3]] [[2]] [[1]] [[0]]+ Render.toString schedule `Hspec.shouldBe` "4 3 2 1 0"++ Hspec.it "works with ranges" $ do+ schedule <- ScheduleSpec.new [[8, 9]] [[6, 7]] [[4, 5]] [[2, 3]] [[0, 1]]+ Render.toString schedule `Hspec.shouldBe` "8-9 6-7 4-5 2-3 0-1"++ Hspec.it "works with choices" $ do+ schedule <- ScheduleSpec.new [[8], [9]] [[6], [7]] [[4], [5]] [[2], [3]] [[0], [1]]+ Render.toString schedule `Hspec.shouldBe` "8,9 6,7 4,5 2,3 0,1"
+ source/libraries/spec/Saturn/Unstable/Type/DaySpec.hs view
@@ -0,0 +1,29 @@+module Saturn.Unstable.Type.DaySpec where++import qualified Data.Maybe as Maybe+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Day as Day+import qualified Saturn.Unstable.Type.FieldSpec as FieldSpec+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Day" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Day.parsec "" (Builder.toLazyText $ Day.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Day.Day+arbitrary = QuickCheck.suchThatMap FieldSpec.arbitrary Day.fromField++shrink :: Day.Day -> [Day.Day]+shrink = Maybe.mapMaybe Day.fromField . FieldSpec.shrink . Day.toField++new :: (MonadFail m) => [[Word.Word8]] -> m Day.Day+new xs = do+ field <- FieldSpec.new xs+ maybe (fail $ "invalid Day: " <> show xs) pure $ Day.fromField field
+ source/libraries/spec/Saturn/Unstable/Type/ElementSpec.hs view
@@ -0,0 +1,45 @@+module Saturn.Unstable.Type.ElementSpec where++import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.Element as Element+import qualified Saturn.Unstable.Type.Number as Number+import qualified Saturn.Unstable.Type.NumberSpec as NumberSpec+import qualified Saturn.Unstable.Type.Range as Range+import qualified Saturn.Unstable.Type.RangeSpec as RangeSpec+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Element" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Element.parsec "" (Builder.toLazyText $ Element.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Element.Element+arbitrary =+ Element.fromEither+ <$> QuickCheck.liftArbitrary2+ RangeSpec.arbitrary+ NumberSpec.arbitrary++shrink :: Element.Element -> [Element.Element]+shrink 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 RangeSpec.shrink NumberSpec.shrink+ $ Element.toEither element++new :: (MonadFail m) => [Word.Word8] -> m Element.Element+new xs = case xs of+ [x] -> pure . Element.fromEither . Right $ Number.fromWord8 x+ [x, y] -> Element.fromEither . Left <$> RangeSpec.new (x, y)+ _ -> fail $ "invalid Element: " <> show xs
+ source/libraries/spec/Saturn/Unstable/Type/FieldSpec.hs view
@@ -0,0 +1,53 @@+module Saturn.Unstable.Type.FieldSpec where++import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Maybe as Maybe+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.ElementSpec as ElementSpec+import qualified Saturn.Unstable.Type.Field as Field+import qualified Saturn.Unstable.Type.Wildcard as Wildcard+import qualified Saturn.Unstable.Type.WildcardSpec as WildcardSpec+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Field" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Field.parsec "" (Builder.toLazyText $ Field.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Field.Field+arbitrary =+ Field.fromEither+ <$> QuickCheck.liftArbitrary2+ WildcardSpec.arbitrary+ (arbitraryNonEmpty ElementSpec.arbitrary)++arbitraryNonEmpty :: QuickCheck.Gen a -> QuickCheck.Gen (NonEmpty.NonEmpty a)+arbitraryNonEmpty g = (NonEmpty.:|) <$> g <*> QuickCheck.listOf g++shrink :: Field.Field -> [Field.Field]+shrink field =+ let xs = case Field.toEither field of+ Left _ -> []+ Right _ -> [Field.fromEither . Left $ Wildcard.fromUnit ()]+ in mappend xs+ . fmap Field.fromEither+ . QuickCheck.liftShrink2 WildcardSpec.shrink (shrinkNonEmpty ElementSpec.shrink)+ $ Field.toEither field++shrinkNonEmpty :: (a -> [a]) -> NonEmpty.NonEmpty a -> [NonEmpty.NonEmpty a]+shrinkNonEmpty f = Maybe.mapMaybe (NonEmpty.nonEmpty . f) . NonEmpty.toList++new :: (MonadFail m) => [[Word.Word8]] -> m Field.Field+new =+ fmap+ ( Field.fromEither+ . maybe (Left $ Wildcard.fromUnit ()) Right+ . NonEmpty.nonEmpty+ )+ . mapM ElementSpec.new
+ source/libraries/spec/Saturn/Unstable/Type/HourSpec.hs view
@@ -0,0 +1,29 @@+module Saturn.Unstable.Type.HourSpec where++import qualified Data.Maybe as Maybe+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.FieldSpec as FieldSpec+import qualified Saturn.Unstable.Type.Hour as Hour+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Hour" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Hour.parsec "" (Builder.toLazyText $ Hour.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Hour.Hour+arbitrary = QuickCheck.suchThatMap FieldSpec.arbitrary Hour.fromField++shrink :: Hour.Hour -> [Hour.Hour]+shrink = Maybe.mapMaybe Hour.fromField . FieldSpec.shrink . Hour.toField++new :: (MonadFail m) => [[Word.Word8]] -> m Hour.Hour+new xs = do+ field <- FieldSpec.new xs+ maybe (fail $ "invalid Hour: " <> show xs) pure $ Hour.fromField field
+ source/libraries/spec/Saturn/Unstable/Type/MinuteSpec.hs view
@@ -0,0 +1,29 @@+module Saturn.Unstable.Type.MinuteSpec where++import qualified Data.Maybe as Maybe+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.FieldSpec as FieldSpec+import qualified Saturn.Unstable.Type.Minute as Minute+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Minute" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Minute.parsec "" (Builder.toLazyText $ Minute.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Minute.Minute+arbitrary = QuickCheck.suchThatMap FieldSpec.arbitrary Minute.fromField++shrink :: Minute.Minute -> [Minute.Minute]+shrink = Maybe.mapMaybe Minute.fromField . FieldSpec.shrink . Minute.toField++new :: (MonadFail m) => [[Word.Word8]] -> m Minute.Minute+new xs = do+ field <- FieldSpec.new xs+ maybe (fail $ "invalid Minute: " <> show xs) pure $ Minute.fromField field
+ source/libraries/spec/Saturn/Unstable/Type/MonthSpec.hs view
@@ -0,0 +1,29 @@+module Saturn.Unstable.Type.MonthSpec where++import qualified Data.Maybe as Maybe+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.FieldSpec as FieldSpec+import qualified Saturn.Unstable.Type.Month as Month+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Month" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Month.parsec "" (Builder.toLazyText $ Month.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Month.Month+arbitrary = QuickCheck.suchThatMap FieldSpec.arbitrary Month.fromField++shrink :: Month.Month -> [Month.Month]+shrink = Maybe.mapMaybe Month.fromField . FieldSpec.shrink . Month.toField++new :: (MonadFail m) => [[Word.Word8]] -> m Month.Month+new xs = do+ field <- FieldSpec.new xs+ maybe (fail $ "invalid Month: " <> show xs) pure $ Month.fromField field
+ source/libraries/spec/Saturn/Unstable/Type/NumberSpec.hs view
@@ -0,0 +1,21 @@+module Saturn.Unstable.Type.NumberSpec where++import qualified Data.Text.Lazy.Builder as Builder+import qualified Saturn.Unstable.Type.Number as Number+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Number" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Number.parsec "" (Builder.toLazyText $ Number.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Number.Number+arbitrary = Number.fromWord8 <$> QuickCheck.arbitrary++shrink :: Number.Number -> [Number.Number]+shrink = fmap Number.fromWord8 . QuickCheck.shrink . Number.toWord8
+ source/libraries/spec/Saturn/Unstable/Type/RangeSpec.hs view
@@ -0,0 +1,38 @@+module Saturn.Unstable.Type.RangeSpec where++import qualified Data.Maybe as Maybe+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 Saturn.Unstable.Type.NumberSpec as NumberSpec+import qualified Saturn.Unstable.Type.Range as Range+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Range" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Range.parsec "" (Builder.toLazyText $ Range.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Range.Range+arbitrary =+ QuickCheck.suchThatMap+ (QuickCheck.liftArbitrary2 NumberSpec.arbitrary NumberSpec.arbitrary)+ Range.fromTuple++shrink :: Range.Range -> [Range.Range]+shrink =+ Maybe.mapMaybe Range.fromTuple+ . QuickCheck.liftShrink2 NumberSpec.shrink NumberSpec.shrink+ . Range.toTuple++new :: (MonadFail m) => (Word.Word8, Word.Word8) -> m Range.Range+new tuple =+ maybe (fail $ "invalid Range: " <> show tuple) pure+ . Range.fromTuple+ $ Tuple.mapBoth Number.fromWord8 tuple
+ source/libraries/spec/Saturn/Unstable/Type/ScheduleSpec.hs view
@@ -0,0 +1,85 @@+module Saturn.Unstable.Type.ScheduleSpec where++import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.DaySpec as DaySpec+import qualified Saturn.Unstable.Type.HourSpec as HourSpec+import qualified Saturn.Unstable.Type.MinuteSpec as MinuteSpec+import qualified Saturn.Unstable.Type.MonthSpec as MonthSpec+import qualified Saturn.Unstable.Type.Schedule as Schedule+import qualified Saturn.Unstable.Type.WeekdaySpec as WeekdaySpec+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Schedule" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Schedule.parsec "" (Builder.toLazyText $ Schedule.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Schedule.Schedule+arbitrary =+ Schedule.Schedule+ <$> MinuteSpec.arbitrary+ <*> HourSpec.arbitrary+ <*> DaySpec.arbitrary+ <*> MonthSpec.arbitrary+ <*> WeekdaySpec.arbitrary++shrink :: Schedule.Schedule -> [Schedule.Schedule]+shrink schedule =+ ( \(minute, hour, day, month, weekday) ->+ Schedule.Schedule+ { Schedule.minute = minute,+ Schedule.hour = hour,+ Schedule.day = day,+ Schedule.month = month,+ Schedule.weekday = weekday+ }+ )+ <$> liftShrink5+ MinuteSpec.shrink+ HourSpec.shrink+ DaySpec.shrink+ MonthSpec.shrink+ WeekdaySpec.shrink+ ( 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)))++new ::+ (MonadFail m) =>+ [[Word.Word8]] ->+ [[Word.Word8]] ->+ [[Word.Word8]] ->+ [[Word.Word8]] ->+ [[Word.Word8]] ->+ m Schedule.Schedule+new minutes hours days months weekdays =+ Schedule.Schedule+ <$> MinuteSpec.new minutes+ <*> HourSpec.new hours+ <*> DaySpec.new days+ <*> MonthSpec.new months+ <*> WeekdaySpec.new weekdays
+ source/libraries/spec/Saturn/Unstable/Type/WeekdaySpec.hs view
@@ -0,0 +1,29 @@+module Saturn.Unstable.Type.WeekdaySpec where++import qualified Data.Maybe as Maybe+import qualified Data.Text.Lazy.Builder as Builder+import qualified Data.Word as Word+import qualified Saturn.Unstable.Type.FieldSpec as FieldSpec+import qualified Saturn.Unstable.Type.Weekday as Weekday+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Weekday" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Weekday.parsec "" (Builder.toLazyText $ Weekday.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Weekday.Weekday+arbitrary = QuickCheck.suchThatMap FieldSpec.arbitrary Weekday.fromField++shrink :: Weekday.Weekday -> [Weekday.Weekday]+shrink = Maybe.mapMaybe Weekday.fromField . FieldSpec.shrink . Weekday.toField++new :: (MonadFail m) => [[Word.Word8]] -> m Weekday.Weekday+new xs = do+ field <- FieldSpec.new xs+ maybe (fail $ "invalid Weekday: " <> show xs) pure $ Weekday.fromField field
+ source/libraries/spec/Saturn/Unstable/Type/WildcardSpec.hs view
@@ -0,0 +1,21 @@+module Saturn.Unstable.Type.WildcardSpec where++import qualified Data.Text.Lazy.Builder as Builder+import qualified Saturn.Unstable.Type.Wildcard as Wildcard+import qualified Test.Hspec as Hspec+import qualified Test.QuickCheck as QuickCheck+import qualified Text.Parsec as Parsec++spec :: Hspec.Spec+spec = Hspec.describe "Saturn.Unstable.Type.Wildcard" $ do+ Hspec.it "round trips"+ . QuickCheck.forAllShrink arbitrary shrink+ $ \x -> do+ Parsec.parse Wildcard.parsec "" (Builder.toLazyText $ Wildcard.toBuilder x)+ `Hspec.shouldBe` Right x++arbitrary :: QuickCheck.Gen Wildcard.Wildcard+arbitrary = Wildcard.fromUnit <$> QuickCheck.arbitrary++shrink :: Wildcard.Wildcard -> [Wildcard.Wildcard]+shrink = fmap Wildcard.fromUnit . QuickCheck.shrink . Wildcard.toUnit
+ source/libraries/spec/SaturnSpec.hs view
@@ -0,0 +1,25 @@+module SaturnSpec where++import qualified Saturn.Unstable.Parse as Parse+import qualified Saturn.Unstable.Render as Render+import qualified Saturn.Unstable.Type.ScheduleSpec as ScheduleSpec+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 ScheduleSpec.arbitrary ScheduleSpec.shrink+ $ \schedule ->+ Parse.fromString (Render.toString schedule) `Hspec.shouldBe` Right schedule++ Hspec.it "through strict text"+ . QuickCheck.forAllShrink ScheduleSpec.arbitrary ScheduleSpec.shrink+ $ \schedule ->+ Parse.fromText (Render.toText schedule) `Hspec.shouldBe` Right schedule++ Hspec.it "through lazy text"+ . QuickCheck.forAllShrink ScheduleSpec.arbitrary ScheduleSpec.shrink+ $ \schedule ->+ Parse.fromLazyText (Render.toLazyText schedule) `Hspec.shouldBe` Right schedule
+ source/libraries/unstable/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/libraries/unstable/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/libraries/unstable/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/libraries/unstable/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/libraries/unstable/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/libraries/unstable/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/libraries/unstable/Saturn/Unstable/Parse.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/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/libraries/unstable/Saturn/Unstable/Type/Day.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Element.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Field.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Hour.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Minute.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Month.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Number.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Range.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Schedule.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Weekday.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE FlexibleContexts #-}++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/libraries/unstable/Saturn/Unstable/Type/Wildcard.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE FlexibleContexts #-}++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/library/Saturn.hs
@@ -1,64 +0,0 @@--- | 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
@@ -1,10 +0,0 @@-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
@@ -1,4 +0,0 @@-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
@@ -1,16 +0,0 @@-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
@@ -1,14 +0,0 @@-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
@@ -1,9 +0,0 @@-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
@@ -1,89 +0,0 @@-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
@@ -1,28 +0,0 @@-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
@@ -1,21 +0,0 @@-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
@@ -1,36 +0,0 @@-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
@@ -1,32 +0,0 @@-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
@@ -1,56 +0,0 @@-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
@@ -1,36 +0,0 @@-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
@@ -1,36 +0,0 @@-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
@@ -1,36 +0,0 @@-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
@@ -1,33 +0,0 @@-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
@@ -1,45 +0,0 @@-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
@@ -1,43 +0,0 @@-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
@@ -1,36 +0,0 @@-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
@@ -1,22 +0,0 @@-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
@@ -3,6 +3,20 @@ import qualified Saturn.Unstable.Extra.ParsecSpec import qualified Saturn.Unstable.Extra.TimeSpec import qualified Saturn.Unstable.Extra.TupleSpec+import qualified Saturn.Unstable.MatchSpec+import qualified Saturn.Unstable.ParseSpec+import qualified Saturn.Unstable.RenderSpec+import qualified Saturn.Unstable.Type.DaySpec+import qualified Saturn.Unstable.Type.ElementSpec+import qualified Saturn.Unstable.Type.FieldSpec+import qualified Saturn.Unstable.Type.HourSpec+import qualified Saturn.Unstable.Type.MinuteSpec+import qualified Saturn.Unstable.Type.MonthSpec+import qualified Saturn.Unstable.Type.NumberSpec+import qualified Saturn.Unstable.Type.RangeSpec+import qualified Saturn.Unstable.Type.ScheduleSpec+import qualified Saturn.Unstable.Type.WeekdaySpec+import qualified Saturn.Unstable.Type.WildcardSpec import qualified SaturnSpec import qualified Test.Hspec as Hspec @@ -16,4 +30,18 @@ Saturn.Unstable.Extra.ParsecSpec.spec Saturn.Unstable.Extra.TimeSpec.spec Saturn.Unstable.Extra.TupleSpec.spec+ Saturn.Unstable.MatchSpec.spec+ Saturn.Unstable.ParseSpec.spec+ Saturn.Unstable.RenderSpec.spec+ Saturn.Unstable.Type.DaySpec.spec+ Saturn.Unstable.Type.ElementSpec.spec+ Saturn.Unstable.Type.FieldSpec.spec+ Saturn.Unstable.Type.HourSpec.spec+ Saturn.Unstable.Type.MinuteSpec.spec+ Saturn.Unstable.Type.MonthSpec.spec+ Saturn.Unstable.Type.NumberSpec.spec+ Saturn.Unstable.Type.RangeSpec.spec+ Saturn.Unstable.Type.ScheduleSpec.spec+ Saturn.Unstable.Type.WeekdaySpec.spec+ Saturn.Unstable.Type.WildcardSpec.spec SaturnSpec.spec
− source/test-suite/Saturn/Unstable/Extra/IntSpec.hs
@@ -1,23 +0,0 @@-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
@@ -1,22 +0,0 @@-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
@@ -1,35 +0,0 @@-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
@@ -1,29 +0,0 @@-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
@@ -1,20 +0,0 @@-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
@@ -1,929 +0,0 @@-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