excel_to_code 0.1.10 → 0.1.11
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/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
|