aws-eventbridge-cron-0.1.0.0: src/AWS/EventBridge/DayOfMonth.hs
{-# LANGUAGE OverloadedStrings #-}
module AWS.EventBridge.DayOfMonth where
import Control.Monad (unless)
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Calendar (Day, fromGregorian, gregorianMonthLength)
import Data.Time.Calendar.WeekDate (toWeekDate)
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer
type Parser = Parsec Void Text
data DayOfMonthExprT
= DomAny
| DomAll
| DomAt Int
| DomRange Int Int
| DomStep Int Int
| DomUnion DayOfMonthExprT DayOfMonthExprT
| DomLast
| DomLastOffset Int
| DomClosestWeekday Int
| DomLastWeekday
deriving (Eq, Show)
isValidDay :: Int -> Bool
isValidDay d = 1 <= d && d <= 31
evaluateDayOfMonthT :: Integer -> Int -> DayOfMonthExprT -> Either String [Int]
evaluateDayOfMonthT y m expr = fmap (S.toAscList . S.fromList . limit) (go expr)
where
dim = gregorianMonthLength y m
limit = filter (\d -> d >= 1 && d <= dim)
go DomAny = Right []
go DomAll = Right [1 .. dim]
go (DomAt d)
| isValidDay d = Right [d]
| otherwise = Left ("invalid day: " ++ show d ++ " (expected 1..31)")
go (DomRange a b)
| not (isValidDay a) = Left ("invalid range start: " ++ show a ++ " (expected 1..31)")
| not (isValidDay b) = Left ("invalid range end: " ++ show b ++ " (expected 1..31)")
| a <= b = Right [a .. b]
| otherwise = Left ("invalid range: start " ++ show a ++ " > end " ++ show b)
go (DomStep s k)
| not (isValidDay s) = Left ("invalid step start: " ++ show s ++ " (expected 1..31)")
| k >= 1 = Right (takeWhile (<= 31) [s, s + k ..])
| otherwise = Left ("invalid step: " ++ show k ++ " (expected >= 1)")
go (DomUnion a b) = (++) <$> go a <*> go b
go DomLast = Right [dim]
go (DomLastOffset off)
| off < 1 = Left ("invalid L-n offset: " ++ show off ++ " (expected >= 1)")
| off >= dim =
Left ("invalid L-n offset: " ++ show off ++ " (expected < " ++ show dim ++ ")")
| otherwise = Right [dim - off]
go (DomClosestWeekday d)
| isValidDay d = Right (maybe [] pure (closestWeekday y m d))
| otherwise = Left ("invalid nW day: " ++ show d ++ " (expected 1..31)")
go DomLastWeekday = Right [lastWeekday y m]
parseDayOfMonth :: Parser DayOfMonthExprT
parseDayOfMonth = do
terms <- domTerm `sepBy1` char ','
eof
case terms of
[only] -> pure only
xs -> do
unless (all isNumeric xs) $ fail "comma-separated day expressions must be numeric"
pure (foldUnion xs)
parseDayOfMonthText :: Text -> Either String DayOfMonthExprT
parseDayOfMonthText input =
case parse parseDayOfMonth "day-of-month" (T.strip input) of
Left err -> Left (errorBundlePretty err)
Right expr -> Right expr
domTerm :: Parser DayOfMonthExprT
domTerm = choice
[ DomAny <$ char '?'
, try parseSpecialTerm
, try parseStepTerm
, try parseRangeTerm
, try (DomAt <$> decimal)
, DomAll <$ char '*'
]
parseRangeTerm :: Parser DayOfMonthExprT
parseRangeTerm = do
start <- decimal
_ <- char '-'
DomRange start <$> decimal
parseStepTerm :: Parser DayOfMonthExprT
parseStepTerm = do
start <- decimal
_ <- char '/'
DomStep start <$> decimal
parseSpecialTerm :: Parser DayOfMonthExprT
parseSpecialTerm = choice
[ try (string "LW" >> pure DomLastWeekday)
, try (do
_ <- char 'L'
_ <- char '-'
DomLastOffset <$> decimal)
, char 'L' >> pure DomLast
, try (do
d <- decimal
_ <- char 'W'
pure (DomClosestWeekday d))
]
isNumeric :: DayOfMonthExprT -> Bool
isNumeric DomAll = True
isNumeric (DomAt _) = True
isNumeric (DomRange _ _) = True
isNumeric (DomStep _ _) = True
isNumeric _ = False
foldUnion :: [DayOfMonthExprT] -> DayOfMonthExprT
foldUnion (x:xs) = go x xs
where
go acc [] = acc
go acc (y : ys) = go (DomUnion acc y) ys
foldUnion [] = DomAny
weekday :: Day -> Int
weekday dayValue = let (_, _, w) = toWeekDate dayValue in w
mkDay :: Integer -> Int -> Int -> Day
mkDay = fromGregorian
closestWeekday :: Integer -> Int -> Int -> Maybe Int
closestWeekday y m d =
let dim = gregorianMonthLength y m
in if d < 1 || d > dim
then Nothing
else
case weekday (mkDay y m d) of
1 -> Just d
2 -> Just d
3 -> Just d
4 -> Just d
5 -> Just d
6 -> if d > 1 then Just (d - 1) else Just (d + 2)
7 -> if d < dim then Just (d + 1) else Just (d - 2)
_ -> Just d
lastWeekday :: Integer -> Int -> Int
lastWeekday y m =
let dim = gregorianMonthLength y m
w = weekday (mkDay y m dim)
in case w of
6 -> dim - 1
7 -> dim - 2
_ -> dim