flex-cartesian 1.3.0 → 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,192 @@
1
+ module FlexCartesianIO
2
+
3
+ # unified wrapper method for output
4
+ def output(rows = nil, **opts)
5
+ if rows == nil
6
+ cartesian_output(**opts)
7
+ else
8
+ table_output(rows, **opts)
9
+ end
10
+ end
11
+
12
+ # internal method
13
+ def separator(sep, format:)
14
+ case format
15
+ when :csv
16
+ [";", ","].include?(sep) ? sep : ";"
17
+ when :markdown
18
+ "|"
19
+ else
20
+ sep
21
+ end
22
+ end
23
+
24
+ # internal method for printing headers
25
+ def output_headers(headers:, format:, widths:, stream:, colorize:, separator:, file:)
26
+ case format
27
+ when :markdown
28
+ stream.print separator
29
+ cells = headers.map.with_index do |h,i|
30
+ cell = h.ljust(widths[i])
31
+ fmt_cell(cell, file: file, colorize: colorize, header: true)
32
+ end
33
+ stream.puts cells.join(separator) + separator
34
+ stream.puts separator + headers.map.with_index { |h,i| "-" * widths[i] }.join(separator) + separator
35
+ when :csv
36
+ stream.puts headers.map.with_index { |h,i| fmt_cell(h, file: file, colorize: colorize, header: true, width: widths[i]) }.join(separator)
37
+ else
38
+ stream.puts separator + headers.map.with_index { |h,i| fmt_cell(h, file: file, colorize: colorize, header: true, width: widths[i]) }.join(separator) + separator
39
+ end
40
+ end
41
+
42
+ # internal method for output from space
43
+ def cartesian_output(separator: "|", colorize: true, format: :plain, limit: nil, file: nil)
44
+
45
+ # output stream
46
+ out = file ? File.open(file, "w") : STDOUT
47
+
48
+ # column separator
49
+ sep = separator(separator, format: format)
50
+
51
+ # table headers
52
+ visible_func_names = @derived.keys - (@function_hidden || Set.new).to_a
53
+ headers = @names.map(&:to_s) + visible_func_names.map(&:to_s)
54
+
55
+ # column widths
56
+ widths = headers.map { |h| @dimension_widths[h.to_sym] == nil ? @default_width : @dimension_widths[h.to_sym] }
57
+
58
+ # print headers
59
+ output_headers(file: file, headers: headers, format: format, widths: widths, stream: out, colorize: colorize, separator: sep)
60
+
61
+ # print rows
62
+ cartesian do |vector|
63
+ values = vector.members.map { |m| vector.send(m) } + visible_func_names.map { |f| @function_results&.dig(vector_to(vector, :hash), f) }
64
+ line = headers.zip(values).map.with_index { |(dim, val), i| fmt_cell(val, file: file, colorize: colorize, width: widths[i]) }.join(sep)
65
+ case format
66
+ when :plain
67
+ out.puts sep + line + sep
68
+ when :markdown
69
+ out.puts sep + line + sep
70
+ when :csv
71
+ out.puts line
72
+ end
73
+ end
74
+
75
+ out.close if out.is_a?(File)
76
+ end
77
+
78
+ def import(path, format: :json)
79
+ data = case format
80
+ when :json
81
+ JSON.parse(File.read(path), symbolize_names: true)
82
+ when :yaml
83
+ YAML.safe_load(File.read(path), symbolize_names: true)
84
+ else
85
+ raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
86
+ end
87
+
88
+ raise TypeError, "Expected parsed data to be a Hash" unless data.is_a?(Hash)
89
+
90
+ @dimensions = data
91
+ self
92
+ end
93
+
94
+ def export(path, format: :json)
95
+ case format
96
+ when :json
97
+ File.write(path, JSON.pretty_generate(@dimensions))
98
+ when :yaml
99
+ File.write(path, YAML.dump(@dimensions))
100
+ else
101
+ raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
102
+ end
103
+ end
104
+
105
+
106
+ def from_json(path)
107
+ data = JSON.parse(File.read(path), symbolize_names: true)
108
+ @dimensions = data
109
+ end
110
+
111
+ def from_yaml(path)
112
+ data = YAML.safe_load(File.read(path), symbolize_names: true)
113
+ @dimensions = data
114
+ end
115
+
116
+ # internal method
117
+ def fmt_cell(value, file:, colorize: false, header: false, width: nil)
118
+ str = case value
119
+ when String then value
120
+ else value.inspect
121
+ end
122
+ str = str.ljust(width) if width
123
+
124
+ # output to file must NOT be colorized to avoid special characters in file
125
+ return str if file
126
+
127
+ if not colorize
128
+ str
129
+ elsif header
130
+ str.colorize(:yellow)
131
+ else
132
+ str.colorize(:cyan)
133
+ end
134
+ end
135
+
136
+ # THIS NEEDS TO BE MERGED INTO standard output method
137
+ def table_output(rows, separator: "|", colorize: true, format: :plain, file: nil)
138
+ return if rows.nil? || rows.empty?
139
+
140
+ # output stream
141
+ out = file ? File.open(file, "w") : STDOUT
142
+
143
+ # column separator
144
+ sep = separator(separator, format: format)
145
+
146
+ # table headers
147
+ headers = rows.first.keys.map(&:to_s)
148
+
149
+ # column widths
150
+ widths = headers.to_h do |h|
151
+ values = rows.map { |row| row[h.to_sym].to_s.size }
152
+ [h, [h.size, *values].max]
153
+ end
154
+
155
+ # print headers
156
+ output_headers(file: file, headers: headers, format: format, widths: widths.values, stream: out, colorize: colorize, separator: sep)
157
+
158
+ rows.each do |row|
159
+ line = headers.map { |h| fmt_cell(row[h.to_sym], file: file, colorize: colorize, width: widths[h]) }.join(sep)
160
+ case format
161
+ when :plain
162
+ out.puts sep + line + sep
163
+ when :markdown
164
+ out.puts sep + line + sep
165
+ when :csv
166
+ out.puts line + sep
167
+ end
168
+ end
169
+
170
+ out.close if out.is_a?(File)
171
+ end
172
+
173
+ end
174
+
175
+ module FlexOutput
176
+ def cartesian_output(separator: "|", colorize: true)
177
+ return puts "(empty struct)" unless respond_to?(:members) && respond_to?(:values)
178
+
179
+ values_list = members.zip(values.map { |v| v.inspect })
180
+
181
+ widths = values_list.map { |k, v| [k.to_s.size, v.size].max }
182
+
183
+ line = values_list.each_with_index.map do |(_, val), i|
184
+ str = val.to_s
185
+ str = str.ljust(widths[i])
186
+ colorize ? str.colorize(:cyan) : str
187
+ end
188
+
189
+ puts line.join(separator)
190
+ end
191
+ end
192
+
@@ -0,0 +1,126 @@
1
+ require 'set'
2
+
3
+ module FlexCartesianUtilities
4
+
5
+ # TODO: .index is O(N), better optimize it using intermediate Hash
6
+ # vector commands
7
+ def vector(command:, vector:, dimension: nil, offset: 1)
8
+ case command
9
+ when :index
10
+ unless dimension
11
+ @names.map { |dim| vector(command: command, vector: vector, dimension: dim) }
12
+ else
13
+ levels = @dimensions[dimension]
14
+ raise "Incorrect dimension name" unless levels
15
+ levels.index(vector[dimension])
16
+ end
17
+ when :shift
18
+ # vector = vector_to(v, :hash)
19
+ return nil if dimension.nil?
20
+ index = vector(command: :index, vector: vector, dimension: dimension)
21
+ index ? @dimensions[dimension][index + offset] : nil
22
+ else
23
+ raise "Incorrect vector command #{command}"
24
+ end
25
+ end
26
+
27
+ # obtain value of the given function on a given vector from parameter space
28
+ # modes:
29
+ # :enforce - recompute function value
30
+ # :reuse - fetch previously computed value or drop error if there isn't one
31
+ # :lazy - recompute if there's no precomputed value or reuse if there's one
32
+ def value(v, function:, mode: :lazy)
33
+ v_struct = vector_to(v, :struct)
34
+ v_hash = vector_to(v, :hash)
35
+
36
+ res = @results[v_struct][function]
37
+
38
+ case mode
39
+ when :reuse
40
+ raise "Value of #{function} function is missing on #{v_hash.inspect} vector" if res.nil?
41
+ return res
42
+ when :enforce
43
+ new_res = @derived[function].call(v_struct)
44
+ @results[v_struct][function] = new_res
45
+ return new_res
46
+ when :lazy
47
+ new_res = res.nil? ? @derived[function].call(v_struct) : res
48
+ @results[v_struct][function] = new_res
49
+ return new_res
50
+ else
51
+ raise "Incorrect function recompute mode: #{mode}"
52
+ end
53
+ res
54
+ end
55
+
56
+ # NOTE: BAD CONFLICT OF NAME "DIMENSIONS"
57
+ #def dimensions(data = @dimensions, raw: false, separator: ', ', dimensions: true, values: true, lazy: false)
58
+ # return nil if !dimensions && !values
59
+
60
+ # unless data.is_a?(Struct) || data.is_a?(Hash)
61
+ # raise ArgumentError, "Incorrect type of dimensions: #{data.class}"
62
+ # end
63
+
64
+ # if data.is_a?(Struct)
65
+ # return nil unless valid?(data)
66
+
67
+ # return data.each_pair.map { |k, val|
68
+ # (dimensions ? "#{k}" : "") +
69
+ # ((dimensions && values) ? "=" : "") +
70
+ # (values ? "#{val}" : "")
71
+ # }.join(separator)
72
+ # end
73
+
74
+ # enum = Enumerator.new do |y|
75
+ # cartesian(data, lazy: lazy) do |v|
76
+ # next unless valid?(v)
77
+ # y << dimensions(v, raw: raw, separator: separator, dimensions: dimensions, values: values, lazy: lazy)
78
+ # end
79
+ # end
80
+
81
+ # return enum if lazy
82
+ # enum.to_a.join("\n")
83
+ #end
84
+
85
+ # Return number of combinations in parameter space, with respect to conditions
86
+ def size
87
+ return 0 unless @dimensions.is_a?(Hash)
88
+
89
+ return @plan.size if @plan # TODO: dead code
90
+
91
+ if @conditions.empty?
92
+ values = @dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
93
+ return 0 if values.any?(&:empty?)
94
+ values.map(&:size).inject(1, :*)
95
+ else
96
+ size = 0
97
+ cartesian do |v|
98
+ next if @conditions.any? { |cond| !cond.call(v) }
99
+ size += 1
100
+ end
101
+ size
102
+ end
103
+ end
104
+
105
+ # Convert first `limit` combinations of parameter space to array
106
+ # or convert vector in parameter space to array
107
+ # with respect to conditions
108
+ def to_a(data = nil, limit: nil)
109
+
110
+ # if no `data` given we assume the data is parameter space
111
+ if data.nil?
112
+ result = []
113
+ cartesian do |v|
114
+ result << v.to_a
115
+ break if limit && result.size >= limit
116
+ end
117
+ return result
118
+ end
119
+
120
+ # otherwise, it's a single vector
121
+ valid?(data)
122
+ data.values
123
+ end
124
+
125
+ end
126
+
@@ -5,343 +5,25 @@ require 'json'
5
5
  require 'yaml'
6
6
  require 'method_source'
7
7
  require 'set'
8
+ require 'logger'
8
9
 
9
- module FlexOutput
10
- def output(separator: " | ", colorize: false, align: true)
11
- return puts "(empty struct)" unless respond_to?(:members) && respond_to?(:values)
10
+ require_relative 'flex-cartesian/flex-cartesian-core'
11
+ require_relative 'flex-cartesian/flex-cartesian-io'
12
+ require_relative 'flex-cartesian/flex-cartesian-utilities'
13
+ require_relative 'flex-cartesian/flex-cartesian-analyzer'
14
+ require_relative 'flex-cartesian/flex-cartesian-deprecations'
15
+ require_relative 'visualization/html'
12
16
 
13
- values_list = members.zip(values.map { |v| v.inspect })
14
17
 
15
- widths = align ? values_list.map { |k, v| [k.to_s.size, v.size].max } : []
16
-
17
- line = values_list.each_with_index.map do |(_, val), i|
18
- str = val.to_s
19
- str = str.ljust(widths[i]) if align
20
- colorize ? str.colorize(:cyan) : str
21
- end
22
-
23
- puts line.join(separator)
24
- end
25
- end
26
18
 
27
19
  class FlexCartesian
28
20
 
29
- def initialize(dimensions = nil, path: nil, format: :json)
30
- if dimensions && path
31
- puts "Please specify either dimensions or path to dimensions"
32
- exit
33
- end
34
- @dimensions = dimensions
35
- @conditions = []
36
- @derived = {}
37
- @order = { first: nil, last: nil }
38
- @function_results = {} # key: Struct instance.object_id => { fname => value }
39
- @function_hidden = Set.new
40
- import(path, format: format) if path
41
- end
42
-
43
- def dimensions(data = @dimensions, raw: false, separator: ', ', dimensions: true, values: true)
44
- return data.inspect if raw # by default, with no data speciaifed, we assume dimensions of Cartesian
45
- return nil if not dimensions and not values
46
-
47
- if data.is_a?(Struct) or data.is_a?(Hash) # vector in Cartesian or entire Cartesian
48
- data.each_pair.map { |k, v| (dimensions ? "#{k}" : "") + ((dimensions and values) ? "=" : "") + (values ? "#{v}" : "") }.join(separator)
49
- else
50
- puts "Incorrect type of dimensions: #{data.class}"
51
- exit
52
- end
53
- end
54
-
55
- def cond(command = :print, index: nil, &block)
56
- case command
57
- when :set
58
- raise ArgumentError, "Block required" unless block_given?
59
- @conditions << block
60
- self
61
- when :unset
62
- raise ArgumentError, "Index of the condition required" unless index
63
- @conditions.delete_at(index)
64
- when :clear
65
- @conditions.clear
66
- self
67
- when :print
68
- return if @conditions.empty?
69
- @conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
70
- else
71
- raise ArgumentError, "unknown condition command: #{command}"
72
- end
73
- end
74
-
75
- def func(command = :print, name = nil, hide: false, progress: false, title: "calculating functions", order: nil, &block)
76
- case command
77
- when :add
78
- raise ArgumentError, "Function name and block required for :add" unless name && block_given?
79
- add_function(name, order: order, &block)
80
- @function_hidden.delete(name.to_sym)
81
- @function_hidden << name.to_sym if hide
82
-
83
- when :del
84
- raise ArgumentError, "Function name required for :del" unless name
85
- remove_function(name)
86
-
87
- when :print
88
- if @derived.empty?
89
- puts "(no functions defined)"
90
- else
91
- @derived.each do |fname, fblock|
92
- source = fblock.source rescue '(source unavailable)'
93
- body = source.sub(/^.*?\s(?=(\{|\bdo\b))/, '').strip
94
- order = ""
95
- if @order.value?(fname.to_sym)
96
- case @order.key(fname.to_sym)
97
- when :first
98
- order = " [FIRST]"
99
- when :last
100
- order = " [LAST]"
101
- end
102
- end
103
- puts " #{fname.inspect.ljust(12)}| #{body}#{@function_hidden.include?(fname) ? ' [HIDDEN]' : ''}#{order}"
104
- end
105
- end
106
-
107
- when :run
108
- @function_results = {}
109
-
110
- if progress
111
- bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')
112
-
113
- cartesian do |v|
114
- @function_results[v] ||= {}
115
- @derived.each do |fname, block|
116
- @function_results[v][fname] = block.call(v)
117
- end
118
- bar.increment if progress
119
- end
120
-
121
- else
122
- cartesian do |v|
123
- @function_results[v] ||= {}
124
- @derived.each do |fname, block|
125
- @function_results[v][fname] = block.call(v)
126
- end
127
- end
128
- end
129
-
130
- else
131
- raise ArgumentError, "Unknown command for function: #{command.inspect}"
132
- end
133
- end
134
-
135
- def add_function(name, order: nil , &block)
136
- raise ArgumentError, "Block required" unless block_given?
137
- if reserved_function_names.include?(name.to_sym)
138
- raise ArgumentError, "Function name '#{name}' has been already added"
139
- elsif reserved_struct_names.include?(name.to_sym)
140
- raise ArgumentError, "Name '#{name}' has been reserved for internal method, you can't use it for a function"
141
- end
142
- if order == :last
143
- @derived[name.to_sym] = block # add to the tail of the hash
144
- @order[:last] = name.to_sym
145
- elsif order == :first
146
- @derived = { name.to_sym => block }.merge(@derived) # add to the head of the hash
147
- @order[:first] = name.to_sym
148
- elsif order == nil
149
- if @order[:last] != nil
150
- last_name = @order[:last]
151
- last_body = @derived[last_name]
152
- @derived.delete(@order[:last]) # remove the tail of the hash
153
- @derived[name.to_sym] = block # add new function to the tail of the hash
154
- @derived[last_name] = last_body # restore :last function in the tail of the hash
155
- else
156
- @derived[name.to_sym] = block
157
- end
158
- else
159
- raise ArgumentError, "unknown function order '#{order}'"
160
- end
161
- end
162
-
163
- def remove_function(name)
164
- @derived.delete(name.to_sym)
165
- @order[:last] = nil if @order[:last] == name.to_sym
166
- @order[:first] = nil if @order[:first] == name.to_sym
167
- end
168
-
169
- def cartesian(dims = nil, lazy: false)
170
- dimensions = dims || @dimensions
171
- return nil unless dimensions.is_a?(Hash)
172
-
173
- names = dimensions.keys
174
- values = dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
175
-
176
- return to_enum(:cartesian, dims, lazy: lazy) unless block_given?
177
- return if values.any?(&:empty?)
178
-
179
- struct_class = Struct.new(*names).tap { |sc| sc.include(FlexOutput) }
180
-
181
- base = values.first.product(*values[1..])
182
- enum = lazy ? base.lazy : base
183
-
184
- enum.each do |combo|
185
- struct_instance = struct_class.new(*combo)
186
-
187
- @derived&.each do |name, block|
188
- struct_instance.define_singleton_method(name) { block.call(struct_instance) }
189
- end
190
-
191
- next if @conditions.any? { |cond| !cond.call(struct_instance) }
192
-
193
- yield struct_instance
194
- end
195
- end
196
-
197
- def size
198
- return 0 unless @dimensions.is_a?(Hash)
199
- if @conditions.empty?
200
- values = @dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
201
- return 0 if values.any?(&:empty?)
202
- values.map(&:size).inject(1, :*)
203
- else
204
- size = 0
205
- cartesian do |v|
206
- next if @conditions.any? { |cond| !cond.call(v) }
207
- size += 1
208
- end
209
- size
210
- end
211
- end
212
-
213
- def to_a(limit: nil)
214
- result = []
215
- cartesian do |v|
216
- result << v
217
- break if limit && result.size >= limit
218
- end
219
- result
220
- end
221
-
222
- def progress_each(lazy: false, title: "Processing")
223
- bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')
224
-
225
- cartesian(@dimensions, lazy: lazy) do |v|
226
- yield v
227
- bar.increment
228
- end
229
- end
230
-
231
- def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil)
232
- rows = if @function_results && !@function_results.empty?
233
- @function_results.keys
234
- else
235
- result = []
236
- cartesian do |v|
237
- result << v
238
- break if limit && result.size >= limit
239
- end
240
- result
241
- end
242
-
243
- return if rows.empty?
244
-
245
- visible_func_names = @derived.keys - (@function_hidden || Set.new).to_a
246
- headers = rows.first.members.map(&:to_s) + visible_func_names.map(&:to_s)
247
-
248
- widths = align ? headers.to_h { |h|
249
- values = rows.map do |r|
250
- val = if r.members.map(&:to_s).include?(h)
251
- r.send(h)
252
- else
253
- @function_results&.dig(r, h.to_sym)
254
- end
255
- fmt_cell(val, false).size
256
- end
257
- [h, [h.size, *values].max]
258
- } : {}
259
-
260
- lines = []
261
-
262
- # Header
263
- case format
264
- when :markdown
265
- lines << "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
266
- lines << "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
267
- when :csv
268
- lines << headers.join(",")
269
- else
270
- lines << headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(separator)
271
- end
272
-
273
- # Rows
274
- rows.each do |row|
275
- values = row.members.map { |m| row.send(m) } +
276
- visible_func_names.map { |fname| @function_results&.dig(row, fname) }
277
-
278
- line = headers.zip(values).map { |(_, val)| fmt_cell(val, colorize, widths[_]) }
279
- lines << (format == :csv ? line.join(",") : line.join(separator))
280
- end
281
-
282
- # Output to console or file
283
- if file
284
- File.write(file, lines.join("\n") + "\n")
285
- else
286
- lines.each { |line| puts line }
287
- end
288
- end
289
-
290
- def import(path, format: :json)
291
- data = case format
292
- when :json
293
- JSON.parse(File.read(path), symbolize_names: true)
294
- when :yaml
295
- YAML.safe_load(File.read(path), symbolize_names: true)
296
- else
297
- raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
298
- end
299
-
300
- raise TypeError, "Expected parsed data to be a Hash" unless data.is_a?(Hash)
301
-
302
- @dimensions = data
303
- self
304
- end
305
-
306
- def export(path, format: :json)
307
- case format
308
- when :json
309
- File.write(path, JSON.pretty_generate(@dimensions))
310
- when :yaml
311
- File.write(path, YAML.dump(@dimensions))
312
- else
313
- raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
314
- end
315
- end
316
-
317
-
318
- def from_json(path)
319
- data = JSON.parse(File.read(path), symbolize_names: true)
320
- @dimensions = data
321
- end
322
-
323
- def from_yaml(path)
324
- data = YAML.safe_load(File.read(path), symbolize_names: true)
325
- @dimensions = data
326
- end
327
-
328
- private
329
-
330
- def reserved_struct_names
331
- (base_struct_methods = Struct.new(:dummy).methods(false) + Struct.new(:dummy).instance_methods(false)).uniq
332
- end
333
-
334
- def reserved_function_names
335
- (self.methods + self.class.instance_methods(false)).uniq
336
- end
21
+ include FlexCartesianCore
22
+ include FlexCartesianIO
23
+ include FlexCartesianUtilities
24
+ include FlexCartesianAnalyzer
25
+ include FlexCartesianDeprecations
26
+ include FlexCartesianVisualization
337
27
 
338
- def fmt_cell(value, colorize, width = nil)
339
- str = case value
340
- when String then value
341
- else value.inspect
342
- end
343
- str = str.ljust(width) if width
344
- colorize ? str.colorize(:cyan) : str
345
- end
346
28
  end
347
29
 
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module FlexCartesian
2
+ VERSION = "2.0.0.beta"
3
+ end