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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +225 -60
  4. data/lib/flex-cartesian.rb +90 -18
  5. metadata +18 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b34c40aea1692a248f2f4ee506b8b7a5a66a8257f90b1f74a7613897f8047c4
4
- data.tar.gz: 3fb4c691a65ebcda1f9606368f436ba1ebee56749dee9e27d04296884cfef26e
3
+ metadata.gz: b89541266e2ac802592bc652ddbf2edffe1aea4e47ab1d732e2623ad2cc5835b
4
+ data.tar.gz: d717084b70f95af0e3f5706a426731685cc0cc7ddcba038b15b662d2d3226b3b
5
5
  SHA512:
6
- metadata.gz: 455053108bada48ae9b224879069cfe6e43fca419391e194f5b90dbaece0523ab7810eb471e1768814c17bc61f4952171493fb5c38283473d7798039f6b8b8dd
7
- data.tar.gz: 63ebb02a0fb22162b67d17caf483ab2abc3499e9d072ddec3749124e82856d4bfcf3106fbe53b84fd927fe3b36279cb1b2103d75064d4a1cf393ab2f72912f38
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 product with a single block argument
9
+ ✅ Enumerate over Cartesian space with a single block argument
12
10
 
13
- Functions over Cartesian vectors are decoupled from dimensionality
11
+ Actions on Cartesian are decoupled from dimensionality: `s.cartesian { |v| do_something(v) }`
14
12
 
15
- Define conditions on Cartesian combinations using `s.cond(:set) { |v| v.dim1 > v.dim2 } }` syntax
13
+ Conditions for Cartesian space: `s.cond(:set) { |v| v.dim1 > v.dim2 } }`
16
14
 
17
- Calculate over named dimensions using `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }` syntax
15
+ Calculation over named dimensions: `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }`
18
16
 
19
- Add functions over dimensions using `s.add_function { |v| v.dim1 + v.dim2 }` syntax
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 combinations
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 .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.
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.add_function(:triple) { |v| v.dim1 * 3 + (v.dim3 ? 1: 0) }
108
- # Note: however, function remains a virtual construct, and it cannot be referenced by name
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.add_function(:test) { |v| v.dim3.to_i }
113
- s.remove_function(:test)
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 restriction of allowed combitnations for Cartesian space.
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
+ ![Ping Benchmark Example](doc/ping_benchmark_example.png)
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(dimensions_hash)
337
+ FlexCartesian.new(dimensions = nil, path: nil, format: :json)
182
338
  ```
183
- - `dimensions_hash`: a hash with named dimensions; each value can be an `Enumerable` (e.g., arrays, ranges).
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
- ### Add / Remove Functions
375
+ ### Handling Functions
217
376
  ```ruby
218
- add_function(name, &block)
219
- remove_function(name)
377
+ func(command = :print, name = nil, hide: false, &block)
220
378
  ```
221
- - `name`: symbol the name of the virtual dimension (e.g. `:label`)
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.add_function(:increment) { |v| v.dim1 + 1 }
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 Table to Console
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('file.json',
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 from JSON or YAML
473
+ ### Export to JSON or YAML
303
474
  ```ruby
304
- export('file.json',
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
- s.cartesian { |v| v.output(colorize: true, align: false) }
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 previously set condition
496
+ s.cond(:unset, 0) # remove the condition
332
497
  s.cond(:clear) # remove all conditions, if any
333
498
  ```
334
499
 
@@ -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
- def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil)
127
- rows = []
128
- cartesian do |v|
129
- rows << v
130
- break if limit && rows.size >= limit
131
- end
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
- headers = (
135
- rows.first.members +
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
- [h, [h.size, *rows.map { |r| fmt_cell(r.send(h), false).size }].max]
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
- puts "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
146
- puts "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
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
- puts headers.join(",")
209
+ lines << headers.join(",")
149
210
  else
150
- puts headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(separator)
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
- line = headers.map { |h| fmt_cell(row.send(h), colorize, widths[h]) }
155
- puts format == :csv ? line.join(",") : line.join(separator)
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: '0.2'
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-12 00:00:00.000000000 Z
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#usage'
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
  - - ">="