bmg 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE.md +22 -0
- data/README.md +3 -0
- data/Rakefile +11 -0
- data/lib/bmg.rb +18 -0
- data/lib/bmg/operator.rb +14 -0
- data/lib/bmg/operator/allbut.rb +44 -0
- data/lib/bmg/operator/autosummarize.rb +190 -0
- data/lib/bmg/operator/autowrap.rb +109 -0
- data/lib/bmg/operator/project.rb +43 -0
- data/lib/bmg/operator/rename.rb +45 -0
- data/lib/bmg/reader.rb +11 -0
- data/lib/bmg/reader/csv.rb +56 -0
- data/lib/bmg/reader/excel.rb +35 -0
- data/lib/bmg/relation.rb +34 -0
- data/lib/bmg/version.rb +8 -0
- data/spec/unit/operator/test_allbut.rb +18 -0
- data/spec/unit/operator/test_autosummarize.rb +183 -0
- data/spec/unit/operator/test_autowrap.rb +88 -0
- data/spec/unit/operator/test_project.rb +18 -0
- data/spec/unit/operator/test_rename.rb +13 -0
- data/spec/unit/reader/example.csv +3 -0
- data/spec/unit/reader/example.numbers +0 -0
- data/spec/unit/reader/example.xlsx +0 -0
- data/spec/unit/reader/test_csv.rb +17 -0
- data/spec/unit/reader/test_excel.rb +17 -0
- data/spec/unit/spec_helper.rb +11 -0
- data/spec/unit/test_relation.rb +132 -0
- data/tasks/gem.rake +39 -0
- data/tasks/test.rake +17 -0
- metadata +130 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Operator
|
3
|
+
#
|
4
|
+
# Rename operator.
|
5
|
+
#
|
6
|
+
# Rename some attribute of input tuples, according to a renaming Hash.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# [{ a: 1, b: 2 }] rename {:b => :c} => [{ a: 1, c: 2 }]
|
11
|
+
#
|
12
|
+
# Keys of the renaming Hash SHOULD be existing attributes of the
|
13
|
+
# input tuples. Values of the renaming Hash SHOULD NOT be existing
|
14
|
+
# attributes of the input tuples.
|
15
|
+
#
|
16
|
+
class Rename
|
17
|
+
include Operator
|
18
|
+
|
19
|
+
def initialize(operand, renaming)
|
20
|
+
@operand = operand
|
21
|
+
@renaming = renaming
|
22
|
+
end
|
23
|
+
|
24
|
+
def each
|
25
|
+
@operand.each do |tuple|
|
26
|
+
yield rename(tuple)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def rename(tuple)
|
33
|
+
tuple.each_with_object({}){|(k,v),h|
|
34
|
+
h[rename_key(k)] = v
|
35
|
+
h
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def rename_key(k)
|
40
|
+
@renaming[k] || k
|
41
|
+
end
|
42
|
+
|
43
|
+
end # class Rename
|
44
|
+
end # module Operator
|
45
|
+
end # module Bmg
|
data/lib/bmg/reader.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Reader
|
3
|
+
class Csv
|
4
|
+
include Reader
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
:headers => true,
|
8
|
+
:return_headers => false
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize(path, options = {})
|
12
|
+
@path = path
|
13
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
14
|
+
@options[:col_sep] ||= infer_col_sep
|
15
|
+
@options[:quote_char] ||= infer_quote_char
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
require 'csv'
|
20
|
+
::CSV.foreach(@path, @options) do |row|
|
21
|
+
yield tuple(row)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def tuple(row)
|
28
|
+
row.to_hash.each_with_object({}){|(k,v),h| h[k.to_sym] = v }
|
29
|
+
end
|
30
|
+
|
31
|
+
def infer_col_sep
|
32
|
+
sniff(text_portion, [",","\t",";"], ",")
|
33
|
+
end
|
34
|
+
|
35
|
+
def infer_quote_char
|
36
|
+
sniff(text_portion, ["'","\""], "\"")
|
37
|
+
end
|
38
|
+
|
39
|
+
def text_portion
|
40
|
+
@text_portion ||= File.foreach(@path).first(10).join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Finds the best candidate among `candidates` for a separator
|
44
|
+
# found in `str`. If none is found, returns `default`.
|
45
|
+
def sniff(str, candidates, default)
|
46
|
+
snif = {}
|
47
|
+
candidates.each {|delim|
|
48
|
+
snif[delim] = str.count(delim)
|
49
|
+
}
|
50
|
+
snif = snif.sort {|a,b| b[1] <=> a[1] }
|
51
|
+
snif.size > 0 ? snif[0][0] : default
|
52
|
+
end
|
53
|
+
|
54
|
+
end # class Csv
|
55
|
+
end # module Reader
|
56
|
+
end # module Bmg
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Bmg
|
2
|
+
module Reader
|
3
|
+
class Excel
|
4
|
+
include Reader
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
skip: 0
|
8
|
+
}
|
9
|
+
|
10
|
+
def initialize(path, options = {})
|
11
|
+
@path = path
|
12
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
require 'roo'
|
17
|
+
xlsx = Roo::Spreadsheet.open(@path)
|
18
|
+
headers = nil
|
19
|
+
xlsx.sheet(0)
|
20
|
+
.each
|
21
|
+
.drop(@options[:skip])
|
22
|
+
.each_with_index
|
23
|
+
.each do |row, i|
|
24
|
+
if i==0
|
25
|
+
headers = row.map(&:to_sym)
|
26
|
+
else
|
27
|
+
tuple = (0...headers.size).each_with_object({}){|i,t| t[headers[i]] = row[i] }
|
28
|
+
yield(tuple)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class Excel
|
34
|
+
end # module Reader
|
35
|
+
end # module Bmg
|
data/lib/bmg/relation.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Bmg
|
2
|
+
class Relation
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(operand)
|
6
|
+
@operand = operand
|
7
|
+
end
|
8
|
+
|
9
|
+
def each(&bl)
|
10
|
+
@operand.each(&bl)
|
11
|
+
end
|
12
|
+
|
13
|
+
def allbut(butlist = [])
|
14
|
+
Relation.new Operator::Allbut.new(@operand, butlist)
|
15
|
+
end
|
16
|
+
|
17
|
+
def autowrap(options = {})
|
18
|
+
Relation.new Operator::Autowrap.new(@operand, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def autosummarize(by = [], summarization = {})
|
22
|
+
Relation.new Operator::Autosummarize.new(@operand, by, summarization)
|
23
|
+
end
|
24
|
+
|
25
|
+
def project(attrlist = [])
|
26
|
+
Relation.new Operator::Project.new(@operand, attrlist)
|
27
|
+
end
|
28
|
+
|
29
|
+
def rename(renaming = {})
|
30
|
+
Relation.new Operator::Rename.new(@operand, renaming)
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class Relation
|
34
|
+
end # module Bmg
|
data/lib/bmg/version.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Bmg
|
3
|
+
module Operator
|
4
|
+
describe Allbut do
|
5
|
+
|
6
|
+
it 'works' do
|
7
|
+
allbut = Allbut.new [{ a: 1, b: 2 }], [:b]
|
8
|
+
expect(allbut.to_a).to eql([{ a: 1 }])
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'removes duplicates' do
|
12
|
+
allbut = Allbut.new [{ a: 1, b: 2 }, { a: 1, b: 3 }], [:b]
|
13
|
+
expect(allbut.to_a).to eql([{ a: 1 }])
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Bmg
|
3
|
+
module Operator
|
4
|
+
describe Autosummarize do
|
5
|
+
|
6
|
+
context 'with empty by and no sums' do
|
7
|
+
let(:by) { [] }
|
8
|
+
let(:sums){ {} }
|
9
|
+
|
10
|
+
it 'filters same tuples' do
|
11
|
+
autosummarize = Autosummarize.new [
|
12
|
+
{ a: 1 },
|
13
|
+
{ a: 1 }
|
14
|
+
], by, sums
|
15
|
+
expect(autosummarize.to_a).to eql([{ a: 1 }])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with a determinant and no sums' do
|
20
|
+
let(:by) { [:a] }
|
21
|
+
let(:sums){ {} }
|
22
|
+
|
23
|
+
it 'applies Same to every unknown dependent' do
|
24
|
+
autosummarize = Autosummarize.new [
|
25
|
+
{ a: 1, b: 2 },
|
26
|
+
{ a: 1, b: 2 },
|
27
|
+
{ a: 2, b: 2 },
|
28
|
+
], by, sums
|
29
|
+
expect(autosummarize.to_a).to eql([
|
30
|
+
{ a: 1, b: 2 },
|
31
|
+
{ a: 2, b: 2 }
|
32
|
+
])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with a by and a DistinctList without comparator' do
|
37
|
+
let(:by) { [ :id ] }
|
38
|
+
let(:sums){ { :a => Autosummarize::DistinctList.new } }
|
39
|
+
|
40
|
+
it 'groups as expected' do
|
41
|
+
autosummarize = Autosummarize.new [
|
42
|
+
{ id: 1, a: 1 },
|
43
|
+
{ id: 1, a: 2 },
|
44
|
+
{ id: 2, a: 1 },
|
45
|
+
{ id: 1, a: 2 }
|
46
|
+
], by, sums
|
47
|
+
expect(autosummarize.to_a).to eql([
|
48
|
+
{ id: 1, a: [1, 2] },
|
49
|
+
{ id: 2, a: [1] },
|
50
|
+
])
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'ignores nulls' do
|
54
|
+
autosummarize = Autosummarize.new [
|
55
|
+
{ id: 1, a: 1 },
|
56
|
+
{ id: 1, a: nil }
|
57
|
+
], by, sums
|
58
|
+
expect(autosummarize.to_a).to eql([
|
59
|
+
{ id: 1, a: [1] },
|
60
|
+
])
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'can be used to mimic the Group operator' do
|
64
|
+
autosummarize = Autosummarize.new [
|
65
|
+
{ id: 1, a: { x: 1, y: 2 } },
|
66
|
+
{ id: 1, a: { x: 2, y: 2 } },
|
67
|
+
{ id: 2, a: { x: 1, y: 2 } },
|
68
|
+
{ id: 1, a: { x: 1, y: 2 } }
|
69
|
+
], by, sums
|
70
|
+
expect(autosummarize.to_a).to eql([
|
71
|
+
{ id: 1, a: [{ x: 1, y: 2 }, { x: 2, y: 2 }] },
|
72
|
+
{ id: 2, a: [{ x: 1, y: 2 }] },
|
73
|
+
])
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'supports the :group shortcut' do
|
77
|
+
autosummarize = Autosummarize.new [
|
78
|
+
{ id: 1, a: 1 },
|
79
|
+
{ id: 1, a: 2 },
|
80
|
+
{ id: 2, a: 1 },
|
81
|
+
{ id: 1, a: 2 }
|
82
|
+
], by, { :a => :group }
|
83
|
+
expect(autosummarize.to_a).to eql([
|
84
|
+
{ id: 1, a: [1, 2] },
|
85
|
+
{ id: 2, a: [1] },
|
86
|
+
])
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with a by and a DistinctList with a comparator' do
|
92
|
+
let(:by) { [ :id ] }
|
93
|
+
let(:sums){ { :a => Autosummarize::DistinctList.new{|x,y| y <=> x } } }
|
94
|
+
|
95
|
+
it 'groups as expected' do
|
96
|
+
autosummarize = Autosummarize.new [
|
97
|
+
{ id: 1, a: 1 },
|
98
|
+
{ id: 1, a: 2 },
|
99
|
+
{ id: 2, a: 1 },
|
100
|
+
{ id: 1, a: 2 }
|
101
|
+
], by, sums
|
102
|
+
expect(autosummarize.to_a).to eql([
|
103
|
+
{ id: 1, a: [2, 1] },
|
104
|
+
{ id: 2, a: [1] },
|
105
|
+
])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with a YByX ignoring nulls' do
|
110
|
+
let(:by) { [ :id ] }
|
111
|
+
let(:sums){ { :a => Autosummarize::YByX.new(:y, :x) } }
|
112
|
+
|
113
|
+
it 'groups as expected and ignores nulls' do
|
114
|
+
autosummarize = Autosummarize.new [
|
115
|
+
{ id: 1, a: { x: "foo", y: "bar" } },
|
116
|
+
{ id: 1, a: { x: "foo", y: "baz" } },
|
117
|
+
{ id: 1, a: { x: "gri", y: "gra" } },
|
118
|
+
{ id: 1, a: { x: "gro", y: nil } },
|
119
|
+
{ id: 1, a: { x: nil, y: "gru" } },
|
120
|
+
], by, sums
|
121
|
+
expect(autosummarize.to_a).to eql([
|
122
|
+
{ id: 1, a: { "foo" => "baz", "gri" => "gra" } }
|
123
|
+
])
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'with a YByX preserving nulls' do
|
128
|
+
let(:by) { [ :id ] }
|
129
|
+
let(:sums){ { :a => Autosummarize::YByX.new(:y, :x, true) } }
|
130
|
+
|
131
|
+
it 'groups as expected and ignores nulls' do
|
132
|
+
autosummarize = Autosummarize.new [
|
133
|
+
{ id: 1, a: { x: "foo", y: "bar" } },
|
134
|
+
{ id: 1, a: { x: "foo", y: "baz" } },
|
135
|
+
{ id: 1, a: { x: "gri", y: "gra" } },
|
136
|
+
{ id: 1, a: { x: "gro", y: nil } },
|
137
|
+
{ id: 1, a: { x: nil, y: "gru" } },
|
138
|
+
], by, sums
|
139
|
+
expect(autosummarize.to_a).to eql([
|
140
|
+
{ id: 1, a: { "foo" => "baz", "gri" => "gra", "gro" => nil } }
|
141
|
+
])
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'with a by and a YsByX and no sorter' do
|
146
|
+
let(:by) { [ :id ] }
|
147
|
+
let(:sums){ { :a => Autosummarize::YsByX.new(:y, :x) } }
|
148
|
+
|
149
|
+
it 'filters same tuples' do
|
150
|
+
autosummarize = Autosummarize.new [
|
151
|
+
{ id: 1, a: { x: 1, y: 3 } },
|
152
|
+
{ id: 1, a: { x: 1, y: 2 } },
|
153
|
+
{ id: 2, a: { x: 1, y: 1 } },
|
154
|
+
{ id: 1, a: { x: 2, y: 7 } }
|
155
|
+
], by, sums
|
156
|
+
expect(autosummarize.to_a).to eql([
|
157
|
+
{ id: 1, a: { 1 => [3, 2], 2 => [7] } },
|
158
|
+
{ id: 2, a: { 1 => [1] } }
|
159
|
+
])
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'with a by and a YsByX and a sorter' do
|
164
|
+
let(:by) { [ :id ] }
|
165
|
+
let(:sums){ { :a => Autosummarize::YsByX.new(:y, :x){|u,v| u[:y] <=> v[:y] } } }
|
166
|
+
|
167
|
+
it 'filters same tuples' do
|
168
|
+
autosummarize = Autosummarize.new [
|
169
|
+
{ id: 1, a: { x: 1, y: 3 } },
|
170
|
+
{ id: 1, a: { x: 1, y: 2 } },
|
171
|
+
{ id: 2, a: { x: 1, y: 1 } },
|
172
|
+
{ id: 1, a: { x: 2, y: 7 } }
|
173
|
+
], by, sums
|
174
|
+
expect(autosummarize.to_a).to eql([
|
175
|
+
{ id: 1, a: { 1 => [2, 3], 2 => [7] } },
|
176
|
+
{ id: 2, a: { 1 => [1] } }
|
177
|
+
])
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Bmg
|
3
|
+
module Operator
|
4
|
+
describe 'Autowrap' do
|
5
|
+
|
6
|
+
context 'when called in default mode' do
|
7
|
+
|
8
|
+
it 'works as an array by default' do
|
9
|
+
autowrap = Autowrap.new [{ a: 1, b: 2 }]
|
10
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: 2 }])
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'wrap levels 1' do
|
14
|
+
autowrap = Autowrap.new [{ a: 1, b_x: 2, b_y: 3 }]
|
15
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: { x: 2, y: 3 } }])
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'wrap levels 2' do
|
19
|
+
autowrap = Autowrap.new [{ a: 1, b_x_u: 2, b_y_v: 3, b_y_w: 4 }]
|
20
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: { x: { u: 2 }, y: { v: 3, w: 4 } } }])
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'keeps LEFT JOIN nils unchanged' do
|
24
|
+
autowrap = Autowrap.new [{ a: 1, b_x: nil, b_y: nil }]
|
25
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: { x: nil, y: nil } }])
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when specifying the separator to use' do
|
31
|
+
|
32
|
+
it 'works as expected' do
|
33
|
+
autowrap = Autowrap.new [{ :a => 1, :"b.x.u" => 2, "b.y.v" => 3, "b.y.w" => 4 }], split: '.'
|
34
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: { x: { u: 2 }, y: { v: 3, w: 4 } } }])
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when called with a Proc post processor' do
|
40
|
+
|
41
|
+
let(:post) {
|
42
|
+
->(t,_){ t.delete(:user) if t[:user][:id].nil?; t }
|
43
|
+
}
|
44
|
+
|
45
|
+
it 'wrap levels 2' do
|
46
|
+
aw = Autowrap.new [
|
47
|
+
{ user_id: 1, user_name: "foo", foo: "bar" },
|
48
|
+
{ user_id: nil, user_name: nil, foo: "baz" }
|
49
|
+
], postprocessor: post
|
50
|
+
expected = [
|
51
|
+
{ user: {id: 1, name: "foo"}, foo: "bar" },
|
52
|
+
{ foo: "baz" }
|
53
|
+
]
|
54
|
+
expect(aw.to_a).to eql(expected)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when called with :delete post processor' do
|
60
|
+
|
61
|
+
it 'automatically removes the results of nil LEFT JOINs' do
|
62
|
+
autowrap = Autowrap.new [{ a: 1, b_x: nil, b_y: nil }], postprocessor: :delete
|
63
|
+
expect(autowrap.to_a).to eql([{ a: 1 }])
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when called with :nil post processor' do
|
69
|
+
|
70
|
+
it 'sets the results of nil LEFT JOINs to nil' do
|
71
|
+
autowrap = Autowrap.new [{ a: 1, b_x: nil, b_y: nil }], postprocessor: :nil
|
72
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: nil }])
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'when called with a Hash post processor' do
|
78
|
+
|
79
|
+
it 'sets the results of nil LEFT JOINs to nil' do
|
80
|
+
autowrap = Autowrap.new [{ a: 1, b_x: nil, b_y: nil, c_x: nil, c_y: nil, d_x: nil }], postprocessor: { b: :nil, c: :delete }
|
81
|
+
expect(autowrap.to_a).to eql([{ a: 1, b: nil, d: { x: nil } }])
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|