excel_to_code 0.2.18 → 0.2.19
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.
- 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
|