furud 0.1.0

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.
@@ -0,0 +1,756 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Furud
6
+ class Engine
7
+ ITERATION_FUNCTIONS = %w[NOW TODAY RAND RANDBETWEEN OFFSET].freeze
8
+ ERROR_HANDLING_FUNCTIONS = %w[IFERROR IFNA FILTER ISERR ISERROR ISNA ISBLANK ISNUMBER ISTEXT ISNONTEXT ISLOGICAL ISEVEN ISODD ISREF ISFORMULA ERROR.TYPE TYPE].freeze
9
+ REFERENCE_FUNCTIONS = %w[ROW COLUMN ISREF ISFORMULA FORMULATEXT CELL OFFSET].freeze
10
+
11
+ attr_reader :source, :locale
12
+
13
+ def initialize(source = nil, functions: Functions.standard, locale: :en,
14
+ iterative: false, max_iterations: 100, epsilon: 0.001)
15
+ @source = source
16
+ @functions = functions
17
+ @locale = locale.to_sym
18
+ @iterative = !!iterative
19
+ @max_iterations = Integer(max_iterations)
20
+ @epsilon = Float(epsilon)
21
+ raise ArgumentError, "max_iterations must be positive" unless @max_iterations.positive?
22
+ raise ArgumentError, "epsilon must be non-negative" if @epsilon.negative?
23
+
24
+ @inputs = {}
25
+ @formula_text = {}
26
+ @formulas = {}
27
+ @dynamic_formulas = Set.new
28
+ @volatile_formulas = Set.new
29
+ @evaluating = Set.new
30
+ @values = {}
31
+ @previous_values = {}
32
+ @dirty = Set.new
33
+ @precedents = {}
34
+ @dependents = Hash.new { |hash, key| hash[key] = Set.new }
35
+ @range_dependents = []
36
+ @names = {}
37
+ @cycles = []
38
+ @spill_values = {}
39
+ @spill_refs = Hash.new { |hash, key| hash[key] = Set.new }
40
+ @spill_parents = {}
41
+ end
42
+
43
+ def set(reference, input)
44
+ reference = coerce_reference(reference)
45
+ ast = Formula.parse(input, origin: reference) if input.is_a?(String) && input.start_with?("=")
46
+ @previous_values[reference] = @values[reference] unless @previous_values.key?(reference)
47
+ remove_formula(reference)
48
+ @inputs[reference] = input
49
+ if ast
50
+ @formulas[reference] = ast
51
+ @formula_text[reference] = input
52
+ add_formula_edges(reference, ast)
53
+ else
54
+ @formula_text.delete(reference)
55
+ @values[reference] = input
56
+ end
57
+ @dynamic_formulas.each { |cell| mark_dirty(cell) }
58
+ mark_dirty(reference)
59
+ input
60
+ end
61
+
62
+ def clear(reference)
63
+ reference = coerce_reference(reference)
64
+ @previous_values[reference] = @values[reference] unless @previous_values.key?(reference)
65
+ remove_formula(reference)
66
+ @inputs.delete(reference)
67
+ @formula_text.delete(reference)
68
+ @values.delete(reference)
69
+ @dynamic_formulas.each { |cell| mark_dirty(cell) }
70
+ mark_dirty(reference)
71
+ nil
72
+ end
73
+
74
+ def value(reference)
75
+ reference = coerce_reference(reference)
76
+ recalculate unless @dirty.empty?
77
+ return @spill_values[reference] if @spill_values.key?(reference)
78
+ return @values[reference] if @values.key?(reference)
79
+
80
+ source_value(reference)
81
+ end
82
+
83
+ def formula(reference)
84
+ @formula_text[coerce_reference(reference)]
85
+ end
86
+
87
+ def recalculate
88
+ volatile_references.each { |reference| mark_dirty(reference) }
89
+ return [] if @dirty.empty?
90
+
91
+ pending = @dirty.dup
92
+ @dirty.clear
93
+ previous_spills = @spill_values.dup
94
+ pending.each { |reference| @previous_values[reference] = @values[reference] unless @previous_values.key?(reference) }
95
+ formula_order, cycle_groups = strongly_connected_components(pending)
96
+ @cycles = @cycles.reject { |cycle| cycle.any? { |reference| pending.include?(reference) } } + cycle_groups
97
+ cycle_members = cycle_groups.flatten.to_set
98
+
99
+ pending.each { |reference| clear_spill(reference) }
100
+
101
+ if @iterative
102
+ cycle_groups.each { |cycle| iterate_cycle(cycle) }
103
+ else
104
+ cycle_members.each { |reference| store_value(reference, ErrorValue.new(code: :cycle)) }
105
+ end
106
+
107
+ formula_order.each do |reference|
108
+ next if cycle_members.include?(reference)
109
+
110
+ evaluate_cell(reference) unless @dirty.include?(reference)
111
+ end
112
+ pending.each { |reference| @previous_values.delete(reference) unless @values.key?(reference) }
113
+ changed = pending.select do |reference|
114
+ previous = @previous_values.delete(reference)
115
+ previous != value_without_recalculation(reference)
116
+ end
117
+ spill_cells = previous_spills.keys | @spill_values.keys
118
+ changed.concat(spill_cells.select { |reference| previous_spills[reference] != @spill_values[reference] })
119
+ changed.uniq.sort_by { |reference| sort_key(reference) }
120
+ end
121
+
122
+ def dirty = @dirty.dup
123
+ def cycles = @cycles.map(&:dup)
124
+
125
+ def insert_rows(sheet, at, count = 1) = adjust_cells(:insert_rows, sheet, at, count)
126
+ def delete_rows(sheet, at, count = 1) = adjust_cells(:delete_rows, sheet, at, count)
127
+ def insert_columns(sheet, at, count = 1) = adjust_cells(:insert_columns, sheet, at, count)
128
+ def delete_columns(sheet, at, count = 1) = adjust_cells(:delete_columns, sheet, at, count)
129
+
130
+ def define_name(name, area)
131
+ normalized = normalize_name(name)
132
+ @names[normalized] = area
133
+ rebuild_formula_graph
134
+ @formulas.each_key { |reference| mark_dirty(reference) }
135
+ area
136
+ end
137
+
138
+ def precedents(reference)
139
+ @precedents.fetch(coerce_reference(reference), []).dup
140
+ end
141
+
142
+ def dependents(reference)
143
+ reference = coerce_reference(reference)
144
+ direct = @dependents.fetch(reference, Set.new).to_a
145
+ ranged = @range_dependents.filter_map { |area, owner| owner if area.include?(reference) }
146
+ (direct + ranged).uniq.sort_by { |cell| sort_key(cell) }
147
+ end
148
+
149
+ private
150
+
151
+ def coerce_reference(reference)
152
+ Formula.coerce_reference(reference)
153
+ end
154
+
155
+ def remove_formula(reference)
156
+ old_precedents = @precedents.delete(reference) || []
157
+ old_precedents.each do |precedent|
158
+ if precedent.is_a?(Reference)
159
+ @dependents[precedent].delete(reference)
160
+ else
161
+ @range_dependents.delete([precedent, reference])
162
+ end
163
+ end
164
+ @formulas.delete(reference)
165
+ @dynamic_formulas.delete(reference)
166
+ @volatile_formulas.delete(reference)
167
+ @formula_text.delete(reference)
168
+ end
169
+
170
+ def add_formula_edges(reference, ast)
171
+ references = (Formula.references(ast) + dynamic_precedents(ast, reference) + name_precedents(ast, reference)).uniq
172
+ @precedents[reference] = references
173
+ @dynamic_formulas.add(reference) if has_dynamic_indirect?(ast)
174
+ @volatile_formulas.add(reference) if volatile_formula?(ast)
175
+ references.each do |precedent|
176
+ if precedent.is_a?(Reference)
177
+ @dependents[precedent].add(reference)
178
+ else
179
+ @range_dependents << [precedent, reference]
180
+ end
181
+ end
182
+ end
183
+
184
+ def mark_dirty(reference)
185
+ queue = [reference]
186
+ visited = Set.new
187
+ index = 0
188
+ while index < queue.length
189
+ current = queue[index]
190
+ index += 1
191
+ next if visited.include?(current)
192
+
193
+ visited.add(current)
194
+ @dirty.add(current)
195
+ queue.concat(@dependents.fetch(current, Set.new).to_a)
196
+ @range_dependents.each { |area, owner| queue << owner if area.include?(current) }
197
+ @spill_refs.fetch(current, Set.new).each { |spill_ref| queue.concat(@dependents.fetch(spill_ref, Set.new).to_a) }
198
+ queue << @spill_parents[current] if @spill_parents[current]
199
+ end
200
+ end
201
+
202
+ def volatile_references
203
+ @volatile_formulas
204
+ end
205
+
206
+ def dependencies(reference)
207
+ precedents = @precedents.fetch(reference, [])
208
+ if precedents.all? { |precedent| precedent.is_a?(Reference) }
209
+ return precedents.select { |precedent| @formulas.key?(precedent) }
210
+ end
211
+
212
+ (precedents.flat_map do |precedent|
213
+ if precedent.is_a?(Reference)
214
+ [precedent]
215
+ else
216
+ @formulas.each_key.select { |candidate| precedent.include?(candidate) }
217
+ end
218
+ end).select { |candidate| @formulas.key?(candidate) }.uniq
219
+ end
220
+
221
+ def strongly_connected_components(pending)
222
+ formula_cells = pending.select { |reference| @formulas.key?(reference) }
223
+ index = 0
224
+ indices = {}
225
+ low = {}
226
+ stack = []
227
+ on_stack = Set.new
228
+ components = []
229
+ ordered = []
230
+ connect = lambda do |reference|
231
+ indices[reference] = low[reference] = index
232
+ index += 1
233
+ stack << reference
234
+ on_stack.add(reference)
235
+ precedents = dependencies(reference)
236
+ precedents.each do |dependency|
237
+ next unless pending.include?(dependency) && @formulas.key?(dependency)
238
+
239
+ if !indices.key?(dependency)
240
+ connect.call(dependency)
241
+ low[reference] = [low[reference], low[dependency]].min
242
+ elsif on_stack.include?(dependency)
243
+ low[reference] = [low[reference], indices[dependency]].min
244
+ end
245
+ end
246
+ return unless low[reference] == indices[reference]
247
+
248
+ if stack.last == reference
249
+ stack.pop
250
+ on_stack.delete(reference)
251
+ ordered << reference
252
+ components << [reference] if precedents.include?(reference)
253
+ next
254
+ end
255
+
256
+ component = []
257
+ loop do
258
+ member = stack.pop
259
+ on_stack.delete(member)
260
+ component << member
261
+ break if member == reference
262
+ end
263
+ component.sort_by! { |cell| sort_key(cell) } if component.length > 1
264
+ ordered.concat(component)
265
+ self_reference = precedents.include?(reference)
266
+ components << component if component.length > 1 || self_reference
267
+ end
268
+ formula_cells.sort_by! { |reference| sort_key(reference) }
269
+ formula_cells.each { |reference| connect.call(reference) unless indices.key?(reference) }
270
+ [ordered, components]
271
+ end
272
+
273
+ def evaluate_cell(reference)
274
+ return @values[reference] unless @formulas.key?(reference)
275
+ return ErrorValue.new(code: :cycle) if @evaluating.include?(reference)
276
+
277
+ clear_spill(reference)
278
+ @evaluating.add(reference)
279
+ store_result(reference, evaluate(@formulas.fetch(reference), reference))
280
+ ensure
281
+ @evaluating.delete(reference)
282
+ end
283
+
284
+ def evaluate(node, origin)
285
+ case node.type
286
+ when :literal then node.value
287
+ when :error then node.value
288
+ when :reference, :qualified_reference then read_cell(node.value)
289
+ when :range then read_range(node, origin)
290
+ when :name then read_name(node.value, origin)
291
+ when :array
292
+ ArrayValue.new(rows: node.children.map { |row| row.children.map { |child| evaluate(child, origin) } })
293
+ when :unary
294
+ value = evaluate(node.children.first, origin)
295
+ return value if value.is_a?(ErrorValue)
296
+ return map_array(value) { |cell| unary(node.value, cell) } if value.is_a?(ArrayValue)
297
+
298
+ unary(node.value, value)
299
+ when :postfix
300
+ value = evaluate(node.children.first, origin)
301
+ return value if value.is_a?(ErrorValue)
302
+ return map_array(value) { |cell| Functions.number!(cell) / 100.0 } if value.is_a?(ArrayValue)
303
+
304
+ Functions.number!(value) / 100.0
305
+ when :binary
306
+ left = evaluate(node.children[0], origin)
307
+ return left if left.is_a?(ErrorValue)
308
+ right = evaluate(node.children[1], origin)
309
+ return right if right.is_a?(ErrorValue)
310
+ binary(node.value, left, right)
311
+ when :call then evaluate_call(node, origin)
312
+ else ErrorValue.new(code: :value)
313
+ end
314
+ rescue ZeroDivisionError
315
+ ErrorValue.new(code: :div0)
316
+ rescue Math::DomainError, RangeError
317
+ ErrorValue.new(code: :num)
318
+ rescue ArgumentError, TypeError
319
+ ErrorValue.new(code: :value)
320
+ end
321
+
322
+ def evaluate_call(node, origin)
323
+ name = node.value
324
+ entry = @functions[name]
325
+ return ErrorValue.new(code: :name) unless entry
326
+ return ErrorValue.new(code: :value) unless entry.accepts?(node.children.length)
327
+
328
+ if name == "IF"
329
+ condition = evaluate(node.children[0], origin)
330
+ return condition if condition.is_a?(ErrorValue)
331
+ return evaluate(node.children[Functions.truthy?(condition) ? 1 : 2], origin) if node.children[Functions.truthy?(condition) ? 1 : 2]
332
+
333
+ return false
334
+ end
335
+ if %w[IFERROR IFNA].include?(name)
336
+ value = evaluate(node.children[0], origin)
337
+ catch_error = value.is_a?(ErrorValue) && (name == "IFERROR" || value.code == :na)
338
+ return catch_error ? evaluate(node.children[1], origin) : value
339
+ end
340
+ if name == "IFS"
341
+ node.children.each_slice(2) do |condition_node, result_node|
342
+ return ErrorValue.new(code: :value) unless result_node
343
+ condition = evaluate(condition_node, origin)
344
+ return condition if condition.is_a?(ErrorValue)
345
+ return evaluate(result_node, origin) if Functions.truthy?(condition)
346
+ end
347
+ return ErrorValue.new(code: :na)
348
+ end
349
+ if name == "INDIRECT"
350
+ source = evaluate(node.children[0], origin)
351
+ return source if source.is_a?(ErrorValue)
352
+ ast = Formula.parse("=#{Functions.text(source)}", origin: origin)
353
+ return evaluate(ast, origin)
354
+ end
355
+ if name == "OFFSET"
356
+ return evaluate_offset(node, origin)
357
+ end
358
+
359
+ args = node.children.map { |child| function_argument(child, name, origin) }
360
+ args = [origin] if args.empty? && %w[ROW COLUMN].include?(name)
361
+ error = args.lazy.map { |value| find_error(value) }.find(&:itself)
362
+ return error if error && !ERROR_HANDLING_FUNCTIONS.include?(name)
363
+
364
+ if name == "FORMULATEXT"
365
+ return @formula_text[args.first] || ErrorValue.new(code: :na)
366
+ end
367
+ if name == "ISFORMULA"
368
+ return @formulas.key?(args.first)
369
+ end
370
+ @functions[name].call(*args)
371
+ end
372
+
373
+ def function_argument(child, name, origin)
374
+ return child.value if %i[reference qualified_reference].include?(child.type) && REFERENCE_FUNCTIONS.include?(name)
375
+ if child.type == :range && REFERENCE_FUNCTIONS.include?(name)
376
+ first, last = child.children.map(&:value)
377
+ sheet = first.sheet || last.sheet || origin.sheet
378
+ return Area.new(sheet: sheet, top: first.row, left: first.column, bottom: last.row, right: last.column)
379
+ end
380
+ evaluate(child, origin)
381
+ end
382
+
383
+ def evaluate_offset(node, origin)
384
+ base = function_argument(node.children.fetch(0), "OFFSET", origin)
385
+ return base if base.is_a?(ErrorValue)
386
+ area = base.is_a?(Area) ? base : Area.new(sheet: base.sheet || origin.sheet,
387
+ top: base.row, left: base.column,
388
+ bottom: base.row, right: base.column)
389
+ row_shift = Functions.integer!(evaluate(node.children.fetch(1), origin))
390
+ column_shift = Functions.integer!(evaluate(node.children.fetch(2), origin))
391
+ height = node.children[3] ? Functions.integer!(evaluate(node.children[3], origin)) : area.bottom - area.top + 1
392
+ width = node.children[4] ? Functions.integer!(evaluate(node.children[4], origin)) : area.right - area.left + 1
393
+ return ErrorValue.new(code: :ref) unless height.positive? && width.positive?
394
+
395
+ shifted = Area.new(sheet: area.sheet, top: area.top + row_shift, left: area.left + column_shift,
396
+ bottom: area.top + row_shift + height - 1, right: area.left + column_shift + width - 1)
397
+ return ErrorValue.new(code: :ref) if shifted.top < 1 || shifted.left < 1 || shifted.bottom > 1_048_576 || shifted.right > 16_384
398
+ return read_cell(Reference.new(sheet: shifted.sheet, row: shifted.top, column: shifted.left)) if height == 1 && width == 1
399
+
400
+ range_from_area(shifted, origin)
401
+ rescue IndexError, ArgumentError, TypeError
402
+ ErrorValue.new(code: :value)
403
+ end
404
+
405
+ def read_range(node, origin)
406
+ first, last = node.children.map(&:value)
407
+ first_sheet = first.sheet || origin.sheet
408
+ last_sheet = last.sheet || first_sheet
409
+ return ErrorValue.new(code: :ref) if first_sheet != last_sheet
410
+
411
+ area = Area.new(sheet: first_sheet, top: first.row, left: first.column,
412
+ bottom: last.row, right: last.column)
413
+ range_from_area(area, origin)
414
+ end
415
+
416
+ def range_from_area(area, origin)
417
+ cells = area.bottom - area.top + 1
418
+ columns = area.right - area.left + 1
419
+ return ErrorValue.new(code: :num) if cells * columns > 1_000_000
420
+
421
+ values = Array.new(cells) { Array.new(columns) }
422
+ @inputs.each do |reference, input|
423
+ next unless area.include?(reference)
424
+ next if @formulas.key?(reference)
425
+
426
+ values[reference.row - area.top][reference.column - area.left] = input
427
+ end
428
+ if @source&.respond_to?(:each_in)
429
+ @source.each_in(area) do |reference, value|
430
+ next unless reference.is_a?(Reference) && area.include?(reference)
431
+ next if @inputs.key?(reference)
432
+
433
+ values[reference.row - area.top][reference.column - area.left] = value
434
+ end
435
+ elsif @source&.respond_to?(:value_at)
436
+ area.top.upto(area.bottom) do |row|
437
+ area.left.upto(area.right) do |column|
438
+ reference = Reference.new(sheet: area.sheet, row: row, column: column)
439
+ next if @inputs.key?(reference)
440
+
441
+ values[row - area.top][column - area.left] = @source.value_at(reference)
442
+ end
443
+ end
444
+ end
445
+ @formulas.each_key do |reference|
446
+ next unless area.include?(reference)
447
+
448
+ values[reference.row - area.top][reference.column - area.left] = @values[reference]
449
+ end
450
+ @spill_values.each do |reference, value|
451
+ values[reference.row - area.top][reference.column - area.left] = value if area.include?(reference)
452
+ end
453
+ ArrayValue.new(rows: values)
454
+ end
455
+
456
+ def find_error(value)
457
+ case value
458
+ when ErrorValue then value
459
+ when ArrayValue then value.rows.flatten.find { |cell| cell.is_a?(ErrorValue) }
460
+ when Array then value.flatten.find { |cell| cell.is_a?(ErrorValue) }
461
+ end
462
+ end
463
+
464
+ def read_name(name, origin)
465
+ value = @names[normalize_name(name)]
466
+ return ErrorValue.new(code: :name) unless value
467
+ return read_cell(value) if value.is_a?(Reference)
468
+ return value unless value.is_a?(Area)
469
+
470
+ first = Reference.new(sheet: value.sheet || origin.sheet, row: value.top, column: value.left)
471
+ last = Reference.new(sheet: value.sheet || origin.sheet, row: value.bottom, column: value.right)
472
+ read_range(Node.new(type: :range, children: [Node.new(type: :reference, value: first), Node.new(type: :reference, value: last)]), origin)
473
+ end
474
+
475
+ def read_cell(reference)
476
+ return ErrorValue.new(code: :cycle) if @evaluating.include?(reference)
477
+
478
+ reference = reference.with(sheet: reference.sheet) if reference.sheet.nil?
479
+ return @spill_values[reference] if @spill_values.key?(reference)
480
+ return @values[reference] if @values.key?(reference)
481
+
482
+ source_value(reference)
483
+ end
484
+
485
+ def source_value(reference)
486
+ if @source.respond_to?(:value_at)
487
+ @source.value_at(reference)
488
+ elsif @source.is_a?(Hash)
489
+ @source.fetch(reference) { @source[[reference.sheet, reference.row, reference.column]] }
490
+ end
491
+ rescue KeyError
492
+ nil
493
+ end
494
+
495
+ def evaluate_binary_scalar(operator, left, right)
496
+ case operator
497
+ when "+" then Functions.number!(left) + Functions.number!(right)
498
+ when "-" then Functions.number!(left) - Functions.number!(right)
499
+ when "*" then Functions.number!(left) * Functions.number!(right)
500
+ when "/"
501
+ denominator = Functions.number!(right)
502
+ denominator.zero? ? ErrorValue.new(code: :div0) : Functions.number!(left).to_f / denominator
503
+ when "^" then Functions.number!(left)**Functions.number!(right)
504
+ when "&" then Functions.text(left) + Functions.text(right)
505
+ when "=" then compare(left, right).zero?
506
+ when "<>" then !compare(left, right).zero?
507
+ when "<" then compare(left, right).negative?
508
+ when ">" then compare(left, right).positive?
509
+ when "<=" then !compare(left, right).positive?
510
+ when ">=" then !compare(left, right).negative?
511
+ else ErrorValue.new(code: :value)
512
+ end
513
+ end
514
+
515
+ def binary(operator, left, right)
516
+ if left.is_a?(ArrayValue) || right.is_a?(ArrayValue)
517
+ left_matrix = matrix(left); right_matrix = matrix(right)
518
+ height = [left_matrix.length, right_matrix.length].max
519
+ width = [left_matrix.first.length, right_matrix.first.length].max
520
+ return ErrorValue.new(code: :value) unless [height, 1].include?(left_matrix.length) && [height, 1].include?(right_matrix.length) &&
521
+ [width, 1].include?(left_matrix.first.length) && [width, 1].include?(right_matrix.first.length)
522
+ return ArrayValue.new(rows: Array.new(height) do |row|
523
+ Array.new(width) do |column|
524
+ a = left_matrix[left_matrix.length == 1 ? 0 : row][left_matrix.first.length == 1 ? 0 : column]
525
+ b = right_matrix[right_matrix.length == 1 ? 0 : row][right_matrix.first.length == 1 ? 0 : column]
526
+ result = evaluate_binary_scalar(operator, a, b)
527
+ result
528
+ end
529
+ end)
530
+ end
531
+ evaluate_binary_scalar(operator, left, right)
532
+ end
533
+
534
+ def unary(operator, value)
535
+ number = Functions.number!(value)
536
+ operator == "-" ? -number : number
537
+ end
538
+
539
+ def compare(left, right)
540
+ if left.is_a?(Numeric) && right.is_a?(Numeric)
541
+ left <=> right
542
+ elsif left.is_a?(Date) && right.is_a?(Date)
543
+ left <=> right
544
+ else
545
+ Functions.text(left).downcase <=> Functions.text(right).downcase
546
+ end
547
+ end
548
+
549
+ def matrix(value) = value.is_a?(ArrayValue) ? value.rows : [[value]]
550
+
551
+ def map_array(value)
552
+ ArrayValue.new(rows: value.rows.map { |row| row.map { |cell| yield cell } })
553
+ end
554
+
555
+ def store_result(reference, result)
556
+ unless result.is_a?(ArrayValue)
557
+ store_value(reference, result)
558
+ return result
559
+ end
560
+
561
+ height = result.height
562
+ width = result.width
563
+ spill = {}
564
+ result.rows.each_with_index do |row, row_offset|
565
+ row.each_with_index do |value, column_offset|
566
+ next if row_offset.zero? && column_offset.zero?
567
+
568
+ target = Reference.new(sheet: reference.sheet, row: reference.row + row_offset,
569
+ column: reference.column + column_offset)
570
+ if target.row > 1_048_576 || target.column > 16_384 || occupied?(target)
571
+ store_value(reference, ErrorValue.new(code: :spill))
572
+ return @values[reference]
573
+ end
574
+ spill[target] = value
575
+ end
576
+ end
577
+ store_value(reference, result[0, 0])
578
+ @spill_refs[reference] = spill.keys.to_set
579
+ spill.each do |target, value|
580
+ @spill_values[target] = value
581
+ @spill_parents[target] = reference
582
+ end
583
+ @values[reference]
584
+ end
585
+
586
+ def clear_spill(reference)
587
+ @spill_refs.delete(reference)&.each do |spill_ref|
588
+ @spill_values.delete(spill_ref)
589
+ @spill_parents.delete(spill_ref)
590
+ end
591
+ end
592
+
593
+ def occupied?(reference)
594
+ @inputs.key?(reference) || @spill_parents.key?(reference) || !source_value(reference).nil?
595
+ end
596
+
597
+ def store_value(reference, value)
598
+ @values[reference] = value
599
+ end
600
+
601
+ def value_without_recalculation(reference)
602
+ return @spill_values[reference] if @spill_values.key?(reference)
603
+ return @values[reference] if @values.key?(reference)
604
+
605
+ source_value(reference)
606
+ end
607
+
608
+ def iterate_cycle(cycle)
609
+ cycle.each { |reference| @values[reference] = 0 unless @values.key?(reference) }
610
+ converged = false
611
+ @max_iterations.times do
612
+ previous = cycle.to_h { |reference| [reference, @values[reference]] }
613
+ cycle.each { |reference| store_result(reference, evaluate(@formulas.fetch(reference), reference)) }
614
+ converged = cycle.all? do |reference|
615
+ old = previous[reference]; current = @values[reference]
616
+ old.is_a?(Numeric) && current.is_a?(Numeric) ? (old - current).abs <= @epsilon : old == current
617
+ end
618
+ break if converged
619
+ end
620
+ cycle.each { |reference| store_value(reference, ErrorValue.new(code: :cycle)) } unless converged
621
+ end
622
+
623
+ def adjust_cells(type, sheet, at, count)
624
+ operation = Adjustment.new(type: type, sheet: sheet, at: at, count: count)
625
+ moved_inputs = {}
626
+ moved_formula_text = {}
627
+ @inputs.each do |reference, input|
628
+ moved_reference = move_cell(reference, operation)
629
+ next unless moved_reference
630
+
631
+ moved_inputs[moved_reference] = input
632
+ if @formulas.key?(reference)
633
+ ast = Formula.adjust(@formulas.fetch(reference), operation)
634
+ moved_formula_text[moved_reference] = Formula.render(ast, origin: moved_reference)
635
+ end
636
+ end
637
+ @inputs = moved_inputs
638
+ @formula_text = moved_formula_text
639
+ @formulas = moved_formula_text.to_h { |reference, text| [reference, Formula.parse(text, origin: reference)] }
640
+ @dynamic_formulas = @formulas.filter_map { |reference, ast| reference if has_dynamic_indirect?(ast) }.to_set
641
+ @volatile_formulas = @formulas.filter_map { |reference, ast| reference if volatile_formula?(ast) }.to_set
642
+ @values = @inputs.reject { |reference, _| @formulas.key?(reference) }
643
+ @previous_values = {}
644
+ @spill_values.clear
645
+ @spill_refs.clear
646
+ @spill_parents.clear
647
+ @cycles.clear
648
+ rebuild_formula_graph
649
+ @dirty = @inputs.keys.to_set
650
+ @inputs.keys.each { |reference| mark_dirty(reference) }
651
+ self
652
+ end
653
+
654
+ def rebuild_formula_graph
655
+ @precedents.clear
656
+ @dependents.clear
657
+ @range_dependents.clear
658
+ @formulas.each { |reference, ast| add_formula_edges(reference, ast) }
659
+ end
660
+
661
+ def move_cell(reference, operation)
662
+ return reference unless operation.sheet.nil? || reference.sheet == operation.sheet
663
+
664
+ axis = operation.type.to_s.end_with?("rows") ? :row : :column
665
+ coordinate = reference.public_send(axis)
666
+ if operation.type.to_s.start_with?("insert")
667
+ coordinate += operation.count if coordinate >= operation.at
668
+ elsif coordinate >= operation.at && coordinate < operation.at + operation.count
669
+ return nil
670
+ elsif coordinate >= operation.at + operation.count
671
+ coordinate -= operation.count
672
+ end
673
+ reference.with(axis => coordinate)
674
+ end
675
+
676
+ def normalize_name(name) = name.to_s.downcase
677
+
678
+ def has_dynamic_indirect?(ast)
679
+ found = false
680
+ Formula.visit(ast) { |node| found ||= node.type == :call && node.value == "INDIRECT" && node.children.first&.type != :literal }
681
+ found
682
+ end
683
+
684
+ def volatile_formula?(ast)
685
+ found = false
686
+ Formula.visit(ast) do |node|
687
+ next unless node.type == :call
688
+
689
+ entry = @functions[node.value]
690
+ found ||= entry&.volatile || ITERATION_FUNCTIONS.include?(node.value)
691
+ end
692
+ found
693
+ end
694
+
695
+ def dynamic_precedents(ast, origin)
696
+ references = []
697
+ Formula.visit(ast) do |node|
698
+ next unless node.type == :call
699
+
700
+ if node.value == "INDIRECT" && node.children.first&.type == :literal && node.children.first.value.is_a?(String)
701
+ begin
702
+ references.concat(Formula.references(Formula.parse("=#{node.children.first.value}", origin: origin)))
703
+ rescue ParseError
704
+ nil
705
+ end
706
+ elsif node.value == "OFFSET" && node.children.first
707
+ next if node.children.length < 3
708
+
709
+ base_node = node.children.first
710
+ base = if base_node.type == :reference
711
+ base_node.value
712
+ elsif base_node.type == :range
713
+ first, last = base_node.children.map(&:value)
714
+ Area.new(sheet: first.sheet || last.sheet || origin.sheet,
715
+ top: first.row, left: first.column, bottom: last.row, right: last.column)
716
+ end
717
+ next unless base
718
+ next unless node.children[1..2].all? { |arg| arg&.type == :literal && arg.value.is_a?(Numeric) }
719
+
720
+ row_delta, column_delta = node.children[1..2].map { |arg| arg.value.to_i }
721
+ top = (base.is_a?(Area) ? base.top : base.row) + row_delta
722
+ left = (base.is_a?(Area) ? base.left : base.column) + column_delta
723
+ height = node.children[3]&.value || (base.is_a?(Area) ? base.bottom - base.top + 1 : 1)
724
+ width = node.children[4]&.value || (base.is_a?(Area) ? base.right - base.left + 1 : 1)
725
+ if [height, width].all? { |size| size.is_a?(Numeric) && size.positive? }
726
+ references << if height == 1 && width == 1
727
+ Reference.new(sheet: base.is_a?(Area) ? base.sheet : (base.sheet || origin.sheet), row: top, column: left)
728
+ else
729
+ Area.new(sheet: base.is_a?(Area) ? base.sheet : (base.sheet || origin.sheet), top: top, left: left,
730
+ bottom: top + height.to_i - 1, right: left + width.to_i - 1)
731
+ end
732
+ end
733
+ end
734
+ end
735
+ references
736
+ end
737
+
738
+ def name_precedents(ast, origin)
739
+ references = []
740
+ Formula.visit(ast) do |node|
741
+ next unless node.type == :name
742
+
743
+ area = @names[normalize_name(node.value)]
744
+ case area
745
+ when Reference
746
+ references << (area.sheet.nil? ? area.with(sheet: origin.sheet) : area)
747
+ when Area
748
+ references << (area.sheet.nil? ? area.with(sheet: origin.sheet) : area)
749
+ end
750
+ end
751
+ references
752
+ end
753
+
754
+ def sort_key(reference) = [reference.sheet.to_s, reference.row, reference.column]
755
+ end
756
+ end