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