packages feed

range-1.0.0.0: Data/Ranges.hs

{-# LANGUAGE Safe #-}

-- | The primary interface to the range library.
--
-- A 'Range' describes a membership set over any 'Ord' type. This module
-- provides the 'Ranges' type — a canonicalised, indexed collection of
-- 'Range' values — along with construction operators, set operations, and
-- membership predicates.
--
-- = Quick start
--
-- Build ranges with the construction operators and combine them with @('<>')@:
--
-- >>> (1 +=+ 5 :: Ranges Integer) <> (3 +=+ 8)
-- Ranges [1 +=+ 8]
--
-- Test membership:
--
-- >>> inRanges (1 +=+ 10 <> 20 +=+ 30 :: Ranges Integer) 5
-- True
-- >>> inRanges (1 +=+ 10 <> 20 +=+ 30 :: Ranges Integer) 15
-- False
--
-- Use 'mconcat' to build from a list:
--
-- >>> mconcat [1 +=+ 5, 10 +=+ 15, 12 +=+ 20 :: Ranges Integer]
-- Ranges [1 +=+ 5,10 +=+ 20]
--
-- = Transforming ranges
--
-- 'Ranges' does not implement 'Functor'. Mapping a function over boundary
-- values is not a well-defined operation for half-infinite ranges: an
-- order-reversing function like @negate@ applied to 'lbi' would need to
-- produce 'ubi', but 'Functor' cannot express that structural flip.
--
-- The idiomatic alternative is to __map the query value__, not the ranges.
-- Instead of converting boundaries to a new domain, convert incoming queries
-- back to the range's domain:
--
-- @
-- -- Unit conversion: test a Fahrenheit value against Celsius ranges
-- let safeTemp = 20 +=+ 37 :: Ranges Double  -- defined in °C
-- let inSafeTemp f = inRanges safeTemp ((f - 32) * 5 / 9)
-- @
--
-- This is always correct regardless of whether the conversion is monotone,
-- never requires re-canonicalisation, and avoids the constructor-flip hazard.
--
-- = Module guide
--
-- * "Data.Ranges" — __start here__. 'Ranges' type, all set operations.
-- * "Data.Range" — deprecated re-export shim; use "Data.Ranges" instead.
-- * "Data.Range.Ord" — 'Data.Range.Ord.KeyRange' and 'Data.Range.Ord.SortedRange' for 'Ord'-requiring contexts.
-- * "Data.Range.Parser" — Parsec-based parser for range strings.
-- * "Data.Range.Algebra" — F-Algebra for deferred, efficient expression trees.
module Data.Ranges (
  -- * Core types
  Range(..),
  Bound(..),
  BoundType(..),
  -- * The Ranges type
  Ranges(unRanges),
  -- * Range creation
  -- $creation
  (+=+),
  (+=*),
  (*=+),
  (*=*),
  lbi,
  lbe,
  ubi,
  ube,
  inf,
  -- * Single-range predicates
  inRange,
  aboveRange,
  belowRange,
  rangesOverlap,
  rangesAdjoin,
  -- * Multi-range predicates
  inRanges,
  aboveRanges,
  belowRanges,
  -- * Set operations
  mergeRanges,
  union,
  intersection,
  difference,
  invert,
  -- * Enumerable methods
  fromRanges,
  joinRanges
) where

-- $setup
-- >>> import Data.Ranges
-- >>> import Data.Foldable (fold)

import Control.DeepSeq (NFData, rnf)

import Data.Range.Data
import Data.Range.Util
  ( againstLowerBound, againstUpperBound, boundIsBetween, boundsOverlapType
  , invertBound, takeEvenly
  )
import Data.Range.RangeInternal
  ( loadRanges, exportRangeMerge, joinRM, buildSpanQuery
  , RangeMerge(..)
  )
import qualified Data.Range.Operators as Op
import qualified Data.Range.Algebra as Alg

-- ---------------------------------------------------------------------------
-- Internal helpers
-- ---------------------------------------------------------------------------

-- | Build an O(log n) membership predicate from a canonical range list.
buildQuery :: Ord a => [Range a] -> a -> Bool
buildQuery rs = case loadRanges rs of
  IRM            -> const True
  RM lb ub spans -> buildSpanQuery lb ub spans

-- | Build an O(1) "above all ranges" predicate from the canonical range list.
-- The last element has the largest upper bound; if @a@ is above it, it is
-- above every range. If the last element is a 'LowerBoundRange' or
-- 'InfiniteRange', nothing can be above it, so the predicate returns 'False'.
buildAboveQuery :: Ord a => [Range a] -> a -> Bool
buildAboveQuery []  = const True
buildAboveQuery rs  = aboveRange (last rs)

-- | Build an O(1) "below all ranges" predicate from the canonical range list.
-- The first element has the smallest lower bound; if @a@ is below it, it is
-- below every range. If the first element is an 'UpperBoundRange' or
-- 'InfiniteRange', nothing can be below it, so the predicate returns 'False'.
buildBelowQuery :: Ord a => [Range a] -> a -> Bool
buildBelowQuery []    = const True
buildBelowQuery (r:_) = belowRange r

-- | Smart constructor. Canonicalises the range list and pre-builds the
-- membership predicate. Every 'Ranges' value in this module is produced
-- through this function.
mkRanges :: Ord a => [Range a] -> Ranges a
mkRanges xs =
  let canonical = Alg.eval $ Alg.union (Alg.const []) (Alg.const xs)
  in Ranges canonical (buildQuery canonical) (buildAboveQuery canonical) (buildBelowQuery canonical)

-- ---------------------------------------------------------------------------
-- The Ranges type
-- ---------------------------------------------------------------------------

-- $creation
-- Each operator constructs a single-element 'Ranges'. Because 'Ranges' is a
-- 'Semigroup', you can combine them directly with '<>':
--
-- >>> (1 +=+ 5 :: Ranges Integer) <> (3 +=+ 8)
-- Ranges [1 +=+ 8]
--
-- The operators mirror those in "Data.Range.Operators" but return 'Ranges'
-- instead of 'Range', so they compose naturally without wrapping.

-- | A set of ranges represented as a merged, canonical list of
-- non-overlapping 'Range' values, with pre-built O(log n) membership,
-- O(1) above, and O(1) below predicates.
--
-- Construct values with the operators ('+=+', 'lbi', etc.) or with
-- 'mergeRanges'. Combine with @('<>')@ or 'mconcat'.
--
-- __Semigroup__: @('<>')@ computes the set union and merges the result into
-- canonical form.
--
-- >>> (1 +=+ 5 :: Ranges Integer) <> (3 +=+ 8)
-- Ranges [1 +=+ 8]
--
-- __Monoid__: 'mempty' is the empty set. 'mconcat' merges an entire list in a
-- single pass, more efficiently than repeated @('<>')@:
--
-- >>> mconcat [1 +=+ 5, 10 +=+ 15, 12 +=+ 20 :: Ranges Integer]
-- Ranges [1 +=+ 5,10 +=+ 20]
--
-- Use 'unRanges' to extract the underlying list.
data Ranges a = Ranges
  { unRanges     :: [Range a]  -- ^ The canonical (sorted, non-overlapping) list.
  , _rangesQuery :: a -> Bool  -- ^ Cached O(log n) membership predicate.
  , _aboveQuery  :: a -> Bool  -- ^ Cached O(1) "above all ranges" predicate.
  , _belowQuery  :: a -> Bool  -- ^ Cached O(1) "below all ranges" predicate.
  }

-- | Two 'Ranges' values are equal when their canonical range lists are equal.
instance Eq a => Eq (Ranges a) where
  a == b = unRanges a == unRanges b

instance Show a => Show (Ranges a) where
  showsPrec i r = showParen (i > 10) $ ("Ranges " ++) . shows (unRanges r)

-- | Forces the canonical range list; the cached predicate closure is not
-- forced (it is derived from the list and adds no new thunks).
instance NFData a => NFData (Ranges a) where
  rnf r = rnf (unRanges r)

instance Ord a => Semigroup (Ranges a) where
  (<>) a b = mkRanges (unRanges a ++ unRanges b)

-- | Evaluates a 'Alg.RangeExpr' tree whose leaves are 'Ranges' values,
-- producing a canonicalised 'Ranges' with a pre-built membership predicate.
--
-- This is the primary evaluation target for user-facing algebra expressions.
-- The implementation converts leaves to @['Range' a]@ internally, folds the
-- tree in a single @'RangeMerge'@ pass (the same efficient path as the
-- @['Range' a]@ instance), then wraps the result with 'mkRanges'.
instance (Ord a) => Alg.RangeAlgebra (Ranges a) where
  eval expr = mkRanges (Alg.eval (fmap unRanges expr))

instance Ord a => Monoid (Ranges a) where
  mempty  = mkRanges []
  mconcat = mkRanges . concatMap unRanges

-- ---------------------------------------------------------------------------
-- Construction operators
-- ---------------------------------------------------------------------------

-- | Mathematically equivalent to @[x, y]@. See 'SpanRange' for the
-- underlying constructor.
--
-- >>> 1 +=+ 5 :: Ranges Integer
-- Ranges [1 +=+ 5]
(+=+) :: Ord a => a -> a -> Ranges a
(+=+) a b = mkRanges [(Op.+=+) a b]

-- | Mathematically equivalent to @[x, y)@.
--
-- >>> 1 +=* 5 :: Ranges Integer
-- Ranges [1 +=* 5]
(+=*) :: Ord a => a -> a -> Ranges a
(+=*) a b = mkRanges [(Op.+=*) a b]

-- | Mathematically equivalent to @(x, y]@.
--
-- >>> 1 *=+ 5 :: Ranges Integer
-- Ranges [1 *=+ 5]
(*=+) :: Ord a => a -> a -> Ranges a
(*=+) a b = mkRanges [(Op.*=+) a b]

-- | Mathematically equivalent to @(x, y)@.
--
-- >>> 1 *=* 5 :: Ranges Integer
-- Ranges [1 *=* 5]
(*=*) :: Ord a => a -> a -> Ranges a
(*=*) a b = mkRanges [(Op.*=*) a b]

-- | Mathematically equivalent to @[x, ∞)@.
--
-- >>> lbi 5 :: Ranges Integer
-- Ranges [lbi 5]
lbi :: Ord a => a -> Ranges a
lbi = mkRanges . (:[]) . Op.lbi

-- | Mathematically equivalent to @(x, ∞)@.
lbe :: Ord a => a -> Ranges a
lbe = mkRanges . (:[]) . Op.lbe

-- | Mathematically equivalent to @(−∞, x]@.
ubi :: Ord a => a -> Ranges a
ubi = mkRanges . (:[]) . Op.ubi

-- | Mathematically equivalent to @(−∞, x)@.
ube :: Ord a => a -> Ranges a
ube = mkRanges . (:[]) . Op.ube

-- | The infinite range, covering all values.
inf :: Ord a => Ranges a
inf = mkRanges [Op.inf]

-- ---------------------------------------------------------------------------
-- Single-range predicates
-- ---------------------------------------------------------------------------

-- | Returns 'True' if the value falls within the single range.
-- Respects 'Inclusive' and 'Exclusive' bounds.
--
-- See 'inRanges' for testing against a 'Ranges' collection.
--
-- >>> inRange (SpanRange (Bound 1 Inclusive) (Bound 10 Inclusive)) (5 :: Integer)
-- True
-- >>> inRange (SpanRange (Bound 1 Inclusive) (Bound 10 Exclusive)) (10 :: Integer)
-- False
inRange :: Ord a => Range a -> a -> Bool
inRange (SingletonRange a)      value = value == a
inRange (SpanRange x y)         value = Overlap == boundIsBetween (Bound value Inclusive) (x, y)
inRange (LowerBoundRange lower) value = Overlap == againstLowerBound (Bound value Inclusive) lower
inRange (UpperBoundRange upper) value = Overlap == againstUpperBound (Bound value Inclusive) upper
inRange InfiniteRange           _     = True

-- | Returns 'True' if the value is strictly above (greater than the upper
-- bound of) the given range.
--
-- >>> aboveRange (SpanRange (Bound 1 Inclusive) (Bound 5 Inclusive)) (6 :: Integer)
-- True
-- >>> aboveRange (LowerBoundRange (Bound 0 Inclusive)) (6 :: Integer)
-- False
aboveRange :: Ord a => Range a -> a -> Bool
aboveRange (SingletonRange a)      value = value > a
aboveRange (SpanRange _ y)         value = Overlap == againstLowerBound (Bound value Inclusive) (invertBound y)
aboveRange (LowerBoundRange _)     _     = False
aboveRange (UpperBoundRange upper) value = Overlap == againstLowerBound (Bound value Inclusive) (invertBound upper)
aboveRange InfiniteRange           _     = False

-- | Returns 'True' if the value is strictly below (less than the lower
-- bound of) the given range.
--
-- >>> belowRange (SpanRange (Bound 1 Inclusive) (Bound 5 Inclusive)) (0 :: Integer)
-- True
-- >>> belowRange (UpperBoundRange (Bound 6 Inclusive)) (0 :: Integer)
-- False
belowRange :: Ord a => Range a -> a -> Bool
belowRange (SingletonRange a)      value = value < a
belowRange (SpanRange x _)         value = Overlap == againstUpperBound (Bound value Inclusive) (invertBound x)
belowRange (LowerBoundRange lower) value = Overlap == againstUpperBound (Bound value Inclusive) (invertBound lower)
belowRange (UpperBoundRange _)     _     = False
belowRange InfiniteRange           _     = False

-- | Returns 'True' if two ranges share at least one value.
--
-- >>> rangesOverlap (SpanRange (Bound 1 Inclusive) (Bound 5 Inclusive)) (SpanRange (Bound 3 Inclusive) (Bound 7 Inclusive) :: Range Integer)
-- True
-- >>> rangesOverlap (SpanRange (Bound 1 Inclusive) (Bound 5 Exclusive)) (SpanRange (Bound 5 Inclusive) (Bound 7 Inclusive) :: Range Integer)
-- False
rangesOverlap :: Ord a => Range a -> Range a -> Bool
rangesOverlap a b = Overlap == rangesOverlapType a b

-- | Returns 'True' if two ranges touch at a single exclusive boundary but
-- share no values.
--
-- >>> rangesAdjoin (SpanRange (Bound 1 Inclusive) (Bound 5 Exclusive)) (SpanRange (Bound 5 Inclusive) (Bound 7 Inclusive) :: Range Integer)
-- True
-- >>> rangesAdjoin (SpanRange (Bound 1 Inclusive) (Bound 5 Inclusive)) (SpanRange (Bound 3 Inclusive) (Bound 7 Inclusive) :: Range Integer)
-- False
rangesAdjoin :: Ord a => Range a -> Range a -> Bool
rangesAdjoin a b = Adjoin == rangesOverlapType a b

rangesOverlapType :: Ord a => Range a -> Range a -> OverlapType
rangesOverlapType (SingletonRange a) x =
  rangesOverlapType (SpanRange (Bound a Inclusive) (Bound a Inclusive)) x
rangesOverlapType (SpanRange x y)        (SpanRange a b)         = boundsOverlapType (x, y) (a, b)
rangesOverlapType (SpanRange _ y)        (LowerBoundRange lower) = againstLowerBound y lower
rangesOverlapType (SpanRange x _)        (UpperBoundRange upper) = againstUpperBound x upper
rangesOverlapType (LowerBoundRange _)    (LowerBoundRange _)     = Overlap
rangesOverlapType (LowerBoundRange lo)   (UpperBoundRange up)    = againstUpperBound lo up
rangesOverlapType (UpperBoundRange _)    (UpperBoundRange _)     = Overlap
rangesOverlapType InfiniteRange          _                       = Overlap
rangesOverlapType a b = rangesOverlapType b a

-- ---------------------------------------------------------------------------
-- Multi-range predicates
-- ---------------------------------------------------------------------------

-- | Returns 'True' if the value falls within any of the given ranges.
--
-- The membership predicate is pre-built when the 'Ranges' value is
-- constructed, so each call is O(log n) in the number of spans. Partial
-- application is idiomatic:
--
-- @
-- let memberOf = inRanges myRanges
-- filter memberOf largeList
-- @
--
-- >>> inRanges (1 +=+ 10 <> 20 +=+ 30 :: Ranges Integer) 5
-- True
-- >>> inRanges (1 +=+ 10 <> 20 +=+ 30 :: Ranges Integer) 15
-- False
inRanges :: Ord a => Ranges a -> a -> Bool
inRanges = _rangesQuery

-- | Returns 'True' if the value is strictly above all of the given ranges.
--
-- This predicate is O(1): the answer is determined by the last element of the
-- canonical range list (which has the largest upper bound), cached at
-- construction time.
--
-- >>> aboveRanges (1 +=+ 5 <> 10 +=+ 15 :: Ranges Integer) 20
-- True
-- >>> aboveRanges (1 +=+ 5 <> lbi 10 :: Ranges Integer) 20
-- False
aboveRanges :: Ord a => Ranges a -> a -> Bool
aboveRanges = _aboveQuery

-- | Returns 'True' if the value is strictly below all of the given ranges.
--
-- This predicate is O(1): the answer is determined by the first element of the
-- canonical range list (which has the smallest lower bound), cached at
-- construction time.
--
-- >>> belowRanges (5 +=+ 10 <> 20 +=+ 30 :: Ranges Integer) 1
-- True
-- >>> belowRanges (ubi 10 <> 20 +=+ 30 :: Ranges Integer) 1
-- False
belowRanges :: Ord a => Ranges a -> a -> Bool
belowRanges = _belowQuery

-- ---------------------------------------------------------------------------
-- Set operations
-- ---------------------------------------------------------------------------

-- | Canonicalise a raw list of 'Range' values into a 'Ranges'. Overlapping
-- ranges are merged; the result is sorted and non-overlapping.
--
-- >>> mergeRanges [LowerBoundRange (Bound 12 Inclusive), SpanRange (Bound 1 Inclusive) (Bound 10 Inclusive), SpanRange (Bound 5 Inclusive) (Bound 15 Inclusive) :: Range Integer]
-- Ranges [lbi 1]
mergeRanges :: Ord a => [Range a] -> Ranges a
mergeRanges = mkRanges

-- | Set union. Equivalent to @('<>')@.
--
-- >>> union (1 +=+ 10) (5 +=+ 15 :: Ranges Integer)
-- Ranges [1 +=+ 15]
union :: Ord a => Ranges a -> Ranges a -> Ranges a
union a b = mkRanges $ Alg.eval $
  Alg.union (Alg.const (unRanges a)) (Alg.const (unRanges b))

-- | Set intersection. Returns only values present in both.
--
-- >>> intersection (1 +=+ 10) (5 +=+ 15 :: Ranges Integer)
-- Ranges [5 +=+ 10]
intersection :: Ord a => Ranges a -> Ranges a -> Ranges a
intersection a b = mkRanges $ Alg.eval $
  Alg.intersection (Alg.const (unRanges a)) (Alg.const (unRanges b))

-- | Set difference: values in the first 'Ranges' not in the second.
--
-- >>> difference (1 +=+ 10) (5 +=+ 15 :: Ranges Integer)
-- Ranges [1 +=* 5]
difference :: Ord a => Ranges a -> Ranges a -> Ranges a
difference a b = mkRanges $ Alg.eval $
  Alg.difference (Alg.const (unRanges a)) (Alg.const (unRanges b))

-- | Complement: all values /not/ covered by the given 'Ranges'.
-- @'invert' . 'invert' == 'id'@.
--
-- >>> invert (1 +=* 10 <> 15 *=+ 20 :: Ranges Integer)
-- Ranges [ube 1,10 +=+ 15,lbe 20]
invert :: Ord a => Ranges a -> Ranges a
invert = mkRanges . Alg.eval . Alg.invert . Alg.const . unRanges

-- ---------------------------------------------------------------------------
-- Enumerable methods
-- ---------------------------------------------------------------------------

-- | Instantiate all values covered by the ranges as a list.
-- __Warning:__ not efficient. Prefer 'inRanges' for membership tests.
-- Combine with 'take' to avoid evaluating infinite ranges.
--
-- >>> take 5 . fromRanges $ (1 +=+ 10 :: Ranges Integer)
-- [1,2,3,4,5]
--
-- >>> take 6 . fromRanges $ (1 +=+ 3 :: Ranges Integer) <> (10 +=+ 12)
-- [1,10,2,11,3,12]
fromRanges :: (Ord a, Enum a) => Ranges a -> [a]
fromRanges = takeEvenly . fmap fromRange . unRanges
  where
    fromRange (SingletonRange x) = [x]
    fromRange (SpanRange (Bound a aType) (Bound b bType)) =
      [ (if aType == Inclusive then a else succ a)
        .. (if bType == Inclusive then b else pred b) ]
    fromRange (LowerBoundRange (Bound x xType)) =
      iterate succ (if xType == Inclusive then x else succ x)
    fromRange (UpperBoundRange (Bound x xType)) =
      iterate pred (if xType == Inclusive then x else pred x)
    fromRange InfiniteRange =
      zero : takeEvenly [iterate succ (succ zero), iterate pred (pred zero)]
      where zero = toEnum 0

-- | Join adjacent ranges that are contiguous for 'Enum' types.
-- For example, @[1 +=+ 5, 6 +=+ 10]@ collapses to @[1 +=+ 10]@ for
-- 'Integer' because there is no integer between 5 and 6.
--
-- >>> joinRanges (mconcat [1 +=+ 5, 6 +=+ 10] :: Ranges Integer)
-- Ranges [1 +=+ 10]
joinRanges :: (Ord a, Enum a) => Ranges a -> Ranges a
joinRanges = mkRanges . exportRangeMerge . joinRM . loadRanges . unRanges