flex-cartesian 0.1.9 → 1.0
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 +13 -0
- data/README.md +173 -2
- data/lib/flex-cartesian.rb +131 -30
- metadata +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b8d43af751cdba0a5c141bab0606576291395a5cc3e3a8cb673bacc86c2920b
|
4
|
+
data.tar.gz: 96bfd43d1328e48d38665c2c4aa3b951cf24b932c510ab73ed704d7b7bf98c10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ff1dbb6deb67e9c9a997d8fb4cea7bb75f8fb4aa9ef8ade0a2452c413e19f97c8687f7816c68d47bef8711fe9cdec98159aae2d80ab468ca55512675f4fd5b3
|
7
|
+
data.tar.gz: 16764e02f4010d4c6431779f8cb2f9b7e11f6f04b97ac0c7546f5b1431e287936b92207a527a9838ab614aeed635f290e268961c235fbc83553eef4e448c2358
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 1.0 - 2025-07-14
|
4
|
+
### Added
|
5
|
+
- Optional flad for hiding function from output
|
6
|
+
|
7
|
+
### Fixed
|
8
|
+
- Simplified default parameters
|
9
|
+
- Unified umbrella method for handling functions
|
10
|
+
- Initialization directly from file of dimensions
|
11
|
+
|
12
|
+
## 0.2 - 2025-07-12
|
13
|
+
### Added
|
14
|
+
- Logical conditions on Cartesian space
|
15
|
+
|
3
16
|
## 0.1.9 - 2025-07-08
|
4
17
|
### Fixed
|
5
18
|
- Documentation
|
data/README.md
CHANGED
@@ -12,6 +12,8 @@
|
|
12
12
|
|
13
13
|
✅ Functions over Cartesian vectors are decoupled from dimensionality
|
14
14
|
|
15
|
+
✅ Define conditions on Cartesian combinations using `s.cond(:set) { |v| v.dim1 > v.dim2 } }` syntax
|
16
|
+
|
15
17
|
✅ Calculate over named dimensions using `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }` syntax
|
16
18
|
|
17
19
|
✅ Add functions over dimensions using `s.add_function { |v| v.dim1 + v.dim2 }` syntax
|
@@ -42,7 +44,7 @@ gem install flex-cartesian-*.gem
|
|
42
44
|
|
43
45
|
## Usage
|
44
46
|
|
45
|
-
```
|
47
|
+
```ruby
|
46
48
|
#!/usr/bin/ruby
|
47
49
|
|
48
50
|
require 'flex-cartesian'
|
@@ -51,6 +53,9 @@ require 'flex-cartesian'
|
|
51
53
|
|
52
54
|
# BASIC CONCEPTS
|
53
55
|
|
56
|
+
# 1. Cartesian object is a set of combinations of values of dimansions.
|
57
|
+
# 2. Dimensions always have names.
|
58
|
+
|
54
59
|
puts "\nDefine named dimensions"
|
55
60
|
example = {
|
56
61
|
dim1: [1, 2],
|
@@ -69,6 +74,12 @@ end
|
|
69
74
|
|
70
75
|
# ITERATION OVER CARTESIAN SPACE
|
71
76
|
|
77
|
+
# 3. Iterator is dimensionality-agnostic, that is, has a vector syntax that hides dimensions under the hood.
|
78
|
+
# This keeps foundational code intact, and isolates modifications in the iterator body 'do_something'.
|
79
|
+
# 4. For efficiency on VERY largse Cartesian spaces, there are
|
80
|
+
# a). lazy evaluation of each combination
|
81
|
+
# b). progress bar to track time-consuming calculations.
|
82
|
+
|
72
83
|
puts "\nIterate over all Cartesian combinations and execute action (dimensionality-agnostic style)"
|
73
84
|
s.cartesian { |v| do_something(v) }
|
74
85
|
|
@@ -85,6 +96,12 @@ s.cartesian(lazy: true).take(2).each { |v| do_something(v) }
|
|
85
96
|
|
86
97
|
# FUNCTIONS ON CARTESIAN SPACE
|
87
98
|
|
99
|
+
# 5. A function is a virtual dimension that is calculated based on a vector of base dimensions.
|
100
|
+
# You can think of a function as a scalar field defined on Cartesian space.
|
101
|
+
# 6. Functions are printed as virtual dimensions in .output method.
|
102
|
+
# 7. However, functions remains virtual construct, and their values can't be referenced by name
|
103
|
+
# (unlike regular dimensions). Also, functions do not add to .size of Cartesian space.
|
104
|
+
|
88
105
|
puts "\nAdd function 'triple'"
|
89
106
|
puts "Note: function is visualized in .output as a new dimension"
|
90
107
|
s.add_function(:triple) { |v| v.dim1 * 3 + (v.dim3 ? 1: 0) }
|
@@ -97,6 +114,28 @@ s.remove_function(:test)
|
|
97
114
|
|
98
115
|
|
99
116
|
|
117
|
+
# CONDITIONS ON CARTESIAN SPACE
|
118
|
+
|
119
|
+
# 8. A condition is a logical restriction of allowed combitnations for Cartesian space.
|
120
|
+
# 9. Using conditions, you can take a slice of Cartesian space.
|
121
|
+
# In particular, you can reflect semantical dependency of dimensional values.
|
122
|
+
|
123
|
+
puts "Build Cartesian space that includes only odd values of 'dim1' dimension"
|
124
|
+
s.cond(:set) { |v| v.dim1.odd? }
|
125
|
+
puts "print all the conditions in format 'index | condition '"
|
126
|
+
s.cond
|
127
|
+
puts "Test the condition: print the updated Cartesian space"
|
128
|
+
s.output
|
129
|
+
puts "Test the condition: check the updated size of Cartesian space"
|
130
|
+
puts "New size: #{s.size}"
|
131
|
+
puts "Clear condition #0"
|
132
|
+
s.cond(:unset, index: 0)
|
133
|
+
puts "Clear all conditions"
|
134
|
+
s.cond(:clear)
|
135
|
+
puts "Restored size without conditions: #{s.size}"
|
136
|
+
|
137
|
+
|
138
|
+
|
100
139
|
# PRINT
|
101
140
|
|
102
141
|
puts "\nPrint Cartesian space as plain table, all functions included"
|
@@ -125,7 +164,7 @@ s.export('example.yaml', format: :yaml)
|
|
125
164
|
# UTILITIES
|
126
165
|
|
127
166
|
puts "\nGet number of Cartesian combinations"
|
128
|
-
puts "Note: .size counts only
|
167
|
+
puts "Note: .size counts only dimensions, it ignores virtual constructs (functions, conditions, etc.)"
|
129
168
|
puts "Total size of Cartesian space: #{s.size}"
|
130
169
|
|
131
170
|
puts "\nPartially converting Cartesian space to array:"
|
@@ -133,7 +172,120 @@ array = s.to_a(limit: 3)
|
|
133
172
|
puts array.inspect
|
134
173
|
```
|
135
174
|
|
175
|
+
## Example
|
176
|
+
|
177
|
+
The most common use case for FlexCartesian is sweep analysis, that is, analysis of target value on all possible combinations of its parameters.
|
178
|
+
FlexCartesian has been designed to provide a concise form for sweep analysis:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
require 'flex-cartesian'
|
182
|
+
|
183
|
+
# create Cartesian space from JSON describing input parameters
|
184
|
+
s = FlexCartesian.new(path: './config.json')
|
185
|
+
|
186
|
+
# Define the values we want to calculate on all possible combinations of parameters
|
187
|
+
s.func(:add, :cmd) { |v| v.threads * v.batch }
|
188
|
+
s.func(:add, :performance) { |v| v.cmd / 3 }
|
189
|
+
|
190
|
+
# Calculate
|
191
|
+
s.func(:run)
|
192
|
+
|
193
|
+
# Save result as CSV, to easily open it in any business analytics tool
|
194
|
+
s.output(format: :csv, file: './benchmark.csv')
|
195
|
+
# For convenience, print result to the terminal
|
196
|
+
s.output
|
197
|
+
```
|
198
|
+
|
199
|
+
As this code is a little artificial, let us build real-world example.
|
200
|
+
Perhaps, we want to analyze PING perfomance from our machine to several DNS providers: Google DNS, CloudFlare DNS, and Cisco DNS.
|
201
|
+
For each of those services, we would like to know:
|
202
|
+
|
203
|
+
- What is our ping time?
|
204
|
+
- How does ping scale by packet size?
|
205
|
+
- How does ping statistics vary based on count of pings?
|
206
|
+
|
207
|
+
These input parameters form the following dimensions.
|
208
|
+
|
209
|
+
```json
|
210
|
+
{
|
211
|
+
"count": [2, 4],
|
212
|
+
"size": [32, 64],
|
213
|
+
"target": [
|
214
|
+
"8.8.8.8", // Google DNS
|
215
|
+
"1.1.1.1", // Cloudflare DNS
|
216
|
+
"208.67.222.222" // Cisco OpenDNS
|
217
|
+
]
|
218
|
+
}
|
219
|
+
```
|
220
|
+
|
221
|
+
Note that '//' isn't officially supported by JSON, and you may want to remove the comments if you experience parser errors.
|
222
|
+
Let us build the code to run over these parameters.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
require 'flex-cartesian'
|
226
|
+
|
227
|
+
s = FlexCartesian.new(path: './ping_config.json') # file with the parameters as given above
|
228
|
+
|
229
|
+
result = {} # here we will store raw result of each ping and fetch target metrics from it
|
230
|
+
|
231
|
+
# this function shows actual ping command
|
232
|
+
s.func(:add, :command) do |v|
|
233
|
+
"ping -c #{v.count} -s #{v.size} #{v.target}"
|
234
|
+
end
|
235
|
+
|
236
|
+
# this function gets raw result of actual ping command
|
237
|
+
s.func(:add, :raw_ping, hide: true) do |v|
|
238
|
+
result[v.command] ||= `#{v.command} 2>&1`
|
239
|
+
end
|
240
|
+
|
241
|
+
# this function extracts ping time
|
242
|
+
s.func(:add, :time) do |v|
|
243
|
+
if v.raw_ping =~ /min\/avg\/max\/(?:mdev|stddev) = [^\/]+\/([^\/]+)/
|
244
|
+
$1.to_f
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# this function extracts minimum ping time
|
249
|
+
s.func(:add, :min) do |v|
|
250
|
+
if v.raw_ping =~ /min\/avg\/max\/(?:mdev|stddev) = ([^\/]+)/
|
251
|
+
$1.to_f
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# funally, this function extracts losses of ping
|
256
|
+
s.func(:add, :loss) do |v|
|
257
|
+
if v.raw_ping =~ /(\d+(?:\.\d+)?)% packet loss/
|
258
|
+
$1.to_f
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# this is the spinal axis of FlexCartesian:
|
263
|
+
# calculate all functions on the entire Cartesian space of parameters aka dimensions
|
264
|
+
s.func(:run)
|
265
|
+
|
266
|
+
# save benchmark results to CSV for convenient analysis in BI tools
|
267
|
+
s.output(format: :csv, file: './benchmark.csv')
|
268
|
+
|
269
|
+
# for convenience, show tabular result on screen as well
|
270
|
+
s.output(colorize: true)
|
271
|
+
```
|
272
|
+
|
273
|
+
This code is 100% practical and illustrative. You can benchmark:
|
274
|
+
|
275
|
+
- Local block devices using 'dd'
|
276
|
+
- GPU-to-Storage connection using 'gdsio'
|
277
|
+
- Local file systems using FS-based utilities
|
278
|
+
- Local CPU RAM using RAM disk or specialized benchmarks for CPU RAM
|
279
|
+
- Database performance using SQL client or non-SQL client utilities
|
280
|
+
- Performance of object storage of cloud providers, be it AWS S3, OCI Object Storage, or anything else
|
281
|
+
- Performance of any AI model, from simplistic YOLO to heavy-weight LLM such as LLAMA, Cohere, or DeepSeek
|
282
|
+
- ... Any other target application or service
|
283
|
+
|
284
|
+
In any use case, FlexCartesian will unfold complete landscape of the target performance over all configurable parameters.
|
285
|
+
As result, you will be able to spot optimal configurations, correlations, bottlenecks, and sweet spots.
|
286
|
+
Moreover, you will make your conclusions in a justifiable way.
|
136
287
|
|
288
|
+
Here is an example of how I used FlexCartesian to [analyze optimal performance/cost of YOLO](https://www.linkedin.com/pulse/comparing-gpu-a10-ampere-a1-shapes-object-oci-yuri-rassokhin-rseqf).
|
137
289
|
|
138
290
|
## API Overview
|
139
291
|
|
@@ -278,6 +430,25 @@ Example:
|
|
278
430
|
s.cartesian { |v| v.output(colorize: true, align: false) }
|
279
431
|
```
|
280
432
|
|
433
|
+
---
|
434
|
+
|
435
|
+
### Conditions on Cartesian Space
|
436
|
+
```ruby
|
437
|
+
cond(command = :print, # or :set, :unset, :clear
|
438
|
+
index: nil, # index of a conditions to unset
|
439
|
+
&block # defintiion of the condition to set
|
440
|
+
)
|
441
|
+
```
|
442
|
+
Example:
|
443
|
+
```ruby
|
444
|
+
s.cond(:set) { |v| v.dim1 > v.dim3 }
|
445
|
+
s.cond # defaults to s.cond(:print) and shows all the conditions in the form 'index | definition'
|
446
|
+
s.cond(:unset, 0) # remove previously set condition
|
447
|
+
s.cond(:clear) # remove all conditions, if any
|
448
|
+
```
|
449
|
+
|
450
|
+
|
451
|
+
|
281
452
|
## License
|
282
453
|
|
283
454
|
This project is licensed under the terms of the GNU General Public License v3.0.
|
data/lib/flex-cartesian.rb
CHANGED
@@ -3,6 +3,8 @@ require 'progressbar'
|
|
3
3
|
require 'colorize'
|
4
4
|
require 'json'
|
5
5
|
require 'yaml'
|
6
|
+
require 'method_source'
|
7
|
+
require 'set'
|
6
8
|
|
7
9
|
module FlexOutput
|
8
10
|
def output(separator: " | ", colorize: false, align: true)
|
@@ -25,11 +27,77 @@ end
|
|
25
27
|
class FlexCartesian
|
26
28
|
attr :dimensions
|
27
29
|
|
28
|
-
def initialize(dimensions = nil)
|
30
|
+
def initialize(dimensions = nil, path: nil, format: :json)
|
31
|
+
if dimensions && path
|
32
|
+
$logger.msg "Please specify either dimensions or path to dimensions", :error
|
33
|
+
end
|
29
34
|
@dimensions = dimensions
|
35
|
+
@conditions = []
|
30
36
|
@derived = {}
|
37
|
+
@function_results = {} # key: Struct instance.object_id => { fname => value }
|
38
|
+
@function_hidden = Set.new
|
39
|
+
import(path, format: format) if path
|
40
|
+
end
|
41
|
+
|
42
|
+
def cond(command = :print, index: nil, &block)
|
43
|
+
case command
|
44
|
+
when :set
|
45
|
+
raise ArgumentError, "Block required" unless block_given?
|
46
|
+
@conditions << block
|
47
|
+
self
|
48
|
+
when :unset
|
49
|
+
raise ArgumentError, "Index of the condition required" unless index
|
50
|
+
@conditions.delete_at(index)
|
51
|
+
when :clear
|
52
|
+
@conditions.clear
|
53
|
+
self
|
54
|
+
when :print
|
55
|
+
return if @conditions.empty?
|
56
|
+
@conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
|
57
|
+
else
|
58
|
+
raise ArgumentError, "unknown condition command: #{command}"
|
59
|
+
end
|
31
60
|
end
|
32
61
|
|
62
|
+
def func(command = :print, name = nil, hide: false, &block)
|
63
|
+
case command
|
64
|
+
when :add
|
65
|
+
raise ArgumentError, "Function name and block required for :add" unless name && block_given?
|
66
|
+
add_function(name, &block)
|
67
|
+
@function_hidden.delete(name.to_sym)
|
68
|
+
@function_hidden << name.to_sym if hide
|
69
|
+
|
70
|
+
when :del
|
71
|
+
raise ArgumentError, "Function name required for :del" unless name
|
72
|
+
remove_function(name)
|
73
|
+
|
74
|
+
when :print
|
75
|
+
if @derived.empty?
|
76
|
+
puts "(no functions defined)"
|
77
|
+
else
|
78
|
+
@derived.each do |fname, fblock|
|
79
|
+
source = fblock.source rescue '(source unavailable)'
|
80
|
+
|
81
|
+
body = source.sub(/^.*?\s(?=(\{|\bdo\b))/, '').strip
|
82
|
+
|
83
|
+
puts " #{fname.inspect.ljust(12)}| #{body}#{@function_hidden.include?(fname) ? ' [HIDDEN]' : ''}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
when :run
|
88
|
+
@function_results = {}
|
89
|
+
cartesian do |v|
|
90
|
+
@function_results[v] ||= {}
|
91
|
+
@derived.each do |fname, block|
|
92
|
+
@function_results[v][fname] = block.call(v)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
else
|
97
|
+
raise ArgumentError, "Unknown command for function: #{command.inspect}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
33
101
|
def add_function(name, &block)
|
34
102
|
raise ArgumentError, "Block required" unless block_given?
|
35
103
|
@derived[name.to_sym] = block
|
@@ -61,18 +129,26 @@ class FlexCartesian
|
|
61
129
|
struct_instance.define_singleton_method(name) { block.call(struct_instance) }
|
62
130
|
end
|
63
131
|
|
132
|
+
next if @conditions.any? { |cond| !cond.call(struct_instance) }
|
133
|
+
|
64
134
|
yield struct_instance
|
65
135
|
end
|
66
136
|
end
|
67
137
|
|
68
|
-
def size
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
138
|
+
def size
|
139
|
+
return 0 unless @dimensions.is_a?(Hash)
|
140
|
+
if @conditions.empty?
|
141
|
+
values = @dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
|
142
|
+
return 0 if values.any?(&:empty?)
|
143
|
+
values.map(&:size).inject(1, :*)
|
144
|
+
else
|
145
|
+
size = 0
|
146
|
+
cartesian do |v|
|
147
|
+
next if @conditions.any? { |cond| !cond.call(v) }
|
148
|
+
size += 1
|
149
|
+
end
|
150
|
+
size
|
151
|
+
end
|
76
152
|
end
|
77
153
|
|
78
154
|
def to_a(limit: nil)
|
@@ -84,46 +160,71 @@ end
|
|
84
160
|
result
|
85
161
|
end
|
86
162
|
|
87
|
-
def progress_each(
|
88
|
-
|
89
|
-
bar = ProgressBar.create(title: title, total: total, format: '%t [%B] %p%% %e')
|
163
|
+
def progress_each(lazy: false, title: "Processing")
|
164
|
+
bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')
|
90
165
|
|
91
|
-
cartesian(
|
166
|
+
cartesian(@dimensions, lazy: lazy) do |v|
|
92
167
|
yield v
|
93
168
|
bar.increment
|
94
169
|
end
|
95
170
|
end
|
96
171
|
|
97
|
-
|
98
|
-
rows =
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
172
|
+
def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil)
|
173
|
+
rows = if @function_results && !@function_results.empty?
|
174
|
+
@function_results.keys
|
175
|
+
else
|
176
|
+
result = []
|
177
|
+
cartesian do |v|
|
178
|
+
result << v
|
179
|
+
break if limit && result.size >= limit
|
180
|
+
end
|
181
|
+
result
|
182
|
+
end
|
183
|
+
|
103
184
|
return if rows.empty?
|
104
185
|
|
105
|
-
|
106
|
-
|
107
|
-
rows.first.singleton_methods(false).reject { |m| m.to_s.start_with?('__') }
|
108
|
-
).map(&:to_s)
|
186
|
+
visible_func_names = @derived.keys - (@function_hidden || Set.new).to_a
|
187
|
+
headers = rows.first.members.map(&:to_s) + visible_func_names.map(&:to_s)
|
109
188
|
|
110
189
|
widths = align ? headers.to_h { |h|
|
111
|
-
|
190
|
+
values = rows.map do |r|
|
191
|
+
val = if r.members.map(&:to_s).include?(h)
|
192
|
+
r.send(h)
|
193
|
+
else
|
194
|
+
@function_results&.dig(r, h.to_sym)
|
195
|
+
end
|
196
|
+
fmt_cell(val, false).size
|
197
|
+
end
|
198
|
+
[h, [h.size, *values].max]
|
112
199
|
} : {}
|
113
200
|
|
201
|
+
lines = []
|
202
|
+
|
203
|
+
# Header
|
114
204
|
case format
|
115
205
|
when :markdown
|
116
|
-
|
117
|
-
|
206
|
+
lines << "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
|
207
|
+
lines << "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
|
118
208
|
when :csv
|
119
|
-
|
209
|
+
lines << headers.join(",")
|
120
210
|
else
|
121
|
-
|
211
|
+
lines << headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(separator)
|
122
212
|
end
|
123
213
|
|
214
|
+
# Rows
|
124
215
|
rows.each do |row|
|
125
|
-
|
126
|
-
|
216
|
+
values = row.members.map { |m| row.send(m) } +
|
217
|
+
visible_func_names.map { |fname| @function_results&.dig(row, fname) }
|
218
|
+
|
219
|
+
line = headers.zip(values).map { |(_, val)| fmt_cell(val, colorize, widths[_]) }
|
220
|
+
lines << (format == :csv ? line.join(",") : line.join(separator))
|
221
|
+
end
|
222
|
+
|
223
|
+
# Output to console or file
|
224
|
+
if file
|
225
|
+
File.write(file, lines.join("\n") + "\n")
|
226
|
+
else
|
227
|
+
lines.each { |line| puts line }
|
127
228
|
end
|
128
229
|
end
|
129
230
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flex-cartesian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: '1.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yury Rassokhin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -52,10 +52,24 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: method_source
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
55
69
|
description: 'Flexible and human-friendly Cartesian product enumerator for Ruby. Supports
|
56
|
-
functions on cartesian, dimensionality-agnostic/dimensionality-aware
|
57
|
-
named dimensions, tabular output, lazy/eager evaluation, progress bar,
|
58
|
-
JSON/YAML, and export to Markdown/CSV. Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#
|
70
|
+
functions and conditions on cartesian, dimensionality-agnostic/dimensionality-aware
|
71
|
+
iterators, named dimensions, tabular output, lazy/eager evaluation, progress bar,
|
72
|
+
import from JSON/YAML, and export to Markdown/CSV. Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#example'
|
59
73
|
email:
|
60
74
|
- yuri.rassokhin@gmail.com
|
61
75
|
executables: []
|
@@ -80,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
94
|
requirements:
|
81
95
|
- - ">="
|
82
96
|
- !ruby/object:Gem::Version
|
83
|
-
version: '0'
|
97
|
+
version: '3.0'
|
84
98
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
99
|
requirements:
|
86
100
|
- - ">="
|