packages feed

conditional-restriction-parser-0.1.0.4: src/ConditionalRestriction/Internal/Evaluate.hs

{-# LANGUAGE LambdaCase #-}

-- | Functions to evaluate conditional restrictions.
module ConditionalRestriction.Internal.Evaluate where

import ConditionalRestriction.Parse.AST
  ( ComparisonOp (Eq, Gt, GtEq, Lt, LtEq),
    Condition (..),
    ConditionalRestriction (..),
    Expression (Expression),
    OpeningHours (..),
    RuleSequence (RuleSequence),
    RuleType (Additional, Normal),
    SelectorSequence
      ( TimeSel,
        TwentyFourSeven,
        WeekdaySel,
        WeekdayTime
      ),
    TimeSelector,
    TimeSpan (..),
    Token,
    WeekdayRange (SingleDay, WdayRange),
    WeekdaySelector,
  )
import ConditionalRestriction.Parse.InputData
  ( ID,
    Type (..),
    Value (..),
  )
import ConditionalRestriction.Result (Result (..))
import Data.Hourglass
  ( DateTime,
    TimeOfDay (TimeOfDay),
    WeekDay (Saturday, Sunday),
    getWeekDay,
    timeGetDate,
    timeGetTimeOfDay,
  )
import Data.List (nub)

-- | Plain text error message.
type ErrorMsg = String

-- | The 'result' function takes input data in the form of ('ID', 'Value') and a 'ConditionalRestriction' and returns
-- the result of that 'ConditionalRestriction' when applied to the input data given. If data needed for the evaluation
-- is missing or of the wrong type, it will return a list of error messages and a list of missing data types instead.
--
-- Note that this function will accept incomplete data if it is enough to evaluate the expression, but will always return
-- a complete list of needed data types.
result :: [(ID, Value)] -> ConditionalRestriction -> Result ([ErrorMsg], [(ID, Type)]) (Maybe Token)
result ds (ConditionalRestriction exprs) =
  find_r (\(Expression _ conds) -> all_r (fulfills ds) conds) (reverse exprs) >>= \case
    Nothing -> Ok Nothing
    Just (Expression tok _) -> Ok $ Just tok
  where
    find_r f (x : xs) = case f x of
      Ok True -> Ok $ Just x
      Ok False -> find_r f xs
      Err (msgs, needed) -> case find_r f xs of
        Err (msgs', needed') -> Err (msgs ++ msgs', nub $ needed ++ needed')
        Ok v -> Err (msgs, needed)
    find_r f [] = Ok Nothing

    all_r f (x : xs) = case f x of
      Ok True -> all_r f xs
      Ok False -> False <$ all_r f xs
      Err (Left msg) -> case all_r f xs of
        Ok _ -> Err ([msg], [])
        Err (msgs, neededs) -> Err (msg : msgs, neededs)
      Err (Right needed) -> case all_r f xs of
        Ok _ -> Err ([], [needed])
        Err (msgs, neededs) -> Err (msgs, needed : neededs)
    all_r f [] = Ok True

-- | The 'fulfills' function takes input data in the form of ('ID', 'Value') and a 'Condition' and returns
-- whether that condition is fulfilled. If some data is missing, it will return the missing data 'ID' and 'Type'
-- and if the given data is of the wrong type, it will return an error message.
fulfills :: [(ID, Value)] -> Condition -> Result (Either ErrorMsg (ID, Type)) Bool
fulfills ds (OH oh) = case lookup "time" ds of
  Just (VTime t) -> Ok $ timeIn t oh
  Just _ -> Err . Left $ "Incorrect input type for time"
  Nothing -> case oh of
    OpeningHours [RuleSequence _ TwentyFourSeven (Just o)] -> Ok o
    _ -> Err $ Right ("time", TTime)
fulfills ds (Comparison tok op val) = case lookup tok ds of
  Just (VNum d) -> Ok $ case op of
    Gt -> d > val
    Lt -> d < val
    GtEq -> d >= val
    LtEq -> d <= val
    Eq -> d == val
  Just _ -> Err . Left $ "Incorrect input type for " ++ tok
  Nothing -> Err $ Right (tok, TNum)
fulfills ds (Absolute tok) = case lookup tok ds of
  Just (VBool b) -> Ok b
  Just _ -> Err . Left $ "Incorrect input type for " ++ tok
  Nothing -> Err $ Right (tok, TBool)

-- | The 'timeIn' function returns wheter a 'DateTime' is within given 'OpeningHours'. Unknown values count as not
-- within the opening hours.
timeIn :: DateTime -> OpeningHours -> Bool
timeIn t oh =
  timeInSelector time (ohTimes oh !! fromEnum weekday)
    || timeExtendedInSelector time (ohTimes oh !! fromEnum previous_weekday)
  where
    date = timeGetDate t
    time = timeGetTimeOfDay t
    weekday = getWeekDay date
    previous_weekday = if weekday == minBound then maxBound else pred weekday

ohTimes :: OpeningHours -> [TimeSelector]
ohTimes (OpeningHours rs) = foldr set_time_ranges [[] | _ <- range Sunday Saturday] (reverse rs)
  where
    set_time_ranges (RuleSequence t TwentyFourSeven (Just o)) = map (integrate t o [Span min_time max_time])
    set_time_ranges (RuleSequence t (WeekdaySel wdrs) (Just o)) = mapDays (combineWeekdays wdrs) (integrate t o [Span min_time max_time])
    set_time_ranges (RuleSequence t (TimeSel ts) (Just o)) = map (integrate t o ts)
    set_time_ranges (RuleSequence t (WeekdayTime wdrs ts) (Just o)) = mapDays (combineWeekdays wdrs) (integrate t o ts)
    set_time_ranges _ = id
    min_time = TimeOfDay 00 00 00 0
    max_time = TimeOfDay 23 59 59 999999999
    integrate Normal = override
    integrate Additional = combine
    override True = const
    override False = \_ -> const []
    combine True = foldr addTimespan
    combine False = foldr subtractTimespan

timeInSelector :: TimeOfDay -> TimeSelector -> Bool
timeInSelector t = any match_t
  where
    match_t (Moment t') = t' == t
    match_t (Span t1 t2) = t >= t1 && (t <= t2 || t1 > t2)

timeExtendedInSelector :: TimeOfDay -> TimeSelector -> Bool
timeExtendedInSelector t = any match_t
  where
    match_t (Moment (TimeOfDay h' m' s' n')) = t == TimeOfDay (h' - 24) m' s' n'
    match_t (Span t1 t2) | t1 > t2 = t <= t2
    match_t (Span t1 (TimeOfDay h' m' s' n')) | h' >= 24 = t < TimeOfDay (h' - 24) m' s' n'
    match_t _ = False

addTimespan :: TimeSpan -> TimeSelector -> TimeSelector
addTimespan ts sel = add_timespan (explicitExtended ts) (map explicitExtended sel)
  where
    add_timespan (Moment t) sel@(Moment t' : ts) | t == t' = sel
    add_timespan (Moment t) (Moment t' : ts) = Moment t' : add_timespan (Moment t) ts
    add_timespan (Moment t) sel@(Span t1 t2 : ts) | t >= t1 && t <= t2 = sel
    add_timespan (Moment t) (Span t1 t2 : ts) = Span t1 t2 : add_timespan (Moment t) ts
    add_timespan (Span t1 t2) (Moment t : ts) | t >= t1 && t <= t2 = Span t1 t2 : ts
    add_timespan (Span t1 t2) (Moment t : ts) = Moment t : add_timespan (Span t1 t2) ts
    add_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) | (tb1 <= ta2 && tb2 > ta1) || (ta1 <= tb2 && ta2 > tb1) = -- where overlaps
                                                      Span (min ta1 tb1) (max ta2 tb2) : ts
    add_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) = Span tb1 tb2 : add_timespan (Span ta1 ta2) ts
    add_timespan x [] = [x]

subtractTimespan :: TimeSpan -> TimeSelector -> TimeSelector
subtractTimespan ts sel = subtract_timespan (explicitExtended ts) (map explicitExtended sel)
  where
    subtract_timespan (Moment t) sel@(Moment t' : ts) | t == t' = ts
    subtract_timespan (Moment t) (Moment t' : ts) = Moment t' : subtract_timespan (Moment t) ts
    subtract_timespan (Moment t) sel@(Span t1 t2 : ts) | t >= t1 && t <= t2 = sel -- don't subtract single moments from time spans
    subtract_timespan (Moment t) (Span t1 t2 : ts) = Span t1 t2 : subtract_timespan (Moment t) ts
    subtract_timespan (Span t1 t2) (Moment t : ts) | t >= t1 && t <= t2 = subtract_timespan (Span t1 t2) ts
    subtract_timespan (Span t1 t2) (Moment t : ts) = Moment t : subtract_timespan (Span t1 t2) ts
    subtract_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) | (tb1 < ta2 && tb2 > ta1) || (ta1 < tb2 && ta2 > tb1) =
                                                           Span (min ta1 tb1) (max ta1 tb1) : Span (min ta2 tb2) (max ta2 tb2) : ts
    subtract_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) | ta1 == tb1 && ta2 == tb2 = ts
    subtract_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) | ta1 == tb1 = Span (min ta2 tb2) (max ta2 tb2) : ts
    subtract_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) | ta2 == tb2 = Span (min ta1 tb1) (max ta1 tb1) : ts
    subtract_timespan (Span ta1 ta2) (Span tb1 tb2 : ts) = Span tb1 tb2 : subtract_timespan (Span ta1 ta2) ts
    subtract_timespan x [] = []

explicitExtended :: TimeSpan -> TimeSpan
explicitExtended (Span t1 t2@(TimeOfDay h m s n)) | t1 >= t2 = Span t1 (TimeOfDay (h + 24) m s n)
explicitExtended other = other

mapDays :: [WeekDay] -> (a -> a) -> [a] -> [a]
mapDays days f = zipWith (\d x -> (if d `elem` days then f x else x)) (range Sunday Saturday)

combineWeekdays :: WeekdaySelector -> [WeekDay]
combineWeekdays = nub . combine
  where
    combine (SingleDay day : wdrs) = day : combineWeekdays wdrs
    combine (WdayRange from to : wdrs) = range from to ++ combineWeekdays wdrs
    combine [] = []

range :: (Ord a, Enum a, Bounded a) => a -> a -> [a]
range a b | b < a = [a ..] ++ [minBound .. b]
range a b = [a .. b]