philiprehberger-differ 0.4.0 → 0.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e8573d84ddf309e3d702cb9b90d1b407c8963cd84cf62235e6620d586949437
4
- data.tar.gz: 79ddab362109ba0b8b7958ba77fe9242426a403904082e81b2c8db2f6246a2c2
3
+ metadata.gz: 43183caae823c98d43466bf7d1e6a2a5c993dc9f573d3d07352f3802fbc1b6d0
4
+ data.tar.gz: a7633ce9d9a19b69229548221475b8890198c272edf0bbb4e9d3fd3aae9eb912
5
5
  SHA512:
6
- metadata.gz: 585f857fb63da7b81aad2ded9a2948e4cf27b5d996c6afa76480a0ff41397d3181ab02cad53ab285a8d629f15df669c6372d2d7d220b68d27168dac72f8993be
7
- data.tar.gz: 20e291749bdbec14d58031833b02c9de65dc5f40bd3675407b478ce8dce20efeeb613ca9e28062b97628fd8a32cf44eb98df5e781964b5f70ac85fe1a09a6618
6
+ metadata.gz: 97bea10cc3793229177af29091a41a7af415ef1c383150604bffc8492a4f4dc4c0f57d78d7cbc9241c86ba3f2fa0c41c5170aafc1098f9fbe8b2e47129799b25
7
+ data.tar.gz: 8d728725be558d50bc31124070f43e47a7980971c75d78bc9150877bcc6dde91c52b1191514134492bae5948eff763ca70f92c4e755bb2c18d5bfb854b959bca
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-05-01
11
+
12
+ ### Added
13
+ - `Philiprehberger::Differ.invert(changeset)` — return a changeset with additions/removals swapped and changed values reversed; lets callers compute the inverse diff without rerunning the comparator
14
+
15
+ ## [0.5.0] - 2026-04-21
16
+
17
+ ### Added
18
+ - `Philiprehberger::Differ.stats` — structured counts of added/removed/changed paths in a changeset
19
+
20
+ ### Fixed
21
+ - `bug_report.yml` — require Ruby version; add Gem version input per guide
22
+
10
23
  ## [0.4.0] - 2026-04-10
11
24
 
12
25
  ### Added
data/README.md CHANGED
@@ -125,6 +125,17 @@ changeset = Philiprehberger::Differ.diff(old_data, new_data)
125
125
  user_changes = Philiprehberger::Differ.subset(changeset, 'user')
126
126
  ```
127
127
 
128
+ ### Inverting a Diff
129
+
130
+ Return the inverse of a changeset — additions and removals swap, and
131
+ changed values are reversed. Applying the inverse undoes the original:
132
+
133
+ ```ruby
134
+ forward = Philiprehberger::Differ.diff(old_data, new_data)
135
+ reverse = Philiprehberger::Differ.invert(forward)
136
+ reverse.apply(forward.apply(old_data)) # => old_data
137
+ ```
138
+
128
139
  ### Three-Way Merge
129
140
 
130
141
  ```ruby
@@ -139,6 +150,20 @@ result[:conflicts] # => [{ path:, theirs:, ours: }]
139
150
  Philiprehberger::Differ.breaking_changes?(changeset) # => true/false
140
151
  ```
141
152
 
153
+ ### Change Statistics
154
+
155
+ Get a structured count summary of a changeset:
156
+
157
+ ```ruby
158
+ changeset = Philiprehberger::Differ.diff(
159
+ { name: 'Alice', age: 30, id: 1 },
160
+ { name: 'Bob', email: 'bob@example.com' }
161
+ )
162
+
163
+ Philiprehberger::Differ.stats(changeset)
164
+ # => { added: 1, removed: 2, changed: 1, total: 4, paths: 4 }
165
+ ```
166
+
142
167
  ## API
143
168
 
144
169
  ### `Philiprehberger::Differ.diff(old_val, new_val, ignore: [], array_key: nil)`
@@ -172,8 +197,10 @@ Returns a Float between 0.0 (completely different) and 1.0 (identical).
172
197
  | Method | Description |
173
198
  |---|---|
174
199
  | `Differ.subset(changeset, path)` | Filter changes to a specific path prefix |
200
+ | `Differ.invert(changeset)` | Return a new changeset that reverses the original |
175
201
  | `Differ.merge(base, theirs, ours)` | Three-way merge with conflict detection |
176
202
  | `Differ.breaking_changes?(changeset)` | Detect removals and type changes |
203
+ | `.stats(changeset)` | Count summary of a changeset |
177
204
 
178
205
  ### `Change`
179
206
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Differ
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -33,6 +33,40 @@ module Philiprehberger
33
33
  Changeset.new(filtered)
34
34
  end
35
35
 
36
+ # Return the inverse of a changeset.
37
+ #
38
+ # Additions become removals (and vice versa), and changed entries swap
39
+ # their old and new values. Applying the inverted changeset to a hash
40
+ # that has already had the original changeset applied will undo it
41
+ # without keeping the prior state around.
42
+ #
43
+ # @param changeset [Changeset] the changeset to invert
44
+ # @return [Changeset] a new changeset with each change reversed
45
+ # @raise [ArgumentError] if `changeset` is not a {Changeset}
46
+ def self.invert(changeset)
47
+ raise ArgumentError, 'changeset must be a Philiprehberger::Differ::Changeset' unless changeset.is_a?(Changeset)
48
+
49
+ reversed = changeset.changes.map do |change|
50
+ case change.type
51
+ when :added
52
+ Change.new(path: change.path, type: :removed, old_value: change.new_value)
53
+ when :removed
54
+ Change.new(path: change.path, type: :added, new_value: change.old_value)
55
+ when :changed
56
+ Change.new(
57
+ path: change.path,
58
+ type: :changed,
59
+ old_value: change.new_value,
60
+ new_value: change.old_value
61
+ )
62
+ else
63
+ change
64
+ end
65
+ end
66
+
67
+ Changeset.new(reversed)
68
+ end
69
+
36
70
  # Perform a three-way merge with conflict detection
37
71
  #
38
72
  # @param base [Hash] the common ancestor
@@ -56,6 +90,31 @@ module Philiprehberger
56
90
  { merged: merged, conflicts: conflicts }
57
91
  end
58
92
 
93
+ # Structured count summary of a changeset.
94
+ #
95
+ # Returns a hash of integer counts for the added, removed, and changed
96
+ # entries in the changeset, along with a running total and the number of
97
+ # unique paths. This method is read-only and never mutates the changeset.
98
+ #
99
+ # @param changeset [Changeset] the changeset to summarize
100
+ # @return [Hash{Symbol => Integer}] counts by kind — `{ added:, removed:, changed:, total:, paths: }`
101
+ # @raise [ArgumentError] if `changeset` is not a {Changeset}
102
+ def self.stats(changeset)
103
+ raise ArgumentError, 'changeset must be a Philiprehberger::Differ::Changeset' unless changeset.is_a?(Changeset)
104
+
105
+ added = changeset.added.length
106
+ removed = changeset.removed.length
107
+ changed = changeset.changed.length
108
+
109
+ {
110
+ added: added,
111
+ removed: removed,
112
+ changed: changed,
113
+ total: added + removed + changed,
114
+ paths: changeset.paths.length
115
+ }
116
+ end
117
+
59
118
  # Detect if a changeset contains breaking changes (removals or type changes)
60
119
  #
61
120
  # @param changeset [Changeset] the changeset to check
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-differ
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-10 00:00:00.000000000 Z
11
+ date: 2026-05-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Ruby library for deep structural diffing of hashes, arrays, and nested
14
14
  objects with apply/revert, JSON Patch output, similarity scoring, and configurable