philiprehberger-graph 0.3.0 → 0.5.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: 178b5edb15123981edaaa93532c454677d40753f5ff3729fbe62026c0020f768
4
+ data.tar.gz: 3446356ba683b0077338dfa55dc052e36546c39d9606e67e0e37ad114368cdfd
5
5
  SHA512:
6
- metadata.gz: 61b9e62ef38c0993c979f032453f7a142f57079e0c46434265371ad7344e2dbcd8b7a9a32ebf92fa1ce22b5fbd42a1b55895479bdf83e7d6b8bce5c907502575
7
- data.tar.gz: 47f5e812b101281eff00b8d0e261caf2473e5f3fc47c5686800fb6a3d5e9c155fbedd67bcadfbe608c2dde0a89b79b8fa0141a350a21e6adf544af441ed49f74
6
+ metadata.gz: f24fbd86ab0237395a61f5b012091817c4b152b24af2797e7264b2e853177fba4502a234863f1c56c83ecd9adb4bd11f5f0af4d804fae3afdf820a574ae09530
7
+ data.tar.gz: 16ecb22d98f3a29f2db9d12ae2f08d7ca4e5b03abd0af0c6a348dc2706a6bd4b0bec4e966a05bf662166c857bd6f73c06842c0cd3610caeafeba8454d9e1bf77
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.5.0] - 2026-04-29
11
+
12
+ ### Added
13
+ - `Graph#total_weight` returning the sum of edge weights across all edges (cheap baseline for MST/flow comparisons)
14
+
15
+ ## [0.4.0] - 2026-04-17
16
+
17
+ ### Added
18
+ - Enumerator-style node iteration (`each_node`)
19
+ - Enumerator-style edge iteration (`each_edge`)
20
+ - Distance-only shortest path query (`shortest_path_distance`)
21
+ - Graph complement operation (`complement`)
22
+
10
23
  ## [0.3.0] - 2026-04-10
11
24
 
12
25
  ### Added
data/README.md CHANGED
@@ -139,6 +139,54 @@ 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
+
180
+ ### Total Edge Weight
181
+
182
+ ```ruby
183
+ g = Philiprehberger::Graph.new
184
+ g.add_edge(:a, :b, weight: 3)
185
+ g.add_edge(:b, :c, weight: 5)
186
+
187
+ g.total_weight # => 8
188
+ ```
189
+
142
190
  ### Query Methods
143
191
 
144
192
  ```ruby
@@ -231,11 +279,15 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
231
279
  | `#edges` | All edges as hashes |
232
280
  | `#node_count` | Number of nodes |
233
281
  | `#edge_count` | Number of edges |
282
+ | `#total_weight` | Sum of edge weights across all edges |
234
283
  | `#empty?` | Whether the graph has no nodes |
284
+ | `#each_node` | Iterate nodes (yields or returns Enumerator) |
285
+ | `#each_edge` | Iterate edges (yields or returns Enumerator) |
235
286
  | `#path?(from, to)` | Check if a path exists |
236
287
  | `#bfs(start)` | Breadth-first search |
237
288
  | `#dfs(start)` | Depth-first search |
238
289
  | `#shortest_path(from, to)` | Dijkstra's shortest path |
290
+ | `#shortest_path_distance(from, to)` | Shortest distance (numeric) |
239
291
  | `#topological_sort` | Topological ordering (DAG only) |
240
292
  | `#cycle?` | Check for cycles |
241
293
  | `#connected_components` | Find connected components |
@@ -249,6 +301,7 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
249
301
  | `#subgraph(nodes)` | Extract a subgraph |
250
302
  | `#transpose` | Reverse all edges (directed) |
251
303
  | `#density` | Graph density (0.0 to 1.0) |
304
+ | `#complement` | Graph complement (missing edges) |
252
305
  | `#to_dot` | Serialize to DOT format |
253
306
  | `#to_json` | Serialize to JSON |
254
307
  | `.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
@@ -157,6 +177,17 @@ module Philiprehberger
157
177
  edges.length
158
178
  end
159
179
 
180
+ # Sum of weights across all edges. Returns 0 for an empty or edgeless graph.
181
+ #
182
+ # For undirected graphs each edge is counted once.
183
+ #
184
+ # @return [Numeric]
185
+ def total_weight
186
+ total = 0
187
+ each_edge { |edge| total += edge[:weight] }
188
+ total
189
+ end
190
+
160
191
  # Whether the graph has no nodes.
161
192
  #
162
193
  # @return [Boolean]
@@ -254,6 +285,36 @@ module Philiprehberger
254
285
  nil
255
286
  end
256
287
 
288
+ # Find the shortest distance between two nodes using Dijkstra's algorithm.
289
+ #
290
+ # @param from [Object] source node
291
+ # @param to [Object] destination node
292
+ # @return [Numeric, nil] the shortest distance, or nil if no path exists
293
+ def shortest_path_distance(from, to)
294
+ return nil unless @adjacency.key?(from) && @adjacency.key?(to)
295
+ return 0 if from == to
296
+
297
+ distances = Hash.new(Float::INFINITY)
298
+ distances[from] = 0
299
+ unvisited = @adjacency.keys.dup
300
+
301
+ until unvisited.empty?
302
+ current = unvisited.min_by { |n| distances[n] }
303
+ break if distances[current] == Float::INFINITY
304
+
305
+ unvisited.delete(current)
306
+
307
+ return distances[to] if current == to
308
+
309
+ (@adjacency[current] || []).each do |edge|
310
+ alt = distances[current] + edge[:weight]
311
+ distances[edge[:node]] = alt if alt < distances[edge[:node]]
312
+ end
313
+ end
314
+
315
+ nil
316
+ end
317
+
257
318
  # Topological sort (directed acyclic graphs only).
258
319
  #
259
320
  # @return [Array<Object>] nodes in topological order
@@ -365,6 +426,29 @@ module Philiprehberger
365
426
  m / max_edges
366
427
  end
367
428
 
429
+ # Return the complement graph.
430
+ # The complement contains edges between all pairs of nodes that are NOT connected
431
+ # in the original graph.
432
+ #
433
+ # @return [Graph] a new graph with complementary edges
434
+ def complement
435
+ g = self.class.new(directed: @directed)
436
+ @adjacency.each_key { |id| g.add_node(id) }
437
+
438
+ node_list = @adjacency.keys
439
+ node_list.each_with_index do |a, i|
440
+ targets = @directed ? node_list : node_list[(i + 1)..]
441
+ targets.each do |b|
442
+ next if a == b
443
+ next if edge?(a, b)
444
+
445
+ g.add_edge(a, b)
446
+ end
447
+ end
448
+
449
+ g
450
+ end
451
+
368
452
  # Find connected components (undirected) or weakly connected components (directed).
369
453
  #
370
454
  # @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.5.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.5.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-29 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