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,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