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,538 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Furud
6
+ module Formula
7
+ Token = Data.define(:type, :value, :position)
8
+ COLUMNS = (1..16_384).to_h do |number|
9
+ value = number
10
+ name = +""
11
+ while value.positive?
12
+ value, remainder = (value - 1).divmod(26)
13
+ name.prepend((65 + remainder).chr)
14
+ end
15
+ [name, number]
16
+ end.freeze
17
+ COLUMN_NAMES = COLUMNS.invert.freeze
18
+ ERROR_CODES = Furud::ERROR_CODES.to_h { |code, text| [text.upcase, code] }.freeze
19
+
20
+ module_function
21
+
22
+ def parse(source, origin: nil)
23
+ Parser.new(source.to_s, origin: coerce_reference(origin)).parse
24
+ end
25
+
26
+ def render(ast, origin: nil)
27
+ "=#{render_node(ast, coerce_reference(origin))}"
28
+ end
29
+
30
+ def references(ast)
31
+ result = []
32
+ collect = lambda do |node|
33
+ case node.type
34
+ when :reference, :qualified_reference
35
+ result << node.value
36
+ when :range
37
+ first, last = node.children.map(&:value)
38
+ result << Area.new(sheet: first.sheet || last.sheet,
39
+ top: first.row, left: first.column,
40
+ bottom: last.row, right: last.column)
41
+ else
42
+ node.children.each { |child| collect.call(child) }
43
+ end
44
+ end
45
+ collect.call(ast)
46
+ result.uniq
47
+ end
48
+
49
+ def translate(ast, from:, to:)
50
+ from = coerce_reference(from)
51
+ to = coerce_reference(to)
52
+ transform(ast) do |node|
53
+ case node.type
54
+ when :range
55
+ children = node.children.map { |child| translate_reference(child, from, to) }
56
+ children.any? { |child| child.type == :error } ? ref_error_node : Node.new(type: :range, children: children)
57
+ when :reference, :qualified_reference
58
+ translate_reference(node, from, to)
59
+ else
60
+ node
61
+ end
62
+ end
63
+ end
64
+
65
+ def adjust(ast, operation)
66
+ operation = normalize_adjustment(operation)
67
+ adjust_node(ast, operation)
68
+ end
69
+
70
+ def coerce_reference(reference)
71
+ case reference
72
+ when Reference then reference
73
+ when Hash
74
+ Reference.new(sheet: reference[:sheet] || reference["sheet"],
75
+ row: reference[:row] || reference["row"] || 1,
76
+ column: reference[:column] || reference["column"] || 1,
77
+ absolute_row: reference[:absolute_row] || reference["absolute_row"],
78
+ absolute_column: reference[:absolute_column] || reference["absolute_column"])
79
+ when nil then Reference.new(sheet: nil, row: 1, column: 1)
80
+ else raise ArgumentError, "origin must be a Furud::Reference, Hash, or nil"
81
+ end
82
+ end
83
+
84
+ def column_number(name)
85
+ COLUMNS[name.upcase]
86
+ end
87
+
88
+ def column_name(number)
89
+ COLUMN_NAMES.fetch(number) { raise ParseError, "column outside A:XFD" }
90
+ end
91
+
92
+ def render_reference(reference)
93
+ prefix = reference.sheet ? "#{render_sheet(reference.sheet)}!" : ""
94
+ row = reference.absolute_row ? "$#{reference.row}" : reference.row.to_s
95
+ column = reference.absolute_column ? "$#{column_name(reference.column)}" : column_name(reference.column)
96
+ "#{prefix}#{column}#{row}"
97
+ end
98
+
99
+ def render_reference_for(reference, origin)
100
+ render_reference(reference.sheet == origin.sheet ? reference.with(sheet: nil) : reference)
101
+ end
102
+
103
+ def render_node(node, origin, parent_precedence = 0)
104
+ case node.type
105
+ when :literal then render_literal(node.value)
106
+ when :reference then render_reference_for(node.value, origin)
107
+ when :qualified_reference then render_reference(node.value)
108
+ when :range
109
+ first, last = node.children
110
+ if last.type == :reference && first.value.sheet == last.value.sheet
111
+ last = Node.new(type: :reference, value: last.value.with(sheet: nil))
112
+ end
113
+ "#{render_node(first, origin, 100)}:#{render_node(last, origin, 100)}"
114
+ when :name then node.value
115
+ when :error then node.value.to_s
116
+ when :array
117
+ "{" + node.children.map { |row| row.children.map { |child| render_node(child, origin) }.join(",") }.join(";") + "}"
118
+ when :call
119
+ "#{node.value}(#{node.children.map { |child| render_node(child, origin) }.join(",")})"
120
+ when :unary
121
+ op = node.value
122
+ child = render_node(node.children.first, origin, 60)
123
+ wrap("#{op}#{child}", 60, parent_precedence)
124
+ when :postfix
125
+ wrap("#{render_node(node.children.first, origin, 70)}%", 70, parent_precedence)
126
+ when :binary
127
+ precedence = PRECEDENCE.fetch(node.value)
128
+ left, right = node.children
129
+ right_precedence = precedence + (node.value == "^" ? 0 : 1)
130
+ wrap("#{render_node(left, origin, precedence)}#{node.value}#{render_node(right, origin, right_precedence)}",
131
+ precedence, parent_precedence)
132
+ else raise Error, "unknown formula node: #{node.type}"
133
+ end
134
+ end
135
+
136
+ PRECEDENCE = { "=" => 10, "<>" => 10, "<" => 10, ">" => 10, "<=" => 10, ">=" => 10,
137
+ "&" => 20, "+" => 30, "-" => 30, "*" => 40, "/" => 40, "^" => 50 }.freeze
138
+
139
+ def visit(node, &block)
140
+ yield node
141
+ node.children.each { |child| visit(child, &block) }
142
+ end
143
+
144
+ def transform(node, &block)
145
+ replaced = block.call(node)
146
+ return replaced unless replaced.equal?(node)
147
+
148
+ children = node.children.map { |child| transform(child, &block) }
149
+ children == node.children ? node : Node.new(type: node.type, value: node.value, children: children)
150
+ end
151
+
152
+ def translate_reference(node, from, to)
153
+ reference = node.value
154
+ row = reference.absolute_row ? reference.row : to.row + reference.row - from.row
155
+ column = reference.absolute_column ? reference.column : to.column + reference.column - from.column
156
+ return ref_error_node if row < 1 || column < 1
157
+
158
+ sheet = node.type == :reference && reference.sheet == from.sheet ? to.sheet : reference.sheet
159
+ ref_node(reference.with(row: row, column: column, sheet: sheet), node.type)
160
+ end
161
+
162
+ def ref_error_node
163
+ Node.new(type: :error, value: ErrorValue.new(code: :ref))
164
+ end
165
+
166
+ def adjust_node(node, operation)
167
+ case node.type
168
+ when :reference, :qualified_reference
169
+ adjust_reference(node.value, operation, node.type)
170
+ when :range
171
+ first_node, last_node = node.children
172
+ first, last = first_node.value, last_node.value
173
+ adjusted = adjust_range(first, last, operation)
174
+ adjusted.is_a?(ErrorValue) ? Node.new(type: :error, value: adjusted) :
175
+ Node.new(type: :range, value: nil, children: [ref_node(adjusted[0], first_node.type), ref_node(adjusted[1], last_node.type)])
176
+ else
177
+ children = node.children.map { |child| adjust_node(child, operation) }
178
+ children == node.children ? node : Node.new(type: node.type, value: node.value, children: children)
179
+ end
180
+ end
181
+
182
+ def adjust_reference(reference, operation, node_type)
183
+ axis = operation.type.to_s.end_with?("rows") ? :row : :column
184
+ kind = operation.type.to_s.start_with?("insert") ? :insert : :delete
185
+ return ref_node(reference, node_type) unless operation.sheet.nil? || reference.sheet == operation.sheet
186
+
187
+ coordinate = reference.public_send(axis)
188
+ if kind == :insert
189
+ coordinate += operation.count if coordinate >= operation.at
190
+ elsif coordinate >= operation.at && coordinate < operation.at + operation.count
191
+ return Node.new(type: :error, value: ErrorValue.new(code: :ref))
192
+ elsif coordinate >= operation.at + operation.count
193
+ coordinate -= operation.count
194
+ end
195
+ ref_node(reference.with(axis => coordinate), node_type)
196
+ end
197
+
198
+ def adjust_range(first, last, operation)
199
+ axis = operation.type.to_s.end_with?("rows") ? :row : :column
200
+ kind = operation.type.to_s.start_with?("insert") ? :insert : :delete
201
+ sheet = first.sheet || last.sheet
202
+ return [first, last] unless operation.sheet.nil? || sheet == operation.sheet
203
+
204
+ start = [first.public_send(axis), last.public_send(axis)].min
205
+ finish = [first.public_send(axis), last.public_send(axis)].max
206
+ if kind == :insert
207
+ if operation.at <= start
208
+ start += operation.count
209
+ finish += operation.count
210
+ elsif operation.at <= finish
211
+ finish += operation.count
212
+ end
213
+ else
214
+ deleted_end = operation.at + operation.count - 1
215
+ overlap = [finish, deleted_end].min - [start, operation.at].max + 1
216
+ if overlap >= finish - start + 1
217
+ return ErrorValue.new(code: :ref)
218
+ elsif overlap.positive?
219
+ finish -= overlap
220
+ start = operation.at if start >= operation.at
221
+ elsif start > deleted_end
222
+ start -= operation.count
223
+ finish -= operation.count
224
+ end
225
+ end
226
+ first_value = first.public_send(axis)
227
+ last_value = last.public_send(axis)
228
+ reversed = first_value > last_value
229
+ if axis == :row
230
+ new_first = first.with(row: reversed ? finish : start)
231
+ new_last = last.with(row: reversed ? start : finish)
232
+ else
233
+ new_first = first.with(column: reversed ? finish : start)
234
+ new_last = last.with(column: reversed ? start : finish)
235
+ end
236
+ [new_first, new_last]
237
+ end
238
+
239
+ def normalize_adjustment(operation)
240
+ return operation if operation.is_a?(Adjustment)
241
+
242
+ values = operation.transform_keys(&:to_sym)
243
+ Adjustment.new(type: values.fetch(:type), sheet: values[:sheet],
244
+ at: values.fetch(:at), count: values.fetch(:count, 1))
245
+ end
246
+
247
+ def ref_node(reference, node_type = :reference)
248
+ return Node.new(type: :error, value: ErrorValue.new(code: :ref)) unless reference.row.between?(1, 1_048_576) && reference.column.between?(1, 16_384)
249
+
250
+ Node.new(type: node_type, value: reference)
251
+ end
252
+
253
+ def render_literal(value)
254
+ case value
255
+ when String then "\"#{value.gsub('"', '""')}\""
256
+ when true then "TRUE"
257
+ when false then "FALSE"
258
+ else value.to_s
259
+ end
260
+ end
261
+
262
+ def render_sheet(sheet)
263
+ return sheet if sheet.match?(/\A[A-Za-z_][A-Za-z0-9_.]*\z/)
264
+
265
+ "'#{sheet.gsub("'", "''")}'"
266
+ end
267
+
268
+ def wrap(text, precedence, parent_precedence)
269
+ precedence < parent_precedence ? "(#{text})" : text
270
+ end
271
+
272
+ class Lexer
273
+ def initialize(source, origin)
274
+ @source = source
275
+ @origin = origin
276
+ @scanner = StringScanner.new(source)
277
+ end
278
+
279
+ def tokens
280
+ result = []
281
+ until @scanner.eos?
282
+ @scanner.skip(/\s+/)
283
+ break if @scanner.eos?
284
+
285
+ position = @scanner.pos
286
+ result << Token.new(*next_token(position))
287
+ end
288
+ result << Token.new(:eof, nil, @scanner.pos)
289
+ end
290
+
291
+ private
292
+
293
+ def next_token(position)
294
+ if (raw = @scanner.scan(/'(?:[^']|'')+'!\$?[A-Za-z]{1,3}\$?\d+/))
295
+ sheet, address = raw.split("!", 2)
296
+ return [:qualified_reference, parse_a1(address, sheet[1...-1].gsub("''", "'")), position]
297
+ end
298
+ if (raw = @scanner.scan(/[A-Za-z_][A-Za-z0-9_.]*!\$?[A-Za-z]{1,3}\$?\d+/))
299
+ sheet, address = raw.split("!", 2)
300
+ return [:qualified_reference, parse_a1(address, sheet), position]
301
+ end
302
+ if (raw = @scanner.scan(/R(?:\d+|\[-?\d+\])?C(?:\d+|\[-?\d+\])?/i))
303
+ return [:reference, parse_r1c1(raw), position]
304
+ end
305
+ if (raw = @scanner.scan(/\$?[A-Za-z]{1,3}\$?\d+/))
306
+ if @scanner.rest.match?(/\A\s*\(/)
307
+ return [:identifier, raw, position]
308
+ end
309
+ return [:reference, parse_a1(raw, nil), position]
310
+ end
311
+ if (raw = @scanner.scan(/#(?:DIV\/0!|VALUE!|REF!|NAME\?|N\/A|NUM!|CYCLE!|SPILL!|CALC!)/i))
312
+ code = ERROR_CODES[raw.upcase]
313
+ return [:error, ErrorValue.new(code: code), position]
314
+ end
315
+ if @scanner.scan(/"/)
316
+ value = +""
317
+ until @scanner.eos?
318
+ part = @scanner.scan(/[^\"]+/)
319
+ value << part if part
320
+ break unless @scanner.scan(/"/)
321
+ if @scanner.scan(/"/)
322
+ value << '"'
323
+ else
324
+ return [:string, value, position]
325
+ end
326
+ end
327
+ raise ParseError, "unterminated string at #{position}"
328
+ end
329
+ if (raw = @scanner.scan(/(?:\d+(?:\.\d*)?|\.\d+)(?:[Ee][+-]?\d+)?/))
330
+ value = raw.match?(/[.Ee]/) ? Float(raw) : Integer(raw)
331
+ return [:number, value, position]
332
+ end
333
+ if (operator = @scanner.scan(/<=|>=|<>|[+\-*\/^&%=<>:(),;{}]/))
334
+ type = { "(" => :lparen, ")" => :rparen, "," => :separator, ";" => :row_separator,
335
+ "{" => :lbrace, "}" => :rbrace, ":" => :colon }[operator] || :operator
336
+ return [type, operator, position]
337
+ end
338
+ if (identifier = @scanner.scan(/[A-Za-z_\\][A-Za-z0-9_.\\]*/))
339
+ value = identifier.upcase
340
+ return [:boolean, value == "TRUE", position] if %w[TRUE FALSE].include?(value)
341
+
342
+ return [:identifier, identifier, position]
343
+ end
344
+
345
+ raise ParseError, "unexpected character #{@scanner.peek(1).inspect} at #{position}"
346
+ end
347
+
348
+ def parse_a1(raw, sheet)
349
+ match = /\A(\$?)([A-Za-z]{1,3})(\$?)(\d+)\z/.match(raw)
350
+ column = Formula.column_number(match[2])
351
+ row = Integer(match[4])
352
+ raise ParseError, "cell reference outside A1:XFD1048576: #{raw}" unless column && row.between?(1, 1_048_576)
353
+
354
+ Reference.new(sheet: sheet, row: row, column: column,
355
+ absolute_row: !match[3].empty?, absolute_column: !match[1].empty?)
356
+ end
357
+
358
+ def parse_r1c1(raw)
359
+ match = /\AR(?:(\d+)|\[(-?\d+)\])?C(?:(\d+)|\[(-?\d+)\])?\z/i.match(raw)
360
+ row = match[1] ? Integer(match[1]) : @origin.row + (match[2] ? Integer(match[2]) : 0)
361
+ column = match[3] ? Integer(match[3]) : @origin.column + (match[4] ? Integer(match[4]) : 0)
362
+ Reference.new(sheet: @origin.sheet, row: row, column: column,
363
+ absolute_row: !match[1].nil?, absolute_column: !match[3].nil?)
364
+ rescue ArgumentError
365
+ raise ParseError, "invalid R1C1 reference: #{raw}"
366
+ end
367
+ end
368
+
369
+ class Parser
370
+ def initialize(source, origin:)
371
+ @origin = origin
372
+ source = source.sub(/\A\s*=/, "")
373
+ @tokens = Lexer.new(source, origin).tokens
374
+ @index = 0
375
+ end
376
+
377
+ def parse
378
+ raise ParseError, "formula is empty" if current.type == :eof
379
+
380
+ node = comparison
381
+ raise ParseError, "unexpected token #{current.value.inspect} at #{current.position}" unless current.type == :eof
382
+
383
+ qualify_sheets(node, @origin.sheet)
384
+ end
385
+
386
+ private
387
+
388
+ def comparison
389
+ binary_chain(:concat, %w[= <> < > <= >=])
390
+ end
391
+
392
+ def concat
393
+ binary_chain(:additive, ["&"])
394
+ end
395
+
396
+ def additive
397
+ binary_chain(:multiplicative, %w[+ -])
398
+ end
399
+
400
+ def multiplicative
401
+ binary_chain(:unary, %w[* /])
402
+ end
403
+
404
+ def unary
405
+ if current.type == :operator && %w[+ -].include?(current.value)
406
+ op = advance.value
407
+ return Node.new(type: :unary, value: op, children: [unary])
408
+ end
409
+
410
+ exponent
411
+ end
412
+
413
+ def exponent
414
+ node = postfix
415
+ if accept_operator("^")
416
+ node = Node.new(type: :binary, value: "^", children: [node, unary])
417
+ end
418
+ node
419
+ end
420
+
421
+ def postfix
422
+ node = primary
423
+ node = Node.new(type: :range, value: nil, children: [node, reference_primary]) if current.type == :colon && advance
424
+ node = Node.new(type: :postfix, value: "%", children: [node]) while accept_operator("%")
425
+ node
426
+ end
427
+
428
+ def primary
429
+ token = advance
430
+ case token.type
431
+ when :number then Node.new(type: :literal, value: token.value)
432
+ when :string then Node.new(type: :literal, value: token.value)
433
+ when :boolean then Node.new(type: :literal, value: token.value)
434
+ when :error then Node.new(type: :error, value: token.value)
435
+ when :reference then Node.new(type: :reference, value: token.value)
436
+ when :qualified_reference then Node.new(type: :qualified_reference, value: token.value)
437
+ when :identifier then identifier(token)
438
+ when :lparen
439
+ node = comparison
440
+ expect(:rparen)
441
+ node
442
+ when :lbrace then array_literal
443
+ else raise ParseError, "expected a value at #{token.position}"
444
+ end
445
+ end
446
+
447
+ def reference_primary
448
+ token = advance
449
+ raise ParseError, "range endpoint must be a cell reference at #{token.position}" unless %i[reference qualified_reference].include?(token.type)
450
+
451
+ Node.new(type: token.type, value: token.value)
452
+ end
453
+
454
+ def qualify_sheets(node, default_sheet)
455
+ if %i[reference qualified_reference].include?(node.type)
456
+ reference = node.value
457
+ return node if node.type == :qualified_reference || reference.sheet
458
+ return Node.new(type: :reference, value: reference.with(sheet: default_sheet))
459
+ end
460
+ if node.type == :range
461
+ first_node, last_node = node.children
462
+ first, last = first_node.value, last_node.value
463
+ first_sheet = first.sheet || default_sheet
464
+ last_sheet = last.sheet || (first_node.type == :qualified_reference ? first_sheet : default_sheet)
465
+ return Node.new(type: :range, value: nil, children: [
466
+ Node.new(type: first_node.type, value: first.with(sheet: first_sheet)),
467
+ Node.new(type: last_node.type, value: last.with(sheet: last_sheet))
468
+ ])
469
+ end
470
+
471
+ children = node.children.map { |child| qualify_sheets(child, default_sheet) }
472
+ children == node.children ? node : Node.new(type: node.type, value: node.value, children: children)
473
+ end
474
+
475
+ def identifier(token)
476
+ if current.type == :lparen
477
+ advance
478
+ args = []
479
+ unless current.type == :rparen
480
+ loop do
481
+ args << comparison
482
+ break unless current.type == :separator
483
+
484
+ advance
485
+ end
486
+ end
487
+ expect(:rparen)
488
+ Node.new(type: :call, value: token.value.delete_prefix("_xlfn.").upcase, children: args)
489
+ else
490
+ Node.new(type: :name, value: token.value)
491
+ end
492
+ end
493
+
494
+ def array_literal
495
+ rows = [[]]
496
+ until current.type == :rbrace
497
+ rows.last << comparison
498
+ case current.type
499
+ when :separator then advance
500
+ when :row_separator then advance; rows << []
501
+ when :rbrace then break
502
+ else raise ParseError, "expected array separator at #{current.position}"
503
+ end
504
+ end
505
+ expect(:rbrace)
506
+ raise ParseError, "empty array" if rows.any?(&:empty?)
507
+ raise ParseError, "array rows must have equal length" unless rows.map(&:length).uniq.one?
508
+
509
+ Node.new(type: :array, children: rows.map { |row| Node.new(type: :row, children: row) })
510
+ end
511
+
512
+ def binary_chain(next_method, operators)
513
+ node = send(next_method)
514
+ while current.type == :operator && operators.include?(current.value)
515
+ operator = advance.value
516
+ node = Node.new(type: :binary, value: operator, children: [node, send(next_method)])
517
+ end
518
+ node
519
+ end
520
+
521
+ def accept_operator(value)
522
+ return false unless current.type == :operator && current.value == value
523
+
524
+ advance
525
+ true
526
+ end
527
+
528
+ def expect(type)
529
+ return advance if current.type == type
530
+
531
+ raise ParseError, "expected #{type}, got #{current.type} at #{current.position}"
532
+ end
533
+
534
+ def current = @tokens[@index]
535
+ def advance = @tokens[@index].tap { @index += 1 }
536
+ end
537
+ end
538
+ end