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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4e983a098e0ba3377ae5b0d42b53c5d627e765e8
4
- data.tar.gz: dd9b3766572dd84f5bf980fb256bfd7ce70fdb49
3
+ metadata.gz: ff7e5d635bef2732929fe6f7706995baa7cb5d52
4
+ data.tar.gz: c2fea67c08a78cd1fc818869492776f52d867ee1
5
5
  SHA512:
6
- metadata.gz: b829c3fb99bd61216b7df001ad320a9a0ecd6f6157fc07ac0c368417354e853544f25ae8b31b43dde6fb788480c6ac9e8559b6646fd8a5d55bd4a054885c0e62
7
- data.tar.gz: 6d1f1519d8daf601c6028c6c116eadd8c76dada8edb5153ee42d49946fa97831b18d0d086e46a59b49823c6126e893638f0247c2e30b2de08a3f053a917bc49a
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
@@ -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 'test/unit'"
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::Unit::TestCase"
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
- all_formulae = all_formulae()
383
-
384
- worksheets do |name,xml_filename|
385
- i = input([name,"Values"])
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
- c = CompileToRubyUnitTest.new
95
- formulae = all_formulae()
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
@@ -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
- total += number_from(sum( array[i].rows * array[i].columns, array[i].array ));
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
- return new_excel_number(pow(a,b));
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 sum of its arguments
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);
@@ -36,6 +36,8 @@ class MapFormulaeToC < MapValuesToC
36
36
  'COUNTA' => 'counta',
37
37
  'FIND2' => 'find_2',
38
38
  'FIND3' => 'find',
39
+ 'HLOOKUP3' => 'hlookup_3',
40
+ 'HLOOKUP4' => 'hlookup',
39
41
  'IF2' => 'excel_if_2',
40
42
  'IF3' => 'excel_if',
41
43
  'IFERROR' => 'iferror',
@@ -15,11 +15,10 @@ class CompileToRubyUnitTest
15
15
  self.new.rewrite(*args)
16
16
  end
17
17
 
18
- def rewrite(input, sloppy, c_name, refs_to_test, o)
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}"
@@ -25,6 +25,7 @@ class MapFormulaeToRuby < MapValuesToRuby
25
25
  'COUNT' => 'count',
26
26
  'COUNTA' => 'counta',
27
27
  'FIND' => 'find',
28
+ 'HLOOKUP' => 'hlookup',
28
29
  'IF' => 'excel_if',
29
30
  'IFERROR' => 'iferror',
30
31
  'INDEX' => 'index',
@@ -79,3 +79,5 @@ require_relative 'excel_functions/mid'
79
79
  require_relative 'excel_functions/pv'
80
80
 
81
81
  require_relative 'excel_functions/text'
82
+
83
+ require_relative 'excel_functions/hlookup'
@@ -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
@@ -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
@@ -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).to_a
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.10
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-06-25 00:00:00.000000000 Z
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. Doesn't
83
- implement all functions (see doc/Which_functions_are_implemented.md)\n4. Doesn't
84
- implement references that involve range unions and lists (but does implement standard
85
- ranges)\n5. Sometimes gives cells as being empty, when excel would give the cell
86
- as having a numeric value of zero\n6. The generated C version does not multithread
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