packages feed

antikythera-0.1.0.0: src/Control/Antikythera/Periodicity.hs

-- |
-- Module        : Control.Antikythera.Periodicity
-- Copyright     : Gautier DI FOLCO
-- License       : ISC
--
-- Maintainer    : Gautier DI FOLCO <gautier.difolco@gmail.com>
-- Stability     : Stable
-- Portability   : Portable
--
-- Defining a 'Periodicity', how often/when an event occurs
module Control.Antikythera.Periodicity
  ( Periodicity (..),
    nextPeriods,

    -- * Base helpers
    never,
    always,

    -- * Combinators
    (.&&),
    (.||),
    allOf,
    anyOf,
    allOf',
    anyOf',

    -- * 'Unit'-based builders
    at,
    ats,
    every,
    inclusiveRange,

    -- * Absolute builders
    sinceInclusive,
    untilInclusive,

    -- * Reexports
    Max (..),
    Min (..),
  )
where

import Control.Antikythera.Unit.Unit
import Control.Arrow ((&&&))
import Control.Monad (mfilter)
import Data.List (unfoldr)
import qualified Data.List.NonEmpty as NE
import Data.Semigroup

-- | Event recurring period
--
-- Are we at @17:*@?
--
-- > (at 17 hour).includes now
--
-- Next @*:05@
--
-- > (at 5 minute).nextPeriod now
data Periodicity a = Periodicity
  { includes :: a -> Bool,
    nextPeriod :: a -> Maybe a
  }

-- | Get a poentially infinite list of upcoming event
--
-- __Warning:__ may loop infinitelly
nextPeriods :: Periodicity a -> a -> [a]
nextPeriods p = unfoldr (fmap (id &&& id) . p.nextPeriod)

-- * Base helpers

-- | Never happen
never :: Periodicity a
never =
  Periodicity
    { includes = const False,
      nextPeriod = const Nothing
    }

-- | Always happen
--
-- Going from minute to minute:
--
-- > always (addUTCTime $ secondsToNominalDiffTime 60)
always ::
  -- | Increment to next value
  (a -> a) ->
  Periodicity a
always f =
  Periodicity
    { includes = const True,
      nextPeriod = Just . f
    }

-- * Combinators

-- | Intersection of two periods
--
-- Everyday at @15:15@
--
-- > at 15 hour .&& at 15 minute
--
-- __Warning:__ may loop infinitelly when impossible constraints, e.g.
--
-- > at 15 minutes .&& at 15 minute
(.&&) :: (Ord a) => Periodicity a -> Periodicity a -> Periodicity a
x .&& y =
  Periodicity
    { includes = \c -> x.includes c && y.includes c,
      nextPeriod =
        let go c =
              case (x.nextPeriod c, y.nextPeriod c) of
                (Just n, Just m) ->
                  let c' = min m n
                   in if x.includes c' && y.includes c'
                        then Just c'
                        else go c'
                _ -> Nothing
         in go
    }

infixr 3 .&&

-- | Union of two periods
--
-- Everyday at @15:*@ or every hour at @*:15@
--
-- > at 15 hour .|| at 15 minute
(.||) :: (Ord a) => Periodicity a -> Periodicity a -> Periodicity a
x .|| y =
  Periodicity
    { includes = \c -> x.includes c || y.includes c,
      nextPeriod = \c ->
        case (x.nextPeriod c, y.nextPeriod c) of
          (Just n, Just m) -> Just $ min n m
          (Just n, _) -> Just n
          (_, o) -> o
    }

infixr 2 .||

-- | Intersections of all periods
--
-- Same as
--
-- > allOf = foldl1 (.&&)
--
-- __Warning:__ may loop infinitelly when impossible constraints, see '(.&&)'
allOf :: (Ord a) => NE.NonEmpty (Periodicity a) -> Periodicity a
allOf = foldl1 (.&&)

-- | Unions of all periods
--
-- Same as
--
-- > anyOf = foldl1 (.||)
anyOf :: (Ord a) => NE.NonEmpty (Periodicity a) -> Periodicity a
anyOf = foldl1 (.||)

-- | Intersections of all periods
--
-- Same as
--
-- > allOf' = foldl (.&&) . always
--
-- __Warning:__ may loop infinitelly when impossible constraints, see '(.&&)'
allOf' :: (Foldable f, Ord a) => (a -> a) -> f (Periodicity a) -> Periodicity a
allOf' = foldl (.&&) . always

-- | Unions of all periods
--
-- Same as
--
-- > anyOf' = foldl (.||) never
anyOf' :: (Foldable f, Ord a) => f (Periodicity a) -> Periodicity a
anyOf' = foldl (.||) never

-- * 'Unit'-based builders

-- | Happens when the 'Unit' has a value
--
-- Every hour at @*:05@
--
-- > at 5 minute
at :: (Eq i) => i -> Unit i a -> Periodicity a
at n u =
  Periodicity
    { includes = (== n) . u.extract,
      nextPeriod = u.nextUnitWith n
    }

-- | Happens when the 'Unit' has one of the values
--
-- Every hour at @*:05@ and @*:35@
--
-- > ats [5, 35] minute
--
-- Equivalent to
--
-- > at 5 minute .|| at 35 minute
ats :: (Ord i) => NE.NonEmpty i -> Unit i a -> Periodicity a
ats ns u =
  Periodicity
    { includes = (`elem` ns') . u.extract,
      nextPeriod = \x -> u.nextUnitWith (nextCandidate $ u.extract x) x
    }
  where
    ns' = NE.sort ns
    nextCandidate x =
      case NE.dropWhile (<= x) ns' of
        (c : _) -> c
        _ -> NE.head ns'

-- | Happens when the 'Unit' has a value with a modulo
--
-- Every hour at @*:00@, @*:15@, @*:30@, and @*:45@
--
-- > every 15 minute
every :: (Integral i) => i -> Unit i a -> Periodicity a
every n u =
  Periodicity
    { includes = ((== 0) . flip mod n) . u.extract,
      nextPeriod = \x -> u.nextUnitWith (nextCandidate $ u.extract x) x
    }
  where
    nextCandidate x = n * succ (x `div` n)

-- | Happens when the 'Unit' has a value in an inclusive range
--
-- Every hour at @*:05@, @*:06@, @*:07@, @*:08@, @*:09@, @*:10@
--
-- > inclusiveRange (Min 5) (Max 10) minute
inclusiveRange :: (Enum i, Ord i) => Min i -> Max i -> Unit i a -> Periodicity a
inclusiveRange (Min lowerBound) (Max upperBound) u =
  Periodicity
    { includes = (\n -> n >= lowerBound && n <= upperBound) . u.extract,
      nextPeriod = \x -> u.nextUnitWith (nextCandidate $ u.extract x) x
    }
  where
    nextCandidate x =
      if x < upperBound
        then succ x
        else lowerBound

-- * Absolute builders

-- | An event only hapenning /since/
sinceInclusive ::
  (Ord a) =>
  -- | Increment to next value
  (a -> a) ->
  a ->
  Periodicity a
sinceInclusive f startingAt =
  Periodicity
    { includes = (>= startingAt),
      nextPeriod =
        \x ->
          Just $
            if x < startingAt
              then startingAt
              else f x
    }

-- | An event only hapenning /until/
untilInclusive ::
  (Ord a) =>
  -- | Increment to next value
  (a -> a) ->
  a ->
  Periodicity a
untilInclusive f endingAt =
  Periodicity
    { includes = (<= endingAt),
      nextPeriod = mfilter (<= endingAt) . Just . f
    }