2013-10-07 19:36:42 +00:00
|
|
|
{- scheduled activities
|
|
|
|
-
|
2015-01-21 16:50:09 +00:00
|
|
|
- Copyright 2013-2014 Joey Hess <id@joeyh.name>
|
2013-10-07 19:36:42 +00:00
|
|
|
-
|
2014-05-10 14:01:27 +00:00
|
|
|
- License: BSD-2-clause
|
2013-10-07 19:36:42 +00:00
|
|
|
-}
|
|
|
|
|
|
|
|
module Utility.Scheduled (
|
|
|
|
Schedule(..),
|
|
|
|
Recurrance(..),
|
2013-10-08 03:02:47 +00:00
|
|
|
ScheduledTime(..),
|
2013-10-08 21:44:20 +00:00
|
|
|
NextTime(..),
|
2014-04-09 04:28:30 +00:00
|
|
|
WeekDay,
|
|
|
|
MonthDay,
|
|
|
|
YearDay,
|
2013-10-08 03:02:47 +00:00
|
|
|
nextTime,
|
Improve handling on monthly/yearly scheduling.
Code was still buggy, it turns out (though the recursion checker caught
it). In the case of (Schedule (Monthly Nothing) AnyTime), where the last
run was on yyyy-12-31, it looped forever.
Also, the handling of (Schedule (Yearly Nothing) AnyTime) was wacky where
the last run was yyyy-12-31. It would suggest a window starting on the 3rd
for the next run (because 31 mod 28 is 3).
I think that originally I was wanted to avoid running on 01-01 if it had
just run on 12-31. But the code didn't accomplish this, and it's not
necessary anyway. This is supposed to calculate the next window meeting the
schedule, and for (Schedule (Monthly Nothing), the window starts at 01-01
and runs through 01-31. If that causes two back-to-back runs, well the next
one will not be until 02-01 at the earliest.
Also, back-to-back runs can be avoided, if desired, by using Divisible 2.
2014-04-12 01:42:43 +00:00
|
|
|
calcNextTime,
|
2014-04-09 04:28:30 +00:00
|
|
|
startTime,
|
2013-10-07 19:36:42 +00:00
|
|
|
fromSchedule,
|
2013-10-11 04:29:28 +00:00
|
|
|
fromScheduledTime,
|
|
|
|
toScheduledTime,
|
|
|
|
fromRecurrance,
|
|
|
|
toRecurrance,
|
2013-10-07 21:05:30 +00:00
|
|
|
toSchedule,
|
|
|
|
parseSchedule,
|
Improve handling on monthly/yearly scheduling.
Code was still buggy, it turns out (though the recursion checker caught
it). In the case of (Schedule (Monthly Nothing) AnyTime), where the last
run was on yyyy-12-31, it looped forever.
Also, the handling of (Schedule (Yearly Nothing) AnyTime) was wacky where
the last run was yyyy-12-31. It would suggest a window starting on the 3rd
for the next run (because 31 mod 28 is 3).
I think that originally I was wanted to avoid running on 01-01 if it had
just run on 12-31. But the code didn't accomplish this, and it's not
necessary anyway. This is supposed to calculate the next window meeting the
schedule, and for (Schedule (Monthly Nothing), the window starts at 01-01
and runs through 01-31. If that causes two back-to-back runs, well the next
one will not be until 02-01 at the earliest.
Also, back-to-back runs can be avoided, if desired, by using Divisible 2.
2014-04-12 01:42:43 +00:00
|
|
|
prop_schedule_roundtrips,
|
2014-04-12 17:29:35 +00:00
|
|
|
prop_past_sane,
|
2013-10-07 19:36:42 +00:00
|
|
|
) where
|
|
|
|
|
2014-04-09 04:28:30 +00:00
|
|
|
import Utility.Data
|
2013-10-07 21:05:30 +00:00
|
|
|
import Utility.QuickCheck
|
2014-04-09 04:28:30 +00:00
|
|
|
import Utility.PartialPrelude
|
|
|
|
import Utility.Misc
|
2013-10-07 19:36:42 +00:00
|
|
|
|
2014-04-09 04:28:30 +00:00
|
|
|
import Data.List
|
2013-10-08 03:02:47 +00:00
|
|
|
import Data.Time.Clock
|
|
|
|
import Data.Time.LocalTime
|
|
|
|
import Data.Time.Calendar
|
|
|
|
import Data.Time.Calendar.WeekDate
|
|
|
|
import Data.Time.Calendar.OrdinalDate
|
|
|
|
import Data.Tuple.Utils
|
2013-10-11 04:29:28 +00:00
|
|
|
import Data.Char
|
2015-05-10 20:19:56 +00:00
|
|
|
import Control.Applicative
|
|
|
|
import Prelude
|
2013-10-08 03:02:47 +00:00
|
|
|
|
2013-10-07 19:36:42 +00:00
|
|
|
{- Some sort of scheduled event. -}
|
2013-10-08 21:44:20 +00:00
|
|
|
data Schedule = Schedule Recurrance ScheduledTime
|
2014-10-09 19:09:26 +00:00
|
|
|
deriving (Eq, Read, Show, Ord)
|
2013-10-07 19:36:42 +00:00
|
|
|
|
|
|
|
data Recurrance
|
|
|
|
= Daily
|
2013-10-15 17:05:41 +00:00
|
|
|
| Weekly (Maybe WeekDay)
|
|
|
|
| Monthly (Maybe MonthDay)
|
|
|
|
| Yearly (Maybe YearDay)
|
2013-10-08 03:02:47 +00:00
|
|
|
| Divisible Int Recurrance
|
2014-04-09 04:28:30 +00:00
|
|
|
-- ^ Days, Weeks, or Months of the year evenly divisible by a number.
|
|
|
|
-- (Divisible Year is years evenly divisible by a number.)
|
2014-10-09 19:09:26 +00:00
|
|
|
deriving (Eq, Read, Show, Ord)
|
2013-10-07 19:36:42 +00:00
|
|
|
|
|
|
|
type WeekDay = Int
|
|
|
|
type MonthDay = Int
|
|
|
|
type YearDay = Int
|
|
|
|
|
2013-10-08 03:02:47 +00:00
|
|
|
data ScheduledTime
|
2013-10-07 19:36:42 +00:00
|
|
|
= AnyTime
|
2013-10-07 22:07:31 +00:00
|
|
|
| SpecificTime Hour Minute
|
2014-10-09 19:09:26 +00:00
|
|
|
deriving (Eq, Read, Show, Ord)
|
2013-10-07 19:36:42 +00:00
|
|
|
|
2013-10-07 22:07:31 +00:00
|
|
|
type Hour = Int
|
|
|
|
type Minute = Int
|
|
|
|
|
2014-04-12 16:58:32 +00:00
|
|
|
-- | Next time a Schedule should take effect. The NextTimeWindow is used
|
|
|
|
-- when a Schedule is allowed to start at some point within the window.
|
2013-10-08 03:02:47 +00:00
|
|
|
data NextTime
|
|
|
|
= NextTimeExactly LocalTime
|
|
|
|
| NextTimeWindow LocalTime LocalTime
|
2014-10-09 19:09:26 +00:00
|
|
|
deriving (Eq, Read, Show)
|
2013-10-08 03:02:47 +00:00
|
|
|
|
2013-10-15 17:05:41 +00:00
|
|
|
startTime :: NextTime -> LocalTime
|
|
|
|
startTime (NextTimeExactly t) = t
|
|
|
|
startTime (NextTimeWindow t _) = t
|
|
|
|
|
2013-10-08 03:02:47 +00:00
|
|
|
nextTime :: Schedule -> Maybe LocalTime -> IO (Maybe NextTime)
|
|
|
|
nextTime schedule lasttime = do
|
|
|
|
now <- getCurrentTime
|
|
|
|
tz <- getTimeZone now
|
|
|
|
return $ calcNextTime schedule lasttime $ utcToLocalTime tz now
|
|
|
|
|
2014-04-12 16:58:32 +00:00
|
|
|
-- | Calculate the next time that fits a Schedule, based on the
|
|
|
|
-- last time it occurred, and the current time.
|
2013-10-08 03:02:47 +00:00
|
|
|
calcNextTime :: Schedule -> Maybe LocalTime -> LocalTime -> Maybe NextTime
|
2014-04-11 22:08:46 +00:00
|
|
|
calcNextTime schedule@(Schedule recurrance scheduledtime) lasttime currenttime
|
2013-10-08 03:02:47 +00:00
|
|
|
| scheduledtime == AnyTime = do
|
2013-10-15 17:05:41 +00:00
|
|
|
next <- findfromtoday True
|
|
|
|
return $ case next of
|
|
|
|
NextTimeWindow _ _ -> next
|
|
|
|
NextTimeExactly t -> window (localDay t) (localDay t)
|
|
|
|
| otherwise = NextTimeExactly . startTime <$> findfromtoday False
|
2013-10-08 03:02:47 +00:00
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
findfromtoday anytime = findfrom recurrance afterday today
|
2013-10-08 03:02:47 +00:00
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
today = localDay currenttime
|
2014-04-12 16:58:32 +00:00
|
|
|
afterday = sameaslastrun || toolatetoday
|
2013-10-13 19:56:07 +00:00
|
|
|
toolatetoday = not anytime && localTimeOfDay currenttime >= nexttime
|
2014-04-12 16:58:32 +00:00
|
|
|
sameaslastrun = lastrun == Just today
|
|
|
|
lastrun = localDay <$> lasttime
|
2013-10-08 03:02:47 +00:00
|
|
|
nexttime = case scheduledtime of
|
|
|
|
AnyTime -> TimeOfDay 0 0 0
|
|
|
|
SpecificTime h m -> TimeOfDay h m 0
|
2013-10-15 17:05:41 +00:00
|
|
|
exactly d = NextTimeExactly $ LocalTime d nexttime
|
|
|
|
window startd endd = NextTimeWindow
|
|
|
|
(LocalTime startd nexttime)
|
|
|
|
(LocalTime endd (TimeOfDay 23 59 0))
|
2014-04-11 22:08:46 +00:00
|
|
|
findfrom r afterday candidate
|
|
|
|
| ynum candidate > (ynum (localDay currenttime)) + 100 =
|
|
|
|
-- avoid possible infinite recusion
|
|
|
|
error $ "bug: calcNextTime did not find a time within 100 years to run " ++
|
|
|
|
show (schedule, lasttime, currenttime)
|
|
|
|
| otherwise = findfromChecked r afterday candidate
|
|
|
|
findfromChecked r afterday candidate = case r of
|
2013-10-08 03:02:47 +00:00
|
|
|
Daily
|
2014-04-11 18:38:23 +00:00
|
|
|
| afterday -> Just $ exactly $ addDays 1 candidate
|
|
|
|
| otherwise -> Just $ exactly candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
Weekly Nothing
|
|
|
|
| afterday -> skip 1
|
2014-04-12 16:58:32 +00:00
|
|
|
| otherwise -> case (wday <$> lastrun, wday candidate) of
|
2014-04-11 18:38:23 +00:00
|
|
|
(Nothing, _) -> Just $ window candidate (addDays 6 candidate)
|
2013-10-15 17:05:41 +00:00
|
|
|
(Just old, curr)
|
2014-04-11 18:38:23 +00:00
|
|
|
| old == curr -> Just $ window candidate (addDays 6 candidate)
|
2013-10-15 17:05:41 +00:00
|
|
|
| otherwise -> skip 1
|
|
|
|
Monthly Nothing
|
|
|
|
| afterday -> skip 1
|
2014-04-12 16:58:32 +00:00
|
|
|
| maybe True (candidate `oneMonthPast`) lastrun ->
|
2014-04-11 18:38:23 +00:00
|
|
|
Just $ window candidate (endOfMonth candidate)
|
2013-10-15 17:05:41 +00:00
|
|
|
| otherwise -> skip 1
|
|
|
|
Yearly Nothing
|
|
|
|
| afterday -> skip 1
|
2014-04-12 16:58:32 +00:00
|
|
|
| maybe True (candidate `oneYearPast`) lastrun ->
|
2014-04-11 18:38:23 +00:00
|
|
|
Just $ window candidate (endOfYear candidate)
|
2013-10-15 17:05:41 +00:00
|
|
|
| otherwise -> skip 1
|
|
|
|
Weekly (Just w)
|
2013-10-08 03:02:47 +00:00
|
|
|
| w < 0 || w > maxwday -> Nothing
|
2014-04-11 18:38:23 +00:00
|
|
|
| w == wday candidate -> if afterday
|
|
|
|
then Just $ exactly $ addDays 7 candidate
|
|
|
|
else Just $ exactly candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
| otherwise -> Just $ exactly $
|
2014-04-11 18:38:23 +00:00
|
|
|
addDays (fromIntegral $ (w - wday candidate) `mod` 7) candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
Monthly (Just m)
|
2013-10-08 03:02:47 +00:00
|
|
|
| m < 0 || m > maxmday -> Nothing
|
|
|
|
-- TODO can be done more efficiently than recursing
|
2014-04-11 18:38:23 +00:00
|
|
|
| m == mday candidate -> if afterday
|
2013-10-15 17:05:41 +00:00
|
|
|
then skip 1
|
2014-04-11 18:38:23 +00:00
|
|
|
else Just $ exactly candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
| otherwise -> skip 1
|
|
|
|
Yearly (Just y)
|
2013-10-08 03:02:47 +00:00
|
|
|
| y < 0 || y > maxyday -> Nothing
|
2014-04-11 18:38:23 +00:00
|
|
|
| y == yday candidate -> if afterday
|
2013-10-15 17:05:41 +00:00
|
|
|
then skip 365
|
2014-04-11 18:38:23 +00:00
|
|
|
else Just $ exactly candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
| otherwise -> skip 1
|
2013-10-08 03:58:26 +00:00
|
|
|
Divisible n r'@Daily -> handlediv n r' yday (Just maxyday)
|
|
|
|
Divisible n r'@(Weekly _) -> handlediv n r' wnum (Just maxwnum)
|
|
|
|
Divisible n r'@(Monthly _) -> handlediv n r' mnum (Just maxmnum)
|
2013-10-15 17:05:41 +00:00
|
|
|
Divisible n r'@(Yearly _) -> handlediv n r' ynum Nothing
|
2014-04-11 18:38:23 +00:00
|
|
|
Divisible _ r'@(Divisible _ _) -> findfrom r' afterday candidate
|
2013-10-08 03:58:26 +00:00
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
skip n = findfrom r False (addDays n candidate)
|
|
|
|
handlediv n r' getval mmax
|
2013-10-08 03:58:26 +00:00
|
|
|
| n > 0 && maybe True (n <=) mmax =
|
2014-04-11 18:38:23 +00:00
|
|
|
findfromwhere r' (divisible n . getval) afterday candidate
|
2013-10-08 03:58:26 +00:00
|
|
|
| otherwise = Nothing
|
2014-04-11 18:38:23 +00:00
|
|
|
findfromwhere r p afterday candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
| maybe True (p . getday) next = next
|
|
|
|
| otherwise = maybe Nothing (findfromwhere r p True . getday) next
|
2013-10-08 03:02:47 +00:00
|
|
|
where
|
2014-04-11 18:38:23 +00:00
|
|
|
next = findfrom r afterday candidate
|
2013-10-15 17:05:41 +00:00
|
|
|
getday = localDay . startTime
|
2013-10-08 03:02:47 +00:00
|
|
|
divisible n v = v `rem` n == 0
|
|
|
|
|
2014-04-12 16:58:32 +00:00
|
|
|
-- Check if the new Day occurs one month or more past the old Day.
|
|
|
|
oneMonthPast :: Day -> Day -> Bool
|
2014-04-12 17:29:35 +00:00
|
|
|
new `oneMonthPast` old = fromGregorian y (m+1) d <= new
|
|
|
|
where
|
|
|
|
(y,m,d) = toGregorian old
|
2014-04-12 16:58:32 +00:00
|
|
|
|
|
|
|
-- Check if the new Day occurs one year or more past the old Day.
|
|
|
|
oneYearPast :: Day -> Day -> Bool
|
2014-04-12 17:29:35 +00:00
|
|
|
new `oneYearPast` old = fromGregorian (y+1) m d <= new
|
|
|
|
where
|
|
|
|
(y,m,d) = toGregorian old
|
2014-04-12 16:58:32 +00:00
|
|
|
|
2013-10-15 17:05:41 +00:00
|
|
|
endOfMonth :: Day -> Day
|
|
|
|
endOfMonth day =
|
|
|
|
let (y,m,_d) = toGregorian day
|
|
|
|
in fromGregorian y m (gregorianMonthLength y m)
|
|
|
|
|
|
|
|
endOfYear :: Day -> Day
|
|
|
|
endOfYear day =
|
|
|
|
let (y,_m,_d) = toGregorian day
|
|
|
|
in endOfMonth (fromGregorian y maxmnum 1)
|
|
|
|
|
|
|
|
-- extracting various quantities from a Day
|
|
|
|
wday :: Day -> Int
|
|
|
|
wday = thd3 . toWeekDate
|
|
|
|
wnum :: Day -> Int
|
|
|
|
wnum = snd3 . toWeekDate
|
|
|
|
mday :: Day -> Int
|
|
|
|
mday = thd3 . toGregorian
|
|
|
|
mnum :: Day -> Int
|
|
|
|
mnum = snd3 . toGregorian
|
|
|
|
yday :: Day -> Int
|
|
|
|
yday = snd . toOrdinalDate
|
|
|
|
ynum :: Day -> Int
|
|
|
|
ynum = fromIntegral . fst . toOrdinalDate
|
2013-10-08 03:02:47 +00:00
|
|
|
|
2014-04-12 16:58:32 +00:00
|
|
|
-- Calendar max values.
|
2013-10-15 17:05:41 +00:00
|
|
|
maxyday :: Int
|
|
|
|
maxyday = 366 -- with leap days
|
|
|
|
maxwnum :: Int
|
|
|
|
maxwnum = 53 -- some years have more than 52
|
|
|
|
maxmday :: Int
|
|
|
|
maxmday = 31
|
|
|
|
maxmnum :: Int
|
|
|
|
maxmnum = 12
|
|
|
|
maxwday :: Int
|
|
|
|
maxwday = 7
|
2013-10-08 03:02:47 +00:00
|
|
|
|
2013-10-07 19:36:42 +00:00
|
|
|
fromRecurrance :: Recurrance -> String
|
2013-10-08 03:02:47 +00:00
|
|
|
fromRecurrance (Divisible n r) =
|
2013-10-07 22:07:31 +00:00
|
|
|
fromRecurrance' (++ "s divisible by " ++ show n) r
|
2013-10-07 19:36:42 +00:00
|
|
|
fromRecurrance r = fromRecurrance' ("every " ++) r
|
|
|
|
|
|
|
|
fromRecurrance' :: (String -> String) -> Recurrance -> String
|
|
|
|
fromRecurrance' a Daily = a "day"
|
|
|
|
fromRecurrance' a (Weekly n) = onday n (a "week")
|
|
|
|
fromRecurrance' a (Monthly n) = onday n (a "month")
|
|
|
|
fromRecurrance' a (Yearly n) = onday n (a "year")
|
2013-10-08 03:02:47 +00:00
|
|
|
fromRecurrance' a (Divisible _n r) = fromRecurrance' a r -- not used
|
2013-10-07 19:36:42 +00:00
|
|
|
|
2013-10-15 17:05:41 +00:00
|
|
|
onday :: Maybe Int -> String -> String
|
|
|
|
onday (Just n) s = "on day " ++ show n ++ " of " ++ s
|
|
|
|
onday Nothing s = s
|
2013-10-07 19:36:42 +00:00
|
|
|
|
|
|
|
toRecurrance :: String -> Maybe Recurrance
|
|
|
|
toRecurrance s = case words s of
|
2013-10-07 22:07:31 +00:00
|
|
|
("every":"day":[]) -> Just Daily
|
2013-10-15 17:05:41 +00:00
|
|
|
("on":"day":sd:"of":"every":something:[]) -> withday sd something
|
|
|
|
("every":something:[]) -> noday something
|
2013-10-07 22:07:31 +00:00
|
|
|
("days":"divisible":"by":sn:[]) ->
|
2013-10-08 03:02:47 +00:00
|
|
|
Divisible <$> getdivisor sn <*> pure Daily
|
2013-10-07 22:07:31 +00:00
|
|
|
("on":"day":sd:"of":something:"divisible":"by":sn:[]) ->
|
2013-10-08 03:02:47 +00:00
|
|
|
Divisible
|
2013-10-07 22:07:31 +00:00
|
|
|
<$> getdivisor sn
|
2013-10-15 17:05:41 +00:00
|
|
|
<*> withday sd something
|
|
|
|
("every":something:"divisible":"by":sn:[]) ->
|
|
|
|
Divisible
|
|
|
|
<$> getdivisor sn
|
|
|
|
<*> noday something
|
2013-10-16 20:10:56 +00:00
|
|
|
(something:"divisible":"by":sn:[]) ->
|
|
|
|
Divisible
|
|
|
|
<$> getdivisor sn
|
|
|
|
<*> noday something
|
2013-10-07 19:36:42 +00:00
|
|
|
_ -> Nothing
|
|
|
|
where
|
2013-10-15 17:05:41 +00:00
|
|
|
constructor "week" = Just Weekly
|
|
|
|
constructor "month" = Just Monthly
|
|
|
|
constructor "year" = Just Yearly
|
|
|
|
constructor u
|
|
|
|
| "s" `isSuffixOf` u = constructor $ reverse $ drop 1 $ reverse u
|
2013-10-07 19:36:42 +00:00
|
|
|
| otherwise = Nothing
|
2014-10-09 18:53:13 +00:00
|
|
|
withday sd u = do
|
2013-10-15 17:05:41 +00:00
|
|
|
c <- constructor u
|
|
|
|
d <- readish sd
|
|
|
|
Just $ c (Just d)
|
|
|
|
noday u = do
|
|
|
|
c <- constructor u
|
|
|
|
Just $ c Nothing
|
2013-10-07 22:07:31 +00:00
|
|
|
getdivisor sn = do
|
|
|
|
n <- readish sn
|
|
|
|
if n > 0
|
|
|
|
then Just n
|
|
|
|
else Nothing
|
2013-10-07 19:36:42 +00:00
|
|
|
|
2013-10-08 03:02:47 +00:00
|
|
|
fromScheduledTime :: ScheduledTime -> String
|
|
|
|
fromScheduledTime AnyTime = "any time"
|
2013-10-11 04:29:28 +00:00
|
|
|
fromScheduledTime (SpecificTime h m) =
|
|
|
|
show h' ++ (if m > 0 then ":" ++ pad 2 (show m) else "") ++ " " ++ ampm
|
2013-10-10 18:12:24 +00:00
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
pad n s = take (n - length s) (repeat '0') ++ s
|
2013-10-11 04:29:28 +00:00
|
|
|
(h', ampm)
|
|
|
|
| h == 0 = (12, "AM")
|
|
|
|
| h < 12 = (h, "AM")
|
|
|
|
| h == 12 = (h, "PM")
|
|
|
|
| otherwise = (h - 12, "PM")
|
2013-10-07 19:36:42 +00:00
|
|
|
|
2013-10-08 03:02:47 +00:00
|
|
|
toScheduledTime :: String -> Maybe ScheduledTime
|
|
|
|
toScheduledTime "any time" = Just AnyTime
|
2013-10-11 04:29:28 +00:00
|
|
|
toScheduledTime v = case words v of
|
|
|
|
(s:ampm:[])
|
|
|
|
| map toUpper ampm == "AM" ->
|
2013-11-01 15:44:00 +00:00
|
|
|
go s h0
|
2013-10-11 04:29:28 +00:00
|
|
|
| map toUpper ampm == "PM" ->
|
2013-11-01 15:44:00 +00:00
|
|
|
go s (\h -> (h0 h) + 12)
|
2013-10-11 04:29:28 +00:00
|
|
|
| otherwise -> Nothing
|
|
|
|
(s:[]) -> go s id
|
|
|
|
_ -> Nothing
|
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
h0 h
|
2013-11-01 15:44:00 +00:00
|
|
|
| h == 12 = 0
|
|
|
|
| otherwise = h
|
2014-10-09 18:53:13 +00:00
|
|
|
go :: String -> (Int -> Int) -> Maybe ScheduledTime
|
2013-10-11 04:29:28 +00:00
|
|
|
go s adjust =
|
|
|
|
let (h, m) = separate (== ':') s
|
|
|
|
in SpecificTime
|
|
|
|
<$> (adjust <$> readish h)
|
|
|
|
<*> if null m then Just 0 else readish m
|
2013-10-07 19:36:42 +00:00
|
|
|
|
|
|
|
fromSchedule :: Schedule -> String
|
2013-10-08 21:44:20 +00:00
|
|
|
fromSchedule (Schedule recurrance scheduledtime) = unwords
|
2013-10-07 19:36:42 +00:00
|
|
|
[ fromRecurrance recurrance
|
|
|
|
, "at"
|
2013-10-08 03:02:47 +00:00
|
|
|
, fromScheduledTime scheduledtime
|
2013-10-07 19:36:42 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
toSchedule :: String -> Maybe Schedule
|
2013-10-07 21:05:30 +00:00
|
|
|
toSchedule = eitherToMaybe . parseSchedule
|
|
|
|
|
|
|
|
parseSchedule :: String -> Either String Schedule
|
|
|
|
parseSchedule s = do
|
|
|
|
r <- maybe (Left $ "bad recurrance: " ++ recurrance) Right
|
|
|
|
(toRecurrance recurrance)
|
2013-10-08 03:02:47 +00:00
|
|
|
t <- maybe (Left $ "bad time of day: " ++ scheduledtime) Right
|
|
|
|
(toScheduledTime scheduledtime)
|
2013-10-08 21:44:20 +00:00
|
|
|
Right $ Schedule r t
|
2013-10-07 19:36:42 +00:00
|
|
|
where
|
2013-10-08 21:44:20 +00:00
|
|
|
(rws, tws) = separate (== "at") (words s)
|
2013-10-07 21:05:30 +00:00
|
|
|
recurrance = unwords rws
|
2013-10-08 03:02:47 +00:00
|
|
|
scheduledtime = unwords tws
|
2013-10-07 21:05:30 +00:00
|
|
|
|
|
|
|
instance Arbitrary Schedule where
|
2013-10-08 21:44:20 +00:00
|
|
|
arbitrary = Schedule <$> arbitrary <*> arbitrary
|
2013-10-07 21:05:30 +00:00
|
|
|
|
2013-10-08 03:02:47 +00:00
|
|
|
instance Arbitrary ScheduledTime where
|
2013-10-07 21:05:30 +00:00
|
|
|
arbitrary = oneof
|
|
|
|
[ pure AnyTime
|
2013-10-07 22:07:31 +00:00
|
|
|
, SpecificTime
|
2013-11-01 15:54:26 +00:00
|
|
|
<$> choose (0, 23)
|
|
|
|
<*> choose (1, 59)
|
2013-10-07 21:05:30 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
instance Arbitrary Recurrance where
|
|
|
|
arbitrary = oneof
|
|
|
|
[ pure Daily
|
2013-10-15 17:05:41 +00:00
|
|
|
, Weekly <$> arbday
|
|
|
|
, Monthly <$> arbday
|
|
|
|
, Yearly <$> arbday
|
2013-10-08 03:02:47 +00:00
|
|
|
, Divisible
|
2013-10-07 21:05:30 +00:00
|
|
|
<$> positive arbitrary
|
|
|
|
<*> oneof -- no nested Divisibles
|
|
|
|
[ pure Daily
|
2013-10-15 17:05:41 +00:00
|
|
|
, Weekly <$> arbday
|
|
|
|
, Monthly <$> arbday
|
|
|
|
, Yearly <$> arbday
|
2013-10-07 21:05:30 +00:00
|
|
|
]
|
|
|
|
]
|
2013-10-15 17:05:41 +00:00
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
arbday = oneof
|
2013-10-15 17:05:41 +00:00
|
|
|
[ Just <$> nonNegative arbitrary
|
|
|
|
, pure Nothing
|
|
|
|
]
|
2013-10-07 21:05:30 +00:00
|
|
|
|
|
|
|
prop_schedule_roundtrips :: Schedule -> Bool
|
2013-10-08 21:44:20 +00:00
|
|
|
prop_schedule_roundtrips s = toSchedule (fromSchedule s) == Just s
|
2014-04-12 17:29:35 +00:00
|
|
|
|
|
|
|
prop_past_sane :: Bool
|
|
|
|
prop_past_sane = and
|
|
|
|
[ all (checksout oneMonthPast) (mplus1 ++ yplus1)
|
|
|
|
, all (not . (checksout oneMonthPast)) (map swap (mplus1 ++ yplus1))
|
|
|
|
, all (checksout oneYearPast) yplus1
|
|
|
|
, all (not . (checksout oneYearPast)) (map swap yplus1)
|
|
|
|
]
|
|
|
|
where
|
|
|
|
mplus1 = -- new date old date, 1+ months before it
|
|
|
|
[ (fromGregorian 2014 01 15, fromGregorian 2013 12 15)
|
|
|
|
, (fromGregorian 2014 01 15, fromGregorian 2013 02 15)
|
|
|
|
, (fromGregorian 2014 02 15, fromGregorian 2013 01 15)
|
|
|
|
, (fromGregorian 2014 03 01, fromGregorian 2013 01 15)
|
|
|
|
, (fromGregorian 2014 03 01, fromGregorian 2013 12 15)
|
|
|
|
, (fromGregorian 2015 01 01, fromGregorian 2010 01 01)
|
|
|
|
]
|
|
|
|
yplus1 = -- new date old date, 1+ years before it
|
|
|
|
[ (fromGregorian 2014 01 15, fromGregorian 2012 01 16)
|
|
|
|
, (fromGregorian 2014 01 15, fromGregorian 2013 01 14)
|
|
|
|
, (fromGregorian 2022 12 31, fromGregorian 2000 01 01)
|
|
|
|
]
|
|
|
|
checksout cmp (new, old) = new `cmp` old
|
|
|
|
swap (a,b) = (b,a)
|