├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── elm.json ├── src └── Diff.elm └── tests └── Tests.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | documentation.json 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.9" 4 | before_script: 5 | - npm install -g elm 6 | - npm install -g elm-test@0.19.0-beta5 7 | script: elm-test --fuzz 100 --seed 287625706785436 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present, Yosuke Torii 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of elm-diff nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-diff 2 | 3 | [![Build Status](https://travis-ci.org/jinjor/elm-diff.svg)](https://travis-ci.org/jinjor/elm-diff) 4 | 5 | A diff implementation for Elm. 6 | 7 | ## The algorithm 8 | 9 | This library implements [Wu's O(NP) algorithm](http://myerslab.mpi-cbg.de/wp-content/uploads/2014/06/np_diff.pdf). It shares the idea with [Myers's O(ND) algorithm](http://www.xmailserver.org/diff2.pdf), but much faster in some cases. 10 | 11 | ## LICENSE 12 | 13 | BSD-3-Clause 14 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "jinjor/elm-diff", 4 | "summary": "A diff implementation for Elm", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.6", 7 | "exposed-modules": [ 8 | "Diff" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm/core": "1.0.0 <= v < 2.0.0" 13 | }, 14 | "test-dependencies": { 15 | "elm-explorations/test": "1.0.0 <= v < 2.0.0" 16 | } 17 | } -------------------------------------------------------------------------------- /src/Diff.elm: -------------------------------------------------------------------------------- 1 | module Diff exposing (Change(..), diff, diffLines) 2 | 3 | {-| Compares two list and returns how they have changed. 4 | Each function internally uses Wu's [O(NP) algorithm](http://myerslab.mpi-cbg.de/wp-content/uploads/2014/06/np_diff.pdf). 5 | 6 | 7 | # Types 8 | 9 | @docs Change 10 | 11 | 12 | # Diffing 13 | 14 | @docs diff, diffLines 15 | 16 | -} 17 | 18 | import Array exposing (Array) 19 | 20 | 21 | {-| This describes how each line has changed and also contains its value. 22 | -} 23 | type Change a 24 | = Added a 25 | | Removed a 26 | | NoChange a 27 | 28 | 29 | type StepResult 30 | = Continue (Array (List ( Int, Int ))) 31 | | Found (List ( Int, Int )) 32 | 33 | 34 | type BugReport 35 | = CannotGetA Int 36 | | CannotGetB Int 37 | | UnexpectedPath ( Int, Int ) (List ( Int, Int )) 38 | 39 | 40 | {-| Compares two text. 41 | 42 | Giving the following text 43 | 44 | a = 45 | """aaa 46 | bbb 47 | ddd""" 48 | 49 | b = 50 | """zzz 51 | aaa 52 | ccc 53 | ddd""" 54 | 55 | results in 56 | 57 | [ Added "zzz" 58 | , NoChange "aaa" 59 | , Removed "bbb" 60 | , Added "ccc" 61 | , NoChange "ddd" 62 | ] 63 | 64 | . 65 | 66 | -} 67 | diffLines : String -> String -> List (Change String) 68 | diffLines a b = 69 | diff (String.lines a) (String.lines b) 70 | 71 | 72 | {-| Compares general lists. 73 | 74 | diff [1, 3] [2, 3] == [Removed 1, Added 2, NoChange 3] -- True 75 | 76 | -} 77 | diff : List a -> List a -> List (Change a) 78 | diff a b = 79 | case testDiff a b of 80 | Ok changes -> 81 | changes 82 | 83 | Err _ -> 84 | [] 85 | 86 | 87 | {-| Test the algolithm itself. 88 | If it returns Err, it should be a bug. 89 | -} 90 | testDiff : List a -> List a -> Result BugReport (List (Change a)) 91 | testDiff a b = 92 | let 93 | arrA = 94 | Array.fromList a 95 | 96 | arrB = 97 | Array.fromList b 98 | 99 | m = 100 | Array.length arrA 101 | 102 | n = 103 | Array.length arrB 104 | 105 | -- Elm's Array doesn't allow null element, 106 | -- so we'll use shifted index to access source. 107 | getA = 108 | \x -> Array.get (x - 1) arrA 109 | 110 | getB = 111 | \y -> Array.get (y - 1) arrB 112 | 113 | path = 114 | -- Is there any case ond is needed? 115 | -- ond getA getB m n 116 | onp getA getB m n 117 | in 118 | makeChanges getA getB path 119 | 120 | 121 | makeChanges : 122 | (Int -> Maybe a) 123 | -> (Int -> Maybe a) 124 | -> List ( Int, Int ) 125 | -> Result BugReport (List (Change a)) 126 | makeChanges getA getB path = 127 | case path of 128 | [] -> 129 | Ok [] 130 | 131 | latest :: tail -> 132 | makeChangesHelp [] getA getB latest tail 133 | 134 | 135 | makeChangesHelp : 136 | List (Change a) 137 | -> (Int -> Maybe a) 138 | -> (Int -> Maybe a) 139 | -> ( Int, Int ) 140 | -> List ( Int, Int ) 141 | -> Result BugReport (List (Change a)) 142 | makeChangesHelp changes getA getB ( x, y ) path = 143 | case path of 144 | [] -> 145 | Ok changes 146 | 147 | ( prevX, prevY ) :: tail -> 148 | let 149 | change = 150 | if x - 1 == prevX && y - 1 == prevY then 151 | case getA x of 152 | Just a -> 153 | Ok (NoChange a) 154 | 155 | Nothing -> 156 | Err (CannotGetA x) 157 | 158 | else if x == prevX then 159 | case getB y of 160 | Just b -> 161 | Ok (Added b) 162 | 163 | Nothing -> 164 | Err (CannotGetB y) 165 | 166 | else if y == prevY then 167 | case getA x of 168 | Just a -> 169 | Ok (Removed a) 170 | 171 | Nothing -> 172 | Err (CannotGetA x) 173 | 174 | else 175 | Err (UnexpectedPath ( x, y ) path) 176 | in 177 | case change of 178 | Ok c -> 179 | makeChangesHelp (c :: changes) getA getB ( prevX, prevY ) tail 180 | 181 | Err e -> 182 | Err e 183 | 184 | 185 | -- Myers's O(ND) algorithm (http://www.xmailserver.org/diff2.pdf) 186 | 187 | 188 | ond : (Int -> Maybe a) -> (Int -> Maybe a) -> Int -> Int -> List ( Int, Int ) 189 | ond getA getB m n = 190 | let 191 | v = 192 | Array.initialize (m + n + 1) (always []) 193 | in 194 | ondLoopDK (snake getA getB) m 0 0 v 195 | 196 | 197 | ondLoopDK : 198 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 199 | -> Int 200 | -> Int 201 | -> Int 202 | -> Array (List ( Int, Int )) 203 | -> List ( Int, Int ) 204 | ondLoopDK snake_ offset d k v = 205 | if k > d then 206 | ondLoopDK snake_ offset (d + 1) (-d - 1) v 207 | 208 | else 209 | case step snake_ offset k v of 210 | Found path -> 211 | path 212 | 213 | Continue v_ -> 214 | ondLoopDK snake_ offset d (k + 2) v_ 215 | 216 | 217 | 218 | -- Wu's O(NP) algorithm (http://myerslab.mpi-cbg.de/wp-content/uploads/2014/06/np_diff.pdf) 219 | 220 | 221 | onp : (Int -> Maybe a) -> (Int -> Maybe a) -> Int -> Int -> List ( Int, Int ) 222 | onp getA getB m n = 223 | let 224 | v = 225 | Array.initialize (m + n + 1) (always []) 226 | 227 | delta = 228 | n - m 229 | in 230 | onpLoopP (snake getA getB) delta m 0 v 231 | 232 | 233 | onpLoopP : 234 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 235 | -> Int 236 | -> Int 237 | -> Int 238 | -> Array (List ( Int, Int )) 239 | -> List ( Int, Int ) 240 | onpLoopP snake_ delta offset p v = 241 | let 242 | ks = 243 | if delta > 0 then 244 | List.reverse (List.range (delta + 1) (delta + p)) 245 | ++ List.range -p delta 246 | 247 | else 248 | List.reverse (List.range (delta + 1) p) 249 | ++ List.range (-p + delta) delta 250 | in 251 | case onpLoopK snake_ offset ks v of 252 | Found path -> 253 | path 254 | 255 | Continue v_ -> 256 | onpLoopP snake_ delta offset (p + 1) v_ 257 | 258 | 259 | onpLoopK : 260 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 261 | -> Int 262 | -> List Int 263 | -> Array (List ( Int, Int )) 264 | -> StepResult 265 | onpLoopK snake_ offset ks v = 266 | case ks of 267 | [] -> 268 | Continue v 269 | 270 | k :: ks_ -> 271 | case step snake_ offset k v of 272 | Found path -> 273 | Found path 274 | 275 | Continue v_ -> 276 | onpLoopK snake_ offset ks_ v_ 277 | 278 | 279 | step : 280 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 281 | -> Int 282 | -> Int 283 | -> Array (List ( Int, Int )) 284 | -> StepResult 285 | step snake_ offset k v = 286 | let 287 | fromLeft = 288 | Maybe.withDefault [] (Array.get (k - 1 + offset) v) 289 | 290 | fromTop = 291 | Maybe.withDefault [] (Array.get (k + 1 + offset) v) 292 | 293 | ( path, ( x, y ) ) = 294 | case ( fromLeft, fromTop ) of 295 | ( [], [] ) -> 296 | ( [], ( 0, 0 ) ) 297 | 298 | ( [], ( topX, topY ) :: _ ) -> 299 | ( fromTop, ( topX + 1, topY ) ) 300 | 301 | ( ( leftX, leftY ) :: _, [] ) -> 302 | ( fromLeft, ( leftX, leftY + 1 ) ) 303 | 304 | ( ( leftX, leftY ) :: _, ( topX, topY ) :: _ ) -> 305 | -- this implies "remove" comes always earlier than "add" 306 | if leftY + 1 >= topY then 307 | ( fromLeft, ( leftX, leftY + 1 ) ) 308 | 309 | else 310 | ( fromTop, ( topX + 1, topY ) ) 311 | 312 | ( newPath, goal ) = 313 | snake_ (x + 1) (y + 1) (( x, y ) :: path) 314 | in 315 | if goal then 316 | Found newPath 317 | 318 | else 319 | Continue (Array.set (k + offset) newPath v) 320 | 321 | 322 | snake : 323 | (Int -> Maybe a) 324 | -> (Int -> Maybe a) 325 | -> Int 326 | -> Int 327 | -> List ( Int, Int ) 328 | -> ( List ( Int, Int ), Bool ) 329 | snake getA getB nextX nextY path = 330 | case ( getA nextX, getB nextY ) of 331 | ( Just a, Just b ) -> 332 | if a == b then 333 | snake 334 | getA 335 | getB 336 | (nextX + 1) 337 | (nextY + 1) 338 | (( nextX, nextY ) :: path) 339 | 340 | else 341 | ( path, False ) 342 | 343 | -- reached bottom-right corner 344 | ( Nothing, Nothing ) -> 345 | ( path, True ) 346 | 347 | _ -> 348 | ( path, False ) 349 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (..) 2 | 3 | import Diff exposing (..) 4 | import Expect exposing (Expectation) 5 | import Test exposing (..) 6 | 7 | 8 | basic : Test 9 | basic = 10 | describe "Basic" 11 | [ test "basic 1" (\_ -> Expect.equal [] (diff [] [])) 12 | , test "basic 2" (\_ -> Expect.equal [ Removed 1 ] (diff [ 1 ] [])) 13 | , test "basic 3" (\_ -> Expect.equal [ Added 1 ] (diff [] [ 1 ])) 14 | , test "basic 4" (\_ -> Expect.equal [ NoChange 1 ] (diff [ 1 ] [ 1 ])) 15 | , test "basic 5" (\_ -> Expect.equal [ NoChange 1, Removed 2 ] (diff [ 1, 2 ] [ 1 ])) 16 | , test "basic 6" (\_ -> Expect.equal [ Removed 1, NoChange 2 ] (diff [ 1, 2 ] [ 2 ])) 17 | , test "basic 7" (\_ -> Expect.equal [ NoChange 1, Added 2 ] (diff [ 1 ] [ 1, 2 ])) 18 | , test "basic 8" (\_ -> Expect.equal [ Added 1, NoChange 2 ] (diff [ 2 ] [ 1, 2 ])) 19 | , test "basic 9" (\_ -> Expect.equal [ NoChange 1, NoChange 2 ] (diff [ 1, 2 ] [ 1, 2 ])) 20 | , test "basic 10" (\_ -> Expect.equal [ Removed 1, Removed 2 ] (diff [ 1, 2 ] [])) 21 | , test "basic 11" (\_ -> Expect.equal [ Added 1, Added 2 ] (diff [] [ 1, 2 ])) 22 | , test "basic 12" (\_ -> Expect.equal [ Removed 1, Added 2 ] (diff [ 1 ] [ 2 ])) 23 | , test "basic 13" (\_ -> Expect.equal [ Removed 1, Added 2, NoChange 3 ] (diff [ 1, 3 ] [ 2, 3 ])) 24 | , test "basic 14" (\_ -> Expect.equal [ Removed 1, Removed 2, Added 3, Added 4 ] (diff [ 1, 2 ] [ 3, 4 ])) 25 | ] 26 | 27 | 28 | runManyTimes : Int -> String -> String -> (() -> Expectation) 29 | runManyTimes times a_ b_ = 30 | let 31 | total = 32 | List.foldl (\i n -> n + List.length (diffLines a_ b_)) 0 (List.range 1 times) 33 | in 34 | \_ -> Expect.true "" (total > 0) 35 | 36 | 37 | perf : Test 38 | perf = 39 | describe "Perf" 40 | [ test "exactly same" (runManyTimes 100 a a) 41 | , test "add line to first" (runManyTimes 100 a b) 42 | , test "add line to last" (runManyTimes 100 a c) 43 | , test "drop first line" (runManyTimes 100 a d) 44 | , test "remove line at middle" (runManyTimes 100 a e) 45 | , test "add line at middle" (runManyTimes 100 a f) 46 | 47 | -- O(ND): 0.63s ( O(ND) = (280*2)*(280*2) ) 48 | -- O(NP): 0.32s ( O(NP) = (280*2)*((280*2-0)/2) ) 49 | , test "modify all" (runManyTimes 10 a g) 50 | 51 | -- O(ND): 0.13s ( O(ND) = 280*280 ) 52 | -- O(NP): 0.0s ( O(NP) = 280*((280-280)/2) ) 53 | , test "add all" (runManyTimes 10 "" a) 54 | 55 | -- O(ND): 0.13s ( O(ND) = 280*280 ) 56 | -- O(NP): 0.0s ( O(NP) = 280*((280-280)/2) ) 57 | , test "remove all" (runManyTimes 10 a "") 58 | ] 59 | 60 | 61 | b = 62 | "first\n" ++ a 63 | 64 | 65 | c = 66 | a ++ "\nlast" 67 | 68 | 69 | d = 70 | mapLines (List.drop 1) a 71 | 72 | 73 | e = 74 | mapLines (List.take 100) a ++ mapLines (List.drop 101) a 75 | 76 | 77 | f = 78 | mapLines (List.take 101) a ++ mapLines (List.drop 100) a 79 | 80 | 81 | g = 82 | mapEachLine ((++) "_") a 83 | 84 | 85 | mapLines f_ s = 86 | String.join "\n" (f_ (String.lines s)) 87 | 88 | 89 | mapEachLine f_ s = 90 | mapLines (List.map f_) s 91 | 92 | 93 | a = 94 | """ 95 | { a = 96 | [ 0 97 | , 1 98 | , 2 99 | , 3 100 | , 4 101 | , 5 102 | , 6 103 | , 7 104 | , 8 105 | , 9 106 | , 1 107 | , 2 108 | , 3 109 | , 4 110 | , 5 111 | , 6 112 | , 7 113 | , 8 114 | , 9 115 | ] 116 | , b = 117 | [ 0 118 | , 1 119 | , 2 120 | , 3 121 | , 4 122 | , 5 123 | , 6 124 | , 7 125 | , 8 126 | , 9 127 | , 1 128 | , 2 129 | , 3 130 | , 4 131 | , 5 132 | , 6 133 | , 7 134 | , 8 135 | , 9 136 | ] 137 | , c = 138 | [ 0 139 | , 1 140 | , 2 141 | , 3 142 | , 4 143 | , 5 144 | , 6 145 | , 7 146 | , 8 147 | , 9 148 | , 1 149 | , 2 150 | , 3 151 | , 4 152 | , 5 153 | , 6 154 | , 7 155 | , 8 156 | , 9 157 | ] 158 | , d = 0 159 | , e = 160 | [ 0 161 | , 1 162 | , 2 163 | , 3 164 | , 4 165 | , 5 166 | , 6 167 | , 7 168 | , 8 169 | , 9 170 | , 1 171 | , 2 172 | , 3 173 | , 4 174 | , 5 175 | , 6 176 | , 7 177 | , 8 178 | , 9 179 | ] 180 | , f = 181 | [ 0 182 | , 1 183 | , 2 184 | , 3 185 | , 4 186 | , 5 187 | , 6 188 | , 7 189 | , 8 190 | , 9 191 | , 1 192 | , 2 193 | , 3 194 | , 4 195 | , 5 196 | , 6 197 | , 7 198 | , 8 199 | , 9 200 | ] 201 | , g = 202 | [ 0 203 | , 1 204 | , 2 205 | , 3 206 | , 4 207 | , 5 208 | , 6 209 | , 7 210 | , 8 211 | , 9 212 | , 1 213 | , 2 214 | , 3 215 | , 4 216 | , 5 217 | , 6 218 | , 7 219 | , 8 220 | , 9 221 | ] 222 | , h = 223 | { a = 1 224 | , b = 1 225 | , c = 1 226 | , d = 1 227 | , e = "1" 228 | , f = "1" 229 | , g = "1" 230 | , h = "1" 231 | , i = "1" 232 | , j = "1" 233 | , k = "1" 234 | } 235 | , i = 0 236 | , j = 237 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 238 | , k = 239 | "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" 240 | , l = 241 | "ccccccccccccccccccccccccccccccccccc" 242 | , o = 243 | "dddddddddddddddddddddddddddddddddddddddddddddddddddddd" 244 | , p = 245 | Just 246 | ( Just 247 | ( Just 248 | ( Just 249 | ( Just 250 | ( Just ( Just ( Just ( Just 1 ) ) ) ) 251 | ) 252 | ) 253 | ) 254 | ) 255 | , q = 256 | Just 257 | ( Just 258 | ( Just 259 | ( Just 260 | ( Just 261 | ( Just ( Just ( Just ( Just 2 ) ) ) ) 262 | ) 263 | ) 264 | ) 265 | ) 266 | , r = 267 | Just 268 | ( Just 269 | ( Just 270 | ( Just 271 | ( Just 272 | ( Just ( Just ( Just ( Just 3 ) ) ) ) 273 | ) 274 | ) 275 | ) 276 | ) 277 | , s = 278 | Just 279 | ( Just 280 | ( Just 281 | ( Just 282 | ( Just 283 | ( Just ( Just ( Just ( Just 4 ) ) ) ) 284 | ) 285 | ) 286 | ) 287 | ) 288 | , t = "Ok, Google" 289 | , u = 123456789 290 | , v = 123.456 291 | , w = 292 | [ 0 293 | , 1 294 | , 2 295 | , 3 296 | , 4 297 | , 5 298 | , 6 299 | , 7 300 | , 8 301 | , 9 302 | , 1 303 | , 2 304 | , 3 305 | , 4 306 | , 5 307 | , 6 308 | , 7 309 | , 8 310 | , 9 311 | ] 312 | , x = 313 | [ 0 314 | , 1 315 | , 2 316 | , 3 317 | , 4 318 | , 5 319 | , 6 320 | , 7 321 | , 8 322 | , 9 323 | , 1 324 | , 2 325 | , 3 326 | , 4 327 | , 5 328 | , 6 329 | , 7 330 | , 8 331 | , 9 332 | ] 333 | , y = 334 | [ 0 335 | , 1 336 | , 2 337 | , 3 338 | , 4 339 | , 5 340 | , 6 341 | , 7 342 | , 8 343 | , 9 344 | , 1 345 | , 2 346 | , 3 347 | , 4 348 | , 5 349 | , 6 350 | , 7 351 | , 8 352 | , 9 353 | ] 354 | , z = 355 | [ 0 356 | , 1 357 | , 2 358 | , 3 359 | , 4 360 | , 5 361 | , 6 362 | , 7 363 | , 8 364 | , 9 365 | , 1 366 | , 2 367 | , 3 368 | , 4 369 | , 5 370 | , 6 371 | , 7 372 | , 8 373 | , 9 374 | ] 375 | } 376 | """ 377 | --------------------------------------------------------------------------------