bmg 0.1.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 +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
|