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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/lib/furud/engine.rb +756 -0
- data/lib/furud/format.rb +244 -0
- data/lib/furud/formula.rb +538 -0
- data/lib/furud/functions.rb +794 -0
- data/lib/furud/types.rb +70 -0
- data/lib/furud/version.rb +5 -0
- data/lib/furud.rb +15 -0
- data/sig/furud.rbs +128 -0
- metadata +57 -0
data/lib/furud/engine.rb
ADDED
|
@@ -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
|