hashdiff 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/RELEASE.md +76 -0
- data/changelog.md +5 -0
- data/lib/hashdiff/lcs.rb +107 -43
- data/lib/hashdiff/linear_compare_array.rb +9 -8
- data/lib/hashdiff/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1cbec06648aef6ff8f519bab7e12a667121f4dec64a603f172198d818f245bb
|
|
4
|
+
data.tar.gz: fe86c5abd6c6c61b8820a3c90e3f09aa56233087831932f75da507e05212858d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e09cd08b570c6fb894bb4e904369f64061e78125a8b83462b22d5b37fb9b549afcd501a1bfc839e8aaf02ce552b420b121d08c12109ed74b47e2c8dc8b1b7957
|
|
7
|
+
data.tar.gz: ede8595360f0da6f409c4955d979f82fe6f72df3434485cb036d0277b6173fcb64371a6d810f89c2c0eeb55e850a16bfbc27c195d8191337d800c25eff5967d6
|
data/RELEASE.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Releasing Hashdiff
|
|
2
|
+
|
|
3
|
+
Prepare releases directly on `master`: update the changelog and version, make a
|
|
4
|
+
release commit, and push it to `origin/master`. Previous releases followed this
|
|
5
|
+
process (for example, `8846a2c` for 1.2.1). A release PR is not required.
|
|
6
|
+
|
|
7
|
+
## Prepare the release
|
|
8
|
+
|
|
9
|
+
1. Start with a clean working tree and bring `master` up to date:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git checkout master
|
|
13
|
+
git pull --ff-only origin master
|
|
14
|
+
git fetch --tags origin
|
|
15
|
+
git status --short
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. Review changes since the previous release, replacing `v1.2.1` with the
|
|
19
|
+
previous release tag:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git log --oneline v1.2.1..HEAD
|
|
23
|
+
git diff v1.2.1..HEAD -- lib spec
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Update `Hashdiff::VERSION` in `lib/hashdiff/version.rb`. The gemspec reads
|
|
27
|
+
this constant; it does not have a separate version to update.
|
|
28
|
+
|
|
29
|
+
4. Add a section at the top of `changelog.md` with the matching version and
|
|
30
|
+
release date (`YYYY-MM-DD`). Summarize changes users will notice and link to
|
|
31
|
+
the relevant pull requests and contributors.
|
|
32
|
+
|
|
33
|
+
## Validate and push
|
|
34
|
+
|
|
35
|
+
The commands below use 1.2.2 as an example; substitute the version being released.
|
|
36
|
+
|
|
37
|
+
1. Install development dependencies and run the same checks as CI (RSpec and
|
|
38
|
+
RuboCop):
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bundle install
|
|
42
|
+
bundle exec rake
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Stage and review the release files, then build the gem. The gemspec uses
|
|
46
|
+
`git ls-files`, so stage any new files that should be packaged before building.
|
|
47
|
+
Generated `.gem` files are ignored and should not be committed.
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
git add changelog.md lib/hashdiff/version.rb RELEASE.md
|
|
51
|
+
git diff --cached --check
|
|
52
|
+
git diff --cached
|
|
53
|
+
gem build hashdiff.gemspec
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
3. Commit and push directly to `master`, then check the GitHub Actions result:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
git commit -m "Release 1.2.2"
|
|
60
|
+
git push origin master
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Publish the gem
|
|
64
|
+
|
|
65
|
+
Pushing to `master` runs CI; it does not publish to RubyGems. When publishing the
|
|
66
|
+
gem, wait for CI to pass and use a maintainer account with RubyGems push access.
|
|
67
|
+
From the release commit, tag the release and publish the gem built above:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
git tag v1.2.2
|
|
71
|
+
git push origin v1.2.2
|
|
72
|
+
gem push hashdiff-1.2.2.gem
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Confirm that RubyGems lists the new version. If a version has already been
|
|
76
|
+
published, prepare a new version rather than reusing it or moving its release tag.
|
data/changelog.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## v1.2.2 2026-11-08
|
|
4
|
+
|
|
5
|
+
* Fix array diffs with `use_lcs: false` so that `patch!` can replay them correctly [#102](https://github.com/liufengyun/hashdiff/pull/102) ([@oboxodo](https://github.com/oboxodo))
|
|
6
|
+
* Reduce LCS memory use and improve performance for equal arrays and arrays with common leading or trailing items [#103](https://github.com/liufengyun/hashdiff/pull/103) ([@oboxodo](https://github.com/oboxodo))
|
|
7
|
+
|
|
3
8
|
## v1.2.1 2025-09-06
|
|
4
9
|
|
|
5
10
|
* Use HTTPS for the source in the Gemfile [#101](https://github.com/liufengyun/hashdiff/issues/101) ([@krzysiek1507](https://github.com/krzysiek1507))
|
data/lib/hashdiff/lcs.rb
CHANGED
|
@@ -6,61 +6,125 @@ module Hashdiff
|
|
|
6
6
|
# caculate array difference using LCS algorithm
|
|
7
7
|
# http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
|
|
8
8
|
def self.lcs(arraya, arrayb, options = {})
|
|
9
|
-
|
|
9
|
+
Lcs.call(arraya, arrayb, options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @private
|
|
13
|
+
#
|
|
14
|
+
# Finds the pairs [index in arraya, index in arrayb] of similar items that
|
|
15
|
+
# make up the longest common subsequence of the two arrays.
|
|
16
|
+
class Lcs
|
|
17
|
+
def self.call(arraya, arrayb, options = {})
|
|
18
|
+
return [] if arraya.empty? || arrayb.empty?
|
|
19
|
+
|
|
20
|
+
new(arraya, arrayb, options).call
|
|
21
|
+
end
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
# The table costs O(n * m), so it only covers the items between the common
|
|
24
|
+
# prefix and suffix. The result is the same as with a full table.
|
|
25
|
+
def call
|
|
26
|
+
pair_common_suffix
|
|
27
|
+
start = common_prefix_length
|
|
28
|
+
backtrack(build_table(start), start)
|
|
29
|
+
backtrack_through_prefix
|
|
30
|
+
pairs.reverse
|
|
31
|
+
end
|
|
12
32
|
|
|
13
|
-
|
|
33
|
+
private
|
|
14
34
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
vector = []
|
|
35
|
+
attr_reader :arraya, :arrayb, :options, :pairs
|
|
36
|
+
# the items the backtrack is at, walking from the last ones to the first
|
|
37
|
+
attr_accessor :a_index, :b_index
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
count = top > left ? top : left
|
|
39
|
+
def initialize(arraya, arrayb, options)
|
|
40
|
+
@arraya = arraya
|
|
41
|
+
@arrayb = arrayb
|
|
42
|
+
@options = { similarity: 0.8 }.merge!(options)
|
|
43
|
+
@options[:prefix] = Hashdiff.prefix_append_array_index(@options[:prefix], '*', @options)
|
|
44
|
+
@pairs = []
|
|
45
|
+
self.a_index = arraya.size - 1
|
|
46
|
+
self.b_index = arrayb.size - 1
|
|
47
|
+
end
|
|
30
48
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
:top
|
|
37
|
-
elsif ai.zero?
|
|
38
|
-
:left
|
|
39
|
-
else
|
|
40
|
-
:both
|
|
41
|
-
end
|
|
49
|
+
# The backtrack starts at the last items and pairs them whenever they are
|
|
50
|
+
# similar, so it always pairs the common suffix item by item.
|
|
51
|
+
def pair_common_suffix
|
|
52
|
+
pair_and_step while a_index >= 0 && b_index >= 0 && similar_at?(a_index, b_index)
|
|
53
|
+
end
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
def common_prefix_length
|
|
56
|
+
length = 0
|
|
57
|
+
length += 1 while length <= a_index && length <= b_index && similar_at?(length, length)
|
|
58
|
+
length
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Each cell holds the length of the LCS of the items up to it, and the
|
|
62
|
+
# direction the backtrack takes from it. When both directions give the
|
|
63
|
+
# same length it goes left.
|
|
64
|
+
#
|
|
65
|
+
# Past a common prefix, the lengths in a full table are the ones in this
|
|
66
|
+
# table plus the prefix length, so both backtracks take the same steps.
|
|
67
|
+
def build_table(start)
|
|
68
|
+
a_items = arraya[start...a_index + 1]
|
|
69
|
+
b_items = arrayb[start...b_index + 1]
|
|
70
|
+
table = []
|
|
71
|
+
b_items.each_with_index do |b_item, bi|
|
|
72
|
+
above = table[bi - 1] if bi > 0
|
|
73
|
+
row = table[bi] = []
|
|
74
|
+
a_items.each_with_index do |a_item, ai|
|
|
75
|
+
row[ai] = if Hashdiff.similar?(a_item, b_item, options)
|
|
76
|
+
[:topleft, (above && ai > 0 ? above[ai - 1][1] : 0) + 1]
|
|
77
|
+
else
|
|
78
|
+
top = above ? above[ai][1] : 0
|
|
79
|
+
left = ai > 0 ? row[ai - 1][1] : 0
|
|
80
|
+
top > left ? [:top, top] : [:left, left]
|
|
81
|
+
end
|
|
44
82
|
end
|
|
45
83
|
end
|
|
84
|
+
table
|
|
46
85
|
end
|
|
47
86
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
x -= 1
|
|
87
|
+
# Cells of length 0 point left, so once the middle has no more pairs the
|
|
88
|
+
# backtrack leaves the table into the prefix, as a full table's would.
|
|
89
|
+
def backtrack(table, start)
|
|
90
|
+
while a_index >= start && b_index >= start
|
|
91
|
+
direction = table[b_index - start][a_index - start].first
|
|
92
|
+
if direction == :topleft
|
|
93
|
+
pair_and_step
|
|
94
|
+
elsif direction == :top
|
|
95
|
+
self.b_index = b_index - 1
|
|
96
|
+
else
|
|
97
|
+
self.a_index = a_index - 1
|
|
98
|
+
end
|
|
61
99
|
end
|
|
62
100
|
end
|
|
63
101
|
|
|
64
|
-
|
|
102
|
+
# Takes the steps a full table would take once an index is in the prefix:
|
|
103
|
+
# it pairs similar items and otherwise steps towards the diagonal, where
|
|
104
|
+
# items are known to be similar. So the last prefix item can pair with a
|
|
105
|
+
# later copy:
|
|
106
|
+
#
|
|
107
|
+
# lcs(%w[x w], %w[x x z]) # => [[0, 1]]
|
|
108
|
+
def backtrack_through_prefix
|
|
109
|
+
while a_index >= 0 && b_index >= 0
|
|
110
|
+
if a_index == b_index || similar_at?(a_index, b_index)
|
|
111
|
+
pair_and_step
|
|
112
|
+
elsif b_index > a_index
|
|
113
|
+
self.b_index = b_index - 1
|
|
114
|
+
else
|
|
115
|
+
self.a_index = a_index - 1
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def similar_at?(index_a, index_b)
|
|
121
|
+
Hashdiff.similar?(arraya[index_a], arrayb[index_b], options)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pair_and_step
|
|
125
|
+
pairs << [a_index, b_index]
|
|
126
|
+
self.a_index = a_index - 1
|
|
127
|
+
self.b_index = b_index - 1
|
|
128
|
+
end
|
|
65
129
|
end
|
|
66
130
|
end
|
|
@@ -16,9 +16,6 @@ module Hashdiff
|
|
|
16
16
|
|
|
17
17
|
self.old_index = 0
|
|
18
18
|
self.new_index = 0
|
|
19
|
-
# by comparing the array lengths we can expect that a number of items
|
|
20
|
-
# are either added or removed
|
|
21
|
-
self.expected_additions = new_array.length - old_array.length
|
|
22
19
|
|
|
23
20
|
loop do
|
|
24
21
|
if extra_items_in_old_array?
|
|
@@ -40,7 +37,7 @@ module Hashdiff
|
|
|
40
37
|
private
|
|
41
38
|
|
|
42
39
|
attr_reader :old_array, :new_array, :options, :additions, :deletions, :differences
|
|
43
|
-
attr_accessor :old_index, :new_index
|
|
40
|
+
attr_accessor :old_index, :new_index
|
|
44
41
|
|
|
45
42
|
def initialize(old_array, new_array, options)
|
|
46
43
|
@old_array = old_array
|
|
@@ -60,6 +57,12 @@ module Hashdiff
|
|
|
60
57
|
new_index < new_array.length && old_index >= old_array.length
|
|
61
58
|
end
|
|
62
59
|
|
|
60
|
+
# by comparing the lengths of what is left of each array we can expect
|
|
61
|
+
# that a number of items are either added or removed
|
|
62
|
+
def expected_additions
|
|
63
|
+
(new_array.length - new_index) - (old_array.length - old_index)
|
|
64
|
+
end
|
|
65
|
+
|
|
63
66
|
def iterated_through_both_arrays?
|
|
64
67
|
old_index >= old_array.length && new_index >= new_array.length
|
|
65
68
|
end
|
|
@@ -69,7 +72,7 @@ module Hashdiff
|
|
|
69
72
|
return if difference.empty?
|
|
70
73
|
|
|
71
74
|
index_after_additions = index_of_match_after_additions
|
|
72
|
-
|
|
75
|
+
append_additions_before_match(index_after_additions)
|
|
73
76
|
|
|
74
77
|
index_after_deletions = index_of_match_after_deletions
|
|
75
78
|
append_deletions_before_match(index_after_deletions)
|
|
@@ -120,11 +123,10 @@ module Hashdiff
|
|
|
120
123
|
nil
|
|
121
124
|
end
|
|
122
125
|
|
|
123
|
-
def
|
|
126
|
+
def append_additions_before_match(match_index)
|
|
124
127
|
return unless match_index
|
|
125
128
|
|
|
126
129
|
(new_index...match_index).each { |i| append_addition(new_array[i], i) }
|
|
127
|
-
self.expected_additions = expected_additions - (match_index - new_index)
|
|
128
130
|
self.new_index = match_index
|
|
129
131
|
end
|
|
130
132
|
|
|
@@ -132,7 +134,6 @@ module Hashdiff
|
|
|
132
134
|
return unless match_index
|
|
133
135
|
|
|
134
136
|
(old_index...match_index).each { |i| append_deletion(old_array[i], i) }
|
|
135
|
-
self.expected_additions = expected_additions + (match_index - new_index)
|
|
136
137
|
self.old_index = match_index
|
|
137
138
|
end
|
|
138
139
|
|
data/lib/hashdiff/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hashdiff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Liu Fengyun
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-11-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bluecloth
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- Gemfile
|
|
98
98
|
- LICENSE
|
|
99
99
|
- README.md
|
|
100
|
+
- RELEASE.md
|
|
100
101
|
- Rakefile
|
|
101
102
|
- changelog.md
|
|
102
103
|
- hashdiff.gemspec
|