packages feed

edit-distance-vector (empty) → 1.0

raw patch · 7 files changed

+411/−0 lines, 7 filesdep +QuickCheckdep +basedep +edit-distance-vectorsetup-changed

Dependencies added: QuickCheck, base, edit-distance-vector, quickcheck-instances, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,4 @@+edit-distance-vector 1.0++    * Initial release extracted from aeson-diff package and rewritten to use+      Data.Vector.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Thomas Sutton++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Thomas Sutton nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,55 @@+Edit Distance: Vector+=====================++[![Build Status][badge]][status]++This is a small library for calculating the edit distance and edit script+between two vectors. It is generic enough that you should be able to use it+with vectors containing *values* of any type you like, with *changes* described+by any type you like, and with *costs* represented by any type you like (with a+few restrictions).++Installing+----------++The `edit-distance-vector` package is a normal Haskell library and can be+installed using the Cabal package management tool.++````{bash}+cabal update+cabal install edit-distance-vector+````++Usage+-----++The interface to `edit-distance-vector` is very small; just import+`Data.Vector.Distance`, create a `Params` value with the correct operations to+deal with your types, and pass this to `leastChanges` along with your+`Vector`s.++````{haskell}+import qualified Data.Vector as V+import           Data.Vector.Distance++-- | Editing vectors of 'Char' values, with '(String, Int, Char)' describing+--   changes, and the additive monoid of 'Int' describing costs.+str :: Params Char (String, Int, Char) (Sum Int)+str = Params+    { equivalent = (==)+    , delete i c = ("delete", i, c)+    , insert i c = ("insert", i, c)+    , substitute i c c' = ("replace", i, c')+    , cost _ = 1+    , positionOffset (op,_,_) | op == "delete" = 0+                              | otherwise      = 1+    }++main :: IO ()+main = do+    print $ leastChanges str (V.fromList "I am thomas")+                             (V.fromList "My name is Thomas")+````++[badge]: https://travis-ci.org/thsutton/edit-distance-vector.svg?branch=master+[status]: https://travis-ci.org/thsutton/edit-distance-vector
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ edit-distance-vector.cabal view
@@ -0,0 +1,51 @@+name:                edit-distance-vector+version:             1.0+synopsis:            Calculate edit distances and edit scripts between vectors.+description:+  An implementation of the Wagner–Fischer dynamic programming algorithm to+  find the optimal edit script and cost between two sequences.+  .+  The implementation in this package is specialised to sequences represented+  with "Data.Vector" but is otherwise agnostic to:+  .+  * The type of values in the vectors;+  .+  * The type representing edit operations; and+  .+  * The type representing the cost of operations.+  .+homepage:            https://github.com/thsutton/edit-distance-vector+license:             BSD3+license-file:        LICENSE+author:              Thomas Sutton+maintainer:          me@thomas-sutton.id.au+copyright:           (c) 2015 Thomas Sutton and others.+category:            Data, Data Structures, Algorithms+build-type:          Simple+extra-source-files:  README.md, CHANGELOG.md+cabal-version:       >=1.10++source-repository    HEAD+  type:              git+  location:          https://github.com/thsutton/edit-distance-vector++library+  default-language:    Haskell2010+  hs-source-dirs:      lib+  exposed-modules:+      Data.Vector.Distance+  build-depends:+      base >=4.7 && <4.8+    , vector >= 0.8++test-suite             properties+  default-language:    Haskell2010+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             properties.hs+  build-depends:     +      base+    , QuickCheck+    , edit-distance-vector+    , quickcheck-instances+    , vector
+ lib/Data/Vector/Distance.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections   #-}+{-# LANGUAGE ViewPatterns    #-}++-- | Description: Calculate differences between vectors.+--+-- This module implements a variation on the+-- <http://en.wikipedia.org/wiki/Wagner–Fischer_algorithm Wagner-Fischer>+-- algorithm to find the shortest sequences of operations which transforms+-- one vector of values into another.+module Data.Vector.Distance (+  -- * Types+  Params(..),+  ChangeMatrix(..),++  -- * Operations+  leastChanges,+  allChanges,++  -- * Example+  strParams,+) where++import           Control.Applicative+import           Control.Arrow       ((***))+import           Data.Function+import           Data.List           hiding (delete, insert)+import           Data.Maybe+import           Data.Monoid+import           Data.Vector         (Vector)+import qualified Data.Vector         as V++-- | Operations invoked by the Wagner-Fischer algorithm.+--+--   The parameters to this type are as follows:+--+--   * 'v' is the type of values being compared,+--   * 'o' is the type representing operations,+--   * 'c' is the type representing costs.+--+--   The chief restrictions on these type parameters is that the cost type 'c' +--   must have instances of 'Monoid' and 'Ord'. A good default choice might be+--   the type @('Sum' 'Int')@.+data Params v o c = Params+    { equivalent     :: v -> v -> Bool+    -- ^ Are two values equivalent?+    , delete         :: Int -> v -> o+    -- ^ Delete the element at an index.+    , insert         :: Int -> v -> o+    -- ^ Insert an element at an index.+    , substitute     :: Int -> v -> v -> o+    -- ^ Substitute an element at an index.+    , cost           :: o -> c+    -- ^ Cost of a change.+    , positionOffset :: o -> Int+    -- ^ Positions to advance after a change. E.g. @0@ for a deletion.+    }+++-- | Matrix of optimal edit scripts and costs for all prefixes of two vectors.+--+--   This is a representation of the @n * m@ dynamic programming matrix+--   constructed by the algorithm.  The matrix is stored in a 'Vector' in+--   row-major format with an additional row and column corresponding to the+--   empty prefix of the source and destination 'Vectors'.+type ChangeMatrix o c = Vector (c, [o])++-- | /O(nm)./ Find the cost and optimal edit script to transform one 'Vector'+--   into another.+leastChanges+    :: (Monoid c, Ord c)+    => Params v o c+    -> Vector v -- ^ \"Source\" vector.+    -> Vector v -- ^ \"Destination" vector.+    -> (c, [o])+leastChanges p ss tt = fmap (catMaybes . reverse) . V.last $ rawChanges p ss tt++-- | /O(nm)./ Calculate the complete matrix of edit scripts and costs between+--   two vectors.+allChanges+    :: (Monoid c, Ord c)+    => Params v o c+    -> Vector v -- ^ \"Source\" vector.+    -> Vector v -- ^ \"Destination" vector.+    -> ChangeMatrix o c+allChanges p src dst = V.map (fmap (catMaybes . reverse)) $ rawChanges p src dst++-- | /O(nm)./ Calculate the complete matrix of edit scripts and costs between+--   two vectors.+--+--   This is a fairly direct implementation of Wagner-Fischer algorithm using+--   the 'Vector' data-type. The 'ChangeMatrix' is constructed in a single-pass.+--+--   Note: The change matrix is \"raw\" in that the edit script in each cell is+--   in reverse order and uses 'Maybe' to allow for steps at which no change is+--   necessary.+rawChanges+    :: (Monoid c, Ord c)+    => Params v o c+    -> Vector v -- ^ \"Source\" vector.+    -> Vector v -- ^ \"Destination" vector.+    -> Vector (c, [Maybe o])+rawChanges p@Params{..} src dst =+    let len_x = 1 + V.length dst+        len_y = 1 + V.length src+        len_n = len_x * len_y+        ix x y = (x * len_y) + y+        -- Get a cell from the 'ChangeMatrix'. It is an error to get a cell+        -- which hasn't been calculated yet!+        get :: Vector (c, [Maybe o]) -> Int -> Int -> (c, [Maybe o])+        get m x y = fromMaybe (error $ "Unable to get " <> show (x,y) <> " from change matrix") (m V.!? (ix x y))+        -- Calculate the position to be updated by the next edit in a script.+        position = sum . fmap (maybe 1 positionOffset)+        -- Given a partially complete 'ChangeMatrix', compute the next cell.+        ctr v = case V.length v `quotRem` len_y of+            -- Do nothing for "" ~> ""+            (        0,         0) -> (mempty, mempty)+            -- Delete everything in src for "..." ~> ""+            (        0, pred -> y) ->+                let o = delete 0 (src V.! y)+                    (pc, po) = get v 0 y+                in (cost o <> pc, Just o : po)+            -- Insert everything in dst for "" ~> "..."+            (pred -> x,         0) ->+                let o = insert x (fromMaybe (error "NAH") $ dst V.!? x)+                    (pc, po) = get v x 0+                in (cost o <> pc, Just o : po)+            -- Compare options between src and dst for "..." ~> "..."+            (pred -> x, pred -> y) ->+                let s = src V.! y+                    d = dst V.! x+                    tl   = get v (x)   (y)+                    top  = get v (x+1) (y)+                    left = get v (x)   (y+1)+                in if s `equivalent` d+                    then (Nothing:) <$> get v x y+                    else minimumBy (compare `on` fst)+                        -- Option 1: perform a deletion.+                        [ let c = delete (position . snd $ top) s+                          in (cost c <>) *** (Just c :) $ top+                        -- Option 2: perform an insertion.+                        , let c = insert (position . snd $ left) d+                          in (cost c <>) *** (Just c :) $ left+                        -- Option 3: perform a substitution.+                        , let c = substitute (position . snd $ tl) s d+                          in (cost c <>) *** (Just c :) $ tl+                        ]+    in V.constructN len_n ctr++-- | Example 'Params' to compare @('Vector' 'Char')@ values.+--+--   The algorithm will produce edit distances in terms of @('Sum' 'Int')@ and+--   edit scripts containing @(String, Int, Char)@ values.+--   +--   The first component of each operation is either @"delete"@, @"insert"@, or+--   @"replace"@.+strParams :: Params Char (String, Int, Char) (Sum Int)+strParams = Params{..}+  where+    equivalent = (==)+    delete     i c    = ("delete", i, c)+    insert     i c    = ("insert", i, c)+    substitute i c c' = ("replace", i, c')+    cost _ = 1+    positionOffset ("delete", _, _) = 0+    positionOffset _ = 1
+ test/properties.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TemplateHaskell   #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Main where++import Control.Monad+import Data.Monoid+import qualified Data.Vector as V+import System.Exit+import Test.QuickCheck+import Test.QuickCheck.Instances ()++import Data.Vector.Distance hiding (str)++-- | Changes to a 'String' (or other sequence, really).+data C a+    = I Int a+    | D Int a+    | S Int a a+  deriving (Show, Eq)++-- | Apply a list of changes.+runC :: [C v] -> [v] -> [v]+runC [] l = l+runC (D i _ : r) l =+    let (h,t) = splitAt i l+        l' = h <> tail t+    in runC r l'+runC (I i a : r) l =+    let (h,t) = splitAt i l+        l' = h <> [a] <> t+    in runC r l'+runC (S i a a' : r) l =+    let (h,t) = splitAt i l+        l' = h <> [a'] <> tail t+    in runC r l'++-- | Edit parameters for 'String'.+str :: Params Char (C Char) (Sum Int)+str = Params{..}+  where+    equivalent = (==)+    delete = D+    insert = I+    substitute = S+    cost op = case op of+        D{} -> 1+        I{} -> 1+        S{} -> 1+    positionOffset op = case op of+        D{} -> 0+        _   -> 1++-- | Patch extracted from identical documents should be mempty.+prop_distance_id :: String -> Bool+prop_distance_id s =+    let s' = V.fromList s+    in leastChanges str s' s' == (0, [])++-- | Delete everything!+prop_distance_delete :: NonEmptyList Char -> Bool+prop_distance_delete (NonEmpty s) =+    leastChanges str (V.fromList s) V.empty == (Sum $ length s, [ D 0 c | c <- s ])++-- | Insert everything!+prop_distance_insert :: NonEmptyList Char -> Bool+prop_distance_insert (NonEmpty s) =+    leastChanges str V.empty (V.fromList s) == (Sum $ length s, [ I i c | (i,c) <- zip [0..] s ])++-- | The examples from wikipedia.+prop_distance_canned :: Bool+prop_distance_canned =+    let sitting = V.fromList ("sitting" :: String)+        kitten = V.fromList ("kitten" :: String)+        saturday = V.fromList ("Saturday" :: String)+        sunday = V.fromList ("Sunday" :: String)+    in leastChanges str sitting kitten == (3, [S 0 's' 'k',S 4 'i' 'e',D 6 'g'])+    && leastChanges str kitten sitting == (3, [S 0 'k' 's',S 4 'e' 'i',I 6 'g'])+    && leastChanges str saturday sunday == (3, [D 1 'a',D 1 't',S 2 'r' 'n'])+    && leastChanges str sunday saturday == (3, [I 1 'a',I 2 't',S 4 'n' 'r'])++-- | Apply the found changes works.+--+-- @apply . leastChanges === id@+prop_distance_apply :: String -> String -> Bool+prop_distance_apply ss tt =+    tt == runC (snd $ leastChanges str (V.fromList ss) (V.fromList tt)) ss++--+-- Use Template Haskell to automatically run all of the properties above.+--++return []+runTests :: IO Bool+runTests = $quickCheckAll++main :: IO ()+main = do+    result <- runTests+    unless result exitFailure