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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +185 -30
  4. data/lib/flex-cartesian.rb +79 -18
  5. metadata +19 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e92633e14160e43566c300b7e747f9f8cf742954652a442073ca44f7e0992ca3
4
- data.tar.gz: c0a3ba79e43094b3c8325f23a9c37c8cf9f29a7fc4fb5bd20b34c514cef3e8d0
3
+ metadata.gz: 4b34c40aea1692a248f2f4ee506b8b7a5a66a8257f90b1f74a7613897f8047c4
4
+ data.tar.gz: 3fb4c691a65ebcda1f9606368f436ba1ebee56749dee9e27d04296884cfef26e
5
5
  SHA512:
6
- metadata.gz: 1bd895985ea81a675c38e9feace76e9d3a6b0dca8502fe9065420695c66a75b8e1b4134b55a1144bf9082fe52a60f4c0812317f113ae486c0e739b6e55266bcd
7
- data.tar.gz: 44edb599dff65eb233ef2bd87623cd0c4cf20ca1d5d9d6cb33dfbe504b8c852a3020374dae019efac8193b552590fd7a684637dac58efc7eb6b6de44117246b0
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
- Export of Cartesian space to Markdown or CSV
27
+ Import of Cartesian space from JSON or YAML
20
28
 
21
- Import of dimension space from JSON or YAML
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
- # Define a Cartesian space with named dimensions:
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
- # Iterate over all combinations and calculate function on each combination:
47
- s.cartesian { |v| puts "#{v.dim1}-#{v.dim2}" if v.dim3 }
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
- def do_something(v)
57
- end
75
+ # ITERATION OVER CARTESIAN SPACE
58
76
 
59
- # Display progress bar (useful for large Cartesian spaces)
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
- # Print Cartesian space as table
63
- s.output(align: true)
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
- # Load from JSON or YAML
69
- File.write('example.json', JSON.pretty_generate(example))
70
- s = FlexCartesian.from_json('example.json')
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
- # Export to Markdown
74
- s.output(format: :markdown, align: true)
144
+ puts "\nPrint Cartesian space as Markdown"
145
+ s.output(format: :markdown)
75
146
 
76
- # Export to CSV
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
- ### Load from JSON or YAML
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
- FlexCartesian.from_json("file.json")
167
- FlexCartesian.from_yaml("file.yaml")
304
+ export('file.json',
305
+ format: :json) # or :yaml
168
306
  ```
169
307
 
170
308
  ---
171
309
 
172
- ### Output from Vectors
310
+ ### Print Cartesian Space
173
311
  Each yielded combination is a `Struct` extended with:
174
312
  ```ruby
175
- output(separator: " | ", colorize: false, align: false)
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.cartesian { |v| v.output(colorize: true, align: true) }
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.
@@ -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: false)
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(dims = nil)
65
- dimensions = dims || @dimensions
66
- return 0 unless dimensions.is_a?(Hash)
67
-
68
- values = dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
69
- return 0 if values.any?(&:empty?)
70
-
71
- values.map(&:size).inject(1, :*)
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(dims = nil, lazy: false, title: "Processing")
84
- total = size(dims)
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(dims, lazy: lazy) do |v|
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: false, format: :plain, limit: nil)
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
- def self.from_json(path)
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
- new(data)
189
+ @dimensions = data
129
190
  end
130
191
 
131
- def self.from_yaml(path)
192
+ def from_yaml(path)
132
193
  data = YAML.safe_load(File.read(path), symbolize_names: true)
133
- new(data)
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.1.8
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-06 00:00:00.000000000 Z
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
- calculated functions, dimension-agnostic iterators, named dimensions, tabular output,
57
- lazy/eager evaluation, progress bar, JSON/YAML loading, and export to Markdown/CSV.
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: []