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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +14 -0
  3. data/README.md +436 -0
  4. data/Rakefile +23 -0
  5. data/lib/active_reporter.rb +26 -0
  6. data/lib/active_reporter/aggregator.rb +9 -0
  7. data/lib/active_reporter/aggregator/array.rb +14 -0
  8. data/lib/active_reporter/aggregator/average.rb +9 -0
  9. data/lib/active_reporter/aggregator/base.rb +73 -0
  10. data/lib/active_reporter/aggregator/count.rb +23 -0
  11. data/lib/active_reporter/aggregator/count_if.rb +23 -0
  12. data/lib/active_reporter/aggregator/max.rb +9 -0
  13. data/lib/active_reporter/aggregator/min.rb +9 -0
  14. data/lib/active_reporter/aggregator/ratio.rb +23 -0
  15. data/lib/active_reporter/aggregator/sum.rb +13 -0
  16. data/lib/active_reporter/calculator.rb +2 -0
  17. data/lib/active_reporter/calculator/base.rb +19 -0
  18. data/lib/active_reporter/calculator/ratio.rb +9 -0
  19. data/lib/active_reporter/dimension.rb +8 -0
  20. data/lib/active_reporter/dimension/base.rb +150 -0
  21. data/lib/active_reporter/dimension/bin.rb +123 -0
  22. data/lib/active_reporter/dimension/bin/set.rb +162 -0
  23. data/lib/active_reporter/dimension/bin/table.rb +43 -0
  24. data/lib/active_reporter/dimension/category.rb +29 -0
  25. data/lib/active_reporter/dimension/enum.rb +32 -0
  26. data/lib/active_reporter/dimension/number.rb +51 -0
  27. data/lib/active_reporter/dimension/time.rb +93 -0
  28. data/lib/active_reporter/evaluator.rb +2 -0
  29. data/lib/active_reporter/evaluator/base.rb +17 -0
  30. data/lib/active_reporter/evaluator/block.rb +15 -0
  31. data/lib/active_reporter/inflector.rb +8 -0
  32. data/lib/active_reporter/invalid_params_error.rb +4 -0
  33. data/lib/active_reporter/report.rb +102 -0
  34. data/lib/active_reporter/report/aggregation.rb +297 -0
  35. data/lib/active_reporter/report/definition.rb +195 -0
  36. data/lib/active_reporter/report/metrics.rb +75 -0
  37. data/lib/active_reporter/report/validation.rb +106 -0
  38. data/lib/active_reporter/serializer.rb +7 -0
  39. data/lib/active_reporter/serializer/base.rb +103 -0
  40. data/lib/active_reporter/serializer/csv.rb +22 -0
  41. data/lib/active_reporter/serializer/form_field.rb +134 -0
  42. data/lib/active_reporter/serializer/hash_table.rb +12 -0
  43. data/lib/active_reporter/serializer/highcharts.rb +200 -0
  44. data/lib/active_reporter/serializer/nested_hash.rb +11 -0
  45. data/lib/active_reporter/serializer/table.rb +21 -0
  46. data/lib/active_reporter/tracker.rb +2 -0
  47. data/lib/active_reporter/tracker/base.rb +15 -0
  48. data/lib/active_reporter/tracker/delta.rb +9 -0
  49. data/lib/active_reporter/version.rb +3 -0
  50. data/lib/tasks/active_reporter_tasks.rake +4 -0
  51. data/spec/acceptance/data_spec.rb +381 -0
  52. data/spec/active_reporter/aggregator_spec.rb +102 -0
  53. data/spec/active_reporter/dimension/base_spec.rb +102 -0
  54. data/spec/active_reporter/dimension/bin/set_spec.rb +83 -0
  55. data/spec/active_reporter/dimension/bin/table_spec.rb +47 -0
  56. data/spec/active_reporter/dimension/bin_spec.rb +77 -0
  57. data/spec/active_reporter/dimension/category_spec.rb +60 -0
  58. data/spec/active_reporter/dimension/enum_spec.rb +94 -0
  59. data/spec/active_reporter/dimension/number_spec.rb +71 -0
  60. data/spec/active_reporter/dimension/time_spec.rb +61 -0
  61. data/spec/active_reporter/report_spec.rb +597 -0
  62. data/spec/active_reporter/serializer/hash_table_spec.rb +45 -0
  63. data/spec/active_reporter/serializer/highcharts_spec.rb +113 -0
  64. data/spec/active_reporter/serializer/table_spec.rb +62 -0
  65. data/spec/dummy/README.rdoc +28 -0
  66. data/spec/dummy/Rakefile +6 -0
  67. data/spec/dummy/app/assets/config/manifest.js +0 -0
  68. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  69. data/spec/dummy/app/assets/stylesheets/application.css +26 -0
  70. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  71. data/spec/dummy/app/controllers/site_controller.rb +11 -0
  72. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  73. data/spec/dummy/app/models/author.rb +4 -0
  74. data/spec/dummy/app/models/comment.rb +4 -0
  75. data/spec/dummy/app/models/data_builder.rb +112 -0
  76. data/spec/dummy/app/models/post.rb +6 -0
  77. data/spec/dummy/app/models/post_report.rb +14 -0
  78. data/spec/dummy/app/views/layouts/application.html.erb +17 -0
  79. data/spec/dummy/app/views/site/report.html.erb +73 -0
  80. data/spec/dummy/bin/bundle +3 -0
  81. data/spec/dummy/bin/rails +4 -0
  82. data/spec/dummy/bin/rake +4 -0
  83. data/spec/dummy/bin/setup +29 -0
  84. data/spec/dummy/config.ru +4 -0
  85. data/spec/dummy/config/application.rb +26 -0
  86. data/spec/dummy/config/boot.rb +5 -0
  87. data/spec/dummy/config/database.yml +22 -0
  88. data/spec/dummy/config/environment.rb +5 -0
  89. data/spec/dummy/config/environments/development.rb +41 -0
  90. data/spec/dummy/config/environments/production.rb +79 -0
  91. data/spec/dummy/config/environments/test.rb +42 -0
  92. data/spec/dummy/config/initializers/assets.rb +11 -0
  93. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  94. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/session_store.rb +3 -0
  99. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  100. data/spec/dummy/config/locales/en.yml +23 -0
  101. data/spec/dummy/config/routes.rb +57 -0
  102. data/spec/dummy/config/secrets.yml +22 -0
  103. data/spec/dummy/db/migrate/20150714202319_add_dummy_models.rb +25 -0
  104. data/spec/dummy/db/schema.rb +43 -0
  105. data/spec/dummy/db/seeds.rb +1 -0
  106. data/spec/dummy/log/test.log +37033 -0
  107. data/spec/dummy/public/404.html +67 -0
  108. data/spec/dummy/public/422.html +67 -0
  109. data/spec/dummy/public/500.html +66 -0
  110. data/spec/dummy/public/favicon.ico +0 -0
  111. data/spec/factories/factories.rb +29 -0
  112. data/spec/spec_helper.rb +40 -0
  113. data/spec/support/float.rb +8 -0
  114. metadata +385 -0
@@ -0,0 +1,2 @@
1
+ require 'active_reporter/evaluator/base'
2
+ require 'active_reporter/evaluator/block'
@@ -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,15 @@
1
+ module ActiveReporter
2
+ module Evaluator
3
+ class Block < ActiveReporter::Evaluator::Base
4
+ def evaluate(*args)
5
+ block.call(*args)
6
+ end
7
+
8
+ private
9
+
10
+ def block
11
+ opts.fetch(:block)
12
+ end
13
+ end
14
+ end
15
+ 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,4 @@
1
+ module ActiveReporter
2
+ class InvalidParamsError < ArgumentError
3
+ end
4
+ 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