philiprehberger-dependency_graph 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +3 -0
- data/lib/philiprehberger/dependency_graph/graph.rb +108 -0
- data/lib/philiprehberger/dependency_graph/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fc52e7f3c4c62be66dc6d19901b9d82c953fa8c5ea32f22b3cdada283c02fd5
|
|
4
|
+
data.tar.gz: d334350bfb017e0172ec0145718904875c94fd1568e6114f3a113b8f1a124ae1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf8205242047d62b202affb156583a13d89cdeb3db6d59fb7c4eeec1d9fb0b06ea23ffe5da925f37d97dafe0fb1b1cec628deba7bd24b33ac975e049add9758a
|
|
7
|
+
data.tar.gz: 5116b0ec4782836b7b74b0f7135b842af9774b2108a9820212f0a73466840769f623688b5384084ca78597fd5299aa4ebb9b9a1dfdf3d4be5c7b5cd5b2c1a11d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Graph#reverse` — return a new graph with all edges flipped (useful for dependent analysis)
|
|
14
|
+
- `Graph#all_dependents_of(item)` — transitive closure of items depending on a node
|
|
15
|
+
- `Graph#independent?(a, b)` — check whether two nodes are mutually unreachable
|
|
16
|
+
|
|
10
17
|
## [0.2.0] - 2026-04-03
|
|
11
18
|
|
|
12
19
|
### Added
|
data/README.md
CHANGED
|
@@ -128,6 +128,9 @@ graph.resolve # => [:a, :b, :c]
|
|
|
128
128
|
| `Graph#roots` | Nodes with no dependencies |
|
|
129
129
|
| `Graph#leaves` | Nodes with no dependents |
|
|
130
130
|
| `Graph#depth(item)` | Maximum dependency depth of a node |
|
|
131
|
+
| `Graph#reverse` | Return a new graph with all edges flipped |
|
|
132
|
+
| `Graph#all_dependents_of(item)` | All transitive dependents of a node |
|
|
133
|
+
| `Graph#independent?(a, b)` | Whether two nodes are mutually unreachable |
|
|
131
134
|
|
|
132
135
|
## Development
|
|
133
136
|
|
|
@@ -208,6 +208,110 @@ module Philiprehberger
|
|
|
208
208
|
@nodes.keys.reject { |node| depended_on.include?(node) }
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
+
# Merge another graph into this one, combining nodes and dependencies
|
|
212
|
+
#
|
|
213
|
+
# @param other [Graph] another graph to merge
|
|
214
|
+
# @return [self]
|
|
215
|
+
def merge(other)
|
|
216
|
+
raise Error, 'Can only merge Graph instances' unless other.is_a?(self.class)
|
|
217
|
+
|
|
218
|
+
other.nodes.each do |node, deps|
|
|
219
|
+
@nodes[node] ||= []
|
|
220
|
+
deps.each do |dep|
|
|
221
|
+
@nodes[node] << dep unless @nodes[node].include?(dep)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
self
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Remove a node and all edges referencing it
|
|
228
|
+
#
|
|
229
|
+
# @param item [Object] the item to remove
|
|
230
|
+
# @return [Boolean] true if the node existed, false otherwise
|
|
231
|
+
def remove(item)
|
|
232
|
+
return false unless @nodes.key?(item)
|
|
233
|
+
|
|
234
|
+
@nodes.delete(item)
|
|
235
|
+
@nodes.each_value { |deps| deps.delete(item) }
|
|
236
|
+
true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Total number of nodes in the graph
|
|
240
|
+
#
|
|
241
|
+
# @return [Integer]
|
|
242
|
+
def size
|
|
243
|
+
@nodes.size
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Whether the graph has no nodes
|
|
247
|
+
#
|
|
248
|
+
# @return [Boolean]
|
|
249
|
+
def empty?
|
|
250
|
+
@nodes.empty?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Return a new graph with all edges reversed (dependents become dependencies)
|
|
254
|
+
#
|
|
255
|
+
# @return [Graph] a new graph where each edge direction is flipped
|
|
256
|
+
def reverse
|
|
257
|
+
new_graph = self.class.new
|
|
258
|
+
@nodes.each_key { |node| new_graph.instance_variable_get(:@nodes)[node] ||= [] }
|
|
259
|
+
@nodes.each do |node, deps|
|
|
260
|
+
deps.each { |dep| new_graph.add(dep, depends_on: [node]) }
|
|
261
|
+
end
|
|
262
|
+
new_graph
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Return all transitive dependents of an item (direct + indirect)
|
|
266
|
+
#
|
|
267
|
+
# @param item [Object] the item to query
|
|
268
|
+
# @return [Array] all items that depend on this item, directly or transitively
|
|
269
|
+
def all_dependents_of(item)
|
|
270
|
+
return [] unless @nodes.key?(item)
|
|
271
|
+
|
|
272
|
+
visited = {}
|
|
273
|
+
queue = dependents_of(item)
|
|
274
|
+
result = []
|
|
275
|
+
|
|
276
|
+
until queue.empty?
|
|
277
|
+
dep = queue.shift
|
|
278
|
+
next if visited[dep]
|
|
279
|
+
|
|
280
|
+
visited[dep] = true
|
|
281
|
+
result << dep
|
|
282
|
+
queue.concat(dependents_of(dep))
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
result
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Check whether two nodes are independent (neither depends on the other transitively)
|
|
289
|
+
#
|
|
290
|
+
# @param node_a [Object]
|
|
291
|
+
# @param node_b [Object]
|
|
292
|
+
# @return [Boolean] true if neither node is reachable from the other
|
|
293
|
+
def independent?(node_a, node_b)
|
|
294
|
+
return false if node_a == node_b
|
|
295
|
+
return false unless @nodes.key?(node_a) && @nodes.key?(node_b)
|
|
296
|
+
|
|
297
|
+
!all_dependencies_of(node_a).include?(node_b) &&
|
|
298
|
+
!all_dependencies_of(node_b).include?(node_a)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Export the graph in Graphviz DOT format
|
|
302
|
+
#
|
|
303
|
+
# @param name [String] the digraph name
|
|
304
|
+
# @return [String] DOT source
|
|
305
|
+
def to_dot(name: 'G')
|
|
306
|
+
lines = ["digraph #{name} {"]
|
|
307
|
+
@nodes.each_key { |node| lines << " #{dot_quote(node)};" }
|
|
308
|
+
@nodes.each do |node, deps|
|
|
309
|
+
deps.each { |dep| lines << " #{dot_quote(node)} -> #{dot_quote(dep)};" }
|
|
310
|
+
end
|
|
311
|
+
lines << '}'
|
|
312
|
+
lines.join("\n")
|
|
313
|
+
end
|
|
314
|
+
|
|
211
315
|
# Calculate maximum dependency depth for a node (longest path from any root to this node)
|
|
212
316
|
#
|
|
213
317
|
# @param item [Object] the item to query
|
|
@@ -221,6 +325,10 @@ module Philiprehberger
|
|
|
221
325
|
|
|
222
326
|
private
|
|
223
327
|
|
|
328
|
+
def dot_quote(node)
|
|
329
|
+
%("#{node.to_s.gsub('"', '\"')}")
|
|
330
|
+
end
|
|
331
|
+
|
|
224
332
|
def build_path(visited, from, to)
|
|
225
333
|
path = [to]
|
|
226
334
|
current = to
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-dependency_graph
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.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-
|
|
11
|
+
date: 2026-04-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Build and resolve dependency graphs using topological sort, detect cycles,
|
|
14
14
|
generate parallel execution batches, query dependencies and dependents, find shortest
|