packages feed

bytes-metrics-0.1.0.0: src/Data/Bytes/Metrics.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.Bytes.Metrics
  ( levenshteinWithTolerance
  , isWithinLevenshtein
  ) where

import Control.Monad.ST (runST)
import Data.Bytes (Bytes)

import qualified Data.Bytes as Bytes
import qualified Data.Primitive.Contiguous as Arr
import qualified Data.Primitive.PrimArray as Prim

{- | Determine if two 'Bytes' are within a given Levenshtein distance of each other (inclusive).
Computes in O(t*min(n,m)) time and O(min(t,n,m)) space,
where @n,m@ are lengths of the input strings and @t@ is the tolerance.
-}
isWithinLevenshtein :: Int -> Bytes -> Bytes -> Bool
isWithinLevenshtein t a b = maybe False (<= t) $ levenshteinWithTolerance t a b

{- | Determine Levenshtein distance between two strings, as long as their
distance is within (inclusive) the given tolerance.
Computes in O(t*min(n,m)) time and O(min(t,n,m)) space,
where @n,m@ are lengths of the input strings and @t@ is the tolerance.
-}
levenshteinWithTolerance :: Int -> Bytes -> Bytes -> Maybe Int
levenshteinWithTolerance !t !a !b
  -- ensure that the first string (which will create columns) is longer
  -- this minimizes the space needed for intermediate results
  | t == 0 = if a == b then Just 0 else Nothing
  | m > n = levenshteinWithWorker t b a
  | otherwise = levenshteinWithWorker t a b
 where
  m = Bytes.length a
  n = Bytes.length b

-- Precondition: Length of A is less than or equal to length of B.
levenshteinWithWorker :: Int -> Bytes -> Bytes -> Maybe Int
levenshteinWithWorker !t !a !b
  | t < deltaN = Nothing
  | otherwise = runST $ do
      -- during table creation, some column indices will be negative:
      -- the contents of such oob cells must not impact the contents of in-bounds cells
      -- using maxBound to initialize could provoke overflow on increment
      -- using n+m will definitely be larger than any entry in the table, but likely small enough to avoid wrapping arithmetic
      row :: Prim.MutablePrimArray s Int <- Arr.replicateMut rowLen (n + m)
      let outerLoop !rowIx
            | rowIx <= m = do
                let innerLoop !bandIx
                      | bandIx < rowLen = do
                          let colIx = rowIx - p + bandIx
                          let initCost = if rowIx == 0 && colIx == 0 then 0 else maxBound
                          let !byteA = Bytes.unsafeIndex a (rowIx - 1)
                          let !byteB = Bytes.unsafeIndex b (colIx - 1)
                          !editCost <-
                            if
                              | not (1 <= colIx && colIx <= n) -> pure maxBound
                              | byteA == byteB -> Arr.read row bandIx
                              | otherwise -> (1 +) <$> Arr.read row bandIx
                          !insCost <-
                            if 0 <= bandIx - 1
                              then (1 +) <$> Arr.read row (bandIx - 1)
                              else pure maxBound
                          !delCost <-
                            if bandIx + 1 < rowLen
                              then (1 +) <$> Arr.read row (bandIx + 1)
                              else pure maxBound
                          let cost = min (min initCost editCost) (min insCost delCost)
                          Arr.write row bandIx cost
                          innerLoop (bandIx + 1)
                      | otherwise = pure ()
                innerLoop 0
                outerLoop (rowIx + 1)
            | otherwise = pure ()
      outerLoop 0
      d <- Arr.read row (deltaN + p)
      pure $ Just d
 where
  m = Bytes.length a
  n = Bytes.length b
  deltaN = n - m
  -- FIXME what a gross name, what even is p really supposed to be? a one-sided external tolerance for the diagonal band?
  p = (t - deltaN) `quot` 2
  -- \| the other way to think of this length is `t - deltaN + (1 - t `mod` 2)`
  -- the floor operation to compute `p` is what gives it that awful last term, and why I'm sticking with the paper's presentation
  rowLen = 1 + deltaN + 2 * p