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 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