├── .gitignore ├── README.md ├── diff.nimble ├── src └── diff.nim └── tests ├── config.nims └── test.nim /.gitignore: -------------------------------------------------------------------------------- 1 | .#* 2 | [#]*# 3 | *~ 4 | *.[568] 5 | *.a 6 | *.bak 7 | *.class 8 | *.dll 9 | *.exe 10 | gpl-[0-9].[0-9].txt 11 | *.ld 12 | *.ldx 13 | *.li 14 | *.lix 15 | louti[0-9]* 16 | Makefile 17 | moc_*.cpp 18 | nimcache 19 | *.o 20 | *.obj 21 | __pycache__ 22 | *.py[co] 23 | qrc_*.cpp 24 | Qrc.py 25 | *.rs.bk 26 | *.so 27 | *.sw[nop] 28 | .*.swp 29 | tags 30 | target/ 31 | test.* 32 | *.tmp 33 | ui_*.h 34 | .wcignore 35 | *.jar 36 | GPLv3.txt 37 | MANIFEST 38 | archive.py 39 | st.sh 40 | tests/test 41 | .hg/ 42 | .hg* 43 | x.nim 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff 2 | Nim implementation of Python difflib's sequence matcher 3 | 4 | diff is a library for finding the differences between two sequences. 5 | 6 | The sequences can be of lines, strings (e.g., words), characters, 7 | bytes, or of any custom “item” type so long as it implements `==` 8 | and `hash()`. 9 | 10 | For other Nim code see http://www.qtrac.eu/sitemap.html#foss 11 | 12 | # Examples 13 | 14 | For example, this code: 15 | ```nim 16 | let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 17 | "As are you.").split('\n') 18 | let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 19 | "And so are you.").split('\n') 20 | for span in spanSlices(a, b): 21 | case span.tag 22 | of tagReplace: 23 | for text in span.a: 24 | echo("- ", text) 25 | for text in span.b: 26 | echo("+ ", text) 27 | of tagDelete: 28 | for text in span.a: 29 | echo("- ", text) 30 | of tagInsert: 31 | for text in span.b: 32 | echo("+ ", text) 33 | of tagEqual: 34 | for text in span.a: 35 | echo("= ", text) 36 | ``` 37 | produces this output: 38 | ``` 39 | - Tulips are yellow, 40 | + Roses are red, 41 | = Violets are blue, 42 | - Agar is sweet, 43 | - As are you. 44 | + Sugar is sweet, 45 | + And so are you. 46 | ``` 47 | 48 | If you need indexes rather than subsequences themselves, use 49 | ``spans(a, b)``. 50 | 51 | To skip the same subsequences pass ``skipEqual = true`` and for 52 | ``tagEqual`` use: ``of tagEqual: doAssert(false)``. 53 | 54 | See also `tests/test.nim`. 55 | 56 | # License 57 | 58 | diff is free open source software (FOSS) licensed under the 59 | Apache License, Version 2.0. 60 | -------------------------------------------------------------------------------- /diff.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.5.0" 4 | author = "Mark Summerfield" 5 | description = "Library for finding the differences between two sequences" 6 | license = "Apache-2.0" 7 | srcDir = "src" 8 | 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.0.4" 14 | -------------------------------------------------------------------------------- /src/diff.nim: -------------------------------------------------------------------------------- 1 | # Copyright © 2019-20 Mark Summerfield. All rights reserved. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may only use this file in compliance with the License. The license 4 | # is available from http://www.apache.org/licenses/LICENSE-2.0 5 | {.experimental: "codeReordering".} 6 | 7 | ## This library provides methods for comparing two sequences. 8 | ## 9 | ## The sequences could be seq[string] of words, or any other sequence 10 | ## providing the elements support ``==`` and ``hash()``. 11 | ## 12 | ## If you only need to compare each pair of sequences once, use 13 | ## ``spans(a, b)`` if you only need indexes, or ``spanSlices(a, b)`` if 14 | ## you need subsequences. 15 | ## 16 | ## If you need to do multiple comparisons on the same sequences, create a 17 | ## ``Diff`` with ``newDiff`` and then use ``diff.spans()`` 18 | ## 19 | ## Example: 20 | ## ```nim 21 | ## let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 22 | ## "As are you.").split('\n') 23 | ## let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 24 | ## "And so are you.").split('\n') 25 | ## for span in spanSlices(a, b): 26 | ## case span.tag 27 | ## of tagReplace: 28 | ## for text in span.a: 29 | ## echo("- ", text) 30 | ## for text in span.b: 31 | ## echo("+ ", text) 32 | ## of tagDelete: 33 | ## for text in span.a: 34 | ## echo("- ", text) 35 | ## of tagInsert: 36 | ## for text in span.b: 37 | ## echo("+ ", text) 38 | ## of tagEqual: 39 | ## for text in span.a: 40 | ## echo("= ", text) 41 | ## ``` 42 | ## 43 | ## (The algorithm is a slightly simplified version of the one used by the 44 | ## Python difflib module's SequenceMatcher.) 45 | ## 46 | ## See also `diff on github `_. 47 | ## For other Nim code see `FOSS `_. 48 | 49 | import algorithm 50 | import math 51 | import sequtils 52 | import sets 53 | import sugar 54 | import tables 55 | 56 | type 57 | Match* = tuple[aStart, bStart, length: int] 58 | 59 | Span* = tuple[tag: Tag, aStart, aEnd, bStart, bEnd: int] 60 | 61 | SpanSlice*[T] = tuple[tag: Tag, a, b: seq[T]] 62 | 63 | Tag* = enum 64 | tagEqual = "equal" 65 | tagInsert = "insert" 66 | tagDelete = "delete" 67 | tagReplace = "replace" 68 | 69 | Diff*[T] = object 70 | a*: seq[T] 71 | b*: seq[T] 72 | b2j: Table[T, seq[int]] 73 | 74 | proc newDiff*[T](a, b: seq[T]): Diff[T] = 75 | ## Creates a new ``Diff`` and computes the comparison data. 76 | ## 77 | ## To get all the spans (equals, insertions, deletions, replacements) 78 | ## necessary to convert sequence `a` into `b`, use ``diff.spans()``. 79 | ## 80 | ## To get all the matches (i.e., the positions and lengths) where `a` 81 | ## and `b` are the same, use ``diff.matches()``. 82 | ## 83 | ## If you need *both* the matches *and* the spans, use 84 | ## ``diff.matches()``, and then use ``spansForMatches()``. 85 | result.a = a 86 | result.b = b 87 | result.b2j = initTable[T, seq[int]]() 88 | result.chainBSeq() 89 | 90 | proc chainBSeq[T](diff: var Diff[T]) = 91 | for (i, key) in diff.b.pairs(): 92 | var indexes = diff.b2j.getOrDefault(key, @[]) 93 | indexes.add(i) 94 | diff.b2j[key] = indexes 95 | if (let length = len(diff.b); length > 200): 96 | let popularLength = int(floor(float(length) / 100.0)) + 1 97 | var bPopular = initHashSet[T]() 98 | for (element, indexes) in diff.b2j.pairs(): 99 | if len(indexes) > popularLength: 100 | bPopular.incl(element) 101 | for element in bPopular.items(): 102 | diff.b2j.del(element) 103 | 104 | iterator spans*[T](a, b: seq[T]; skipEqual = false): Span = 105 | ## Directly diffs and yields all the spans (equals, insertions, 106 | ## deletions, replacements) necessary to convert sequence ``a`` into 107 | ## ``b``. If ``skipEqual`` is ``true``, spans don't contain 108 | ## ``tagEqual``. 109 | ## 110 | ## If you need *both* the matches *and* the spans, use 111 | ## ``diff.matches()``, and then use ``spansForMatches()``. 112 | let diff = newDiff(a, b, skipEqual = skipEqual) 113 | let matches = diff.matches() 114 | for span in spansForMatches(matches, skipEqual = skipEqual): 115 | yield span 116 | 117 | iterator spans*[T](diff: Diff[T]; skipEqual = false): Span = 118 | ## Yields all the spans (equals, insertions, deletions, replacements) 119 | ## necessary to convert sequence ``a`` into ``b``. 120 | ## If ``skipEqual`` is ``true``, spans don't contain ``tagEqual``. 121 | ## 122 | ## If you need *both* the matches *and* the spans, use 123 | ## ``diff.matches()``, and then use ``spansForMatches()``. 124 | let matches = diff.matches() 125 | for span in spansForMatches(matches, skipEqual = skipEqual): 126 | yield span 127 | 128 | proc matches*[T](diff: Diff[T]): seq[Match] = 129 | ## Returns every ``Match`` between the two sequences. 130 | ## 131 | ## The differences are the spans between matches. 132 | ## 133 | ## To get all the spans (equals, insertions, deletions, replacements) 134 | ## necessary to convert sequence ``a`` into ``b``, use ``diff.spans()``. 135 | let aLen = len(diff.a) 136 | let bLen = len(diff.b) 137 | var queue = @[(0, aLen, 0, bLen)] 138 | var matches = newSeq[Match]() 139 | while len(queue) > 0: 140 | let (aStart, aEnd, bStart, bEnd) = queue.pop() 141 | let match = diff.longestMatch(aStart, aEnd, bStart, bEnd) 142 | let i = match.aStart 143 | let j = match.bStart 144 | let k = match.length 145 | if k > 0: 146 | matches.add(match) 147 | if aStart < i and bStart < j: 148 | queue.add((aStart, i, bStart, j)) 149 | if i + k < aEnd and j + k < bEnd: 150 | queue.add((i + k, aEnd, j + k, bEnd)) 151 | matches.sort() 152 | var aStart = 0 153 | var bStart = 0 154 | var length = 0 155 | for match in matches: 156 | if aStart + length == match.aStart and bStart + length == match.bStart: 157 | length += match.length 158 | else: 159 | if length != 0: 160 | result.add(newMatch(aStart, bStart, length)) 161 | aStart = match.aStart 162 | bStart = match.bStart 163 | length = match.length 164 | if length != 0: 165 | result.add(newMatch(aStart, bStart, length)) 166 | result.add(newMatch(aLen, bLen, 0)) 167 | 168 | proc longestMatch*[T](diff: Diff[T], aStart, aEnd, bStart, bEnd: int): 169 | Match = 170 | ## Returns the longest ``Match`` between the two given sequences, within 171 | ## the given index ranges. 172 | ## 173 | ## This is used internally, but may be useful, e.g., when called 174 | ## with say, ``diff.longest_match(0, len(a), 0, len(b))``. 175 | var bestI = aStart 176 | var bestJ = bStart 177 | var bestSize = 0 178 | var j2Len = initTable[int, int]() 179 | for i in aStart ..< aEnd: 180 | var tempJ2Len = initTable[int, int]() 181 | var indexes = diff.b2j.getOrDefault(diff.a[i], @[]) 182 | if len(indexes) > 0: 183 | for j in indexes: 184 | if j < bStart: 185 | continue 186 | if j >= bEnd: 187 | break 188 | let k = j2Len.getOrDefault(j - 1, 0) + 1 189 | tempJ2Len[j] = k 190 | if k > bestSize: 191 | bestI = i - k + 1 192 | bestJ = j - k + 1 193 | bestSize = k 194 | j2len = tempJ2Len 195 | while bestI > aStart and bestJ > bStart and 196 | diff.a[bestI - 1] == diff.b[bestJ - 1]: 197 | dec bestI 198 | dec bestJ 199 | inc bestSize 200 | while bestI + bestSize < aEnd and bestJ + bestSize < bEnd and 201 | diff.a[bestI + bestSize] == diff.b[bestJ + bestSize]: 202 | inc bestSize 203 | newMatch(bestI, bestJ, bestSize) 204 | 205 | iterator spansForMatches*(matches: seq[Match]; skipEqual = false): Span = 206 | ## Yields all the spans (equals, insertions, deletions, replacements) 207 | ## necessary to convert sequence ``a`` into ``b``, given the precomputed 208 | ## matches. Drops any ``tagEqual`` spans if ``skipEqual`` is true. 209 | ## 210 | ## Use this if you need *both* matches *and* spans, to avoid needlessly 211 | ## recomputing the matches, i.e., call ``diff.matches()`` to get the 212 | ## matches, and then this function for the spans. 213 | ## 214 | ## If you don't need the matches, then use ``diff.spans()``. 215 | var i = 0 216 | var j = 0 217 | for match in matches: 218 | var tag = tagEqual 219 | if i < match.aStart and j < match.bStart: 220 | tag = tagReplace 221 | elif i < match.aStart: 222 | tag = tagDelete 223 | elif j < match.bStart: 224 | tag = tagInsert 225 | if tag != tagEqual: 226 | yield newSpan(tag, i, match.aStart, j, match.bStart) 227 | i = match.aStart + match.length 228 | j = match.bStart + match.length 229 | if match.length != 0 and not skipEqual: 230 | yield newSpan(tagEqual, match.aStart, i, match.bStart, j) 231 | 232 | iterator spanSlices*[T](a, b: seq[T]; skipEqual = false): SpanSlice[T] = 233 | ## Directly diffs and yields all the span texts (equals, insertions, 234 | ## deletions, replacements) necessary to convert sequence ``a`` into 235 | ## ``b``. 236 | ## Drops any ``tagEqual`` spans if ``skipEqual`` is true. 237 | ## This is designed to make output easier. 238 | let diff = newDiff(a, b) 239 | var i = 0 240 | var j = 0 241 | for match in diff.matches(): 242 | var tag = tagEqual 243 | if i < match.aStart and j < match.bStart: 244 | tag = tagReplace 245 | elif i < match.aStart: 246 | tag = tagDelete 247 | elif j < match.bStart: 248 | tag = tagInsert 249 | if tag != tagEqual: 250 | yield newSpanSlice[T](tag, a[i ..< match.aStart], 251 | b[j ..< match.bStart]) 252 | i = match.aStart + match.length 253 | j = match.bStart + match.length 254 | if match.length != 0 and not skipEqual: 255 | yield newSpanSlice[T](tagEqual, a[match.aStart ..< i], 256 | b[match.bStart ..< j]) 257 | 258 | proc newMatch*(aStart, bStart, length: int): Match = 259 | ## Creates a new match: *only public for testing purposes*. 260 | (aStart, bStart, length) 261 | 262 | proc newSpan*(tag: Tag, aStart, aEnd, bStart, bEnd: int): Span = 263 | ## Creates a new span: *only public for testing purposes*. 264 | result.tag = tag 265 | result.aStart = aStart 266 | result.aEnd = aEnd 267 | result.bStart = bStart 268 | result.bEnd = bEnd 269 | 270 | proc newSpanSlice*[T](tag: Tag, a, b: seq[T]): SpanSlice[T] = 271 | ## Creates a new span: *only public for testing purposes*. 272 | result.tag = tag 273 | result.a = a 274 | result.b = b 275 | 276 | proc `==`*(a, b: Span): bool = 277 | ## Compares spans: *only public for testing purposes*. 278 | a.tag == b.tag and a.aStart == b.aStart and a.aEnd == b.aEnd and 279 | a.bStart == b.bStart and a.bEnd == b.bEnd 280 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | # Copyright © 2019-20 Mark Summerfield. All rights reserved. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may only use this file in compliance with the License. The license 4 | # is available from http://www.apache.org/licenses/LICENSE-2.0 5 | 6 | import diff 7 | import hashes 8 | import sequtils 9 | import strformat 10 | import strutils 11 | import sugar 12 | import unittest 13 | 14 | proc replacements*[T](a, b: seq[T]; prefix="% ", sep=" => "): string = 15 | var i = 0 16 | var j = 0 17 | while i < len(a) and j < len(b): 18 | result.add(prefix & $a[i] & sep & $b[j] & "\n") 19 | inc i 20 | inc j 21 | while i < len(a): 22 | result.add(prefix & $a[i] & sep & "\n") 23 | inc i 24 | while j < len(b): 25 | result.add(prefix & sep & $b[i] & "\n") 26 | inc j 27 | if result.endsWith('\n'): 28 | result = result[0 .. ^2] 29 | 30 | # For Items, we only consider the text (to make testing easier) 31 | type 32 | Item = object 33 | x: int 34 | y: int 35 | text: string 36 | 37 | proc newItem(x, y: int, text: string): Item = 38 | result.x = x 39 | result.y = y 40 | result.text = text 41 | 42 | # This must match `==` 43 | proc hash(i: Item): Hash = 44 | var h: Hash = 0 45 | h = h !& hash(i.text) 46 | !$h 47 | 48 | # This must match hash() 49 | proc `==`(a, b: Item): bool = 50 | a.text == b.text 51 | 52 | suite "diff tests": 53 | 54 | test "01": 55 | let a = "the quick brown fox jumped over the lazy dogs".split() 56 | let b = "the quick red fox jumped over the very busy dogs".split() 57 | let diff = newDiff(a, b) 58 | let expected = @[ 59 | newSpan(tagEqual, 0, 2, 0, 2), # the quick 60 | newSpan(tagReplace, 2, 3, 2, 3), # brown -> red 61 | newSpan(tagEqual, 3, 7, 3, 7), # fox jumped over the 62 | newSpan(tagReplace, 7, 8, 7, 9), # lazy -> very busy 63 | newSpan(tagEqual, 8, 9, 9, 10), # dogs 64 | ] 65 | let spans = toSeq(diff.spans()) 66 | check(len(expected) == len(spans)) 67 | for (act, exp) in zip(spans, expected): 68 | check(act == exp) 69 | 70 | test "02": 71 | let a = toSeq("qabxcd") 72 | let b = toSeq("abycdf") 73 | let diff = newDiff(a, b) 74 | let expected = @[ 75 | newSpan(tagDelete, 0, 1, 0, 0), # q -> 76 | newSpan(tagEqual, 1, 3, 0, 2), # ab 77 | newSpan(tagReplace, 3, 4, 2, 3), # x -> y 78 | newSpan(tagEqual, 4, 6, 3, 5), # cd 79 | newSpan(tagInsert, 6, 6, 5, 6), # -> f 80 | ] 81 | let spans = toSeq(diff.spans()) 82 | check(len(expected) == len(spans)) 83 | for (act, exp) in zip(spans, expected): 84 | check(act == exp) 85 | 86 | test "03": 87 | let a = toSeq("private Thread currentThread;") 88 | let b = toSeq("private volatile Thread currentThread;") 89 | let diff = newDiff(a, b) 90 | let expected = @[ 91 | newSpan(tagEqual, 0, 6, 0, 6), # privat 92 | newSpan(tagInsert, 6, 6, 6, 15), # -> e volatil 93 | newSpan(tagEqual, 6, 29, 15, 38), # e Thread currentThread; 94 | ] 95 | let spans = toSeq(diff.spans()) 96 | check(len(expected) == len(spans)) 97 | for (act, exp) in zip(spans, expected): 98 | check(act == exp) 99 | 100 | test "04": 101 | let a = "the quick brown fox jumped over the lazy dogs".split() 102 | let b = "the quick red fox jumped over the very busy dogs".split() 103 | let diff = newDiff(a, b) 104 | let longest = diff.longestMatch(0, len(a), 0, len(b)) 105 | check(newMatch(3, 3, 4) == longest) 106 | 107 | test "05": 108 | let a = "a s c ( 99 ) x z".split() 109 | let b = "r s b c ( 99 )".split() 110 | let diff = newDiff(a, b) 111 | let longest = diff.longestMatch(0, len(a), 0, len(b)) 112 | check(newMatch(2, 3, 4) == longest) 113 | 114 | test "06": 115 | let a = "foo\nbar\nbaz\nquux".split('\n') 116 | let b = "foo\nbaz\nbar\nquux".split('\n') 117 | let diff = newDiff(a, b) 118 | let expected = @[ 119 | newSpan(tagEqual, 0, 1, 0, 1), # foo 120 | newSpan(tagInsert, 1, 1, 1, 2), # -> baz 121 | newSpan(tagEqual, 1, 2, 2, 3), # bar 122 | newSpan(tagDelete, 2, 3, 3, 3), # baz -> 123 | newSpan(tagEqual, 3, 4, 3, 4), # quux 124 | ] 125 | let spans = toSeq(diff.spans()) 126 | check(len(expected) == len(spans)) 127 | for (act, exp) in zip(spans, expected): 128 | check(act == exp) 129 | 130 | test "07": 131 | let a = "foo\nbar\nbaz\nquux".split('\n') 132 | let b = "foo\nbaz\nbar\nquux".split('\n') 133 | let diff = newDiff(a, b) 134 | let expected = @[ 135 | newSpan(tagInsert, 1, 1, 1, 2), # -> baz 136 | newSpan(tagDelete, 2, 3, 3, 3), # baz -> 137 | ] 138 | # See test 08 for a better solution 139 | let spans = filter(toSeq(diff.spans()), span => span.tag != tagEqual) 140 | check(len(expected) == len(spans)) 141 | for (act, exp) in zip(spans, expected): 142 | check(act == exp) 143 | 144 | test "08": 145 | let a = "foo\nbar\nbaz\nquux".split('\n') 146 | let b = "foo\nbaz\nbar\nquux".split('\n') 147 | let diff = newDiff(a, b) 148 | let expected = @[ 149 | newSpan(tagInsert, 1, 1, 1, 2), # -> baz 150 | newSpan(tagDelete, 2, 3, 3, 3), # baz -> 151 | ] 152 | let spans = toSeq(diff.spans(skipEqual=true)) 153 | check(len(expected) == len(spans)) 154 | for (act, exp) in zip(spans, expected): 155 | check(act == exp) 156 | 157 | test "09": 158 | let a = @[1, 2, 3, 4, 5, 6] 159 | let b = @[2, 3, 5, 7] 160 | let diff = newDiff(a, b) 161 | let expected = @[ 162 | newSpan(tagDelete, 0, 1, 0, 0), # 1 -> 163 | newSpan(tagEqual, 1, 3, 0, 2), # 2 3 164 | newSpan(tagDelete, 3, 4, 2, 2), # 4 -> 165 | newSpan(tagEqual, 4, 5, 2, 3), # 5 166 | newSpan(tagReplace, 5, 6, 3, 4), # 6 -> 7 167 | ] 168 | let spans = toSeq(diff.spans()) 169 | check(len(expected) == len(spans)) 170 | for (act, exp) in zip(spans, expected): 171 | check(act == exp) 172 | 173 | test "10": 174 | let a = toSeq("qabxcd") 175 | let b = toSeq("abycdf") 176 | let diff = newDiff(a, b) 177 | let expected = @[ 178 | newSpan(tagDelete, 0, 1, 0, 0), # q -> 179 | newSpan(tagEqual, 1, 3, 0, 2), # a b 180 | newSpan(tagReplace, 3, 4, 2, 3), # x -> y 181 | newSpan(tagEqual, 4, 6, 3, 5), # c d 182 | newSpan(tagInsert, 6, 6, 5, 6), # -> f 183 | ] 184 | let spans = toSeq(diff.spans()) 185 | check(len(expected) == len(spans)) 186 | for (act, exp) in zip(spans, expected): 187 | check(act == exp) 188 | 189 | test "11": 190 | let a = @[ 191 | newItem(1, 3, "A"), 192 | newItem(2, 4, "B"), 193 | newItem(3, 8, "C"), 194 | newItem(5, 9, "D"), 195 | newItem(7, 2, "E"), 196 | newItem(3, 8, "F"), 197 | newItem(1, 6, "G"), 198 | ] 199 | let b = @[ 200 | newItem(3, 1, "A"), 201 | newItem(8, 3, "C"), 202 | newItem(9, 5, "B"), 203 | newItem(8, 3, "D"), 204 | newItem(6, 1, "E"), 205 | newItem(4, 2, "G"), 206 | ] 207 | let diff = newDiff(a, b) 208 | let expected = @[ 209 | newSpan(tagEqual, 0, 1, 0, 1), # A 210 | newSpan(tagInsert, 1, 1, 1, 2), # -> C 211 | newSpan(tagEqual, 1, 2, 2, 3), # B 212 | newSpan(tagDelete, 2, 3, 3, 3), # C -> 213 | newSpan(tagEqual, 3, 5, 3, 5), # D E 214 | newSpan(tagDelete, 5, 6, 5, 5), # F -> 215 | newSpan(tagEqual, 6, 7, 5, 6), # G 216 | ] 217 | let spans = toSeq(diff.spans()) 218 | check(len(expected) == len(spans)) 219 | for (act, exp) in zip(spans, expected): 220 | check(act == exp) 221 | 222 | test "12": 223 | let a = @[ 224 | newItem(1, 3, "quebec"), 225 | newItem(2, 4, "alpha"), 226 | newItem(3, 8, "bravo"), 227 | newItem(5, 9, "x-ray"), 228 | ] 229 | let b = @[ 230 | newItem(3, 1, "alpha"), 231 | newItem(8, 3, "bravo"), 232 | newItem(9, 5, "yankee"), 233 | newItem(8, 3, "charlie"), 234 | ] 235 | let diff = newDiff(a, b) 236 | let expected = @[ 237 | newSpan(tagDelete, 0, 1, 0, 0), # quebec -> 238 | newSpan(tagEqual, 1, 3, 0, 2), # alpha bravo 239 | newSpan(tagReplace, 3, 4, 2, 4), # x-ray -> yankee charlie 240 | ] 241 | let spans = toSeq(diff.spans()) 242 | check(len(expected) == len(spans)) 243 | for (act, exp) in zip(spans, expected): 244 | check(act == exp) 245 | 246 | test "13": 247 | let a = toSeq("abxcd") 248 | let b = toSeq("abcd") 249 | let diff = newDiff(a, b) 250 | let expected = @[ 251 | newMatch(0, 0, 2), 252 | newMatch(3, 2, 2), 253 | newMatch(5, 4, 0), 254 | ] 255 | let matches = diff.matches() 256 | check(len(expected) == len(matches)) 257 | for (act, exp) in zip(matches, expected): 258 | check(act == exp) 259 | 260 | test "14": 261 | let a = "the quick brown fox jumped over the lazy dogs".split() 262 | let b = newSeq[string]() # empty 263 | let diff = newDiff(a, b) 264 | let expected = @[newSpan(tagDelete, 0, 9, 0, 0)] 265 | let spans = toSeq(diff.spans()) 266 | check(len(expected) == len(spans)) 267 | for (act, exp) in zip(spans, expected): 268 | check(act == exp) 269 | 270 | test "15": 271 | let a = newSeq[string]() # empty 272 | let b = "the quick red fox jumped over the very busy dogs".split() 273 | let diff = newDiff(a, b) 274 | let expected = @[newSpan(tagInsert, 0, 0, 0, 10)] 275 | let spans = toSeq(diff.spans()) 276 | check(len(expected) == len(spans)) 277 | for (act, exp) in zip(spans, expected): 278 | check(act == exp) 279 | 280 | test "16": 281 | let a = newSeq[string]() # empty 282 | let b = newSeq[string]() # empty 283 | let diff = newDiff(a, b) 284 | let spans = toSeq(diff.spans()) 285 | check(len(spans) == 0) 286 | 287 | test "17": 288 | # 0 1 2 3 4 5 6 7 8 289 | let a = "the quick brown fox jumped over the lazy dogs".split() 290 | # 0 1 2 3 4 5 6 7 8 9 291 | let b = "the quick red fox jumped over the very busy dogs".split() 292 | let diff = newDiff(a, b) 293 | let expected = @[ 294 | newSpan(tagEqual, 0, 2, 0, 2), # the quick 295 | newSpan(tagReplace, 2, 3, 2, 3), # brown -> red 296 | newSpan(tagEqual, 3, 7, 3, 7), # fox jumped over the 297 | newSpan(tagReplace, 7, 8, 7, 9), # lazy -> very busy 298 | newSpan(tagEqual, 8, 9, 9, 10), # dogs 299 | ] 300 | let spans = toSeq(diff.spans()) 301 | check(len(expected) == len(spans)) 302 | for (act, exp) in zip(spans, expected): 303 | check(act == exp) 304 | 305 | test "18": 306 | let a = "the quick brown fox jumped over the lazy dogs".split() 307 | let b = "the slow red fox jumped over the very busy dogs".split() 308 | let expected = @[ 309 | """change "quick brown" => "slow red"""", 310 | """change "lazy" => "very busy"""" 311 | ] 312 | var spans = newSeq[string]() 313 | let diff = newDiff(a, b) 314 | for span in toSeq(diff.spans(skipEqual = true)): 315 | let aspan = join(a[span.aStart ..< span.aEnd], " ") 316 | let bspan = join(b[span.bStart ..< span.bEnd], " ") 317 | case span.tag 318 | of tagReplace: 319 | spans.add("change \"" & aspan & "\" => \"" & bspan & "\"") 320 | of tagInsert: spans.add("insert \"" & bspan & "\"") 321 | of tagDelete: spans.add("delete \"" & aspan & "\"") 322 | of tagEqual: doAssert(false) # Should never occur 323 | check(len(expected) == len(spans)) 324 | for (act, exp) in zip(spans, expected): 325 | check(act == exp) 326 | 327 | test "19": 328 | let a = "the quick brown fox jumped over the lazy dogs".split() 329 | let b = "the slow red fox jumped over the very busy dogs".split() 330 | let expected = @[ 331 | """replace "quick brown" => "slow red"""", 332 | """replace "lazy" => "very busy"""", 333 | ] 334 | var spans = newSeq[string]() 335 | let diff = newDiff(a, b) 336 | for span in diff.spans(skipEqual = true): 337 | let aspan = join(a[span.aStart ..< span.aEnd], " ") 338 | let bspan = join(b[span.bStart ..< span.bEnd], " ") 339 | case span.tag 340 | of tagReplace: spans.add("replace \"" & aspan & "\" => \"" & 341 | bspan & "\"") 342 | of tagInsert: spans.add("insert \"" & bspan & "\"") 343 | of tagDelete: spans.add("delete \"" & aspan & "\"") 344 | of tagEqual: doAssert(false) # Should never occur 345 | check(len(expected) == len(spans)) 346 | for (act, exp) in zip(spans, expected): 347 | check(act == exp) 348 | 349 | test "20": 350 | let a = "quebec alpha bravo x-ray yankee".split() 351 | let b = "alpha bravo yankee charlie".split() 352 | let expected = @[ 353 | """delete "quebec"""", 354 | """delete "x-ray"""", 355 | """insert "charlie"""", 356 | ] 357 | var spans = newSeq[string]() 358 | let diff = newDiff(a, b) 359 | for span in diff.spans(skipEqual = true): 360 | let aspan = join(a[span.aStart ..< span.aEnd], " ") 361 | let bspan = join(b[span.bStart ..< span.bEnd], " ") 362 | case span.tag 363 | of tagReplace: 364 | spans.add("change \"" & aspan & "\" => \"" & bspan & "\"") 365 | of tagInsert: spans.add("insert \"" & bspan & "\"") 366 | of tagDelete: spans.add("delete \"" & aspan & "\"") 367 | of tagEqual: doAssert(false) # Should never occur 368 | check(len(expected) == len(spans)) 369 | for (act, exp) in zip(spans, expected): 370 | check(act == exp) 371 | 372 | test "21": 373 | let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 374 | "As are you.").split('\n') 375 | let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 376 | "And so are you.").split('\n') 377 | let expected = @[ 378 | "replace a[0:1]: Tulips are yellow, => Roses are red,", 379 | "replace a[2:4]: Agar is sweet, NL As are you. => " & 380 | "Sugar is sweet, NL And so are you." 381 | ] 382 | var spans = newSeq[string]() 383 | let diff = newDiff(a, b) 384 | for span in diff.spans(skipEqual = true): 385 | case span.tag 386 | of tagReplace: 387 | spans.add(&"replace a[{span.aStart}:{span.aEnd}]: " & 388 | join(a[span.aStart ..< span.aEnd], " NL ") & " => " & 389 | join(b[span.bStart ..< span.bEnd], " NL ")) 390 | of tagDelete: 391 | spans.add(&"delete a[{span.aStart}:{span.aEnd}]: " & 392 | join(a[span.aStart ..< span.aEnd], " NL ")) 393 | of tagInsert: 394 | spans.add(&"insert b[{span.bStart}:{span.bEnd}]: " & 395 | join(b[span.bStart ..< span.bEnd], " NL ")) 396 | of tagEqual: doAssert(false) # Should never occur 397 | check(len(expected) == len(spans)) 398 | for (act, exp) in zip(spans, expected): 399 | check(act == exp) 400 | 401 | test "22": 402 | let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 403 | "As are you.").split('\n') 404 | let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 405 | "And so are you.").split('\n') 406 | let expected = @[ 407 | "replace [0:1]: Tulips are yellow, => Roses are red,", 408 | "replace [2:4]: Agar is sweet, NL As are you. => Sugar is sweet, " & 409 | "NL And so are you.", 410 | ] 411 | var spans = newSeq[string]() 412 | let diff = newDiff(a, b) 413 | for span in diff.spans(skipEqual = true): 414 | case span.tag 415 | of tagReplace: 416 | spans.add(&"replace [{span.aStart}:{span.aEnd}]: " & 417 | join(a[span.aStart ..< span.aEnd], " NL ") & " => " & 418 | join(b[span.bStart ..< span.bEnd], " NL ")) 419 | of tagDelete: 420 | spans.add(&"delete a[{span.aStart}:{span.aEnd}]: " & 421 | join(a[span.aStart ..< span.aEnd], " NL ")) 422 | of tagInsert: 423 | spans.add(&"insert b[{span.bStart}:{span.bEnd}]: " & 424 | join(b[span.bStart ..< span.bEnd], " NL ")) 425 | of tagEqual: doAssert(false) # Should never occur 426 | check(len(expected) == len(spans)) 427 | for (act, exp) in zip(spans, expected): 428 | check(act == exp) 429 | 430 | test "23": 431 | let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 432 | "As are you.").split('\n') 433 | let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 434 | "And so are you.").split('\n') 435 | let expected = @[ 436 | "% Tulips are yellow, => Roses are red,", 437 | "= Violets are blue,", 438 | "% Agar is sweet, => Sugar is sweet,\n" & 439 | "% As are you. => And so are you.", 440 | ] 441 | var spans = newSeq[string]() 442 | for span in spanSlices(a, b): 443 | case span.tag 444 | of tagReplace: spans.add(replacements(span.a, span.b)) 445 | of tagDelete: spans.add("- " & join(span.a, "\n")) 446 | of tagInsert: spans.add("+ " & join(span.b, "\n")) 447 | of tagEqual: spans.add("= " & join(span.a, "\n")) 448 | check(len(expected) == len(spans)) 449 | for (act, exp) in zip(spans, expected): 450 | check(act == exp) 451 | 452 | test "24": 453 | let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 454 | "As are you.").split('\n') 455 | let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 456 | "And so are you.").split('\n') 457 | let expected = @[ 458 | "- Tulips are yellow,", 459 | "+ Roses are red,", 460 | "= Violets are blue,", 461 | "- Agar is sweet,\nAs are you.", 462 | "+ Sugar is sweet,\nAnd so are you.", 463 | ] 464 | var spans = newSeq[string]() 465 | for span in spanSlices(a, b): 466 | case span.tag 467 | of tagReplace: 468 | spans.add("- " & join(span.a, "\n")) 469 | spans.add("+ " & join(span.b, "\n")) 470 | of tagDelete: spans.add("- " & join(span.a, "\n")) 471 | of tagInsert: spans.add("+ " & join(span.b, "\n")) 472 | of tagEqual: spans.add("= " & join(span.a, "\n")) 473 | check(len(expected) == len(spans)) 474 | for (act, exp) in zip(spans, expected): 475 | check(act == exp) 476 | 477 | test "25": 478 | let a = ("Tulips are yellow,\nViolets are blue,\nAgar is sweet,\n" & 479 | "As are you.").split('\n') 480 | let b = ("Roses are red,\nViolets are blue,\nSugar is sweet,\n" & 481 | "And so are you.").split('\n') 482 | for span in spanSlices(a, b): 483 | case span.tag 484 | of tagReplace: 485 | for text in span.a: 486 | echo("- ", text) 487 | for text in span.b: 488 | echo("+ ", text) 489 | of tagDelete: 490 | for text in span.a: 491 | echo("- ", text) 492 | of tagInsert: 493 | for text in span.b: 494 | echo("+ ", text) 495 | of tagEqual: 496 | for text in span.a: 497 | echo("= ", text) 498 | --------------------------------------------------------------------------------