Class: FlexCartesian

Inherits:
Object
  • Object
show all
Defined in:
lib/flex-cartesian.rb

Instance Method Summary collapse

Constructor Details

#initialize(dimensions = nil, path: nil, format: :json) ⇒ FlexCartesian

Returns a new instance of FlexCartesian.



29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/flex-cartesian.rb', line 29

def initialize(dimensions = nil, path: nil, format: :json)
  if dimensions && path
    puts "Please specify either dimensions or path to dimensions"
    exit
  end
  @dimensions = dimensions
  @conditions = []
  @derived = {}
  @order = { first: nil, last: nil }
  @function_results = {}  # key: Struct instance.object_id => { fname => value }
  @function_hidden = Set.new
  import(path, format: format) if path
end

Instance Method Details

#add_function(name, order: nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/flex-cartesian.rb', line 135

def add_function(name, order: nil , &block)
  raise ArgumentError, "Block required" unless block_given?
  if reserved_function_names.include?(name.to_sym)
    raise ArgumentError, "Function name '#{name}' has been already added"
  elsif reserved_struct_names.include?(name.to_sym)
    raise ArgumentError, "Name '#{name}' has been reserved for internal method, you can't use it for a function"
  end
  if order == :last
    @derived[name.to_sym] = block # add to the tail of the hash
    @order[:last] = name.to_sym
  elsif order == :first
    @derived = { name.to_sym => block }.merge(@derived) # add to the head of the hash
    @order[:first] = name.to_sym
  elsif order == nil
    if @order[:last] != nil
      last_name = @order[:last]
      last_body = @derived[last_name]
      @derived.delete(@order[:last]) # remove the tail of the hash
      @derived[name.to_sym] = block # add new function to the tail of the hash
      @derived[last_name] = last_body # restore :last function in the tail of the hash
    else
      @derived[name.to_sym] = block
    end
  else
    raise ArgumentError, "unknown function order '#{order}'"
  end
end

#cartesian(dims = nil, lazy: false) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/flex-cartesian.rb', line 169

def cartesian(dims = nil, lazy: false)
  dimensions = dims || @dimensions
  return nil unless dimensions.is_a?(Hash)

  names = dimensions.keys
  values = dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }

  return to_enum(:cartesian, dims, lazy: lazy) unless block_given?
  return if values.any?(&:empty?)

  struct_class = Struct.new(*names).tap { |sc| sc.include(FlexOutput) }

  base = values.first.product(*values[1..])
  enum = lazy ? base.lazy : base

  enum.each do |combo|
    struct_instance = struct_class.new(*combo)

    @derived&.each do |name, block|
      struct_instance.define_singleton_method(name) { block.call(struct_instance) }
    end

  next if @conditions.any? { |cond| !cond.call(struct_instance) }

    yield struct_instance
  end
end

#cond(command = :print, index: nil, &block) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/flex-cartesian.rb', line 55

def cond(command = :print, index: nil, &block)
  case command
  when :set
    raise ArgumentError, "Block required" unless block_given?
    @conditions << block
    self
  when :unset
    raise ArgumentError, "Index of the condition required" unless index
    @conditions.delete_at(index)
  when :clear 
    @conditions.clear
    self
  when :print 
    return if @conditions.empty?
    @conditions.each_with_index { |cond, idx| puts "#{idx} | #{cond.source.gsub(/^.*?\s/, '')}" }
  else
    raise ArgumentError, "unknown condition command: #{command}"
  end
end

#dimensions(data = @dimensions, raw: false, separator: ', ', dimensions: true, values: true) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/flex-cartesian.rb', line 43

def dimensions(data = @dimensions, raw: false, separator: ', ', dimensions: true, values: true)
  return data.inspect if raw # by default, with no data speciaifed, we assume dimensions of Cartesian
  return nil if not dimensions and not values

  if data.is_a?(Struct) or data.is_a?(Hash) # vector in Cartesian or entire Cartesian
    data.each_pair.map { |k, v| (dimensions ? "#{k}" : "") + ((dimensions and values) ? "=" : "") + (values ? "#{v}" : "") }.join(separator)
  else
    puts "Incorrect type of dimensions: #{data.class}"
    exit
  end
end

#export(path, format: :json) ⇒ Object



311
312
313
314
315
316
317
318
319
320
# File 'lib/flex-cartesian.rb', line 311

def export(path, format: :json)
  case format
  when :json
    File.write(path, JSON.pretty_generate(@dimensions))
  when :yaml
    File.write(path, YAML.dump(@dimensions))
  else
    raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
  end
end

#from_json(path) ⇒ Object



323
324
325
326
# File 'lib/flex-cartesian.rb', line 323

def from_json(path)
  data = JSON.parse(File.read(path), symbolize_names: true)
  @dimensions = data
end

#from_yaml(path) ⇒ Object



328
329
330
331
# File 'lib/flex-cartesian.rb', line 328

def from_yaml(path)
  data = YAML.safe_load(File.read(path), symbolize_names: true)
  @dimensions = data
end

#func(command = :print, name = nil, hide: false, progress: false, title: "calculating functions", order: nil, &block) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/flex-cartesian.rb', line 75

def func(command = :print, name = nil, hide: false, progress: false, title: "calculating functions", order: nil, &block)
  case command
  when :add
    raise ArgumentError, "Function name and block required for :add" unless name && block_given?
    add_function(name, order: order, &block)
    @function_hidden.delete(name.to_sym)
    @function_hidden << name.to_sym if hide

  when :del
    raise ArgumentError, "Function name required for :del" unless name
    remove_function(name)

  when :print
    if @derived.empty?
      puts "(no functions defined)"
    else
      @derived.each do |fname, fblock|
        source = fblock.source rescue '(source unavailable)'
        body = source.sub(/^.*?\s(?=(\{|\bdo\b))/, '').strip
        order = ""
        if @order.value?(fname.to_sym)
          case @order.key(fname.to_sym)
          when :first
            order = " [FIRST]"
          when :last
            order = " [LAST]"
          end
        end
        puts "  #{fname.inspect.ljust(12)}| #{body}#{@function_hidden.include?(fname) ? ' [HIDDEN]' : ''}#{order}"
      end
    end

  when :run
    @function_results = {}

    if progress
    bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')

    cartesian do |v|
      @function_results[v] ||= {}
      @derived.each do |fname, block|
        @function_results[v][fname] = block.call(v)
      end
      bar.increment if progress
    end

  else
    cartesian do |v|
      @function_results[v] ||= {}
      @derived.each do |fname, block|
        @function_results[v][fname] = block.call(v)
      end
    end
  end

  else
    raise ArgumentError, "Unknown command for function: #{command.inspect}"
  end
end

#import(path, format: :json) ⇒ Object

Raises:

  • (TypeError)


295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/flex-cartesian.rb', line 295

def import(path, format: :json)
  data = case format
  when :json
    JSON.parse(File.read(path), symbolize_names: true)
  when :yaml
    YAML.safe_load(File.read(path), symbolize_names: true)
  else
    raise ArgumentError, "Unsupported format: #{format}. Only :json and :yaml are supported."
  end

  raise TypeError, "Expected parsed data to be a Hash" unless data.is_a?(Hash)

  @dimensions = data
  self
end

#output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/flex-cartesian.rb', line 231

def output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil)
  sep = if format == :csv
          [";", ","].include?(separator) ? separator : ";"
        else
          separator
        end
  rows = if @function_results && !@function_results.empty?
           @function_results.keys
         else
           result = []
           cartesian do |v|
             result << v
             break if limit && result.size >= limit
           end
           result
         end

  return if rows.empty?

  visible_func_names = @derived.keys - (@function_hidden || Set.new).to_a
  headers = rows.first.members.map(&:to_s) + visible_func_names.map(&:to_s)

  widths = align ? headers.to_h { |h|
    values = rows.map do |r|
      val = if r.members.map(&:to_s).include?(h)
              r.send(h)
            else
              @function_results&.dig(r, h.to_sym)
            end
      fmt_cell(val, false).size
    end
    [h, [h.size, *values].max]
  } : {}

  lines = []

  # Header
  case format
  when :markdown
    lines << "| " + headers.map { |h| h.ljust(widths[h] || h.size) }.join(" | ") + " |"
    lines << "|-" + headers.map { |h| "-" * (widths[h] || h.size) }.join("-|-") + "-|"
  when :csv
    lines << headers.join(sep)
  else
    lines << headers.map { |h| fmt_cell(h, colorize, widths[h]) }.join(sep)
  end

  # Rows
  rows.each do |row|
    values = row.members.map { |m| row.send(m) } +
             visible_func_names.map { |fname| @function_results&.dig(row, fname) }

    line = headers.zip(values).map { |(_, val)| fmt_cell(val, colorize, widths[_]) }
    lines << line.join(sep)
  end

  # Output to console or file
  if file
    File.write(file, lines.join("\n") + "\n")
  else
    lines.each { |line| puts line }
  end
end

#progress_each(lazy: false, title: "Processing") ⇒ Object



222
223
224
225
226
227
228
229
# File 'lib/flex-cartesian.rb', line 222

def progress_each(lazy: false, title: "Processing")
  bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')

  cartesian(@dimensions, lazy: lazy) do |v|
    yield v
    bar.increment
  end
end

#remove_function(name) ⇒ Object



163
164
165
166
167
# File 'lib/flex-cartesian.rb', line 163

def remove_function(name)
  @derived.delete(name.to_sym)
  @order[:last] = nil if @order[:last] == name.to_sym
  @order[:first] = nil if @order[:first] == name.to_sym
end

#sizeObject



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/flex-cartesian.rb', line 197

def size
  return 0 unless @dimensions.is_a?(Hash)
  if @conditions.empty?
    values = @dimensions.values.map { |dim| dim.is_a?(Enumerable) ? dim.to_a : [dim] }
    return 0 if values.any?(&:empty?)
    values.map(&:size).inject(1, :*)
  else
    size = 0
    cartesian do |v|
      next if @conditions.any? { |cond| !cond.call(v) }
      size += 1
    end
    size
  end
end

#to_a(limit: nil) ⇒ Object



213
214
215
216
217
218
219
220
# File 'lib/flex-cartesian.rb', line 213

def to_a(limit: nil)
  result = []
  cartesian do |v|
    result << v
    break if limit && result.size >= limit
  end
  result
end