active_reporter 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
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