martyr 0.1.74.pre

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 (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