Took some time to explore possible improvements to the “Gestalt Pattern Matching for file diffs”.
First, let me introduce simple example that I will reference later.
(Line-by-line case is named (L) diff_key in tables below.)
Before:
def foo1(a, b):
a += 1
b += 1
return a + b
def foo2(a, b):
a += 2
b += 2
return a + b
def foo3(a, b):
c = a + b
d = c + a * b
r = sum(range(d))
return r
After:
def foo3(a, b):
c = a + b
d = c + a * b
r = sum(range(d))
return r
#
def foo1(a, b):
a += 1
b += 1
return a + b
#
def foo2(a, b):
a += 2
b += 2
return a + b
The issue with difflib is that it greedily captures block of length 5, giving up 2 blocks of length 4.
First of all, patience diff.
From my preliminary testing it does not seem to be working well in conjunction with block approach of difflib.
Its not that it doesn’t do what it promises.
Which is - “I will improve quality at the expense of less matches”.
So it does give a good compromise when used in conjunction with Myers diff (non-contiguous LCS) as Myers diff is unrestricted algorithm that optimizes at fines granularity, which for diff quality can be a bit overkill. Thus, the opportunity for “patient” preprocessing.
But difflib’s block approach already “sacrifices” a great number of matches for the sake of contiguous blocks. Thus, when used in conjunction with “patience diff”, the final result does seem to sacrifice too many matches in total.
Having that said, patience diff does correct the simple example above:
- (C) means raw strings - character by character comparison
- (L) means strings are split into lines
- Numbers are:
total_number_of_match_points-how_much_it_is_lower_than_LLCS (L) diff_keycase is the simple example above as presented - line-by-line- Rest of cases can be found in: Code Diff Test Cases · GitHub
Gestalt==difflib(autojunk=False)
| case | Myer/LLCS | patient Myer | Gestalt | patient Gestalt |
|---|---|---|---|---|
| (C) diff_key ( 194, 196) | 143 | 82 -61 | 82 -61 | 82 -61 |
| (L) diff_key ( 15, 15) | 8 | 8 0 | 5 -3 | 8 0 |
| (C) diff_mid1 ( 537, 533) | 335 | 335 0 | 326 -9 | 326 -9 |
| (L) diff_mid1 ( 28, 27) | 11 | 11 0 | 11 0 | 11 0 |
| (C) diff_mid2 (2907, 2907) | 2139 | 2000 -139 | 1827 -312 | 1717 -422 |
| (L) diff_mid2 ( 128, 128) | 73 | 63 -10 | 72 -1 | 63 -10 |
| (C) diff_mid3 (3139, 3139) | 2266 | 2266 0 | 1329 -937 | 1329 -937 |
| (L) diff_mid3 ( 131, 131) | 63 | 62 -1 | 50 -13 | 62 -1 |
| (L) diff_lib (2085, 2380) | 1930 | 1929 -1 | 1926 -4 | 1929 -1 |
| ----- | ||||
| Total: | 6968 | 6756 -212 | 5628 -1340 | 5527 -1441 |
| Avg % diff: | 0% | 7.17% | 17.89% | 13.53% |
| Runtime (s): | 0.473 | 0.276 | 0.046 | 0.042 |
Although avg % diff is better due to perfectly fixing the (L) diff_key, the total number of matches suffers. On the other hand, when it is used with Myer, the cost seems much more acceptable.
Which brings me to the next thing.
Alternatively, there is another idea, which might be more suitable for difflib’s block approach.
Skew fix.:
- Once LCSUB/block is calculated, check its “skew” (skewed if in very different positions in 2 sequences) and calculate couple alternatives accordingly (or not).
- For each candidate calculate 2 nearby LCSUBS/blocks. So several candidates, 3 LCSUBs/blocks each.
- Then take best (which is now 3 blocks) and recurse in gaps.
At worst case, runs at 3x premium.
However, in practice, it is ~30% extra runtime.
This is due to:
- Automaton is being reused for calculations on same
(blo, bhi)ranges and this can partially exploit it. - Gestalt approach is in a way
quicksortand this approach often nudges matches to be more “centered”. Thus, often results in more balanced split points.
It fixes the example case above in the same way as patience diff does.
However, as opposed to reducing number of matches in other cases, it (mostly) improves upon them as well:
| case | Myer/LLCS | patient Myer | Gestalt | patient Gestalt | Gestalt SkewFix |
|---|---|---|---|---|---|
| (C) diff_key ( 194, 196) | 143 | 82 -61 | 82 -61 | 82 -61 | 110 -33 |
| (L) diff_key ( 15, 15) | 8 | 8 0 | 5 -3 | 8 0 | 8 0 |
| (C) diff_mid1 ( 537, 533) | 335 | 335 0 | 326 -9 | 326 -9 | 328 -7 |
| (L) diff_mid1 ( 28, 27) | 11 | 11 0 | 11 0 | 11 0 | 11 0 |
| (C) diff_mid2 (2907, 2907) | 2139 | 2000 -139 | 1827 -312 | 1717 -422 | 1842 -297 |
| (L) diff_mid2 ( 128, 128) | 73 | 63 -10 | 72 -1 | 63 -10 | 72 -1 |
| (C) diff_mid3 (3139, 3139) | 2266 | 2266 0 | 1329 -937 | 1329 -937 | 1329 -937 |
| (L) diff_mid3 ( 131, 131) | 63 | 62 -1 | 50 -13 | 62 -1 | 51 -12 |
| (L) diff_lib (2085, 2380) | 1930 | 1929 -1 | 1926 -4 | 1929 -1 | 1926 -4 |
| ----- | |||||
| Total: | 6968 | 6756 -212 | 5628 -1340 | 5527 -1441 | 5677 -1291 |
| Avg % diff: | 0% | 7.17% | 17.89% | 13.53% | 11.23% |
| Runtime (s): | 0.466 | 0.272 | 0.043 | 0.064 | 0.083 |