flex-cartesian 1.3.1 → 2.0.0.beta

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.
@@ -0,0 +1,624 @@
1
+ module FlexCartesianCore
2
+
3
+ attr_reader :function_results, :derived, :names, :dimensiality, :dimensions, :struct, :levels, :index_show, :log
4
+
5
+ def index_show
6
+ @index
7
+ end
8
+
9
+ def initialize(dims = nil, path: nil, format: :json, logger: nil, log_level: Logger::WARN, source: nil, uri: nil, dimensions: nil, separator: ',')
10
+ init_logger(logger: logger, log_level: log_level)
11
+
12
+ # guarantee existence of the cache from the data gets loaded from index/import
13
+ @dimensions_hash = Hash.new { |h, k| h[k] = {} }
14
+
15
+ init_dimensions(dims, path: path, format: format, source: source, uri: uri, dimensions: dimensions, separator: separator)
16
+
17
+ update_space_structures
18
+ end
19
+
20
+ def cond(command = :print, index: nil, &block)
21
+ case command
22
+ when :set
23
+ raise ArgumentError, "Block required" unless block_given?
24
+ @conditions << block
25
+ self
26
+ when :unset
27
+ raise ArgumentError, "Index of the condition required" unless index
28
+ @conditions.delete_at(index)
29
+ when :clear
30
+ @conditions.clear
31
+ self
32
+ when :print
33
+ return if @conditions.empty?
34
+ @conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
35
+ else
36
+ raise ArgumentError, "unknown condition command: #{command}"
37
+ end
38
+ end
39
+
40
+ def func(command = :print, *names, hide: false, progress: false, title: "Computing function(s)", order: nil, mode: :lazy, &block)
41
+ case command
42
+
43
+ when :add
44
+ raise ArgumentError, "Function name required for :add" if names.empty?
45
+ raise ArgumentError, "Block required for :add" unless block_given?
46
+ @logger.warn "You are adding #{names.size} identical functions" if names.size > 1
47
+
48
+ names.each do |name|
49
+ add_function(name, order: order, &block)
50
+ @function_hidden.delete(name.to_sym)
51
+ @function_hidden << name.to_sym if hide
52
+ end
53
+
54
+ when :del
55
+ raise ArgumentError, "Function name(s) required for :del" if names.empty?
56
+ names.each do |name|
57
+ remove_function(name)
58
+ end
59
+
60
+ when :print
61
+ if @derived.empty?
62
+ puts "(no functions defined)"
63
+ else
64
+ functions_found = names.empty? ? @derived : @derived.slice(*names)
65
+ functions_missing = names.empty? ? [] : names - @derived.keys
66
+
67
+ functions_found.each do |fname, fblock|
68
+ source = fblock.source rescue '(source unavailable)'
69
+ body = source.sub(/^.*?\s(?=(\{|\bdo\b))/, '').strip
70
+ order = ""
71
+ if @order.value?(fname.to_sym)
72
+ case @order.key(fname.to_sym)
73
+ when :first
74
+ order = " [FIRST]"
75
+ when :last
76
+ order = " [LAST]"
77
+ end
78
+ end
79
+ puts " #{fname.inspect.ljust(12)}| #{body}#{@function_hidden.include?(fname) ? ' [HIDDEN]' : ''}#{order}"
80
+ end
81
+
82
+ functions_missing.each { |fname| puts "#{fname.inspect.ljust(12)}| (no function defined)" }
83
+ end
84
+
85
+ when :run
86
+ functions_missing = names.empty? ? [] : names - @derived.keys
87
+ raise "No function(s) defined: #{functions_missing.join(', ')}" unless functions_missing.empty?
88
+
89
+ functions_found = names.empty? ? @derived : @derived.slice(*names)
90
+ # @function_results ||= {} # probably exccessive
91
+ cartesian(progress: progress, title: title) { |v| functions_update_value(vector: v, functions: functions_found, mode: mode) }
92
+ else
93
+ raise ArgumentError, "Unknown command for function: #{command.inspect}"
94
+ end
95
+ end
96
+
97
+ def cartesian(dims = nil, lazy: false, progress: false, title: "Iterating over parameter space")
98
+
99
+ # process edge cases and initialize data structures
100
+ return to_enum(:cartesian, dims, lazy: lazy) unless block_given?
101
+ dimensions = dims || @dimensions
102
+ return nil unless dimensions.is_a?(Hash)
103
+
104
+ # create actual cartesian product as iterator of all combinations
105
+ values = dimension_values(dimensions)
106
+ enum = Enumerator.product(*values)
107
+ space = lazy ? enum.lazy : enum
108
+
109
+ # visualize progress bar, if requested
110
+ bar = progress ? ProgressBar.create(title: title, total: self.size, format: '%t [%B] %p%% %e') : nil
111
+
112
+ space.each do |combination|
113
+ # create current vector as Struct
114
+ vector = @struct.new(*combination)
115
+ # skip current vector if it doesn't respect space conditions
116
+ next unless fit?(vector)
117
+
118
+ # guarantee that functions can refer to one another within cartesian block
119
+ @derived&.each do |name, block|
120
+ vector.define_singleton_method(name) { block.call(vector) }
121
+ end
122
+
123
+ # process current vector as Struct
124
+ yield vector
125
+
126
+ # update progress bar, if it's enabled
127
+ bar&.increment
128
+ end
129
+ end
130
+
131
+ # check if `v` is a valid vector in parameter space, with respect to space conditions
132
+ # vector can be Struct, Hash, or Array. If it's Array, then order of dimensions is assumed from parameter space
133
+ # this check is computationally aggressive and only makes sense for cmanually constructed vector
134
+ def valid?(v)
135
+ # DEBUG HERE
136
+ # check if vector class is recognizable, and names & number of dimensions are aligned with parameter space
137
+ return false unless vector_consistent?(v)
138
+ # check if vector elements present among their respective dimensional values
139
+ return false unless vector_to(v, :hash).each_pair.all? { |dim, v| @dimensions[dim].include?(v) }
140
+ # check if vector respects conditions
141
+ fit?(v)
142
+ end
143
+
144
+ def fit?(v)
145
+ @conditions.none? { |cond| !cond.call(vector_to(v, :struct)) }
146
+ end
147
+
148
+ def function(vector, function, substitute: 0)
149
+ v_hash = vector_to(vector, :hash)
150
+
151
+ unless @function_results.key?(v_hash) and @function_results[v_hash].key?(function)
152
+ return substitute
153
+ end
154
+
155
+ @function_results[v_hash][function]
156
+ end
157
+
158
+ # reads from target column using data source created by `data` method
159
+ def lookup(vector, target)
160
+ vec = vector_to(vector, :hash)
161
+ return nil unless @index[vec]
162
+ @index[vec][target.to_sym] ? @index[vec][target.to_sym] : @index[vec][target.to_s]
163
+ end
164
+
165
+ # creates cartesian space and index from URI
166
+ # index is a hash that maps { dim1: value1, ..., dimN: valueN} to a row of the data source
167
+ def index(source:, uri:, dimensions:, separator: ',')
168
+ @index = {}
169
+ @dimensions ||= {}
170
+ # initialize empty dimensions
171
+ dimensions.each do |dim|
172
+ @dimensions[dim.to_sym] ||= []
173
+ end
174
+
175
+ case source
176
+ when :csv
177
+ require 'csv'
178
+ table = CSV.read(uri, headers: true, header_converters: :symbol, col_sep: separator, strip: true)
179
+
180
+ table.each do |row|
181
+ key = dimensions.each_with_object({}) do |dim, hash|
182
+ value = row[dim].to_s.strip
183
+ unless @dimensions_hash[dim][value]
184
+ @dimensions[dim] << value
185
+ @dimensions_hash[dim][value] = true
186
+ end
187
+ hash[dim.to_sym] = value
188
+ end
189
+ @index[key] = row
190
+ end
191
+ when :xlsx
192
+ require 'roo'
193
+
194
+ xlsx = Roo::Excelx.new(uri)
195
+ sheet = xlsx.sheet(0)
196
+
197
+ # each row is a hash of ALL columns from the XSLX
198
+ data = sheet.parse(headers: true)
199
+ # skip headers in the first row of XLSX sheet
200
+ data.shift
201
+ data.each do |row|
202
+ next if row.values.all?(&:nil?)
203
+
204
+ # index key is an array of dimensional values from the specified dimensions only
205
+ # this key points to FULL row which is assumed to have values of the future functions
206
+ key = dimensions.each_with_object({}) do |dim, hash|
207
+ # TODO: XSLX values are converted to string for uniformity with CSV, even though XSLX returns proper types
208
+ value = row[dim.to_s].to_s.strip
209
+ dim_sym = dim.to_sym
210
+ unless @dimensions_hash[dim_sym][value]
211
+ @dimensions[dim_sym] << value
212
+ @dimensions_hash[dim_sym][value] = true
213
+ end
214
+ hash[dim.to_sym]= value
215
+ end
216
+ @index[key] = row
217
+ end
218
+ else
219
+ raise "Unknown source type #{source}"
220
+ end
221
+
222
+ self
223
+ end
224
+
225
+ # TODO: Dimensions can be omitted - in this case, automatically fetch all dimensions from CSV header
226
+ def source(command, vector: nil, target: nil )
227
+ case command
228
+ when :read
229
+ return nil if (vector.size == 0 or target.nil?)
230
+ lookup(vector, target)
231
+ else
232
+ @logger.error "Unknown data command `#{command}`"
233
+ end
234
+ end
235
+
236
+ # convert Struct or Array vector to Hash with all checks
237
+ def vector_to(v, type)
238
+ return nil unless vector_consistent?(v)
239
+
240
+ case type
241
+ when :hash
242
+ vector_to_hash!(v)
243
+ when :struct
244
+ vector_to_struct!(v)
245
+ else
246
+ raise "Incorrect target type for vector conversion: #{type}"
247
+ end
248
+ end
249
+
250
+ def dim(command, *dims)
251
+ case command
252
+ when :add
253
+ dims.each do |d|
254
+ raise ArgumentError, "Incorrect description of the dimensions #{dims.inspect}, must be Hash" unless d.is_a?(Hash)
255
+ @dimensions.update(d)
256
+ end
257
+ update_space_structures
258
+ when :del
259
+ dims.each do |dim|
260
+ raise ArgumentError, "Incorrect dimension name #{dim.inspect}, must be Symbol" unless dim.is_a?(Symbol)
261
+ @dimensions.delete(dim)
262
+ end
263
+ update_space_structures
264
+ else
265
+ raise "Incorrect dimension command: #{command}"
266
+ end
267
+ end
268
+
269
+
270
+
271
+ private
272
+
273
+ # For a given array of dimension names, return those that are used in `block` as fields of its iterator
274
+ # This method provides a correctness check in the case we're removing dimensions
275
+ # It allows to determine the functions broken by the dimension removal
276
+ # NOTE: this check will miss any dependencies if executed from IRB
277
+
278
+ def check_dimension_deps(dimension_names)
279
+ @derived.each do |func, body|
280
+ deps = dimension_deps(body, dimension_names)
281
+ @logger.error "Function `#{func}` depends on removed dimension(s): #{deps.join(', ')}" unless deps.empty?
282
+ end
283
+
284
+ @conditions.each_with_index do |body, index|
285
+ deps = dimension_deps(body, dimension_names)
286
+ @logger.error "Condition ##{index} depends on removed dimension(s): #{deps.join(', ')}" unless deps.empty?
287
+ end
288
+ end
289
+
290
+
291
+
292
+ def dimension_deps(block, dimension_names)
293
+ require 'ast'
294
+
295
+ # Get AST of the function body
296
+ ast = RubyVM::AbstractSyntaxTree.of(block)
297
+ return [] unless ast
298
+
299
+ # Get the name of iterator based on parameters of the block
300
+ # block.parameters returns something like [[:opt, :v]] or [[:req, :vector]]
301
+ # We simply take the first argument
302
+ iterator_var_name = block.parameters.first&.last
303
+ return [] unless iterator_var_name
304
+
305
+ found_dimensions = []
306
+
307
+ # Traverse the AST recursively
308
+ search = ->(node) do
309
+ return unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
310
+
311
+ # We are interested in :call only
312
+ if node.type == :CALL
313
+ receiver = node.children[0] # Object called
314
+ method_name = node.children[1] # What method is called , such as :size
315
+
316
+ # Main criteria:
317
+ # - Object of the call exists
318
+ # - It is a local variable :LVAR
319
+ # - Its name is identical to the iterator's name, such as :v
320
+ is_iterator_receiver = receiver &&
321
+ receiver.is_a?(RubyVM::AbstractSyntaxTree::Node) &&
322
+ [:LVAR, :DVAR].include?(receiver.type) &&
323
+ receiver.children[0] == iterator_var_name
324
+
325
+ if is_iterator_receiver && dimension_names.include?(method_name)
326
+ found_dimensions << method_name
327
+ end
328
+ end
329
+
330
+ # Recursively traverse nested nodes
331
+ node.children.each { |child| search.call(child) }
332
+ end
333
+
334
+ # Search and return unique findings
335
+ search.call(ast)
336
+
337
+ found_dimensions.uniq
338
+ end
339
+
340
+ # if dimensions were changed, update hash of function results, accordingly
341
+ def function_results_immerse
342
+ return if @function_results.empty?
343
+
344
+ # check if dimensions were added or removed
345
+ change = @function_results.first.first.size - @dimensiality
346
+
347
+ return if change == 0
348
+
349
+ if change > 0
350
+ # dimensions were removed
351
+ removed_dimensions = @function_results.first.first.keys - @names
352
+ check_dimension_deps(removed_dimensions)
353
+ # NOTE: When we reduce dimensiality, then vectors as keys of function_results cease to be unique!
354
+ # NOTE: As a new-unique vector key appear, Ruby just silently rewrite the same hash entry
355
+ # NOTE: Having said this, only the _last_ function value will survive!
356
+ @function_results.transform_keys! { |vector| vector.except(*removed_dimensions) }
357
+ else
358
+ # dimensions were added
359
+ # as hash elements are added in order, to the end of hash, we take the `change` of last elements in @dimensiality
360
+ # and - by agreement - we take the first dimensional values for each added dimension
361
+ new_dimensions = @names - @function_results.first.first.keys
362
+ # this is a hash of added dimensions with only first dimensional value for each dimension
363
+ new_first_values = @dimensions.slice(*new_dimensions).transform_values!(&:first)
364
+ # Immerse existing vectors to higher-dimensiality space by adding new dimensions with their first values
365
+ # Note: this implies that existing functions will be defined in the immerse sub-space, and nil in the rest of the new space
366
+ @function_results.transform_keys! { |vector| vector.merge(new_first_values) }
367
+ end
368
+ end
369
+
370
+ # For a given subset of space functions, update their values in a given vector
371
+ def functions_update_value(vector: , functions: , mode: )
372
+ v = vector_to(vector, :hash)
373
+ @function_results[v] ||= {}
374
+
375
+ functions.each do |fname, block|
376
+ @function_results[v] ||= {}
377
+ results = @function_results[v]
378
+
379
+ case mode
380
+ when :enforce
381
+ results[fname] = block.call(vector)
382
+
383
+ when :reuse
384
+ unless results.key?(fname)
385
+ raise ArgumentError, "Compute mode #{mode} requires function #{fname} to have value in #{vector_to(vector, :array).inspect}"
386
+ end
387
+
388
+ when :lazy
389
+ unless results.key?(fname)
390
+ results[fname] = block.call(vector)
391
+ end
392
+
393
+ else
394
+ raise ArgumentError, "Incorrect computing mode #{mode.inspect}"
395
+ end
396
+
397
+ ensure_dimension_width(fname, results[fname])
398
+ end
399
+ end
400
+
401
+ def update_space_structures
402
+ update_dimensional_structures
403
+ update_conditional_structures
404
+ update_functional_structures
405
+ end
406
+
407
+ def init_logger(logger:, log_level:)
408
+ @logger = logger || Logger.new($stdout)
409
+ @logger.level = log_level
410
+
411
+ @logger.formatter = proc do |severity, _datetime, _progname, msg|
412
+ "#{severity}: #{msg}\n"
413
+ end
414
+
415
+ # make this logger instance kill the program if severity is error or worse
416
+ def @logger.add(severity, message = nil, progname = nil, &block)
417
+ super
418
+ raise SystemExit.new(1, message || progname) if severity >= Logger::ERROR
419
+ end
420
+
421
+ end
422
+
423
+ def init_dimensions(dims, path:, format:, source:, uri:, dimensions:, separator:)
424
+ # get hash of dimensions: name => array of dimensional values
425
+ if dims && path
426
+ raise "Cannot specify both dimensions and path to dimensions"
427
+ elsif dims
428
+ @dimensions = dims
429
+ elsif path
430
+ import(path, format: format)
431
+ else
432
+ # finally, we read entire space from URI
433
+ raise "Missing data source type" if source.empty?
434
+ raise "Missing data URI" if uri.empty?
435
+ raise "Missing data dimensions" if dimensions.empty?
436
+ index(source: source, uri: uri, dimensions: dimensions, separator: separator)
437
+ end
438
+ end
439
+
440
+ def update_dimensional_structures
441
+ @dimensions = normalize_dimensions(@dimensions)
442
+
443
+ # internal structure that allows us to quickly check if we're adding new or existing dimensional value
444
+ # such hash is O(1) to the contrast with straightforward .include? which is O(n) and VERY slow on huge tables
445
+ @dimensions_hash ||= Hash.new { |h, k| h[k] = {} }
446
+
447
+ # array of arrays of dimension values (not a Cartesian product yet)
448
+ @levels = dimension_values(@dimensions)
449
+ # total size of Cartesian space (number of vectors, that is, ALL combinations, ignoring conditions)
450
+ @raw_size = @levels.map(&:size).inject(:*)
451
+ # array of dimension names
452
+ @names = @dimensions.keys
453
+ # internal structure: for each dimension, minimal textual width that fits all values in this dimension - required for table output
454
+ # the width is determined for each actual dimension and for each function
455
+ if @dimension_widths.nil?
456
+ @dimension_widths = @names.zip(dimension_widths).to_h
457
+ else
458
+ @dimension_widths.update(@names.zip(dimension_widths).to_h)
459
+ end
460
+ # however, we must remove widths of removed dimensions or functions
461
+ # @dimension_widths.keep_if { |k,_| @dimensions.key?(k) || @derived.key?(k) }
462
+ @default_width = 10
463
+
464
+ # define class for a vector represented as Struct, to be able to access its elements using `.<dimension_name>`
465
+ # NOTE: this class must be unique - otherwise, Struct objects as Hash keys won't coincide for different Struct classes
466
+ # even if fields of such structs are identical
467
+ @struct = Struct.new(*@names).tap { |sc| sc.include(FlexOutput) }
468
+
469
+ # number of dimensions of parameter space
470
+ @dimensiality = @names.size
471
+ end
472
+
473
+ def update_conditional_structures
474
+ # array of conditions for valid vectors in parameter space
475
+ @conditions ||= []
476
+ end
477
+
478
+ def update_functional_structures
479
+ # functions in parameter space
480
+ @derived ||= {}
481
+ # ordering of the functions
482
+ @order ||= { first: nil, last: nil }
483
+ # Hash: instance of @struct vector => { fname => value }
484
+ @function_results ||= {}
485
+ function_results_immerse
486
+ @function_hidden ||= Set.new
487
+ end
488
+
489
+ # create tabular widths for basic dimensions (that is, excluding functions)
490
+ def dimension_widths
491
+ @dimensions.map do |dim, values|
492
+ max_width = ([dim.to_s] + values).inject(0) do |max, e|
493
+ len = e.to_s.length
494
+ len > max ? len : max
495
+ end
496
+ end
497
+ end
498
+
499
+ # update tabular width of a dynamic dimension (i.e., function)
500
+ # to be called wherever you expect new dimension or a new dimensional value to appear
501
+ def ensure_dimension_width(name, value = nil)
502
+ raise "Dimension name is empty" unless name
503
+
504
+ if value == nil # adding new dynamic dimension with default width, if not added before
505
+ @dimension_widths[name] = @default_width unless @dimension_widths[name]
506
+ else
507
+ value.to_s.size > @dimension_widths[name] # adding new value of a dynamic dimension
508
+ @dimension_widths[name] = value.to_s.size
509
+ end
510
+ end
511
+
512
+ # convert dimensional values to array, for conformity
513
+ def normalize_dimensions(dimensions)
514
+ dimensions.transform_values do |values|
515
+ values.is_a?(Enumerable) && !values.is_a?(String) ? values.to_a : [values]
516
+ end
517
+ end
518
+
519
+ def dimension_values(dimensions)
520
+ # array of arrays of dimensional values, not a cartesian product yet
521
+ res = dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
522
+ # check if any dimension has no values
523
+ res.each { |dim| raise "dimension cannot be empty: ``" if dim.empty? }
524
+ res
525
+ end
526
+
527
+ def add_function(name, order: nil , &block)
528
+ raise ArgumentError, "Block required" unless block_given?
529
+ if reserved_function_names.include?(name.to_sym)
530
+ raise ArgumentError, "Function name '#{name}' has been already added"
531
+ elsif reserved_struct_names.include?(name.to_sym)
532
+ raise ArgumentError, "Name '#{name}' has been reserved for internal method, you can't use it for a function"
533
+ end
534
+ if order == :last
535
+ @derived[name.to_sym] = block # add to the tail of the hash
536
+ @order[:last] = name.to_sym
537
+ elsif order == :first
538
+ @derived = { name.to_sym => block }.merge(@derived) # add to the head of the hash
539
+ @order[:first] = name.to_sym
540
+ elsif order == nil
541
+ if @order[:last] != nil
542
+ last_name = @order[:last]
543
+ last_body = @derived[last_name]
544
+ @derived.delete(@order[:last]) # remove the tail of the hash
545
+ @derived[name.to_sym] = block # add new function to the tail of the hash
546
+ @derived[last_name] = last_body # restore :last function in the tail of the hash
547
+ else
548
+ @derived[name.to_sym] = block
549
+ end
550
+ else
551
+ raise ArgumentError, "unknown function order '#{order}'"
552
+ end
553
+ ensure_dimension_width(name)
554
+ end
555
+
556
+ def remove_function(name)
557
+ @derived.delete(name.to_sym)
558
+ @order[:last] = nil if @order[:last] == name.to_sym
559
+ @order[:first] = nil if @order[:first] == name.to_sym
560
+ end
561
+
562
+ def reserved_struct_names
563
+ (base_struct_methods = Struct.new(:dummy).methods(false) + Struct.new(:dummy).instance_methods(false)).uniq
564
+ end
565
+
566
+ def reserved_function_names
567
+ (self.methods + self.class.instance_methods(false)).uniq
568
+ end
569
+
570
+ def decorate_point(v)
571
+ @derived&.each do |name, block|
572
+ v.define_singleton_method(name) { block.call(v) }
573
+ end
574
+ v
575
+ end
576
+
577
+ def log
578
+ @logger
579
+ end
580
+
581
+ # convert Struct or Array vector to Hash, keeping order of dimension names as it is in parameter space
582
+ # Note: conditions and dimension consistency are NOT respected
583
+ def vector_to_hash!(v)
584
+ return v if v.is_a?(Hash)
585
+
586
+ if v.is_a?(Array)
587
+ @names.zip(v).to_h
588
+ elsif v.is_a?(Struct)
589
+ v.members.zip(v.values).to_h
590
+ else
591
+ raise "Incorrect vector type `#{v.class}`"
592
+ end
593
+ end
594
+
595
+ # convert Hash or Array vector to Struct, keeping order of dimension names as it is in parameter space
596
+ # Note: conditions and dimension consistency are NOT respected
597
+ def vector_to_struct!(v)
598
+ return v if v.is_a?(Struct)
599
+
600
+ if v.is_a?(Array)
601
+ @struct.new(*v)
602
+ elsif v.is_a?(Hash)
603
+ @struct.new(*v.values)
604
+ else
605
+ raise "Incorrect vector type `#{v.class}`"
606
+ end
607
+ end
608
+
609
+ # check consistency of the vector internal structure relatively to parameter space
610
+ # Note: conditions are NOT checked
611
+ def vector_consistent?(v)
612
+ raise "Incorrect vector type `#{v.class}`" unless v.is_a?(Enumerable)
613
+ raise "Incorrect dimensiality of vector '#{v.inspect}'" unless vector_to_hash!(v).size == @dimensiality
614
+ raise "Incorrect vector dimensions #{v.keys.inspect}" unless @names.to_set == vector_to_hash!(v).keys.to_set
615
+ true
616
+ end
617
+
618
+ def add_dimension(dim)
619
+ raise "Incorrect description of the dimension #{dim.inspect}, Hash required" unless dim.is_a(Hash)
620
+ @dimensions << dim
621
+ end
622
+
623
+ end
624
+
@@ -0,0 +1,13 @@
1
+ module FlexCartesianDeprecations
2
+
3
+ WARNINGS = [
4
+ "`.dimensions` is deprecated and will be renamed to `.elements` in the next version",
5
+ "flag `.dimensions(... raw: ...) is deprecated and will be removed in the next version, please use `.inspect` instead"
6
+ ]
7
+
8
+ def deprecations
9
+ WARNINGS.each { |msg| log.warn msg }
10
+ end
11
+
12
+ end
13
+