martyr 0.1.74.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.tags +868 -0
  7. data/.travis.yml +3 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +265 -0
  11. data/Rakefile +1 -0
  12. data/TODO.txt +54 -0
  13. data/bin/console +62 -0
  14. data/bin/setup +7 -0
  15. data/lib/martyr/base_cube.rb +73 -0
  16. data/lib/martyr/cube.rb +134 -0
  17. data/lib/martyr/dimension_reference.rb +26 -0
  18. data/lib/martyr/errors.rb +20 -0
  19. data/lib/martyr/helpers/delegators.rb +17 -0
  20. data/lib/martyr/helpers/intervals.rb +222 -0
  21. data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
  22. data/lib/martyr/helpers/registrable.rb +15 -0
  23. data/lib/martyr/helpers/sorter.rb +79 -0
  24. data/lib/martyr/helpers/translations.rb +34 -0
  25. data/lib/martyr/level_concern/has_level_collection.rb +11 -0
  26. data/lib/martyr/level_concern/level.rb +45 -0
  27. data/lib/martyr/level_concern/level_collection.rb +60 -0
  28. data/lib/martyr/level_concern/level_comparator.rb +45 -0
  29. data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
  30. data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
  31. data/lib/martyr/runtime/data_set/element.rb +66 -0
  32. data/lib/martyr/runtime/data_set/element_common.rb +51 -0
  33. data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
  34. data/lib/martyr/runtime/data_set/fact.rb +83 -0
  35. data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
  36. data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
  37. data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
  38. data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
  39. data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
  40. data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
  41. data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
  42. data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
  43. data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
  44. data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
  45. data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
  46. data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
  47. data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
  48. data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
  49. data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
  50. data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
  51. data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
  52. data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
  53. data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
  54. data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
  55. data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
  56. data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
  57. data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
  58. data/lib/martyr/runtime/query/query_context.rb +246 -0
  59. data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
  60. data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
  61. data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
  62. data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
  63. data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
  64. data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
  65. data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
  66. data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
  67. data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
  68. data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
  69. data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
  70. data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
  71. data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
  72. data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
  73. data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
  74. data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
  75. data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
  76. data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
  77. data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
  78. data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
  79. data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
  80. data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
  81. data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
  82. data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
  83. data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
  84. data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
  85. data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
  86. data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
  87. data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
  88. data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
  89. data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
  90. data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
  91. data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
  92. data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
  93. data/lib/martyr/schema/metrics/base_metric.rb +77 -0
  94. data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
  95. data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
  96. data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
  97. data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
  98. data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
  99. data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
  100. data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
  101. data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
  102. data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
  103. data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
  104. data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
  105. data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
  106. data/lib/martyr/version.rb +3 -0
  107. data/lib/martyr/virtual_cube.rb +74 -0
  108. data/lib/martyr.rb +55 -0
  109. data/martyr.gemspec +41 -0
  110. metadata +296 -0
@@ -0,0 +1,17 @@
1
+ module Martyr
2
+ module Delegators
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def each_child_delegator(*method_names, to:)
7
+ method_names.each do |method_name|
8
+ define_method(method_name) do |*args|
9
+ send(to).each do |obj|
10
+ obj.send(method_name, *args)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,222 @@
1
+ # Mini and naive library for handling interval overlaps.
2
+ # Used predominantly for compounding metric slices.
3
+ module Martyr
4
+ class IntervalSet
5
+ attr_reader :set
6
+
7
+ def initialize(**options)
8
+ @set = []
9
+ add(**options) if options.present?
10
+ end
11
+
12
+ def null?
13
+ set.empty?
14
+ end
15
+
16
+ def continuous?
17
+ set.length == 1
18
+ end
19
+
20
+ def add(from: -Float::INFINITY, to: Float::INFINITY)
21
+ new_interval = Interval.new(from, to)
22
+ new_interval_set = []
23
+ @set.each do |old_interval|
24
+ if old_interval.touch?(new_interval)
25
+ new_interval = new_interval.union(old_interval)
26
+ else
27
+ new_interval_set << old_interval
28
+ end
29
+ end
30
+ set_interval_set new_interval_set + [new_interval]
31
+ end
32
+
33
+ # @param other [IntervalSet]
34
+ def intersect(other)
35
+ new_interval_set = []
36
+ other.set.each do |other_interval|
37
+ new_interval_set += set.select{|x| x.overlap?(other_interval)}.map{|x| x.intersect(other_interval)}
38
+ end
39
+ set_interval_set new_interval_set
40
+ end
41
+
42
+ def set_interval_set(set)
43
+ @set = set.sort_by{|interval| interval.from.x.to_f}
44
+ self
45
+ end
46
+
47
+ # @return [Array<Numeric>] array of holes - these are open edges that touch each other
48
+ # @note self will be amended to have the holes filled
49
+ def extract_and_fill_holes
50
+ holes = []
51
+ last_edge = nil
52
+ set.each do |interval|
53
+ holes << last_edge if interval.from.open? and interval.from.x == last_edge
54
+ last_edge = interval.to.x if interval.to.open?
55
+ end
56
+
57
+ # Fill in the holes
58
+ holes.each do |hole|
59
+ add from: [hole], to: [hole]
60
+ end
61
+
62
+ holes
63
+ end
64
+
65
+ # @return [Array<>]
66
+ def extract_and_remove_points
67
+ points = @set.select(&:point?).map{|interval| interval.from.x}
68
+ @set.reject!(&:point?)
69
+ points
70
+ end
71
+
72
+ # @return [nil, PointInterval]
73
+ def upper_bound
74
+ return nil if null?
75
+ upper_point = set.last.to
76
+ upper_point.infinity? ? nil : upper_point
77
+ end
78
+
79
+ # @return [PointInterval]
80
+ def lower_bound
81
+ return nil if null?
82
+ upper_point = set.first.from
83
+ upper_point.infinity? ? nil : upper_point
84
+ end
85
+ end
86
+
87
+ # The convention for a basic interval is as follows:
88
+ # Array value - means including that point
89
+ # Integer value - means not including that point
90
+ #
91
+ # Example:
92
+ # Interval.new [5], 6 - legal
93
+ # Interval.new [5], [5] - legal
94
+ # Interval.new [5], 5 - illegal
95
+ class Interval
96
+ attr_reader :from, :to
97
+
98
+ def initialize(from, to)
99
+ @from = PointInterval.new(Array.wrap(from).first, from.is_a?(Array), :right)
100
+ @to = PointInterval.new(Array.wrap(to).first, to.is_a?(Array), :left)
101
+ raise Martyr::Error.new('from cannot be bigger than to') if @from.outside?(@to) or @from.equal_but_empty?(@to)
102
+ end
103
+
104
+ def point?
105
+ from.closed? and to.closed? and from.x == to.x
106
+ end
107
+
108
+ def overlap?(other)
109
+ doesnt_overlap = (to.outside?(other.from) or to.equal_but_empty?(other.from) or from.outside?(other.to) or from.equal_but_empty?(other.to))
110
+ !doesnt_overlap
111
+ end
112
+
113
+ def touch?(other)
114
+ return true if overlap?(other)
115
+ return true if to.equal_and_mergeable?(other.from) or from.equal_and_mergeable?(other.to)
116
+ false
117
+ end
118
+
119
+ def intersect(other)
120
+ return nil unless overlap?(other)
121
+ self.class.new from.max_with(other.from).to_param, to.min_with(other.to).to_param
122
+ end
123
+
124
+ def union(other)
125
+ return nil unless touch?(other)
126
+ self.class.new from.min_with(other.from).to_param, to.max_with(other.to).to_param
127
+ end
128
+ end
129
+
130
+ # This represents a starting point and a direction (:left or :right), together with whether the interval includes
131
+ # or excludes the point.
132
+ class PointInterval
133
+ attr_reader :x, :direction
134
+
135
+ def initialize(x, closed, direction)
136
+ @x = x
137
+ @closed = closed
138
+ @direction = direction.to_sym
139
+ raise 'direction must be either left or right' unless [:left, :right].include?(@direction)
140
+ end
141
+
142
+ def to_param
143
+ closed? ? Array.wrap(x) : x
144
+ end
145
+
146
+ def infinity?
147
+ x == Float::INFINITY or x == -Float::INFINITY
148
+ end
149
+
150
+ def open?
151
+ !@closed
152
+ end
153
+
154
+ def closed?
155
+ !!@closed
156
+ end
157
+
158
+ def right?
159
+ direction == :right
160
+ end
161
+
162
+ def left?
163
+ direction == :left
164
+ end
165
+
166
+ def equal_and_mergeable?(other)
167
+ return false if x != other.x
168
+ return false if open? and other.open?
169
+ true
170
+ end
171
+
172
+ def equal_but_empty?(other)
173
+ return false if x != other.x
174
+ return false if closed? and other.closed?
175
+ true
176
+ end
177
+
178
+ def inside?(other)
179
+ raise 'other is pointing to the same direction' if direction == other.direction
180
+ if other.right? and other.closed?
181
+ x >= other.x
182
+ elsif other.right? and other.open?
183
+ x > other.x
184
+ elsif other.left? and other.closed?
185
+ x <= other.x
186
+ elsif other.left? and other.open?
187
+ x < other.x
188
+ end
189
+ end
190
+
191
+ def outside?(other)
192
+ raise 'other is pointing to the same direction' if direction == other.direction
193
+ if other.right? and other.closed?
194
+ x.to_f < other.x.to_f
195
+ elsif other.right? and other.open?
196
+ x.to_f <= other.x.to_f
197
+ elsif other.left? and other.closed?
198
+ x.to_f > other.x.to_f
199
+ elsif other.left? and other.open?
200
+ x.to_f >= other.x.to_f
201
+ end
202
+ end
203
+
204
+ def max_with(other)
205
+ raise 'other is pointing to a different direction' unless direction == other.direction
206
+ if x == other.x
207
+ return [self, other].select(&:closed?).first || self if left?
208
+ return [self, other].select(&:open?).first || self if right?
209
+ end
210
+ [self, other].sort_by{|p| p.x.to_f}.last
211
+ end
212
+
213
+ def min_with(other)
214
+ raise 'other is pointing to a different direction' unless direction == other.direction
215
+ if x == other.x
216
+ return [self, other].select(&:open?).first || self if left?
217
+ return [self, other].select(&:closed?).first || self if right?
218
+ end
219
+ [self, other].sort_by{|p| p.x.to_f}.first
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,47 @@
1
+ module Martyr
2
+ class MetricIdStandardizer
3
+ include Martyr::Translations
4
+
5
+ def initialize(cube_name = nil, raise_if_not_ok: false)
6
+ @cube_name = cube_name
7
+ @raise_if_not_ok = raise_if_not_ok
8
+ end
9
+
10
+ def standardize(object)
11
+ if object.is_a?(String) or object.is_a?(Symbol)
12
+ standardize_id(object)
13
+ elsif object.is_a?(Array)
14
+ standardize_arr(object)
15
+ elsif object.is_a?(Hash)
16
+ standardize_hash(object)
17
+ else
18
+ raise Internal::Error.new("Does not know how to standardize #{object.inspect}")
19
+ end
20
+ end
21
+
22
+ def standardize_id(id)
23
+ with_standard_id(id) do |dimension_or_cube_or_metric, level_or_metric|
24
+ level_or_metric ? id : add_cube_name_to(dimension_or_cube_or_metric)
25
+ end
26
+ end
27
+
28
+ def standardize_arr(arr)
29
+ arr.map{|id| standardize_id(id)}
30
+ end
31
+
32
+ def standardize_hash(hash)
33
+ arr = hash.map do |key, value|
34
+ [standardize_id(key), value]
35
+ end
36
+ Hash[arr]
37
+ end
38
+
39
+ private
40
+
41
+ def add_cube_name_to(id)
42
+ raise Query::Error.new("Invalid metric #{id}: must be preceded with cube name") if @raise_if_not_ok
43
+ @cube_name.nil? ? id : "#{@cube_name}.#{id}"
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module Martyr
2
+ module Registrable
3
+ def register(object)
4
+ self.[]=(object.name.to_s, object)
5
+ end
6
+
7
+ def find_or_nil(name)
8
+ self.[](name.to_s)
9
+ end
10
+
11
+ def find_or_error(name)
12
+ find_or_nil(name) || raise(Schema::Error.new "#{self.class.name}: Could not find `#{name}`")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ module Martyr
2
+ class Sorter
3
+
4
+ def self.default_for_query(label_field)
5
+ ->(record) { record.try(label_field) }
6
+ end
7
+
8
+ def self.identity
9
+ ->(value) { value }
10
+ end
11
+
12
+ def self.args_to_hash(arg)
13
+ arg.is_a?(Hash) ? arg : Hash[Array.wrap(arg).map{|x| [x, :asc]}]
14
+ end
15
+
16
+ def initialize(args)
17
+ @order_hash = self.class.args_to_hash(args)
18
+ @definition_arr = []
19
+ @order_hash.keys.each do |key|
20
+ @definition_arr << yield(key)
21
+ end
22
+ end
23
+
24
+ def sort(elements)
25
+ return elements unless @definition_arr.present?
26
+ uniq_values = extract_uniq_values(elements)
27
+ lookups = build_sort_order_lookup(uniq_values)
28
+
29
+ elements.sort_by do |element|
30
+ @definition_arr.each_with_index.map{|definition, i| lookups[i][extract_value_from_definition(element, definition)] }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def extract_value_from_definition(element, definition)
37
+ definition.is_a?(Schema::QueryLevelDefinition) ? element.record_for(definition.id) : element.fetch(definition.id)
38
+ end
39
+
40
+ def direction_at(index)
41
+ @order_hash.values[index]
42
+ end
43
+
44
+ def sorter_proc_at(index)
45
+ direction = direction_at(index)
46
+ return direction if direction.respond_to?(:call)
47
+ @definition_arr[index].sort
48
+ end
49
+
50
+ # @param elements [Array<Element>] elements to be sorted
51
+ # @return [Array<Array>] each index of the master array corresponds to one level of nested sorting.
52
+ # E.g., if the user asked to sort by ['genres.name', 'media_types.name'], the result will be an array where
53
+ # index 0 contains all unique values of the genres extracted from the elements, and index 0 contains all
54
+ # unique values of the media types extracted from the elements.
55
+ #
56
+ def extract_uniq_values(elements)
57
+ arr = []
58
+ @definition_arr.each_with_index do |definition, i|
59
+ elements.each do |element|
60
+ value = extract_value_from_definition(element, definition)
61
+ arr[i] ||= {}
62
+ arr[i][value] = true
63
+ end
64
+ end
65
+ arr.map!(&:keys)
66
+ end
67
+
68
+ # @param uniq_values [Array<Hash>] see return value of #extract_uniq_values
69
+ def build_sort_order_lookup(uniq_values)
70
+ lookups = []
71
+ uniq_values.each_with_index do |uniq_values_arr, i|
72
+ sorted_values = uniq_values_arr.sort_by { |x| sorter_proc_at(i).call(x) }
73
+ sorted_values.reverse! if direction_at(i).to_s == 'desc'
74
+ lookups << Hash[sorted_values.each_with_index.map{|value, i| [value, i]}]
75
+ end
76
+ lookups
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ module Martyr
2
+ module Translations
3
+
4
+ def with_standard_id(id)
5
+ x, y = id_components(id)
6
+ y.nil? ? yield(id.to_s) : yield(x, y)
7
+ end
8
+
9
+ def id_components(id)
10
+ id_s = id.to_s
11
+ id_s.split('.')
12
+ end
13
+
14
+ def first_element_from_id(id)
15
+ id.to_s.include?('.') ? id.to_s.split('.').first : id.to_s
16
+ end
17
+
18
+ # @param id [String]
19
+ # @option fallback [Boolean] if true, will return the id if only one element exists in the id
20
+ def second_element_from_id(id, fallback: false)
21
+ if id.to_s.include?('.')
22
+ id.to_s.split('.').last
23
+ else
24
+ fallback ? id.to_s : nil
25
+ end
26
+ end
27
+
28
+ def to_id(object)
29
+ return object if object.is_a?(String)
30
+ object.id
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module Martyr
2
+ module HasLevelCollection
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_reader :levels
7
+ delegate :lowest_level, :level_above, :find_level, :level_names, :level_objects, :has_level?, to: :levels
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module Martyr
2
+ module Level
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_accessor :collection
7
+ end
8
+
9
+ def id
10
+ "#{dimension_name}.#{name}"
11
+ end
12
+
13
+ # Used for reflection
14
+ def level_object?
15
+ true
16
+ end
17
+
18
+ def level_above
19
+ @_level_above ||= collection.level_above(name)
20
+ end
21
+
22
+ def level_below
23
+ @_level_below ||= collection.level_below(name)
24
+ end
25
+
26
+ def level_index
27
+ @_level_index ||= collection.level_index(name)
28
+ end
29
+ alias_method :to_i, :level_index
30
+
31
+ def query_level_below
32
+ @_query_level_below ||= collection.query_level_below(name)
33
+ end
34
+
35
+ def level_and_above
36
+ @_level_and_above ||= collection.level_and_above(name)
37
+ end
38
+
39
+ def level_and_below
40
+ @_level_and_below ||= collection.level_and_below(name)
41
+ end
42
+
43
+ end
44
+ end
45
+
@@ -0,0 +1,60 @@
1
+ module Martyr
2
+ module LevelCollection
3
+ extend ActiveSupport::Concern
4
+
5
+ include ActiveModel::Model
6
+ include Martyr::Registrable
7
+
8
+ included do
9
+ attr_accessor :dimension
10
+ delegate :dimension_definition, to: :dimension
11
+ delegate :name, to: :dimension, prefix: true
12
+ alias_method :name, :dimension_name # Allows using #register for LevelCollection
13
+ alias_method :has_level?, :has_key?
14
+ alias_method :find_level, :find_or_error
15
+ alias_method :level_names, :keys
16
+ alias_method :level_objects, :values
17
+ end
18
+
19
+ # @param level_name [String, Symbol]
20
+ # @return [Integer]
21
+ def level_index(level_name)
22
+ to_a.index { |name, _object| name.to_s == level_name.to_s }
23
+ end
24
+
25
+ # @return [BaseLevelDefinition]
26
+ def level_above(level_name)
27
+ above_index = level_index(level_name) - 1
28
+ return nil if above_index < 0
29
+ values[above_index]
30
+ end
31
+
32
+ def level_below(level_name)
33
+ below_index = level_index(level_name) + 1
34
+ values[below_index]
35
+ end
36
+
37
+ # @param level_name [String, Symbol]
38
+ # @return [Array<Martyr::Level>] the first level of type `query` below the provided level
39
+ def query_level_below(level_name)
40
+ level_objects[level_index(level_name) + 1..-1].find{|level| level.query?}
41
+ end
42
+
43
+ # @param level_name [String, Symbol]
44
+ # @return [Array<Martyr::Level>] the provided level and all the levels above it
45
+ def level_and_above(level_name)
46
+ level_objects[0..level_index(level_name)]
47
+ end
48
+
49
+ # @param level_name [String, Symbol]
50
+ # @return [Array<Martyr::Level>] the provided level and all the levels below it
51
+ def level_and_below(level_name)
52
+ level_objects[level_index(level_name)..-1]
53
+ end
54
+
55
+ def lowest_level
56
+ values.max_by(&:to_i)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,45 @@
1
+ module Martyr
2
+ module LevelComparator
3
+
4
+ # @param level1 [LevelAssociation, LevelDefinition nil]
5
+ # @param level2 [LevelAssociation, LevelDefinition, nil]
6
+ # @return [LevelAssociation, LevelDefinition, nil]
7
+ # - nil if both nil
8
+ # - level1 if level1 is "equal level" or "lower level" than level2, or if level2 is nil
9
+ # - level2 if level2 is "lower level" than level1 or if level1 is nil
10
+ def more_detailed_level(level1, level2)
11
+ return nil unless level1 or level2
12
+ return level1 || level2 unless level1 and level2
13
+ return level1 if level1.to_i >= level2.to_i
14
+ level2
15
+ end
16
+
17
+
18
+ # @param level_definition [BaseLevelDefinition] must be level definition so that #level_and_below performs a full search
19
+ # @param level_associations_arr [Array<LevelAssociation>] of supported levels
20
+ # @option prefer_query [Boolean]
21
+ # @return [BaseLevelDefinition, nil]
22
+ # prefer_query is false:
23
+ # Finds the highest supported level in the cube that is equal or below level_definition.
24
+ # prefer_query is true:
25
+ # Finds the highest supported +query+ level in the cube that is equal or below level_definition.
26
+ # If no such level exists, falls back on all levels.
27
+ #
28
+ def find_common_denominator_level(level_definition, level_associations_arr, prefer_query: false)
29
+ raise Internal::Error.new('level_definition must be level definition object') unless level_definition.is_a?(Schema::BaseLevelDefinition)
30
+ raise Internal::Error.new('level_associations_arr cannot be nil') unless level_associations_arr
31
+ supported_index = level_associations_arr.index_by(&:name)
32
+
33
+ levels_to_consider = level_associations_arr.select(&:query?).map(&:level).presence if prefer_query
34
+ levels_to_consider ||= level_definition.level_and_below
35
+ levels_to_consider.find{|l| supported_index[l.name] }
36
+ end
37
+
38
+ # @param levels [Array<Martyr::Level>]
39
+ # @return [Array<LevelDefinition>] lowest levels from the levels array in each dimension
40
+ def lowest_level_of(levels)
41
+ LevelDefinitionsByDimension.new(levels).lowest_levels
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module Martyr
2
+ class LevelDefinitionsByDimension
3
+
4
+ attr_reader :dimensions
5
+
6
+ # @param levels [Martyr::Level, Array<Martyr::Level>]
7
+ def initialize(levels = nil)
8
+ @dimensions = {}
9
+ Array.wrap(levels).each{|x| add_level(x)}
10
+ end
11
+
12
+ # @param level [Martyr::Level]
13
+ def add_level(level)
14
+ @dimensions[level.dimension_name] ||= {}
15
+ @dimensions[level.dimension_name][level.id] = level
16
+ end
17
+
18
+ def lowest_levels
19
+ dimensions.values.map do |levels_hash|
20
+ levels_hash.values.max_by(&:to_i)
21
+ end
22
+ end
23
+ end
24
+ end