packages feed

hgeometry-combinatorial-0.14: src/Data/Range.hs

{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE DeriveAnyClass  #-}
--------------------------------------------------------------------------------
-- |
-- Module      :  Data.Range
-- Copyright   :  (C) Frank Staals
-- License     :  see the LICENSE file
-- Maintainer  :  Frank Staals
--
-- Data type for representing Generic Ranges (Intervals) and functions that
-- work with them.
--
--------------------------------------------------------------------------------
module Data.Range( EndPoint(..)
                 , isOpen, isClosed
                 , unEndPoint
                 , Range(.., OpenRange, ClosedRange, Range')
                 , prettyShow
                 , lower, upper
                 , inRange, width, clipLower, clipUpper, midPoint, clampTo
                 , isValidRange, covers

                 , shiftLeft, shiftRight
                 ) where

import Control.Monad((<=<))
import Control.DeepSeq
import Control.Lens
import Control.Applicative
import Data.Intersection
import Data.Vinyl.CoRec
import GHC.Generics (Generic)
import Test.QuickCheck
import Data.Functor.Classes
import Text.Read

--------------------------------------------------------------------------------
-- * Representing Endpoints of a Range

-- | Endpoints of a range may either be open or closed.
data EndPoint a = Open   !a
                | Closed !a
                deriving (Eq,Functor,Foldable,Traversable,Generic,NFData)

instance (Show a) => Show (EndPoint a) where
  showsPrec = liftShowsPrec showsPrec showList

instance Show1 EndPoint where
  liftShowsPrec sp _sl d (Open a)   = showsUnaryWith sp "Open" d a
  liftShowsPrec sp _sl d (Closed a) = showsUnaryWith sp "Closed" d a

instance (Read a) => Read (EndPoint a) where
  readPrec     = liftReadPrec readPrec readListPrec
  readListPrec = readListPrecDefault

instance Read1 EndPoint where
  liftReadPrec rp _rl = readData $
      readUnaryWith rp "Open" Open <|>
      readUnaryWith rp "Closed" Closed
  liftReadListPrec = liftReadListPrecDefault


instance Ord a => Ord (EndPoint a) where
  -- | order on the actual value, and Open before Closed
  a `compare` b = f a `compare` f b
    where
      f (Open x)   = (x,False)
      f (Closed x) = (x,True)

instance Arbitrary r => Arbitrary (EndPoint r) where
  arbitrary = frequency [ (1, Open   <$> arbitrary)
                        , (9, Closed <$> arbitrary)
                        ]

_unEndPoint            :: EndPoint a -> a
_unEndPoint (Open a)   = a
_unEndPoint (Closed a) = a

-- | Access lens for EndPoint value regardless of whether it is open or closed.
--
-- >>> Open 5 ^. unEndPoint
-- 5
-- >>> Closed 10 ^. unEndPoint
-- 10
-- >>> Open 4 & unEndPoint .~ 0
-- Open 0
unEndPoint :: Lens (EndPoint a) (EndPoint b) a b
unEndPoint = lens _unEndPoint f
  where
    f (Open _) a   = Open a
    f (Closed _) a = Closed a
{-# INLINE unEndPoint #-}

-- | True iff EndPoint is open.
isOpen          :: EndPoint a -> Bool
isOpen Open{} = True
isOpen _      = False

-- | True iff EndPoint is closed.
isClosed :: EndPoint a -> Bool
isClosed = not . isOpen


--------------------------------------------------------------------------------
-- * The Range Data type

-- | Data type for representing ranges.
data Range a = Range { _lower :: !(EndPoint a)
                     , _upper :: !(EndPoint a)
                     }
               deriving (Eq,Functor,Foldable,Traversable,Generic,NFData)

-- | Lens access for the lower part of a range.
lower :: Lens' (Range a) (EndPoint a)
lower = lens _lower (\r l -> r{_lower=l})

-- | Lens access for the upper part of a range.
upper :: Lens' (Range a) (EndPoint a)
upper = lens _upper (\r u -> r{_upper=u})

-- instance Show a => Show (Range a) where
--   show (Range l u) = printf "Range (%s) (%s)" (show l) (show u)

instance (Show a) => Show (Range a) where
  showsPrec = liftShowsPrec showsPrec showList

instance Show1 Range where
  liftShowsPrec sp sl d (Range l u) =
      showsBinaryWith (liftShowsPrec sp sl) (liftShowsPrec sp sl) "Range" d l u

instance (Read a) => Read (Range a) where
  readPrec     = liftReadPrec readPrec readListPrec
  readListPrec = readListPrecDefault

instance Read1 Range where
  liftReadPrec rp rl = readData $
      readBinaryWith (liftReadPrec rp rl) (liftReadPrec rp rl) "Range" Range
  liftReadListPrec = liftReadListPrecDefault


pattern OpenRange       :: a -> a -> Range a
pattern OpenRange   l u = Range (Open l)   (Open u)

pattern ClosedRange     :: a -> a -> Range a
pattern ClosedRange l u = Range (Closed l) (Closed u)

-- | A range from l to u, ignoring/forgetting the type of the endpoints
pattern Range'     :: a -> a -> Range a
pattern Range' l u <- ((\r -> (r^.lower.unEndPoint,r^.upper.unEndPoint) -> (l,u)))
{-# COMPLETE Range' #-}

instance (Arbitrary r, Ord r) => Arbitrary (Range r) where
  arbitrary = do
                l <- arbitrary
                r <- suchThat arbitrary (p l)
                return $ Range l r
   where
     p (Open l)   r = l <  r^.unEndPoint
     p (Closed l) r = l <= r^.unEndPoint


-- | Helper function to show a range in mathematical notation.
--
-- >>> prettyShow $ OpenRange 0 2
-- "(0,2)"
-- >>> prettyShow $ ClosedRange 0 2
-- "[0,2]"
-- >>> prettyShow $ Range (Open 0) (Closed 5)
-- "(0,5]"
prettyShow             :: Show a => Range a -> String
prettyShow (Range l u) = concat [ lowerB, show (l^.unEndPoint), ","
                                , show (u^.unEndPoint), upperB
                                ]
  where
    lowerB = if isOpen l then "(" else "["
    upperB = if isOpen u then ")" else "]"



-- | Test if a value lies in a range.
--
-- >>> 1 `inRange` (OpenRange 0 2)
-- True
-- >>> 1 `inRange` (OpenRange 0 1)
-- False
-- >>> 1 `inRange` (ClosedRange 0 1)
-- True
-- >>> 1 `inRange` (ClosedRange 1 1)
-- True
-- >>> 10 `inRange` (OpenRange 1 10)
-- False
-- >>> 10 `inRange` (ClosedRange 0 1)
-- False
--
-- This one is kind of weird
--
-- >>> 0 `inRange` Range (Closed 0) (Open 0)
-- False
inRange                 :: Ord a => a -> Range a -> Bool
x `inRange` (Range l u) = case ((l^.unEndPoint) `compare` x, x `compare` (u^.unEndPoint)) of
    (_, GT) -> False
    (GT, _) -> False
    (LT,LT) -> True
    (LT,EQ) -> include u -- depends on only u
    (EQ,LT) -> include l -- depends on only l
    (EQ,EQ) -> include l && include u -- depends on l and u
  where
    include = isClosed

type instance IntersectionOf (Range a) (Range a) = [ NoIntersection, Range a]

instance Ord a => Range a `HasIntersectionWith` Range a
instance Ord a => Range a `IsIntersectableWith` Range a where

  nonEmptyIntersection = defaultNonEmptyIntersection

  -- The intersection is empty, if after clipping, the order of the end points is inverted
  -- or if the endpoints are the same, but both are open.
  (Range l u) `intersect` s = case (clipLower l <=< clipUpper u $ s) of
                                Nothing -> coRec NoIntersection
                                Just i  -> coRec i

-- | Get the width of the interval
--
-- >>> width $ ClosedRange 1 10
-- 9
-- >>> width $ OpenRange 5 10
-- 5
width   :: Num r => Range r -> r
width i = i^.upper.unEndPoint - i^.lower.unEndPoint

-- | Compute the halfway point between the start and end of a range.
midPoint   :: Fractional r => Range r -> r
midPoint r = let w = width r in r^.lower.unEndPoint + (w / 2)

-- | Clamps a value to a range. I.e. if the value lies outside the range we
-- report the closest value "in the range". Note that if an endpoint of the
-- range is open we report that value anyway, so we return a value that is
-- truely inside the range only if that side of the range is closed.
--
-- >>> clampTo (ClosedRange 0 10) 20
-- 10
-- >>> clampTo (ClosedRange 0 10) (-20)
-- 0
-- >>> clampTo (ClosedRange 0 10) 5
-- 5
-- >>> clampTo (OpenRange 0 10) 20
-- 10
-- >>> clampTo (OpenRange 0 10) (-20)
-- 0
-- >>> clampTo (OpenRange 0 10) 5
-- 5
clampTo                :: Ord r => Range r -> r -> r
clampTo (Range' l u) x = (x `max` l) `min` u


--------------------------------------------------------------------------------
-- * Helper functions

-- | Clip the interval from below. I.e. intersect with the interval {l,infty),
-- where { is either open, (, orr closed, [.
clipLower     :: Ord a => EndPoint a -> Range a -> Maybe (Range a)
clipLower p rr@(Range l r) = case (p^.unEndPoint) `compare` (r^.unEndPoint) of
                               GT                        -> Nothing
                               EQ | isOpen r || isOpen p -> Nothing
                               _                         -> Just $
                                 case (p^.unEndPoint) `compare` (l^.unEndPoint) of
                                   LT -> rr
                                   EQ -> if isOpen p then Range p r else rr
                                   GT -> Range p r

-- | Clip the interval from above. I.e. intersect with (-\infty, u}, where } is
-- either open, ), or closed, ],
clipUpper     :: Ord a => EndPoint a -> Range a -> Maybe (Range a)
clipUpper p (Range l r) = case (p^.unEndPoint) `compare` (l^.unEndPoint) of
                            LT                        -> Nothing
                            EQ | isOpen l || isOpen p -> Nothing
                            _                         -> Just $ Range l (p `min` r)


-- | Wether or not the first range completely covers the second one
covers       :: forall a. Ord a => Range a -> Range a -> Bool
x `covers` y = (== Just y) . asA @(Range a) $ x `intersect` y

-- | Check if the range is valid and nonEmpty, i.e. if the lower endpoint is
-- indeed smaller than the right endpoint. Note that we treat empty open-ranges
-- as invalid as well.
--
-- >>> isValidRange $ Range (Open 4) (Closed 4)
-- False
-- >>> isValidRange $ Range (Open 5) (Closed 4)
-- False
-- >>> isValidRange $ Range (Open 4) (Closed 5)
-- True
-- >>> isValidRange $ Range (Closed 5) (Closed 40)
-- True
isValidRange             :: Ord a => Range a -> Bool
isValidRange (Range l u) = case _unEndPoint l `compare` _unEndPoint u of
                             LT                            -> True
                             EQ | isClosed l && isClosed u -> True
                             _                             -> False

--------------------------------------------------------------------------------

-- | Shift a range x units to the left
--
-- >>> prettyShow $ shiftLeft 10 (ClosedRange 10 20)
-- "[0,10]"
-- >>> prettyShow $ shiftLeft 10 (OpenRange 15 25)
-- "(5,15)"
shiftLeft   :: Num r => r -> Range r -> Range r
shiftLeft x = shiftRight (-x)

-- | Shifts the range to the right
--
-- >>> prettyShow $ shiftRight 10 (ClosedRange 10 20)
-- "[20,30]"
-- >>> prettyShow $ shiftRight 10 (OpenRange 15 25)
-- "(25,35)"
shiftRight   :: Num r => r -> Range r -> Range r
shiftRight x = fmap (+x)