bmg 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ module Bmg
2
+ module Reader
3
+
4
+ def to_a
5
+ to_enum(:each).to_a
6
+ end
7
+
8
+ end
9
+ end
10
+ require_relative "reader/csv"
11
+ require_relative "reader/excel"
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ module Bmg
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+ end
7
+ VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
+ end
@@ -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