packages feed

loc-0.2.0.0: src/Data/Loc/Area.hs

module Data.Loc.Area
  ( Area,

    -- * Constructing
    fromTo,
    spanArea,

    -- * Combining
    (+),
    (-),
    addSpan,

    -- * Querying
    firstSpan,
    lastSpan,
    start,
    end,
    areaSpan,
    spansAsc,
    spanCount,

    -- * Show and Read
    areaShowsPrec,
    areaReadPrec,
  )
where

import Data.Foldable qualified as Foldable
import Data.Loc.Internal.Map qualified as Map
import Data.Loc.Internal.Prelude
import Data.Loc.Loc (Loc)
import Data.Loc.Span (Span)
import Data.Loc.Span qualified as Span
import Data.Set qualified as Set

data Terminus = Start | End
  deriving (Eq, Ord)

-- | A set of non-overlapping, non-abutting 'Span's
--
-- You may also think of an 'Area' like a span that can be empty or have “gaps.”
--
-- Construct and combine areas using 'mempty', 'spanArea', 'fromTo', '+', and '-'.
newtype Area = Area (Map Loc Terminus)
  deriving (Eq, Ord)

-- | 'showsPrec' = 'areaShowsPrec'
instance Show Area where
  showsPrec = areaShowsPrec

-- | 'readPrec' = 'areaReadPrec'
instance Read Area where
  readPrec = areaReadPrec

instance Monoid Area where
  mempty = Area Map.empty

-- | '<>' = '+'
instance Semigroup Area where
  (<>) = (+)

areaShowsPrec :: Int -> Area -> ShowS
areaShowsPrec _ a =
  showList (spansAsc a)

-- |
--
-- >>> readPrec_to_S areaReadPrec minPrec "[]"
-- [([],"")]
--
-- >>> readPrec_to_S areaReadPrec minPrec "[3:2-5:5,8:3-11:4]"
-- [([3:2-5:5,8:3-11:4],"")]
--
-- >>> readPrec_to_S areaReadPrec minPrec "[3:2-5:5,11:4-8:3]"
-- [([3:2-5:5,8:3-11:4],"")]
--
-- >>> readPrec_to_S areaReadPrec minPrec "[3:2-5:5,8:3-8:3]"
-- []
areaReadPrec :: ReadPrec Area
areaReadPrec =
  foldMap spanArea <$> readListPrec

-- | Construct a contiguous 'Area' consisting of a single 'Span' specified
-- by two 'Loc's
--
-- The lesser loc will be the start, and the greater loc will be the end.
-- If the two locs are equal, the area will be empty.
fromTo ::
  -- | Start
  Loc ->
  -- | End
  Loc ->
  Area
fromTo a b
  | a == b = mempty
  | otherwise = spanArea (Span.fromTo a b)

-- | Construct an 'Area' consisting of a single 'Span'
--
-- >>> spanArea (read "4:5-6:3")
-- [4:5-6:3]
spanArea :: Span -> Area
spanArea s = Area (Map.fromList locs)
  where
    locs =
      [ (Span.start s, Start),
        (Span.end s, End)
      ]

-- | A 'Span' from 'start' to 'end', or 'Nothing' if the 'Area' is empty
--
-- >>> areaSpan mempty
-- Nothing
--
-- >>> areaSpan (read "[3:4-7:2]")
-- Just 3:4-7:2
--
-- >>> areaSpan (read "[3:4-7:2,15:6-17:9]")
-- Just 3:4-17:9
areaSpan :: Area -> Maybe Span
areaSpan x =
  start x >>= \a ->
    end x <&> \b ->
      Span.fromTo a b

-- | A list of the 'Span's that constitute an 'Area', sorted in ascending order
--
-- >>> spansAsc mempty
-- []
--
-- >>> spansAsc (read "[3:4-7:2,15:6-17:9]")
-- [3:4-7:2,15:6-17:9]
spansAsc :: Area -> [Span]
spansAsc (Area m) =
  mapAccumL f Nothing (Map.keys m) & snd & catMaybes
  where
    f Nothing l = (Just l, Nothing)
    f (Just l) l' = (Nothing, Just $ Span.fromTo l l')

-- |
--
-- >>> spanCount mempty
-- 0
--
-- >>> spanCount (read "[3:4-7:2]")
-- 1
--
-- >>> spanCount (read "[3:4-7:2,15:6-17:9]")
-- 2
spanCount :: Area -> Natural
spanCount (Area locs) =
  fromIntegral (Foldable.length locs `div` 2)

-- | The first contiguous 'Span' in the 'Area', or 'Nothing' if the area is empty
--
-- >>> firstSpan mempty
-- Nothing
--
-- >>> firstSpan (read "[3:4-7:2]")
-- Just 3:4-7:2
--
-- >>> firstSpan (read "[3:4-7:2,15:6-17:9]")
-- Just 3:4-7:2
firstSpan :: Area -> Maybe Span
firstSpan (Area m) =
  case Set.toAscList (Map.keysSet m) of
    a : b : _ -> Just (Span.fromTo a b)
    _ -> Nothing

-- | The last contiguous 'Span' in the 'Area', or 'Nothing' if the area is empty
--
-- >>> lastSpan mempty
-- Nothing
--
-- >>> lastSpan (read "[3:4-7:2]")
-- Just 3:4-7:2
--
-- >>> lastSpan (read "[3:4-7:2,15:6-17:9]")
-- Just 15:6-17:9
lastSpan :: Area -> Maybe Span
lastSpan (Area m) =
  case Set.toDescList (Map.keysSet m) of
    b : a : _ -> Just (Span.fromTo a b)
    _ -> Nothing

-- | The 'Loc' at which the 'Area' starts, or 'Nothing' if the 'Area' is empty
--
-- >>> start mempty
-- Nothing
--
-- >>> start (read "[3:4-7:2]")
-- Just 3:4
--
-- >>> start (read "[3:4-7:2,15:6-17:9]")
-- Just 3:4
start :: Area -> Maybe Loc
start (Area m) =
  case Map.minViewWithKey m of
    Just ((l, _), _) -> Just l
    Nothing -> Nothing

-- | The 'Loc' at which the 'Area' ends, or 'Nothing' if the 'Area' is empty
--
-- >>> end mempty
-- Nothing
--
-- >>> end (read "[3:4-7:2]")
-- Just 7:2
--
-- >>> end (read "[3:4-7:2,15:6-17:9]")
-- Just 17:9
end :: Area -> Maybe Loc
end (Area locs) =
  case Map.maxViewWithKey locs of
    Just ((l, _), _) -> Just l
    Nothing -> Nothing

-- | The union of two 'Area's
--
-- Spans that overlap or abut will be merged in the result.
--
-- >>> read "[1:1-1:2]" + mempty
-- [1:1-1:2]
--
-- >>> read "[1:1-1:2]" + read "[1:2-1:3]"
-- [1:1-1:3]
--
-- >>> read "[1:1-1:2]" + read "[1:1-3:1]"
-- [1:1-3:1]
--
-- >>> read "[1:1-1:2]" + read "[1:1-11:1]"
-- [1:1-11:1]
--
-- >>> read "[1:1-3:1,6:1-6:2]" + read "[1:1-6:1]"
-- [1:1-6:2]
--
-- >>> read "[1:1-3:1]" + read "[5:1-6:2]"
-- [1:1-3:1,5:1-6:2]
(+) :: Area -> Area -> Area
a + b
  | spanCount a >= spanCount b = foldr addSpan a (spansAsc b)
  | otherwise = b + a

-- | @'addSpan' s a@ is the union of @'Area' a@ and @'Span' s@
--
-- >>> addSpan (read "1:1-6:1") (read "[1:1-3:1,6:1-6:2]")
-- [1:1-6:2]
addSpan :: Span -> Area -> Area
addSpan b (Area as) =
  let -- Spans lower than b that do not abut or overlap b.
      -- These spans will remain completely intact in the result.
      unmodifiedSpansBelow :: Map Loc Terminus

      -- Spans greater than b that do not abut or overlap b.
      -- These spans will remain completely intact in the result.
      unmodifiedSpansAbove :: Map Loc Terminus

      -- The start location of a span that starts below b but doesn't end below b,
      -- if such a span exists. This span will be merged into the 'middle'.
      startBelow :: Maybe Loc

      -- The end location of a span that ends above b but doesn't start above b,
      -- if such a span exists. This span will be merged into the 'middle'.
      endAbove :: Maybe Loc

      -- b, plus any spans it abuts or overlaps.
      middle :: Map Loc Terminus

      (unmodifiedSpansBelow, startBelow) =
        let below = Map.below (Span.start b) as
         in case Map.maxViewWithKey below of
              Just ((l, Start), xs) -> (xs, Just l)
              _ -> (below, Nothing)

      (unmodifiedSpansAbove, endAbove) =
        let above = Map.above (Span.end b) as
         in case Map.minViewWithKey above of
              Just ((l, End), xs) -> (xs, Just l)
              _ -> (above, Nothing)

      middle =
        Map.fromList
          [ (minimum $ Foldable.toList startBelow <> [Span.start b], Start),
            (maximum $ Foldable.toList endAbove <> [Span.end b], End)
          ]
   in Area $ unmodifiedSpansBelow <> middle <> unmodifiedSpansAbove

-- | The difference between two 'Area's
--
-- @a '-' b@ contains what is covered by @a@ and not covered by @b@.
(-) :: Area -> Area -> Area
a - b = foldr subtractSpan a (spansAsc b)

-- | @'subtractSpan' s a@ is the subset of 'Area' @a@ that is not
-- covered by 'Span' @s@
subtractSpan :: Span -> Area -> Area
subtractSpan b (Area as) =
  let resultBelow :: Map Loc Terminus =
        let below = Map.belowInclusive (Span.start b) as
         in case Map.maxViewWithKey below of
              Just ((l, Start), xs) ->
                if l == Span.start b
                  then xs
                  else below & Map.insert (Span.start b) End
              _ -> below

      resultAbove :: Map Loc Terminus =
        let above = Map.aboveInclusive (Span.end b) as
         in case Map.minViewWithKey above of
              Just ((l, End), xs) ->
                if l == Span.end b
                  then xs
                  else above & Map.insert (Span.end b) Start
              _ -> above
   in Area $ resultBelow <> resultAbove