Update.
Table below for various diff cases represents scores based on scoring function
f(blocks) = \sum_i^n \text{len}(block_i)^{1.3}
Thus, it is not a sheer individual matched line count (for which naturally exact LCS algorithm delivers optimal result), but a metric biased towards retaining large blocks, which is the main idea behind readable diffs by using difflib’s Gestalt pattern matching approach.
Results would look much better for greedy Gestalt methodology with higher powers in the formula above – without quality improvements Gestalt’s optimization function can be thought of as f(blocks) = \sum_i^n \text{len}(block_i)^{p\rightarrow\infty}. While p=1.3 is adequately conservative and captures the real quality benefit by keeping the balance between amount of data matched and large block bias. It can be interpreted according to this equality: c^p == (0.9c)^p + (0.2c)^p - i.e. block of size c has the same value as 2 blocks: one of size 0.9c and another of size 0.2c.
Table explanation:
case column: name (length_of_seq1, length_of_seq2)
algorithm column: values are scores according to f(blocks) = \sum_i^n \text{len}(block_i)^{1.3}. Number on the right of it is difference of this score compared to the one Base.
avg % diff = \frac{\sum \frac{f(alg_i) - f(Base_i)}{f(Base_i)}}{\text{number of cases}}
total % diff is percentage difference of aggregate summed scores compared to Base
6 Algorithms:
base - standard exact LCS (implementation - bit vector)
Patient Myer - runs patience/histogram and defaults to (1) when runs out of options
difflib(aj=1) - SequenceMatcher(autojunk=True)
difflib(aj=0) - SequenceMatcher(autojunk=True)
Gestalt2 - O(n) time / O(n) space optimization of difflib(aj=0). As it can be seen, its results are identical are identical to difflib(aj=0).
Gestalt2B - (5), but with balancing=True, which is an integrated and improved “Skew Fix” feature from earlier discussion which aims to amend issues of greedy commitment. So far it seems that it manages to retain complexity while delivers bigger quality improvement compared to initial version.
| case |
Base |
Patient Myer |
difflib(aj=1) |
difflib(aj=0) |
Gestalt2 |
Gestalt2B |
| [L] diff_key ( 30, 30) |
32 |
32 0 |
28 -4 |
28 -4 |
28 -4 |
28 -4 |
| [L] diff_mid1 ( 56, 54) |
34 |
34 0 |
34 0 |
34 0 |
34 0 |
34 0 |
| [L] diff_mid2 ( 256, 256) |
297 |
301 4 |
240 -57 |
298 1 |
298 1 |
298 1 |
| [L] diff_mid3 ( 262, 262) |
224 |
226 2 |
105 -119 |
132 -92 |
132 -92 |
226 2 |
| [L] difflib.py ( 4082, 4650) |
16865 |
16865 0 |
16874 9 |
16876 11 |
16876 11 |
16880 15 |
| [L] linux_sched.c (22746, 23664) |
92266 |
92266 0 |
92349 83 |
92486 220 |
92486 220 |
92494 228 |
| [L] react.js ( 7110, 6674) |
16602 |
16602 0 |
16510 -92 |
16541 -61 |
16541 -61 |
16542 -60 |
| [L] three.js (72564, 73586) |
288327 |
288354 27 |
147526 -140801 |
288779 452 |
288779 452 |
288883 556 |
| [L] webpack.js ( 4654, 6858) |
3020 |
3020 0 |
2679 -341 |
2996 -24 |
2996 -24 |
3012 -8 |
| [L] difflib PR ( 4206, 4582) |
13352 |
13352 0 |
13347 -5 |
13623 271 |
13623 271 |
13625 273 |
|
|
|
|
|
|
|
| Total: |
431019 |
431052 33 |
289692 -141327 |
431793 774 |
431793 774 |
432022 1003 |
| Avg % diff: |
0% |
0.22% |
-14.54% |
-5.19% |
-5.19% |
-0.93% |
| Total % diff: |
0% |
0.01% |
-32.79% |
0.18% |
0.18% |
0.23% |
| Runtime (s): |
2.582 |
2.489 |
1.719 |
138.369 |
0.761 |
0.903 |
Performance
Here are aggregate timings for datasets above scaled by a factor of 4 (difflib(aj=0) is removed as it is uncertain when it would complete):
| Algorithm |
Runtime (s) |
| Base (Bit Vector LCS) |
27.757 |
| Patient Myer |
37.997 |
| difflib(aj=1) |
27.527 |
| Gestalt2 |
3.383 |
| Gestalt2B |
3.989 |
Quality improvement of Gestalt2B.
It is rather simple idea, which surprisingly manages to improve results in some properly tricky cases.
The core issue that it solves is likely best represented by this:
seq1 = 'aaaa+bbbb+cccc+dddd+eeee+ffff'
seq2 = 'bbbb-cccc-dddd-eeee-ffff-aaaa'
print(gestalt2(seq1, seq2, balancing=0, aligns=False))
# [(0, 25, 4)]
print(gestalt2(seq1, seq2, balancing=1, aligns=False))
# [(5, 0, 4), (10, 5, 4), (15, 10, 4), (20, 15, 4), (25, 20, 4)]
It also covers (to a reasonable degree) cases where the problem at hand is pure LCS:
seq1 = '_a___a_bcd_____a_ef_g__'
seq2 = 'cb+e+d+++af+g++'
print(gestalt2(seq1, seq2, balancing=0, aligns=False))
# [(1, 9, 1), (18, 10, 1), (20, 12, 1)]
print(gestalt2(seq1, seq2, balancing=1, aligns=False))
# [(7, 1, 1), (9, 5, 1), (15, 9, 1), (18, 10, 1), (20, 12, 1)]
To sum up
I have been working on more complex methods to achieve quality. However, I am content with what I have arrived here – it is unlikely that quality improvements of more complex methods would justify needed complexity and inevitable difference in performance – the above delivers reasonable and reliable quality with incredible performance. No fallbacks, no hybrid strategies, just original gestalt pattern matching with a bit of situational awareness. Implementation is manageable – (not counting SuffixAutomaton and some general utilities) it is roughly 300LOC (of which ~30% is “skew_fix” quality improvement).
I am pleased with the results.
Thank you to everyone who participated and followed - this was one of the most enjoyable discussions over my time here.