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.
@@ -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