philiprehberger-graph 0.2.4 → 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 +16 -0
- data/README.md +69 -0
- data/lib/philiprehberger/graph/graph.rb +160 -0
- data/lib/philiprehberger/graph/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e3ace407427d0a54b4279defb1a165c04aa85f1286c2234be016e96a359ad27
|
|
4
|
+
data.tar.gz: 03d2e98d913ee10c80ed8a2d18f5c896095e568f1e21f57dfb023815c55d8e72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61b9e62ef38c0993c979f032453f7a142f57079e0c46434265371ad7344e2dbcd8b7a9a32ebf92fa1ce22b5fbd42a1b55895479bdf83e7d6b8bce5c907502575
|
|
7
|
+
data.tar.gz: 47f5e812b101281eff00b8d0e261caf2473e5f3fc47c5686800fb6a3d5e9c155fbedd67bcadfbe608c2dde0a89b79b8fa0141a350a21e6adf544af441ed49f74
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ 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-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Node and edge existence checks (`node?`, `edge?`)
|
|
14
|
+
- Edge weight retrieval (`weight`)
|
|
15
|
+
- Graph size queries (`node_count`, `edge_count`, `empty?`)
|
|
16
|
+
- Directed graph degree decomposition (`in_degree`, `out_degree`)
|
|
17
|
+
- Path existence check (`path?`)
|
|
18
|
+
- Subgraph extraction (`subgraph`)
|
|
19
|
+
- Graph transpose (`transpose`)
|
|
20
|
+
- Graph density calculation (`density`)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Update gemspec description to reflect full feature set
|
|
24
|
+
- Add missing fields to GitHub issue templates
|
|
25
|
+
|
|
10
26
|
## [0.2.4] - 2026-04-08
|
|
11
27
|
|
|
12
28
|
### Changed
|
data/README.md
CHANGED
|
@@ -139,6 +139,63 @@ g.add_edge(:d, :c)
|
|
|
139
139
|
g.strongly_connected_components # => [[:d, :c], [:b, :a]]
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
### Query Methods
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
g = Philiprehberger::Graph.new
|
|
146
|
+
g.add_edge(:a, :b, weight: 3)
|
|
147
|
+
g.add_edge(:b, :c)
|
|
148
|
+
g.add_node(:d)
|
|
149
|
+
|
|
150
|
+
g.node?(:a) # => true
|
|
151
|
+
g.edge?(:a, :b) # => true
|
|
152
|
+
g.weight(:a, :b) # => 3
|
|
153
|
+
g.node_count # => 4
|
|
154
|
+
g.edge_count # => 2
|
|
155
|
+
g.empty? # => false
|
|
156
|
+
g.path?(:a, :c) # => true
|
|
157
|
+
g.path?(:a, :d) # => false
|
|
158
|
+
g.density # => 0.333...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Directed Degree
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
g = Philiprehberger::Graph.new(directed: true)
|
|
165
|
+
g.add_edge(:a, :b)
|
|
166
|
+
g.add_edge(:a, :c)
|
|
167
|
+
g.add_edge(:b, :c)
|
|
168
|
+
|
|
169
|
+
g.in_degree(:c) # => 2
|
|
170
|
+
g.out_degree(:a) # => 2
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Subgraph
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
g = Philiprehberger::Graph.new
|
|
177
|
+
g.add_edge(:a, :b)
|
|
178
|
+
g.add_edge(:b, :c)
|
|
179
|
+
g.add_edge(:c, :d)
|
|
180
|
+
|
|
181
|
+
sg = g.subgraph([:a, :b, :c])
|
|
182
|
+
sg.nodes # => [:a, :b, :c]
|
|
183
|
+
sg.edge?(:a, :b) # => true
|
|
184
|
+
sg.node?(:d) # => false
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Transpose
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
g = Philiprehberger::Graph.new(directed: true)
|
|
191
|
+
g.add_edge(:a, :b)
|
|
192
|
+
g.add_edge(:b, :c)
|
|
193
|
+
|
|
194
|
+
t = g.transpose
|
|
195
|
+
t.edge?(:b, :a) # => true
|
|
196
|
+
t.edge?(:c, :b) # => true
|
|
197
|
+
```
|
|
198
|
+
|
|
142
199
|
### Serialization
|
|
143
200
|
|
|
144
201
|
```ruby
|
|
@@ -163,10 +220,19 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
|
|
|
163
220
|
| `#add_edge(from, to, weight:)` | Add a weighted edge |
|
|
164
221
|
| `#remove_node(id)` | Remove a node and its edges |
|
|
165
222
|
| `#remove_edge(from, to)` | Remove an edge |
|
|
223
|
+
| `#node?(id)` | Check if a node exists |
|
|
224
|
+
| `#edge?(from, to)` | Check if an edge exists |
|
|
225
|
+
| `#weight(from, to)` | Get edge weight (nil if none) |
|
|
166
226
|
| `#neighbors(node)` | Get neighbor node ids |
|
|
167
227
|
| `#degree(node)` | Number of edges on a node |
|
|
228
|
+
| `#in_degree(node)` | Incoming edges (directed) |
|
|
229
|
+
| `#out_degree(node)` | Outgoing edges (directed) |
|
|
168
230
|
| `#nodes` | All node ids |
|
|
169
231
|
| `#edges` | All edges as hashes |
|
|
232
|
+
| `#node_count` | Number of nodes |
|
|
233
|
+
| `#edge_count` | Number of edges |
|
|
234
|
+
| `#empty?` | Whether the graph has no nodes |
|
|
235
|
+
| `#path?(from, to)` | Check if a path exists |
|
|
170
236
|
| `#bfs(start)` | Breadth-first search |
|
|
171
237
|
| `#dfs(start)` | Depth-first search |
|
|
172
238
|
| `#shortest_path(from, to)` | Dijkstra's shortest path |
|
|
@@ -180,6 +246,9 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
|
|
|
180
246
|
| `#bipartite?` | Check if graph is bipartite |
|
|
181
247
|
| `#bipartite_sets` | Get bipartite partition sets |
|
|
182
248
|
| `#strongly_connected_components` | Tarjan's SCC algorithm |
|
|
249
|
+
| `#subgraph(nodes)` | Extract a subgraph |
|
|
250
|
+
| `#transpose` | Reverse all edges (directed) |
|
|
251
|
+
| `#density` | Graph density (0.0 to 1.0) |
|
|
183
252
|
| `#to_dot` | Serialize to DOT format |
|
|
184
253
|
| `#to_json` | Serialize to JSON |
|
|
185
254
|
| `.from_json(str)` | Deserialize from JSON |
|
|
@@ -79,6 +79,63 @@ module Philiprehberger
|
|
|
79
79
|
(@adjacency[node] || []).length
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Return the in-degree of a node (directed graphs).
|
|
83
|
+
# For undirected graphs, returns the same as degree.
|
|
84
|
+
#
|
|
85
|
+
# @param node [Object] the node
|
|
86
|
+
# @return [Integer]
|
|
87
|
+
def in_degree(node)
|
|
88
|
+
return degree(node) unless @directed
|
|
89
|
+
|
|
90
|
+
count = 0
|
|
91
|
+
@adjacency.each_value do |edges_list|
|
|
92
|
+
edges_list.each { |e| count += 1 if e[:node] == node }
|
|
93
|
+
end
|
|
94
|
+
count
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Return the out-degree of a node (directed graphs).
|
|
98
|
+
# For undirected graphs, returns the same as degree.
|
|
99
|
+
#
|
|
100
|
+
# @param node [Object] the node
|
|
101
|
+
# @return [Integer]
|
|
102
|
+
def out_degree(node)
|
|
103
|
+
return degree(node) unless @directed
|
|
104
|
+
|
|
105
|
+
(@adjacency[node] || []).length
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if a node exists.
|
|
109
|
+
#
|
|
110
|
+
# @param id [Object] the node identifier
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def node?(id)
|
|
113
|
+
@adjacency.key?(id)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check if an edge exists between two nodes.
|
|
117
|
+
#
|
|
118
|
+
# @param from [Object] source node
|
|
119
|
+
# @param to [Object] destination node
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def edge?(from, to)
|
|
122
|
+
return false unless @adjacency.key?(from)
|
|
123
|
+
|
|
124
|
+
@adjacency[from].any? { |e| e[:node] == to }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get the weight of an edge.
|
|
128
|
+
#
|
|
129
|
+
# @param from [Object] source node
|
|
130
|
+
# @param to [Object] destination node
|
|
131
|
+
# @return [Numeric, nil] the edge weight, or nil if no edge exists
|
|
132
|
+
def weight(from, to)
|
|
133
|
+
return nil unless @adjacency.key?(from)
|
|
134
|
+
|
|
135
|
+
edge = @adjacency[from].find { |e| e[:node] == to }
|
|
136
|
+
edge&.dig(:weight)
|
|
137
|
+
end
|
|
138
|
+
|
|
82
139
|
# Return all node ids.
|
|
83
140
|
#
|
|
84
141
|
# @return [Array<Object>]
|
|
@@ -86,6 +143,27 @@ module Philiprehberger
|
|
|
86
143
|
@adjacency.keys
|
|
87
144
|
end
|
|
88
145
|
|
|
146
|
+
# Return the number of nodes.
|
|
147
|
+
#
|
|
148
|
+
# @return [Integer]
|
|
149
|
+
def node_count
|
|
150
|
+
@adjacency.length
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Return the number of edges.
|
|
154
|
+
#
|
|
155
|
+
# @return [Integer]
|
|
156
|
+
def edge_count
|
|
157
|
+
edges.length
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Whether the graph has no nodes.
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
def empty?
|
|
164
|
+
@adjacency.empty?
|
|
165
|
+
end
|
|
166
|
+
|
|
89
167
|
# Return all edges as [from, to, weight] tuples.
|
|
90
168
|
#
|
|
91
169
|
# @return [Array<Hash>]
|
|
@@ -205,6 +283,88 @@ module Philiprehberger
|
|
|
205
283
|
end
|
|
206
284
|
end
|
|
207
285
|
|
|
286
|
+
# Check if a path exists between two nodes using BFS.
|
|
287
|
+
#
|
|
288
|
+
# @param from [Object] source node
|
|
289
|
+
# @param to [Object] destination node
|
|
290
|
+
# @return [Boolean]
|
|
291
|
+
def path?(from, to)
|
|
292
|
+
return false unless @adjacency.key?(from) && @adjacency.key?(to)
|
|
293
|
+
return true if from == to
|
|
294
|
+
|
|
295
|
+
visited = { from => true }
|
|
296
|
+
queue = [from]
|
|
297
|
+
|
|
298
|
+
until queue.empty?
|
|
299
|
+
node = queue.shift
|
|
300
|
+
neighbors(node).each do |neighbor|
|
|
301
|
+
return true if neighbor == to
|
|
302
|
+
|
|
303
|
+
unless visited[neighbor]
|
|
304
|
+
visited[neighbor] = true
|
|
305
|
+
queue << neighbor
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
false
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Extract a subgraph containing only the specified nodes and edges between them.
|
|
314
|
+
#
|
|
315
|
+
# @param node_ids [Array<Object>] the nodes to include
|
|
316
|
+
# @return [Graph] a new graph
|
|
317
|
+
def subgraph(node_ids)
|
|
318
|
+
node_set = node_ids.is_a?(Set) ? node_ids : Set.new(node_ids)
|
|
319
|
+
g = self.class.new(directed: @directed)
|
|
320
|
+
|
|
321
|
+
node_set.each { |id| g.add_node(id) if @adjacency.key?(id) }
|
|
322
|
+
|
|
323
|
+
@adjacency.each do |from, edges_list|
|
|
324
|
+
next unless node_set.include?(from)
|
|
325
|
+
|
|
326
|
+
edges_list.each do |edge|
|
|
327
|
+
next unless node_set.include?(edge[:node])
|
|
328
|
+
next if !@directed && from.to_s > edge[:node].to_s
|
|
329
|
+
|
|
330
|
+
g.add_edge(from, edge[:node], weight: edge[:weight])
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
g
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Return a new graph with all edges reversed.
|
|
338
|
+
# Only works on directed graphs.
|
|
339
|
+
#
|
|
340
|
+
# @return [Graph] a new graph with reversed edges
|
|
341
|
+
# @raise [Error] if the graph is not directed
|
|
342
|
+
def transpose
|
|
343
|
+
raise Error, 'transpose requires a directed graph' unless @directed
|
|
344
|
+
|
|
345
|
+
g = self.class.new(directed: true)
|
|
346
|
+
@adjacency.each_key { |id| g.add_node(id) }
|
|
347
|
+
@adjacency.each do |from, edges_list|
|
|
348
|
+
edges_list.each do |edge|
|
|
349
|
+
g.add_edge(edge[:node], from, weight: edge[:weight])
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
g
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Calculate the density of the graph.
|
|
356
|
+
# Density is the ratio of actual edges to possible edges.
|
|
357
|
+
#
|
|
358
|
+
# @return [Float] density between 0.0 and 1.0
|
|
359
|
+
def density
|
|
360
|
+
n = @adjacency.length
|
|
361
|
+
return 0.0 if n < 2
|
|
362
|
+
|
|
363
|
+
m = edge_count.to_f
|
|
364
|
+
max_edges = @directed ? n * (n - 1) : n * (n - 1) / 2.0
|
|
365
|
+
m / max_edges
|
|
366
|
+
end
|
|
367
|
+
|
|
208
368
|
# Find connected components (undirected) or weakly connected components (directed).
|
|
209
369
|
#
|
|
210
370
|
# @return [Array<Array<Object>>] arrays of node ids per component
|
metadata
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-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-11 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
|
|
15
|
-
detection,
|
|
15
|
+
detection, connected components, minimum spanning tree, maximum flow, graph coloring,
|
|
16
|
+
bipartiteness checking, strongly connected components, and DOT/JSON serialization.
|
|
16
17
|
email:
|
|
17
18
|
- me@philiprehberger.com
|
|
18
19
|
executables: []
|