excel_to_code 0.2.18 → 0.2.19

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 337fc100f3818bf193414bcd8fb3aaec557e1a6b
4
- data.tar.gz: 34218ff7c787a63a1e0081b688ff328d1235316e
3
+ metadata.gz: 74698377e3e89c2dae52564440694e4e2e97f2f1
4
+ data.tar.gz: 73e3a3af8271ac02a3f68727f43e5874153d2d2f
5
5
  SHA512:
6
- metadata.gz: 10b5b04a5fe6ae0b5df092a010b5cbf0cad48025db71b99350da7a040e2992c5301c08f23652040b4eb340fcb2f064aa7bae7c02b765c53c8849e11480a66a71
7
- data.tar.gz: 3ec3fbfbad7b341990559ae40d121dfdbd46839aa3f92f5a1b41b58ce597e43606d91de146bc940d5976b682320b593925bea0619463f9e183b98800f0b15a5c
6
+ metadata.gz: f97a19688abe21b1cdbc7755a77158379746f012bda38154b35c7114add213ef4d1cee34f6121ce61d09d3bced7ae5de98f413e297ce5c0beecc9bd586a7177d
7
+ data.tar.gz: b911c226eed1bd735828b5a80927b8198ea1b226b312032c937c9bb96a98f10ccb3bff9fc13e391c2aef0c3163307e3b1e541e371c74459ae628ac714aa34027
@@ -228,10 +228,16 @@ class ExcelToX
228
228
 
229
229
  # Setting this to false may make it easier to figure out errors
230
230
  self.extract_repeated_parts_of_formulae = true if @extract_repeated_parts_of_formulae == nil
231
+
232
+ # This setting is used for debugging, and makes the system only do the conversion on a subset of the the
231
233
  if self.isolate
232
- self.cells_to_keep = { isolate.to_s => :all }
233
- self.isolate = self.isolate.to_sym
234
- log.warn "Isolating #{@isolate} worksheet. No other sheets will be converted"
234
+ self.isolate = [self.isolate] unless self.isolate.is_a?(Array)
235
+ self.cells_to_keep ||= {}
236
+ self.isolate.each do |sheet|
237
+ self.cells_to_keep[sheet.to_s] = :all
238
+ end
239
+ self.isolate = self.isolate.map { |s| s.to_sym }
240
+ log.warn "Isolating #{@isolate} worksheet(s). No other sheets will be converted"
235
241
  end
236
242
  end
237
243
 
@@ -283,6 +289,7 @@ class ExcelToX
283
289
  end
284
290
  # Then we parse them
285
291
  @named_references.each do |name, reference|
292
+ begin
286
293
  parsed = CachingFormulaParser.parse(reference)
287
294
  if parsed
288
295
  @named_references[name] = parsed
@@ -290,6 +297,10 @@ class ExcelToX
290
297
  $stderr.puts "Named reference #{name} #{reference} not parsed"
291
298
  exit
292
299
  end
300
+ rescue Exception
301
+ $stderr.puts "Named reference #{name} #{reference} not parsed"
302
+ raise
303
+ end
293
304
  end
294
305
  # Replace A$1:B2 with [A1, A2, B1, B2]
295
306
  @replace_ranges_with_array_literals_replacer ||= ReplaceRangesWithArrayLiteralsAst.new
@@ -456,7 +467,8 @@ class ExcelToX
456
467
  # This is used in debugging large worksheets to limit
457
468
  # the optimisation to a particular worksheet
458
469
  if isolate
459
- extractor.only_extract_values = (isolate != name)
470
+ log.info "Only extracting values from #{name}: #{!isolate.include?(name)}"
471
+ extractor.only_extract_values = !isolate.include?(name)
460
472
  end
461
473
 
462
474
  log.info "Extracting data from #{name}"
@@ -648,10 +660,13 @@ class ExcelToX
648
660
  # FIXME: THIS IS THE MOST HORRIFIC BODGE. I HATE IT.
649
661
  emergency_indirect_replacement_bodge = EmergencyArrayFormulaReplaceIndirectBodge.new
650
662
  emergency_indirect_replacement_bodge.references = @values
663
+ emergency_indirect_replacement_bodge.tables = @tables
664
+ emergency_indirect_replacement_bodge.named_references = @named_references
651
665
 
652
666
  @formulae_array.each do |ref, details|
653
667
  @shared_string_replacer.map(details.last)
654
668
  emergency_indirect_replacement_bodge.current_sheet_name = ref.first
669
+ emergency_indirect_replacement_bodge.referring_cell = ref.last
655
670
  emergency_indirect_replacement_bodge.replace(details.last)
656
671
 
657
672
  named_reference_replacer.default_sheet_name = ref.first
@@ -930,22 +945,27 @@ class ExcelToX
930
945
  references_that_need_updating = {}
931
946
 
932
947
  @cells_with_formulae.each do |ref, ast|
933
- # FIXME: Shouldn't need to wrap ref.fist in an array
934
- inline_replacer.current_sheet_name = [ref.first]
935
- inline_replacer.map(ast)
936
- # If a formula references a cell containing a value, the reference is replaced with the value (e.g., if A1 := 2 and A2 := A1 + 1 then becomes: A2 := 2 + 1)
937
- value_replacer.map(ast)
938
- column_and_row_function_replacement.current_reference = ref.last
939
- if column_and_row_function_replacement.replace(ast)
940
- references_that_need_updating[ref] = ast
941
- end
942
- if offset_replacement.replace(ast)
943
- references_that_need_updating[ref] = ast
944
- end
945
- if indirect_replacement.replace(ast)
946
- references_that_need_updating[ref] = ast
948
+ begin
949
+ # FIXME: Shouldn't need to wrap ref.fist in an array
950
+ inline_replacer.current_sheet_name = [ref.first]
951
+ inline_replacer.map(ast)
952
+ # If a formula references a cell containing a value, the reference is replaced with the value (e.g., if A1 := 2 and A2 := A1 + 1 then becomes: A2 := 2 + 1)
953
+ value_replacer.map(ast)
954
+ column_and_row_function_replacement.current_reference = ref.last
955
+ if column_and_row_function_replacement.replace(ast)
956
+ references_that_need_updating[ref] = ast
957
+ end
958
+ if offset_replacement.replace(ast)
959
+ references_that_need_updating[ref] = ast
960
+ end
961
+ if indirect_replacement.replace(ast)
962
+ references_that_need_updating[ref] = ast
963
+ end
964
+ @cells_with_formulae.delete(ref) if VALUE_TYPE[ast[0]]
965
+ rescue Exception => e
966
+ log.fatal "Exception when replacing formulae with results in #{ref}: #{ast}"
967
+ raise
947
968
  end
948
- @cells_with_formulae.delete(ref) if VALUE_TYPE[ast[0]]
949
969
  end
950
970
 
951
971
  simplify(references_that_need_updating)
@@ -1032,8 +1052,13 @@ class ExcelToX
1032
1052
  r.references = @formulae
1033
1053
  r.inline_ast = inline_ast_decision
1034
1054
  @cells_with_formulae.each do |ref, ast|
1035
- r.current_sheet_name = [ref.first]
1036
- r.map(ast)
1055
+ begin
1056
+ r.current_sheet_name = [ref.first]
1057
+ r.map(ast)
1058
+ rescue Exception => e
1059
+ log.fatal "Exception when inlining formulae only used once in #{ref}: #{ast}"
1060
+ raise
1061
+ end
1037
1062
  end
1038
1063
  end
1039
1064
 
data/src/compile/c/a.out CHANGED
Binary file
@@ -308,6 +308,9 @@ static ExcelValue ensure_is_number(ExcelValue maybe_number_v) {
308
308
  if(maybe_number_v.type == ExcelNumber) {
309
309
  return maybe_number_v;
310
310
  }
311
+ if(maybe_number_v.type == ExcelError) {
312
+ return maybe_number_v;
313
+ }
311
314
  NUMBER(maybe_number_v, maybe_number)
312
315
  CHECK_FOR_CONVERSION_ERROR
313
316
  return new_excel_number(maybe_number);
@@ -847,7 +850,7 @@ static ExcelValue excel_match(ExcelValue lookup_value, ExcelValue lookup_array,
847
850
  }
848
851
 
849
852
  static ExcelValue excel_match_2(ExcelValue lookup_value, ExcelValue lookup_array ) {
850
- return excel_match(lookup_value, lookup_array, ZERO);
853
+ return excel_match(lookup_value, lookup_array, ONE);
851
854
  }
852
855
 
853
856
  static ExcelValue find(ExcelValue find_text_v, ExcelValue within_text_v, ExcelValue start_number_v) {
@@ -1615,9 +1618,10 @@ static ExcelValue subtotal(ExcelValue subtotal_type_v, int number_of_arguments,
1615
1618
  }
1616
1619
 
1617
1620
 
1621
+ // FIXME: Check if this deals with errors correctly
1618
1622
  static ExcelValue filter_range(ExcelValue original_range_v, int number_of_arguments, ExcelValue *arguments) {
1619
1623
  // First, set up the original_range
1620
- CHECK_FOR_PASSED_ERROR(original_range_v);
1624
+ //CHECK_FOR_PASSED_ERROR(original_range_v);
1621
1625
 
1622
1626
  // Set up the sum range
1623
1627
  ExcelValue *original_range;
@@ -115,11 +115,9 @@ int test_functions() {
115
115
  ExcelValue excel_match_array_5_v = new_excel_range(excel_match_array_5,1,3);
116
116
 
117
117
  // Two argument version
118
- assert(excel_match_2(new_excel_number(10),excel_match_array_1_v).number == 1);
119
- assert(excel_match_2(new_excel_number(100),excel_match_array_1_v).number == 2);
120
- assert(excel_match_2(new_excel_number(1000),excel_match_array_1_v).type == ExcelError);
121
- assert(excel_match_2(new_excel_number(0), excel_match_array_4_v).number == 2);
122
- assert(excel_match_2(BLANK, excel_match_array_5_v).number == 2);
118
+ assert(excel_match_2(new_excel_number(14),excel_match_array_1_v).number == 1);
119
+ assert(excel_match_2(new_excel_number(110),excel_match_array_1_v).number == 2);
120
+ assert(excel_match_2(new_excel_number(-10),excel_match_array_1_v).type == ExcelError);
123
121
 
124
122
  // Three argument version
125
123
  assert(excel_match(new_excel_number(10.0), excel_match_array_1_v, new_excel_number(0) ).number == 1);
@@ -512,7 +510,8 @@ int test_functions() {
512
510
  assert(sumifs(new_excel_number(100),2,sumifs_array_14).number == 0);
513
511
 
514
512
  // ... should return an error if range argument is an error
515
- assert(sumifs(REF,2,sumifs_array_13).type == ExcelError);
513
+ ExcelValue sumifs_array_15[] = {ONE, ONE};
514
+ assert(sumifs(REF,2,sumifs_array_15).type == ExcelError);
516
515
 
517
516
 
518
517
  // Test SUMIF
@@ -20,7 +20,7 @@ class MapValuesToC
20
20
  end
21
21
 
22
22
  def inlined_blank
23
- "ZERO"
23
+ "BLANK"
24
24
  end
25
25
 
26
26
  def constant(name)
@@ -30,16 +30,7 @@ class MapValuesToC
30
30
  alias :null :blank
31
31
 
32
32
  def number(text)
33
- n = case text
34
- when /\./
35
- text.to_f.to_s
36
- when /e/i
37
- text.to_f.to_s
38
- else
39
- text.to_i.to_s
40
- end
41
-
42
- case n
33
+ case text.to_f
43
34
  when 0; "ZERO"
44
35
  when 1; "ONE"
45
36
  when 2; "TWO"
@@ -52,6 +43,14 @@ class MapValuesToC
52
43
  when 9; "NINE"
53
44
  when 10; "TEN"
54
45
  else
46
+ n = case text
47
+ when /\./
48
+ text.to_f.to_s
49
+ when /e/i
50
+ text.to_f.to_s
51
+ else
52
+ text.to_i.to_s
53
+ end
55
54
  "new_excel_number(#{n})"
56
55
  end
57
56
  end
@@ -1,76 +1,10 @@
1
1
  module ExcelFunctions
2
2
 
3
- def averageifs(average_range, *criteria)
4
- # First, get rid of the errors
5
- return average_range if average_range.is_a?(Symbol)
6
- error = criteria.find { |a| a.is_a?(Symbol) }
7
- return error if error
8
-
9
- # Sort out the average range
10
- average_range = [average_range] unless average_range.is_a?(Array)
11
- average_range = average_range.flatten
12
-
13
- # Sort out the criteria
14
- 0.step(criteria.length-1,2).each do |i|
15
- if criteria[i].is_a?(Array)
16
- criteria[i] = criteria[i].flatten
17
- else
18
- criteria[i] = [criteria[i]]
19
- end
20
- end
21
-
22
- # This will hold the items that pass the test
23
- accumulator = []
24
-
25
- # Work through each part of the average range
26
- average_range.each_with_index do |potential_average,index|
27
- next unless potential_average.is_a?(Numeric)
28
-
29
- # If a criteria fails, this is set to false and no further criteria are evaluated
30
- pass = true
31
-
32
- 0.step(criteria.length-1,2).each do |i|
33
- check_range = criteria[i]
34
- required_value = criteria[i+1]
35
- return :value if index >= check_range.length
36
- check_value = check_range[index]
37
-
38
- pass = case check_value
39
- when String
40
- check_value.downcase == required_value.to_s.downcase
41
- when true, false
42
- check_value == required_value
43
- when nil
44
- required_value == ""
45
- when Numeric
46
- case required_value
47
- when Numeric
48
- check_value == required_value
49
- when String
50
- required_value =~ /^(<=|>=|<|>)?([-+]?[0-9]+\.?[0-9]*([eE][-+]?[0-9]+)?)$/
51
- if $1 && $2
52
- check_value.send($1,$2.to_f)
53
- elsif $2
54
- check_value == $2.to_f
55
- else
56
- false
57
- end
58
- else
59
- check_value == required_value
60
- end
61
- end # case check_value
62
-
63
- break unless pass
64
- end # criteria loop
65
-
66
- accumulator << potential_average if pass
67
-
68
- end
3
+ # See sumifs.rb for _filtered_range
69
4
 
70
- accumulator.delete_if { |a| !a.is_a?(Numeric) }
71
- return :div0 if accumulator.empty?
72
- accumulator.inject(0.0) { |m,i| m + i.to_f } / accumulator.size.to_f
73
-
5
+ def averageifs(average_range, *criteria)
6
+ filtered = _filtered_range(average_range, *criteria)
7
+ average(*filtered)
74
8
  end
75
9
 
76
10
  end
@@ -1,6 +1,9 @@
1
1
  module ExcelFunctions
2
2
 
3
- def excel_match(lookup_value,lookup_array,match_type = 0)
3
+ # FIXME: Excel doesn't use this algorithm for matching. Seems
4
+ # to do some bisecting. Only matters if doing an ordered search
5
+ # and the range is not in order
6
+ def excel_match(lookup_value,lookup_array,match_type = 1)
4
7
  return lookup_value if lookup_value.is_a?(Symbol)
5
8
  return lookup_array if lookup_array.is_a?(Symbol)
6
9
  return match_type if match_type.is_a?(Symbol)
@@ -1,14 +1,9 @@
1
1
  module ExcelFunctions
2
-
3
- def sumifs(sum_range,*criteria)
4
- # First, get rid of the errors
5
- return sum_range if sum_range.is_a?(Symbol)
6
- error = criteria.find { |a| a.is_a?(Symbol) }
7
- return error if error
8
-
2
+
3
+ def _filtered_range(range, *criteria)
9
4
  # Sort out the sum range
10
- sum_range = [sum_range] unless sum_range.is_a?(Array)
11
- sum_range = sum_range.flatten
5
+ range = [range] unless range.is_a?(Array)
6
+ range = range.flatten
12
7
 
13
8
  # Sort out the criteria
14
9
  0.step(criteria.length-1,2).each do |i|
@@ -18,13 +13,12 @@ module ExcelFunctions
18
13
  criteria[i] = [criteria[i]]
19
14
  end
20
15
  end
21
-
22
- # This will be the final answer
23
- accumulator = 0
16
+
17
+ filtered = []
24
18
 
25
19
  # Work through each part of the sum range
26
- sum_range.each_with_index do |potential_sum,index|
27
- next unless potential_sum.is_a?(Numeric)
20
+ range.each_with_index do |potential,index|
21
+ #next unless potential.is_a?(Numeric)
28
22
 
29
23
  # If a criteria fails, this is set to false and no further criteria are evaluated
30
24
  pass = true
@@ -58,17 +52,22 @@ module ExcelFunctions
58
52
  else
59
53
  check_value == required_value
60
54
  end
55
+ when Symbol
56
+ check_value == required_value
61
57
  end # case check_value
62
58
 
63
59
  break unless pass
64
60
  end # criteria loop
65
61
 
66
- accumulator += potential_sum if pass
67
-
68
- end
69
-
70
- return accumulator
71
-
62
+ filtered << potential if pass
72
63
  end
64
+
65
+ return filtered
66
+ end
67
+
68
+ def sumifs(range,*criteria)
69
+ filtered = _filtered_range(range,*criteria)
70
+ sum(*filtered)
71
+ end
73
72
 
74
73
  end
@@ -18,7 +18,7 @@ class Formula < RubyPeg
18
18
  end
19
19
 
20
20
  def thing
21
- function || array || brackets || any_reference || string || percentage || number || boolean || prefix || error || named_reference
21
+ percentage || function || array || brackets || any_reference || string || number || boolean || prefix || error || named_reference
22
22
  end
23
23
 
24
24
  def argument
@@ -93,7 +93,7 @@ class Formula < RubyPeg
93
93
 
94
94
  def percentage
95
95
  node :percentage do
96
- terminal(/[-+]?[0-9]+\.?[0-9]*/) && ignore { terminal("%") }
96
+ (terminal(/[-+]?[0-9]+\.?[0-9]*/) || function) && ignore { terminal("%") }
97
97
  end
98
98
  end
99
99
 
@@ -1,6 +1,6 @@
1
1
  formula := space? expression+
2
2
  expression = string_join | comparison | arithmetic | thing
3
- thing = function | array | brackets | any_reference | string | percentage | number | boolean | prefix | error | named_reference
3
+ thing = percentage | function | array | brackets | any_reference | string | number | boolean | prefix | error | named_reference
4
4
  argument = expression | null
5
5
  function := /[A-Z]+/ `'(' space argument? (space `',' space argument)* space `')'
6
6
  brackets := `'(' space expression+ space `')'
@@ -14,7 +14,7 @@ comparator := '>=' | '<=' | '<>' | '>' | '<' | '='
14
14
  string := `'"' /(""|[^"])*/ `'"'
15
15
  any_reference = external_reference | any_internal_reference
16
16
  any_internal_reference = table_reference | local_table_reference | sheet_reference | sheetless_reference
17
- percentage := /[-+]?[0-9]+\.?[0-9]*/ `'%'
17
+ percentage := (/[-+]?[0-9]+\.?[0-9]*/ | function) `'%'
18
18
  number := /[-+]?[0-9]+\.?[0-9]*([eE][-+]?[0-9]+)?/
19
19
  operator := '+' | '-' | '/' | '*' | '^'
20
20
  external_reference := /\[\d+\]!?/ (any_internal_reference | named_reference)
data/src/excel/table.rb CHANGED
@@ -17,14 +17,14 @@ class Table
17
17
 
18
18
  case structured_reference
19
19
  when /\[#Headers\],\[(.*?)\]:\[(.*?)\]/io
20
- column_number_start = @column_name_array.find_index($1.strip.downcase)
21
- column_number_finish = @column_name_array.find_index($2.strip.downcase)
20
+ column_number_start = column_number_for($1)
21
+ column_number_finish = column_number_for($2)
22
22
  return ref_error unless column_number_start && column_number_finish
23
23
  ast_for_area @area.excel_start.offset(0,column_number_start), @area.excel_start.offset(0,column_number_finish)
24
24
 
25
25
  when /\[#Totals\],\[(.*?)\]:\[(.*?)\]/io
26
- column_number_start = @column_name_array.find_index($1.strip.downcase)
27
- column_number_finish = @column_name_array.find_index($2.strip.downcase)
26
+ column_number_start = column_number_for($1)
27
+ column_number_finish = column_number_for($2)
28
28
  return ref_error unless column_number_start && column_number_finish
29
29
  ast_for_area @area.excel_start.offset(@area.height,column_number_start), @area.excel_start.offset(@area.height,column_number_finish)
30
30
 
@@ -32,24 +32,24 @@ class Table
32
32
  r = Reference.for(calling_cell)
33
33
  r.calculate_excel_variables
34
34
  row = r.excel_row_number
35
- column_number_start = @column_name_array.find_index($1.strip.downcase)
36
- column_number_finish = @column_name_array.find_index($2.strip.downcase)
35
+ column_number_start = column_number_for($1)
36
+ column_number_finish = column_number_for($2)
37
37
  return ref_error unless column_number_start && column_number_finish
38
38
  ast_for_area @area.excel_start.offset(row - @area.excel_start.excel_row_number,column_number_start), @area.excel_start.offset(row - @area.excel_start.excel_row_number,column_number_finish)
39
39
 
40
40
  when /\[(.*?)\]:\[(.*?)\]/io
41
- column_number_start = @column_name_array.find_index($1.strip.downcase)
42
- column_number_finish = @column_name_array.find_index($2.strip.downcase)
41
+ column_number_start = column_number_for($1)
42
+ column_number_finish = column_number_for($2)
43
43
  return ref_error unless column_number_start && column_number_finish
44
44
  ast_for_area @area.excel_start.offset(1,column_number_start), @area.excel_start.offset(@area.height - @number_of_total_rows,column_number_finish)
45
45
 
46
46
  when /\[#Headers\],\[(.*?)\]/io
47
- column_number = @column_name_array.find_index($1.strip.downcase)
47
+ column_number = column_number_for($1)
48
48
  return ref_error unless column_number
49
49
  ast_for_cell @area.excel_start.offset(0,column_number)
50
50
 
51
51
  when /\[#Totals\],\[(.*?)\]/io
52
- column_number = @column_name_array.find_index($1.strip.downcase)
52
+ column_number = column_number_for($1)
53
53
  return ref_error unless column_number
54
54
  ast_for_cell @area.excel_start.offset(@area.height,column_number)
55
55
 
@@ -57,7 +57,7 @@ class Table
57
57
  r = Reference.for(calling_cell)
58
58
  r.calculate_excel_variables
59
59
  row = r.excel_row_number
60
- column_number = @column_name_array.find_index($1.strip.downcase)
60
+ column_number = column_number_for($1)
61
61
  return ref_error unless column_number
62
62
  ast_for_cell @area.excel_start.offset(row - @area.excel_start.excel_row_number,column_number)
63
63
 
@@ -96,11 +96,11 @@ class Table
96
96
  r = Reference.for(calling_cell)
97
97
  r.calculate_excel_variables
98
98
  row = r.excel_row_number
99
- column_number = @column_name_array.find_index(structured_reference.strip.downcase)
99
+ column_number = column_number_for(structured_reference)
100
100
  return ref_error unless column_number
101
101
  ast_for_cell @area.excel_start.offset(row - @area.excel_start.excel_row_number,column_number)
102
102
  else
103
- column_number = @column_name_array.find_index(structured_reference.strip.downcase)
103
+ column_number = column_number_for(structured_reference)
104
104
  return ref_error unless column_number
105
105
  ast_for_area @area.excel_start.offset(1,column_number), @area.excel_start.offset(@area.height - @number_of_total_rows,column_number)
106
106
  end
@@ -118,6 +118,11 @@ class Table
118
118
  def ref_error
119
119
  [:error,"#REF!"]
120
120
  end
121
+
122
+ def column_number_for(name)
123
+ name = name.strip.downcase.gsub("'#","#")
124
+ @column_name_array.find_index(name)
125
+ end
121
126
 
122
127
  def includes?(sheet,reference)
123
128
  return false unless @worksheet == sheet
data/src/excel_to_code.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  class ExcelToCode
2
- def self.version() "0.2.18" end
2
+ def self.version() "0.2.19" end
3
3
  end
4
4
 
5
5
  require_relative 'commands'
@@ -90,7 +90,12 @@ class CachingFormulaParser
90
90
  end
91
91
 
92
92
  def percentage(ast)
93
- ast[1] = ast[1].to_f
93
+ if ast[1].is_a?(Array)
94
+ ast.replace([:arithmetic, map(ast[1]), operator([:operator, :'/']), number([:number, 100])])
95
+ else
96
+ ast[1] = ast[1].to_f / 100.0
97
+ ast[0] = :number
98
+ end
94
99
  @percentage_cache[ast] ||= ast
95
100
  end
96
101
 
@@ -4,11 +4,16 @@ class EmergencyArrayFormulaReplaceIndirectBodge
4
4
 
5
5
  attr_accessor :current_sheet_name
6
6
  attr_accessor :references
7
+ attr_accessor :named_references
8
+ attr_accessor :tables
9
+ attr_accessor :referring_cell
7
10
 
8
11
  def initialize
9
12
  @indirect_replacer = ReplaceIndirectsWithReferencesAst.new
10
13
  @formulae_to_value_replacer = MapFormulaeToValues.new
11
14
  @inline_formulae_replacer = InlineFormulaeAst.new
15
+ @simplify_arithmetic_replacer ||= SimplifyArithmeticAst.new
16
+ @replace_ranges_with_array_literals_replacer ||= ReplaceRangesWithArrayLiteralsAst.new
12
17
  end
13
18
 
14
19
  def replace(ast)
@@ -25,17 +30,36 @@ class EmergencyArrayFormulaReplaceIndirectBodge
25
30
 
26
31
  def function(ast)
27
32
  new_ast = deep_copy(ast)
33
+ @replace_ranges_with_array_literals_replacer.map(new_ast)
28
34
  @inline_formulae_replacer.references = @references
29
35
  @inline_formulae_replacer.current_sheet_name = [@current_sheet_name]
30
36
  @inline_formulae_replacer.map(new_ast)
37
+ @simplify_arithmetic_replacer.map(new_ast)
38
+
39
+ @formulae_to_value_replacer.map(new_ast)
40
+
41
+ @named_reference_replacer ||= ReplaceNamedReferencesAst.new(@named_references)
42
+ @named_reference_replacer.default_sheet_name = @current_sheet_name
43
+ @named_reference_replacer.map(new_ast)
44
+
45
+ @table_reference_replacer ||= ReplaceTableReferenceAst.new(@tables)
46
+ @table_reference_replacer.worksheet = @current_sheet_name
47
+ @table_reference_replacer.referring_cell = @referring_cell
48
+ @table_reference_replacer.map(new_ast)
49
+
50
+ @replace_ranges_with_array_literals_replacer.map(new_ast)
51
+ @inline_formulae_replacer.map(new_ast)
31
52
  @formulae_to_value_replacer.map(new_ast)
53
+
32
54
  @indirect_replacer.replace(new_ast)
55
+
33
56
  ast.replace(new_ast)
34
57
  ast
35
58
  end
36
59
 
37
60
  def deep_copy(ast)
38
61
  return ast if ast.is_a?(Symbol)
62
+ return ast if ast.is_a?(Numeric)
39
63
  return ast.dup unless ast.is_a?(Array)
40
64
  ast.map do |a|
41
65
  deep_copy(a)
@@ -1,5 +1,3 @@
1
- module BlankCell; end
2
-
3
1
  class InlineFormulaeAst
4
2
 
5
3
  attr_accessor :references, :current_sheet_name, :inline_ast
@@ -40,6 +38,7 @@ class InlineFormulaeAst
40
38
 
41
39
  # Should be of the form [:sheet_reference, sheet_name, reference]
42
40
  # FIXME: Can we rely on reference always being a [:cell, ref] at this stage?
41
+ # FIXME: NO! Because they won't be when they are used in EmergencyArrayFormulaReplaceIndirectBodge
43
42
  def sheet_reference(ast)
44
43
  return unless ast[2][0] == :cell
45
44
  sheet = ast[1].to_sym
@@ -63,7 +63,7 @@ class MapFormulaeToValues
63
63
  case operator.last
64
64
  when :+
65
65
  ast.replace(n(right))
66
- when :*, :/, :^
66
+ when :*
67
67
  ast.replace([:number, 0])
68
68
  end
69
69
  elsif r == 0
@@ -140,6 +140,27 @@ class MapFormulaeToValues
140
140
  ast.replace(formula_value( ast[1],*values))
141
141
  end
142
142
 
143
+ def map_if(ast)
144
+ condition_ast = ast[2]
145
+ true_option_ast = ast[3]
146
+ false_option_ast = ast[4] || [:boolean_false]
147
+
148
+ condition_value = value(condition_ast)
149
+ return if condition_value == :not_a_value
150
+
151
+ case condition_value
152
+ when Symbol
153
+ ast.replace(condition_ast)
154
+ when String
155
+ ast.replace([:error, :"#VALUE!"])
156
+ when false, 0
157
+ ast.replace(false_option_ast)
158
+ else
159
+ ast.replace(true_option_ast)
160
+ end
161
+ ast
162
+ end
163
+
143
164
  def map_right(ast)
144
165
  normal_function(ast, "")
145
166
  end
@@ -161,11 +182,38 @@ class MapFormulaeToValues
161
182
  end
162
183
 
163
184
  def map_sumifs(ast)
164
- sum_value = value(ast[2])
165
185
  values = ast[3..-1].map.with_index { |a,i| value(a, (i % 2) == 0 ? 0 : nil ) }
166
- return if sum_value == :not_a_value
167
186
  return if values.any? { |a| a == :not_a_value }
168
- ast.replace(formula_value( ast[1], sum_value, *values))
187
+ return unless [:sheet_reference, :cell, :area, :array, :number, :string, :boolean_true, :boolean_false].include?(ast[2].first)
188
+ sum_value = value(ast[2])
189
+ if sum_value == :not_a_value
190
+ partially_map_sumifs(ast)
191
+ else
192
+ ast.replace(formula_value( ast[1], sum_value, *values))
193
+ end
194
+ end
195
+
196
+ def partially_map_sumifs(ast)
197
+ values = ast[3..-1].map.with_index { |a,i| value(a, (i % 2) == 0 ? 0 : nil ) }
198
+ sum_range = []
199
+ if ast[2].first == :array
200
+ ast[2].each do |row|
201
+ next if row.is_a?(Symbol)
202
+ row.each do |cell|
203
+ next if cell.is_a?(Symbol)
204
+ sum_range << cell
205
+ end
206
+ end
207
+ else
208
+ sum_range = [ast[2]]
209
+ end
210
+ sum_range_indexes = 0.upto(sum_range.length-1).to_a
211
+ filtered_range = @calculator._filtered_range(sum_range_indexes, *values)
212
+ if filtered_range.is_a?(Symbol)
213
+ ast.replace(value(filtered_range))
214
+ else
215
+ ast.replace([:function, :SUM, *sum_range.values_at(*filtered_range)])
216
+ end
169
217
  end
170
218
 
171
219
  # [:function, "COUNT", range]
@@ -180,6 +228,7 @@ class MapFormulaeToValues
180
228
  return map_index_with_only_two_arguments(ast) if ast.length == 4
181
229
 
182
230
  array_mapped = ast[2]
231
+ return ast.replace(ast[2]) if ast[2].first == :error
183
232
  row_as_number = value(ast[3])
184
233
  column_as_number = value(ast[4])
185
234
 
@@ -198,6 +247,7 @@ class MapFormulaeToValues
198
247
  # [:function, "INDEX", array, row_number]
199
248
  def map_index_with_only_two_arguments(ast)
200
249
  array_mapped = ast[2]
250
+ return ast.replace(ast[2]) if ast[2].first == :error
201
251
  row_as_number = value(ast[3])
202
252
  return if row_as_number == :not_a_value
203
253
  array_as_values = array_as_values(array_mapped)
@@ -227,9 +277,10 @@ class MapFormulaeToValues
227
277
  ast.replace([:number, number_total])
228
278
  # FIXME: Will I be haunted by this? What if doing a sum of something that isn't a number
229
279
  # and so what is expected is a VALUE error?. YES. This doesn't work well.
230
- #elsif number_total == 0 && not_number_array.size == 1
231
- # p not_number_array[0]
232
- # ast.replace(not_number_array[0])
280
+ elsif ast.length == 3 && [:cell, :sheet_reference].include?(ast[2].first)
281
+ ast.replace(n(ast[2]))
282
+ elsif ast.length == 3 && ast[2][0] == :function && ast[2][1] == :ENSURE_IS_NUMBER
283
+ ast.replace(ast[2])
233
284
  else
234
285
  new_ast = [:function, :SUM].concat(not_number_array)
235
286
  new_ast.push([:number, number_total]) unless number_total == 0
@@ -243,13 +294,30 @@ class MapFormulaeToValues
243
294
  not_number_array = []
244
295
  case ast.first
245
296
  when :array
297
+ array_total = 0
298
+ array_not_numbers = []
299
+ # First we just have a go at splitting the array into a list of numbers
300
+ # and not numbers.
246
301
  array_as_values(ast).each do |row|
247
302
  row.each do |c|
248
303
  result = filter_numbers_and_not(c)
249
- number_total += result.first
250
- not_number_array.concat(result.last)
304
+ array_total += result.first
305
+ array_not_numbers.concat(result.last)
251
306
  end
252
307
  end
308
+ # If there are no not_numbers, or only one, we are good
309
+ if array_not_numbers.length <= 1
310
+ number_total += array_total
311
+ not_number_array.concat(array_not_numbers)
312
+ # If there are more than on not_numbers we aren't neccessarily good
313
+ # unless all those not numbers are simple
314
+ elsif array_not_numbers.all? { |c| [:cell, :area, :sheet_reference].include?(c.first)}
315
+ number_total += array_total
316
+ not_number_array.concat(array_not_numbers)
317
+ # Otherwise, leave that array alone
318
+ else
319
+ not_number_array.push(ast)
320
+ end
253
321
  when :blank, :number, :percentage, :string, :boolean_true, :boolean_false
254
322
  number = @calculator.number_argument(value(ast))
255
323
  if number.is_a?(Symbol)
@@ -14,7 +14,6 @@ class ReplaceArithmeticOnRangesAst
14
14
  # Three different options, array on the left, array on the right, or both
15
15
  # array on the left first
16
16
  if left.first == :array && right.first != :array
17
- map(right)
18
17
  ast.replace(
19
18
  array_map(left) do |cell|
20
19
  [:comparison, map(cell), operator, right]
@@ -23,7 +22,6 @@ class ReplaceArithmeticOnRangesAst
23
22
 
24
23
  # array on the right next
25
24
  elsif left.first != :array && right.first == :array
26
- map(left)
27
25
  ast.replace(
28
26
  array_map(right) do |cell|
29
27
  [:comparison, left, operator, map(cell)]
@@ -49,9 +47,6 @@ class ReplaceArithmeticOnRangesAst
49
47
  end
50
48
  end
51
49
  )
52
- else
53
- map(left)
54
- map(right)
55
50
  end
56
51
  end
57
52
 
@@ -64,7 +59,6 @@ class ReplaceArithmeticOnRangesAst
64
59
  # Three different options, array on the left, array on the right, or both
65
60
  # array on the left first
66
61
  if left.first == :array && right.first != :array
67
- map(right)
68
62
  ast.replace(
69
63
  array_map(left) do |cell|
70
64
  [:arithmetic, map(cell), operator, right]
@@ -73,7 +67,6 @@ class ReplaceArithmeticOnRangesAst
73
67
 
74
68
  # array on the right next
75
69
  elsif left.first != :array && right.first == :array
76
- map(left)
77
70
  ast.replace(
78
71
  array_map(right) do |cell|
79
72
  [:arithmetic, left, operator, map(cell)]
@@ -99,9 +92,6 @@ class ReplaceArithmeticOnRangesAst
99
92
  end
100
93
  end
101
94
  )
102
- else
103
- map(left)
104
- map(right)
105
95
  end
106
96
  end
107
97
 
@@ -18,10 +18,13 @@ class ReplaceArraysWithSingleCellsAst
18
18
  # Special case, only change if at the top level
19
19
  elsif ast[0] == :function && ast[1] == :CHOOSE && check_choose(ast)
20
20
  # Replacement made in check
21
+ # Special case, only change if at the top level
22
+ elsif ast[0] == :function && ast[1] == :IF && check_if(ast)
23
+ # Replacement made in check
21
24
  else
22
25
  do_map(ast)
23
- ast
24
26
  end
27
+ ast
25
28
  end
26
29
 
27
30
  def do_map(ast)
@@ -74,6 +77,19 @@ class ReplaceArraysWithSingleCellsAst
74
77
  replacement_made
75
78
  end
76
79
 
80
+ def check_if(ast)
81
+ replacement_made = false
82
+ if ast[3] && ast[3].first == :array
83
+ replacement_made = true
84
+ ast[3] = try_and_convert_array(ast[3])
85
+ end
86
+ if ast[4] && ast[4].first == :array
87
+ replacement_made = true
88
+ ast[4] = try_and_convert_array(ast[3])
89
+ end
90
+ replacement_made
91
+ end
92
+
77
93
  def check_match(ast)
78
94
  return false unless ast[2].first == :array
79
95
  ast[2] = try_and_convert_array(ast[2])
@@ -10,18 +10,32 @@ class MapValuesToConstants
10
10
  end
11
11
  end
12
12
 
13
- POTENTIAL_CONSTANTS = { :number => true, :percentage => true, :string => true}
14
-
15
13
  def map(ast)
16
14
  return ast unless ast.is_a?(Array)
17
15
  operator = ast[0]
18
- if POTENTIAL_CONSTANTS.has_key?(operator)
16
+ if replace?(ast)
19
17
  ast.replace([:constant, constants[ast.dup]])
20
18
  else
21
19
  ast.each { |a| map(a) }
22
20
  end
23
21
  end
24
22
 
23
+ def replace?(ast)
24
+ case ast.first
25
+ when :string; return true
26
+ when :percentage, :number
27
+ n = ast.last.to_f
28
+ # Don't use constant if an integer less than ten,
29
+ # as will use constant predefined in the c runtime
30
+ return true if n > 10
31
+ return true if n < 0
32
+ return false if n % 1 == 0
33
+ return true
34
+ else
35
+ return false
36
+ end
37
+ end
38
+
25
39
  end
26
40
 
27
41
 
@@ -9,7 +9,9 @@ class SimplifyArithmeticAst
9
9
 
10
10
  def simplify_arithmetic(ast)
11
11
  return ast unless ast.is_a?(Array)
12
- ast.each { |a| simplify_arithmetic(a) }
12
+ ast.each do |a|
13
+ simplify_arithmetic(a) if a.is_a?(Array)
14
+ end
13
15
  case ast[0]
14
16
  when :arithmetic; arithmetic(ast)
15
17
  when :brackets; @brackets_to_remove << ast
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excel_to_code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.18
4
+ version: 0.2.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Counsell, Green on Black Ltd
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-02 00:00:00.000000000 Z
11
+ date: 2014-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubypeg
@@ -277,7 +277,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
277
277
  version: '0'
278
278
  requirements: []
279
279
  rubyforge_project:
280
- rubygems_version: 2.2.0
280
+ rubygems_version: 2.2.2
281
281
  signing_key:
282
282
  specification_version: 4
283
283
  summary: Converts .xlxs files into pure ruby 1.9 code or pure C code so that they