flex-cartesian 0.1.8 → 0.2
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 -0
- data/README.md +185 -30
- data/lib/flex-cartesian.rb +79 -18
- metadata +19 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b34c40aea1692a248f2f4ee506b8b7a5a66a8257f90b1f74a7613897f8047c4
|
4
|
+
data.tar.gz: 3fb4c691a65ebcda1f9606368f436ba1ebee56749dee9e27d04296884cfef26e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 455053108bada48ae9b224879069cfe6e43fca419391e194f5b90dbaece0523ab7810eb471e1768814c17bc61f4952171493fb5c38283473d7798039f6b8b8dd
|
7
|
+
data.tar.gz: 63ebb02a0fb22162b67d17caf483ab2abc3499e9d072ddec3749124e82856d4bfcf3106fbe53b84fd927fe3b36279cb1b2103d75064d4a1cf393ab2f72912f38
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.1.9 - 2025-07-08
|
4
|
+
### Fixed
|
5
|
+
- Documentation
|
6
|
+
|
7
|
+
### Added
|
8
|
+
- Unified methods for import and export
|
9
|
+
- JSON and YAML can be imported, not only exported
|
10
|
+
- Functions can be removed
|
11
|
+
- Minor changes in default values of named parameters
|
12
|
+
|
3
13
|
## 0.1.8 - 2025-07-07
|
4
14
|
### Fixed
|
5
15
|
- Documentation
|
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
**Ruby implementation of flexible and human-friendly operations on Cartesian products**
|
4
4
|
|
5
|
+
|
6
|
+
|
5
7
|
## Features
|
6
8
|
|
7
9
|
✅ Named dimensions with arbitrary keys
|
@@ -10,18 +12,26 @@
|
|
10
12
|
|
11
13
|
✅ Functions over Cartesian vectors are decoupled from dimensionality
|
12
14
|
|
15
|
+
✅ Define conditions on Cartesian combinations using `s.cond(:set) { |v| v.dim1 > v.dim2 } }` syntax
|
16
|
+
|
13
17
|
✅ Calculate over named dimensions using `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }` syntax
|
14
18
|
|
19
|
+
✅ Add functions over dimensions using `s.add_function { |v| v.dim1 + v.dim2 }` syntax
|
20
|
+
|
15
21
|
✅ Lazy and eager evaluation
|
16
22
|
|
17
|
-
✅ Progress bars for large Cartesian combinations
|
23
|
+
✅ Progress bars for large Cartesian combinations
|
24
|
+
|
25
|
+
✅ Export of Cartesian space to Markdown or CSV
|
18
26
|
|
19
|
-
✅
|
27
|
+
✅ Import of Cartesian space from JSON or YAML
|
20
28
|
|
21
|
-
✅
|
29
|
+
✅ Export of Cartesian space to Markdown or CSV
|
22
30
|
|
23
31
|
✅ Structured and colorized terminal output
|
24
32
|
|
33
|
+
|
34
|
+
|
25
35
|
## Installation
|
26
36
|
|
27
37
|
```bash
|
@@ -30,53 +40,140 @@ gem build flex-cartesian.gemspec
|
|
30
40
|
gem install flex-cartesian-*.gem
|
31
41
|
```
|
32
42
|
|
43
|
+
|
44
|
+
|
33
45
|
## Usage
|
34
46
|
|
35
47
|
```ruby
|
48
|
+
#!/usr/bin/ruby
|
49
|
+
|
36
50
|
require 'flex-cartesian'
|
37
51
|
|
38
|
-
|
52
|
+
|
53
|
+
|
54
|
+
# BASIC CONCEPTS
|
55
|
+
|
56
|
+
# 1. Cartesian object is a set of combinations of values of dimansions.
|
57
|
+
# 2. Dimensions always have names.
|
58
|
+
|
59
|
+
puts "\nDefine named dimensions"
|
39
60
|
example = {
|
40
61
|
dim1: [1, 2],
|
41
62
|
dim2: ['x', 'y'],
|
42
63
|
dim3: [true, false]
|
43
64
|
}
|
65
|
+
|
66
|
+
puts "\nCreate Cartesian space"
|
44
67
|
s = FlexCartesian.new(example)
|
45
68
|
|
46
|
-
|
47
|
-
|
69
|
+
def do_something(v)
|
70
|
+
# do something here on vector v and its components
|
71
|
+
end
|
48
72
|
|
49
|
-
# Get number of Cartesian combinations:
|
50
|
-
puts "Total size: #{s.size}"
|
51
73
|
|
52
|
-
# Convert Cartesian space to array of combinations
|
53
|
-
array = s.to_a(limit: 3)
|
54
|
-
puts array.inspect
|
55
74
|
|
56
|
-
|
57
|
-
end
|
75
|
+
# ITERATION OVER CARTESIAN SPACE
|
58
76
|
|
59
|
-
#
|
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
|
+
|
83
|
+
puts "\nIterate over all Cartesian combinations and execute action (dimensionality-agnostic style)"
|
84
|
+
s.cartesian { |v| do_something(v) }
|
85
|
+
|
86
|
+
puts "\nIterate over all Cartesian combinations and execute action (dimensionality-aware style)"
|
87
|
+
s.cartesian { |v| puts "#{v.dim1} & #{v.dim2}" if v.dim3 }
|
88
|
+
|
89
|
+
puts "\nIterate and display progress bar (useful for large Cartesian spaces)"
|
60
90
|
s.progress_each { |v| do_something(v) }
|
61
91
|
|
62
|
-
|
63
|
-
s.
|
92
|
+
puts "\nIterate in lLazy mode, without materializing entire Cartesian product in memory"
|
93
|
+
s.cartesian(lazy: true).take(2).each { |v| do_something(v) }
|
64
94
|
|
65
|
-
# Lazy evaluation without materializing entire Cartesian product in memory:
|
66
|
-
s.cartesian(lazy: true).take(2).each { |v| puts v.inspect }
|
67
95
|
|
68
|
-
|
69
|
-
|
70
|
-
|
96
|
+
|
97
|
+
# FUNCTIONS ON CARTESIAN SPACE
|
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
|
+
|
105
|
+
puts "\nAdd function 'triple'"
|
106
|
+
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
|
109
|
+
s.output
|
110
|
+
|
111
|
+
puts "\Add and then remove function 'test'"
|
112
|
+
s.add_function(:test) { |v| v.dim3.to_i }
|
113
|
+
s.remove_function(:test)
|
114
|
+
|
115
|
+
|
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
|
+
|
139
|
+
# PRINT
|
140
|
+
|
141
|
+
puts "\nPrint Cartesian space as plain table, all functions included"
|
71
142
|
s.output
|
72
143
|
|
73
|
-
|
74
|
-
s.output(format: :markdown
|
144
|
+
puts "\nPrint Cartesian space as Markdown"
|
145
|
+
s.output(format: :markdown)
|
75
146
|
|
76
|
-
|
147
|
+
puts "\nPrint Cartesian space as CSV"
|
77
148
|
s.output(format: :csv)
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
# IMPORT / EXPORT
|
153
|
+
|
154
|
+
puts "\nImport Cartesian space from JSON (similar method for YAML)"
|
155
|
+
File.write('example.json', JSON.pretty_generate(example))
|
156
|
+
puts "\nNote: after import, all assigned functions will calculate again, and they appear in the output"
|
157
|
+
s.import('example.json').output
|
158
|
+
|
159
|
+
puts "\nExport Cartesian space to YAML (similar method for JSON)"
|
160
|
+
s.export('example.yaml', format: :yaml)
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
# UTILITIES
|
165
|
+
|
166
|
+
puts "\nGet number of Cartesian combinations"
|
167
|
+
puts "Note: .size counts only dimensions, it ignores virtual constructs (functions, conditions, etc.)"
|
168
|
+
puts "Total size of Cartesian space: #{s.size}"
|
169
|
+
|
170
|
+
puts "\nPartially converting Cartesian space to array:"
|
171
|
+
array = s.to_a(limit: 3)
|
172
|
+
puts array.inspect
|
78
173
|
```
|
79
174
|
|
175
|
+
|
176
|
+
|
80
177
|
## API Overview
|
81
178
|
|
82
179
|
### Initialization
|
@@ -116,6 +213,33 @@ s.cartesian { |v| puts "#{v.dim1} - #{v.dim2}" }
|
|
116
213
|
|
117
214
|
---
|
118
215
|
|
216
|
+
### Add / Remove Functions
|
217
|
+
```ruby
|
218
|
+
add_function(name, &block)
|
219
|
+
remove_function(name)
|
220
|
+
```
|
221
|
+
- `name`: symbol — the name of the virtual dimension (e.g. `:label`)
|
222
|
+
- `block`: a function that receives each vector and returns a computed value
|
223
|
+
|
224
|
+
Functions show up in `.output` like additional (virtual) dimensions.
|
225
|
+
|
226
|
+
Example:
|
227
|
+
```ruby
|
228
|
+
s = FlexCartesian.new( { dim1: [1, 2], dim2: ['A', 'B'] } )
|
229
|
+
s.add_function(:increment) { |v| v.dim1 + 1 }
|
230
|
+
|
231
|
+
s.output(format: :markdown)
|
232
|
+
# | dim1 | dim2 | increment |
|
233
|
+
# |------|------|--------|
|
234
|
+
# | 1 | "A" | 2 |
|
235
|
+
# | 1 | "B" | 2 |
|
236
|
+
# ...
|
237
|
+
```
|
238
|
+
|
239
|
+
> Note: functions are virtual — they are not part of the base dimensions, but they integrate seamlessly in output.
|
240
|
+
|
241
|
+
---
|
242
|
+
|
119
243
|
### Count Total Combinations
|
120
244
|
```ruby
|
121
245
|
size(dims = nil) → Integer
|
@@ -161,24 +285,55 @@ Markdown example:
|
|
161
285
|
|
162
286
|
---
|
163
287
|
|
164
|
-
###
|
288
|
+
### Import from JSON or YAML
|
289
|
+
```ruby
|
290
|
+
import('file.json',
|
291
|
+
format: :json) # or :yaml
|
292
|
+
```
|
293
|
+
|
294
|
+
Obsolete import methods:
|
295
|
+
```ruby
|
296
|
+
s.from_json("file.json")
|
297
|
+
s.from_yaml("file.yaml")
|
298
|
+
```
|
299
|
+
|
300
|
+
---
|
301
|
+
|
302
|
+
### Export from JSON or YAML
|
165
303
|
```ruby
|
166
|
-
|
167
|
-
|
304
|
+
export('file.json',
|
305
|
+
format: :json) # or :yaml
|
168
306
|
```
|
169
307
|
|
170
308
|
---
|
171
309
|
|
172
|
-
###
|
310
|
+
### Print Cartesian Space
|
173
311
|
Each yielded combination is a `Struct` extended with:
|
174
312
|
```ruby
|
175
|
-
output(separator: " | ", colorize: false, align:
|
313
|
+
output(separator: " | ", colorize: false, align: true)
|
314
|
+
```
|
315
|
+
Example:
|
316
|
+
```ruby
|
317
|
+
s.cartesian { |v| v.output(colorize: true, align: false) }
|
176
318
|
```
|
319
|
+
|
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
|
+
)
|
177
327
|
Example:
|
178
328
|
```ruby
|
179
|
-
s.
|
329
|
+
s.cond(:set) { |v| v.dim1 > v.dim3 }
|
330
|
+
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
|
332
|
+
s.cond(:clear) # remove all conditions, if any
|
180
333
|
```
|
181
334
|
|
335
|
+
|
336
|
+
|
182
337
|
## License
|
183
338
|
|
184
339
|
This project is licensed under the terms of the GNU General Public License v3.0.
|
data/lib/flex-cartesian.rb
CHANGED
@@ -3,9 +3,10 @@ require 'progressbar'
|
|
3
3
|
require 'colorize'
|
4
4
|
require 'json'
|
5
5
|
require 'yaml'
|
6
|
+
require 'method_source'
|
6
7
|
|
7
8
|
module FlexOutput
|
8
|
-
def output(separator: " | ", colorize: false, align:
|
9
|
+
def output(separator: " | ", colorize: false, align: true)
|
9
10
|
return puts "(empty struct)" unless respond_to?(:members) && respond_to?(:values)
|
10
11
|
|
11
12
|
values_list = members.zip(values.map { |v| v.inspect })
|
@@ -27,14 +28,39 @@ class FlexCartesian
|
|
27
28
|
|
28
29
|
def initialize(dimensions = nil)
|
29
30
|
@dimensions = dimensions
|
31
|
+
@conditions = []
|
30
32
|
@derived = {}
|
31
33
|
end
|
32
34
|
|
35
|
+
def cond(command = :print, index: nil, &block)
|
36
|
+
case command
|
37
|
+
when :set
|
38
|
+
raise ArgumentError, "Block required" unless block_given?
|
39
|
+
@conditions << block
|
40
|
+
self
|
41
|
+
when :unset
|
42
|
+
raise ArgumentError, "Index of the condition required" unless index
|
43
|
+
@conditions.delete_at(index)
|
44
|
+
when :clear
|
45
|
+
@conditions.clear
|
46
|
+
self
|
47
|
+
when :print
|
48
|
+
return if @conditions.empty?
|
49
|
+
@conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
|
50
|
+
else
|
51
|
+
raise ArgumentError, "unknown condition command: #{command}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
33
55
|
def add_function(name, &block)
|
34
56
|
raise ArgumentError, "Block required" unless block_given?
|
35
57
|
@derived[name.to_sym] = block
|
36
58
|
end
|
37
59
|
|
60
|
+
def remove_function(name)
|
61
|
+
@derived.delete(name.to_sym)
|
62
|
+
end
|
63
|
+
|
38
64
|
def cartesian(dims = nil, lazy: false)
|
39
65
|
dimensions = dims || @dimensions
|
40
66
|
return nil unless dimensions.is_a?(Hash)
|
@@ -57,18 +83,26 @@ class FlexCartesian
|
|
57
83
|
struct_instance.define_singleton_method(name) { block.call(struct_instance) }
|
58
84
|
end
|
59
85
|
|
86
|
+
next if @conditions.any? { |cond| !cond.call(struct_instance) }
|
87
|
+
|
60
88
|
yield struct_instance
|
61
89
|
end
|
62
90
|
end
|
63
91
|
|
64
|
-
def size
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
92
|
+
def size
|
93
|
+
return 0 unless @dimensions.is_a?(Hash)
|
94
|
+
if @conditions.empty?
|
95
|
+
values = @dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
|
96
|
+
return 0 if values.any?(&:empty?)
|
97
|
+
values.map(&:size).inject(1, :*)
|
98
|
+
else
|
99
|
+
size = 0
|
100
|
+
cartesian do |v|
|
101
|
+
next if @conditions.any? { |cond| !cond.call(v) }
|
102
|
+
size += 1
|
103
|
+
end
|
104
|
+
size
|
105
|
+
end
|
72
106
|
end
|
73
107
|
|
74
108
|
def to_a(limit: nil)
|
@@ -80,17 +114,16 @@ end
|
|
80
114
|
result
|
81
115
|
end
|
82
116
|
|
83
|
-
def progress_each(
|
84
|
-
|
85
|
-
bar = ProgressBar.create(title: title, total: total, format: '%t [%B] %p%% %e')
|
117
|
+
def progress_each(lazy: false, title: "Processing")
|
118
|
+
bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')
|
86
119
|
|
87
|
-
cartesian(
|
120
|
+
cartesian(@dimensions, lazy: lazy) do |v|
|
88
121
|
yield v
|
89
122
|
bar.increment
|
90
123
|
end
|
91
124
|
end
|
92
125
|
|
93
|
-
def output(separator: " | ", colorize: false, align:
|
126
|
+
def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil)
|
94
127
|
rows = []
|
95
128
|
cartesian do |v|
|
96
129
|
rows << v
|
@@ -123,14 +156,42 @@ end
|
|
123
156
|
end
|
124
157
|
end
|
125
158
|
|
126
|
-
|
159
|
+
def import(path, format: :json)
|
160
|
+
data = case format
|
161
|
+
when :json
|
162
|
+
JSON.parse(File.read(path), symbolize_names: true)
|
163
|
+
when :yaml
|
164
|
+
YAML.safe_load(File.read(path), symbolize_names: true)
|
165
|
+
else
|
166
|
+
raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
|
167
|
+
end
|
168
|
+
|
169
|
+
raise TypeError, "Expected parsed data to be a Hash" unless data.is_a?(Hash)
|
170
|
+
|
171
|
+
@dimensions = data
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
def export(path, format: :json)
|
176
|
+
case format
|
177
|
+
when :json
|
178
|
+
File.write(path, JSON.pretty_generate(@dimensions))
|
179
|
+
when :yaml
|
180
|
+
File.write(path, YAML.dump(@dimensions))
|
181
|
+
else
|
182
|
+
raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
def from_json(path)
|
127
188
|
data = JSON.parse(File.read(path), symbolize_names: true)
|
128
|
-
|
189
|
+
@dimensions = data
|
129
190
|
end
|
130
191
|
|
131
|
-
def
|
192
|
+
def from_yaml(path)
|
132
193
|
data = YAML.safe_load(File.read(path), symbolize_names: true)
|
133
|
-
|
194
|
+
@dimensions = data
|
134
195
|
end
|
135
196
|
|
136
197
|
private
|
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.
|
4
|
+
version: '0.2'
|
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-12 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
|
-
|
57
|
-
lazy/eager evaluation, progress bar,
|
58
|
-
Code example: https://github.com/Yuri-Rassokhin/flex-cartesian/blob/main/README.md#usage'
|
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#usage'
|
59
73
|
email:
|
60
74
|
- yuri.rassokhin@gmail.com
|
61
75
|
executables: []
|