interval-algebra-0.5.0: src/IntervalAlgebra/IntervalUtilities.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE FlexibleInstances #-}
{-|
Module : Interval Algebra Utilities
Description : Functions for operating on containers of Intervals.
Copyright : (c) NoviSci, Inc 2020
License : BSD3
Maintainer : bsaul@novisci.com
Stability : experimental
-}
module IntervalAlgebra.IntervalUtilities (
combineIntervals
, combineIntervals'
, gaps
, gaps'
, durations
, clip
, relations
, relations'
, gapsWithin
, nothingIf
, nothingIfNone
, nothingIfAny
, nothingIfAll
-- * Filtering functions
, filterBefore
, filterMeets
, filterOverlaps
, filterFinishedBy
, filterContains
, filterStarts
, filterEquals
, filterStartedBy
, filterDuring
, filterFinishes
, filterOverlappedBy
, filterMetBy
, filterAfter
, filterDisjoint
, filterNotDisjoint
, filterWithin
) where
import GHC.Base
( otherwise, ($), (.), (<*>), seq, not
, Semigroup((<>))
, Functor(fmap)
, Applicative(pure)
, Int, Bool)
import GHC.Num ()
import Data.Tuple ( fst )
import Data.Foldable ( Foldable(null, foldl', toList), all, any )
import Data.Monoid ( (<>), Monoid(mempty) )
import IntervalAlgebra
( Interval, Intervallic(..), IntervalAlgebraic(..)
, IntervalCombinable(..), IntervalSizeable(..)
, IntervalRelation(..)
, ComparativePredicateOf)
import Data.Maybe (mapMaybe, catMaybes, fromMaybe, Maybe(..))
import Data.List ( (++), map, head, init, last, tail )
import Witherable ( Filterable(filter) )
-------------------------------------------------
-- Unexported utilties used in functions below --
-------------------------------------------------
intInt :: Int -> Int -> Interval Int
intInt = unsafeInterval
-- Fold over consecutive pairs of foldable structure and collect the results in
-- a monoidal structure.
foldlAccume :: (Foldable f, Applicative m, Monoid (m a))=>
(b -> b -> a) -- ^ @f@: a function to apply to consecutive elements of @f b@
-> f b
-> m a
foldlAccume f x = fst $ foldl' (applyAccume f) (mempty, Nothing) x
-- Apply a function and accumulate the results in a monoidal structure.
applyAccume :: (Monoid (f a), Applicative f) =>
(b -> b -> a) -- ^ @f@: a function combining two @b@s to get an @a@
-> (f a, Maybe b) -- ^ a pair (accumulating monoid for @b@s, optional @a@)
-> b -- ^ this will be the second argument to @f@
-> (f a, Maybe b)
applyAccume f (fs, Nothing) x = (fs, Just x)
applyAccume f (fs, Just x) y = (fs <> pure (f x y), Just y)
-- Lifts a list to a foldable, applicative monoid
liftListToFoldable :: (Applicative f
, Monoid (f a)
, Foldable f) =>
[a] -> f a
liftListToFoldable = foldl' (\x y -> x <> pure y) mempty
-- Box to avoid overlapping instances
newtype Box a = Box { unBox :: [a] }
-- Defines how a Box of Intervals are combined. Specifically, the last element of
-- x and first element of y are combined by '<+>'.
instance (IntervalCombinable a) => Semigroup (Box (Interval a)) where
Box x <> Box y
| null x = Box y
| null y = Box x
| otherwise = Box $ init x ++ (lx <+> fy) ++ tail y
where lx = last x
fy = head y
-------------------------------------------------
-- | Returns a container of intervals where any intervals that meet or share support
-- are combined into one interval. *To work properly, the input should
-- be sorted*. See 'combineIntervals'' for a version that works only on lists.
--
-- >>> combineIntervals [intInt 0 10, intInt 2 7, intInt 10 12, intInt 13 15]
-- [(0, 12),(13, 15)]
combineIntervals :: (IntervalCombinable a
, Applicative f
, Monoid (f (Interval a))
, Foldable f) =>
f (Interval a) ->
f (Interval a)
combineIntervals x = liftListToFoldable (combineIntervals' $ toList x)
-- | Returns a list of intervals where any intervals that meet or share support
-- are combined into one interval. *To work properly, the input list should
-- be sorted*.
--
-- >>> combineIntervals' [intInt 0 10, intInt 2 7, intInt 10 12, intInt 13 15]
-- [(0, 12),(13, 15)]
combineIntervals' :: (IntervalCombinable a) => [Interval a] -> [Interval a]
combineIntervals' l = unBox $ foldl' (<>) (Box []) (map (\z -> Box [z]) l)
-- | Returns a (possibly empty) container of intervals consisting of the gaps
-- between intervals in the input. *To work properly, the input should be
-- sorted*. See 'gaps'' for a version that returns a list.
--
-- >>> gaps [intInt 1 5, intInt 8 12, intInt 11 14]
-- [(5, 8)]
gaps :: (IntervalCombinable a
, Applicative f
, Monoid (f (Interval a))
, Foldable f) =>
f (Interval a) ->
f (Interval a)
gaps x = liftListToFoldable (gaps' x)
-- | Returns a (possibly empty) list of intervals consisting of the gaps between
-- intervals in the input container. *To work properly, the input should be
-- sorted*. This version outputs a list. See 'gaps' for a version that lifts
-- the result to same input structure @f@.
gaps' :: (IntervalCombinable a
, Applicative f
, Monoid (f (Interval a))
, Foldable f) =>
f (Interval a) ->
[Interval a]
gaps' x = catMaybes (foldlAccume (><) x)
-- | Returns the 'duration' of each 'Interval' in the 'Functor' @f@.
--
-- >>> durations [intInt 1 10, intInt 2 12, intInt 5 6]
-- [9,10,1]
durations :: (Functor f, IntervalSizeable a b)=>
f (Interval a)
-> f b
durations = fmap duration
-- | In the case that x y are not disjoint, clips y to the extent of x.
--
-- >>> clip (intInt 0 5) (intInt 3 6)
-- Just (3, 5)
--
-- >>> clip (intInt 0 3) (intInt 4 6)
-- Nothing
clip :: (IntervalAlgebraic a, IntervalSizeable a b)=>
Interval a
-> Interval a
-> Maybe (Interval a)
clip x y
| overlaps x y = Just $ enderval (diff (end x) (begin y)) (end x)
| overlappedBy x y = Just $ beginerval (diff (end y) (begin x)) (begin x)
| jx x y = Just x
| jy x y = Just y
| disjoint x y = Nothing
where jy = equals <|> startedBy <|> contains <|> finishedBy
jx = starts <|> during <|> finishes
-- | Returns a list of the 'IntervalRelation' between each consecutive pair
-- of intervals. This the specialized form of 'relations'' which can return
-- any 'Applicative', 'Monoid' structure.
--
-- >>> relations [intInt 0 1, intInt 1 2]
-- [Meets]
relations :: (IntervalAlgebraic a, Foldable f)=>
f (Interval a)
-> [IntervalRelation a]
relations = relations'
-- | A generic form of 'relations' which can output any 'Applicative' and
-- 'Monoid' structure.
-- >>> (relations' [intInt 0 1, intInt 1 2]) :: [IntervalRelation Int]
-- [Meets]
relations' :: ( IntervalAlgebraic a
, Foldable f
, Applicative m
, Monoid (m (IntervalRelation a)) )=>
f (Interval a)
-> m (IntervalRelation a)
relations' = foldlAccume relate
-- | Applies 'gaps' to all the non-disjoint intervals in @x@ that are *not* disjoint
-- from @i@. Intervals that 'overlaps' or are 'overlappedBy' @i@ are 'clip'ped
-- to @i@, so that all the intervals are 'within' @i@. If there are no gaps, then
-- 'Nothing' is returned.
--
-- >>> gapsWithin (intInt 1 10) [intInt 0 5, intInt 7 9, intInt 12 15]
-- Just [(5, 7),(9, 10)]
--
gapsWithin :: ( Applicative f
, Foldable f
, Monoid (f (Interval a))
, IntervalSizeable a b
, IntervalCombinable a
, Filterable f
, IntervalAlgebraic a)=>
Interval a -- ^ i
-> f (Interval a) -- ^ x
-> Maybe (f (Interval a))
gapsWithin i x
| null ivs = Nothing
| otherwise = Just $ gaps $ pure s <> ivs <> pure e
where s = enderval 0 (begin i)
e = beginerval 0 (end i)
nd = toList (filterNotDisjoint i x)
ivs = liftListToFoldable (mapMaybe (clip i) nd)
-- | Given a predicate combinator, a predicate, and list of intervals, returns
-- the input unchanged if the predicate combinator is @True@. Otherwise, returns
-- an empty list. See 'emptyIfAny' and 'emptyIfNone' for examples.
nothingIf :: (Monoid (f (Interval a)), Filterable f, IntervalAlgebraic a)=>
((Interval a -> Bool) -> f (Interval a) -> Bool) -- ^ e.g. 'any' or 'all'
-> (Interval a -> Bool) -- ^ predicate to apply to each element of input list
-> f (Interval a)
-> Maybe (f (Interval a))
nothingIf quantifier predicate x = if quantifier predicate x then Nothing else Just x
-- | Returns the empty monoid structure if *none* of the element of input satisfy
-- the predicate condition.
--
-- For example, the following returns the empty list because none of the intervals
-- in the input list 'starts' (3, 5).
--
-- >>> nothingIfNone (starts (intInt 3 5)) [intInt 3 4, intInt 5 6]
--
-- In the following, (3, 5) 'starts' (3, 6), so the input is returned.
--
-- >>> nothingIfNone (starts (intInt 3 5)) [intInt 3 6, intInt 5 6]
--
nothingIfNone :: (Monoid (f (Interval a)), Foldable f, Filterable f, IntervalAlgebraic a)=>
(Interval a -> Bool) -- ^ predicate to apply to each element of input list
-> f (Interval a)
-> Maybe (f (Interval a))
nothingIfNone = nothingIf (\f x -> (not.any f) x)
-- | Returns the empty monoid structure if *any* of the element of input satisfy
-- the predicate condition
nothingIfAny :: (Monoid (f (Interval a)), Foldable f, Filterable f, IntervalAlgebraic a)=>
(Interval a -> Bool) -- ^ predicate to apply to each element of input list
-> f (Interval a)
-> Maybe (f (Interval a))
nothingIfAny = nothingIf any
-- | Returns the empty monoid structure if *all* of the element of input satisfy
-- the predicate condition
nothingIfAll :: (Monoid (f (Interval a)), Foldable f, Filterable f, IntervalAlgebraic a)=>
(Interval a -> Bool) -- ^ predicate to apply to each element of input list
-> f (Interval a)
-> Maybe (f (Interval a))
nothingIfAll = nothingIf all
{- |
Filter functions provides means for filtering 'Filterable' containers of
@'Interval'@s based on @'IntervalAlgebraic'@ relations.
-}
-- |Creates a function for filtering a 'Witherable.Filterable' of @Interval a@s based on a predicate
filterMaker :: (Filterable f, IntervalAlgebraic a) =>
ComparativePredicateOf (Interval a)
-> Interval a
-> (f (Interval a) -> f (Interval a))
filterMaker f p = Witherable.filter (`f` p)
-- | Filter a 'Witherable.Filterable' of @Interval a@s to those that 'overlaps' the @Interval a@
-- in the first argument.
filterOverlaps :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterOverlaps = filterMaker overlaps
-- | Filter a 'Witherable.Filterable' of @Interval a@s to those 'overlappedBy' the @Interval a@
-- in the first argument.
filterOverlappedBy :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterOverlappedBy = filterMaker overlappedBy
-- | Filter a 'Witherable.Filterable' of Interval as to those 'before' the @Interval a@
-- in the first argument.
filterBefore :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterBefore = filterMaker before
-- | Filter a 'Witherable.Filterable' of Interval as to those 'after' the @Interval a@
-- in the first argument.
filterAfter :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterAfter = filterMaker after
-- | Filter a 'Witherable.Filterable' of Interval as to those 'starts' the @Interval a@
-- in the first argument.
filterStarts :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterStarts = filterMaker starts
-- | Filter a 'Witherable.Filterable' of Interval as to those 'startedBy' the @Interval a@
-- in the first argument.
filterStartedBy :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterStartedBy = filterMaker startedBy
-- | Filter a 'Witherable.Filterable' of Interval as to those 'finishes' the @Interval a@
-- in the first argument.
filterFinishes :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterFinishes = filterMaker finishes
-- | Filter a 'Witherable.Filterable' of Interval as to those 'finishedBy' the @Interval a@
-- in the first argument.
filterFinishedBy :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterFinishedBy = filterMaker finishedBy
-- | Filter a 'Witherable.Filterable' of Interval as to those that 'meets' the @Interval a@
-- in the first argument.
filterMeets :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterMeets = filterMaker meets
-- | Filter a 'Witherable.Filterable' of Interval as to those 'metBy' the @Interval a@
-- in the first argument.
filterMetBy :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterMetBy = filterMaker metBy
-- | Filter a 'Witherable.Filterable' of Interval as to those 'during' the @Interval a@
-- in the first argument.
filterDuring :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterDuring = filterMaker during
-- | Filter a 'Witherable.Filterable' of Interval as to those that 'contains'
-- the @Interval a@ in the first argument.
filterContains :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterContains = filterMaker contains
-- | Filter a 'Witherable.Filterable' of Interval as to those that 'equals'
-- the @Interval a@ in the first argument.
filterEquals :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterEquals = filterMaker equals
-- | Filter a 'Witherable.Filterable' of Interval as to those that are 'disjoint'
-- from the @Interval a@ in the first argument.
filterDisjoint :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterDisjoint = filterMaker disjoint
-- | Filter a 'Witherable.Filterable' of Interval as to those that are 'notDisjoint'
-- from the @Interval a@ in the first argument.
filterNotDisjoint :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterNotDisjoint = filterMaker notDisjoint
-- | Filter a 'Witherable.Filterable' of Interval as to those that are 'within'
-- the @Interval a@ in the first argument.
filterWithin :: (Filterable f, IntervalAlgebraic a) =>
Interval a -> f (Interval a) -> f (Interval a)
filterWithin = filterMaker disjoint