2013-10-07 19:36:42 +00:00
|
|
|
{- scheduled activities
|
|
|
|
-
|
|
|
|
- Copyright 2013 Joey Hess <joey@kitenet.net>
|
|
|
|
-
|
|
|
|
- Licensed under the GNU GPL version 3 or higher.
|
|
|
|
-}
|
|
|
|
|
|
|
|
module Utility.Scheduled (
|
|
|
|
Schedule(..),
|
|
|
|
Recurrance(..),
|
2013-10-08 03:02:47 +00:00
|
|
|
ScheduledTime(..),
|
2013-10-08 21:44:20 +00:00
|
|
|
NextTime(..),
|
2013-10-08 03:02:47 +00:00
|
|
|
nextTime,
|
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,
|
|
|
|
prop_schedule_roundtrips
|
2013-10-07 19:36:42 +00:00
|
|
|
) where
|
|
|
|
|
|
|
|
import Common
|
2013-10-07 21:05:30 +00:00
|
|
|
import Utility.QuickCheck
|
2013-10-07 19:36:42 +00:00
|
|
|
|
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
|
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
|
2013-10-08 15:48:28 +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
|
|
|
-- Days, Weeks, or Months of the year evenly divisible by a number.
|
|
|
|
-- (Divisible Year is years evenly divisible by a number.)
|
|
|
|
| Divisible Int Recurrance
|
2013-10-08 15:48:28 +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
|
2013-10-08 15:48:28 +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
|
|
|
|
|
2013-10-08 03:02:47 +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. -}
|
|
|
|
data NextTime
|
|
|
|
= NextTimeExactly LocalTime
|
|
|
|
| NextTimeWindow LocalTime LocalTime
|
2013-10-08 15:48:28 +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
|
|
|
|
|
|
|
|
{- Calculate the next time that fits a Schedule, based on the
|
|
|
|
- last time it occurred, and the current time. -}
|
|
|
|
calcNextTime :: Schedule -> Maybe LocalTime -> LocalTime -> Maybe NextTime
|
2013-10-08 21:44:20 +00:00
|
|
|
calcNextTime (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
|
2013-10-15 17:05:41 +00:00
|
|
|
findfromtoday anytime = findfrom recurrance afterday today
|
2013-10-08 03:02:47 +00:00
|
|
|
where
|
|
|
|
today = localDay currenttime
|
|
|
|
afterday = sameaslastday || toolatetoday
|
2013-10-13 19:56:07 +00:00
|
|
|
toolatetoday = not anytime && localTimeOfDay currenttime >= nexttime
|
2013-10-15 17:05:41 +00:00
|
|
|
sameaslastday = lastday == Just today
|
|
|
|
lastday = 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))
|
|
|
|
findfrom r afterday day = case r of
|
2013-10-08 03:02:47 +00:00
|
|
|
Daily
|
2013-10-15 17:05:41 +00:00
|
|
|
| afterday -> Just $ exactly $ addDays 1 day
|
|
|
|
| otherwise -> Just $ exactly day
|
|
|
|
Weekly Nothing
|
|
|
|
| afterday -> skip 1
|
|
|
|
| otherwise -> case (wday <$> lastday, wday day) of
|
|
|
|
(Nothing, _) -> Just $ window day (addDays 6 day)
|
|
|
|
(Just old, curr)
|
|
|
|
| old == curr -> Just $ window day (addDays 6 day)
|
|
|
|
| otherwise -> skip 1
|
|
|
|
Monthly Nothing
|
|
|
|
| afterday -> skip 1
|
|
|
|
| maybe True (\old -> mnum day > mday old && mday day >= (mday old `mod` minmday)) lastday ->
|
|
|
|
-- Window only covers current month,
|
|
|
|
-- in case there is a Divisible requirement.
|
|
|
|
Just $ window day (endOfMonth day)
|
|
|
|
| otherwise -> skip 1
|
|
|
|
Yearly Nothing
|
|
|
|
| afterday -> skip 1
|
|
|
|
| maybe True (\old -> ynum day > ynum old && yday day >= (yday old `mod` minyday)) lastday ->
|
|
|
|
Just $ window day (endOfYear day)
|
|
|
|
| otherwise -> skip 1
|
|
|
|
Weekly (Just w)
|
2013-10-08 03:02:47 +00:00
|
|
|
| w < 0 || w > maxwday -> Nothing
|
|
|
|
| w == wday day -> if afterday
|
2013-10-15 17:05:41 +00:00
|
|
|
then Just $ exactly $ addDays 7 day
|
|
|
|
else Just $ exactly day
|
|
|
|
| otherwise -> Just $ exactly $
|
2013-10-08 03:02:47 +00:00
|
|
|
addDays (fromIntegral $ (w - wday day) `mod` 7) day
|
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
|
|
|
|
| m == mday day -> if afterday
|
2013-10-15 17:05:41 +00:00
|
|
|
then skip 1
|
|
|
|
else Just $ exactly day
|
|
|
|
| otherwise -> skip 1
|
|
|
|
Yearly (Just y)
|
2013-10-08 03:02:47 +00:00
|
|
|
| y < 0 || y > maxyday -> Nothing
|
|
|
|
| y == yday day -> if afterday
|
2013-10-15 17:05:41 +00:00
|
|
|
then skip 365
|
|
|
|
else Just $ exactly day
|
|
|
|
| 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
|
|
|
|
Divisible _ r'@(Divisible _ _) -> findfrom r' afterday day
|
2013-10-08 03:58:26 +00:00
|
|
|
where
|
2013-10-15 17:05:41 +00:00
|
|
|
skip n = findfrom r False (addDays n day)
|
2013-10-08 03:58:26 +00:00
|
|
|
handlediv n r' getval mmax
|
|
|
|
| n > 0 && maybe True (n <=) mmax =
|
2013-10-15 17:05:41 +00:00
|
|
|
findfromwhere r' (divisible n . getval) afterday day
|
2013-10-08 03:58:26 +00:00
|
|
|
| otherwise = Nothing
|
2013-10-15 17:05:41 +00:00
|
|
|
findfromwhere r p afterday day
|
|
|
|
| maybe True (p . getday) next = next
|
|
|
|
| otherwise = maybe Nothing (findfromwhere r p True . getday) next
|
2013-10-08 03:02:47 +00:00
|
|
|
where
|
2013-10-15 17:05:41 +00:00
|
|
|
next = findfrom r afterday day
|
|
|
|
getday = localDay . startTime
|
2013-10-08 03:02:47 +00:00
|
|
|
divisible n v = v `rem` n == 0
|
|
|
|
|
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
|
|
|
|
2013-10-15 17:05:41 +00:00
|
|
|
{- Calendar max and mins. -}
|
|
|
|
maxyday :: Int
|
|
|
|
maxyday = 366 -- with leap days
|
|
|
|
minyday :: Int
|
|
|
|
minyday = 365
|
|
|
|
maxwnum :: Int
|
|
|
|
maxwnum = 53 -- some years have more than 52
|
|
|
|
maxmday :: Int
|
|
|
|
maxmday = 31
|
|
|
|
minmday :: Int
|
|
|
|
minmday = 28
|
|
|
|
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
|
2013-10-15 17:05:41 +00:00
|
|
|
withday sd u = do
|
|
|
|
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
|
|
|
|
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
|
2013-11-01 15:44:00 +00:00
|
|
|
h0 h
|
|
|
|
| h == 12 = 0
|
|
|
|
| otherwise = h
|
2013-10-11 04:29:28 +00:00
|
|
|
go :: String -> (Int -> Int) -> Maybe ScheduledTime
|
|
|
|
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
|
|
|
|
arbday = oneof
|
|
|
|
[ 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
|