martyr 0.1.74.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.tags +868 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/Rakefile +1 -0
- data/TODO.txt +54 -0
- data/bin/console +62 -0
- data/bin/setup +7 -0
- data/lib/martyr/base_cube.rb +73 -0
- data/lib/martyr/cube.rb +134 -0
- data/lib/martyr/dimension_reference.rb +26 -0
- data/lib/martyr/errors.rb +20 -0
- data/lib/martyr/helpers/delegators.rb +17 -0
- data/lib/martyr/helpers/intervals.rb +222 -0
- data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
- data/lib/martyr/helpers/registrable.rb +15 -0
- data/lib/martyr/helpers/sorter.rb +79 -0
- data/lib/martyr/helpers/translations.rb +34 -0
- data/lib/martyr/level_concern/has_level_collection.rb +11 -0
- data/lib/martyr/level_concern/level.rb +45 -0
- data/lib/martyr/level_concern/level_collection.rb +60 -0
- data/lib/martyr/level_concern/level_comparator.rb +45 -0
- data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
- data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
- data/lib/martyr/runtime/data_set/element.rb +66 -0
- data/lib/martyr/runtime/data_set/element_common.rb +51 -0
- data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
- data/lib/martyr/runtime/data_set/fact.rb +83 -0
- data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
- data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
- data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
- data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
- data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
- data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
- data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
- data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
- data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
- data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
- data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
- data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
- data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
- data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
- data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
- data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
- data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
- data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
- data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
- data/lib/martyr/runtime/query/query_context.rb +246 -0
- data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
- data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
- data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
- data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
- data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
- data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
- data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
- data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
- data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
- data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
- data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
- data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
- data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
- data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
- data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
- data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
- data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
- data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
- data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
- data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
- data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
- data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
- data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
- data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
- data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
- data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
- data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
- data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
- data/lib/martyr/schema/metrics/base_metric.rb +77 -0
- data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
- data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
- data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
- data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
- data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
- data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
- data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
- data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
- data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
- data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
- data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
- data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
- data/lib/martyr/version.rb +3 -0
- data/lib/martyr/virtual_cube.rb +74 -0
- data/lib/martyr.rb +55 -0
- data/martyr.gemspec +41 -0
- 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,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
|