packages feed

diff-loc-0.1.0.0: src/DiffLoc/Interval.hs

{-# LANGUAGE
  FlexibleContexts,
  StandaloneDeriving,
  TypeFamilies,
  UndecidableInstances #-}

-- | 'Interval' implements 'Shift'.
module DiffLoc.Interval
  ( Interval(..)
  , isZeroLength
  , Replace(..)
  ) where

import Prelude hiding ((+))
import DiffLoc.Shift
import DiffLoc.Unsafe ((.-.))

-- $setup
-- >>> import DiffLoc
-- >>> import DiffLoc.Test
-- >>> import Test.QuickCheck

-- Nicer looking formulas this way.

infixl 6 +
(+) :: Semigroup a => a -> a -> a
(+) = (<>)

-- | @(i ':..' n)@ represents a span of text between index @i@ and index @i+n@.
--
-- The type of indices @p@ is expected to be an instance of 'Amor'.
--
-- The length @n@ in an interval @(i :.. n)@ may be zero.
--
-- The elements of the interval can be thought of as indexing the interstices
-- /between/ characters. A span of length zero is a single interstice between
-- two characters, where some chunk of text may be inserted.
--
-- Example: drawing of @1 :.. 2@ in "abcde".
--
-- >  a b c d e
-- > 0 1 2 3 4 5
-- >   ^b+c+ length = 2
-- >   ^
-- >   ^ start = 1
data Interval p = !p :.. !(Trans p)

infixl 3 :..

-- | Does the interval have length zero?
isZeroLength :: (Eq (Trans p), Monoid (Trans p)) => Interval p -> Bool
isZeroLength (_ :.. n) = n == mempty

deriving instance (Eq p, Eq (Trans p)) => Eq (Interval p)
deriving instance (Show p, Show (Trans p)) => Show (Interval p)

instance Amor p => BlockOrder (Interval p) where
  precedes (i :.. n) (j :.. _) = i .+ n <= j
  distantlyPrecedes (i :.. n) (j :.. _) = i .+ n < j

-- | A minimalistic representation of text replacements.
--
-- A replacement @'Replace' i n m@ is given by a start location @i@, the length
-- @n@ of the interval to replace (source) and the length @m@ of its
-- replacement (target).
-- This representation does not keep track of the actual data being inserted.
--
-- This may overapproximate the underlying text replacement,
-- with intervals being wider than necessary.
-- For example, the transformation from "abc" to "ac" could be represented
-- by @mkReplace 1 1 0@ (replace "b" with "" at location 1), and also by
-- @mkReplace 0 2 1@ (replace "ab" with "a" at location 0).
--
-- > source   abc   abc
-- >      -    b    ab
-- >      +         a
-- > target   a c   a c
--
-- Insertions are replacements with source intervals of length zero.
-- Deletions are replacements with target intervals of length zero.
data Replace p = Replace !p !(Trans p) !(Trans p)

deriving instance (Eq p, Eq (Trans p)) => Eq (Replace p)
deriving instance (Show p, Show (Trans p)) => Show (Replace p)

-- | The composition of two replacements @l <> r@ represents the replacement @r@
-- followed by @l@, as one replacement of an span that contains both @r@ and @l@.
--
-- The right-to-left order of composition has the nice property that when
-- @l `'precedes` r@, @l <> r@ can also be viewed intuitively as performing @l@ and
-- @r@ simultaneously.
--
-- === Properties
--
-- prop> (x <> y) <> z === x <> (y <> z :: Replace (Plain Int))
instance Amor p => Semigroup (Replace p) where
  Replace li ln lm <> Replace ri rn rm
    | li .+ ln <= ri
      -- Disjoint, l on the left.
      --
      -- Before:
      -- > |---l---|       |---r---|
      -- > li      li+ln   ri      ri+rn
      --
      -- After both replacements (r first),
      -- with ld = lm-ln
      --
      -- > |---l---|       |---r---|
      -- > li      li+lm   ri+ld   ri+rm+ld
      --
    = Replace li ((ri .+ rn) .-. li) (lm + (ri .-. (li .+ ln)) + rm)

    | li <= ri
      -- l straddles the left end of r
      --
      -- Note that the indices in l should be interpreted
      -- as indices after r.
      -- After replacing r, the replaced span r and the to-be-replaced
      -- span l look like this:
      --
      -- >       |------r----|
      -- > |----l-----|
      -- > li    ri   li+ln  ri+rm
      --
      -- or this:
      --
      -- >      |--r--|
      -- > |-------l----------|
      -- > li   ri    ri+rm   li+ln
      --
    = let (n, m) = if li .+ ln < ri .+ rm
                   then ((ri .+ rn) .-. li, lm + ((ri .+ rm) .-. (li .+ ln)))
                   else ((ri .-. li) + rn + ((li .+ ln) .-. (ri .+ rm)), lm)
      in Replace li n m

    | li < ri .+ rm
      -- r straddles the left end of l
      --
      -- > |----r-----|
      -- >       |------l----|
      -- > ri    li   ri+rm  li+ln
      --
      -- or
      --
      -- > |-------r----------|
      -- >      |--l--|
      -- > ri   li    li+ln   ri+rm
      --
    = let (n, m) = if ri .+ rm < li .+ ln
                   then (rn + ((li .+ ln) .-. (ri .+ rm)), (li .+ lm) .-. ri)
                   else (rn, (li .-. ri) + lm + ((ri .+ rm) .-. (li .+ ln)))
      in Replace ri n m

    | otherwise
      --
      -- > |---r---|       |---l---|
      -- > ri      rm      li      ln
      --
    = Replace ri (rn + (li .-. (ri .+ rm)) + ln) ((li .+ lm) .-. ri)

instance Amor p => Shift (Replace p) where
  type Block (Replace p) = Interval p
  dual (Replace i n m) = Replace i m n

  src (Replace i n _) = i :.. n

  shiftBlock (Replace i n m) jp@(j :.. p)
    | j .+ p <= i = Just jp
    | i .+ n <= j = Just (i .+ (m + (j .-. (i .+ n))) :.. p)
    | otherwise = Nothing

  shiftR (Replace i n m) jpq@(Replace j p q)
    | j .+ p <= i = Just jpq
    | i .+ n <= j = Just (Replace (i .+ (m + (j .-. (i .+ n)))) p q)
    | otherwise = Nothing