excel_to_code 0.1.23 → 0.2.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/src/commands/excel_to_c.rb +39 -92
  3. data/src/commands/excel_to_ruby.rb +9 -35
  4. data/src/commands/excel_to_x.rb +515 -536
  5. data/src/compile/c/a.out +0 -0
  6. data/src/compile/c/compile_named_reference_setters.rb +4 -6
  7. data/src/compile/c/compile_to_c.rb +34 -21
  8. data/src/compile/c/compile_to_c_header.rb +7 -7
  9. data/src/compile/c/excel_to_c_runtime.c +8 -4
  10. data/src/compile/c/map_formulae_to_c.rb +85 -86
  11. data/src/compile/c/map_values_to_c.rb +7 -1
  12. data/src/compile/c/map_values_to_c_structs.rb +1 -1
  13. data/src/compile/ruby/compile_to_ruby.rb +14 -11
  14. data/src/compile/ruby/compile_to_ruby_unit_test.rb +17 -10
  15. data/src/compile/ruby/map_formulae_to_ruby.rb +56 -56
  16. data/src/compile/ruby/map_values_to_ruby.rb +14 -2
  17. data/src/excel/area.rb +6 -8
  18. data/src/excel/excel_functions/hlookup.rb +1 -1
  19. data/src/excel/excel_functions/vlookup.rb +1 -1
  20. data/src/excel/formula_peg.rb +1 -1
  21. data/src/excel/formula_peg.txt +1 -1
  22. data/src/excel/reference.rb +4 -3
  23. data/src/excel/table.rb +4 -4
  24. data/src/extract.rb +1 -0
  25. data/src/extract/check_for_unknown_functions.rb +2 -2
  26. data/src/extract/extract_array_formulae.rb +9 -9
  27. data/src/extract/extract_everything.rb +140 -0
  28. data/src/extract/extract_formulae.rb +30 -20
  29. data/src/extract/extract_named_references.rb +37 -22
  30. data/src/extract/extract_relationships.rb +16 -3
  31. data/src/extract/extract_shared_formulae.rb +8 -11
  32. data/src/extract/extract_shared_formulae_targets.rb +1 -6
  33. data/src/extract/extract_shared_strings.rb +21 -8
  34. data/src/extract/extract_simple_formulae.rb +11 -6
  35. data/src/extract/extract_table.rb +26 -13
  36. data/src/extract/extract_values.rb +35 -11
  37. data/src/extract/extract_worksheet_dimensions.rb +13 -3
  38. data/src/extract/extract_worksheet_names.rb +16 -3
  39. data/src/extract/extract_worksheet_table_relationships.rb +16 -4
  40. data/src/extract/simple_extract_from_xml.rb +9 -11
  41. data/src/rewrite.rb +3 -0
  42. data/src/rewrite/ast_copy_formula.rb +5 -1
  43. data/src/rewrite/ast_expand_array_formulae.rb +71 -59
  44. data/src/rewrite/caching_formula_parser.rb +110 -0
  45. data/src/rewrite/rewrite_array_formulae.rb +21 -14
  46. data/src/rewrite/rewrite_cell_references_to_include_sheet.rb +41 -13
  47. data/src/rewrite/rewrite_shared_formulae.rb +17 -18
  48. data/src/rewrite/rewrite_values_to_ast.rb +2 -0
  49. data/src/rewrite/rewrite_whole_row_column_references_to_areas.rb +28 -25
  50. data/src/simplify.rb +1 -0
  51. data/src/simplify/count_formula_references.rb +22 -23
  52. data/src/simplify/emergency_array_formula_replace_indirect_bodge.rb +44 -0
  53. data/src/simplify/identify_dependencies.rb +7 -8
  54. data/src/simplify/identify_repeated_formula_elements.rb +5 -6
  55. data/src/simplify/inline_formulae.rb +48 -48
  56. data/src/simplify/map_formulae_to_values.rb +197 -79
  57. data/src/simplify/remove_cells.rb +13 -6
  58. data/src/simplify/replace_arithmetic_on_ranges.rb +42 -28
  59. data/src/simplify/replace_arrays_with_single_cells.rb +11 -5
  60. data/src/simplify/replace_column_with_column_number.rb +31 -23
  61. data/src/simplify/replace_common_elements_in_formulae.rb +16 -17
  62. data/src/simplify/replace_indirects_with_references.rb +26 -21
  63. data/src/simplify/replace_named_references.rb +26 -31
  64. data/src/simplify/replace_offsets_with_references.rb +33 -34
  65. data/src/simplify/replace_ranges_with_array_literals.rb +48 -20
  66. data/src/simplify/replace_shared_strings.rb +15 -13
  67. data/src/simplify/replace_string_join_on_ranges.rb +7 -9
  68. data/src/simplify/replace_table_references.rb +16 -11
  69. data/src/simplify/replace_values_with_constants.rb +6 -4
  70. data/src/simplify/simplify_arithmetic.rb +33 -19
  71. data/src/simplify/sort_into_calculation_order.rb +13 -13
  72. data/src/simplify/wrap_formulae_that_return_arrays_and_are_not_in_arrays.rb +21 -13
  73. metadata +19 -2
@@ -1,42 +1,39 @@
1
1
  class ReplaceOffsetsWithReferencesAst
2
2
 
3
- attr_accessor :replacements_made_in_the_last_pass
3
+ attr_accessor :count_replaced
4
+ attr_accessor :replacement_made
4
5
 
5
6
  def initialize
6
- @replacements_made_in_the_last_pass = 0
7
+ @count_replaced = 0
7
8
  end
8
9
 
10
+ def replace(ast)
11
+ @replacement_made = false
12
+ map(ast)
13
+ @replacement_made
14
+ end
15
+
9
16
  def map(ast)
10
17
  return ast unless ast.is_a?(Array)
11
- operator = ast[0]
12
- if respond_to?(operator)
13
- send(operator,*ast[1..-1])
14
- else
15
- [operator,*ast[1..-1].map {|a| map(a) }]
16
- end
18
+ function(ast) if ast[0] == :function
19
+ ast.each { |a| map(a) }
20
+ ast
17
21
  end
18
22
 
19
- def function(name,*args)
20
- if name == "OFFSET"
21
- try_to_replace_offset(*args)
22
- else
23
- [:function,name,*args.map { |a| map(a) }]
24
- end
25
- end
26
-
27
- def try_to_replace_offset(reference, row_offset, column_offset, height = [:number, 1], width = [:number, 1])
28
- if [row_offset, column_offset, height, width].all? { |a| a.first == :number }
29
- if reference.first == :cell
30
- offset_cell(reference, row_offset, column_offset, height, width)
31
- elsif reference.first == :sheet_reference && reference[2].first == :cell
32
- [:sheet_reference, reference[1], offset_cell(reference[2], row_offset, column_offset, height, width)]
33
- else
34
- puts "#{[:function, "OFFSET", reference, row_offset, column_offset, height, width]} not replaced"
35
- [:function, "OFFSET", reference, row_offset, column_offset, height, width]
36
- end
37
- else
38
- puts "#{[:function, "OFFSET", reference, row_offset, column_offset, height, width]} not replaced"
39
- [:function, "OFFSET", reference, row_offset, column_offset, height, width]
23
+ def function(ast)
24
+ name = ast[1]
25
+ args = ast[2..-1]
26
+ return unless ast[1] == :OFFSET
27
+ reference = ast[2]
28
+ row_offset = ast[3]
29
+ column_offset = ast[4]
30
+ height = ast[5] || [:number, 1]
31
+ width = ast[6] || [:number, 1]
32
+ return unless [row_offset, column_offset, height, width].all? { |a| a.first == :number }
33
+ if reference.first == :cell
34
+ ast.replace(offset_cell(reference, row_offset, column_offset, height, width))
35
+ elsif reference.first == :sheet_reference && reference[2].first == :cell
36
+ ast.replace([:sheet_reference, reference[1], offset_cell(reference[2], row_offset, column_offset, height, width)])
40
37
  end
41
38
  end
42
39
 
@@ -48,8 +45,10 @@ class ReplaceOffsetsWithReferencesAst
48
45
  height = height[1].to_i
49
46
  width = width[1].to_i
50
47
 
51
- @replacements_made_in_the_last_pass += 1
52
- reference = Reference.for(reference.gsub("$",""))
48
+ @count_replaced += 1
49
+ @replacement_made = true
50
+
51
+ reference = Reference.for(reference).unfix
53
52
  start_reference = reference.offset(row_offset.to_i, column_offset.to_i)
54
53
  end_reference = reference.offset(row_offset.to_i + height.to_i - 1, column_offset.to_i + width.to_i - 1)
55
54
  if start_reference == end_reference
@@ -68,19 +67,19 @@ class ReplaceOffsetsWithReferences
68
67
  self.new.replace(*args)
69
68
  end
70
69
 
71
- attr_accessor :replacements_made_in_the_last_pass
70
+ attr_accessor :count_replaced
72
71
 
73
72
  def replace(input,output)
74
73
  rewriter = ReplaceOffsetsWithReferencesAst.new
75
74
  input.each_line do |line|
76
75
  # Looks to match lines with references
77
- if line =~ /"OFFSET"/
76
+ if line =~ /:OFFSET/
78
77
  ref, ast = line.split("\t")
79
78
  output.puts "#{ref}\t#{rewriter.map(eval(ast)).inspect}"
80
79
  else
81
80
  output.puts line
82
81
  end
83
82
  end
84
- @replacements_made_in_the_last_pass = rewriter.replacements_made_in_the_last_pass
83
+ @count_replaced = rewriter.count_replaced
85
84
  end
86
85
  end
@@ -1,40 +1,68 @@
1
1
  require_relative '../excel'
2
2
 
3
3
  class ReplaceRangesWithArrayLiteralsAst
4
+ def initialize
5
+ @cache = {}
6
+ end
7
+
4
8
  def map(ast)
5
- if ast.is_a?(Array)
6
- operator = ast.shift
7
- if respond_to?(operator)
8
- send(operator,*ast)
9
- else
10
- [operator,*ast.map {|a| map(a) }]
11
- end
9
+ r = do_map(ast)
10
+ ast.replace(r) unless r.object_id == ast.object_id
11
+ ast
12
+ end
13
+
14
+ def do_map(ast)
15
+ return ast unless ast.is_a?(Array)
16
+ case ast[0]
17
+ when :sheet_reference; return sheet_reference(ast)
18
+ when :area; return area(ast)
12
19
  else
13
- return ast
20
+ ast.each.with_index do |a,i|
21
+ next unless a.is_a?(Array)
22
+ case a[0]
23
+ when :sheet_reference; ast[i] = sheet_reference(a)
24
+ when :area; ast[i] = area(a)
25
+ else do_map(a)
26
+ end
27
+ end
14
28
  end
29
+ ast
15
30
  end
16
31
 
17
- def sheet_reference(sheet,reference)
18
- if reference.first == :area
19
- area = Area.for("#{reference[1]}:#{reference[2]}")
20
- a = area.to_array_literal(sheet)
21
-
22
- # Don't convert single cell ranges
23
- return a[1][1] if a.size == 2 && a[1].size == 2
24
- a
32
+ # Of the form [:sheet_reference, sheet, reference]
33
+ def sheet_reference(ast)
34
+ @cache[ast] || calculate_expansion_for(ast)
35
+ end
25
36
 
37
+ def calculate_expansion_for(ast)
38
+ sheet = ast[1]
39
+ reference = ast[2]
40
+ return ast unless reference.first == :area
41
+ area = Area.for("#{reference[1]}:#{reference[2]}")
42
+ a = area.to_array_literal(sheet)
43
+
44
+ # Don't convert single cell ranges
45
+ result = if a.size == 2 && a[1].size == 2
46
+ a[1][1]
26
47
  else
27
- [:sheet_reference,sheet,reference]
48
+ a
28
49
  end
50
+ @cache[ast.dup] = result
29
51
  end
30
52
 
31
- def area(start,finish)
53
+ # Of the form [:area, start, finish]
54
+ def area(ast)
55
+ start = ast[1]
56
+ finish = ast[2]
32
57
  area = Area.for("#{start}:#{finish}")
33
58
  a = area.to_array_literal
34
59
 
35
60
  # Don't convert single cell ranges
36
- return a[1][1] if a.size == 2 && a[1].size == 2
37
- a
61
+ if a.size == 2 && a[1].size == 2
62
+ a[1][1]
63
+ else
64
+ a
65
+ end
38
66
  end
39
67
 
40
68
  end
@@ -3,26 +3,28 @@ class ReplaceSharedStringAst
3
3
  attr_accessor :shared_strings
4
4
 
5
5
  def initialize(shared_strings)
6
- @shared_strings = shared_strings.map do |s|
7
- s[/^(.*?)\n$/,1]
8
- end
6
+ @shared_strings = shared_strings
9
7
  end
10
8
 
9
+ # This is convoluted so as to always
10
+ # return the same shared string
11
11
  def map(ast)
12
- if ast.is_a?(Array)
13
- operator = ast.shift
14
- if respond_to?(operator)
15
- send(operator,*ast)
12
+ return ast unless ast.is_a?(Array)
13
+ return shared_string(ast) if ast[0] == :shared_string
14
+ ast.each.with_index do |a, i|
15
+ next unless a.is_a?(Array)
16
+ if a[0] == :shared_string
17
+ ast[i] = shared_string(a)
16
18
  else
17
- [operator,*ast.map {|a| map(a) }]
19
+ map(a)
18
20
  end
19
- else
20
- return ast
21
21
  end
22
+ ast
22
23
  end
23
24
 
24
- def shared_string(string_id)
25
- [:string,shared_strings[string_id.to_i]]
25
+ # Format [:shared_string, string_id]
26
+ def shared_string(ast)
27
+ ast.replace(shared_strings[ast[1].to_i])
26
28
  end
27
29
  end
28
30
 
@@ -35,7 +37,7 @@ class ReplaceSharedStrings
35
37
 
36
38
  # Rewrites ast with shared strings to strings
37
39
  def replace(values,shared_strings,output)
38
- rewriter = ReplaceSharedStringAst.new(shared_strings.readlines)
40
+ rewriter = ReplaceSharedStringAst.new(shared_strings)
39
41
  values.each_line do |line|
40
42
  # Looks to match shared string lines
41
43
  if line =~ /\[:shared_string/
@@ -2,17 +2,15 @@ class ReplaceStringJoinOnRangesAST
2
2
 
3
3
  def map(ast)
4
4
  return ast unless ast.is_a?(Array)
5
- operator = ast[0]
6
- if respond_to?(operator)
7
- send(operator,*ast[1..-1])
8
- else
9
- [operator,*ast[1..-1].map {|a| map(a) }]
10
- end
5
+ string_join(ast) if ast[0] == :string_join
6
+ ast.each { |a| map(a) }
7
+ ast
11
8
  end
12
9
 
13
- def string_join(*strings)
10
+ def string_join(ast)
11
+ strings = ast[1..-1]
14
12
  # Make sure there is actually a conversion to do
15
- return [:string_join, *strings] unless strings.any? { |s| s.first == :array }
13
+ return unless strings.any? { |s| s.first == :array }
16
14
  # Now work out the largest dimensions
17
15
  # Arrays look like this [:array, [:row, 1, 2, 3], [:row, 4, 5, 6]]
18
16
  max_rows = 0
@@ -37,7 +35,7 @@ class ReplaceStringJoinOnRangesAST
37
35
  end
38
36
  result << row
39
37
  end
40
- result
38
+ ast.replace(result)
41
39
  end
42
40
 
43
41
  def select_from(maybe_array, row_index, column_index)
@@ -8,25 +8,30 @@ class ReplaceTableReferenceAst
8
8
 
9
9
  def map(ast)
10
10
  return ast unless ast.is_a?(Array)
11
- operator = ast[0]
12
- if respond_to?(operator)
13
- send(operator,*ast[1..-1])
14
- else
15
- [operator,*ast[1..-1].map {|a| map(a) }]
11
+ case ast[0]
12
+ when :table_reference; table_reference(ast)
13
+ when :local_table_reference; local_table_reference(ast)
16
14
  end
15
+ ast.each { |a| map(a) }
16
+ ast
17
17
  end
18
18
 
19
- def table_reference(table_name,table_reference)
20
- return [:error,"#REF!"] unless tables.has_key?(table_name.downcase)
21
- tables[table_name.downcase].reference_for(table_name,table_reference,worksheet,referring_cell)
19
+ # Of the format [:table_reference, table_name, table_reference]
20
+ def table_reference(ast)
21
+ table_name = ast[1]
22
+ table_reference = ast[2]
23
+ return ast.replace([:error, :"#REF!"]) unless tables.has_key?(table_name.downcase)
24
+ ast.replace(tables[table_name.downcase].reference_for(table_name,table_reference,worksheet,referring_cell))
22
25
  end
23
26
 
24
- def local_table_reference(table_reference)
27
+ # Of the format [:local_table_reference, table_reference]
28
+ def local_table_reference(ast)
29
+ table_reference = ast[1]
25
30
  table = tables.values.find do |table|
26
31
  table.includes?(worksheet,referring_cell)
27
32
  end
28
- return [:error,"#REF!"] unless table
29
- table.reference_for(table.name,table_reference,worksheet,referring_cell)
33
+ return ast.replace([:error, :"#REF!"]) unless table
34
+ ast.replace(table.reference_for(table.name,table_reference,worksheet,referring_cell))
30
35
  end
31
36
 
32
37
  end
@@ -6,17 +6,19 @@ class MapValuesToConstants
6
6
  count = 0
7
7
  @constants = Hash.new do |hash,key|
8
8
  count += 1
9
- hash[key] = "C#{count}"
9
+ hash[key] = "constant#{count}"
10
10
  end
11
11
  end
12
12
 
13
+ POTENTIAL_CONSTANTS = { :number => true, :percentage => true, :string => true}
14
+
13
15
  def map(ast)
14
16
  return ast unless ast.is_a?(Array)
15
17
  operator = ast[0]
16
- if [:number,:percentage,:string].include?(operator)
17
- [:constant, constants[ast]]
18
+ if POTENTIAL_CONSTANTS.has_key?(operator)
19
+ ast.replace([:constant, constants[ast.dup]])
18
20
  else
19
- [operator,*ast[1..-1].map {|a| map(a) }]
21
+ ast.each { |a| map(a) }
20
22
  end
21
23
  end
22
24
 
@@ -2,31 +2,45 @@ class SimplifyArithmeticAst
2
2
 
3
3
  def map(ast)
4
4
  return ast unless ast.is_a?(Array)
5
- operator = ast[0]
6
- if respond_to?(operator)
7
- send(operator,*ast[1..-1])
8
- else
9
- [operator,*ast[1..-1].map {|a| map(a) }]
5
+ case ast[0]
6
+ when :brackets; brackets(ast)
7
+ when :arithmetic; arithmetic(ast)
10
8
  end
9
+ ast.each { |a| map(a) }
10
+ ast
11
11
  end
12
12
 
13
- def brackets(*args)
14
- raise NotSupportedException.new("Multiple arguments not supported in brackets #{args.inspect}") if args.size > 1
15
- map(args.first)
13
+ def brackets(ast)
14
+ raise NotSupportedException.new("Multiple arguments not supported in brackets #{args.inspect}") if ast.size > 2
15
+ ast.replace(map(ast[1]))
16
16
  end
17
17
 
18
- def arithmetic(*args)
19
- return map(args.first) if args.size == 1
20
- return [:arithmetic,*args.map {|a| map(a) }] if args.size == 3
21
- [['^'],['*','/'],['+','-']].each do |op|
22
- i = args.find_index { |a| a[0] == :operator && op.include?(a[1])}
23
- next unless i
24
- pre_operation = i == 1 ? [] : args[0..(i-2)]
25
- operation = args[(i-1)..(i+1)]
26
- post_operation = (i + 2) > args.size ? [] : args[(i+2)..-1]
27
- return arithmetic(*pre_operation.map {|a| map(a) },[:arithmetic,*operation],*post_operation.map {|a| map(a) })
18
+ def arithmetic(ast)
19
+ case ast.size
20
+ when 2; return map(ast[1]) # Not really arithmetic
21
+ when 4; # Normal arithmetic that doesn't need re-arranging
22
+ ast.each { |a| map(a) }
23
+ else
24
+ # This sets the operator precedence
25
+ i = nil
26
+ [{:'^' => 1},{:'*' => 2,:'/' => 2},{:'+' => 3,:'-' => 3}].each do |op|
27
+ i = ast.find_index { |a| a[0] == :operator && op.has_key?(a[1])}
28
+ break if i
29
+ end
30
+ if i
31
+ # Now we need to wrap that operation in its own arithmetic clause
32
+ old_clause = ast[(i-1)..(i+1)]
33
+ # Make sure we do any mapping
34
+ old_clause.each { |a| map(a) }
35
+ # Now create a new clause
36
+ new_clause = [:arithmetic, *old_clause]
37
+ # And insert it back into the ast
38
+ ast[(i-1)..(i+1)] = [new_clause]
39
+ # Redo the mapping
40
+ map(ast)
41
+ end
28
42
  end
29
- return [:arithmetic,*args.map {|a| map(a) }]
43
+ ast.each { |a| map(a) }
30
44
  end
31
45
 
32
46
  end
@@ -9,21 +9,20 @@ class SortIntoCalculationOrder
9
9
  def sort(references)
10
10
  @current_sheet = []
11
11
  @ordered_references = []
12
+ @counted = {}
12
13
  @references = references
13
14
 
14
15
  # First we find the references that are at the top of the tree
15
16
  references_with_counts = CountFormulaReferences.new.count(references)
16
17
  tops = []
17
- references_with_counts.each do |sheet, references|
18
- references.each do |cell, count|
19
- next unless count == 0
20
- tops << [sheet, cell]
21
- end
18
+ references_with_counts.each do |reference, count|
19
+ next unless count == 0
20
+ tops << reference
22
21
  end
23
22
  # Then we have to work through those tops
24
23
  # recursively adding the cells that they depend on
25
- tops.each do |ref|
26
- add_ordered_references_for ref
24
+ tops.each do |reference|
25
+ add_ordered_references_for reference
27
26
  end
28
27
  @ordered_references
29
28
  end
@@ -32,10 +31,11 @@ class SortIntoCalculationOrder
32
31
  sheet = ref.first
33
32
  cell = ref.last
34
33
  current_sheet.push(sheet)
35
- ast = @references[sheet][cell]
34
+ ast = @references[ref]
36
35
  map(ast)
37
36
  current_sheet.pop
38
- ordered_references << ref
37
+ @ordered_references << ref
38
+ @counted[ref] = true
39
39
  end
40
40
 
41
41
  def map(ast)
@@ -51,14 +51,14 @@ class SortIntoCalculationOrder
51
51
  end
52
52
 
53
53
  def sheet_reference(sheet,reference)
54
- ref = [sheet, reference.last.gsub('$','')]
55
- return if @ordered_references.include?(ref)
54
+ ref = [sheet, reference.last.to_s.gsub('$','').to_sym]
55
+ return if @counted.has_key?(ref)
56
56
  add_ordered_references_for(ref)
57
57
  end
58
58
 
59
59
  def cell(reference)
60
- ref = [current_sheet.last, reference.gsub('$','')]
61
- return if @ordered_references.include?(ref)
60
+ ref = [current_sheet.last, reference.to_s.gsub('$','').to_sym]
61
+ return if @counted.has_key?(ref)
62
62
  add_ordered_references_for(ref)
63
63
  end
64
64