bmg 0.23.3 → 0.23.5
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/README.md +38 -0
- data/lib/bmg/algebra.rb +9 -0
- data/lib/bmg/operator/generator.rb +68 -0
- data/lib/bmg/operator/shared/zeroary.rb +24 -0
- data/lib/bmg/operator/undress.rb +66 -0
- data/lib/bmg/operator.rb +11 -3
- data/lib/bmg/relation.rb +1 -5
- data/lib/bmg/support/factory.rb +53 -0
- data/lib/bmg/support/tuple_transformer.rb +17 -0
- data/lib/bmg/support.rb +1 -0
- data/lib/bmg/type.rb +37 -11
- data/lib/bmg/version.rb +1 -1
- data/lib/bmg/writer/text.rb +200 -0
- data/lib/bmg/writer.rb +1 -0
- data/lib/bmg.rb +13 -45
- metadata +24 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb0b47d8dbd6924eb1f4fb7e773635a5d1fc83fc09b09c60ead3ac913bbd2051
|
4
|
+
data.tar.gz: ad0919fc045b59b5299254d0f476b31390069b6d39e086334cd55790eedf56e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fa7c3dfa036762e82dd86275c522af811eba747901f288e8b1804388ddee0094350374795527b0884cf8d5355eea649f9c3ce4a66ec7c9742eeaef5fff81a16
|
7
|
+
data.tar.gz: 5cee0487c9e70df6a049791ef48d93fda5ff516d769b7f8d25f9ea6f29d9d780bec8aebca78a8e88e435113acde6860fc1c7dbbbb2c9bb7f70edf88d22bfe739
|
data/README.md
CHANGED
@@ -22,6 +22,7 @@ further down this README.
|
|
22
22
|
* [Connecting to SQL databases](#connecting-to-sql-databases)
|
23
23
|
* [Reading data files](#reading-data-files-json-csv-yaml-text-xls--xlsx)
|
24
24
|
* [Connecting to Redis databases](#connecting-to-redis-databases)
|
25
|
+
* [The Generator](#the-generator)
|
25
26
|
* [Your own relations](#your-own-relations)
|
26
27
|
* [The Database abstraction](#the-database-abstraction)
|
27
28
|
* [List of supported operators](#supported-operators)
|
@@ -238,6 +239,42 @@ and is optional.
|
|
238
239
|
The redis relvars support basic algorithms for insert/update/delete.
|
239
240
|
No optimization is currently supported.
|
240
241
|
|
242
|
+
### The Generator
|
243
|
+
|
244
|
+
Somewhat similar to PostgreSQL's `generate_series`, Bmg supports the generation
|
245
|
+
of relations. Unlike PostgreSQL though, the result is a Relation that can be
|
246
|
+
"chained" as any other relation, not a sequence of scalars :
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
r = Bmg
|
250
|
+
.generate(1, 10, :step => 2, :as => :index)
|
251
|
+
.restrict(:index => 5)
|
252
|
+
```
|
253
|
+
|
254
|
+
If you don't specify the `:as` option, the attribute name will be `:i`.
|
255
|
+
|
256
|
+
The step can be negative :
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
Bmg.generate(10, 1, :step => -2, :as => :index)
|
260
|
+
```
|
261
|
+
|
262
|
+
The step can also be computed :
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
Bmg.generate(1, 100, :step => ->(current){ current * 2 })
|
266
|
+
```
|
267
|
+
|
268
|
+
Last but not least, you can use any ordinal data type. For instance, the
|
269
|
+
following code will work and generate all months from May to December 2025
|
270
|
+
(under the assumption that ActiveRecord's `next_month` is available).
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
min = Date.new(2025,5,1)
|
274
|
+
max = Date.new(2025,12,1)
|
275
|
+
Bmg.generate(min, max, :step => ->(current){ current.next_month })
|
276
|
+
```
|
277
|
+
|
241
278
|
### Your own relations
|
242
279
|
|
243
280
|
As noted earlier, Bmg has a simple relation interface where you only have to
|
@@ -353,6 +390,7 @@ r.transform(:to_s) # all-attrs transformation
|
|
353
390
|
r.transform(&:to_s) # similar, but Proc-driven
|
354
391
|
r.transform(:foo => :upcase, ...) # specific-attrs tranformation
|
355
392
|
r.transform([:to_s, :upcase]) # chain-transformation
|
393
|
+
r.undress() # convert all values to a scalar (number, string, bool, object, array)
|
356
394
|
r.ungroup([:a, :b, ...]) # ungroup relation-valued attributes within parent tuple
|
357
395
|
r.ungroup(:a) # shortcut over ungroup([:a])
|
358
396
|
r.union(right) # set union
|
data/lib/bmg/algebra.rb
CHANGED
@@ -184,6 +184,15 @@ module Bmg
|
|
184
184
|
end
|
185
185
|
protected :_transform
|
186
186
|
|
187
|
+
def undress(options = {})
|
188
|
+
_undress self.type.undress(options), options
|
189
|
+
end
|
190
|
+
|
191
|
+
def _undress(type, options)
|
192
|
+
Operator::Undress.new(type, self, options)
|
193
|
+
end
|
194
|
+
protected :_undress
|
195
|
+
|
187
196
|
def ungroup(attrs)
|
188
197
|
_ungroup self.type.ungroup(attrs), attrs
|
189
198
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Operator
|
3
|
+
#
|
4
|
+
# Generator operator.
|
5
|
+
#
|
6
|
+
# Generates a relation. Most inspired by PostgreSQL's generate_series.
|
7
|
+
#
|
8
|
+
class Generator
|
9
|
+
include Operator::Zeroary
|
10
|
+
|
11
|
+
DEFAULT_OPTIONS = {
|
12
|
+
:as => :i,
|
13
|
+
:step => 1,
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(type, from, to, options = {})
|
17
|
+
options = { step: options } unless options.is_a?(Hash)
|
18
|
+
@type = type
|
19
|
+
@from = from
|
20
|
+
@to = to
|
21
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
22
|
+
raise ArgumentError, "from, to and step must be defined" if from.nil? || to.nil? || step.nil?
|
23
|
+
end
|
24
|
+
attr_reader :from, :to, :options
|
25
|
+
|
26
|
+
def each
|
27
|
+
return to_enum unless block_given?
|
28
|
+
|
29
|
+
current = from
|
30
|
+
as = options[:as]
|
31
|
+
|
32
|
+
until overflowed?(current)
|
33
|
+
yield({ as => current })
|
34
|
+
current = next_of(current)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def step
|
41
|
+
@step ||= options[:step]
|
42
|
+
end
|
43
|
+
|
44
|
+
def positive_step?
|
45
|
+
!step.is_a?(Numeric) || step > 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def next_of(current)
|
49
|
+
step.is_a?(Proc) ? step.call(current) : current + step
|
50
|
+
end
|
51
|
+
|
52
|
+
def overflowed?(current)
|
53
|
+
positive_step? ? current > to : current < to
|
54
|
+
end
|
55
|
+
|
56
|
+
protected ### inspect
|
57
|
+
|
58
|
+
def operands
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
|
62
|
+
def args
|
63
|
+
[ from, to, options ]
|
64
|
+
end
|
65
|
+
|
66
|
+
end # class Generator
|
67
|
+
end # module Operator
|
68
|
+
end # module Bmg
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Operator
|
3
|
+
module Zeroary
|
4
|
+
include Operator
|
5
|
+
|
6
|
+
def bind(binding)
|
7
|
+
dup
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
attr_accessor :operand
|
13
|
+
|
14
|
+
def _visit(parent, visitor)
|
15
|
+
visitor.call(self, parent)
|
16
|
+
end
|
17
|
+
|
18
|
+
def operands
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
end # module Zeroary
|
23
|
+
end # module Operator
|
24
|
+
end # module Bmg
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Operator
|
3
|
+
#
|
4
|
+
# Undress operator.
|
5
|
+
#
|
6
|
+
# Transform all values to keep only integer, strings & booleans
|
7
|
+
#
|
8
|
+
class Undress
|
9
|
+
include Operator::Unary
|
10
|
+
|
11
|
+
DEFAULT_OPTIONS = {}
|
12
|
+
|
13
|
+
def initialize(type, operand, options = {})
|
14
|
+
@type = type
|
15
|
+
@operand = operand
|
16
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
public
|
24
|
+
|
25
|
+
def each
|
26
|
+
return to_enum unless block_given?
|
27
|
+
|
28
|
+
@operand.each do |tuple|
|
29
|
+
yield undress(tuple)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_ast
|
34
|
+
[ :transform, operand.to_ast, transformation.dup ]
|
35
|
+
end
|
36
|
+
|
37
|
+
protected ### inspect
|
38
|
+
|
39
|
+
def args
|
40
|
+
[ ]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def undress(value)
|
46
|
+
case value
|
47
|
+
when ->(v) { v.respond_to?(:undress) }
|
48
|
+
value.undress
|
49
|
+
when Hash
|
50
|
+
value.each_with_object({}) do |(k,v), undressed|
|
51
|
+
undressed[k] = undress(v)
|
52
|
+
end
|
53
|
+
when Relation
|
54
|
+
Relation.new value.map{|tuple| undress(tuple) }
|
55
|
+
when Date, Time
|
56
|
+
value.iso8601
|
57
|
+
when Array
|
58
|
+
value.map{|v| undress(v) }
|
59
|
+
else
|
60
|
+
value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end # class Undress
|
65
|
+
end # module Operator
|
66
|
+
end # module Bmg
|
data/lib/bmg/operator.rb
CHANGED
@@ -17,15 +17,20 @@ module Bmg
|
|
17
17
|
|
18
18
|
def inspect
|
19
19
|
str = "(#{self.class.name.split('::').last.downcase}\n"
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
unless operands.empty?
|
21
|
+
str << operands.map{|op| op.inspect.gsub(/^/m, " ") }.join("\n")
|
22
|
+
str << "\n"
|
23
|
+
end
|
24
|
+
unless args.empty?
|
25
|
+
str << args.map{|a| a.inspect.gsub(/^/m, " ") }.join("\n")
|
26
|
+
end
|
23
27
|
str << ")"
|
24
28
|
str
|
25
29
|
end
|
26
30
|
|
27
31
|
end # module Operator
|
28
32
|
end # module Bmg
|
33
|
+
require_relative 'operator/shared/zeroary'
|
29
34
|
require_relative 'operator/shared/unary'
|
30
35
|
require_relative 'operator/shared/binary'
|
31
36
|
require_relative 'operator/shared/nary'
|
@@ -48,6 +53,9 @@ require_relative 'operator/restrict'
|
|
48
53
|
require_relative 'operator/rxmatch'
|
49
54
|
require_relative 'operator/summarize'
|
50
55
|
require_relative 'operator/transform'
|
56
|
+
require_relative 'operator/undress'
|
51
57
|
require_relative 'operator/ungroup'
|
52
58
|
require_relative 'operator/union'
|
53
59
|
require_relative 'operator/unwrap'
|
60
|
+
|
61
|
+
require_relative 'operator/generator'
|
data/lib/bmg/relation.rb
CHANGED
@@ -2,17 +2,13 @@ module Bmg
|
|
2
2
|
module Relation
|
3
3
|
include Enumerable
|
4
4
|
include Algebra
|
5
|
+
extend Factory
|
5
6
|
|
6
7
|
def self.new(operand, type = Type::ANY)
|
7
8
|
raise ArgumentError, "Missing type" if type.nil?
|
8
9
|
operand.is_a?(Relation) ? operand : Bmg.in_memory(operand, type)
|
9
10
|
end
|
10
11
|
|
11
|
-
def self.empty(type = Type::ANY)
|
12
|
-
raise ArgumentError, "Missing type" if type.nil?
|
13
|
-
Relation::Empty.new(type)
|
14
|
-
end
|
15
|
-
|
16
12
|
def bind(binding)
|
17
13
|
self
|
18
14
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Factory
|
3
|
+
def empty(type = Type::ANY)
|
4
|
+
raise ArgumentError, "Missing type" if type.nil?
|
5
|
+
Relation::Empty.new(type)
|
6
|
+
end
|
7
|
+
|
8
|
+
def mutable(enumerable, type = Type::ANY)
|
9
|
+
Relation::InMemory::Mutable.new(type, enumerable).spied(Bmg.main_spy)
|
10
|
+
end
|
11
|
+
|
12
|
+
def in_memory(enumerable, type = Type::ANY)
|
13
|
+
Relation::InMemory.new(type, enumerable).spied(Bmg.main_spy)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate(from, to, options = {})
|
17
|
+
type = Type::ANY.with_attrlist([options[:as] || Operator::Generator::DEFAULT_OPTIONS[:as]])
|
18
|
+
Operator::Generator.new(type, from, to, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def text_file(path, options = {}, type = Type::ANY)
|
22
|
+
Reader::TextFile.new(type, path, options).spied(Bmg.main_spy)
|
23
|
+
end
|
24
|
+
|
25
|
+
def csv(path, options = {}, type = Type::ANY)
|
26
|
+
Reader::Csv.new(type, path, options).spied(Bmg.main_spy)
|
27
|
+
end
|
28
|
+
|
29
|
+
def json_file(path, options = {}, type = Type::ANY)
|
30
|
+
in_memory(path.load.map{|tuple| TupleAlgebra.symbolize_keys(tuple) })
|
31
|
+
end
|
32
|
+
|
33
|
+
def json(*args, &bl)
|
34
|
+
json_file(*args, &bl)
|
35
|
+
end
|
36
|
+
|
37
|
+
def yaml_file(path, options = {}, type = Type::ANY)
|
38
|
+
in_memory(path.load.map{|tuple| TupleAlgebra.symbolize_keys(tuple) })
|
39
|
+
end
|
40
|
+
|
41
|
+
def yaml(*args, &bl)
|
42
|
+
yaml_file(*args, &bl)
|
43
|
+
end
|
44
|
+
|
45
|
+
def excel_file(path, options = {}, type = Type::ANY)
|
46
|
+
Reader::Excel.new(type, path, options).spied(Bmg.main_spy)
|
47
|
+
end
|
48
|
+
|
49
|
+
def excel(*args, &bl)
|
50
|
+
excel_file(*args, &bl)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -49,6 +49,11 @@ module Bmg
|
|
49
49
|
with.inject(tuple){|dup,on|
|
50
50
|
transform_tuple(dup, on)
|
51
51
|
}
|
52
|
+
when Type
|
53
|
+
unless with.knows_types?
|
54
|
+
raise ArgumentError, "Heading must specify the attribute types`"
|
55
|
+
end
|
56
|
+
transform_tuple(tuple, with.heading)
|
52
57
|
else
|
53
58
|
raise ArgumentError, "Unexpected transformation `#{with.inspect}`"
|
54
59
|
end
|
@@ -78,6 +83,18 @@ module Bmg
|
|
78
83
|
with.call(value)
|
79
84
|
when Hash
|
80
85
|
with[value]
|
86
|
+
when Type
|
87
|
+
unless with.knows_types?
|
88
|
+
raise ArgumentError, "Heading must specify the attribute types`"
|
89
|
+
end
|
90
|
+
case value
|
91
|
+
when Hash
|
92
|
+
transform_tuple(value, with.heading)
|
93
|
+
when Enumerable
|
94
|
+
Relation.new(value.map{|t| transform_tuple(t, with.heading) })
|
95
|
+
else
|
96
|
+
raise ArgumentError, "Unexpected value `#{value.inspect}`"
|
97
|
+
end
|
81
98
|
else
|
82
99
|
raise ArgumentError, "Unexpected transformation `#{with.inspect}`"
|
83
100
|
end
|
data/lib/bmg/support.rb
CHANGED
data/lib/bmg/type.rb
CHANGED
@@ -9,6 +9,10 @@ module Bmg
|
|
9
9
|
|
10
10
|
ANY = Type.new
|
11
11
|
|
12
|
+
def self.for_heading(heading)
|
13
|
+
Type.new.with_heading(heading)
|
14
|
+
end
|
15
|
+
|
12
16
|
public ## type checking
|
13
17
|
|
14
18
|
attr_writer :typechecked
|
@@ -41,6 +45,22 @@ module Bmg
|
|
41
45
|
}
|
42
46
|
end
|
43
47
|
|
48
|
+
public ## heading
|
49
|
+
|
50
|
+
attr_accessor :heading
|
51
|
+
protected :heading=
|
52
|
+
|
53
|
+
def with_heading(heading)
|
54
|
+
dup.tap{|x|
|
55
|
+
x.heading = heading
|
56
|
+
x.attrlist = heading.keys
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def knows_types?
|
61
|
+
!@heading.nil?
|
62
|
+
end
|
63
|
+
|
44
64
|
public ## attrlist
|
45
65
|
|
46
66
|
attr_accessor :attrlist
|
@@ -201,6 +221,17 @@ module Bmg
|
|
201
221
|
self
|
202
222
|
end
|
203
223
|
|
224
|
+
def minus(other)
|
225
|
+
union_compatible!(other, "Minus")
|
226
|
+
dup.tap{|x|
|
227
|
+
# 1. attributes do not change
|
228
|
+
# 2. predicate does not change, we can't strengthen it in any way but it
|
229
|
+
# does not become weaker either
|
230
|
+
# 3. we can safely keep all existing keys, but it's not obvious we can
|
231
|
+
# gain some new ones
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
204
235
|
def not_matching(right, on)
|
205
236
|
join_compatible!(right, on) if typechecked? && knows_attrlist?
|
206
237
|
self
|
@@ -278,6 +309,12 @@ module Bmg
|
|
278
309
|
}
|
279
310
|
end
|
280
311
|
|
312
|
+
def undress(options)
|
313
|
+
dup.tap{|x|
|
314
|
+
x.predicate = Predicate.tautology
|
315
|
+
}
|
316
|
+
end
|
317
|
+
|
281
318
|
def ungroup(attrlist)
|
282
319
|
known_attributes!(attrlist) if typechecked? && knows_attrlist?
|
283
320
|
dup.tap{|x|
|
@@ -296,17 +333,6 @@ module Bmg
|
|
296
333
|
}
|
297
334
|
end
|
298
335
|
|
299
|
-
def minus(other)
|
300
|
-
union_compatible!(other, "Minus")
|
301
|
-
dup.tap{|x|
|
302
|
-
# 1. attributes do not change
|
303
|
-
# 2. predicate does not change, we can't strengthen it in any way but it
|
304
|
-
# does not become weaker either
|
305
|
-
# 3. we can safely keep all existing keys, but it's not obvious we can
|
306
|
-
# gain some new ones
|
307
|
-
}
|
308
|
-
end
|
309
|
-
|
310
336
|
def unwrap(attrlist)
|
311
337
|
known_attributes!(attrlist) if typechecked? && knows_attrlist?
|
312
338
|
dup.tap{|x|
|
data/lib/bmg/version.rb
CHANGED
@@ -0,0 +1,200 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Writer
|
3
|
+
class Text
|
4
|
+
include Writer
|
5
|
+
|
6
|
+
TupleLike = lambda{|t| t.is_a?(Hash) || (defined?(OpenStruct) && t.is_a?(OpenStruct)) }
|
7
|
+
RelationLike = lambda{|r| r.is_a?(Relation) || (r.is_a?(Enumerable) && r.all?{|t| TupleLike === t }) }
|
8
|
+
|
9
|
+
module Utils
|
10
|
+
|
11
|
+
def max(x, y)
|
12
|
+
return y if x.nil?
|
13
|
+
return x if y.nil?
|
14
|
+
x > y ? x : y
|
15
|
+
end
|
16
|
+
end
|
17
|
+
include Utils
|
18
|
+
|
19
|
+
class Cell
|
20
|
+
include Utils
|
21
|
+
|
22
|
+
def initialize(renderer, value)
|
23
|
+
@renderer = renderer
|
24
|
+
@value = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def min_width
|
28
|
+
@min_width ||= rendering_lines.inject(0) do |maxl,line|
|
29
|
+
max(maxl,line.size)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def rendering_lines(size = nil)
|
34
|
+
if size.nil?
|
35
|
+
text_rendering.split(/\n/)
|
36
|
+
elsif @value.is_a?(Numeric)
|
37
|
+
rendering_lines(nil).map{|l| "%#{size}s" % l}
|
38
|
+
else
|
39
|
+
rendering_lines(nil).map{|l| "%-#{size}s" % l}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def text_rendering
|
44
|
+
@text_rendering ||= case (value = @value)
|
45
|
+
when NilClass
|
46
|
+
"[nil]"
|
47
|
+
when Symbol
|
48
|
+
value.inspect
|
49
|
+
when Float
|
50
|
+
(@renderer.text_options[:float_format] || "%.3f") % value
|
51
|
+
when Hash
|
52
|
+
value.inspect
|
53
|
+
when RelationLike
|
54
|
+
@renderer.render(value, "")
|
55
|
+
when Array
|
56
|
+
array_rendering(value)
|
57
|
+
when Time, DateTime
|
58
|
+
value.to_s
|
59
|
+
else
|
60
|
+
value.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def array_rendering(value)
|
65
|
+
if TupleLike === value.first
|
66
|
+
@renderer.render(value, "")
|
67
|
+
elsif value.empty?
|
68
|
+
"[]"
|
69
|
+
else
|
70
|
+
values = value.map{|x| Cell.new(x).text_rendering}
|
71
|
+
if values.inject(0){|memo,s| memo + s.size} < 20
|
72
|
+
"[" + values.join(", ") + "]"
|
73
|
+
else
|
74
|
+
"[" + values.join(",\n ") + "]"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end # class Cell
|
80
|
+
|
81
|
+
class Row
|
82
|
+
include Utils
|
83
|
+
|
84
|
+
def initialize(renderer, values)
|
85
|
+
@renderer = renderer
|
86
|
+
@cells = values.map{|v| Cell.new(renderer, v) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def min_widths
|
90
|
+
@cells.map{|cell| cell.min_width}
|
91
|
+
end
|
92
|
+
|
93
|
+
def rendering_lines(sizes = min_widths)
|
94
|
+
nb_lines = 0
|
95
|
+
by_cell = @cells.zip(sizes).map do |cell,size|
|
96
|
+
lines = cell.rendering_lines(size)
|
97
|
+
nb_lines = max(nb_lines, lines.size)
|
98
|
+
lines
|
99
|
+
end
|
100
|
+
grid = (0...nb_lines).map do |line_i|
|
101
|
+
"| " + by_cell.zip(sizes).map{|cell_lines, size|
|
102
|
+
cell_lines[line_i] || " "*size
|
103
|
+
}.join(" | ") + " |"
|
104
|
+
end
|
105
|
+
grid.empty? ? ["| |"] : grid
|
106
|
+
end
|
107
|
+
|
108
|
+
end # class Row
|
109
|
+
|
110
|
+
class Table
|
111
|
+
include Utils
|
112
|
+
|
113
|
+
def initialize(renderer, records, attributes)
|
114
|
+
@renderer = renderer
|
115
|
+
@header = Row.new(renderer, attributes.map(&:to_s))
|
116
|
+
@rows = records.map{|r| Row.new(renderer, r) }
|
117
|
+
end
|
118
|
+
attr_reader :renderer, :header, :rows
|
119
|
+
|
120
|
+
def sizes
|
121
|
+
@sizes ||= rows.inject(header.min_widths) do |memo,row|
|
122
|
+
memo.zip(row.min_widths).map{|x,y| max(x,y)}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def sep
|
127
|
+
@sep ||= '+-' << sizes.map{|s| '-' * s}.join('-+-') << '-+'
|
128
|
+
end
|
129
|
+
|
130
|
+
def each_line(pretty = renderer.text_options[:pretty])
|
131
|
+
if pretty && trim = renderer.text_options[:trim_at]
|
132
|
+
each_line(false) do |line|
|
133
|
+
yield(line[0..trim])
|
134
|
+
end
|
135
|
+
else
|
136
|
+
yield(sep)
|
137
|
+
yield(header.rendering_lines(sizes).first)
|
138
|
+
yield(sep)
|
139
|
+
rows.each do |row|
|
140
|
+
row.rendering_lines(sizes).each do |line|
|
141
|
+
yield(line)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
yield(sep)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def each
|
149
|
+
return to_enum unless block_given?
|
150
|
+
each_line do |line|
|
151
|
+
yield(line.strip << "\n")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_s
|
156
|
+
each.each_with_object(""){|line,buf| buf << line}
|
157
|
+
end
|
158
|
+
|
159
|
+
end # class Table
|
160
|
+
|
161
|
+
|
162
|
+
DEFAULT_OPTIONS = {
|
163
|
+
}
|
164
|
+
|
165
|
+
def initialize(text_options = {}, output_preferences = nil)
|
166
|
+
@text_options = DEFAULT_OPTIONS.merge(text_options)
|
167
|
+
@output_preferences = OutputPreferences.dress(output_preferences)
|
168
|
+
end
|
169
|
+
attr_reader :text_options, :output_preferences
|
170
|
+
|
171
|
+
def call(relation, output = "")
|
172
|
+
each_line(relation) do |str|
|
173
|
+
output << str
|
174
|
+
end
|
175
|
+
output
|
176
|
+
end
|
177
|
+
alias :render :call
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def each_line(relation, &bl)
|
182
|
+
input = relation
|
183
|
+
input = [input.to_hash] if TupleLike === input
|
184
|
+
relation = input.to_a
|
185
|
+
attrs = relation.inject([]){|memo,t| (memo | t.keys) }
|
186
|
+
records = relation.map{|t| attrs.map{|a| t[a]} }
|
187
|
+
table = Table.new(self, records, attrs)
|
188
|
+
table.each(&bl)
|
189
|
+
end
|
190
|
+
|
191
|
+
end # class Text
|
192
|
+
end # module Writer
|
193
|
+
module Relation
|
194
|
+
|
195
|
+
def to_text(options = {}, preferences = {}, output = "")
|
196
|
+
Writer::Text.new(options, preferences).render(self, output)
|
197
|
+
end
|
198
|
+
|
199
|
+
end # module Relation
|
200
|
+
end # module Bmg
|
data/lib/bmg/writer.rb
CHANGED
data/lib/bmg.rb
CHANGED
@@ -4,51 +4,6 @@ require 'forwardable'
|
|
4
4
|
require 'set'
|
5
5
|
module Bmg
|
6
6
|
|
7
|
-
def mutable(enumerable, type = Type::ANY)
|
8
|
-
Relation::InMemory::Mutable.new(type, enumerable).spied(main_spy)
|
9
|
-
end
|
10
|
-
module_function :mutable
|
11
|
-
|
12
|
-
def in_memory(enumerable, type = Type::ANY)
|
13
|
-
Relation::InMemory.new(type, enumerable).spied(main_spy)
|
14
|
-
end
|
15
|
-
module_function :in_memory
|
16
|
-
|
17
|
-
def text_file(path, options = {}, type = Type::ANY)
|
18
|
-
Reader::TextFile.new(type, path, options).spied(main_spy)
|
19
|
-
end
|
20
|
-
module_function :text_file
|
21
|
-
|
22
|
-
def csv(path, options = {}, type = Type::ANY)
|
23
|
-
Reader::Csv.new(type, path, options).spied(main_spy)
|
24
|
-
end
|
25
|
-
module_function :csv
|
26
|
-
|
27
|
-
def json(path, options = {}, type = Type::ANY)
|
28
|
-
in_memory(path.load.map{|tuple| TupleAlgebra.symbolize_keys(tuple) })
|
29
|
-
end
|
30
|
-
module_function :json
|
31
|
-
|
32
|
-
def yaml(path, options = {}, type = Type::ANY)
|
33
|
-
in_memory(path.load.map{|tuple| TupleAlgebra.symbolize_keys(tuple) })
|
34
|
-
end
|
35
|
-
module_function :yaml
|
36
|
-
|
37
|
-
def excel(path, options = {}, type = Type::ANY)
|
38
|
-
Reader::Excel.new(type, path, options).spied(main_spy)
|
39
|
-
end
|
40
|
-
module_function :excel
|
41
|
-
|
42
|
-
def main_spy
|
43
|
-
@main_spy
|
44
|
-
end
|
45
|
-
module_function :main_spy
|
46
|
-
|
47
|
-
def main_spy=(spy)
|
48
|
-
@main_spy = spy
|
49
|
-
end
|
50
|
-
module_function :main_spy=
|
51
|
-
|
52
7
|
require_relative 'bmg/version'
|
53
8
|
require_relative 'bmg/error'
|
54
9
|
require_relative 'bmg/support'
|
@@ -69,6 +24,19 @@ module Bmg
|
|
69
24
|
|
70
25
|
require_relative 'bmg/database'
|
71
26
|
|
27
|
+
def main_spy
|
28
|
+
@main_spy
|
29
|
+
end
|
30
|
+
module_function :main_spy
|
31
|
+
|
32
|
+
def main_spy=(spy)
|
33
|
+
@main_spy = spy
|
34
|
+
end
|
35
|
+
module_function :main_spy=
|
36
|
+
|
37
|
+
# Add all factory methods
|
38
|
+
extend Factory
|
39
|
+
|
72
40
|
# Deprecated
|
73
41
|
Leaf = Relation::InMemory
|
74
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bmg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.23.
|
4
|
+
version: 0.23.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-09-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: predicate
|
@@ -128,6 +128,20 @@ dependencies:
|
|
128
128
|
- - ">="
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: activesupport
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
131
145
|
description: Bmg is Alf's relational algebra for ruby, but much simpler and lighter
|
132
146
|
than Alf itself
|
133
147
|
email: blambeau@gmail.com
|
@@ -153,6 +167,7 @@ files:
|
|
153
167
|
- lib/bmg/operator/autowrap.rb
|
154
168
|
- lib/bmg/operator/constants.rb
|
155
169
|
- lib/bmg/operator/extend.rb
|
170
|
+
- lib/bmg/operator/generator.rb
|
156
171
|
- lib/bmg/operator/group.rb
|
157
172
|
- lib/bmg/operator/image.rb
|
158
173
|
- lib/bmg/operator/join.rb
|
@@ -167,8 +182,10 @@ files:
|
|
167
182
|
- lib/bmg/operator/shared/binary.rb
|
168
183
|
- lib/bmg/operator/shared/nary.rb
|
169
184
|
- lib/bmg/operator/shared/unary.rb
|
185
|
+
- lib/bmg/operator/shared/zeroary.rb
|
170
186
|
- lib/bmg/operator/summarize.rb
|
171
187
|
- lib/bmg/operator/transform.rb
|
188
|
+
- lib/bmg/operator/undress.rb
|
172
189
|
- lib/bmg/operator/ungroup.rb
|
173
190
|
- lib/bmg/operator/union.rb
|
174
191
|
- lib/bmg/operator/unwrap.rb
|
@@ -296,6 +313,7 @@ files:
|
|
296
313
|
- lib/bmg/summarizer/value_by.rb
|
297
314
|
- lib/bmg/summarizer/variance.rb
|
298
315
|
- lib/bmg/support.rb
|
316
|
+
- lib/bmg/support/factory.rb
|
299
317
|
- lib/bmg/support/keys.rb
|
300
318
|
- lib/bmg/support/ordering.rb
|
301
319
|
- lib/bmg/support/output_preferences.rb
|
@@ -305,6 +323,7 @@ files:
|
|
305
323
|
- lib/bmg/version.rb
|
306
324
|
- lib/bmg/writer.rb
|
307
325
|
- lib/bmg/writer/csv.rb
|
326
|
+
- lib/bmg/writer/text.rb
|
308
327
|
- lib/bmg/writer/xlsx.rb
|
309
328
|
- lib/bmg/xlsx.rb
|
310
329
|
- tasks/gem.rake
|
@@ -313,7 +332,7 @@ homepage: http://github.com/enspirit/bmg
|
|
313
332
|
licenses:
|
314
333
|
- MIT
|
315
334
|
metadata: {}
|
316
|
-
post_install_message:
|
335
|
+
post_install_message:
|
317
336
|
rdoc_options: []
|
318
337
|
require_paths:
|
319
338
|
- lib
|
@@ -329,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
329
348
|
version: '0'
|
330
349
|
requirements: []
|
331
350
|
rubygems_version: 3.3.27
|
332
|
-
signing_key:
|
351
|
+
signing_key:
|
333
352
|
specification_version: 4
|
334
353
|
summary: Bmg is Alf's successor.
|
335
354
|
test_files: []
|