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 +4 -4
- data/src/commands/excel_to_x.rb +46 -21
- data/src/compile/c/a.out +0 -0
- data/src/compile/c/excel_to_c_runtime.c +6 -2
- data/src/compile/c/excel_to_c_runtime_test.c +5 -6
- data/src/compile/c/map_values_to_c.rb +10 -11
- data/src/excel/excel_functions/averageifs.rb +4 -70
- data/src/excel/excel_functions/excel_match.rb +4 -1
- data/src/excel/excel_functions/sumifs.rb +19 -20
- data/src/excel/formula_peg.rb +2 -2
- data/src/excel/formula_peg.txt +2 -2
- data/src/excel/table.rb +18 -13
- data/src/excel_to_code.rb +1 -1
- data/src/rewrite/caching_formula_parser.rb +6 -1
- data/src/simplify/emergency_array_formula_replace_indirect_bodge.rb +24 -0
- data/src/simplify/inline_formulae.rb +1 -2
- data/src/simplify/map_formulae_to_values.rb +77 -9
- data/src/simplify/replace_arithmetic_on_ranges.rb +0 -10
- data/src/simplify/replace_arrays_with_single_cells.rb +17 -1
- data/src/simplify/replace_values_with_constants.rb +17 -3
- data/src/simplify/simplify_arithmetic.rb +3 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74698377e3e89c2dae52564440694e4e2e97f2f1
|
4
|
+
data.tar.gz: 73e3a3af8271ac02a3f68727f43e5874153d2d2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f97a19688abe21b1cdbc7755a77158379746f012bda38154b35c7114add213ef4d1cee34f6121ce61d09d3bced7ae5de98f413e297ce5c0beecc9bd586a7177d
|
7
|
+
data.tar.gz: b911c226eed1bd735828b5a80927b8198ea1b226b312032c937c9bb96a98f10ccb3bff9fc13e391c2aef0c3163307e3b1e541e371c74459ae628ac714aa34027
|
data/src/commands/excel_to_x.rb
CHANGED
@@ -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.
|
233
|
-
self.
|
234
|
-
|
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
|
-
|
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
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
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
|
-
|
1036
|
-
|
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,
|
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(
|
119
|
-
assert(excel_match_2(new_excel_number(
|
120
|
-
assert(excel_match_2(new_excel_number(
|
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
|
-
|
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
|
-
"
|
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
|
-
|
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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
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
|
-
|
11
|
-
|
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
|
-
|
23
|
-
accumulator = 0
|
16
|
+
|
17
|
+
filtered = []
|
24
18
|
|
25
19
|
# Work through each part of the sum range
|
26
|
-
|
27
|
-
next unless
|
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
|
-
|
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
|
data/src/excel/formula_peg.rb
CHANGED
@@ -18,7 +18,7 @@ class Formula < RubyPeg
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def thing
|
21
|
-
function || array || brackets || any_reference || string ||
|
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
|
|
data/src/excel/formula_peg.txt
CHANGED
@@ -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 |
|
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 =
|
21
|
-
column_number_finish =
|
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 =
|
27
|
-
column_number_finish =
|
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 =
|
36
|
-
column_number_finish =
|
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 =
|
42
|
-
column_number_finish =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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
@@ -90,7 +90,12 @@ class CachingFormulaParser
|
|
90
90
|
end
|
91
91
|
|
92
92
|
def percentage(ast)
|
93
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
250
|
-
|
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
|
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
|
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.
|
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-
|
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.
|
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
|