packages feed

cron 0.4.2 → 0.5.0

raw patch · 15 files changed

+729/−4 lines, 15 filesdep +data-default-classdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: data-default-class

Dependency ranges changed: base

API changes (from Hackage documentation)

+ System.Cron.Describe: data OptionBuilder
+ System.Cron.Describe: defaultOpts :: OptionBuilder
+ System.Cron.Describe: describe :: OptionBuilder -> CronSchedule -> String
+ System.Cron.Describe: notVerbose :: OptionBuilder
+ System.Cron.Describe: twelveHourFormat :: OptionBuilder
+ System.Cron.Describe: twentyFourHourFormat :: OptionBuilder
+ System.Cron.Describe: verbose :: OptionBuilder
+ System.Cron.Internal.Describe.Descriptors: domDescriptor :: Descriptor
+ System.Cron.Internal.Describe.Descriptors: dowDescriptor :: Descriptor
+ System.Cron.Internal.Describe.Descriptors: hourDescriptor :: TimeFormat -> Descriptor
+ System.Cron.Internal.Describe.Descriptors: minuteDescriptor :: Descriptor
+ System.Cron.Internal.Describe.Descriptors: monthDescriptor :: Descriptor
+ System.Cron.Internal.Describe.Options: Builder :: (Options -> Options) -> OptionBuilder
+ System.Cron.Internal.Describe.Options: Opts :: TimeFormat -> Verbosity -> Options
+ System.Cron.Internal.Describe.Options: [timeFormat] :: Options -> TimeFormat
+ System.Cron.Internal.Describe.Options: [verbosity] :: Options -> Verbosity
+ System.Cron.Internal.Describe.Options: data OptionBuilder
+ System.Cron.Internal.Describe.Options: data Options
+ System.Cron.Internal.Describe.Options: defaultOpts :: OptionBuilder
+ System.Cron.Internal.Describe.Options: getOpts :: OptionBuilder -> Options
+ System.Cron.Internal.Describe.Options: instance Data.Default.Class.Default System.Cron.Internal.Describe.Options.Options
+ System.Cron.Internal.Describe.Options: instance Data.Semigroup.Semigroup System.Cron.Internal.Describe.Options.OptionBuilder
+ System.Cron.Internal.Describe.Options: instance GHC.Base.Monoid System.Cron.Internal.Describe.Options.OptionBuilder
+ System.Cron.Internal.Describe.Options: notVerbose :: OptionBuilder
+ System.Cron.Internal.Describe.Options: twelveHourFormat :: OptionBuilder
+ System.Cron.Internal.Describe.Options: twentyFourHourFormat :: OptionBuilder
+ System.Cron.Internal.Describe.Options: verbose :: OptionBuilder
+ System.Cron.Internal.Describe.Time: Hour :: Int -> Hour
+ System.Cron.Internal.Describe.Time: Minute :: Int -> Minute
+ System.Cron.Internal.Describe.Time: format :: TimeFormat -> Minute -> Hour -> String
+ System.Cron.Internal.Describe.Time: newtype Hour
+ System.Cron.Internal.Describe.Time: newtype Minute
+ System.Cron.Internal.Describe.Types: April :: Month
+ System.Cron.Internal.Describe.Types: August :: Month
+ System.Cron.Internal.Describe.Types: Concrete :: String -> DescribedValue
+ System.Cron.Internal.Describe.Types: ConcreteTime :: String -> Time
+ System.Cron.Internal.Describe.Types: December :: Month
+ System.Cron.Internal.Describe.Types: Desc :: Time -> Maybe DescribedValue -> Maybe DescribedValue -> Maybe DescribedValue -> Description
+ System.Cron.Internal.Describe.Types: Descriptor :: String -> String -> String -> String -> String -> (Int -> String) -> String -> String -> (Int -> Maybe String) -> String -> Maybe String -> Descriptor
+ System.Cron.Internal.Describe.Types: Every :: String -> DescribedValue
+ System.Cron.Internal.Describe.Types: February :: Month
+ System.Cron.Internal.Describe.Types: Friday :: Weekday
+ System.Cron.Internal.Describe.Types: Hour12 :: TimeFormat
+ System.Cron.Internal.Describe.Types: Hour24 :: TimeFormat
+ System.Cron.Internal.Describe.Types: January :: Month
+ System.Cron.Internal.Describe.Types: July :: Month
+ System.Cron.Internal.Describe.Types: June :: Month
+ System.Cron.Internal.Describe.Types: March :: Month
+ System.Cron.Internal.Describe.Types: May :: Month
+ System.Cron.Internal.Describe.Types: Monday :: Weekday
+ System.Cron.Internal.Describe.Types: NotVerbose :: Verbosity
+ System.Cron.Internal.Describe.Types: November :: Month
+ System.Cron.Internal.Describe.Types: October :: Month
+ System.Cron.Internal.Describe.Types: Other :: (Maybe DescribedValue) -> (Maybe DescribedValue) -> Time
+ System.Cron.Internal.Describe.Types: Saturday :: Weekday
+ System.Cron.Internal.Describe.Types: September :: Month
+ System.Cron.Internal.Describe.Types: Sunday :: Weekday
+ System.Cron.Internal.Describe.Types: Sunday2 :: Weekday
+ System.Cron.Internal.Describe.Types: Thursday :: Weekday
+ System.Cron.Internal.Describe.Types: Tuesday :: Weekday
+ System.Cron.Internal.Describe.Types: Verbose :: Verbosity
+ System.Cron.Internal.Describe.Types: Wednesday :: Weekday
+ System.Cron.Internal.Describe.Types: [_dom] :: Description -> Maybe DescribedValue
+ System.Cron.Internal.Describe.Types: [_dow] :: Description -> Maybe DescribedValue
+ System.Cron.Internal.Describe.Types: [_month] :: Description -> Maybe DescribedValue
+ System.Cron.Internal.Describe.Types: [_time] :: Description -> Time
+ System.Cron.Internal.Describe.Types: [displayItem] :: Descriptor -> Int -> String
+ System.Cron.Internal.Describe.Types: [listPrefix] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [listSuffix] :: Descriptor -> Maybe String
+ System.Cron.Internal.Describe.Types: [pluralDesc] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [rangeJoiner] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [rangePrefix] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [rangeSuffix] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [singularDesc] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [specificPrefix] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [specificSuffix] :: Descriptor -> String
+ System.Cron.Internal.Describe.Types: [stepSpecificSuffix] :: Descriptor -> Int -> Maybe String
+ System.Cron.Internal.Describe.Types: data DescribedValue
+ System.Cron.Internal.Describe.Types: data Description
+ System.Cron.Internal.Describe.Types: data Descriptor
+ System.Cron.Internal.Describe.Types: data Month
+ System.Cron.Internal.Describe.Types: data Time
+ System.Cron.Internal.Describe.Types: data TimeFormat
+ System.Cron.Internal.Describe.Types: data Verbosity
+ System.Cron.Internal.Describe.Types: data Weekday
+ System.Cron.Internal.Describe.Types: instance GHC.Enum.Bounded System.Cron.Internal.Describe.Types.Month
+ System.Cron.Internal.Describe.Types: instance GHC.Enum.Bounded System.Cron.Internal.Describe.Types.Weekday
+ System.Cron.Internal.Describe.Types: instance GHC.Enum.Enum System.Cron.Internal.Describe.Types.Month
+ System.Cron.Internal.Describe.Types: instance GHC.Enum.Enum System.Cron.Internal.Describe.Types.Weekday
+ System.Cron.Internal.Describe.Types: instance GHC.Show.Show System.Cron.Internal.Describe.Types.DescribedValue
+ System.Cron.Internal.Describe.Types: instance GHC.Show.Show System.Cron.Internal.Describe.Types.Description
+ System.Cron.Internal.Describe.Types: instance GHC.Show.Show System.Cron.Internal.Describe.Types.Month
+ System.Cron.Internal.Describe.Types: instance GHC.Show.Show System.Cron.Internal.Describe.Types.Time
+ System.Cron.Internal.Describe.Types: instance GHC.Show.Show System.Cron.Internal.Describe.Types.Weekday
+ System.Cron.Internal.Describe.Types: safeIntToMonth :: Int -> Month
+ System.Cron.Internal.Describe.Types: safeIntToWeekDay :: Int -> Weekday
+ System.Cron.Internal.Describe.Utils: allWords :: [String] -> String
+ System.Cron.Internal.Describe.Utils: cap :: String -> String
+ System.Cron.Internal.Describe.Utils: joinWords :: [String] -> String
+ System.Cron.Internal.Describe.Utils: viewHour :: HourSpec -> Maybe Hour
+ System.Cron.Internal.Describe.Utils: viewHourList :: HourSpec -> Maybe (NonEmpty BaseField)
+ System.Cron.Internal.Describe.Utils: viewMinute :: MinuteSpec -> Maybe Minute
+ System.Cron.Internal.Describe.Utils: viewMinuteRange :: MinuteSpec -> Maybe (Minute, Minute)

Files

README.md view
@@ -10,7 +10,9 @@ `scheduleMatches`, which you can use to compare a time against a `CronSchedule` to see if an action needs to be performed.  System.Cron.Parser is where you will find the parsers `cronSchedule`, `crontabEntry` and `cronTab`. To parse-individual schedules up to full crontab files.+individual schedules up to full crontab files. System.Cron.Descrive is where+you will find the `describe` function for creating human-readable strings from+cron schedules, as well as any options to control how the description is created.   To do anything, you'll need to install cabal-dev with cabal.@@ -51,9 +53,21 @@ job2 = putStrLn "Job 2" ``` +#### Describe+```haskell+main :: IO ()+main = do+  let Right cs1 = parseCronSchedule "*/2 * 3 * 4,5,6"+  print $ describe defaultOpts cs1++  let Right cs2 = parseCronSchedule "*/2 12 3 * 4,5,6"+  print $ describe (twentyFourHourFormat <> verbose) cs2+```+ ## Contributors  * [Simon Hengel](https://github.com/sol) * [Alberto Valverde](https://github.com/albertov) * [Andrew Rademacher](https://github.com/AndrewRademacher) * [Peter Simons](https://github.com/peti)+* [Joseph Canero](https://github.com/caneroj1)
bench/Main.hs view
@@ -22,6 +22,7 @@   , scheduleMatchesBenchmarks   , nextMatchBenchmarks   , serializeBenchmarks+  , describeBenchmarks   ]  @@ -67,6 +68,21 @@   -------------------------------------------------------------------------------+describeBenchmarks :: Benchmark+describeBenchmarks = bgroup "description"+  [+    bgroup "verbose" [+      bench "simple" (whnf (describe verbose) simpleCronSchedule)+    , bench "complicated" (whnf (describe verbose) complexCronSchedule)+    ]+  , bgroup "non-verbose" [+      bench "simple" (whnf (describe notVerbose) simpleCronSchedule)+    , bench "complicated" (whnf (describe notVerbose) complexCronSchedule)+    ]+  ]+++------------------------------------------------------------------------------- parserBench :: String -> Parser a -> Text -> Benchmark parserBench n parser txt = bench n (whnf (parseOnly parser) txt) @@ -86,9 +102,24 @@   -------------------------------------------------------------------------------+mkCronSchedule :: Text -> CronSchedule+mkCronSchedule t = let (Right cs) = parseCronSchedule t in cs+++-------------------------------------------------------------------------------+simpleCronSchedule :: CronSchedule+simpleCronSchedule = mkCronSchedule "1 2 3 * *"+++-------------------------------------------------------------------------------+complexCronSchedule :: CronSchedule+complexCronSchedule = mkCronSchedule "1-10 3 1-20/2 * 3,5"+++------------------------------------------------------------------------------- exampleCrontab :: Crontab exampleCrontab = Crontab (concat (zipWith (\x y -> [x,y]) (replicate 50 cmd) (repeat envSet)))-  where +  where     cmd = CommandEntry weekly (CronCommand "do something")     envSet = EnvVariable "FOO" "BAR" 
changelog view
@@ -1,3 +1,5 @@+# 0.5.0+* Add System.Cron.Describe which describes cron schedules in words (English only for now). Big thanks to Joe Canero for this feature! # 0.4.2 * Drop dependency on derive in tests. * Add some Generic and Typeable instances.
cron.cabal view
@@ -1,5 +1,5 @@ Name:                cron-Version:             0.4.2+Version:             0.5.0 Description:   Cron data structure and Attoparsec parser. The idea is to embed it in larger   systems which want to roll their own scheduled tasks in a format that people@@ -32,6 +32,7 @@   test/Main.hs   test/SpecHelper.hs   test/System/Test/Cron.hs+  test/System/Test/Cron/Describe.hs   test/System/Test/Cron/Parser.hs   test/System/Test/Cron/Schedule.hs Homepage:           http://github.com/michaelxavier/cron@@ -43,7 +44,13 @@  library   Exposed-modules:   System.Cron+                   , System.Cron.Internal.Describe.Descriptors+                   , System.Cron.Internal.Describe.Options+                   , System.Cron.Internal.Describe.Time+                   , System.Cron.Internal.Describe.Types+                   , System.Cron.Internal.Describe.Utils                    , System.Cron.Internal.Check+                   , System.Cron.Describe                    , System.Cron.Parser                    , System.Cron.Schedule                    , System.Cron.Types@@ -57,6 +64,7 @@                    , mtl             >= 2.0.1                    , mtl-compat      >= 0.2.1                    , semigroups+                   , data-default-class >= 0.0.1   if flag(lib-Werror)     ghc-options: -Werror @@ -70,6 +78,7 @@   default-language:    Haskell2010   other-modules:    SpecHelper                   , System.Test.Cron+                  , System.Test.Cron.Describe                   , System.Test.Cron.Parser                   , System.Test.Cron.Schedule   Build-Depends:    base
src/System/Cron.hs view
@@ -4,6 +4,7 @@     ( module System.Cron.Types     , module System.Cron.Parser     , module System.Cron.Schedule+    , module System.Cron.Describe     , scheduleMatches     , nextMatch     ) where@@ -11,6 +12,7 @@  ------------------------------------------------------------------------------- import           System.Cron.Internal.Check+import           System.Cron.Describe import           System.Cron.Parser import           System.Cron.Schedule import           System.Cron.Types
+ src/System/Cron/Describe.hs view
@@ -0,0 +1,190 @@+{-# LANGUAGE ViewPatterns    #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE CPP             #-}+--------------------------------------------------------------------+-- |+-- Module      : System.Cron.Describe+-- Description : Turn a cron schedule into a human-readable string+-- Copyright   : (c) Joseph Canero 2016+-- License     : MIT+--+-- Maintainer: Joseph Canero <jmc41493@gmail.com>+-- Portability: portable+--+--+-- > import System.Cron+-- >+-- > main :: IO ()+-- > main = do+-- >   let Right cs1 = parseCronSchedule "*/2 * 3 * 4,5,6"+-- >   print $ describe defaultOpts cs1+-- >+-- >   let Right cs2 = parseCronSchedule "*/2 12 3 * 4,5,6"+-- >   print $ describe (twentyFourHourFormat <> verbose) cs2+--------------------------------------------------------------------+module System.Cron.Describe+    (+      -- * Options handling+      defaultOpts+    , twentyFourHourFormat+    , twelveHourFormat+    , verbose+    , notVerbose+    , OptionBuilder+      -- * Describe a CronSchedule+    , describe+    ) where++-------------------------------------------------------------------------------+import Control.Monad+import Data.List.NonEmpty                       (NonEmpty (..), toList)+import Data.Maybe                               (fromJust)+#if !MIN_VERSION_base(4,8,0)+import Data.Traversable                         (traverse)+#endif+-------------------------------------------------------------------------------+import System.Cron.Internal.Describe.Descriptors+import System.Cron.Internal.Describe.Options+import System.Cron.Internal.Describe.Time+import System.Cron.Internal.Describe.Types+import System.Cron.Internal.Describe.Utils+import System.Cron.Types+-------------------------------------------------------------------------------+++-- | Given an 'OptionBuilder' and a 'CronSchedule' parsed with+-- 'System.Cron.Parser.parseCronSchedule', return a human-readable string+-- describing when that schedule will match.+describe :: OptionBuilder -> CronSchedule -> String+describe ob = cap                      .+              show                     .+              matchVerbosity verbosity .+              description timeFormat+  where Opts{..} = getOpts ob+++-------------------------------------------------------------------------------+-- Internals+-------------------------------------------------------------------------------+++describeRange :: RangeField -> Descriptor -> String+describeRange rf d = allWords [rangePrefix d,+                               displayItem d (rfBegin rf),+                               rangeJoiner d,+                               displayItem d (rfEnd rf),+                               rangeSuffix d]+++describeBaseField :: Descriptor -> BaseField -> DescribedValue+describeBaseField d (RangeField' rf)   = Concrete $ describeRange rf d+describeBaseField d Star               = Every $ "every " ++ singularDesc d+describeBaseField d (SpecificField' s) =+  Concrete $ allWords [specificPrefix d,+                       displayItem d (specificField s),+                       specificSuffix d]+++type StarOrDesc = Either String String+++describeListFields :: (BaseField -> String) -> NonEmpty BaseField -> StarOrDesc+describeListFields f (l :| ls) =+  fmap joinWords . foldM describeF [] $ reverse (l:ls)+  where describeF _ Star = Left $ f Star+        describeF e bf   = Right $ f bf : e++++describeCronField :: Descriptor -> CronField -> DescribedValue+describeCronField d (Field f) = describeBaseField d f+++describeCronField d (StepField' sf) = Concrete $+  stepPrefix ++ maybe "" (", " ++) (stepSuffix $ sfField sf)+  where+    stepPrefix = unwords ["every", show (sfStepping sf), pluralDesc d]+    stepSuffix Star              = Nothing+    stepSuffix (RangeField' rf)  = Just $ describeRange rf d+    stepSuffix (SpecificField' s) = stepSpecificSuffix d $ specificField s+++describeCronField d (ListField ls) =+  case describeListFields describeBF ls of+    Left  s -> Every s+    Right s -> Concrete $ unwords [listPrefix d,+                                   maybe s ((s ++ " ") ++) (listSuffix d)]+  where+    describeBF Star               = "every " ++ singularDesc d+    describeBF (SpecificField' s) = displayItem d $ specificField s+    describeBF (RangeField' rf)   = unwords [displayItem d (rfBegin rf),+                                             "through",+                                             displayItem d (rfEnd rf)]++-- There are a few special cases to handle when describing the minute and hour+-- fields that will make the cron description easier to read.+-- For the most part, these are pretty straight forward. The first three+-- pattern matches look for specific patterns in the minute and hour fields that+-- can be formatted differently. The last pattern match just defaults+-- to describing the fields using existing rules.+describeTime :: TimeFormat -> MinuteSpec -> HourSpec -> Time+describeTime tf (viewMinute -> Just m) (viewHour -> Just h) =+  ConcreteTime $ "at " ++ format tf m h+describeTime tf (viewMinuteRange -> Just (m1, m2)) (viewHour -> Just h) =+  ConcreteTime $ unwords ["every minute between",+                          format tf m1 h,+                          "and",+                          format tf m2 h]+describeTime tf (viewMinute -> Just m) (viewHourList -> Just hs) =+  describeMultHours tf m hs+describeTime tf (minuteSpec -> m) (hourSpec -> h) =+  Other (return $ describeCronField minuteDescriptor m)+        (return $ describeCronField (hourDescriptor tf) h)++-- We want to create a description for multiple hours given a concrete minute.+-- This is rather ugly, as the ListField type allows for any BaseField, so+-- we can potentially have a '*' within the list. In that case, we don't need+-- to describe the rest of the BaseFields for hour list, since we will just be+-- firing each hour.+describeMultHours :: TimeFormat -> Minute -> NonEmpty BaseField -> Time+describeMultHours t mn@(Minute m) ls =+  maybe mkOther (formatAllFields . toList) $ traverse formatBaseField ls+  where hourCF   = ListField ls+        minuteCF = Field (SpecificField' (fromJust $ mkSpecificField m))++        formatAllFields = ConcreteTime . ("at " ++) . joinWords++        formatBaseField (SpecificField' s) =+          Just $ format t mn (Hour (specificField s))+        formatBaseField Star               = Nothing+        formatBaseField f@(RangeField' _)  =+          Just $ unwords [show describedMinute,+                          show $ describeCronField (hourDescriptor t) (Field f)]++        mkOther = Other (return describedMinute)+                        (return $ describeCronField (hourDescriptor t) hourCF)+        describedMinute = describeCronField minuteDescriptor minuteCF+++description :: TimeFormat -> CronSchedule -> Description+description t c = Desc (describeTime t (minute c) (hour c))+                       (return ddom)+                       (return dm)+                       (return ddow)+  where ddom = describeCronField domDescriptor $ dayOfMonthSpec (dayOfMonth c)+        dm   = describeCronField monthDescriptor $ monthSpec (month c)+        ddow = describeCronField dowDescriptor $ dayOfWeekSpec (dayOfWeek c)+++matchVerbosity :: Verbosity -> Description -> Description+matchVerbosity v d@Desc{..} = d{  _dom   = stripEvery v =<< _dom+                                , _dow   = stripEvery v =<< _dow+                                , _time  = stripTime _time+                                , _month = stripEvery NotVerbose =<< _month}+  where stripTime t@(ConcreteTime _)   = t+        stripTime (Other mbMin mbHour) = Other mbMin (stripEvery v =<< mbHour)+++stripEvery :: Verbosity -> DescribedValue -> Maybe DescribedValue+stripEvery NotVerbose (Every _) = Nothing+stripEvery _          c         = Just c
+ src/System/Cron/Internal/Describe/Descriptors.hs view
@@ -0,0 +1,98 @@+module System.Cron.Internal.Describe.Descriptors where++import System.Cron.Internal.Describe.Time+import System.Cron.Internal.Describe.Types++minuteDescriptor :: Descriptor+minuteDescriptor = Descriptor {+    pluralDesc = "minutes"+  , singularDesc = "minute"+  , rangePrefix = "minutes"+  , rangeSuffix = "past the hour"+  , rangeJoiner = "through"+  , displayItem = show+  , specificPrefix = "at"+  , specificSuffix = "minutes past the hour"+  , stepSpecificSuffix = sss+  , listPrefix = "at"+  , listSuffix = Nothing+  }+  where+    sss n+      | n == 0    = Nothing+      | otherwise = Just $ "starting at " ++ show n ++ " minutes past the hour"+++hourDescriptor :: TimeFormat -> Descriptor+hourDescriptor tf = Descriptor {+    pluralDesc = "hours"+  , singularDesc = "hour"+  , rangePrefix = "between"+  , rangeSuffix = ""+  , rangeJoiner = "and"+  , displayItem = toHour+  , specificPrefix = "at"+  , specificSuffix = ""+  , stepSpecificSuffix = sss+  , listPrefix = "at"+  , listSuffix = Nothing+  }+  where toHour h = format tf (Minute 0) (Hour h)+        sss n+          | n == 0    = Nothing+          | otherwise = Just $ "starting at " ++ toHour n+++domDescriptor :: Descriptor+domDescriptor = Descriptor {+    pluralDesc = "days"+  , singularDesc = "day"+  , rangePrefix = "between days"+  , rangeSuffix = "of the month"+  , rangeJoiner = "and"+  , displayItem = show+  , specificPrefix = "on day"+  , specificSuffix = "of the month"+  , stepSpecificSuffix = sss+  , listPrefix = "on days"+  , listSuffix = Just "of the month"+  }+  where sss n = Just $ "starting on day " ++ show n ++ " of the month"+++monthDescriptor :: Descriptor+monthDescriptor = Descriptor {+    pluralDesc = "months"+  , singularDesc = "month"+  , rangePrefix = ""+  , rangeSuffix = ""+  , rangeJoiner = "through"+  , displayItem = toMonth+  , specificPrefix = "only in"+  , specificSuffix = ""+  , stepSpecificSuffix = sss+  , listPrefix = "only in"+  , listSuffix = Nothing+  }+  where toMonth = show . safeIntToMonth+        sss n+          | n == 1    = Nothing+          | otherwise = Just $ toMonth n ++ " through " ++ toMonth 12+++dowDescriptor :: Descriptor+dowDescriptor = Descriptor {+    pluralDesc = "days of the week"+  , singularDesc = "day of the week"+  , rangePrefix = ""+  , rangeSuffix = ""+  , rangeJoiner = "through"+  , displayItem = toWeekday+  , specificPrefix = "only on"+  , specificSuffix = ""+  , stepSpecificSuffix = sss+  , listPrefix = "only on"+  , listSuffix = Nothing+  }+  where toWeekday = show . safeIntToWeekDay+        sss n = Just $ toWeekday n ++ " through " ++ show Saturday
+ src/System/Cron/Internal/Describe/Options.hs view
@@ -0,0 +1,85 @@+--------------------------------------------------------------------+-- |+-- Module      : System.Cron.Internal.Describe.Optons+-- Description : Functions for constructing the options that control how+--               cron schedules are described+-- Copyright   : (c) Joseph Canero 2016+-- License     : MIT+--+-- Maintainer: Joseph Canero <jmc41493@gmail.com>+-- Portability: portable+--------------------------------------------------------------------+module System.Cron.Internal.Describe.Options where++-------------------------------------------------------------------------------+import Data.Default.Class+import Data.Semigroup+-------------------------------------------------------------------------------+import System.Cron.Internal.Describe.Types+-------------------------------------------------------------------------------+++-- | Type that holds onto information for constructing options for+-- 'System.Cron.Describe.describe'.+data OptionBuilder = Builder (Options -> Options)+++-- | Return a builder that creates the default options for+-- 'System.Cron.Describe.describe'. The default options are:+-- 'System.Cron.Describe.notVerbose' and 'System.Cron.Describe.twelveHourFormat'.+defaultOpts :: OptionBuilder+defaultOpts = Builder (const def)+++-- | Return a builder that sets the options to use a 24-hour time format.+-- This changes how hours are described. Using the 24-hour format,+-- all hours are returned as their left-padded numeric value (01:00, 22:00, etc)+twentyFourHourFormat :: OptionBuilder+twentyFourHourFormat = Builder (\o -> o {timeFormat = Hour24} )+++-- | Return a builder that sets the options to use a 12-hour time format.+-- This changes how hours are described. Using the 12-hour format,+-- all hours are returned as their left-padded numeric value with their period+-- (01:00 AM, 10:00 PM, etc)+twelveHourFormat :: OptionBuilder+twelveHourFormat = Builder (\o -> o {timeFormat = Hour12} )+++-- | Return a builder that sets the options to be verbose. A verbose description+-- doesn't eliminate unnecessary information. The only caveat being that month+-- information is only ever displayed if it isn't "*".+verbose :: OptionBuilder+verbose = Builder (\o -> o {verbosity = Verbose})+++-- | Return a builder that sets the options to not be verbose. All information+-- about the described cron schedule is returned. The only caveat being that+-- month information is only ever displayed if it isn't "*".+notVerbose :: OptionBuilder+notVerbose = Builder (\o -> o {verbosity = NotVerbose})+++-------------------------------------------------------------------------------+-- Internals+-------------------------------------------------------------------------------+++getOpts :: OptionBuilder -> Options+getOpts (Builder f) = f def+++data Options = Opts {+    timeFormat    :: TimeFormat+  , verbosity     :: Verbosity+  }++instance Default Options where+  def = Opts {timeFormat = Hour12, verbosity = NotVerbose}++instance Semigroup OptionBuilder where+  (Builder f) <> (Builder a) = Builder (a . f)++instance Monoid OptionBuilder where+  mempty = Builder (const def)+  mappend = (<>)
+ src/System/Cron/Internal/Describe/Time.hs view
@@ -0,0 +1,20 @@+module System.Cron.Internal.Describe.Time where++import System.Cron.Internal.Describe.Types++newtype Minute = Minute Int+newtype Hour   = Hour Int++format :: TimeFormat -> Minute -> Hour -> String+format t (Minute m) (Hour h) = leftPad (hour t) ++ ":" ++ leftPad m ++ suffix t+  where leftPad n+          | n < 10    = "0" ++ show n+          | otherwise = show n+        suffix Hour24 = ""+        suffix Hour12+          | h < 12    = " AM"+          | otherwise = " PM"+        hour Hour24 = h+        hour Hour12+          | h > 12 = h `mod` 12+          | otherwise = h
+ src/System/Cron/Internal/Describe/Types.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE RecordWildCards #-}++module System.Cron.Internal.Describe.Types where++import Data.List  (intercalate)+import Data.Maybe (catMaybes)+++data Descriptor = Descriptor {+    pluralDesc         :: String+  , singularDesc       :: String+  , rangePrefix        :: String+  , rangeSuffix        :: String+  , rangeJoiner        :: String+  , displayItem        :: Int -> String+  , specificPrefix     :: String+  , specificSuffix     :: String+  , stepSpecificSuffix :: Int -> Maybe String+  , listPrefix         :: String+  , listSuffix         :: Maybe String+  }+++data Month = January | February | March     | April   | May      | June     |+             July    | August   | September | October | November | December+             deriving (Enum, Bounded, Show)+++safeIntToMonth :: Int -> Month+safeIntToMonth = toEnum . subtract 1 . min 12 . max 1+++data Weekday = Sunday | Monday | Tuesday | Wednesday |+               Thursday | Friday | Saturday | Sunday2+               deriving (Enum, Bounded, Show)+++safeIntToWeekDay :: Int -> Weekday+safeIntToWeekDay n+  | n == 7    = Sunday+  | otherwise = toEnum . min 6 $ max 0 n+++data Verbosity = Verbose | NotVerbose+++data TimeFormat = Hour24 | Hour12+++data DescribedValue = Concrete String+                    | Every String+++instance Show DescribedValue where+  show (Concrete s) = s+  show (Every s)    = s+++data Time = ConcreteTime String+          | Other (Maybe DescribedValue) (Maybe DescribedValue)+++instance Show Time where+  show (ConcreteTime s) = s+  show (Other md1 md2)  = intercalate ", " .+                          map show         $ catMaybes [md1, md2]+++data Description = Desc {+    _time   :: Time+  , _dom    :: Maybe DescribedValue+  , _month  :: Maybe DescribedValue+  , _dow    :: Maybe DescribedValue+  }+++instance Show Description where+  show Desc{..} = intercalate ", " .+                  (:) (show _time) .+                  map show         $ catMaybes [_dom, _dow, _month]
+ src/System/Cron/Internal/Describe/Utils.hs view
@@ -0,0 +1,56 @@+module System.Cron.Internal.Describe.Utils+(+  viewHour+, viewMinute+, viewMinuteRange+, viewHourList+, allWords+, cap+, joinWords+) where++import           Data.Char               (toUpper)+import           Data.List.NonEmpty      (NonEmpty)+import           System.Cron.Types+import           System.Cron.Internal.Describe.Time++viewHour :: HourSpec -> Maybe Hour+viewHour = viewSpecificTime Hour . hourSpec+++viewMinute :: MinuteSpec -> Maybe Minute+viewMinute = viewSpecificTime Minute . minuteSpec+++viewMinuteRange :: MinuteSpec -> Maybe (Minute, Minute)+viewMinuteRange = viewRange . minuteSpec+  where viewRange (Field (RangeField' rf)) = Just (Minute $ rfBegin rf,+                                                   Minute $ rfEnd rf)+        viewRange _                        = Nothing+++viewHourList :: HourSpec -> Maybe (NonEmpty BaseField)+viewHourList = viewList . hourSpec+  where viewList (ListField ne) = Just ne+        viewList _              = Nothing+++viewSpecificTime :: (Int -> a) -> CronField -> Maybe a+viewSpecificTime f (Field (SpecificField' s)) = Just . f $ specificField s+viewSpecificTime _ _                          = Nothing+++allWords :: [String] -> String+allWords = unwords . filter (not . null)+++cap :: String -> String+cap []     = []+cap (x:xs) = toUpper x : xs+++joinWords :: [String] -> String+joinWords [] = []+joinWords [x] = x+joinWords [x, y] = x ++ " and " ++ y+joinWords (x:xs) = x ++ ", " ++ joinWords xs
src/System/Cron/Parser.hs view
@@ -15,7 +15,7 @@ -- > import System.Cron.Parser -- > -- > main :: IO ()--- > main = don+-- > main = do -- >   print $ parseCronSchedule "*/2 * 3 * 4,5,6" -- --------------------------------------------------------------------
src/System/Cron/Types.hs view
@@ -46,6 +46,10 @@     -- * Rendering     , serializeCronSchedule     , serializeCrontab++    -- * Converting to human-readable string+    -- , displayCronField+    -- , Display(..)     ) where  
test/Main.hs view
@@ -7,6 +7,7 @@ ------------------------------------------------------------------------------- import           SpecHelper import qualified System.Test.Cron+import qualified System.Test.Cron.Describe import qualified System.Test.Cron.Parser import qualified System.Test.Cron.Schedule -------------------------------------------------------------------------------@@ -15,6 +16,7 @@ main :: IO () main = defaultMain $ testGroup "cron"   [ System.Test.Cron.tests+  , System.Test.Cron.Describe.tests   , System.Test.Cron.Parser.tests   , System.Test.Cron.Schedule.tests   ]
+ test/System/Test/Cron/Describe.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE OverloadedStrings #-}++module System.Test.Cron.Describe (tests) where++import           SpecHelper+-------------------------------------------------------------------------------+++tests :: TestTree+tests = testGroup "System.Cron.Describe"+  [ describeDisplayCronSchedule+  ]+++-------------------------------------------------------------------------------+describeDisplayCronSchedule :: TestTree+describeDisplayCronSchedule = testGroup "describeCronSchedule"+  [+      testGroup "describes all stars" [+        testCase "verbose" $+          "Every minute, every hour, every day, every day of the week" @=? describeV "* * * * *"++      , testCase "non-verbose" $+          "Every minute" @=? describeNV "* * * * *"+    ]+    , testGroup "describes specific values" [+        testCase "verbose" $+          "At 02:01, on day 3 of the month, every day of the week" @=? describeV "1 2 3 * *"++      , testCase "non-verbose" $+          "At 02:01, on day 3 of the month" @=? describeNV "1 2 3 * *"++      , testCase "12-hour (verbose)" $+          "At 02:01 AM, on day 3 of the month, every day of the week" @=? describe12 "1 2 3 * *"+    ]+    , testGroup "describes list values" [+        testCase "verbose" $+          "Every minute, every hour, on days 3 and 5 of the month, every day of the week" @=? describeV "* * 3,5 * *"++      , testCase "non-verbose" $+          "Every minute, on days 3 and 5 of the month" @=? describeNV "* * 3,5 * *"+    ]+    , testGroup "describes range values" [+        testCase "verbose" $+          "Every minute, every hour, between days 3 and 4 of the month, every day of the week" @=? describeV "* * 3-4 * *"++      , testCase "non-verbose" $+          "Every minute, between days 3 and 4 of the month" @=? describeNV "* * 3-4 * *"+    ]+    , testGroup "describes step values" [+        testCase "verbose" $+          "Every 2 minutes, every hour, every 4 days, between days 2 and 10 of the month, every day of the week" @=? describeV "*/2 * 2-10/4 * *"++      , testCase "non-verbose" $+          "Every 2 minutes, every 4 days, between days 2 and 10 of the month" @=? describeNV "*/2 * 2-10/4 * *"+    ]+    , testGroup "describes other values" [+        testCase "verbose" $+          "Every 2 minutes, minutes 1 through 59 past the hour, every hour, every day, Wednesday through Friday, only in February" @=? describeV "1-59/2 * * 2 3-5"++      , testCase "non-verbose" $+          "Every 2 minutes, minutes 1 through 59 past the hour, Wednesday through Friday, only in February" @=? describeNV "1-59/2 * * 2 3-5"+    ]+    , testGroup "handles Sunday weirdness" [+        testCase "range of values (Sunday at 0)" $+          "Every minute, every hour, every day, Sunday through Friday" @=? describeV "* * * * 0-5"++      , testCase "range of values (Sunday at 7)" $+          "Every minute, every hour, every day, Friday through Sunday" @=? describeV "* * * * 5-7"++      , testCase "range of values" $+          "Every minute, every hour, every day, Sunday through Sunday" @=? describeV "* * * * 0-7"+    ]+    , testGroup "describes complicated times" [+        testCase "describes specific times - twenty four hour" $+          "At 13:01, every day, every day of the week" @=? describeV "1 13 * * *"++      , testCase "describes specific times - twelve hour" $+        "At 01:01 PM, every day, every day of the week" @=? describe12 "1 13 * * *"++      , testCase "describes a range of minutes - twenty four hour" $+          "Every minute between 12:01 and 12:10, every day, every day of the week" @=? describeV "1-10 12 * * *"++      , testCase "describes a range of minutes - twelve hour" $+          "Every minute between 12:01 PM and 12:10 PM, every day, every day of the week" @=? describe12 "1-10 12 * * *"++      , testGroup "describes times for lists of hours" [+          testCase "simple list of hours - twenty four hour" $+            "At 02:01 and 03:01, every day, every day of the week" @=? describeV "1 2,3 * * *"++        , testCase "simple list of hours - twelve hour" $+          "At 02:01 AM and 03:01 AM, every day, every day of the week" @=? describe12 "1 2,3 * * *"++        , testCase "list of hours, and range - twenty four hour" $+            "At 02:01, 03:01 and at 1 minutes past the hour between 04:00 and 13:00, every day, every day of the week" @=?+            describeV "1 2,3,4-13 * * *"++        , testCase "list of hours, and range - twelve hour" $+            "At 02:01 AM, 03:01 AM and at 1 minutes past the hour between 04:00 AM and 01:00 PM, every day, every day of the week" @=?+            describe12 "1 2,3,4-13 * * *"++        , testCase "list of hours, and star" $+            "At 1 minutes past the hour, every hour, every day, every day of the week" @=? describeV "1 2,* * * *"+      ]+    , testGroup "describes other times" [+        testCase "range of minutes, range of hours" $+          "Minutes 10 through 15 past the hour, between 01:00 and 03:00, every day, every day of the week" @=?+          describeV "10-15 1-3 * * *"++      , testCase "range of minutes, every hour" $+          "Minutes 10 through 15 past the hour, every hour, every day, every day of the week" @=?+          describeV "10-15 * * * *"++      , testCase "range of minutes, interval of hours" $+          "Minutes 10 through 15 past the hour, every 3 hours, starting at 03:00, every day, every day of the week" @=?+          describeV "10-15 3/3 * * *"++      , testCase "list of minutes, at an hour" $+          "Every minute, at 03:00, every day, every day of the week" @=?+          describeV "2,* 3 * * *"++      , testCase "step minutes, step hours" $+          "Every 3 minutes, minutes 10 through 15 past the hour, every 5 hours, starting at 10:00, every day, every day of the week" @=?+          describeV "10-15/3 10/5 * * *"+      ]+    ]+  ]+  where+    mkCronSchedule t = let (Right cs) = parseCronSchedule t in cs+    describeNV = describe (twentyFourHourFormat <> notVerbose) . mkCronSchedule+    describeV  = describe (twentyFourHourFormat <> verbose) . mkCronSchedule+    describe12 = describe verbose . mkCronSchedule