active_reporter 0.5.8
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/MIT-LICENSE +14 -0
- data/README.md +436 -0
- data/Rakefile +23 -0
- data/lib/active_reporter.rb +26 -0
- data/lib/active_reporter/aggregator.rb +9 -0
- data/lib/active_reporter/aggregator/array.rb +14 -0
- data/lib/active_reporter/aggregator/average.rb +9 -0
- data/lib/active_reporter/aggregator/base.rb +73 -0
- data/lib/active_reporter/aggregator/count.rb +23 -0
- data/lib/active_reporter/aggregator/count_if.rb +23 -0
- data/lib/active_reporter/aggregator/max.rb +9 -0
- data/lib/active_reporter/aggregator/min.rb +9 -0
- data/lib/active_reporter/aggregator/ratio.rb +23 -0
- data/lib/active_reporter/aggregator/sum.rb +13 -0
- data/lib/active_reporter/calculator.rb +2 -0
- data/lib/active_reporter/calculator/base.rb +19 -0
- data/lib/active_reporter/calculator/ratio.rb +9 -0
- data/lib/active_reporter/dimension.rb +8 -0
- data/lib/active_reporter/dimension/base.rb +150 -0
- data/lib/active_reporter/dimension/bin.rb +123 -0
- data/lib/active_reporter/dimension/bin/set.rb +162 -0
- data/lib/active_reporter/dimension/bin/table.rb +43 -0
- data/lib/active_reporter/dimension/category.rb +29 -0
- data/lib/active_reporter/dimension/enum.rb +32 -0
- data/lib/active_reporter/dimension/number.rb +51 -0
- data/lib/active_reporter/dimension/time.rb +93 -0
- data/lib/active_reporter/evaluator.rb +2 -0
- data/lib/active_reporter/evaluator/base.rb +17 -0
- data/lib/active_reporter/evaluator/block.rb +15 -0
- data/lib/active_reporter/inflector.rb +8 -0
- data/lib/active_reporter/invalid_params_error.rb +4 -0
- data/lib/active_reporter/report.rb +102 -0
- data/lib/active_reporter/report/aggregation.rb +297 -0
- data/lib/active_reporter/report/definition.rb +195 -0
- data/lib/active_reporter/report/metrics.rb +75 -0
- data/lib/active_reporter/report/validation.rb +106 -0
- data/lib/active_reporter/serializer.rb +7 -0
- data/lib/active_reporter/serializer/base.rb +103 -0
- data/lib/active_reporter/serializer/csv.rb +22 -0
- data/lib/active_reporter/serializer/form_field.rb +134 -0
- data/lib/active_reporter/serializer/hash_table.rb +12 -0
- data/lib/active_reporter/serializer/highcharts.rb +200 -0
- data/lib/active_reporter/serializer/nested_hash.rb +11 -0
- data/lib/active_reporter/serializer/table.rb +21 -0
- data/lib/active_reporter/tracker.rb +2 -0
- data/lib/active_reporter/tracker/base.rb +15 -0
- data/lib/active_reporter/tracker/delta.rb +9 -0
- data/lib/active_reporter/version.rb +3 -0
- data/lib/tasks/active_reporter_tasks.rake +4 -0
- data/spec/acceptance/data_spec.rb +381 -0
- data/spec/active_reporter/aggregator_spec.rb +102 -0
- data/spec/active_reporter/dimension/base_spec.rb +102 -0
- data/spec/active_reporter/dimension/bin/set_spec.rb +83 -0
- data/spec/active_reporter/dimension/bin/table_spec.rb +47 -0
- data/spec/active_reporter/dimension/bin_spec.rb +77 -0
- data/spec/active_reporter/dimension/category_spec.rb +60 -0
- data/spec/active_reporter/dimension/enum_spec.rb +94 -0
- data/spec/active_reporter/dimension/number_spec.rb +71 -0
- data/spec/active_reporter/dimension/time_spec.rb +61 -0
- data/spec/active_reporter/report_spec.rb +597 -0
- data/spec/active_reporter/serializer/hash_table_spec.rb +45 -0
- data/spec/active_reporter/serializer/highcharts_spec.rb +113 -0
- data/spec/active_reporter/serializer/table_spec.rb +62 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +26 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/site_controller.rb +11 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/author.rb +4 -0
- data/spec/dummy/app/models/comment.rb +4 -0
- data/spec/dummy/app/models/data_builder.rb +112 -0
- data/spec/dummy/app/models/post.rb +6 -0
- data/spec/dummy/app/models/post_report.rb +14 -0
- data/spec/dummy/app/views/layouts/application.html.erb +17 -0
- data/spec/dummy/app/views/site/report.html.erb +73 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +22 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +57 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/migrate/20150714202319_add_dummy_models.rb +25 -0
- data/spec/dummy/db/schema.rb +43 -0
- data/spec/dummy/db/seeds.rb +1 -0
- data/spec/dummy/log/test.log +37033 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/factories.rb +29 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/float.rb +8 -0
- metadata +385 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveReporter
|
2
|
+
module Evaluator
|
3
|
+
class Base
|
4
|
+
attr_reader :name, :report, :opts
|
5
|
+
|
6
|
+
def initialize(name, report, opts={})
|
7
|
+
@name = name
|
8
|
+
@report = report
|
9
|
+
@opts = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_value
|
13
|
+
opts.fetch(:default_value, nil)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
ActiveSupport::Inflector.inflections(:_gem_active_reporter) do |inflect|
|
2
|
+
sys_inflect = ActiveSupport::Inflector.inflections
|
3
|
+
%i(acronyms humans uncountables singulars plurals acronyms_camelize_regex acronyms_underscore_regex).each do |var|
|
4
|
+
inflect.instance_variable_set("@#{var}", sys_inflect.send(var).dup)
|
5
|
+
end
|
6
|
+
|
7
|
+
inflect.uncountable 'delta'
|
8
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
Dir.glob(File.join(__dir__, 'report', '*.rb')).each { |file| require file }
|
2
|
+
|
3
|
+
module ActiveReporter
|
4
|
+
class Report
|
5
|
+
include ActiveReporter::Report::Definition
|
6
|
+
include ActiveReporter::Report::Validation
|
7
|
+
include ActiveReporter::Report::Metrics
|
8
|
+
include ActiveReporter::Report::Aggregation
|
9
|
+
|
10
|
+
attr_reader :params, :parent_report, :parent_groupers, :supplements
|
11
|
+
|
12
|
+
def initialize(params = {})
|
13
|
+
@params = params
|
14
|
+
|
15
|
+
# prepare the params for processing
|
16
|
+
clean_params
|
17
|
+
|
18
|
+
# When using a Calculator you may need the parent report data. Pass in a ActiveReporter::Report object when
|
19
|
+
# instantiating a new ActiveReporter::Report instance as :parent_report. This will allow you to calculate a data
|
20
|
+
# based on the #total_report of this passed :parent_report. For example, if the parent report includes a sum
|
21
|
+
# aggregated 'views' column, the child report can use Report::Calculator::Ratio to caluclate the ratio of 'views'
|
22
|
+
# on a given row versus the total 'views' from the parent report.
|
23
|
+
@parent_report = @params.delete(:parent_report)
|
24
|
+
@parent_groupers = @params.delete(:parent_groupers) || ( grouper_names & Array(parent_report&.grouper_names) )
|
25
|
+
|
26
|
+
# Supplements -> supplemental reports and data
|
27
|
+
#
|
28
|
+
# we need 2 items:
|
29
|
+
# 1- the #supplements, a hash of reports and data, we can refrence by name
|
30
|
+
# => this is passed into the report initializer, the key is the name the value is the enrire report object
|
31
|
+
# 2- a configuration class, this will allow you to specify a special aggregator in the report class that
|
32
|
+
# => take a block. The block defines { |key, row| return_value }, the block has access to the data in
|
33
|
+
# #supplements available to use when calculating return the value.
|
34
|
+
@supplements = @params.delete(:supplements)
|
35
|
+
|
36
|
+
# You may pass in pre-compiled :row_data if you don't want ActiveReporter to compile this data for you. All
|
37
|
+
# :calculators and :trackers will still be processed when :raw_data is passed in.
|
38
|
+
@raw_data = @params.delete(:raw_data)
|
39
|
+
|
40
|
+
# You may pass in pre-aggregated :total_report object as an instance of ActiveReporter::Report if you don't want
|
41
|
+
# ActiveReporter to total this data for you no additional processing is completed on #total_report when a
|
42
|
+
# :total_report value is passed.
|
43
|
+
@total_report = @params.delete(:total_report)
|
44
|
+
|
45
|
+
# Instead or in addition to passing a :total_report you may pass :total_data, which is used when report data is
|
46
|
+
# built. In the case that both :total_report and :total_data are passed, the :total_report object will be used
|
47
|
+
# for all :calculators. If only :total_data is passed, the :total_report object will not be populated and no
|
48
|
+
# :calculators will be processed. Data in :total_data is never altered or appended.
|
49
|
+
@total_data = @params.delete(:total_data) || @total_report&.data
|
50
|
+
|
51
|
+
validate_params!
|
52
|
+
|
53
|
+
# After params are parsed and validated you can call #data (or any derivitive of: #raw_data, #flat_data,
|
54
|
+
# #hashed_data, #nested_data, etc.) on the ActiveReporter::Report object to #aggregate the data. This will
|
55
|
+
# aggregate all the raw data by the configured dimensions, process any calculators, and then process any
|
56
|
+
# trackers.
|
57
|
+
|
58
|
+
# Caclulators calculate values using the current row data and the #parent_report.
|
59
|
+
|
60
|
+
# Trackers calculate values using the current row data and prior row data.
|
61
|
+
|
62
|
+
|
63
|
+
# If pre-compiled raw data was passed in, process all :calculators and :trackers now.
|
64
|
+
aggregate if @raw_data.present? && ( @params.include?(:calculators) || @params.include?(:trackers) )
|
65
|
+
total if @total_data.present?
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def clean_params
|
71
|
+
@params = @params.deep_symbolize_keys.deep_dup.compact
|
72
|
+
strip_blank_params unless @params[:strip_blanks] == false
|
73
|
+
compact_params
|
74
|
+
end
|
75
|
+
|
76
|
+
def strip_blank_params(check_params = @params)
|
77
|
+
check_params.delete_if do |_, value|
|
78
|
+
case value
|
79
|
+
when Hash then strip_blank_params(value)
|
80
|
+
when Array then value.reject! { |v| v.try(:blank?) }
|
81
|
+
else value
|
82
|
+
end.try(:blank?) unless value.is_a?(ActiveRecord::Relation)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def compact_params
|
87
|
+
# exclude raw report data in compact
|
88
|
+
include_raw_data = @params.include?(:raw_data)
|
89
|
+
raw_data = @params.delete(:raw_data) if include_raw_data
|
90
|
+
include_total_data = @params.include?(:total_data)
|
91
|
+
total_data = @params.delete(:total_data) if include_total_data
|
92
|
+
|
93
|
+
DeeplyEnumerable::Hash.deep_compact(@params)
|
94
|
+
|
95
|
+
# add raw report data back into params
|
96
|
+
@params[:raw_data] = raw_data if include_raw_data
|
97
|
+
@params[:total_data] = total_data if include_total_data
|
98
|
+
|
99
|
+
@params
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
module ActiveReporter
|
2
|
+
class Report
|
3
|
+
module Aggregation
|
4
|
+
def raw_data
|
5
|
+
@raw_data ||= aggregate
|
6
|
+
end
|
7
|
+
|
8
|
+
# flat hash of
|
9
|
+
# { [x1, x2, x3] => y }
|
10
|
+
def flat_data
|
11
|
+
@flat_data ||= flatten_data
|
12
|
+
end
|
13
|
+
|
14
|
+
def hashed_data
|
15
|
+
@hashed_data ||= hash_data
|
16
|
+
end
|
17
|
+
|
18
|
+
# nested array of
|
19
|
+
# [{ key: x3, values: [{ key: x2, values: [{ key: x1, value: y }] }] }]
|
20
|
+
def nested_data
|
21
|
+
@nested_data ||= nest_data
|
22
|
+
end
|
23
|
+
alias_method :data, :nested_data
|
24
|
+
|
25
|
+
def total_data
|
26
|
+
@total_data ||= total
|
27
|
+
end
|
28
|
+
alias_method :totals, :total_data
|
29
|
+
|
30
|
+
def source_data
|
31
|
+
@source_data ||= aggregators.values.reduce(groups) do |relation, aggregator|
|
32
|
+
# append each aggregator into the base relation (groups)
|
33
|
+
relation.merge(aggregator.aggregate(base_relation))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def aggregate
|
40
|
+
tracker_dimension_key = :_tracker_dimension
|
41
|
+
|
42
|
+
if trackable? && trackers.any?
|
43
|
+
prior_obj = prior_bin_report.source_data.first
|
44
|
+
prior_row = prior_bin_report.hashed_data.first.with_indifferent_access
|
45
|
+
|
46
|
+
results_key_prefix = groupers.map { |g| g.extract_sql_value(prior_obj) }
|
47
|
+
prior_row[tracker_dimension_key] = results_key_prefix[0..-2]
|
48
|
+
else
|
49
|
+
prior_obj = nil
|
50
|
+
prior_row = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
source_data.each_with_object({}) do |current_obj, results|
|
54
|
+
# collect all group values and append to results
|
55
|
+
# for the results we store and use as the key prefix for each value
|
56
|
+
results_key_prefix = groupers.map { |g| g.extract_sql_value(current_obj) }.freeze
|
57
|
+
# for the current_row appended as individual keys and values to the data object
|
58
|
+
current_row = groupers.collect(&:name).zip(results_key_prefix).to_h.with_indifferent_access
|
59
|
+
|
60
|
+
# collect all aggregator fields into the results from each current_obj in the base relation
|
61
|
+
aggregators.each do |name, aggregator|
|
62
|
+
aggregated_value = current_obj.attributes[aggregator.sql_value_name] || aggregator.default_value
|
63
|
+
results[results_key_prefix + [name.to_s]] = aggregated_value
|
64
|
+
current_row[name.to_s] = aggregated_value
|
65
|
+
end
|
66
|
+
|
67
|
+
# append all calculator fields
|
68
|
+
if calculable?
|
69
|
+
calculators.each do |name, calculator|
|
70
|
+
calc_report = calculator.totals? ? parent_report.total_report : parent_report
|
71
|
+
|
72
|
+
parent_row = match_parent_row_for_calculator(current_row, calc_report, calculator)
|
73
|
+
next if parent_row.nil?
|
74
|
+
|
75
|
+
calculated_value = calculator.calculate(current_row, parent_row) || calculator.default_value
|
76
|
+
results[results_key_prefix + [name.to_s]] = calculated_value
|
77
|
+
current_row[name.to_s] = calculated_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# append all tracker fields
|
82
|
+
# Trackers can only be applied if the last grouper is a bin dimension, since bin dimensions are series of the
|
83
|
+
# same data set with a pre-defined sequence. Bin dimension results also allow us to determine if an empty set
|
84
|
+
# is present, because the bins are pre-defined.
|
85
|
+
# If additional demensions are included the trackers reset each time these groups change. For example, if the
|
86
|
+
# category dimension "author.id" and time dimension "created_at" with bin_width "day" are used, each time the
|
87
|
+
# "author.id" value (bin) changes the tracker is reset so we do not track changes from the last day of each
|
88
|
+
# "author.id" to the first day of the next "author.id".
|
89
|
+
if trackable?
|
90
|
+
current_row[tracker_dimension_key] = results_key_prefix[0..-2]
|
91
|
+
|
92
|
+
if current_row[tracker_dimension_key] == prior_row[tracker_dimension_key] && bins_are_adjacent?(current_obj, prior_obj)
|
93
|
+
trackers.each do |name, tracker|
|
94
|
+
calculated_value = tracker.track(current_row, prior_row) || tracker.default_value
|
95
|
+
results[results_key_prefix + [name.to_s]] = calculated_value
|
96
|
+
current_row[name.to_s] = calculated_value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
if evaluatable?
|
102
|
+
evaluators.each do |name, evaluator|
|
103
|
+
results_key = results_key_prefix + [name.to_s]
|
104
|
+
calculated_value = evaluator.evaluate(results_key, current_row, self) || evaluator.default_value
|
105
|
+
results[results_key] = calculated_value
|
106
|
+
current_row[name.to_s] = calculated_value
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
prior_obj, prior_row = current_obj, current_row
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def flatten_data
|
115
|
+
group_values.each_with_object({}) do |group, results|
|
116
|
+
aggregators.map do |name, aggregator|
|
117
|
+
aggregator_group = group + [name.to_s]
|
118
|
+
results[aggregator_group] = (raw_data[aggregator_group] || aggregator.default_value)
|
119
|
+
end
|
120
|
+
|
121
|
+
calculators.each do |name, calculator|
|
122
|
+
calculator_group = group + [name.to_s]
|
123
|
+
results[calculator_group] = calculable? ? (raw_data[calculator_group] || calculator.default_value) : nil
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
trackers.each do |name, tracker|
|
128
|
+
tracker_group = group + [name.to_s]
|
129
|
+
results[tracker_group] = trackable? ? (raw_data[tracker_group] || tracker.default_value) : nil
|
130
|
+
end
|
131
|
+
|
132
|
+
evaluators.each do |name, evaluator|
|
133
|
+
evaluator_group = group + [name.to_s]
|
134
|
+
results[evaluator_group] = evaluatable? ? (raw_data[evaluator_group] || evaluator.default_value) : nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def hash_data
|
140
|
+
group_values.collect do |group|
|
141
|
+
grouper_names.zip(group).to_h.tap do |row|
|
142
|
+
aggregators.each do |name, aggregator|
|
143
|
+
row[name] = (raw_data[group + [name.to_s]] || aggregator.default_value)
|
144
|
+
end
|
145
|
+
|
146
|
+
calculators.each do |name, calculator|
|
147
|
+
row[name] = calculable? ? (raw_data[group + [name.to_s]] || calculator.default_value) : nil
|
148
|
+
end
|
149
|
+
|
150
|
+
trackers.each do |name, tracker|
|
151
|
+
row[name] = trackable? ? (raw_data[group + [name.to_s]] || tracker.default_value) : nil
|
152
|
+
end
|
153
|
+
|
154
|
+
evaluators.each do |name, evaluator|
|
155
|
+
row[name] = evaluatable? ? (raw_data[group + [name.to_s]] || evaluator.default_value) : nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def nest_data(groupers = self.groupers, prefix = [])
|
162
|
+
nest_groupers = groupers.dup
|
163
|
+
group = nest_groupers.pop
|
164
|
+
|
165
|
+
group.group_values.map do |group_value|
|
166
|
+
value_prefix = [group_value] + prefix
|
167
|
+
values = []
|
168
|
+
|
169
|
+
if nest_groupers.any?
|
170
|
+
values = nest_data(nest_groupers, value_prefix)
|
171
|
+
else
|
172
|
+
aggregators.each do |name, aggregator|
|
173
|
+
value = raw_data[value_prefix+[name.to_s]] || aggregator.default_value
|
174
|
+
values.push({ key: name.to_s, value: value })
|
175
|
+
end
|
176
|
+
|
177
|
+
calculators.each do |name, calculator|
|
178
|
+
value = calculable? ? (raw_data[value_prefix+[name.to_s]] || calculator.default_value) : nil
|
179
|
+
values.push({ key: name.to_s, value: value })
|
180
|
+
end
|
181
|
+
|
182
|
+
trackers.each do |name, tracker|
|
183
|
+
value = trackable? ? (raw_data[value_prefix+[name.to_s]] || tracker.default_value) : nil
|
184
|
+
values.push({ key: name.to_s, value: value })
|
185
|
+
end
|
186
|
+
|
187
|
+
evaluators.each do |name, evaluator|
|
188
|
+
value = evaluatable? ? (raw_data[value_prefix+[name.to_s]] || evaluator.default_value) : nil
|
189
|
+
values.push({ key: name.to_s, value: value })
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
{ key: group_value, values: values }
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def total
|
198
|
+
results = @total_data || total_report.raw_data
|
199
|
+
|
200
|
+
results.merge!(results.collect do |row, value|
|
201
|
+
calculators.collect do |name, calculator|
|
202
|
+
row_data = hash_raw_row(row, value, ['totals'])
|
203
|
+
calc_report = parent_report.total_report
|
204
|
+
|
205
|
+
parent_row = match_parent_row_for_calculator(row_data, calc_report, calculator)
|
206
|
+
[['totals', name.to_s], calculator.calculate(row_data, parent_row)] unless parent_row.nil?
|
207
|
+
end
|
208
|
+
end.flatten(1).to_h) unless parent_report.nil?
|
209
|
+
|
210
|
+
results
|
211
|
+
end
|
212
|
+
|
213
|
+
def group_values
|
214
|
+
@group_values ||= all_combinations_of(groupers.map(&:group_values))
|
215
|
+
end
|
216
|
+
|
217
|
+
def all_combinations_of(values)
|
218
|
+
values[0].product(*values[1..-1])
|
219
|
+
end
|
220
|
+
|
221
|
+
def hash_raw_row(row, value, grouper_names)
|
222
|
+
grouper_names.dup.push(:dimension, :value).zip(row.dup.push(value)).to_h.tap do |row_hash|
|
223
|
+
row_hash[row_hash.delete(:dimension)] = row_hash.delete(:value)
|
224
|
+
row_hash.symbolize_keys!
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def match_parent_row_for_calculator(row_data, parent_report, calculator)
|
229
|
+
parent_report.hashed_data.detect { |parent_row_data| parent_groupers.all? { |g| row_data[g] == parent_row_data[g] } }
|
230
|
+
end
|
231
|
+
|
232
|
+
def bins_are_adjacent?(obj_a, obj_b, dimension = tracker_dimension)
|
233
|
+
return false if obj_a.nil? || obj_b.nil?
|
234
|
+
|
235
|
+
# Categories are not sequential, even if they appear to be. Instead, a category is a group by on a specific
|
236
|
+
# field with identical values. If the field type is integer we can deduce the bin width to be 1, but if the
|
237
|
+
# type is string or float the the width is less evident.
|
238
|
+
# For example, if the field is float and the first value is 1.0 should the next sequential value be 1.1? What
|
239
|
+
# if we have 1.0001? Should we skip 1.0002 if it does not exist and skip right to 1.01? What if we habe 1.0,
|
240
|
+
# 1.1, 1.11, and 1.13 but no 1.12? So we determine that 1.13 is sequentially after 1.11 or de we reset the
|
241
|
+
# tracker? Even if there is a "correct" method for one report it may not be correct for a different report. The
|
242
|
+
# same problem applies to strings. Which character is after "z"? The ASCII hex value is "{", which would work
|
243
|
+
# fine for ordering, but maybe not for determining when a tracker should be reset. Additionally, we need to
|
244
|
+
# deal with strings of different lengths. Alphabetically you could order 'A', 'AA', 'AAA', 'B' but how do know
|
245
|
+
# when to reset the tracker? If we get a new value of 'AAAA' we have entirelly new values used to calculate the
|
246
|
+
# tracker value for the 'B' row, effectivally making the tracker values irrelevent.
|
247
|
+
# Even going back to the integer example, the value allowed to be stored increments by 1, but there is no
|
248
|
+
# guerentee that these are the actual values being used in the field.
|
249
|
+
# For these reasons we will not attempt to track any dimension that does not specifically specify a bin width.
|
250
|
+
|
251
|
+
# Any class that inherits from Bin will be evaluated, this includes both Number and Time classes, all other
|
252
|
+
# classes will be skipped.
|
253
|
+
return false unless dimension.is_a?(ActiveReporter::Dimension::Bin)
|
254
|
+
|
255
|
+
bin_a = dimension.extract_sql_value(obj_a)
|
256
|
+
bin_b = dimension.extract_sql_value(obj_b)
|
257
|
+
|
258
|
+
# Do not find identical dimensions adjacent
|
259
|
+
return false if bin_a.min == bin_b.min && bin_b.max == bin_a.max
|
260
|
+
|
261
|
+
# Do not find two undefined dimensions adjacent
|
262
|
+
return false if [bin_a.min, bin_a.max, bin_b.min, bin_b.max].compact.none?
|
263
|
+
|
264
|
+
# Check if either dimension's min matches the other's max
|
265
|
+
bin_a.min == bin_b.max || bin_b.min == bin_a.max
|
266
|
+
end
|
267
|
+
|
268
|
+
def calculable?
|
269
|
+
@calculable ||= parent_report.present?
|
270
|
+
end
|
271
|
+
|
272
|
+
def trackable?
|
273
|
+
@trackable ||= tracker_dimension.is_a?(ActiveReporter::Dimension::Bin) && tracker_dimension.min.present?
|
274
|
+
end
|
275
|
+
|
276
|
+
def evaluatable?
|
277
|
+
@evaluatable ||= true
|
278
|
+
end
|
279
|
+
|
280
|
+
def tracker_dimension
|
281
|
+
@tracker_dimension ||= groupers.last
|
282
|
+
end
|
283
|
+
|
284
|
+
def prior_bin_report
|
285
|
+
@prior_bin_report ||= if trackable? && trackers.any?
|
286
|
+
first_bin_min = tracker_dimension.group_values.first.min
|
287
|
+
prior_bin_params = {
|
288
|
+
dimensions: { tracker_dimension.name => { only: { min: (first_bin_min - tracker_dimension.bin_width), max: first_bin_min }}},
|
289
|
+
trackers: nil
|
290
|
+
}
|
291
|
+
tracker_report_params = params.deep_merge(prior_bin_params)
|
292
|
+
self.class.new(params.merge(tracker_report_params))
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|