excel_to_code 0.1.23 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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