google_data_source 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +31 -0
- data/google_data_source.gemspec +32 -0
- data/lib/assets/images/google_data_source/chart_bar_add.png +0 -0
- data/lib/assets/images/google_data_source/chart_bar_delete.png +0 -0
- data/lib/assets/images/google_data_source/loader.gif +0 -0
- data/lib/assets/javascripts/google_data_source/data_source_init.js +3 -0
- data/lib/assets/javascripts/google_data_source/extended_data_table.js +76 -0
- data/lib/assets/javascripts/google_data_source/filter_form.js +180 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/combo_table.js.erb +113 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/table.js +116 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/timeline.js +13 -0
- data/lib/assets/javascripts/google_data_source/google_visualization/visualization.js.erb +141 -0
- data/lib/assets/javascripts/google_data_source/index.js +7 -0
- data/lib/dummy_engine.rb +5 -0
- data/lib/google_data_source.rb +33 -0
- data/lib/google_data_source/base.rb +281 -0
- data/lib/google_data_source/column.rb +31 -0
- data/lib/google_data_source/csv_data.rb +23 -0
- data/lib/google_data_source/data_date.rb +17 -0
- data/lib/google_data_source/data_date_time.rb +17 -0
- data/lib/google_data_source/helper.rb +69 -0
- data/lib/google_data_source/html_data.rb +6 -0
- data/lib/google_data_source/invalid_data.rb +14 -0
- data/lib/google_data_source/json_data.rb +78 -0
- data/lib/google_data_source/railtie.rb +36 -0
- data/lib/google_data_source/sql/models.rb +266 -0
- data/lib/google_data_source/sql/parser.rb +239 -0
- data/lib/google_data_source/sql_parser.rb +82 -0
- data/lib/google_data_source/template_handler.rb +31 -0
- data/lib/google_data_source/test_helper.rb +26 -0
- data/lib/google_data_source/version.rb +3 -0
- data/lib/google_data_source/xml_data.rb +25 -0
- data/lib/locale/de.yml +5 -0
- data/lib/reporting/action_controller_extension.rb +19 -0
- data/lib/reporting/grouped_set.rb +58 -0
- data/lib/reporting/helper.rb +110 -0
- data/lib/reporting/reporting.rb +352 -0
- data/lib/reporting/reporting_adapter.rb +27 -0
- data/lib/reporting/reporting_entry.rb +147 -0
- data/lib/reporting/sql_reporting.rb +220 -0
- data/test/lib/empty_reporting.rb +2 -0
- data/test/lib/test_reporting.rb +33 -0
- data/test/lib/test_reporting_b.rb +9 -0
- data/test/lib/test_reporting_c.rb +3 -0
- data/test/locales/en.models.yml +6 -0
- data/test/locales/en.reportings.yml +5 -0
- data/test/rails/reporting_renderer_test.rb +47 -0
- data/test/test_helper.rb +50 -0
- data/test/units/base_test.rb +340 -0
- data/test/units/csv_data_test.rb +36 -0
- data/test/units/grouped_set_test.rb +60 -0
- data/test/units/json_data_test.rb +68 -0
- data/test/units/reporting_adapter_test.rb +20 -0
- data/test/units/reporting_entry_test.rb +149 -0
- data/test/units/reporting_test.rb +374 -0
- data/test/units/sql_parser_test.rb +111 -0
- data/test/units/sql_reporting_test.rb +307 -0
- data/test/units/xml_data_test.rb +32 -0
- metadata +286 -0
@@ -0,0 +1,239 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
module Sql
|
4
|
+
class ::Method; include RParsec::FunctorMixin; end
|
5
|
+
class ::Proc; include RParsec::FunctorMixin; end
|
6
|
+
|
7
|
+
module Parser
|
8
|
+
include RParsec
|
9
|
+
include Functors
|
10
|
+
include Parsers
|
11
|
+
|
12
|
+
extend Parsers
|
13
|
+
# TODO drop keywords
|
14
|
+
MyKeywords = Keywords.case_insensitive(%w{
|
15
|
+
select from where group by having order desc asc
|
16
|
+
inner left right full outer inner join on cross
|
17
|
+
union all distinct as exists in between limit offset
|
18
|
+
case when else end and or not true false
|
19
|
+
})
|
20
|
+
MyOperators = Operators.new(%w{+ - * / % = > < >= <= <> != : ( ) . ,})
|
21
|
+
def self.operators(*ops)
|
22
|
+
result = []
|
23
|
+
ops.each do |op|
|
24
|
+
result << (MyOperators[op] >> op.to_sym)
|
25
|
+
end
|
26
|
+
sum(*result)
|
27
|
+
end
|
28
|
+
Comparators = operators(*%w{= > < >= <= <> !=})
|
29
|
+
quote_mapper = Proc.new do |raw|
|
30
|
+
# is this really different to raw.gsub! ???
|
31
|
+
raw.replace(raw.gsub(/\\'/, "'").gsub(/\\\\/, "\\"))
|
32
|
+
end
|
33
|
+
|
34
|
+
StringLiteral = char(?') >> ((str("\\\\")|str("\\'")|not_char(?')).many_.fragment).map("e_mapper) << char(?')
|
35
|
+
QuotedName = char(?`) >> not_char(?`).many_.fragment << char(?`)
|
36
|
+
Variable = char(?$) >> word
|
37
|
+
MyLexer = number.token(:number) | StringLiteral.token(:string) | Variable.token(:var) |
|
38
|
+
QuotedName.token(:word) | MyKeywords.lexer | MyOperators.lexer
|
39
|
+
MyLexeme = MyLexer.lexeme(whitespaces | comment_line('#')) << eof
|
40
|
+
|
41
|
+
|
42
|
+
######################################### utilities #########################################
|
43
|
+
def keyword
|
44
|
+
MyKeywords
|
45
|
+
end
|
46
|
+
|
47
|
+
def operator
|
48
|
+
MyOperators
|
49
|
+
end
|
50
|
+
|
51
|
+
def comma
|
52
|
+
operator[',']
|
53
|
+
end
|
54
|
+
|
55
|
+
def list expr
|
56
|
+
paren(expr.delimited(comma))
|
57
|
+
end
|
58
|
+
|
59
|
+
def word(&block)
|
60
|
+
if block.nil?
|
61
|
+
token(:word, &Id)
|
62
|
+
else
|
63
|
+
token(:word, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def paren parser
|
68
|
+
operator['('] >> parser << operator[')']
|
69
|
+
end
|
70
|
+
|
71
|
+
def ctor cls
|
72
|
+
cls.method :new
|
73
|
+
end
|
74
|
+
|
75
|
+
def rctor cls, arity=2
|
76
|
+
ctor(cls).reverse_curry arity
|
77
|
+
end
|
78
|
+
|
79
|
+
################################### predicate parser #############################
|
80
|
+
def logical_operator op
|
81
|
+
proc{|a,b|CompoundPredicate.new(a,op,b)}
|
82
|
+
end
|
83
|
+
|
84
|
+
def make_predicate expr, rel
|
85
|
+
expr_list = list expr
|
86
|
+
comparison = make_comparison_predicate expr, rel
|
87
|
+
group_comparison = sequence(expr_list, Comparators, expr_list, &ctor(GroupComparisonPredicate))
|
88
|
+
bool = nil
|
89
|
+
lazy_bool = lazy{bool}
|
90
|
+
bool_term = keyword[:true] >> true | keyword[:false] >> false |
|
91
|
+
comparison | group_comparison | paren(lazy_bool) |
|
92
|
+
make_exists(rel) | make_not_exists(rel)
|
93
|
+
bool_table = OperatorTable.new.
|
94
|
+
infixl(keyword[:or] >> logical_operator(:or), 20).
|
95
|
+
infixl(keyword[:and] >> logical_operator(:and), 30).
|
96
|
+
prefix(keyword[:not] >> ctor(NotPredicate), 40)
|
97
|
+
bool = Expressions.build(bool_term, bool_table)
|
98
|
+
end
|
99
|
+
|
100
|
+
def make_exists rel
|
101
|
+
keyword[:exists] >> rel.map(&ctor(ExistsPredicate))
|
102
|
+
end
|
103
|
+
|
104
|
+
def make_not_exists rel
|
105
|
+
keyword[:not] >> keyword[:exists] >> rel.map(&ctor(NotExistsPredicate))
|
106
|
+
end
|
107
|
+
|
108
|
+
def make_in expr
|
109
|
+
keyword[:in] >> list(expr) >> map(&rctor(InPredicate))
|
110
|
+
end
|
111
|
+
|
112
|
+
def make_not_in expr
|
113
|
+
keyword[:not] >> keyword[:in] >> list(expr) >> map(&rctor(NotInPredicate))
|
114
|
+
end
|
115
|
+
|
116
|
+
def make_in_relation rel
|
117
|
+
keyword[:in] >> rel.map(&rctor(InRelationPredicate))
|
118
|
+
end
|
119
|
+
|
120
|
+
def make_not_in_relation rel
|
121
|
+
keyword[:not] >> keyword[:in] >> rel.map(&rctor(NotInRelationPredicate))
|
122
|
+
end
|
123
|
+
|
124
|
+
def make_between expr
|
125
|
+
make_between_clause(expr, &ctor(BetweenPredicate))
|
126
|
+
end
|
127
|
+
|
128
|
+
def make_not_between expr
|
129
|
+
keyword[:not] >> make_between_clause(expr, &ctor(NotBetweenPredicate))
|
130
|
+
end
|
131
|
+
|
132
|
+
def make_comparison_predicate expr, rel
|
133
|
+
comparison = sequence(Comparators, expr) do |op,e2|
|
134
|
+
proc{|e1|ComparePredicate.new(e1, op, e2)}
|
135
|
+
end
|
136
|
+
in_clause = make_in expr
|
137
|
+
not_in_clause = make_not_in expr
|
138
|
+
in_relation = make_in_relation rel
|
139
|
+
not_in_relation = make_not_in_relation rel
|
140
|
+
between = make_between expr
|
141
|
+
not_between = make_not_between expr
|
142
|
+
compare_with = comparison | in_clause | not_in_clause |
|
143
|
+
in_relation | not_in_relation | between | not_between
|
144
|
+
sequence(expr, compare_with, &Feed)
|
145
|
+
end
|
146
|
+
|
147
|
+
def make_between_clause expr, &maker
|
148
|
+
factory = proc do |a,_,b|
|
149
|
+
proc {|v|maker.call(v,a,b)}
|
150
|
+
end
|
151
|
+
variant1 = keyword[:between] >> paren(sequence(expr, comma, expr, &factory))
|
152
|
+
variant2 = keyword[:between] >> sequence(expr, keyword[:and], expr, &factory)
|
153
|
+
variant1 | variant2
|
154
|
+
end
|
155
|
+
|
156
|
+
################################ expression parser ###############################
|
157
|
+
def calculate_simple_cases(val, cases, default)
|
158
|
+
SimpleCaseExpr.new(val, cases, default)
|
159
|
+
end
|
160
|
+
|
161
|
+
def calculate_full_cases(cases, default)
|
162
|
+
CaseExpr.new(cases, default)
|
163
|
+
end
|
164
|
+
|
165
|
+
def make_expression predicate, rel
|
166
|
+
expr = nil
|
167
|
+
lazy_expr = lazy{expr}
|
168
|
+
|
169
|
+
wildcard = operator[:*] >> WildcardExpr::Instance
|
170
|
+
lit = token(:number, :string, &ctor(LiteralExpr)) | token(:var, &ctor(VarExpr))
|
171
|
+
atom = lit | wildcard | word(&ctor(WordExpr))
|
172
|
+
term = atom | (operator['('] >> lazy_expr << operator[')'])
|
173
|
+
|
174
|
+
table = OperatorTable.new.
|
175
|
+
infixl(operator['+'] >> Plus, 20).
|
176
|
+
infixl(operator['-'] >> Minus, 20).
|
177
|
+
infixl(operator['*'] >> Mul, 30).
|
178
|
+
infixl(operator['/'] >> Div, 30).
|
179
|
+
infixl(operator['%'] >> Mod, 30).
|
180
|
+
prefix(operator['-'] >> Neg, 50)
|
181
|
+
expr = Expressions.build(term, table)
|
182
|
+
end
|
183
|
+
|
184
|
+
################################ relation parser ###############################
|
185
|
+
def make_relation expr, pred
|
186
|
+
where_clause = keyword[:where] >> pred
|
187
|
+
order_element = sequence(expr, (keyword[:asc] >> true | keyword[:desc] >> false).optional(true),
|
188
|
+
&ctor(OrderElement))
|
189
|
+
order_elements = order_element.separated1(comma)
|
190
|
+
exprs = expr.separated1(comma)
|
191
|
+
|
192
|
+
# setup clauses
|
193
|
+
select_clause = keyword[:select] >> exprs
|
194
|
+
order_by_clause = keyword[:order] >> keyword[:by] >> order_elements
|
195
|
+
group_by = keyword[:group] >> keyword[:by] >> exprs
|
196
|
+
group_by_clause = sequence(group_by, (keyword[:having] >> pred).optional, &ctor(GroupByClause))
|
197
|
+
limit_clause = keyword[:limit] >> token(:number, &To_i)
|
198
|
+
offset_clause = keyword[:offset] >> token(:number, &To_i)
|
199
|
+
|
200
|
+
# build relation
|
201
|
+
relation = sequence(
|
202
|
+
select_clause.optional([WildcardExpr.new]),
|
203
|
+
where_clause.optional, group_by_clause.optional, order_by_clause.optional,
|
204
|
+
limit_clause.optional, offset_clause.optional
|
205
|
+
) do |select, where, groupby, orderby, limit, offset|
|
206
|
+
SelectRelation.new(select, where, groupby, orderby, limit, offset)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
########################## put together ###############################
|
211
|
+
def expression
|
212
|
+
assemble[0]
|
213
|
+
end
|
214
|
+
|
215
|
+
def relation
|
216
|
+
assemble[2]
|
217
|
+
end
|
218
|
+
def predicate
|
219
|
+
assemble[1]
|
220
|
+
end
|
221
|
+
|
222
|
+
def assemble
|
223
|
+
pred = nil
|
224
|
+
rel = nil
|
225
|
+
lazy_predicate = lazy{pred}
|
226
|
+
lazy_rel = lazy{rel}
|
227
|
+
expr = make_expression lazy_predicate, lazy_rel
|
228
|
+
pred = make_predicate expr, lazy_rel
|
229
|
+
rel = make_relation expr, pred
|
230
|
+
return expr, pred, rel
|
231
|
+
end
|
232
|
+
|
233
|
+
def make parser
|
234
|
+
MyLexeme.nested(parser << eof)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
class SimpleSqlException < Exception; end
|
4
|
+
|
5
|
+
class SqlParser
|
6
|
+
include Sql::Parser
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Parses a simple SQL query and return the result
|
10
|
+
# The where statement may only contain equality comparisons that are connected with 'and'
|
11
|
+
# Only a single ordering parameter is accepted
|
12
|
+
# Throws a +SimpleSqlException+ if these conditions are not satisfied
|
13
|
+
def simple_parse(query)
|
14
|
+
result = parse(query)
|
15
|
+
OpenStruct.new({
|
16
|
+
:select => result.select.collect(&:to_s),
|
17
|
+
:conditions => simple_where_parser(result.where),
|
18
|
+
:orderby => simple_orderby_parser(result.orderby),
|
19
|
+
:groupby => simple_groupby_parser(result.groupby),
|
20
|
+
:limit => result.limit,
|
21
|
+
:offset => result.offset
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parses a SQL query and returns the result
|
26
|
+
def parse(query)
|
27
|
+
parser.parse(query)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
# Helper to the +simple_parse+ method
|
32
|
+
def simple_where_parser(predicate, result = Hash.new)
|
33
|
+
case predicate.class.name.split('::').last
|
34
|
+
when 'CompoundPredicate'
|
35
|
+
raise SimpleSqlException.new("Operator forbidden (use only 'and')") unless predicate.op == :and
|
36
|
+
simple_where_parser(predicate.left, result)
|
37
|
+
simple_where_parser(predicate.right, result)
|
38
|
+
when 'ComparePredicate'
|
39
|
+
case predicate.op
|
40
|
+
when :"="
|
41
|
+
result[predicate.left.to_s] = predicate.right.to_s
|
42
|
+
when :"<", :">", :">=", :"<=", :"<>", :"!="
|
43
|
+
result[predicate.left.to_s] ||= Array.new
|
44
|
+
raise SimpleSqlException.new("Condition clach") unless result[predicate.left.to_s].is_a?(Array)
|
45
|
+
result[predicate.left.to_s] << OpenStruct.new(:op => predicate.op.to_s, :value => predicate.right.to_s)
|
46
|
+
else
|
47
|
+
raise SimpleSqlException.new("Comparator forbidden (use only '=,<,>')") unless predicate.op == :"="
|
48
|
+
end
|
49
|
+
when 'InPredicate'
|
50
|
+
result[predicate.expr.to_s] ||= Array.new
|
51
|
+
raise SimpleSqlException.new("Condition clach") unless result[predicate.expr.to_s].is_a?(Array)
|
52
|
+
result[predicate.expr.to_s] << OpenStruct.new(:op => 'in', :value => predicate.vals.map(&:to_s))
|
53
|
+
when 'NilClass'
|
54
|
+
# do nothing
|
55
|
+
else
|
56
|
+
raise SimpleSqlException.new("Unknown syntax error")
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
# Helper to the +simple_parse+ method
|
62
|
+
def simple_orderby_parser(orderby)
|
63
|
+
return nil if orderby.nil?
|
64
|
+
raise SimpleSqlException.new("Too many ordering arguments (1 allowed)") if orderby.size > 1
|
65
|
+
[orderby.first.expr.to_s, orderby.first.asc ? :asc : :desc]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Helper to the +simple_parse+ method
|
69
|
+
def simple_groupby_parser(groupby)
|
70
|
+
return [] if groupby.nil?
|
71
|
+
groupby.exprs.collect(&:to_s)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the parser
|
75
|
+
def parser
|
76
|
+
sql = self.new
|
77
|
+
sql.make(sql.relation)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module DataSource
|
3
|
+
# A simple template handler for a data source
|
4
|
+
# Provides a GoogleDataSource::Base object datasource in the template so the template
|
5
|
+
# can fill it with data
|
6
|
+
class TemplateHandler
|
7
|
+
|
8
|
+
def self.call(template)
|
9
|
+
<<-EOT
|
10
|
+
datasource = GoogleDataSource::DataSource::Base.from_params(params)
|
11
|
+
#{template.source.dup}
|
12
|
+
if !datasource.reporting.nil? && datasource.reporting.has_form?
|
13
|
+
datasource.callback = "$('\\\#\#{datasource.reporting.form_id}').html(\#{render(:partial => datasource.reporting.partial).to_json});"
|
14
|
+
end
|
15
|
+
|
16
|
+
if datasource.format == 'csv'
|
17
|
+
headers['Content-Type'] = 'text/csv; charset=utf-8'
|
18
|
+
headers['Content-Disposition'] = "attachment; filename=\\"\#{datasource.export_filename}.csv\\""
|
19
|
+
end
|
20
|
+
|
21
|
+
if datasource.format == 'xml'
|
22
|
+
headers['Content-Type'] = 'application/xml; charset=utf-8'
|
23
|
+
headers['Content-Disposition'] = "attachment; filename=\\"\#{datasource.export_filename}.xml\\""
|
24
|
+
end
|
25
|
+
|
26
|
+
datasource.response
|
27
|
+
EOT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
# Some usefull helpers for testing datasources
|
3
|
+
module TestHelper
|
4
|
+
|
5
|
+
# Returns the parsed JSON argument of the datasource response
|
6
|
+
def datasource_response
|
7
|
+
first_cmd = @response.body.match(/([^;"]*|"(\\"|[^"])*;?(\\"|[^"])*")*;/)[0]
|
8
|
+
response = OpenStruct.new(JSON.parse(first_cmd.match(/^[^(]*\((.*)\);$/)[1]))
|
9
|
+
response.table = OpenStruct.new(response.table)
|
10
|
+
response
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the columns array of the JSON response
|
14
|
+
def datasource_column(column)
|
15
|
+
response = datasource_response
|
16
|
+
column_no = response.table.cols.collect { |c| c['id'] }.index(column.to_s)
|
17
|
+
response.table.rows.collect { |r| r['c'][column_no] }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the column ids of the JSON response
|
21
|
+
def datasource_column_ids
|
22
|
+
datasource_response.table.cols.collect { |c| c['id'] }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module GoogleDataSource
|
4
|
+
module DataSource
|
5
|
+
class XmlData < Base
|
6
|
+
#include ActionView::Helpers::NumberHelper
|
7
|
+
def response
|
8
|
+
cols = columns.map { |col| col.id || col.type }
|
9
|
+
builder = ::Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
|
10
|
+
xml.send(xml_class.pluralize, :type => 'array') do
|
11
|
+
data.each do |datarow|
|
12
|
+
xml.send(xml_class) do
|
13
|
+
datarow.zip(cols).each do |val, key|
|
14
|
+
xml.send("#{key}", val.is_a?(Hash) ? val[:v] : val)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
builder.to_xml
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/locale/de.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module GoogleDataSource
|
2
|
+
module Reporting
|
3
|
+
module ActionControllerExtension
|
4
|
+
# Add the option +:reporting+ to the +render+ method.
|
5
|
+
# Takes a +Reporting+ object and renders the data-source response
|
6
|
+
# including the form validation results
|
7
|
+
def render_with_reporting(*args)
|
8
|
+
if !args.first.nil? && args.first.is_a?(Hash) && args.first.has_key?(:reporting)
|
9
|
+
reporting = args.first[:reporting]
|
10
|
+
datasource = GoogleDataSource::DataSource::Base.from_params(params)
|
11
|
+
datasource.set(reporting)
|
12
|
+
render_for_text datasource.response
|
13
|
+
else
|
14
|
+
render_without_reporting(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Represents an array of Objects which can be grouped according to grouping keys
|
2
|
+
# or a grouping block, which returns the hash used for grouping
|
3
|
+
#
|
4
|
+
# The objects must support the '+' operation which is used to implode sets of objects
|
5
|
+
# with the same grouping hash.
|
6
|
+
class GroupedSet < Array
|
7
|
+
#attr_reader :data
|
8
|
+
|
9
|
+
# Standard constructor
|
10
|
+
# +data+ is an Array of Objects implementing a '+' operation
|
11
|
+
#def initialize(data)
|
12
|
+
# @data = data
|
13
|
+
#end
|
14
|
+
|
15
|
+
# Bang method for the regrouping of the data
|
16
|
+
# The method takes either a set of keys or a block as grouping criterion
|
17
|
+
#
|
18
|
+
# If keys are set the grouping hash is built by calling +object.send(key).to_s+
|
19
|
+
# and concatening for all keys. (e.g. if +Object+ supports the method +date+ the
|
20
|
+
# grouping by +date+ is calculated by calling +regroup!(:date)+
|
21
|
+
#
|
22
|
+
# If a block is passed, it is called for every entry and it's result is taken as
|
23
|
+
# grouping hash. (e.g. +regroup! { |entry| entry.date }+ for grouping by date
|
24
|
+
#
|
25
|
+
# Objects with identical grouping hash are collapsed by calling the '+' operator
|
26
|
+
# on them.
|
27
|
+
#
|
28
|
+
# ATTENTION:
|
29
|
+
# This method won't recognize senseless grouping (e.g. calling
|
30
|
+
# +regroup! { |entry| entry.date.month }+ followed by
|
31
|
+
# +regroup! { |entry| entry.date }+ )
|
32
|
+
def regroup(*keys, &block)
|
33
|
+
# handle block
|
34
|
+
return regroup_by_proc(block) if block_given?
|
35
|
+
|
36
|
+
# handle keys
|
37
|
+
block = Proc.new do |entry|
|
38
|
+
keys.inject([]) { |memo, column| memo.push entry.send(column.to_sym).to_s }.join('-')
|
39
|
+
end
|
40
|
+
regroup_by_proc(block)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Collapse the collection to a single entry
|
44
|
+
def collapse
|
45
|
+
regroup.first
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
# Helper method for regroup with groups the data using a proc as grouping hash generator
|
50
|
+
def regroup_by_proc(grouping_proc)
|
51
|
+
data = self.group_by { |entry| grouping_proc.call(entry) }.values
|
52
|
+
result = data.collect do |entries|
|
53
|
+
# Uses the class of the first element to build the composite element
|
54
|
+
entries.first.class.composite(entries)
|
55
|
+
end
|
56
|
+
self.class.new(result)
|
57
|
+
end
|
58
|
+
end
|