-- SPDX-FileCopyrightText: 2022 Gregor Kleen ,Sarah Vaupel -- -- SPDX-License-Identifier: AGPL-3.0-or-later {-# OPTIONS_GHC -fno-warn-overlapping-patterns #-} module Utils.Occurrences ( normalizeOccurrences ) where import ClassyPrelude import Model.Types import Utils import Utils.Lens import Control.Monad.Trans.Reader (runReader) import Control.Monad.Trans.Except (ExceptT, throwE, runExceptT) import qualified Data.Set as Set import Data.Time import Data.Time.Calendar.WeekDate normalizeOccurrences :: Occurrences -> Occurrences -- ^ -- -- - Removes unnecessary exceptions -- - Merges overlapping schedules normalizeOccurrences initial | Left new <- runReader (runExceptT go) initial = normalizeOccurrences new | otherwise = initial where go :: ExceptT Occurrences (Reader Occurrences) () -- Find some inconsistency and `throwE` a version without it go = do scheduled <- view _occurrencesScheduled forM_ scheduled $ \case a@ScheduleWeekly{} -> do let merge b@ScheduleWeekly{} | scheduleDayOfWeek a == scheduleDayOfWeek b -- b starts during a , scheduleStart a <= scheduleStart b , scheduleEnd a >= scheduleStart b = Just $ ScheduleWeekly (scheduleDayOfWeek a) (scheduleStart a) ((max `on` scheduleEnd) a b) | scheduleDayOfWeek a == scheduleDayOfWeek b -- b ends during a , scheduleStart a <= scheduleEnd b , scheduleEnd a >= scheduleEnd b = Just $ ScheduleWeekly (scheduleDayOfWeek a) ((min `on` scheduleStart) a b) (scheduleEnd a) | otherwise = Nothing merge _ = Nothing merges <- views _occurrencesScheduled $ mapMaybe (\b -> (b, ) <$> merge b) . Set.toList . Set.delete a case merges of [] -> return () ((b, merged) : _) -> throwE =<< asks (over _occurrencesScheduled $ Set.insert merged . Set.delete b . Set.delete a) exceptions <- view _occurrencesExceptions forM_ exceptions $ \case needle@ExceptNoOccur{..} -> do let LocalTime{..} = exceptTime (_, _, toEnum . (`mod` 7) -> localWeekDay) = toWeekDate localDay needed <- views _occurrencesScheduled . any $ \case ScheduleWeekly{..} -> and [ scheduleDayOfWeek == localWeekDay , scheduleStart <= localTimeOfDay , localTimeOfDay <= scheduleEnd ] unless needed $ throwE =<< asks (over _occurrencesExceptions $ Set.delete needle) needle@ExceptOccur{..} -> do let (_, _, toEnum . (`mod` 7) -> localWeekDay) = toWeekDate exceptDay -- | Does this ExceptNoOccur target within needle? withinNeedle ExceptNoOccur{..} = LocalTime exceptDay exceptStart <= exceptTime && exceptTime <= LocalTime exceptDay exceptEnd withinNeedle _ = False needed <- views _occurrencesScheduled . none $ \case ScheduleWeekly{..} -> and [ scheduleDayOfWeek == localWeekDay , scheduleStart == exceptStart , scheduleEnd == exceptEnd ] unless needed $ throwE =<< asks (over _occurrencesExceptions $ Set.filter (not . withinNeedle) . Set.delete needle)