rmatrix 0.1.3 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rmatrix.rb +1 -0
- data/lib/rmatrix/indices.rb +107 -0
- data/lib/rmatrix/matrix.rb +152 -29
- data/lib/rmatrix/vector.rb +1 -1
- data/lib/rmatrix/version.rb +1 -1
- data/rmatrix.gemspec +1 -0
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ef041f60282c09c58765f159e598b55a3cc7cd4
|
4
|
+
data.tar.gz: 624800e2934fe3858adfbbd62e6455574bce512b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e8bf852ff5b4c8cad5e541b68604784a401ab140c01654f8bd442b4899c5b047de3d55dca050ab0e8452488f2fe56bd22cb0f40f7afa6620eb7277975ffffd2
|
7
|
+
data.tar.gz: 35fd9c5279d0b45c7a651c11ea06c9b42f26035697e0093a1b6cc32f95bb6dbb2a3b79ce6fd1f111d1fe0e0c6d3b0c5b316abba785500f298a8b42effdc030a4
|
data/lib/rmatrix.rb
CHANGED
@@ -0,0 +1,107 @@
|
|
1
|
+
module RMatrix
|
2
|
+
module Indices
|
3
|
+
def [](*args)
|
4
|
+
indices = unmap_args(args)
|
5
|
+
result_row_map = build_result_map(self.row_map, indices.first, self.rows) if self.row_map
|
6
|
+
result_column_map = build_result_map(self.column_map, indices.last, self.columns) if self.column_map
|
7
|
+
raw[*indices, column_map: result_column_map, row_map: result_row_map]
|
8
|
+
end
|
9
|
+
|
10
|
+
def raw
|
11
|
+
@raw ||= begin
|
12
|
+
raw = Struct.new(:narray, :typecode).new(self.narray, self.typecode)
|
13
|
+
def raw.[](*args, column_map: nil, row_map: nil)
|
14
|
+
args.all?{|x| Fixnum === x } ? narray[*args.reverse] : Matrix.new(narray[*args.reverse], typecode, column_map: column_map, row_map: row_map)
|
15
|
+
end
|
16
|
+
raw
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_result_map(existing, indices, size)
|
21
|
+
return existing if indices == true
|
22
|
+
result_map = {}
|
23
|
+
indexify(indices, result_map, size)
|
24
|
+
result_map.default_proc = ->(h,k) do
|
25
|
+
existing_index = existing[k]
|
26
|
+
case existing_index
|
27
|
+
when TrueClass
|
28
|
+
existing_index
|
29
|
+
when Range
|
30
|
+
if existing_index.exclude_end?
|
31
|
+
h[k] = h[existing_index.first]...h[existing_index.end]
|
32
|
+
else
|
33
|
+
h[k] = h[existing_index.first]..h[existing_index.end]
|
34
|
+
end
|
35
|
+
when nil
|
36
|
+
raise "Couldn't find key #{k} in index mapping"
|
37
|
+
else
|
38
|
+
h[existing_index]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
result_map
|
42
|
+
end
|
43
|
+
|
44
|
+
def indexify(indices, results, size, total=0)
|
45
|
+
Array(indices).each do |index|
|
46
|
+
case index
|
47
|
+
when TrueClass
|
48
|
+
(0...size).each do |i|
|
49
|
+
results[i] = i
|
50
|
+
end
|
51
|
+
when Fixnum
|
52
|
+
results[index] ||= total
|
53
|
+
total += 1
|
54
|
+
when Array
|
55
|
+
indexify(index, results, size, total)
|
56
|
+
when Range
|
57
|
+
inclusive = index.exclude_end? ? index.first..(index.end - 1) : index
|
58
|
+
flat_range = inclusive.end < inclusive.first ? [*inclusive.end..inclusive.first].reverse : [*inclusive]
|
59
|
+
flat_range.each do |elm|
|
60
|
+
indexify(elm, results, size, total)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def unmap_args(args)
|
67
|
+
if args.length == 1
|
68
|
+
if row_map
|
69
|
+
return [unmap_index(self.row_map, args[0]), true] rescue nil
|
70
|
+
end
|
71
|
+
if column_map
|
72
|
+
return [true, [unmap_index(self.column_map, args[0])]] rescue nil
|
73
|
+
end
|
74
|
+
return [args[0]]
|
75
|
+
else
|
76
|
+
[
|
77
|
+
self.row_map ? unmap_index(self.row_map, args[0]) : args[0],
|
78
|
+
Array(self.column_map ? unmap_index(self.column_map, args[1]) : args[1])
|
79
|
+
]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def unmap_index(map, columns)
|
84
|
+
case columns
|
85
|
+
when TrueClass, FalseClass
|
86
|
+
columns
|
87
|
+
when Array
|
88
|
+
columns.map{|col| unmap_index(map, col)}.flatten
|
89
|
+
when Range
|
90
|
+
first = unmap_index(map, columns.first)
|
91
|
+
last = unmap_index(map, columns.end)
|
92
|
+
first = Range === first ? first.first : first
|
93
|
+
if columns.exclude_end?
|
94
|
+
last = Range === last ? last.first : last
|
95
|
+
first...last
|
96
|
+
else
|
97
|
+
last = Range === last ? last.end : last
|
98
|
+
first..last
|
99
|
+
end
|
100
|
+
else
|
101
|
+
index = map[columns]
|
102
|
+
raise "Value not present in index mapping: #{columns}" unless index
|
103
|
+
index
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/rmatrix/matrix.rb
CHANGED
@@ -2,21 +2,25 @@ module RMatrix
|
|
2
2
|
class Matrix
|
3
3
|
require 'narray'
|
4
4
|
require_relative 'typecode'
|
5
|
+
|
5
6
|
include Enumerable
|
7
|
+
include Indices
|
6
8
|
|
7
|
-
attr_accessor :invert_next_operation, :
|
9
|
+
attr_accessor :invert_next_operation, :narray, :typecode, :row_map, :column_map
|
10
|
+
attr_writer :matrix
|
8
11
|
|
9
|
-
def initialize(source, typecode=Typecode::
|
12
|
+
def initialize(source, typecode=Typecode::SFLOAT, column_map: nil, row_map: nil)
|
10
13
|
self.typecode = typecode
|
11
14
|
self.narray = two_dimensional(source, typecode)
|
15
|
+
self.row_map, self.column_map = row_map, column_map
|
12
16
|
end
|
13
17
|
|
14
18
|
def matrix
|
15
|
-
@matrix ||= NMatrix.refer(narray)
|
19
|
+
@matrix ||= narray.empty? ? narray : NMatrix.refer(narray)
|
16
20
|
end
|
17
21
|
|
18
|
-
def self.blank(rows,
|
19
|
-
self.new(NArray.new(typecode,
|
22
|
+
def self.blank(rows: 1, columns: 1, typecode: Typecode::SFLOAT, initial: 0)
|
23
|
+
self.new(NArray.new(typecode, columns, rows), typecode) + initial
|
20
24
|
end
|
21
25
|
|
22
26
|
def _dump(level)
|
@@ -37,6 +41,24 @@ module RMatrix
|
|
37
41
|
block_given? ? e.each(&block) : e
|
38
42
|
end
|
39
43
|
|
44
|
+
def each_column(&block)
|
45
|
+
e = Enumerator.new do |enum|
|
46
|
+
(0...self.columns).each do |i|
|
47
|
+
enum << self.raw[true, i]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
block_given? ? e.each(&block) : e
|
51
|
+
end
|
52
|
+
|
53
|
+
def each_row(&block)
|
54
|
+
e = Enumerator.new do |enum|
|
55
|
+
(0...self.rows).each do |i|
|
56
|
+
enum << self.raw[i, true]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
block_given? ? e.each(&block) : e
|
60
|
+
end
|
61
|
+
|
40
62
|
def mmap
|
41
63
|
as_na = NArray.to_na(
|
42
64
|
matrix.each.map do |elm|
|
@@ -46,13 +68,23 @@ module RMatrix
|
|
46
68
|
Matrix.new(as_na.reshape(*shape), typecode)
|
47
69
|
end
|
48
70
|
|
71
|
+
def mask
|
72
|
+
mmap do |elm|
|
73
|
+
(yield elm) ? 0 : elm
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def abs
|
78
|
+
(self ** 2) ** 0.5
|
79
|
+
end
|
80
|
+
|
49
81
|
def coerce(other)
|
50
82
|
self.invert_next_operation = true
|
51
83
|
[self, other]
|
52
84
|
end
|
53
85
|
|
54
86
|
def size
|
55
|
-
self.shape.inject(:*)
|
87
|
+
self.shape.inject(:*).to_i
|
56
88
|
end
|
57
89
|
|
58
90
|
def rows
|
@@ -63,12 +95,68 @@ module RMatrix
|
|
63
95
|
self.shape.first
|
64
96
|
end
|
65
97
|
|
98
|
+
def diag(dim=0)
|
99
|
+
raise "Must be square matrix" unless self.shape[0] == self.shape[1]
|
100
|
+
Matrix.new((self.class.identity(self.shape[0]).mult self).sum(dim))
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.identity(size)
|
104
|
+
blank = self.blank(rows: size, columns: size)
|
105
|
+
blank.diagonal(1)
|
106
|
+
end
|
107
|
+
|
66
108
|
def sum_rows
|
67
|
-
|
109
|
+
sum(1)
|
68
110
|
end
|
69
111
|
|
70
112
|
def sum_columns
|
71
|
-
|
113
|
+
sum(0)
|
114
|
+
end
|
115
|
+
|
116
|
+
def concat(*others, rows: true)
|
117
|
+
others.map!{|o| Matrix === o ? o.narray : NArray.to_na(o)}
|
118
|
+
|
119
|
+
joined = case rows
|
120
|
+
when true
|
121
|
+
# raise "Rows must match #{self.rows}, #{others.map(&:rows)}" unless [self.rows, *others.map(&:shape).map(&:last)].uniq.count.one?
|
122
|
+
height = self.rows + others.map(&:shape).map(&:last).inject(:+)
|
123
|
+
width = others[0].shape.first
|
124
|
+
joined = ::NArray.new(typecode, width, height)
|
125
|
+
joined[true, 0...self.rows] = self.narray
|
126
|
+
current_row = self.rows
|
127
|
+
others.each do |slice|
|
128
|
+
slice_height = slice.shape[1]
|
129
|
+
joined[true, current_row...current_row+slice_height] = slice
|
130
|
+
current_row += slice_height
|
131
|
+
end
|
132
|
+
joined
|
133
|
+
else
|
134
|
+
width = self.columns + others.map(&:shape).map(&:first).inject(:+)
|
135
|
+
height = others[0].shape.last
|
136
|
+
joined = ::NArray.new(typecode, width, height)
|
137
|
+
joined[0...self.columns, true] = self.narray
|
138
|
+
current_col = self.columns
|
139
|
+
others.each do |slice|
|
140
|
+
slice_width = slice.shape[0]
|
141
|
+
joined[current_col...current_col+slice_width, true] = slice
|
142
|
+
current_col += slice_width
|
143
|
+
end
|
144
|
+
joined
|
145
|
+
# raise "Rows must match #{self.columns}, #{others.map(&:columns)}" unless [self.columns, *others.map(&:columns)].uniq.count.one?
|
146
|
+
end
|
147
|
+
|
148
|
+
Matrix.new(joined, typecode)
|
149
|
+
end
|
150
|
+
|
151
|
+
def join(other)
|
152
|
+
case true
|
153
|
+
when self.rows == 1 && other.rows == 1
|
154
|
+
Vector.new(NArray.to_na([self.narray,other.narray]).to_type(self.typecode).reshape(self.columns + other.columns, 1))
|
155
|
+
when self.columns == 1 && other.columns == 1
|
156
|
+
Vector.new(NArray.to_na([self.narray,other.narray]).to_type(self.typecode).reshape(1, self.rows + other.rows))
|
157
|
+
else
|
158
|
+
raise "Couldn't join mismatched dimensions"
|
159
|
+
end
|
72
160
|
end
|
73
161
|
|
74
162
|
def two_dimensional(source, type)
|
@@ -82,6 +170,12 @@ module RMatrix
|
|
82
170
|
source = NArray.to_na([source])
|
83
171
|
else
|
84
172
|
source = NArray.to_na(source)
|
173
|
+
if type != RMatrix::Matrix::Typecode::OBJECT &&
|
174
|
+
source.typecode == RMatrix::Matrix::Typecode::OBJECT &&
|
175
|
+
RMatrix::Matrix === source[0]
|
176
|
+
source = NArray.to_na(source.map(&:to_a).to_a).to_type(typecode)
|
177
|
+
end
|
178
|
+
source
|
85
179
|
end
|
86
180
|
|
87
181
|
source = source.to_type(type) unless type == source.typecode
|
@@ -89,7 +183,7 @@ module RMatrix
|
|
89
183
|
case source.dim
|
90
184
|
when 1
|
91
185
|
source.reshape(source.length, 1)
|
92
|
-
when 2
|
186
|
+
when 2, 0
|
93
187
|
source
|
94
188
|
else
|
95
189
|
raise "Source for matrix must be either one or two dimensional" unless source.shape[2..-1].all?{|x| x == 1}
|
@@ -116,11 +210,11 @@ module RMatrix
|
|
116
210
|
|
117
211
|
def determinant
|
118
212
|
raise "Cannot calculate determinant of non-square matrix" unless columns == rows
|
119
|
-
return self[0, 0] * self[1, 1]- self[0, 1] * self[1, 0] if(self.columns == 2)
|
213
|
+
return self.raw[0, 0] * self.raw[1, 1]- self.raw[0, 1] * self.raw[1, 0] if(self.columns == 2)
|
120
214
|
sign = 1
|
121
215
|
det = 0
|
122
216
|
self.columns.times do |i|
|
123
|
-
det += sign * self[0,i] * self.minor(0, i).determinant
|
217
|
+
det += sign * self.raw[0,i] * self.minor(0, i).determinant
|
124
218
|
sign *= -1
|
125
219
|
end
|
126
220
|
return det
|
@@ -159,9 +253,13 @@ module RMatrix
|
|
159
253
|
mmap{|x| x.round(dp) }
|
160
254
|
end
|
161
255
|
|
162
|
-
def
|
163
|
-
|
256
|
+
def is_vector?
|
257
|
+
[rows, columns].include?(1)
|
258
|
+
end
|
164
259
|
|
260
|
+
def inspect
|
261
|
+
return 'M[Empty]' if empty?
|
262
|
+
return Vector::inspect_vector(self) if self.is_vector?
|
165
263
|
inspect_rows = [10, self.rows].min
|
166
264
|
inspect_columns = [10, self.columns].min
|
167
265
|
more_rows = inspect_rows < self.rows
|
@@ -176,29 +274,28 @@ module RMatrix
|
|
176
274
|
"#{rows} x #{columns} Matrix\nM#{box_lines(as_strings.map{|row| row.join(", ") }, more_columns)}"
|
177
275
|
end
|
178
276
|
|
179
|
-
|
180
|
-
def [](*args)
|
181
|
-
args.all?{|x| Fixnum === x } ? narray[*args.reverse] : Matrix.new(narray[*args.reverse], typecode)
|
182
|
-
end
|
183
|
-
|
184
277
|
def transpose
|
185
278
|
Matrix.new(self.matrix.transpose, typecode)
|
186
279
|
end
|
187
280
|
|
188
|
-
def self.[](*inputs)
|
281
|
+
def self.[](*inputs, typecode: Typecode::SFLOAT, row_map: nil, column_map: nil)
|
189
282
|
if inputs.length == 1 && Matrix === inputs[0]
|
190
283
|
inputs[0]
|
191
284
|
elsif inputs.length == 1 && [String, Symbol].include?(inputs[0].class)
|
192
285
|
if ['byte', 'sint', 'int', 'sfloat', 'float', 'scomplex', 'complex', 'object'].include?(inputs[0])
|
193
|
-
->(*source){ Matrix.new(source, inputs[0])
|
286
|
+
->(*source){ Matrix.new(source, inputs[0], row_map: row_map, column_map: column_map)}
|
194
287
|
else
|
195
|
-
Matrix.new(inputs[0])
|
288
|
+
Matrix.new(inputs[0], typecode, row_map: row_map, column_map: column_map)
|
196
289
|
end
|
197
290
|
else
|
198
|
-
Matrix.new(inputs)
|
291
|
+
Matrix.new(inputs, typecode, row_map: row_map, column_map: column_map)
|
199
292
|
end
|
200
293
|
end
|
201
294
|
|
295
|
+
def zip(*others)
|
296
|
+
Matrix.new(super(*others), self.typecode)
|
297
|
+
end
|
298
|
+
|
202
299
|
def self.gen_mutator(name)
|
203
300
|
define_method(name) do |*args, &blk|
|
204
301
|
matrix.send(name, *args, &blk)
|
@@ -212,9 +309,22 @@ module RMatrix
|
|
212
309
|
end
|
213
310
|
end
|
214
311
|
|
312
|
+
def sum(dim=nil)
|
313
|
+
case dim
|
314
|
+
when nil then
|
315
|
+
res = self.narray.sum
|
316
|
+
NArray === res ? Matrix.new(0, typecode)[0] : res
|
317
|
+
else Matrix.new(self.matrix.sum(dim), typecode)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
215
321
|
def self.gen_delegator(name)
|
216
322
|
define_method(name) do |*args, &blk|
|
217
|
-
matrix.send(name, *args, &blk)
|
323
|
+
result = matrix.send(name, *args, &blk)
|
324
|
+
case result
|
325
|
+
when NArray then Matrix.new(result, typecode)
|
326
|
+
else result
|
327
|
+
end
|
218
328
|
end
|
219
329
|
end
|
220
330
|
|
@@ -234,13 +344,12 @@ module RMatrix
|
|
234
344
|
end
|
235
345
|
|
236
346
|
[:fill!, :random!, :indgen!].each(&method(:gen_mutator))
|
237
|
-
[:reshape, :sort, :sort_index, :inverse, :lu, :delete_at, :where, :where2, :not,
|
238
|
-
[:
|
347
|
+
[:reshape, :sort, :sort_index, :inverse, :lu, :delete_at, :where, :where2, :not, :-@, :reverse, :diagonal].each(&method(:gen_matrix_delegator))
|
348
|
+
[:prod, :min, :max, :stddev, :mean, :rms, :rmsdev, :shape, :empty?].each(&method(:gen_delegator))
|
239
349
|
[:byte, :sint, :int, :sfloat, :float, :scomplex, :complex, :object].each(&method(:gen_typeconstructor))
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
define_method(op) do |other|
|
350
|
+
[:+, :/, :-, :**, :&, :^, :|].each do |_op|
|
351
|
+
op = translate_op(_op)
|
352
|
+
define_method(_op) do |other|
|
244
353
|
result = case other
|
245
354
|
when Matrix
|
246
355
|
apply_elementwise(op, other)
|
@@ -253,10 +362,23 @@ module RMatrix
|
|
253
362
|
end
|
254
363
|
end
|
255
364
|
|
365
|
+
def to_a
|
366
|
+
return narray.reshape(narray.length).to_a if is_vector?
|
367
|
+
return narray.to_a
|
368
|
+
end
|
369
|
+
|
256
370
|
def self.seed(seed)
|
257
371
|
NArray.srand(seed)
|
258
372
|
end
|
259
373
|
|
374
|
+
def to_m
|
375
|
+
self
|
376
|
+
end
|
377
|
+
|
378
|
+
alias_method :cols, :columns
|
379
|
+
alias_method :avg, :mean
|
380
|
+
alias_method :length, :size
|
381
|
+
|
260
382
|
private
|
261
383
|
def test_inverse
|
262
384
|
if self.invert_next_operation
|
@@ -287,6 +409,7 @@ module RMatrix
|
|
287
409
|
end
|
288
410
|
end
|
289
411
|
|
412
|
+
|
290
413
|
end
|
291
414
|
|
292
415
|
class ::NArray; include Enumerable; end
|
data/lib/rmatrix/vector.rb
CHANGED
@@ -14,7 +14,7 @@ module RMatrix
|
|
14
14
|
def self.[](*inputs)
|
15
15
|
if inputs.length == 1 && [String, Symbol].include?(inputs[0].class)
|
16
16
|
if ['byte', 'sint', 'int', 'sfloat', 'float', 'scomplex', 'complex', 'object'].include?(inputs[0].to_s)
|
17
|
-
->(*source){ Matrix.new(source,
|
17
|
+
->(*source){ Matrix.new(source, inputs[0].to_s) }
|
18
18
|
else
|
19
19
|
Vector.new(inputs[0])
|
20
20
|
end
|
data/lib/rmatrix/version.rb
CHANGED
data/rmatrix.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rmatrix
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wouter Coppieters
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: opencl_ruby_ffi
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
description: |-
|
98
112
|
RMatrix is a lightning fast library for Ruby. It provides numerous enhancements over the Matrix class in the standard library.
|
99
113
|
Features include the ability to calculate Matrix inverse, transpose, determinant, minor, adjoint, cofactor_matrix, hadamard product and other elementwise operations, slicing, masking and more.
|
@@ -114,6 +128,7 @@ files:
|
|
114
128
|
- bin/setup
|
115
129
|
- lib/rmatrix.rb
|
116
130
|
- lib/rmatrix/core.rb
|
131
|
+
- lib/rmatrix/indices.rb
|
117
132
|
- lib/rmatrix/matrix.rb
|
118
133
|
- lib/rmatrix/shortcuts.rb
|
119
134
|
- lib/rmatrix/typecode.rb
|
@@ -141,9 +156,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
156
|
version: '0'
|
142
157
|
requirements: []
|
143
158
|
rubyforge_project:
|
144
|
-
rubygems_version: 2.
|
159
|
+
rubygems_version: 2.5.1
|
145
160
|
signing_key:
|
146
161
|
specification_version: 4
|
147
162
|
summary: Fast matrix/linear algebra library for Ruby
|
148
163
|
test_files: []
|
149
|
-
has_rdoc:
|