flex-cartesian 0.2 → 1.1
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 +225 -60
- data/lib/flex-cartesian.rb +90 -18
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b89541266e2ac802592bc652ddbf2edffe1aea4e47ab1d732e2623ad2cc5835b
|
4
|
+
data.tar.gz: d717084b70f95af0e3f5706a426731685cc0cc7ddcba038b15b662d2d3226b3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26b83b8486d89c151ae8fbc23b7ed9c19f5417a582fd6843b9e654264f0e1deb2615686fe6872b053252c9059a843ca416c41a97ccbc546de3da32b0228b7c61
|
7
|
+
data.tar.gz: aa43ccd2936336564a8b14e70a6142167a17db9b93e646ea56445f25b477cd95c8a219fdeac383801c71ea4a07210c00fb1ecbd67d51f18e97afe7ad7b01c8da
|
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
@@ -2,25 +2,23 @@
|
|
2
2
|
|
3
3
|
**Ruby implementation of flexible and human-friendly operations on Cartesian products**
|
4
4
|
|
5
|
-
|
6
|
-
|
7
5
|
## Features
|
8
6
|
|
9
7
|
✅ Named dimensions with arbitrary keys
|
10
8
|
|
11
|
-
✅ Enumerate over Cartesian
|
9
|
+
✅ Enumerate over Cartesian space with a single block argument
|
12
10
|
|
13
|
-
✅
|
11
|
+
✅ Actions on Cartesian are decoupled from dimensionality: `s.cartesian { |v| do_something(v) }`
|
14
12
|
|
15
|
-
✅
|
13
|
+
✅ Conditions for Cartesian space: `s.cond(:set) { |v| v.dim1 > v.dim2 } }`
|
16
14
|
|
17
|
-
✅
|
15
|
+
✅ Calculation over named dimensions: `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }`
|
18
16
|
|
19
|
-
✅
|
17
|
+
✅ Functions on Cartesian space: `s.func(:add, :my_sum) { |v| v.dim1 + v.dim2 }`
|
20
18
|
|
21
19
|
✅ Lazy and eager evaluation
|
22
20
|
|
23
|
-
✅ Progress bars for large Cartesian
|
21
|
+
✅ Progress bars for large Cartesian spaces
|
24
22
|
|
25
23
|
✅ Export of Cartesian space to Markdown or CSV
|
26
24
|
|
@@ -30,7 +28,77 @@
|
|
30
28
|
|
31
29
|
✅ Structured and colorized terminal output
|
32
30
|
|
31
|
+
## Use Cases
|
32
|
+
|
33
|
+
`FlexCartesian` is especially useful in the following scenarios.
|
34
|
+
|
35
|
+
### 1. Sweep Analysis of Performance
|
36
|
+
|
37
|
+
Systematically evaluate an application or algorithm across all combinations of parameters:
|
38
|
+
|
39
|
+
- Parameters: `threads`, `batch_size`, `backend`, etc
|
40
|
+
- Metrics: `throughput`, `latency`, `memory`
|
41
|
+
- Output: CSV or Markdown tables
|
42
|
+
|
43
|
+
### 2. Hyperparameter Tuning for ML Models
|
44
|
+
|
45
|
+
Iterate over all combinations of hyperparameters:
|
46
|
+
|
47
|
+
- Examples: `learning_rate`, `max_depth`, `subsample`, `n_estimators`
|
48
|
+
- With constraints (e.g., `max_depth < 10 if learning_rate > 0.1`)
|
49
|
+
- With computed evaluation metrics like `accuracy`, `AUC`, etc
|
50
|
+
|
51
|
+
### 3. Infrastructure and System Configuration
|
52
|
+
|
53
|
+
Generate all valid infrastructure configurations:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
region: ["us-west", "eu-central"]
|
57
|
+
tier: ["basic", "pro"]
|
58
|
+
replicas: [1, 3, 5]
|
59
|
+
```
|
60
|
+
|
61
|
+
With conditions like "basic tier cannot have more than one replica:
|
62
|
+
```ruby
|
63
|
+
s.cond(:set) { |v| (v.tier == "basic" ? v.replicas == 1 : true) }
|
64
|
+
```
|
65
|
+
|
66
|
+
### 4. Mass Testing of CLI Commands
|
67
|
+
Generate and benchmark all valid CLI calls:
|
68
|
+
|
69
|
+
```bash
|
70
|
+
myapp --threads=4 --batch=32 --backend=torch
|
71
|
+
```
|
72
|
+
|
73
|
+
Capture runtime, output, errors, etc.
|
74
|
+
|
75
|
+
### 5. Input Generation for UI/API Testing
|
76
|
+
Automatically cover input parameter spaces for:
|
77
|
+
|
78
|
+
- HTTP methods: ["GET", "POST"]
|
79
|
+
- User roles: ["guest", "user", "admin"]
|
80
|
+
- Language settings: ["en", "fr", "de"]
|
81
|
+
|
82
|
+
### 6. Scientific and Engineering Simulations
|
83
|
+
Generate multidimensional experimental spaces for:
|
33
84
|
|
85
|
+
- Physics simulations
|
86
|
+
- Bioinformatics parameter sweeps
|
87
|
+
- Network behavior modeling, etc
|
88
|
+
|
89
|
+
### 7. Structured Reporting and Visualization
|
90
|
+
Output Cartesian data as:
|
91
|
+
|
92
|
+
- Markdown (for GitHub rendering)
|
93
|
+
- CSV (for Excel, Google Sheets, and more advanced BI tools)
|
94
|
+
- Plain text (for CLI previews)
|
95
|
+
|
96
|
+
### 8. Test Case Generation
|
97
|
+
Use it to drive automated test inputs for:
|
98
|
+
|
99
|
+
- RSpec shared examples
|
100
|
+
- Minitest table-driven tests
|
101
|
+
- PyTest parameterization
|
34
102
|
|
35
103
|
## Installation
|
36
104
|
|
@@ -98,25 +166,24 @@ s.cartesian(lazy: true).take(2).each { |v| do_something(v) }
|
|
98
166
|
|
99
167
|
# 5. A function is a virtual dimension that is calculated based on a vector of base dimensions.
|
100
168
|
# You can think of a function as a scalar field defined on Cartesian space.
|
101
|
-
# 6. Functions are printed as virtual dimensions in
|
102
|
-
# 7.
|
103
|
-
# (unlike regular dimensions). Also, functions do not add to .size of Cartesian space.
|
169
|
+
# 6. Functions are printed as virtual dimensions in `.output`.
|
170
|
+
# 7. Functions do not add to `.size` of Cartesian space.
|
104
171
|
|
105
172
|
puts "\nAdd function 'triple'"
|
106
173
|
puts "Note: function is visualized in .output as a new dimension"
|
107
|
-
s.
|
108
|
-
|
174
|
+
s.func(:add, :triple) { |v| v.dim1 * 3 + (v.dim3 ? 1: 0) }
|
175
|
+
s.func(:run)
|
109
176
|
s.output
|
110
177
|
|
111
178
|
puts "\Add and then remove function 'test'"
|
112
|
-
s.
|
113
|
-
s.
|
179
|
+
s.func(:add, :test) { |v| v.dim3.to_i }
|
180
|
+
s.func(:del, :test)
|
114
181
|
|
115
182
|
|
116
183
|
|
117
184
|
# CONDITIONS ON CARTESIAN SPACE
|
118
185
|
|
119
|
-
# 8. A condition is a logical
|
186
|
+
# 8. A condition is a logical constraint for allowed combitnations of Cartesian space.
|
120
187
|
# 9. Using conditions, you can take a slice of Cartesian space.
|
121
188
|
# In particular, you can reflect semantical dependency of dimensional values.
|
122
189
|
|
@@ -140,10 +207,8 @@ puts "Restored size without conditions: #{s.size}"
|
|
140
207
|
|
141
208
|
puts "\nPrint Cartesian space as plain table, all functions included"
|
142
209
|
s.output
|
143
|
-
|
144
210
|
puts "\nPrint Cartesian space as Markdown"
|
145
211
|
s.output(format: :markdown)
|
146
|
-
|
147
212
|
puts "\nPrint Cartesian space as CSV"
|
148
213
|
s.output(format: :csv)
|
149
214
|
|
@@ -155,7 +220,6 @@ puts "\nImport Cartesian space from JSON (similar method for YAML)"
|
|
155
220
|
File.write('example.json', JSON.pretty_generate(example))
|
156
221
|
puts "\nNote: after import, all assigned functions will calculate again, and they appear in the output"
|
157
222
|
s.import('example.json').output
|
158
|
-
|
159
223
|
puts "\nExport Cartesian space to YAML (similar method for JSON)"
|
160
224
|
s.export('example.yaml', format: :yaml)
|
161
225
|
|
@@ -166,21 +230,115 @@ s.export('example.yaml', format: :yaml)
|
|
166
230
|
puts "\nGet number of Cartesian combinations"
|
167
231
|
puts "Note: .size counts only dimensions, it ignores virtual constructs (functions, conditions, etc.)"
|
168
232
|
puts "Total size of Cartesian space: #{s.size}"
|
169
|
-
|
170
233
|
puts "\nPartially converting Cartesian space to array:"
|
171
234
|
array = s.to_a(limit: 3)
|
172
235
|
puts array.inspect
|
173
236
|
```
|
174
237
|
|
238
|
+
## Example
|
239
|
+
|
240
|
+
The most common use case for FlexCartesian is sweep analysis, that is, analysis of target value on all possible combinations of its parameters.
|
241
|
+
FlexCartesian has been designed to provide a concise form for sweep analysis:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
require 'flex-cartesian'
|
245
|
+
|
246
|
+
# create Cartesian space from JSON describing input parameters
|
247
|
+
s = FlexCartesian.new(path: './config.json')
|
248
|
+
|
249
|
+
# Define the values we want to calculate on all possible combinations of parameters
|
250
|
+
s.func(:add, :cmd) { |v| v.threads * v.batch }
|
251
|
+
s.func(:add, :performance) { |v| v.cmd / 3 }
|
252
|
+
|
253
|
+
# Calculate
|
254
|
+
s.func(:run)
|
255
|
+
|
256
|
+
# Save result as CSV, to easily open it in any business analytics tool
|
257
|
+
s.output(format: :csv, file: './benchmark.csv')
|
258
|
+
# For convenience, print result to the terminal
|
259
|
+
s.output
|
260
|
+
```
|
261
|
+
|
262
|
+
As this code is a little artificial, let us build real-world example.
|
263
|
+
Perhaps, we want to analyze PING perfomance from our machine to several DNS providers: Google DNS, CloudFlare DNS, and Cisco DNS.
|
264
|
+
For each of those services, we would like to know:
|
265
|
+
|
266
|
+
- What is our ping time?
|
267
|
+
- How does ping scale by packet size?
|
268
|
+
- How does ping statistics vary based on count of pings?
|
269
|
+
|
270
|
+
These input parameters form the following dimensions.
|
271
|
+
|
272
|
+
```json
|
273
|
+
{
|
274
|
+
"count": [2, 4],
|
275
|
+
"size": [32, 64],
|
276
|
+
"target": [
|
277
|
+
"8.8.8.8", // Google DNS
|
278
|
+
"1.1.1.1", // Cloudflare DNS
|
279
|
+
"208.67.222.222" // Cisco OpenDNS
|
280
|
+
]
|
281
|
+
}
|
282
|
+
```
|
283
|
+
|
284
|
+
Note that `//` isn't officially supported by JSON, and you may want to remove the comments if you experience parser errors.
|
285
|
+
Let us build the code to run over these parameters.
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
require 'flex-cartesian'
|
289
|
+
|
290
|
+
s = FlexCartesian.new(path: './ping_parameters.json') # file with the parameters as given above
|
291
|
+
|
292
|
+
result = {} # raw result of each ping
|
293
|
+
|
294
|
+
s.func(:add, :command) { |v| "ping -c #{v.count} -s #{v.size} #{v.target}" } # ping command
|
295
|
+
s.func(:add, :raw_ping, hide: true) { |v| result[v.command] ||= `#{v.command} 2>&1` } # capturing ping result
|
296
|
+
s.func(:add, :time) { |v| v.raw_ping[/min\/avg\/max\/(?:mdev|stddev) = [^\/]+\/([^\/]+)/, 1]&.to_f } # fetch ping time from result
|
297
|
+
s.func(:add, :min) { |v| v.raw_ping[/min\/avg\/max\/(?:mdev|stddev) = ([^\/]+)/, 1]&.to_f } # fetch min time from result
|
298
|
+
s.func(:add, :loss) { |v| v.raw_ping[/(\d+(?:\.\d+)?)% packet loss/, 1]&.to_f } # fetch ping loss from result
|
299
|
+
|
300
|
+
s.func(:run) # Sweep analysis! Benchmark all possible combinations of parameters
|
301
|
+
|
302
|
+
s.output(format: :csv, file: './result.csv') # save benchmark result as CSV
|
303
|
+
|
304
|
+
s.output(colorize: true) # for convenience, show result in terminal
|
305
|
+
```
|
306
|
+
|
307
|
+
If you run the code, after a while it will generate benchmark results on the screen:
|
308
|
+
|
309
|
+

|
310
|
+
|
311
|
+
Additionally, CSV version of this result is saved as `./benchmark.csv`
|
312
|
+
|
313
|
+
The PING benchmarking code above is 100% practical and illustrative.
|
314
|
+
You can modify it and benchmark virtually anything:
|
315
|
+
|
316
|
+
- Local block devices using `dd`
|
317
|
+
- GPU-to-Storage connection using `gdsio`
|
318
|
+
- Local file systems using FS-based utilities
|
319
|
+
- Local CPU RAM using RAM disk or specialized benchmarks for CPU RAM
|
320
|
+
- Database performance using SQL client or non-SQL client utilities
|
321
|
+
- Performance of object storage of cloud providers, be it AWS S3, OCI Object Storage, or anything else
|
322
|
+
- Performance of any AI model, from simplistic YOLO to heavy-weight LLM such as LLAMA, Cohere, or DeepSeek
|
323
|
+
- ... Any other target application or service
|
324
|
+
|
325
|
+
In any use case, FlexCartesian will unfold complete landscape of the target performance over all configurable parameters.
|
326
|
+
As result, you will be able to spot optimal configurations, correlations, bottlenecks, and sweet spots.
|
327
|
+
Moreover, you will make your conclusions in a justifiable way.
|
328
|
+
|
329
|
+
Here is example of using FlexCartesian for [performance/cost analysis of YOLO](https://www.linkedin.com/pulse/comparing-gpu-a10-ampere-a1-shapes-object-oci-yuri-rassokhin-rseqf).
|
330
|
+
|
175
331
|
|
176
332
|
|
177
333
|
## API Overview
|
178
334
|
|
179
335
|
### Initialization
|
180
336
|
```ruby
|
181
|
-
FlexCartesian.new(
|
337
|
+
FlexCartesian.new(dimensions = nil, path: nil, format: :json)
|
182
338
|
```
|
183
|
-
- `dimensions_hash`:
|
339
|
+
- `dimensions_hash`: optional hash with named dimensions; each value can be an `Enumerable` (arrays, ranges, etc)
|
340
|
+
- `path`: optional path to file with stored dimensions, JSON and YAML supported
|
341
|
+
- `format`: optional format of `path` file, defaults to JSON
|
184
342
|
|
185
343
|
Example:
|
186
344
|
```ruby
|
@@ -196,10 +354,11 @@ FlexCartesian.new(dimensions)
|
|
196
354
|
---
|
197
355
|
|
198
356
|
### Iterate Over All Combinations
|
357
|
+
|
358
|
+
Example:
|
199
359
|
```ruby
|
200
360
|
# With block
|
201
361
|
cartesian(dims = nil, lazy: false) { |vector| ... }
|
202
|
-
|
203
362
|
# Without block: returns Enumerator
|
204
363
|
cartesian(dims = nil, lazy: false)
|
205
364
|
```
|
@@ -213,20 +372,31 @@ s.cartesian { |v| puts "#{v.dim1} - #{v.dim2}" }
|
|
213
372
|
|
214
373
|
---
|
215
374
|
|
216
|
-
###
|
375
|
+
### Handling Functions
|
217
376
|
```ruby
|
218
|
-
|
219
|
-
remove_function(name)
|
377
|
+
func(command = :print, name = nil, hide: false, &block)
|
220
378
|
```
|
221
|
-
- `
|
379
|
+
- `command`: symbol, one of the following
|
380
|
+
- `:add` to add function as a virtual dimension to Cartesian space
|
381
|
+
- `:del` to delete function from Cartesian space
|
382
|
+
- `:print` as defaut action, prints all the functions added to Cartesian space
|
383
|
+
- `:run` to calculate all the functions defined for Cartesian space
|
384
|
+
- `name`: symbol, name of the virtual dimension, e.g. `:my_function`
|
385
|
+
- `hide`: flag that hides or shows the function in .output; it is useful to hide intermediate calculations
|
222
386
|
- `block`: a function that receives each vector and returns a computed value
|
223
387
|
|
224
388
|
Functions show up in `.output` like additional (virtual) dimensions.
|
225
389
|
|
390
|
+
> Note: functions must be calculated excpliticy using `:run` command.
|
391
|
+
> Before the first calculation, a function has `nil` values in `.output`.
|
392
|
+
> Explicit :run is reequired to unambigously control points in the execution flow where high computational resource is to be consumed.
|
393
|
+
> Otherwise, automated recalculation of functions, perhaps, during `.output` would be a difficult-to-track computational burden.
|
394
|
+
|
226
395
|
Example:
|
227
396
|
```ruby
|
228
397
|
s = FlexCartesian.new( { dim1: [1, 2], dim2: ['A', 'B'] } )
|
229
|
-
s.
|
398
|
+
s.func(:add, :increment) { |v| v.dim1 + 1 }
|
399
|
+
s.func(:run)
|
230
400
|
|
231
401
|
s.output(format: :markdown)
|
232
402
|
# | dim1 | dim2 | increment |
|
@@ -236,7 +406,6 @@ s.output(format: :markdown)
|
|
236
406
|
# ...
|
237
407
|
```
|
238
408
|
|
239
|
-
> Note: functions are virtual — they are not part of the base dimensions, but they integrate seamlessly in output.
|
240
409
|
|
241
410
|
---
|
242
411
|
|
@@ -264,19 +433,20 @@ Displays a progress bar using `ruby-progressbar`.
|
|
264
433
|
|
265
434
|
---
|
266
435
|
|
267
|
-
### Print
|
436
|
+
### Print Cartesian
|
268
437
|
```ruby
|
269
|
-
output(
|
270
|
-
separator: " | ",
|
271
|
-
colorize: false,
|
272
|
-
align: false,
|
273
|
-
format: :plain # or :markdown, :csv
|
274
|
-
limit: nil
|
275
|
-
)
|
438
|
+
output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil)
|
276
439
|
```
|
440
|
+
- `separator`: how to visually separate columns in the output
|
441
|
+
- `colorize`: whether to colorize output or not
|
442
|
+
- `align`: whether to align output by column or not
|
443
|
+
- `format`: one of `:plain`, `:markdown`, or `:csv`
|
444
|
+
- `limit`: break the output after the first `limit` Cartesian combinations
|
445
|
+
- `file`: print to `file`
|
446
|
+
|
277
447
|
Prints all combinations in table form (plain/markdown/CSV).
|
278
448
|
Markdown example:
|
279
|
-
```
|
449
|
+
```markdown
|
280
450
|
| dim1 | dim2 |
|
281
451
|
|------|------|
|
282
452
|
| 1 | "a" |
|
@@ -287,9 +457,10 @@ Markdown example:
|
|
287
457
|
|
288
458
|
### Import from JSON or YAML
|
289
459
|
```ruby
|
290
|
-
import(
|
291
|
-
format: :json) # or :yaml
|
460
|
+
import(path, format: :json)
|
292
461
|
```
|
462
|
+
- `path`: input file
|
463
|
+
- `format`: format to read, `:json` and `:yaml` supported
|
293
464
|
|
294
465
|
Obsolete import methods:
|
295
466
|
```ruby
|
@@ -299,36 +470,30 @@ s.from_yaml("file.yaml")
|
|
299
470
|
|
300
471
|
---
|
301
472
|
|
302
|
-
### Export
|
473
|
+
### Export to JSON or YAML
|
303
474
|
```ruby
|
304
|
-
export(
|
305
|
-
format: :json) # or :yaml
|
475
|
+
export(path, format: :json)
|
306
476
|
```
|
477
|
+
- `path`: output file
|
478
|
+
- `format`: format to export, `:json` and `:yaml` supported
|
307
479
|
|
308
|
-
|
309
|
-
|
310
|
-
### Print Cartesian Space
|
311
|
-
Each yielded combination is a `Struct` extended with:
|
312
|
-
```ruby
|
313
|
-
output(separator: " | ", colorize: false, align: true)
|
314
|
-
```
|
315
|
-
Example:
|
480
|
+
### Conditions on Cartesian Space
|
316
481
|
```ruby
|
317
|
-
|
482
|
+
cond(command = :print, index: nil, &block)
|
318
483
|
```
|
484
|
+
- `command`: one of the following
|
485
|
+
- `:set` to set the condition to Cartesian space
|
486
|
+
- `:unset` to remove the `index` condition from Cartesian space
|
487
|
+
- `:clear` to remove all conditions from Cartesian space
|
488
|
+
- `:print` default command, prints all the conditions on the Cartesian space
|
489
|
+
- `index`: index of the condition set to Cartesian space, it is used to remove specified condition
|
490
|
+
- `block`: definition of the condition, it should return `true` or `false` to avoid unpredictable behavior
|
319
491
|
|
320
|
-
---
|
321
|
-
|
322
|
-
### Conditions on Cartesian Space
|
323
|
-
cond(command = :print, # or :set, :unset, :clear
|
324
|
-
index: nil, # index of a conditions to unset
|
325
|
-
&block # defintiion of the condition to set
|
326
|
-
)
|
327
492
|
Example:
|
328
493
|
```ruby
|
329
494
|
s.cond(:set) { |v| v.dim1 > v.dim3 }
|
330
495
|
s.cond # defaults to s.cond(:print) and shows all the conditions in the form 'index | definition'
|
331
|
-
s.cond(:unset, 0) # remove
|
496
|
+
s.cond(:unset, 0) # remove the condition
|
332
497
|
s.cond(:clear) # remove all conditions, if any
|
333
498
|
```
|
334
499
|
|
data/lib/flex-cartesian.rb
CHANGED
@@ -4,6 +4,7 @@ require 'colorize'
|
|
4
4
|
require 'json'
|
5
5
|
require 'yaml'
|
6
6
|
require 'method_source'
|
7
|
+
require 'set'
|
7
8
|
|
8
9
|
module FlexOutput
|
9
10
|
def output(separator: " | ", colorize: false, align: true)
|
@@ -26,10 +27,16 @@ end
|
|
26
27
|
class FlexCartesian
|
27
28
|
attr :dimensions
|
28
29
|
|
29
|
-
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
|
30
34
|
@dimensions = dimensions
|
31
35
|
@conditions = []
|
32
36
|
@derived = {}
|
37
|
+
@function_results = {} # key: Struct instance.object_id => { fname => value }
|
38
|
+
@function_hidden = Set.new
|
39
|
+
import(path, format: format) if path
|
33
40
|
end
|
34
41
|
|
35
42
|
def cond(command = :print, index: nil, &block)
|
@@ -52,6 +59,45 @@ class FlexCartesian
|
|
52
59
|
end
|
53
60
|
end
|
54
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
|
+
|
55
101
|
def add_function(name, &block)
|
56
102
|
raise ArgumentError, "Block required" unless block_given?
|
57
103
|
@derived[name.to_sym] = block
|
@@ -123,36 +169,62 @@ end
|
|
123
169
|
end
|
124
170
|
end
|
125
171
|
|
126
|
-
|
127
|
-
rows =
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
+
|
132
184
|
return if rows.empty?
|
133
185
|
|
134
|
-
|
135
|
-
|
136
|
-
rows.first.singleton_methods(false).reject { |m| m.to_s.start_with?('__') }
|
137
|
-
).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)
|
138
188
|
|
139
189
|
widths = align ? headers.to_h { |h|
|
140
|
-
|
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]
|
141
199
|
} : {}
|
142
200
|
|
201
|
+
lines = []
|
202
|
+
|
203
|
+
# Header
|
143
204
|
case format
|
144
205
|
when :markdown
|
145
|
-
|
146
|
-
|
206
|
+
lines << "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
|
207
|
+
lines << "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
|
147
208
|
when :csv
|
148
|
-
|
209
|
+
lines << headers.join(",")
|
149
210
|
else
|
150
|
-
|
211
|
+
lines << headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(separator)
|
151
212
|
end
|
152
213
|
|
214
|
+
# Rows
|
153
215
|
rows.each do |row|
|
154
|
-
|
155
|
-
|
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 }
|
156
228
|
end
|
157
229
|
end
|
158
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.1'
|
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-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: progressbar
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.13'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.13'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: json
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -69,7 +83,7 @@ dependencies:
|
|
69
83
|
description: 'Flexible and human-friendly Cartesian product enumerator for Ruby. Supports
|
70
84
|
functions and conditions on cartesian, dimensionality-agnostic/dimensionality-aware
|
71
85
|
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#
|
86
|
+
import from JSON/YAML, and export to Markdown/CSV. Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#example'
|
73
87
|
email:
|
74
88
|
- yuri.rassokhin@gmail.com
|
75
89
|
executables: []
|
@@ -94,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
108
|
requirements:
|
95
109
|
- - ">="
|
96
110
|
- !ruby/object:Gem::Version
|
97
|
-
version: '0'
|
111
|
+
version: '3.0'
|
98
112
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
113
|
requirements:
|
100
114
|
- - ">="
|