philiprehberger-graph 0.3.0 → 0.4.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: 8e3ace407427d0a54b4279defb1a165c04aa85f1286c2234be016e96a359ad27
4
- data.tar.gz: 03d2e98d913ee10c80ed8a2d18f5c896095e568f1e21f57dfb023815c55d8e72
3
+ metadata.gz: 73596f486ec100249fa2df0e4a9ef6aa1bdcd5e37e16b9b587dd6802f3fc9584
4
+ data.tar.gz: 0ddc969e510fb5543d9b981d53f73c15b53e73066defb0b7226998c79ef7ec15
5
5
  SHA512:
6
- metadata.gz: 61b9e62ef38c0993c979f032453f7a142f57079e0c46434265371ad7344e2dbcd8b7a9a32ebf92fa1ce22b5fbd42a1b55895479bdf83e7d6b8bce5c907502575
7
- data.tar.gz: 47f5e812b101281eff00b8d0e261caf2473e5f3fc47c5686800fb6a3d5e9c155fbedd67bcadfbe608c2dde0a89b79b8fa0141a350a21e6adf544af441ed49f74
6
+ metadata.gz: 4cffa4c6398bfa251b9b8f82704d883d36e4c2dc7f1296369ff073364b8a71d0256a8f75b056d78372a38ca99809e8a09c560d549da4b8b8efa38ccf35130811
7
+ data.tar.gz: 80ca432cc5c0b1b35c979ccfebb1e7409d9170687a99ae9f4336e5159ce72a8d063cd5c8f7cc4829a4e22d9bad34c8d329e4fb8e8d3a3a668876994455ac628d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-04-17
11
+
12
+ ### Added
13
+ - Enumerator-style node iteration (`each_node`)
14
+ - Enumerator-style edge iteration (`each_edge`)
15
+ - Distance-only shortest path query (`shortest_path_distance`)
16
+ - Graph complement operation (`complement`)
17
+
10
18
  ## [0.3.0] - 2026-04-10
11
19
 
12
20
  ### Added
data/README.md CHANGED
@@ -139,6 +139,44 @@ g.add_edge(:d, :c)
139
139
  g.strongly_connected_components # => [[:d, :c], [:b, :a]]
140
140
  ```
141
141
 
142
+ ### Iteration
143
+
144
+ ```ruby
145
+ g = Philiprehberger::Graph.new
146
+ g.add_edge(:a, :b)
147
+ g.add_edge(:a, :c)
148
+
149
+ g.each_node { |id| puts id }
150
+ g.each_edge { |e| puts "#{e[:from]} -> #{e[:to]}" }
151
+
152
+ # Enumerator chaining
153
+ high_degree = g.each_node.select { |id| g.degree(id) > 1 }
154
+ heavy_edges = g.each_edge.select { |e| e[:weight] > 5 }
155
+ ```
156
+
157
+ ### Shortest Path Distance
158
+
159
+ ```ruby
160
+ g = Philiprehberger::Graph.new
161
+ g.add_edge(:a, :b, weight: 1)
162
+ g.add_edge(:b, :c, weight: 2)
163
+ g.add_edge(:a, :c, weight: 10)
164
+
165
+ g.shortest_path_distance(:a, :c) # => 3
166
+ ```
167
+
168
+ ### Graph Complement
169
+
170
+ ```ruby
171
+ g = Philiprehberger::Graph.new
172
+ g.add_edge(:a, :b)
173
+ g.add_edge(:b, :c)
174
+
175
+ c = g.complement
176
+ c.edge?(:a, :c) # => true
177
+ c.edge?(:a, :b) # => false
178
+ ```
179
+
142
180
  ### Query Methods
143
181
 
144
182
  ```ruby
@@ -232,10 +270,13 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
232
270
  | `#node_count` | Number of nodes |
233
271
  | `#edge_count` | Number of edges |
234
272
  | `#empty?` | Whether the graph has no nodes |
273
+ | `#each_node` | Iterate nodes (yields or returns Enumerator) |
274
+ | `#each_edge` | Iterate edges (yields or returns Enumerator) |
235
275
  | `#path?(from, to)` | Check if a path exists |
236
276
  | `#bfs(start)` | Breadth-first search |
237
277
  | `#dfs(start)` | Depth-first search |
238
278
  | `#shortest_path(from, to)` | Dijkstra's shortest path |
279
+ | `#shortest_path_distance(from, to)` | Shortest distance (numeric) |
239
280
  | `#topological_sort` | Topological ordering (DAG only) |
240
281
  | `#cycle?` | Check for cycles |
241
282
  | `#connected_components` | Find connected components |
@@ -249,6 +290,7 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
249
290
  | `#subgraph(nodes)` | Extract a subgraph |
250
291
  | `#transpose` | Reverse all edges (directed) |
251
292
  | `#density` | Graph density (0.0 to 1.0) |
293
+ | `#complement` | Graph complement (missing edges) |
252
294
  | `#to_dot` | Serialize to DOT format |
253
295
  | `#to_json` | Serialize to JSON |
254
296
  | `.from_json(str)` | Deserialize from JSON |
@@ -17,6 +17,26 @@ module Philiprehberger
17
17
  @directed
18
18
  end
19
19
 
20
+ # Iterate over all node ids.
21
+ #
22
+ # @yield [Object] each node id
23
+ # @return [Enumerator] if no block given
24
+ def each_node(&)
25
+ return enum_for(:each_node) unless block_given?
26
+
27
+ @adjacency.each_key(&)
28
+ end
29
+
30
+ # Iterate over all edges as {from:, to:, weight:} hashes.
31
+ #
32
+ # @yield [Hash] each edge
33
+ # @return [Enumerator] if no block given
34
+ def each_edge(&)
35
+ return enum_for(:each_edge) unless block_given?
36
+
37
+ edges.each(&)
38
+ end
39
+
20
40
  # Add a node to the graph.
21
41
  #
22
42
  # @param id [Object] the node identifier
@@ -254,6 +274,36 @@ module Philiprehberger
254
274
  nil
255
275
  end
256
276
 
277
+ # Find the shortest distance between two nodes using Dijkstra's algorithm.
278
+ #
279
+ # @param from [Object] source node
280
+ # @param to [Object] destination node
281
+ # @return [Numeric, nil] the shortest distance, or nil if no path exists
282
+ def shortest_path_distance(from, to)
283
+ return nil unless @adjacency.key?(from) && @adjacency.key?(to)
284
+ return 0 if from == to
285
+
286
+ distances = Hash.new(Float::INFINITY)
287
+ distances[from] = 0
288
+ unvisited = @adjacency.keys.dup
289
+
290
+ until unvisited.empty?
291
+ current = unvisited.min_by { |n| distances[n] }
292
+ break if distances[current] == Float::INFINITY
293
+
294
+ unvisited.delete(current)
295
+
296
+ return distances[to] if current == to
297
+
298
+ (@adjacency[current] || []).each do |edge|
299
+ alt = distances[current] + edge[:weight]
300
+ distances[edge[:node]] = alt if alt < distances[edge[:node]]
301
+ end
302
+ end
303
+
304
+ nil
305
+ end
306
+
257
307
  # Topological sort (directed acyclic graphs only).
258
308
  #
259
309
  # @return [Array<Object>] nodes in topological order
@@ -365,6 +415,29 @@ module Philiprehberger
365
415
  m / max_edges
366
416
  end
367
417
 
418
+ # Return the complement graph.
419
+ # The complement contains edges between all pairs of nodes that are NOT connected
420
+ # in the original graph.
421
+ #
422
+ # @return [Graph] a new graph with complementary edges
423
+ def complement
424
+ g = self.class.new(directed: @directed)
425
+ @adjacency.each_key { |id| g.add_node(id) }
426
+
427
+ node_list = @adjacency.keys
428
+ node_list.each_with_index do |a, i|
429
+ targets = @directed ? node_list : node_list[(i + 1)..]
430
+ targets.each do |b|
431
+ next if a == b
432
+ next if edge?(a, b)
433
+
434
+ g.add_edge(a, b)
435
+ end
436
+ end
437
+
438
+ g
439
+ end
440
+
368
441
  # Find connected components (undirected) or weakly connected components (directed).
369
442
  #
370
443
  # @return [Array<Array<Object>>] arrays of node ids per component
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Graph
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.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 00:00:00.000000000 Z
11
+ date: 2026-04-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Graph data structure supporting directed and undirected modes with adjacency
14
14
  list storage. Includes BFS, DFS, Dijkstra shortest path, topological sort, cycle