edits-0.1.0.0: src/Data/Text/EditMatrix.hs
{-# LANGUAGE OverloadedLists #-}
-- | This module computes a matrix keeping track of the costs necessary
-- to edit one piece of text so that it is transformed into another one
-- From that matrix it is possible to extract the sequence of edit operations with the minimum cost
module Data.Text.EditMatrix where
import Data.Matrix hiding (matrix, (!), getElem, setElem)
import Data.Matrix qualified as M
import Data.Text.Costs
import Data.Text.EditOperation
import Data.Vector as V (Vector, cons, fromList, head, take, (!))
import Protolude
-- | Create a edit matrix where costs are computed using dynamic programming
createEditMatrix :: Costs a -> [a] -> [a] -> Matrix Cost
createEditMatrix costs as1 as2 = do
let initialMatrix = M.matrix (length as1 + 1) (length as2 + 1) (const $ NoAction 0)
let coordinates = cartesian (length as1) (length as2)
foldl'
( \ma (i, j) ->
let newCost
| i == 0 = Insertion j -- no more letters for as1, we do j insertions
| j == 0 = Deletion i -- no more letters for as2, we do i suppressions
| otherwise = costOf i j ma -- otherwise we compute the cost and operation to go from as1[i] to as2[j]
in setElement newCost i j ma
)
initialMatrix
coordinates
where
(vs1, vs2) = (V.fromList as1, V.fromList as2)
-- compute the cartesian product of the [0..i] x [0..j] lists
cartesian :: Int -> Int -> [(Int, Int)]
cartesian m n = [(i, j) | i <- [0 .. m], j <- [0 .. n]]
-- compute the cost of going from as1[i] to as2[j], knowing the existing costs
-- (i-1, j-1) (i-1, j)
-- (i, j-1) (i, j)
--
-- going from (i-1, j) to (i, j) means that we delete as1[i]
-- going from (i-1, j-1) to (i, j) means that we substitute as1[i] with as2[j]
-- going from (i, j-1) to (i, j) means that we insert as2[j]
costOf :: Int -> Int -> Matrix Cost -> Cost
costOf i j matrix = do
let i1 = i - 1
let j1 = j - 1
let i1j = getElement i1 j matrix
let i1j1 = getElement i1 j1 matrix
let ij1 = getElement i j1 matrix
let result =
lowerCost
costs
(vs1 ! i1)
(vs2 ! j1)
(cost i1j + deletionCost costs (vs1 ! i1)) -- suppression
(cost i1j1 + substitutionCost costs (vs1 ! i1) (vs2 ! j1)) -- substitution
(cost ij1 + insertionCost costs (vs2 ! j1)) -- insertion
-- in case of a substitution if the resulting cost of (i, j) is the same as (i-1, j-1)
-- this means that we have substituted the same letter and it is the same as doing no action
case result of
Substitution {} | cost i1j1 == cost result -> NoAction (cost result)
_ -> result
-- | From the original lists of characters, given the cost matrix
-- return a list of edit operations allowing to edit one text and eventually get the second one
makeEditOperations :: forall a. Vector a -> Vector a -> Matrix Cost -> Vector (EditOperation a)
makeEditOperations [] _ _ = []
makeEditOperations _ [] _ = []
makeEditOperations as1 as2 matrix =
go (length as1) (length as2) []
where
go :: Int -> Int -> Vector (EditOperation a) -> Vector (EditOperation a)
go 0 0 _ = []
go i j ops = do
let op = getElement i j matrix
let dist = cost op
if i == 1 && j == 1
then
if dist == 0
then V.cons (Keep (V.head as1)) ops
else V.cons (Substitute (V.head as1) (V.head as2)) ops
else
if j < 0
then fmap Delete (V.take i as1) <> ops
else
if i < 0
then fmap Insert (V.take j as2) <> ops
else case op of
Insertion {} -> go i (j - 1) (V.cons (Insert (as2 ! (j - 1))) ops)
Deletion {} -> go (i - 1) j (V.cons (Delete (as1 ! (i - 1))) ops)
Substitution {} -> go (i - 1) (j - 1) (V.cons (Substitute (as1 ! (i - 1)) (as2 ! (j - 1))) ops)
_ -> go (i - 1) (j - 1) (V.cons (Keep (as1 ! (i - 1))) ops)
-- | Return the element at position i, j in the matrix
-- A matrix in the matrix package is 1-indexed but all the computations in this module are 0-indexed
-- so we need to shift the indices
getElement :: Int -> Int -> Matrix a -> a
getElement i j = M.getElem (i + 1) (j + 1)
-- | Set the element at position i, j in the matrix
-- A matrix in the matrix package is 1-indexed but all the computations in this module are 0-indexed
-- so we need to shift the indices
setElement :: a -> Int -> Int -> Matrix a -> Matrix a
setElement a i j = M.setElem a (i + 1, j + 1)