flex-cartesian 1.3.1 → 2.0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -1
- data/LICENSE +6 -0
- data/README.md +133 -439
- data/lib/analyzer.rb +48 -0
- data/lib/analyzers/morris.rb +268 -0
- data/lib/flex-cartesian/flex-cartesian-analyzer.rb +16 -0
- data/lib/flex-cartesian/flex-cartesian-core.rb +624 -0
- data/lib/flex-cartesian/flex-cartesian-deprecations.rb +13 -0
- data/lib/flex-cartesian/flex-cartesian-io.rb +192 -0
- data/lib/flex-cartesian/flex-cartesian-utilities.rb +126 -0
- data/lib/flex-cartesian.rb +13 -336
- data/lib/version.rb +3 -0
- data/lib/visualization/html.rb +217 -0
- metadata +49 -26
|
@@ -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
|
+
|
data/lib/flex-cartesian.rb
CHANGED
|
@@ -5,348 +5,25 @@ require 'json'
|
|
|
5
5
|
require 'yaml'
|
|
6
6
|
require 'method_source'
|
|
7
7
|
require 'set'
|
|
8
|
+
require 'logger'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
sep = if format == :csv
|
|
233
|
-
[";", ","].include?(separator) ? separator : ";"
|
|
234
|
-
else
|
|
235
|
-
separator
|
|
236
|
-
end
|
|
237
|
-
rows = if @function_results && !@function_results.empty?
|
|
238
|
-
@function_results.keys
|
|
239
|
-
else
|
|
240
|
-
result = []
|
|
241
|
-
cartesian do |v|
|
|
242
|
-
result << v
|
|
243
|
-
break if limit && result.size >= limit
|
|
244
|
-
end
|
|
245
|
-
result
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
return if rows.empty?
|
|
249
|
-
|
|
250
|
-
visible_func_names = @derived.keys - (@function_hidden || Set.new).to_a
|
|
251
|
-
headers = rows.first.members.map(&:to_s) + visible_func_names.map(&:to_s)
|
|
252
|
-
|
|
253
|
-
widths = align ? headers.to_h { |h|
|
|
254
|
-
values = rows.map do |r|
|
|
255
|
-
val = if r.members.map(&:to_s).include?(h)
|
|
256
|
-
r.send(h)
|
|
257
|
-
else
|
|
258
|
-
@function_results&.dig(r, h.to_sym)
|
|
259
|
-
end
|
|
260
|
-
fmt_cell(val, false).size
|
|
261
|
-
end
|
|
262
|
-
[h, [h.size, *values].max]
|
|
263
|
-
} : {}
|
|
264
|
-
|
|
265
|
-
lines = []
|
|
266
|
-
|
|
267
|
-
# Header
|
|
268
|
-
case format
|
|
269
|
-
when :markdown
|
|
270
|
-
lines << "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
|
|
271
|
-
lines << "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
|
|
272
|
-
when :csv
|
|
273
|
-
lines << headers.join(sep)
|
|
274
|
-
else
|
|
275
|
-
lines << headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(sep)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Rows
|
|
279
|
-
rows.each do |row|
|
|
280
|
-
values = row.members.map { |m| row.send(m) } +
|
|
281
|
-
visible_func_names.map { |fname| @function_results&.dig(row, fname) }
|
|
282
|
-
|
|
283
|
-
line = headers.zip(values).map { |(_, val)| fmt_cell(val, colorize, widths[_]) }
|
|
284
|
-
lines << line.join(sep)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Output to console or file
|
|
288
|
-
if file
|
|
289
|
-
File.write(file, lines.join("\n") + "\n")
|
|
290
|
-
else
|
|
291
|
-
lines.each { |line| puts line }
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def import(path, format: :json)
|
|
296
|
-
data = case format
|
|
297
|
-
when :json
|
|
298
|
-
JSON.parse(File.read(path), symbolize_names: true)
|
|
299
|
-
when :yaml
|
|
300
|
-
YAML.safe_load(File.read(path), symbolize_names: true)
|
|
301
|
-
else
|
|
302
|
-
raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
raise TypeError, "Expected parsed data to be a Hash" unless data.is_a?(Hash)
|
|
306
|
-
|
|
307
|
-
@dimensions = data
|
|
308
|
-
self
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def export(path, format: :json)
|
|
312
|
-
case format
|
|
313
|
-
when :json
|
|
314
|
-
File.write(path, JSON.pretty_generate(@dimensions))
|
|
315
|
-
when :yaml
|
|
316
|
-
File.write(path, YAML.dump(@dimensions))
|
|
317
|
-
else
|
|
318
|
-
raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def from_json(path)
|
|
324
|
-
data = JSON.parse(File.read(path), symbolize_names: true)
|
|
325
|
-
@dimensions = data
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def from_yaml(path)
|
|
329
|
-
data = YAML.safe_load(File.read(path), symbolize_names: true)
|
|
330
|
-
@dimensions = data
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
private
|
|
334
|
-
|
|
335
|
-
def reserved_struct_names
|
|
336
|
-
(base_struct_methods = Struct.new(:dummy).methods(false) + Struct.new(:dummy).instance_methods(false)).uniq
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def reserved_function_names
|
|
340
|
-
(self.methods + self.class.instance_methods(false)).uniq
|
|
341
|
-
end
|
|
21
|
+
include FlexCartesianCore
|
|
22
|
+
include FlexCartesianIO
|
|
23
|
+
include FlexCartesianUtilities
|
|
24
|
+
include FlexCartesianAnalyzer
|
|
25
|
+
include FlexCartesianDeprecations
|
|
26
|
+
include FlexCartesianVisualization
|
|
342
27
|
|
|
343
|
-
def fmt_cell(value, colorize, width = nil)
|
|
344
|
-
str = case value
|
|
345
|
-
when String then value
|
|
346
|
-
else value.inspect
|
|
347
|
-
end
|
|
348
|
-
str = str.ljust(width) if width
|
|
349
|
-
colorize ? str.colorize(:cyan) : str
|
|
350
|
-
end
|
|
351
28
|
end
|
|
352
29
|
|
data/lib/version.rb
ADDED