graphs 0.2.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 712ed1650338434fe66d21f64141a38ffc156032
4
+ data.tar.gz: de13347bd21397c4e211748eaab0557193a52250
5
+ SHA512:
6
+ metadata.gz: 058240c841658dbcc58c8c76a74e049afda210d17b4a7b7476a3dd3c55c1624f93692cf316e40724dca4565a8b0b7df463addc795482ee7dec5c5d1878db1e71
7
+ data.tar.gz: 2f7b48932434a3f06b22745e8973157806784d8b087ebae412f3aca06c90298ec383e6bebfb5732555d8a530c39e52cd28a972c5d14f6d68c79b253f34f9dfc3
Binary file
@@ -0,0 +1 @@
1
+ K��iΑV�!�x�@^�|H��}K^חT�:q����CBu�ۗ�]%�|�q= *E,]����G��o��]��%�k��S�g"�A�D�y�u-].w��V#��T��a^��h�ெ�%�9{u���.z �Gt��ȸ��0�C�Bߐ�w� 8�S����nn��(��>���"�W`x��Բ�U�i�Q�c%KU�Zj��\�mX��Ó���dv ��Qr����}�YИ�3�6Z�!p��zc۩��IA�
@@ -0,0 +1,571 @@
1
+ #! /usr/bin/env ruby
2
+ # -*- coding: UTF-8 -*-
3
+
4
+ require 'yaml'
5
+
6
+ # A graph with nodes and edges
7
+ class Graph
8
+
9
+ # Return a new Graph which is the intersection of every given graph.
10
+ # Each node of the intersection is in every given graph (idem for edges).
11
+ # The last argument may be a hash of options.
12
+ # @option options [Boolean] +:same_fields+ use only fields which are in
13
+ # every graph to compare nodes/edges to perform
14
+ # the intersection
15
+ # @see Graph#&
16
+ # @see Graph.union
17
+ # @see Graph.xor
18
+ # @return [Graph]
19
+ def Graph::intersection(*graphs)
20
+ perform_graphs_group_op(*graphs, &:&)
21
+ end
22
+
23
+ # Return a new {Graph} which is the union of every given graph.
24
+ # Each node of the union is in one or more given graph(s) (idem for edges).
25
+ # The last argument may be a hash of options.
26
+ # @option options [Boolean] +:same_fields+ use only fields which are in
27
+ # every graph to compare nodes/edges to perform
28
+ # the union
29
+ # @see Graph#|
30
+ # @see Graph.intersection
31
+ # @see Graph.xor
32
+ # @return [Graph]
33
+ def Graph::union(*graphs)
34
+ perform_graphs_group_op(*graphs, &:|)
35
+ end
36
+
37
+ # Perform a XOR operation on all given graphs, and returns the result.
38
+ # The last argument may be a hash of options.
39
+ # @option options [Boolean] :same_fields use only fields which are in every
40
+ # graph to compare nodes/edges to perform the XOR operation
41
+ # @see Graph#^
42
+ # @see Graph.union
43
+ # @see Graph.intersection
44
+ # @return [Graph]
45
+ def Graph::xor(*graphs)
46
+ perform_graphs_group_op(*graphs, &:^)
47
+ end
48
+
49
+ # A node. This class is just a wrapper around a hash of attributes. Before
50
+ # 0.1.6, nodes were simple hashs
51
+ # @since 0.1.6
52
+ class Node
53
+
54
+ # @return Node's attributes
55
+ attr_accessor :attrs
56
+
57
+ # Create a new Node
58
+ # @param attrs [Node, Hash]
59
+ def initialize(attrs=nil)
60
+ @attrs = attrs.is_a?(Node) ? attrs.attrs : attrs || {}
61
+ end
62
+
63
+ # compare two nodes
64
+ # @param other [Node]
65
+ # @return [Boolean]
66
+ def ==(other)
67
+ return false if !other.is_a?(Node)
68
+
69
+ @attrs == other.attrs
70
+ end
71
+
72
+ # Update the current node, like the +Hash#update+ method.
73
+ # @param h [Hash]
74
+ # @return [Node]
75
+ def update(h)
76
+ Node.new super(h)
77
+ end
78
+
79
+ def method_missing(method, *args, &block)
80
+ return @attrs[method.to_sym] if @attrs.has_key? method.to_sym
81
+ return @attrs[method.to_s] if @attrs.has_key? method.to_s
82
+
83
+ @attrs.send(method, *args, &block)
84
+ end
85
+
86
+ end
87
+
88
+ # An edge. This class is just a wrapper around a hash of
89
+ # attributes since before version 0.1.5 edges were simple hashes
90
+ # @since 0.1.6
91
+ class Edge
92
+
93
+ # @return Edge's attributes
94
+ attr_accessor :attrs
95
+
96
+ # Create a new edge
97
+ # @param attrs [Edge, Hash]
98
+ def initialize(attrs=nil)
99
+ @attrs = attrs.is_a?(Edge) ? attrs.attrs : attrs || {}
100
+ end
101
+
102
+ # Compare two edges
103
+ # @param other [Edge]
104
+ # @return [Boolean]
105
+ def ==(other)
106
+ return false if !other.is_a?(Edge)
107
+
108
+ @attrs == other.attrs
109
+ end
110
+
111
+ # Update the current edge, like the +Hash#update+ method.
112
+ # @param h [Hash]
113
+ # @return [Edge]
114
+ def update(h)
115
+ Edge.new super(h)
116
+ end
117
+
118
+ def method_missing(method, *args, &block)
119
+ return @attrs[method.to_sym] if @attrs.has_key? method.to_sym
120
+ return @attrs[method.to_s] if @attrs.has_key? method.to_s
121
+
122
+ @attrs.send(method, *args, &block)
123
+ end
124
+
125
+ end
126
+
127
+ # An array of Node objects
128
+ class NodeArray < Array
129
+
130
+ # Create a new +NodeArray+ from an existing +Array+.
131
+ # @param li [Array]
132
+ def initialize(li)
133
+ nodes = li.map { |n| n.is_a?(Node) ? n : Node.new(n) }
134
+ super(nodes)
135
+ @defaults = {}
136
+ end
137
+
138
+ # Set some default values for current elements.
139
+ # @note This method can be called multiple times.
140
+ # @param dict [Hash]
141
+ # @return [NodeArray]
142
+ # @example Set all nodes's 'created-at' value to '2012-05-03'
143
+ # myNodeList.set_default({'created-at'=>'2012-05-03'})
144
+ def set_default(dict)
145
+ @defaults.update(dict)
146
+ self.map! { |e| e.update(@defaults) }
147
+ end
148
+
149
+ # Add the given node at the end of the list
150
+ # @param n [Node]
151
+ # @return [NodeArray]
152
+ def push(n)
153
+ if (!n.is_a?(Hash) && !n.is_a?(Node))
154
+ raise TypeError.new "#{n.inspect} is not an Hash nor a Node!"
155
+ end
156
+
157
+ n = Node.new(n) if (n.is_a?(Hash))
158
+
159
+ super(n.clone.update(@defaults))
160
+ end
161
+
162
+ end
163
+
164
+ # An array of Edge objects
165
+ class EdgeArray < Array
166
+
167
+ # Create a new +EdgeArray+ from an existing +Array+.
168
+ # @param li [Array<Edge, Hash>]
169
+ def initialize(li)
170
+ edges = li.map { |n| n.is_a?(Edge) ? n : Edge.new(n) }
171
+ super(edges)
172
+ @defaults = {}
173
+ end
174
+
175
+ # Set some default values for current elements.
176
+ # @note This method can be called multiple times.
177
+ # @example Set all edges's 'created-at' value to '2012-05-03'
178
+ # myEdgeList.set_default({'created-at'=>'2012-05-03'})
179
+ # @param dict [Hash]
180
+ def set_default(dict)
181
+ @defaults.update(dict)
182
+ self.map! { |e| e.update(@defaults) }
183
+ end
184
+
185
+ # Add the given edge at the end of the list
186
+ # @param e [Edge]
187
+ # @return [EdgeArray]
188
+ def push(e)
189
+ if (!e.is_a?(Hash) && !e.is_a?(Edge))
190
+ raise TypeError.new "#{e.inspect} is not an Hash nor an Edge!"
191
+ end
192
+
193
+ e = Edge.new(e) if (e.is_a?(Hash))
194
+
195
+ super(e.clone.update(@defaults))
196
+ end
197
+
198
+ end
199
+
200
+ # @return [NodeArray] the graph's nodes
201
+ attr_accessor :nodes
202
+
203
+ # @return [EdgeArray] the graph's edges
204
+ attr_accessor :edges
205
+
206
+ # @return [Hash] the graph's attributes
207
+ attr_accessor :attrs
208
+
209
+ # Create a new +Graph+ from one set of nodes and one set of edges
210
+ # @param nodes [Array] Nodes of the graph
211
+ # @param edges [Array] Edges of the graph
212
+ def initialize(nodes=nil, edges=nil)
213
+ @nodes = NodeArray.new(nodes || [])
214
+ @edges = EdgeArray.new(edges || [])
215
+ @attrs = { :directed => true }
216
+ end
217
+
218
+ # Test if current graph has same nodes and edges as the other
219
+ # graph.
220
+ # @param other [Graph]
221
+ # @return [Boolean]
222
+ def ==(other)
223
+ if (!other.is_a?(Graph))
224
+ return false
225
+ end
226
+ (self.nodes === other.nodes) && (self.edges === other.edges)
227
+ end
228
+
229
+ # Perform an intersection between the current graph and the other.
230
+ # Returns a new Graph which nodes are both in the current graph and
231
+ # the other (idem for edges).
232
+ # @param other [Graph]
233
+ # @return [Graph]
234
+ # @see Graph#^
235
+ # @see Graph.intersection
236
+ def &(other)
237
+ return unless other.is_a?(Graph)
238
+
239
+ nodes = @nodes & other.nodes
240
+ edges = @edges & other.edges
241
+
242
+ Graph.new(nodes, edges)
243
+ end
244
+
245
+ # Perform a XOR operation between the current graph and the other. Returns
246
+ # a new Graph which nodes are in the current graph or in the other, but not
247
+ # in both (idem for edges).
248
+ # @param other [Graph]
249
+ # @return [Graph]
250
+ # @see Graph#&
251
+ def ^(other)
252
+ return unless other.is_a?(Graph)
253
+
254
+ nodes = (@nodes - other.nodes) + (other.nodes - @nodes)
255
+ edges = (@edges - other.edges) + (other.edges - @edges)
256
+
257
+ Graph.new(nodes, edges)
258
+ end
259
+
260
+ # Add two graphs, keeping duplicate nodes and edges
261
+ # @param other [Graph]
262
+ # @return [Graph]
263
+ def +(other)
264
+ return unless other.is_a?(Graph)
265
+
266
+ nodes = @nodes + other.nodes
267
+ edges = @edges + other.edges
268
+
269
+ Graph.new(nodes, edges)
270
+ end
271
+
272
+ # Perform an OR operation on the current Graph and the given one. Returns a
273
+ # new graph which every node is in the current Graph and/or the other
274
+ # (idem for edges).
275
+ # @param other [Graph]
276
+ # @return [Graph]
277
+ def |(other)
278
+ return unless other.is_a?(Graph)
279
+
280
+ nodes = @nodes | other.nodes
281
+ edges = @edges | other.edges
282
+
283
+ Graph.new(nodes, edges)
284
+ end
285
+
286
+ # Returns a new Graph, which is a copy of the current graph without nodes
287
+ # and edges which are in the given Graph.
288
+ # @param other [Graph]
289
+ # @return [Graph]
290
+ def -(other)
291
+ return unless other.is_a?(Graph)
292
+
293
+ nodes = @nodes - other.nodes
294
+ edges = @edges - other.edges
295
+
296
+ Graph.new(nodes, edges)
297
+ end
298
+
299
+ # (see Graph#-)
300
+ def not(other)
301
+ self - other
302
+ end
303
+
304
+ # Return true if the Graph is directed.
305
+ # @return [Boolean]
306
+ # @see Graph.attrs
307
+ def directed?()
308
+ !!self.attrs[:directed]
309
+ end
310
+
311
+ # Clone the current graph. All nodes and edges are also cloned. A new Graph
312
+ # is returned.
313
+ # @return [Graph] a new graph
314
+ def clone()
315
+ g = Graph.new
316
+ g.nodes = self.nodes.clone
317
+ g.edges = self.edges.clone
318
+
319
+ g.nodes.map! {|h| h.clone}
320
+ g.edges.map! {|h| h.clone}
321
+
322
+ g
323
+ end
324
+
325
+ # Write the current Graph into a file.
326
+ # @param filename [String] A valid filename
327
+ # @param opts [Hash] A customizable set of options
328
+ # @return []
329
+ # @option opts [Boolean] :gephi Should be <tt>true</tt> if the file will be
330
+ # used with Gephi.
331
+ def write(filename, opts=nil)
332
+
333
+ has_ext = filename.split('.')
334
+ ext = (has_ext.length>1) ? has_ext[-1] : 'unknow'
335
+
336
+ m = (self.methods - Object.methods).map {|e| e.to_s}
337
+
338
+ if (m.include? 'write_'+ext.downcase)
339
+ self.send('write_'+ext.downcase, filename, opts)
340
+
341
+ elsif (ext == 'unknow' || ext == 'yml')
342
+ # YAML (default)
343
+ nodes = self.nodes.to_a
344
+ edges = self.edges.to_a
345
+
346
+ data = {'nodes'=>nodes, 'edges'=>edges}.to_yaml
347
+ f = open(filename, 'w')
348
+ f.write(data)
349
+ f.close
350
+ else
351
+ raise NoMethodError.new("No method to handle #{ext} file extension.")
352
+ end
353
+ end
354
+
355
+ # Return the degree of the node n in the current graph, i.e. the number
356
+ # of edges which are connected to this node. Note that this is useful
357
+ # only for a undirected graph, for a directed one, you should use
358
+ # Graph#in_degree_of and/or Graph#out_degree_of.
359
+ #
360
+ # Edges must have the +node1+ and +node2+ attributes, which must contain
361
+ # the +label+ attributes of nodes.
362
+ #
363
+ # @param n [Node,String] A node or a label of one
364
+ # @return [Integer]
365
+ # @see Graph#in_degree_of
366
+ # @see Graph#out_degree_of
367
+ def degree_of(n)
368
+ label = Graph::get_label(n)
369
+
370
+ degree = 0
371
+
372
+ # This is more efficient than in_degree_of(n)+out_degree_of(n)
373
+ # since it goes only once through the edges array
374
+ self.edges.each do |e|
375
+ degree += 1 if (e['node1'] || e[:node1]).to_s == label
376
+ degree += 1 if (e['node2'] || e[:node2]).to_s == label
377
+ end
378
+
379
+ degree
380
+ end
381
+
382
+ # Return the “in degree” of the node n in the current graph, i.e. the
383
+ # number of edges which are directed to this node. Note that the graph must
384
+ # be oriented.
385
+ #
386
+ # Edges must have the +node1+ and +node2+ attributes, which must contain
387
+ # the +label+ attributes of nodes.
388
+ #
389
+ # @param n [Node,String] A node or a label of one
390
+ # @return [Integer]
391
+ # @see Graph#degree_of
392
+ # @see Graph#out_degree_of
393
+ def in_degree_of(n)
394
+ label = Graph::get_label(n)
395
+
396
+ degree = 0
397
+
398
+ self.edges.each do |e|
399
+ degree += 1 if (e['node2'] || e[:node2]).to_s == label
400
+ end
401
+
402
+ degree
403
+ end
404
+
405
+ # Return the “out degree” of the node n in the current graph, i.e. the
406
+ # number of edges which are directed from this node. Note that the graph
407
+ # must be oriented.
408
+ #
409
+ # Edges must have the +node1+ and +node2+ attributes, which must contain
410
+ # the +label+ attributes of nodes.
411
+ #
412
+ # @param n [Node,String] A node or a node's label
413
+ # @return [Integer]
414
+ # @see Graph#degree_of
415
+ # @see Graph#out_degree_of
416
+ def out_degree_of(n)
417
+ label = Graph::get_label(n)
418
+
419
+ degree = 0
420
+
421
+ self.edges.each do |e|
422
+ degree += 1 if (e['node1'] || e[:node1]).to_s == label
423
+ end
424
+
425
+ degree
426
+ end
427
+
428
+ # return the first node which mach the given label in the current graph
429
+ # @param label [String] A node's label
430
+ # @return [Node]
431
+ def get_node(label)
432
+ label = Graph::get_label(label)
433
+
434
+ self.nodes.find { |n| n.label == label }
435
+ end
436
+
437
+ # return an array of the neighbours of a node in the current graph.
438
+ # @param n [Node,String] A node with a 'label' or :label attribute, or a
439
+ # string
440
+ # @return [Array<Node>]
441
+ def get_neighbours(n)
442
+
443
+ label = Graph::get_label n
444
+ neighbours = NodeArray.new []
445
+
446
+ self.edges.each do |e|
447
+
448
+ l1 = e[:node1] || e['node1']
449
+ l2 = e[:node2] || e['node2']
450
+
451
+ if l2 && l1 == label
452
+
453
+ n2 = self.get_node l2
454
+
455
+ unless n2.nil? || neighbours.include?(n2)
456
+
457
+ neighbours.push(n2)
458
+
459
+ end
460
+
461
+ end
462
+
463
+ if l1 && l2 == label && !self.directed?
464
+
465
+ n1 = self.get_node l1
466
+
467
+ unless n1.nil? || neighbours.include?(n1)
468
+
469
+ neighbours.push(n1)
470
+
471
+ end
472
+
473
+ end
474
+
475
+ end
476
+
477
+ neighbours
478
+
479
+ end
480
+
481
+ # return the label of a node. Raise a TypeError exception if the argument
482
+ # is not a Node nor a String object.
483
+ # @param n [Node,String] A node with a 'label' or :label attribute, or a
484
+ # string
485
+ # @return [String]
486
+ def Graph::get_label(n)
487
+ label = n.is_a?(Node) \
488
+ ? n.label.to_s \
489
+ : n.is_a?(String) ? n : nil
490
+
491
+ if label.nil?
492
+ raise TypeError.new("#{n.inspect} must be a Node or String object.")
493
+ end
494
+
495
+ label
496
+ end
497
+
498
+ private
499
+
500
+ # return the provided set of graphs, from which every node/edge label which
501
+ # is not in all graphs has been removed. So every returned graph has same
502
+ # node/edge labels than each other
503
+ def Graph::keep_only_same_fields(*graphs)
504
+ graphs.map! {|g| g.clone}
505
+
506
+ # every first node of every graphs
507
+ nodes_ref = graphs.map {|g| g.nodes[0] || {}}
508
+ # every first edge of every graphs
509
+ edges_ref = graphs.map {|g| g.edges[0] || {}}
510
+
511
+ nodes_keys_ref = nodes_ref.map {|n| n.keys}
512
+ edges_keys_ref = edges_ref.map {|e| e.keys}
513
+
514
+ # keep only same keys
515
+ nodes_keys_uniq = nodes_keys_ref.inject {|i,e| i &= e}
516
+ edges_keys_uniq = edges_keys_ref.inject {|i,e| i &= e}
517
+
518
+ graphs.map do |g|
519
+ g.nodes.map! do |n|
520
+
521
+ newnode = {}
522
+
523
+ n.each_key do |k|
524
+ newnode[k] = n[k] if nodes_keys_uniq.include?(k)
525
+ end
526
+
527
+ newnode
528
+ end
529
+ g.edges.map! do |n|
530
+
531
+ newedge = {}
532
+
533
+ n.each_key do |k|
534
+ newedge[k] = n[k] if edges_keys_uniq.include?(k)
535
+ end
536
+
537
+ newedge
538
+ end
539
+ g
540
+ end
541
+ end
542
+
543
+ # Perform an operation on a graphs group
544
+ # @param graphs [Array<Graph>]
545
+ # @param block [Block] operation
546
+ # @return [Graph]
547
+ def Graph::perform_graphs_group_op(*graphs, &block)
548
+ return if graphs.length == 0
549
+
550
+ # options
551
+ opts = {}
552
+
553
+ # if the last arg is an hash, use it as a set of options and remove it
554
+ # from the arguments
555
+ if graphs[-1].is_a?(Hash)
556
+ return if graphs.length == 1
557
+ opts = graphs.pop
558
+ end
559
+
560
+ # return nil if one argument is not a graph
561
+ graphs.each do |g|
562
+ return if !g.is_a?(Graph)
563
+ end
564
+
565
+ # if :same_fields option is set, call `keep_only_same_fields` function
566
+ graphs = keep_only_same_fields(*graphs) if opts[:same_fields]
567
+
568
+ # perform an and operation on all graph list
569
+ graphs.inject(&block)
570
+ end
571
+ end