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,15 +1,28 @@
1
- require_relative 'simple_extract_from_xml'
1
+ require 'nokogiri'
2
2
 
3
- class ExtractValues < SimpleExtractFromXML
4
-
5
- attr_accessor :ref, :type
3
+ class ExtractValues < Nokogiri::XML::SAX::Document
4
+
5
+ def self.extract(*args)
6
+ self.new.extract(*args)
7
+ end
8
+
9
+ def extract(sheet_name, input)
10
+ @sheet_name = sheet_name.to_sym
11
+ @output = {}
12
+ @parsing = false
13
+ @fp = CachingFormulaParser.instance
14
+ Nokogiri::XML::SAX::Parser.new(self).parse(input)
15
+ @output
16
+ end
17
+
18
+ attr_accessor :ref, :type, :value
6
19
 
7
20
  def start_element(name,attributes)
8
21
  if name == "v"
9
- self.parsing = true
10
- output.write "#{@ref}\t#{@type}\t"
22
+ @parsing = true
23
+ @value = []
11
24
  elsif name == "c"
12
- @ref = attributes.assoc('r').last
25
+ @ref = attributes.assoc('r').last.to_sym
13
26
  type = attributes.assoc('t')
14
27
  @type = type ? type.last : "n"
15
28
  end
@@ -17,13 +30,24 @@ class ExtractValues < SimpleExtractFromXML
17
30
 
18
31
  def end_element(name)
19
32
  return unless name == "v"
20
- self.parsing = false
21
- output.putc "\n"
33
+ @parsing = false
34
+ value = @value.join
35
+ ast = case @type
36
+ when 'b'; value == "1" ? [:boolean_true] : [:boolean_false]
37
+ when 's'; [:shared_string,value.to_i]
38
+ when 'n'; [:number,value.to_f]
39
+ when 'e'; [:error,value.to_sym]
40
+ when 'str'; [:string,value.gsub(/_x[0-9A-F]{4}_/,'').freeze]
41
+ else
42
+ $stderr.puts "Type #{type} not known #{@sheet_name} #{@ref}"
43
+ exit
44
+ end
45
+ @output[[@sheet_name, @ref]] = @fp.map(ast)
22
46
  end
23
47
 
24
48
  def characters(string)
25
- return unless parsing
26
- output.write string
49
+ return unless @parsing
50
+ @value << string
27
51
  end
28
52
 
29
53
  end
@@ -1,11 +1,21 @@
1
- require_relative 'simple_extract_from_xml'
1
+ require 'nokogiri'
2
2
 
3
- class ExtractWorksheetDimensions < SimpleExtractFromXML
3
+ class ExtractWorksheetDimensions < Nokogiri::XML::SAX::Document
4
+
5
+ def self.extract(*args)
6
+ self.new.extract(*args)
7
+ end
8
+
9
+ def extract(input)
10
+ parser = Nokogiri::XML::SAX::Parser.new(self)
11
+ parser.parse(input)
12
+ @output
13
+ end
4
14
 
5
15
  # FIXME: Is there an elegant way to abort once we have found the dimension tag?
6
16
  def start_element(name,attributes)
7
17
  return false unless name == "dimension"
8
- output.puts "#{attributes.assoc('ref').last}"
18
+ @output = attributes.assoc('ref').last
9
19
  end
10
20
 
11
21
  end
@@ -1,10 +1,23 @@
1
- require_relative 'simple_extract_from_xml'
1
+ require 'nokogiri'
2
2
 
3
- class ExtractWorksheetNames < SimpleExtractFromXML
3
+ class ExtractWorksheetNames < Nokogiri::XML::SAX::Document
4
+
5
+ attr_accessor :input, :output
6
+
7
+ def self.extract(*args)
8
+ self.new.extract(*args)
9
+ end
10
+
11
+ def extract(input)
12
+ @input, @output = input, {}
13
+ parser = Nokogiri::XML::SAX::Parser.new(self)
14
+ parser.parse(input)
15
+ output
16
+ end
4
17
 
5
18
  def start_element(name,attributes)
6
19
  return false unless name == "sheet"
7
- output.puts "#{attributes.assoc('r:id').last}\t#{attributes.assoc('name').last}"
20
+ output[attributes.assoc('name').last] = attributes.assoc('r:id').last
8
21
  end
9
22
 
10
23
  end
@@ -1,10 +1,22 @@
1
- require_relative 'simple_extract_from_xml'
1
+ require 'nokogiri'
2
2
 
3
- class ExtractWorksheetTableRelationships < SimpleExtractFromXML
3
+
4
+ class ExtractWorksheetTableRelationships < Nokogiri::XML::SAX::Document
5
+
6
+ def self.extract(*args)
7
+ self.new.extract(*args)
8
+ end
9
+
10
+ def extract(input)
11
+ @output = []
12
+ @parsing = false
13
+ Nokogiri::XML::SAX::Parser.new(self).parse(input)
14
+ @output
15
+ end
4
16
 
5
17
  def start_element(name,attributes)
6
18
  return false unless name == "tablePart"
7
- output.puts "#{attributes.assoc('r:id').last}"
19
+ @output << "#{attributes.assoc('r:id').last}"
8
20
  end
9
21
 
10
- end
22
+ end
@@ -2,18 +2,16 @@ require 'nokogiri'
2
2
 
3
3
  class SimpleExtractFromXML < Nokogiri::XML::SAX::Document
4
4
 
5
- attr_accessor :parsing, :input, :output
6
-
7
- def self.extract(input,output)
8
- self.new.extract(input,output)
5
+ def self.extract(*args)
6
+ self.new.extract(*args)
9
7
  end
10
8
 
11
- def extract(input,output)
12
- @input, @output = input, output
13
- parsing = false
14
- parser = Nokogiri::XML::SAX::Parser.new(self)
15
- parser.parse(input)
16
- output
9
+ def extract(sheet_name, input)
10
+ @sheet_name = sheet_name
11
+ @output = {}
12
+ @parsing = false
13
+ Nokogiri::XML::SAX::Parser.new(self).parse(input)
14
+ @output
17
15
  end
18
16
 
19
- end
17
+ end
data/src/rewrite.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "excel/formula_peg"
1
2
  require_relative "rewrite/rewrite_formulae_to_ast"
2
3
  require_relative "rewrite/rewrite_worksheet_names"
3
4
  require_relative "rewrite/rewrite_whole_row_column_references_to_areas"
@@ -9,3 +10,5 @@ require_relative "rewrite/rewrite_relationship_id_to_filename"
9
10
  require_relative "rewrite/rewrite_merge_formulae_and_values"
10
11
  require_relative "rewrite/rewrite_cell_references_to_include_sheet"
11
12
  require_relative "rewrite/rewrite_named_reference_names"
13
+ require_relative "rewrite/caching_formula_parser"
14
+
@@ -10,11 +10,15 @@ class AstCopyFormula
10
10
  self.columns_to_move = 0
11
11
  end
12
12
 
13
+ DO_NOT_MAP = {:number => true, :string => true, :blank => true, :null => true, :error => true, :boolean_true => true, :boolean_false => true, :operator => true, :comparator => true}
14
+
13
15
  def copy(ast)
14
16
  return ast unless ast.is_a?(Array)
15
17
  operator = ast[0]
16
18
  if respond_to?(operator)
17
19
  send(operator,*ast[1..-1])
20
+ elsif DO_NOT_MAP[operator]
21
+ return ast
18
22
  else
19
23
  [operator,*ast[1..-1].map {|a| copy(a) }]
20
24
  end
@@ -39,4 +43,4 @@ class AstCopyFormula
39
43
  raise NotSupportedException.new("Row ranges not suported in AstCopyFormula")
40
44
  end
41
45
 
42
- end
46
+ end
@@ -5,39 +5,44 @@ class AstExpandArrayFormulae
5
5
  def map(ast)
6
6
  return ast unless ast.is_a?(Array)
7
7
  operator = ast[0]
8
- if respond_to?(operator)
9
- send(operator,*ast[1..-1])
10
- else
11
- [operator,*ast[1..-1].map {|a| map(a) }]
12
- end
8
+ send(operator, ast) if respond_to?(operator)
9
+ ast.each {|a| map(a) }
10
+ ast
13
11
  end
14
12
 
15
- def arithmetic(left,operator,right)
16
- left = map(left)
17
- right = map(right)
18
- return [:arithmetic, left, operator, right] unless array?(left,right)
13
+ # Format [:arithmetic, left, operator, right]
14
+ def arithmetic(ast)
15
+ ast.each {|a| map(a) }
16
+ return unless array?(ast[1], ast[3])
19
17
 
20
- map_arrays([left,right]) do |arrayed|
21
- [:arithmetic,arrayed[0],operator,arrayed[1]]
22
- end
18
+ ast.replace(
19
+ map_arrays([ast[1],ast[3]]) do |arrayed|
20
+ [:arithmetic,arrayed[0],ast[2],arrayed[1]]
21
+ end
22
+ )
23
23
  end
24
24
 
25
- def comparison(left,operator,right)
26
- left = map(left)
27
- right = map(right)
28
- return [:comparison, left, operator, right] unless array?(left,right)
29
-
30
- map_arrays([left,right]) do |arrayed|
31
- [:comparison,arrayed[0],operator,arrayed[1]]
32
- end
25
+ # Format [:comparison, left, operator, right]
26
+ def comparison(ast)
27
+ ast.each {|a| map(a) }
28
+ return unless array?(ast[1], ast[3])
29
+
30
+ ast.replace(
31
+ map_arrays([ast[1],ast[3]]) do |arrayed|
32
+ [:comparison,arrayed[0],ast[2],arrayed[1]]
33
+ end
34
+ )
33
35
  end
34
36
 
35
- def string_join(*strings)
36
- strings = strings.map { |s| map(s) }
37
- return [:string_join, *strings] unless array?(*strings)
38
- map_arrays(strings) do |arrayed_strings|
39
- [:string_join, *arrayed_strings]
40
- end
37
+ # Format [:string_join, stringA, stringB, ...]
38
+ def string_join(ast)
39
+ ast.each {|a| map(a) }
40
+ return unless array?(*ast[1..-1])
41
+ ast.replace(
42
+ map_arrays(ast[1..-1]) do |arrayed_strings|
43
+ [:string_join, *arrayed_strings]
44
+ end
45
+ )
41
46
  end
42
47
 
43
48
  def map_arrays(arrays, &block)
@@ -58,56 +63,62 @@ class AstExpandArrayFormulae
58
63
  return [:array, *max_rows.times.map do |row|
59
64
  [:row, *max_columns.times.map do |column|
60
65
  block.call(arrays.map do |a|
61
- a[row][column] || [:error, "#N/A"]
66
+ (a[row] && a[row][column]) || CachingFormulaParser.map([:error, :"#N/A"])
62
67
  end)
63
68
  end]
64
69
  end]
65
70
  end
66
71
 
67
- FUNCTIONS_THAT_ACCEPT_RANGES_FOR_ALL_ARGUMENTS = %w{AVERAGE COUNT COUNTA MAX MIN SUM SUMPRODUCT MMULT}
72
+ FUNCTIONS_THAT_ACCEPT_RANGES_FOR_ALL_ARGUMENTS = {AVERAGE: true, COUNT: true, COUNTA: true, MAX: true, MIN: true, SUM: true, SUMPRODUCT: true, MMULT: true}
68
73
 
69
- def function(name,*arguments)
70
- if FUNCTIONS_THAT_ACCEPT_RANGES_FOR_ALL_ARGUMENTS.include?(name)
71
- [:function, name, *arguments.map { |a| map(a) }]
74
+ # Format [:function, function_name, arg1, arg2, ...]
75
+ def function(ast)
76
+ name = ast[1]
77
+ arguments = ast[2..-1]
78
+ if FUNCTIONS_THAT_ACCEPT_RANGES_FOR_ALL_ARGUMENTS.has_key?(name)
79
+ ast.each { |a| map(a) }
80
+ return # No need to alter anything
72
81
  elsif respond_to?("map_#{name.downcase}")
73
- send("map_#{name.downcase}",*arguments)
82
+ # These typically have some arguments that accept ranges, but not all
83
+ send("map_#{name.downcase}",ast)
74
84
  else
75
- function_that_does_not_accept_ranges(name,arguments)
85
+ function_that_does_not_accept_ranges(ast)
76
86
  end
77
87
  end
78
88
 
79
- def function_that_does_not_accept_ranges(name,arguments)
80
- return [:function, name] if arguments.empty?
81
- array_map arguments, name, *Array.new(arguments.length,false)
89
+ def function_that_does_not_accept_ranges(ast)
90
+ return if ast.length == 2
91
+ name = ast[1]
92
+ arguments = ast[2..-1]
93
+ array_map(ast, *Array.new(arguments.length,false))
82
94
  end
83
95
 
84
- def map_match(*args)
85
- a = array_map args, 'MATCH', false, true, false
86
- a
96
+ def map_match(ast)
97
+ array_map(ast, false, true, false)
87
98
  end
88
99
 
89
- def map_subtotal(*args)
90
- array_map args, 'SUBTOTAL', false, *Array.new(args.length-1,true)
100
+ def map_subtotal(ast)
101
+ array_map ast, false, *Array.new(ast.length-3,true)
91
102
  end
92
103
 
93
- def map_index(*args)
94
- array_map args, 'INDEX', true, false, false
104
+ def map_index(ast)
105
+ array_map ast, true, false, false
95
106
  end
96
107
 
97
- def map_sumif(*args)
98
- array_map args, 'SUMIF', true, false, true
108
+ def map_sumif(ast)
109
+ array_map ast, true, false, true
99
110
  end
100
111
 
101
- def map_sumifs(*args)
102
- if args.length > 3
103
- array_map args, 'SUMIFS', true, true, false, *([true,false]*((args.length-3)/2))
112
+ def map_sumifs(ast)
113
+ if ast.length > 5
114
+ array_map ast, true, true, false, *([true,false]*((ast.length-5)/2))
104
115
  else
105
- array_map args, 'SUMIFS', true, true, false
116
+ array_map ast, true, true, false
106
117
  end
107
118
  end
108
119
 
109
- def map_vlookup(*args)
110
- array_map args, "VLOOKUP", false, true, false, false
120
+ def map_vlookup(ast)
121
+ array_map ast, false, true, false, false
111
122
  end
112
123
 
113
124
  private
@@ -121,15 +132,16 @@ class AstExpandArrayFormulae
121
132
  true
122
133
  end
123
134
 
124
- def array_map(args,function,*ok_to_be_an_array)
125
- args = args.map { |a| map(a) }
126
- return [:function, function, *args ] if no_need_to_array?(args,ok_to_be_an_array)
135
+ def array_map(ast,*ok_to_be_an_array)
136
+ ast.each { |a| map(a) }
137
+
138
+ return if no_need_to_array?(ast[2..-1],ok_to_be_an_array)
127
139
 
128
140
  # Turn the relevant arguments into ruby arrays and store the dimensions
129
141
  # Enumerable#max and Enumerable#min don't return Enumerators, so can't do it using those methods
130
142
  max_rows = 1
131
143
  max_columns = 1
132
- args = args.map.with_index do |a,i|
144
+ args = ast[2..-1].map.with_index do |a,i|
133
145
  unless ok_to_be_an_array[i]
134
146
  a = array_ast_to_ruby_array(a)
135
147
  r = a.length
@@ -147,17 +159,17 @@ class AstExpandArrayFormulae
147
159
  args = args.map.with_index { |a,i| (!ok_to_be_an_array[i] && a.first.length == 1) ? Array.new(max_columns,a.flatten(1)).transpose : a }
148
160
 
149
161
  # Now iterate through
150
- return [:array, *max_rows.times.map do |row|
162
+ ast.replace( [:array, *max_rows.times.map do |row|
151
163
  [:row, *max_columns.times.map do |column|
152
- [:function, function, *args.map.with_index do |a,i|
164
+ [:function, ast[1], *args.map.with_index do |a,i|
153
165
  if ok_to_be_an_array[i]
154
166
  a
155
167
  else
156
- a[row][column] || [:error, "#N/A"]
168
+ a[row][column] || [:error, :"#N/A"]
157
169
  end
158
170
  end]
159
171
  end]
160
- end]
172
+ end])
161
173
  end
162
174
 
163
175
  def array?(*args)
@@ -0,0 +1,110 @@
1
+ require 'singleton'
2
+
3
+ class CachingFormulaParser
4
+ include Singleton
5
+
6
+ def self.parse(*args)
7
+ instance.parse(*args)
8
+ end
9
+
10
+ def self.map(*args)
11
+ instance.map(*args)
12
+ end
13
+
14
+ def initialize
15
+ @number_cache = {}
16
+ @string_cache = {}
17
+ @percentage_cache = {}
18
+ @error_cache = {}
19
+ @operator_cache = {}
20
+ @comparator_cache = {}
21
+ @sheet_reference_cache = {}
22
+ end
23
+
24
+ def parse(text)
25
+ ast = Formula.parse(text)
26
+ if ast
27
+ map(ast.to_ast[1])
28
+ else
29
+ nil
30
+ end
31
+ end
32
+
33
+ def map(ast)
34
+ return ast unless ast.is_a?(Array)
35
+ ast[1] = ast[1].to_sym if ast[0] == :function
36
+ if respond_to?(ast[0])
37
+ ast = send(ast[0], ast)
38
+ else
39
+ ast.each.with_index do |a,i|
40
+ next unless a.is_a?(Array)
41
+ a[1] = a[1].to_sym if a[0] == :function
42
+ ast[i] = map(a)
43
+ end
44
+ end
45
+ ast
46
+ end
47
+
48
+ def sheet_reference(ast)
49
+ ast[1] = ast[1].to_sym
50
+ ast[2] = map(ast[2])
51
+ @sheet_reference_cache[ast] ||= ast
52
+ end
53
+
54
+ def cell(ast)
55
+ ast[1] = ast[1].to_sym
56
+ ast
57
+ end
58
+
59
+ def area(ast)
60
+ ast[1] = ast[1].to_sym
61
+ ast[2] = ast[2].to_sym
62
+ ast
63
+ end
64
+
65
+ def number(ast)
66
+ ast[1] = ast[1].to_f
67
+ @number_cache[ast] ||= ast
68
+ end
69
+
70
+ def percentage(ast)
71
+ ast[1] = ast[1].to_f
72
+ @percentage_cache[ast] ||= ast
73
+ end
74
+
75
+ def string(ast)
76
+ return @string_cache[ast] ||= ast
77
+ end
78
+
79
+ TRUE = [:boolean_true]
80
+ FALSE = [:boolean_false]
81
+ BLANK = [:blank]
82
+
83
+ def boolean_true(ast)
84
+ TRUE
85
+ end
86
+
87
+ def boolean_false(ast)
88
+ FALSE
89
+ end
90
+
91
+ def error(ast)
92
+ ast[1] = ast[1].to_sym
93
+ @error_cache[ast] ||= ast
94
+ end
95
+
96
+ def blank(ast)
97
+ BLANK
98
+ end
99
+
100
+ def operator(ast)
101
+ ast[1] = ast[1].to_sym
102
+ @operator_cache[ast] ||= ast
103
+ end
104
+
105
+ def comparator(ast)
106
+ ast[1] = ast[1].to_sym
107
+ @comparator_cache[ast] ||= ast
108
+ end
109
+
110
+ end