excel_to_code 0.1.10 → 0.1.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -1
- data/src/commands/excel_to_c.rb +11 -20
- data/src/commands/excel_to_ruby.rb +4 -21
- data/src/commands/excel_to_x.rb +60 -0
- data/src/compile/c/a.out +0 -0
- data/src/compile/c/excel_to_c_runtime.c +106 -3
- data/src/compile/c/map_formulae_to_c.rb +2 -0
- data/src/compile/ruby/compile_to_ruby_unit_test.rb +2 -3
- data/src/compile/ruby/map_formulae_to_ruby.rb +1 -0
- data/src/excel/excel_functions.rb +2 -0
- data/src/excel/excel_functions/hlookup.rb +48 -0
- data/src/excel/excel_functions/multiply.rb +0 -8
- data/src/excel/formula_peg.rb +7 -1
- data/src/excel/formula_peg.txt +2 -1
- data/src/extract/extract_shared_formulae.rb +4 -0
- data/src/extract/extract_shared_formulae_targets.rb +5 -0
- data/src/rewrite/rewrite_shared_formulae.rb +5 -4
- data/src/rewrite/rewrite_values_to_ast.rb +2 -1
- data/src/simplify.rb +1 -0
- data/src/simplify/map_formulae_to_values.rb +2 -0
- data/src/simplify/replace_ranges_with_array_literals.rb +11 -2
- data/src/simplify/sort_into_calculation_order.rb +65 -0
- metadata +10 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff7e5d635bef2732929fe6f7706995baa7cb5d52
|
4
|
+
data.tar.gz: c2fea67c08a78cd1fc818869492776f52d867ee1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17939af6392b8e9cc47fb773a6b431e7f217689fdf6ab9703b2276fdf19867536c21679a94d9eec59a3065f90db950985d68030bfedcdcbd083a22d5efe576ac
|
7
|
+
data.tar.gz: 05eb3b5352fb9559ae52ad4a97fdb753d1f6d8267c942566a1a1cf787dfbce15202d1182d682b4566155813fc78d7bb0ede20218293adc639fa337de725dcde8
|
data/README.md
CHANGED
@@ -40,8 +40,9 @@ There are some how to guides in the doc folder.
|
|
40
40
|
# Limitations
|
41
41
|
|
42
42
|
1. Not tested at all on Windows
|
43
|
-
2. INDIRECT formula must be convertable at runtime into a standard formula
|
43
|
+
2. INDIRECT and OFFSET formula must be convertable at runtime into a standard formula
|
44
44
|
3. Doesn't implement all functions (see doc/Which_functions_are_implemented.md)
|
45
45
|
4. Doesn't implement references that involve range unions and lists (but does implement standard ranges)
|
46
46
|
5. Sometimes gives cells as being empty, when excel would give the cell as having a numeric value of zero
|
47
47
|
6. The generated C version does not multithread and will give bad results if you try
|
48
|
+
7. Newlines are removed from strings
|
data/src/commands/excel_to_c.rb
CHANGED
@@ -372,34 +372,25 @@ END
|
|
372
372
|
o.puts "# Test for #{name}"
|
373
373
|
o.puts "require 'rubygems'"
|
374
374
|
o.puts "gem 'minitest'"
|
375
|
-
o.puts "require '
|
375
|
+
o.puts "require 'minitest/autorun'"
|
376
376
|
o.puts "require_relative '#{output_name.downcase}'"
|
377
377
|
o.puts
|
378
|
-
o.puts "class Test#{ruby_module_name} < Test
|
378
|
+
o.puts "class Test#{ruby_module_name} < Minitest::Test"
|
379
|
+
o.puts " def self.runnable_methods"
|
380
|
+
o.puts " puts 'Overriding minitest to run tests in a defined order'"
|
381
|
+
o.puts " methods = methods_matching(/^test_/)"
|
382
|
+
o.puts " end"
|
379
383
|
o.puts " def worksheet; @worksheet ||= init_spreadsheet; end"
|
380
384
|
o.puts " def init_spreadsheet; #{ruby_module_name}Shim.new end"
|
381
385
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
o.puts
|
387
|
-
o.puts " # start of #{name}"
|
388
|
-
c_name = c_name_for_worksheet_name(name)
|
389
|
-
if !cells_to_keep || cells_to_keep.empty? || cells_to_keep[name] == :all
|
390
|
-
refs_to_test = all_formulae[name].keys
|
391
|
-
else
|
392
|
-
refs_to_test = cells_to_keep[name]
|
393
|
-
end
|
394
|
-
if refs_to_test && !refs_to_test.empty?
|
395
|
-
refs_to_test = refs_to_test.map(&:upcase)
|
396
|
-
CompileToCUnitTest.rewrite(i, sloppy_tests, c_name, refs_to_test, o)
|
397
|
-
end
|
398
|
-
close(i)
|
399
|
-
end
|
386
|
+
i = input("References to test")
|
387
|
+
CompileToCUnitTest.rewrite(i, sloppy_tests, o)
|
388
|
+
close(i)
|
389
|
+
|
400
390
|
o.puts "end"
|
401
391
|
close(o)
|
402
392
|
end
|
393
|
+
|
403
394
|
|
404
395
|
def compile_code
|
405
396
|
return unless actually_compile_code || actually_run_tests
|
@@ -90,27 +90,10 @@ class ExcelToRuby < ExcelToX
|
|
90
90
|
o.puts
|
91
91
|
o.puts "class Test#{ruby_module_name} < Test::Unit::TestCase"
|
92
92
|
o.puts " def worksheet; @worksheet ||= #{ruby_module_name}.new; end"
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
worksheets do |name,xml_filename|
|
98
|
-
i = input(name,"Values")
|
99
|
-
o.puts " # Start of #{name}"
|
100
|
-
c_name = c_name_for_worksheet_name(name)
|
101
|
-
if !cells_to_keep || cells_to_keep.empty? || cells_to_keep[name] == :all
|
102
|
-
refs_to_test = formulae[name].keys
|
103
|
-
else
|
104
|
-
refs_to_test = cells_to_keep[name]
|
105
|
-
end
|
106
|
-
if refs_to_test && !refs_to_test.empty?
|
107
|
-
refs_to_test = refs_to_test.map(&:upcase)
|
108
|
-
c.rewrite(i, sloppy_tests, c_name, refs_to_test, o)
|
109
|
-
end
|
110
|
-
o.puts " # End of #{name}"
|
111
|
-
o.puts ""
|
112
|
-
close(i)
|
113
|
-
end
|
93
|
+
|
94
|
+
i = input("References to test")
|
95
|
+
CompileToCUnitTest.rewrite(i, sloppy_tests, o)
|
96
|
+
close(i)
|
114
97
|
o.puts "end"
|
115
98
|
close(o)
|
116
99
|
end
|
data/src/commands/excel_to_x.rb
CHANGED
@@ -186,6 +186,7 @@ class ExcelToX
|
|
186
186
|
inline_formulae_that_are_only_used_once
|
187
187
|
separate_formulae_elements
|
188
188
|
replace_values_with_constants
|
189
|
+
create_sorted_references_to_test
|
189
190
|
|
190
191
|
# This actually creates the code (implemented in subclasses)
|
191
192
|
write_code
|
@@ -601,6 +602,9 @@ class ExcelToX
|
|
601
602
|
r.sheet_name = name
|
602
603
|
replace r, [name, 'Formulae'], 'Named references', [name, 'Formulae']
|
603
604
|
|
605
|
+
# The result of the indirect might contain arithmetic, which we need to simplify
|
606
|
+
replace SimplifyArithmetic, [name, 'Formulae'], [name, 'Formulae']
|
607
|
+
|
604
608
|
# The result of the indirect might be a table reference, which we need to simplify
|
605
609
|
r = ReplaceTableReferences.new
|
606
610
|
r.sheet_name = name
|
@@ -734,6 +738,51 @@ class ExcelToX
|
|
734
738
|
remove_any_cells_not_needed_for_outputs
|
735
739
|
end
|
736
740
|
|
741
|
+
# This comes up with a list of references to test, in the form of a file called 'References to test'.
|
742
|
+
# It is structured to contain one reference per row:
|
743
|
+
# worksheet_c_name \t ref \t value_ast
|
744
|
+
# These will be sorted so that later refs depend on earlier refs. This should mean that the first test that
|
745
|
+
# fails will be the root cause of the problem
|
746
|
+
def create_sorted_references_to_test
|
747
|
+
all_formulae = all_formulae()
|
748
|
+
references_to_test = {}
|
749
|
+
|
750
|
+
# First get the list of references we should test
|
751
|
+
worksheets do |name, xml_filename|
|
752
|
+
log.info "Workingout references to test for #{name}"
|
753
|
+
|
754
|
+
# Either keep all the cells on the sheet
|
755
|
+
if !cells_to_keep || cells_to_keep.empty? || cells_to_keep[name] == :all
|
756
|
+
keep = all_formulae[name].keys || []
|
757
|
+
else # Or just those specified as cells that will be kept
|
758
|
+
keep = cells_to_keep[name] || []
|
759
|
+
end
|
760
|
+
|
761
|
+
# Now go through and match the cells to keep with their values
|
762
|
+
i = input([name,"Values"])
|
763
|
+
i.each_line do |line|
|
764
|
+
ref, formula = line.split("\t")
|
765
|
+
next unless keep.include?(ref.upcase)
|
766
|
+
references_to_test[[name, ref]] = formula
|
767
|
+
end
|
768
|
+
close(i)
|
769
|
+
end
|
770
|
+
|
771
|
+
# Now work out dependency tree
|
772
|
+
sorted_references = SortIntoCalculationOrder.new.sort(all_formulae)
|
773
|
+
|
774
|
+
references_to_test_file = intermediate("References to test")
|
775
|
+
sorted_references.each do |ref|
|
776
|
+
ast = references_to_test[ref]
|
777
|
+
next unless ast
|
778
|
+
c_name = c_name_for_worksheet_name(ref[0])
|
779
|
+
references_to_test_file.puts "#{c_name}\t#{ref[1]}\t#{ast}"
|
780
|
+
end
|
781
|
+
|
782
|
+
close references_to_test_file
|
783
|
+
end
|
784
|
+
|
785
|
+
|
737
786
|
# This looks for repeated formula parts, and separates them out. It is the opposite of inlining:
|
738
787
|
# e.g., A1 := (B1 + B3) + B10; A2 := (B1 + B3) + 3 gets transformed to: Common1 := B1 + B3 ; A1 := Common1 + B10 ; A2 := Common1 + 3
|
739
788
|
def separate_formulae_elements
|
@@ -812,6 +861,7 @@ class ExcelToX
|
|
812
861
|
count.each do |sheet,keys|
|
813
862
|
keys.each do |ref,count|
|
814
863
|
next unless count >= 1
|
864
|
+
next unless references[sheet]
|
815
865
|
ast = references[sheet][ref]
|
816
866
|
next unless ast
|
817
867
|
if [:blank,:number,:null,:string,:shared_string,:constant,:percentage,:error,:boolean_true,:boolean_false].include?(ast.first)
|
@@ -954,6 +1004,7 @@ class ExcelToX
|
|
954
1004
|
filename = versioned_filename_write(intermediate_directory,*args)
|
955
1005
|
if run_in_memory
|
956
1006
|
@files ||= {}
|
1007
|
+
remove_obsolete_versioned_filenames(intermediate_directory, *args)
|
957
1008
|
@files[filename] = StringIO.new("",'w')
|
958
1009
|
else
|
959
1010
|
FileUtils.mkdir_p(File.dirname(filename))
|
@@ -979,6 +1030,15 @@ class ExcelToX
|
|
979
1030
|
@ruby_module_name = @ruby_module_name.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
980
1031
|
@ruby_module_name
|
981
1032
|
end
|
1033
|
+
|
1034
|
+
def remove_obsolete_versioned_filenames(*args)
|
1035
|
+
return unless run_in_memory
|
1036
|
+
standardised_name = standardise_name(args)
|
1037
|
+
counter = @versioned_filenames[standardised_name] || 0
|
1038
|
+
0.upto(counter-1).map do |c|
|
1039
|
+
@files.delete(filename_with_counter(c, args))
|
1040
|
+
end
|
1041
|
+
end
|
982
1042
|
|
983
1043
|
def versioned_filename_read(*args)
|
984
1044
|
@versioned_filenames ||= {}
|
data/src/compile/c/a.out
CHANGED
Binary file
|
@@ -55,6 +55,8 @@ static ExcelValue less_than(ExcelValue a_v, ExcelValue b_v);
|
|
55
55
|
static ExcelValue less_than_or_equal(ExcelValue a_v, ExcelValue b_v);
|
56
56
|
static ExcelValue find_2(ExcelValue string_to_look_for_v, ExcelValue string_to_look_in_v);
|
57
57
|
static ExcelValue find(ExcelValue string_to_look_for_v, ExcelValue string_to_look_in_v, ExcelValue position_to_start_at_v);
|
58
|
+
static ExcelValue hlookup_3(ExcelValue lookup_value_v,ExcelValue lookup_table_v, ExcelValue row_number_v);
|
59
|
+
static ExcelValue hlookup(ExcelValue lookup_value_v,ExcelValue lookup_table_v, ExcelValue row_number_v, ExcelValue match_type_v);
|
58
60
|
static ExcelValue iferror(ExcelValue value, ExcelValue value_if_error);
|
59
61
|
static ExcelValue excel_index(ExcelValue array_v, ExcelValue row_number_v, ExcelValue column_number_v);
|
60
62
|
static ExcelValue excel_index_2(ExcelValue array_v, ExcelValue row_number_v);
|
@@ -957,13 +959,19 @@ static ExcelValue multiply(ExcelValue a_v, ExcelValue b_v) {
|
|
957
959
|
static ExcelValue sum(int array_size, ExcelValue *array) {
|
958
960
|
double total = 0;
|
959
961
|
int i;
|
962
|
+
ExcelValue r;
|
960
963
|
for(i=0;i<array_size;i++) {
|
961
964
|
switch(array[i].type) {
|
962
965
|
case ExcelNumber:
|
963
966
|
total += array[i].number;
|
964
967
|
break;
|
965
968
|
case ExcelRange:
|
966
|
-
|
969
|
+
r = sum( array[i].rows * array[i].columns, array[i].array );
|
970
|
+
if(r.type == ExcelError) {
|
971
|
+
return r;
|
972
|
+
} else {
|
973
|
+
total += number_from(r);
|
974
|
+
}
|
967
975
|
break;
|
968
976
|
case ExcelError:
|
969
977
|
return array[i];
|
@@ -1133,7 +1141,12 @@ static ExcelValue power(ExcelValue a_v, ExcelValue b_v) {
|
|
1133
1141
|
NUMBER(a_v, a)
|
1134
1142
|
NUMBER(b_v, b)
|
1135
1143
|
CHECK_FOR_CONVERSION_ERROR
|
1136
|
-
|
1144
|
+
double result = pow(a,b);
|
1145
|
+
if(isnan(result) == 1) {
|
1146
|
+
return NUM;
|
1147
|
+
} else {
|
1148
|
+
return new_excel_number(result);
|
1149
|
+
}
|
1137
1150
|
}
|
1138
1151
|
|
1139
1152
|
static ExcelValue excel_round(ExcelValue number_v, ExcelValue decimal_places_v) {
|
@@ -1635,6 +1648,9 @@ static ExcelValue vlookup(ExcelValue lookup_value_v,ExcelValue lookup_table_v, E
|
|
1635
1648
|
if(lookup_value_v.type == ExcelEmpty) return NA;
|
1636
1649
|
if(lookup_table_v.type != ExcelRange) return NA;
|
1637
1650
|
if(column_number_v.type != ExcelNumber) return NA;
|
1651
|
+
if(match_type_v.type == ExcelNumber && match_type_v.number >= 0 && match_type_v.number <= 1) {
|
1652
|
+
match_type_v.type = ExcelBoolean;
|
1653
|
+
}
|
1638
1654
|
if(match_type_v.type != ExcelBoolean) return NA;
|
1639
1655
|
|
1640
1656
|
int i;
|
@@ -1671,6 +1687,57 @@ static ExcelValue vlookup(ExcelValue lookup_value_v,ExcelValue lookup_table_v, E
|
|
1671
1687
|
return NA;
|
1672
1688
|
}
|
1673
1689
|
|
1690
|
+
static ExcelValue hlookup_3(ExcelValue lookup_value_v,ExcelValue lookup_table_v, ExcelValue row_number_v) {
|
1691
|
+
return hlookup(lookup_value_v,lookup_table_v,row_number_v,TRUE);
|
1692
|
+
}
|
1693
|
+
|
1694
|
+
static ExcelValue hlookup(ExcelValue lookup_value_v,ExcelValue lookup_table_v, ExcelValue row_number_v, ExcelValue match_type_v) {
|
1695
|
+
CHECK_FOR_PASSED_ERROR(lookup_value_v)
|
1696
|
+
CHECK_FOR_PASSED_ERROR(lookup_table_v)
|
1697
|
+
CHECK_FOR_PASSED_ERROR(row_number_v)
|
1698
|
+
CHECK_FOR_PASSED_ERROR(match_type_v)
|
1699
|
+
|
1700
|
+
if(lookup_value_v.type == ExcelEmpty) return NA;
|
1701
|
+
if(lookup_table_v.type != ExcelRange) return NA;
|
1702
|
+
if(row_number_v.type != ExcelNumber) return NA;
|
1703
|
+
if(match_type_v.type == ExcelNumber && match_type_v.number >= 0 && match_type_v.number <= 1) {
|
1704
|
+
match_type_v.type = ExcelBoolean;
|
1705
|
+
}
|
1706
|
+
if(match_type_v.type != ExcelBoolean) return NA;
|
1707
|
+
|
1708
|
+
int i;
|
1709
|
+
int last_good_match = 0;
|
1710
|
+
int rows = lookup_table_v.rows;
|
1711
|
+
int columns = lookup_table_v.columns;
|
1712
|
+
ExcelValue *array = lookup_table_v.array;
|
1713
|
+
ExcelValue possible_match_v;
|
1714
|
+
|
1715
|
+
if(row_number_v.number > rows) return REF;
|
1716
|
+
if(row_number_v.number < 1) return VALUE;
|
1717
|
+
|
1718
|
+
if(match_type_v.number == false) { // Exact match required
|
1719
|
+
for(i=0; i< columns; i++) {
|
1720
|
+
possible_match_v = array[i];
|
1721
|
+
if(excel_equal(lookup_value_v,possible_match_v).number == true) {
|
1722
|
+
return array[((((int) row_number_v.number)-1)*columns)+(i)];
|
1723
|
+
}
|
1724
|
+
}
|
1725
|
+
return NA;
|
1726
|
+
} else { // Highest value that is less than or equal
|
1727
|
+
for(i=0; i< columns; i++) {
|
1728
|
+
possible_match_v = array[i];
|
1729
|
+
if(lookup_value_v.type != possible_match_v.type) continue;
|
1730
|
+
if(more_than(possible_match_v,lookup_value_v).number == true) {
|
1731
|
+
if(i == 0) return NA;
|
1732
|
+
return array[((((int) row_number_v.number)-1)*columns)+(i-1)];
|
1733
|
+
} else {
|
1734
|
+
last_good_match = i;
|
1735
|
+
}
|
1736
|
+
}
|
1737
|
+
return array[((((int) row_number_v.number)-1)*columns)+(last_good_match)];
|
1738
|
+
}
|
1739
|
+
return NA;
|
1740
|
+
}
|
1674
1741
|
|
1675
1742
|
|
1676
1743
|
int test_functions() {
|
@@ -2038,9 +2105,10 @@ int test_functions() {
|
|
2038
2105
|
assert((pmt(new_excel_number(0),new_excel_number(2),new_excel_number(10)).number - -5) < 0.01);
|
2039
2106
|
|
2040
2107
|
// Test power
|
2041
|
-
// ... should return
|
2108
|
+
// ... should return power of its arguments
|
2042
2109
|
assert(power(new_excel_number(2),new_excel_number(3)).number == 8);
|
2043
2110
|
assert(power(new_excel_number(4.0),new_excel_number(0.5)).number == 2.0);
|
2111
|
+
assert(power(new_excel_number(-4.0),new_excel_number(0.5)).type == ExcelError);
|
2044
2112
|
|
2045
2113
|
// Test round
|
2046
2114
|
assert(excel_round(new_excel_number(1.1), new_excel_number(0)).number == 1.0);
|
@@ -2256,6 +2324,9 @@ int test_functions() {
|
|
2256
2324
|
assert(vlookup(new_excel_number(2.6),vlookup_a1_v,new_excel_number(2),FALSE).type == ExcelError);
|
2257
2325
|
assert(vlookup(new_excel_string("HELLO"),vlookup_a2_v,new_excel_number(2),FALSE).number == 10);
|
2258
2326
|
assert(vlookup(new_excel_string("HELMP"),vlookup_a2_v,new_excel_number(2),TRUE).number == 10);
|
2327
|
+
// .. the four argument variant should accept 0 and 1 instead of TRUE and FALSE
|
2328
|
+
assert(vlookup(new_excel_string("HELLO"),vlookup_a2_v,new_excel_number(2),ZERO).number == 10);
|
2329
|
+
assert(vlookup(new_excel_string("HELMP"),vlookup_a2_v,new_excel_number(2),ONE).number == 10);
|
2259
2330
|
// ... BLANK should not match with anything" do
|
2260
2331
|
assert(vlookup_3(BLANK,vlookup_a3_v,new_excel_number(2)).type == ExcelError);
|
2261
2332
|
// ... should return an error if an argument is an error" do
|
@@ -2265,6 +2336,38 @@ int test_functions() {
|
|
2265
2336
|
assert(vlookup(new_excel_number(2.0),vlookup_a1_v,new_excel_number(2),VALUE).type == ExcelError);
|
2266
2337
|
assert(vlookup(VALUE,VALUE,VALUE,VALUE).type == ExcelError);
|
2267
2338
|
|
2339
|
+
// Test HLOOKUP
|
2340
|
+
ExcelValue hlookup_a1[] = {new_excel_number(1),new_excel_number(2),new_excel_number(3),new_excel_number(10),new_excel_number(20),new_excel_number(30)};
|
2341
|
+
ExcelValue hlookup_a2[] = {new_excel_string("hello"),new_excel_number(2),new_excel_number(3),new_excel_number(10),new_excel_number(20),new_excel_number(30)};
|
2342
|
+
ExcelValue hlookup_a3[] = {BLANK,new_excel_number(2),new_excel_number(3),new_excel_number(10),new_excel_number(20),new_excel_number(30)};
|
2343
|
+
ExcelValue hlookup_a1_v = new_excel_range(hlookup_a1,2,3);
|
2344
|
+
ExcelValue hlookup_a2_v = new_excel_range(hlookup_a2,2,3);
|
2345
|
+
ExcelValue hlookup_a3_v = new_excel_range(hlookup_a3,2,3);
|
2346
|
+
// ... should match the first argument against the first column of the table in the second argument, returning the value in the column specified by the third argument
|
2347
|
+
assert(hlookup_3(new_excel_number(2.0),hlookup_a1_v,new_excel_number(2)).number == 20);
|
2348
|
+
assert(hlookup_3(new_excel_number(1.5),hlookup_a1_v,new_excel_number(2)).number == 10);
|
2349
|
+
assert(hlookup_3(new_excel_number(0.5),hlookup_a1_v,new_excel_number(2)).type == ExcelError);
|
2350
|
+
assert(hlookup_3(new_excel_number(10),hlookup_a1_v,new_excel_number(2)).number == 30);
|
2351
|
+
assert(hlookup_3(new_excel_number(2.6),hlookup_a1_v,new_excel_number(2)).number == 20);
|
2352
|
+
// ... has a four argument variant that matches the lookup type
|
2353
|
+
assert(hlookup(new_excel_number(2.6),hlookup_a1_v,new_excel_number(2),TRUE).number == 20);
|
2354
|
+
assert(hlookup(new_excel_number(2.6),hlookup_a1_v,new_excel_number(2),FALSE).type == ExcelError);
|
2355
|
+
assert(hlookup(new_excel_string("HELLO"),hlookup_a2_v,new_excel_number(2),FALSE).number == 10);
|
2356
|
+
assert(hlookup(new_excel_string("HELMP"),hlookup_a2_v,new_excel_number(2),TRUE).number == 10);
|
2357
|
+
// ... that four argument variant should accept 0 or 1 for the lookup type
|
2358
|
+
assert(hlookup(new_excel_number(2.6),hlookup_a1_v,new_excel_number(2),ONE).number == 20);
|
2359
|
+
assert(hlookup(new_excel_number(2.6),hlookup_a1_v,new_excel_number(2),ZERO).type == ExcelError);
|
2360
|
+
assert(hlookup(new_excel_string("HELLO"),hlookup_a2_v,new_excel_number(2),ZERO).number == 10);
|
2361
|
+
assert(hlookup(new_excel_string("HELMP"),hlookup_a2_v,new_excel_number(2),ONE).number == 10);
|
2362
|
+
// ... BLANK should not match with anything" do
|
2363
|
+
assert(hlookup_3(BLANK,hlookup_a3_v,new_excel_number(2)).type == ExcelError);
|
2364
|
+
// ... should return an error if an argument is an error" do
|
2365
|
+
assert(hlookup(VALUE,hlookup_a1_v,new_excel_number(2),FALSE).type == ExcelError);
|
2366
|
+
assert(hlookup(new_excel_number(2.0),VALUE,new_excel_number(2),FALSE).type == ExcelError);
|
2367
|
+
assert(hlookup(new_excel_number(2.0),hlookup_a1_v,VALUE,FALSE).type == ExcelError);
|
2368
|
+
assert(hlookup(new_excel_number(2.0),hlookup_a1_v,new_excel_number(2),VALUE).type == ExcelError);
|
2369
|
+
assert(hlookup(VALUE,VALUE,VALUE,VALUE).type == ExcelError);
|
2370
|
+
|
2268
2371
|
// Test SUM
|
2269
2372
|
ExcelValue sum_array_0[] = {new_excel_number(1084.4557258064517),new_excel_number(32.0516914516129),new_excel_number(137.36439193548387)};
|
2270
2373
|
ExcelValue sum_array_0_v = new_excel_range(sum_array_0,3,1);
|
@@ -15,11 +15,10 @@ class CompileToRubyUnitTest
|
|
15
15
|
self.new.rewrite(*args)
|
16
16
|
end
|
17
17
|
|
18
|
-
def rewrite(input, sloppy,
|
18
|
+
def rewrite(input, sloppy, o)
|
19
19
|
mapper = MapValuesToRuby.new
|
20
20
|
input.each_line do |line|
|
21
|
-
ref, formula = line.split("\t")
|
22
|
-
next unless refs_to_test.include?(ref.upcase)
|
21
|
+
c_name, ref, formula = line.split("\t")
|
23
22
|
ast = eval(formula)
|
24
23
|
value = mapper.map(ast)
|
25
24
|
full_reference = "worksheet.#{c_name}_#{ref.downcase}"
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ExcelFunctions
|
2
|
+
|
3
|
+
def hlookup(lookup_value, lookup_table, row_number, match_type = true)
|
4
|
+
return lookup_value if lookup_value.is_a?(Symbol)
|
5
|
+
return lookup_table if lookup_table.is_a?(Symbol)
|
6
|
+
return row_number if row_number.is_a?(Symbol)
|
7
|
+
return match_type if match_type.is_a?(Symbol)
|
8
|
+
|
9
|
+
return :na if lookup_value == nil
|
10
|
+
return :na if lookup_table == nil
|
11
|
+
return :na if row_number == nil
|
12
|
+
return :na if match_type == nil
|
13
|
+
|
14
|
+
lookup_value = lookup_value.downcase if lookup_value.is_a?(String)
|
15
|
+
|
16
|
+
last_good_match = 0
|
17
|
+
|
18
|
+
return :value unless row_number > 0
|
19
|
+
return :ref unless row_number <= lookup_table.size
|
20
|
+
|
21
|
+
lookup_table.first.each_with_index do |possible_match, column_number|
|
22
|
+
|
23
|
+
next if lookup_value.is_a?(String) && !possible_match.is_a?(String)
|
24
|
+
next if lookup_value.is_a?(Numeric) && !possible_match.is_a?(Numeric)
|
25
|
+
|
26
|
+
possible_match.downcase! if lookup_value.is_a?(String)
|
27
|
+
|
28
|
+
if lookup_value == possible_match
|
29
|
+
return lookup_table[row_number-1][column_number]
|
30
|
+
elsif match_type == true
|
31
|
+
if possible_match > lookup_value
|
32
|
+
return :na if column_number == 0
|
33
|
+
return lookup_table[row_number-1][last_good_match]
|
34
|
+
else
|
35
|
+
last_good_match = column_number
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# We don't have a match
|
41
|
+
if match_type == true
|
42
|
+
return lookup_table[row_number - 1][last_good_match]
|
43
|
+
else
|
44
|
+
return :na
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -4,9 +4,6 @@ require_relative 'apply_to_range'
|
|
4
4
|
module ExcelFunctions
|
5
5
|
|
6
6
|
def multiply(a,b)
|
7
|
-
begin
|
8
|
-
# return apply_to_range(a,b) { |a,b| multiply(a,b) } if a.is_a?(Array) || b.is_a?(Array)
|
9
|
-
|
10
7
|
a = number_argument(a)
|
11
8
|
b = number_argument(b)
|
12
9
|
|
@@ -14,11 +11,6 @@ module ExcelFunctions
|
|
14
11
|
return b if b.is_a?(Symbol)
|
15
12
|
|
16
13
|
a * b
|
17
|
-
|
18
|
-
rescue Error => e
|
19
|
-
print e.backtrace.join('\n')
|
20
|
-
raise
|
21
|
-
end
|
22
14
|
end
|
23
15
|
|
24
16
|
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 || percentage || number || boolean || prefix || named_reference
|
21
|
+
function || array || brackets || any_reference || string || percentage || number || boolean || prefix || error || named_reference
|
22
22
|
end
|
23
23
|
|
24
24
|
def argument
|
@@ -235,4 +235,10 @@ class Formula < RubyPeg
|
|
235
235
|
end
|
236
236
|
end
|
237
237
|
|
238
|
+
def error
|
239
|
+
node :error do
|
240
|
+
terminal("#REF!") || terminal("#NAME?") || terminal("#VALUE!") || terminal("#DIV/0!") || terminal("#N/A") || terminal("#NUM!")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
238
244
|
end
|
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 | percentage | number | boolean | prefix | named_reference
|
3
|
+
thing = function | array | brackets | any_reference | string | percentage | 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 `')'
|
@@ -42,4 +42,5 @@ boolean_false := `'FALSE'
|
|
42
42
|
prefix := /[-+]/ thing
|
43
43
|
space = `/[ \n]*/
|
44
44
|
null := &','
|
45
|
+
error := '#REF!' | '#NAME?' | '#VALUE!' | '#DIV/0!' | '#N/A' | '#NUM!'
|
45
46
|
|
@@ -3,10 +3,12 @@ require_relative 'extract_formulae'
|
|
3
3
|
class ExtractSharedFormulae < ExtractFormulae
|
4
4
|
|
5
5
|
attr_accessor :shared_range
|
6
|
+
attr_accessor :shared_formula_identifier
|
6
7
|
|
7
8
|
def start_formula(type,attributes)
|
8
9
|
return unless type == 'shared' && attributes.assoc('ref')
|
9
10
|
@shared_range = attributes.assoc('ref').last
|
11
|
+
@shared_formula_identifier = attributes.assoc('si').last
|
10
12
|
@parsing = true
|
11
13
|
end
|
12
14
|
|
@@ -16,6 +18,8 @@ class ExtractSharedFormulae < ExtractFormulae
|
|
16
18
|
output.write "\t"
|
17
19
|
output.write @shared_range
|
18
20
|
output.write "\t"
|
21
|
+
output.write @shared_formula_identifier
|
22
|
+
output.write "\t"
|
19
23
|
output.write @formula.join.gsub(/[\n\r]+/,'')
|
20
24
|
output.write "\n"
|
21
25
|
end
|
@@ -2,13 +2,18 @@ require_relative 'extract_formulae'
|
|
2
2
|
|
3
3
|
class ExtractSharedFormulaeTargets < ExtractFormulae
|
4
4
|
|
5
|
+
attr_accessor :shared_formula_identifier
|
6
|
+
|
5
7
|
def start_formula(type,attributes)
|
6
8
|
return unless type == 'shared'
|
9
|
+
@shared_formula_identifier = attributes.assoc('si').last
|
7
10
|
@parsing = true
|
8
11
|
end
|
9
12
|
|
10
13
|
def write_formula
|
11
14
|
output.write @ref
|
15
|
+
output.write "\t"
|
16
|
+
output.write @shared_formula_identifier
|
12
17
|
output.write "\n"
|
13
18
|
end
|
14
19
|
|
@@ -6,14 +6,14 @@ class RewriteSharedFormulae
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def rewrite(input, shared_targets, output)
|
9
|
-
shared_targets = shared_targets.each_line.map(&:strip).
|
9
|
+
shared_targets = Hash[*shared_targets.each_line.map { |l| l.split("\t").map(&:strip) }.flatten]
|
10
10
|
input.each_line do |line|
|
11
|
-
ref, copy_range, formula = line.split("\t")
|
12
|
-
share_formula(ref, formula, copy_range, shared_targets, output)
|
11
|
+
ref, copy_range, shared_formula_identifier, formula = line.split("\t")
|
12
|
+
share_formula(ref, formula, copy_range, shared_formula_identifier, shared_targets, output)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
def share_formula(ref, formula, copy_range, shared_targets, output)
|
16
|
+
def share_formula(ref, formula, copy_range, shared_formula_identifier, shared_targets, output)
|
17
17
|
shared_ast = eval(formula)
|
18
18
|
copier = AstCopyFormula.new
|
19
19
|
copy_range = Area.for(copy_range)
|
@@ -30,6 +30,7 @@ class RewriteSharedFormulae
|
|
30
30
|
copy_range.offsets.each do |row,column|
|
31
31
|
new_ref = start_reference.offset(row,column)
|
32
32
|
next unless shared_targets.include?(new_ref)
|
33
|
+
next unless shared_formula_identifier == shared_targets[new_ref]
|
33
34
|
copier.rows_to_move = row + offset_from_formula_to_start_rows
|
34
35
|
copier.columns_to_move = column + offset_from_formula_to_start_columns
|
35
36
|
ast = copier.copy(shared_ast)
|
@@ -8,6 +8,7 @@ class RewriteValuesToAst
|
|
8
8
|
|
9
9
|
# input should be in the form: 'thing\tthing\tformula\n' where the last field is always a forumla
|
10
10
|
# output will be in the form 'thing\tthing\tast\n'
|
11
|
+
# FIXME: Removes newlines and other unprintables from str types. Should actually process them.
|
11
12
|
def rewrite(input,output)
|
12
13
|
input.each_line do |line|
|
13
14
|
line =~ /^(.*?)\t(.*?)\t(.*)\n/
|
@@ -17,7 +18,7 @@ class RewriteValuesToAst
|
|
17
18
|
when 's'; [:shared_string,value]
|
18
19
|
when 'n'; [:number,value]
|
19
20
|
when 'e'; [:error,value]
|
20
|
-
when 'str'; [:string,value]
|
21
|
+
when 'str'; [:string,value.gsub(/_x[0-9A-F]{4}_/,'')]
|
21
22
|
else
|
22
23
|
$stderr.puts "Type #{type} not known in #{line}"
|
23
24
|
[:parse_error,line.inspect]
|
data/src/simplify.rb
CHANGED
@@ -15,3 +15,4 @@ require_relative "simplify/identify_repeated_formula_elements"
|
|
15
15
|
require_relative "simplify/replace_common_elements_in_formulae"
|
16
16
|
require_relative "simplify/replace_arrays_with_single_cells"
|
17
17
|
require_relative "simplify/replace_values_with_constants"
|
18
|
+
require_relative "simplify/sort_into_calculation_order"
|
@@ -108,6 +108,7 @@ class MapFormulaeToValues
|
|
108
108
|
return [:function, "INDEX", array_mapped, map(row_number), map(column_number)] unless array_as_values
|
109
109
|
|
110
110
|
result = @calculator.send(MapFormulaeToRuby::FUNCTIONS["INDEX"],array_as_values,row_as_number,column_as_number)
|
111
|
+
result = [:number, 0] if result == [:blank]
|
111
112
|
result = ast_for_value(result)
|
112
113
|
result
|
113
114
|
end
|
@@ -119,6 +120,7 @@ class MapFormulaeToValues
|
|
119
120
|
array_as_values = array_as_values(array)
|
120
121
|
return [:function, "INDEX", array_mapped, map(row_number)] unless array_as_values
|
121
122
|
result = @calculator.send(MapFormulaeToRuby::FUNCTIONS["INDEX"],array_as_values,row_as_number)
|
123
|
+
result = [:number, 0] if result == [:blank]
|
122
124
|
result = ast_for_value(result)
|
123
125
|
result
|
124
126
|
end
|
@@ -17,7 +17,12 @@ class ReplaceRangesWithArrayLiteralsAst
|
|
17
17
|
def sheet_reference(sheet,reference)
|
18
18
|
if reference.first == :area
|
19
19
|
area = Area.for("#{reference[1]}:#{reference[2]}")
|
20
|
-
area.to_array_literal(sheet)
|
20
|
+
a = area.to_array_literal(sheet)
|
21
|
+
|
22
|
+
# Don't convert single cell ranges
|
23
|
+
return a[1][1] if a.size == 2 && a[1].size == 2
|
24
|
+
a
|
25
|
+
|
21
26
|
else
|
22
27
|
[:sheet_reference,sheet,reference]
|
23
28
|
end
|
@@ -25,7 +30,11 @@ class ReplaceRangesWithArrayLiteralsAst
|
|
25
30
|
|
26
31
|
def area(start,finish)
|
27
32
|
area = Area.for("#{start}:#{finish}")
|
28
|
-
area.to_array_literal
|
33
|
+
a = area.to_array_literal
|
34
|
+
|
35
|
+
# Don't convert single cell ranges
|
36
|
+
return a[1][1] if a.size == 2 && a[1].size == 2
|
37
|
+
a
|
29
38
|
end
|
30
39
|
|
31
40
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class SortIntoCalculationOrder
|
2
|
+
|
3
|
+
attr_accessor :references
|
4
|
+
attr_accessor :current_sheet
|
5
|
+
attr_accessor :ordered_references
|
6
|
+
|
7
|
+
|
8
|
+
# FIXME: Probably not the best algorithm for this
|
9
|
+
def sort(references)
|
10
|
+
@current_sheet = []
|
11
|
+
@ordered_references = []
|
12
|
+
@references = references
|
13
|
+
|
14
|
+
# First we find the references that are at the top of the tree
|
15
|
+
references_with_counts = CountFormulaReferences.new.count(references)
|
16
|
+
tops = []
|
17
|
+
references_with_counts.each do |sheet, references|
|
18
|
+
references.each do |cell, count|
|
19
|
+
next unless count == 0
|
20
|
+
tops << [sheet, cell]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# Then we have to work through those tops
|
24
|
+
# recursively adding the cells that they depend on
|
25
|
+
tops.each do |ref|
|
26
|
+
add_ordered_references_for ref
|
27
|
+
end
|
28
|
+
@ordered_references
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_ordered_references_for(ref)
|
32
|
+
sheet = ref.first
|
33
|
+
cell = ref.last
|
34
|
+
current_sheet.push(sheet)
|
35
|
+
ast = @references[sheet][cell]
|
36
|
+
map(ast)
|
37
|
+
current_sheet.pop
|
38
|
+
ordered_references << ref
|
39
|
+
end
|
40
|
+
|
41
|
+
def map(ast)
|
42
|
+
return ast unless ast.is_a?(Array)
|
43
|
+
operator = ast[0]
|
44
|
+
if respond_to?(operator)
|
45
|
+
send(operator,*ast[1..-1])
|
46
|
+
else
|
47
|
+
ast[1..-1].each do |a|
|
48
|
+
map(a)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sheet_reference(sheet,reference)
|
54
|
+
ref = [sheet, reference.last.gsub('$','')]
|
55
|
+
return if @ordered_references.include?(ref)
|
56
|
+
add_ordered_references_for(ref)
|
57
|
+
end
|
58
|
+
|
59
|
+
def cell(reference)
|
60
|
+
ref = [current_sheet.last, reference.gsub('$','')]
|
61
|
+
return if @ordered_references.include?(ref)
|
62
|
+
add_ordered_references_for(ref)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
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.1.
|
4
|
+
version: 0.1.11
|
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: 2013-
|
11
|
+
date: 2013-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubypeg
|
@@ -79,12 +79,12 @@ description: "# excel_to_code\n\nConverts some excel spreadsheets (.xlsx, not .x
|
|
79
79
|
use sudo\n3. bundle\n4. rspec spec/*\n\nTo test the C runtime:\n1. cd src/compile/c\n2.
|
80
80
|
cc excel_to_c_runtime\n3. ./a.out\n\n# Hacking excel_to_code\n\nThere are some how
|
81
81
|
to guides in the doc folder. \n\n# Limitations\n\n1. Not tested at all on Windows\n2.
|
82
|
-
INDIRECT formula must be convertable at runtime into a standard formula\n3.
|
83
|
-
implement all functions (see doc/Which_functions_are_implemented.md)\n4.
|
84
|
-
implement references that involve range unions and lists (but does implement
|
85
|
-
ranges)\n5. Sometimes gives cells as being empty, when excel would give
|
86
|
-
as having a numeric value of zero\n6. The generated C version does not
|
87
|
-
and will give bad results if you try\n"
|
82
|
+
INDIRECT and OFFSET formula must be convertable at runtime into a standard formula\n3.
|
83
|
+
Doesn't implement all functions (see doc/Which_functions_are_implemented.md)\n4.
|
84
|
+
Doesn't implement references that involve range unions and lists (but does implement
|
85
|
+
standard ranges)\n5. Sometimes gives cells as being empty, when excel would give
|
86
|
+
the cell as having a numeric value of zero\n6. The generated C version does not
|
87
|
+
multithread and will give bad results if you try\n7. Newlines are removed from strings\n"
|
88
88
|
email: tamc@greenonblack.com
|
89
89
|
executables:
|
90
90
|
- excel_to_c
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- src/excel/excel_functions/excel_if.rb
|
134
134
|
- src/excel/excel_functions/excel_match.rb
|
135
135
|
- src/excel/excel_functions/find.rb
|
136
|
+
- src/excel/excel_functions/hlookup.rb
|
136
137
|
- src/excel/excel_functions/iferror.rb
|
137
138
|
- src/excel/excel_functions/index.rb
|
138
139
|
- src/excel/excel_functions/int.rb
|
@@ -222,6 +223,7 @@ files:
|
|
222
223
|
- src/simplify/replace_table_references.rb
|
223
224
|
- src/simplify/replace_values_with_constants.rb
|
224
225
|
- src/simplify/simplify_arithmetic.rb
|
226
|
+
- src/simplify/sort_into_calculation_order.rb
|
225
227
|
- src/simplify.rb
|
226
228
|
- src/util/not_supported_exception.rb
|
227
229
|
- src/util/try.rb
|