packages feed

interval-algebra-0.4.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
-}

{-# LANGUAGE FlexibleContexts #-}
module IntervalAlgebra.IntervalUtilities (
      combineIntervals
    , gaps
    , durations
    , clip
    , relations
    , gapsWithin
    , emptyIf
    , emptyIfNone
    , emptyIfAny
    , emptyIfAll
) where

import GHC.Base
    ( (++), map, foldr, otherwise, ($), (.), (<*>), seq, not
    , Semigroup((<>)), Functor(fmap), Maybe(..)
    , Int, Bool)
import GHC.Num ()
import Data.Tuple ( uncurry )
import Data.Foldable ( Foldable(null, foldl'), all, any )
import Data.Monoid ( (<>), Monoid(mempty) )
import IntervalAlgebra
    ( Interval, Intervallic(..), IntervalAlgebraic(..)
    , IntervalCombinable(..), IntervalSizeable(..)
    , IntervalFilterable(..)
    , IntervalRelation(..))
import Data.Maybe (mapMaybe)
import Data.List ( (++), head, init, last, tail, zip )
import Witherable ( Filterable )

intInt :: Int -> Int -> Interval Int
intInt = unsafeInterval

-- | Box to avoid overlapping instances
-- TODO: avoid the head/tail footguns
newtype Box a = Box { unBox :: [a] }
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 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) list of intervals consisting of the gaps between
--   intervals in the input list. *To work properly, the input list should be sorted*.
gaps :: (IntervalCombinable a) => [Interval a] -> [Interval a]
gaps l = mapMaybe (uncurry (><)) ((zip <*> tail) l)

-- | 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

-- | Finds the 'IntervalRelation' between each consecutive pair of intervals.
-- 
-- >>> relations [intInt 0 1, intInt 1 2] 
-- [Meets]
relations :: (IntervalAlgebraic a)=> [Interval a] -> [IntervalRelation a]
-- TODO: generalize to collections besides list
relations x = map (uncurry relate) ((zip <*> tail) x)

-- | 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@.
--
-- >>> gapsWithin (intInt 1 10) [intInt 0 5, intInt 7 9, intInt 12 15]
-- [(5, 7),(9, 10)]
gapsWithin :: (IntervalSizeable a b, IntervalCombinable a, IntervalFilterable [] a)=>
      Interval a  -- ^ i
  -> [Interval a] -- ^ x
  -> [Interval a]
-- TODO: generalize to collections besides list
gapsWithin i x = gaps $ enderval 0 (begin i) :
                        mapMaybe (clip i) (filterNotDisjoint i x) ++
                        [beginerval 0 (end i)]

-- | 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.
emptyIf :: (Monoid (f (Interval a)), Foldable f, IntervalFilterable f 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)
  -> f (Interval a)
emptyIf g f x = if g f x then mempty else 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).
--
-- >>> emptyIfNone (starts (intInt 3 5)) [intInt 3 4, intInt 5 6]
-- []
--
-- In the following, (3, 5) 'starts' (3, 6), so the input is returned.
--
-- >>> emptyIfNone (starts (intInt 3 5)) [intInt 3 6, intInt 5 6]
-- [(3, 6),(5, 6)]
emptyIfNone :: (Monoid (f (Interval a)), Foldable f, IntervalFilterable f a)=>
    (Interval a -> Bool) -- ^ predicate to apply to each element of input list
  -> f (Interval a)
  -> f (Interval a)
emptyIfNone = emptyIf (\f x -> (not.any f) x)

-- | Returns the empty monoid structure if *any* of the element of input satisfy
--   the predicate condition
emptyIfAny :: (Monoid (f (Interval a)), Foldable f, IntervalFilterable f a)=>
    (Interval a -> Bool) -- ^ predicate to apply to each element of input list
  -> f (Interval a)
  -> f (Interval a)
emptyIfAny = emptyIf any

-- | Returns the empty monoid structure if *all* of the element of input satisfy
--   the predicate condition
emptyIfAll :: (Monoid (f (Interval a)), Foldable f, IntervalFilterable f a)=>
    (Interval a -> Bool) -- ^ predicate to apply to each element of input list
  -> f (Interval a)
  -> f (Interval a)
emptyIfAll = emptyIf all