excel_to_code 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. data/README +41 -0
  2. data/bin/excel_to_c +63 -0
  3. data/bin/excel_to_ruby +9 -0
  4. data/src/commands.rb +2 -0
  5. data/src/commands/excel_to_c.rb +858 -0
  6. data/src/commands/excel_to_ruby.rb +620 -0
  7. data/src/compile.rb +2 -0
  8. data/src/compile/c.rb +5 -0
  9. data/src/compile/c/compile_to_c.rb +62 -0
  10. data/src/compile/c/compile_to_c_header.rb +26 -0
  11. data/src/compile/c/compile_to_c_unit_test.rb +42 -0
  12. data/src/compile/c/excel_to_c_runtime.c +2029 -0
  13. data/src/compile/c/map_formulae_to_c.rb +184 -0
  14. data/src/compile/c/map_sheet_names_to_c_names.rb +19 -0
  15. data/src/compile/c/map_values_to_c.rb +85 -0
  16. data/src/compile/c/map_values_to_c_structs.rb +37 -0
  17. data/src/compile/ruby.rb +3 -0
  18. data/src/compile/ruby/compile_to_ruby.rb +33 -0
  19. data/src/compile/ruby/compile_to_ruby_unit_test.rb +28 -0
  20. data/src/compile/ruby/excel_to_ruby_runtime.rb +1 -0
  21. data/src/compile/ruby/map_formulae_to_ruby.rb +95 -0
  22. data/src/compile/ruby/map_sheet_names_to_ruby_names.rb +19 -0
  23. data/src/compile/ruby/map_values_to_ruby.rb +65 -0
  24. data/src/excel.rb +5 -0
  25. data/src/excel/area.rb +93 -0
  26. data/src/excel/excel_functions.rb +84 -0
  27. data/src/excel/excel_functions/abs.rb +14 -0
  28. data/src/excel/excel_functions/add.rb +18 -0
  29. data/src/excel/excel_functions/and.rb +30 -0
  30. data/src/excel/excel_functions/apply_to_range.rb +17 -0
  31. data/src/excel/excel_functions/average.rb +12 -0
  32. data/src/excel/excel_functions/choose.rb +18 -0
  33. data/src/excel/excel_functions/cosh.rb +9 -0
  34. data/src/excel/excel_functions/count.rb +9 -0
  35. data/src/excel/excel_functions/counta.rb +8 -0
  36. data/src/excel/excel_functions/divide.rb +23 -0
  37. data/src/excel/excel_functions/excel_equal.rb +20 -0
  38. data/src/excel/excel_functions/excel_if.rb +8 -0
  39. data/src/excel/excel_functions/excel_match.rb +51 -0
  40. data/src/excel/excel_functions/find.rb +39 -0
  41. data/src/excel/excel_functions/iferror.rb +10 -0
  42. data/src/excel/excel_functions/index.rb +48 -0
  43. data/src/excel/excel_functions/left.rb +12 -0
  44. data/src/excel/excel_functions/less_than.rb +26 -0
  45. data/src/excel/excel_functions/less_than_or_equal.rb +26 -0
  46. data/src/excel/excel_functions/max.rb +12 -0
  47. data/src/excel/excel_functions/min.rb +12 -0
  48. data/src/excel/excel_functions/mod.rb +15 -0
  49. data/src/excel/excel_functions/more_than.rb +26 -0
  50. data/src/excel/excel_functions/more_than_or_equal.rb +26 -0
  51. data/src/excel/excel_functions/multiply.rb +24 -0
  52. data/src/excel/excel_functions/negative.rb +12 -0
  53. data/src/excel/excel_functions/not_equal.rb +19 -0
  54. data/src/excel/excel_functions/number_argument.rb +30 -0
  55. data/src/excel/excel_functions/pi.rb +7 -0
  56. data/src/excel/excel_functions/pmt.rb +16 -0
  57. data/src/excel/excel_functions/power.rb +18 -0
  58. data/src/excel/excel_functions/round.rb +13 -0
  59. data/src/excel/excel_functions/rounddown.rb +14 -0
  60. data/src/excel/excel_functions/roundup.rb +17 -0
  61. data/src/excel/excel_functions/string_join.rb +19 -0
  62. data/src/excel/excel_functions/subtotal.rb +13 -0
  63. data/src/excel/excel_functions/subtract.rb +18 -0
  64. data/src/excel/excel_functions/sum.rb +8 -0
  65. data/src/excel/excel_functions/sumif.rb +7 -0
  66. data/src/excel/excel_functions/sumifs.rb +74 -0
  67. data/src/excel/excel_functions/sumproduct.rb +32 -0
  68. data/src/excel/excel_functions/vlookup.rb +49 -0
  69. data/src/excel/formula_peg.rb +238 -0
  70. data/src/excel/formula_peg.txt +45 -0
  71. data/src/excel/reference.rb +56 -0
  72. data/src/excel/table.rb +108 -0
  73. data/src/excel_to_code.rb +7 -0
  74. data/src/extract.rb +13 -0
  75. data/src/extract/check_for_unknown_functions.rb +20 -0
  76. data/src/extract/extract_array_formulae.rb +23 -0
  77. data/src/extract/extract_formulae.rb +36 -0
  78. data/src/extract/extract_named_references.rb +38 -0
  79. data/src/extract/extract_relationships.rb +10 -0
  80. data/src/extract/extract_shared_formulae.rb +23 -0
  81. data/src/extract/extract_shared_strings.rb +20 -0
  82. data/src/extract/extract_simple_formulae.rb +18 -0
  83. data/src/extract/extract_table.rb +24 -0
  84. data/src/extract/extract_values.rb +29 -0
  85. data/src/extract/extract_worksheet_dimensions.rb +11 -0
  86. data/src/extract/extract_worksheet_names.rb +10 -0
  87. data/src/extract/extract_worksheet_table_relationships.rb +10 -0
  88. data/src/extract/simple_extract_from_xml.rb +19 -0
  89. data/src/rewrite.rb +10 -0
  90. data/src/rewrite/ast_copy_formula.rb +42 -0
  91. data/src/rewrite/ast_expand_array_formulae.rb +180 -0
  92. data/src/rewrite/rewrite_array_formulae.rb +71 -0
  93. data/src/rewrite/rewrite_array_formulae_to_arrays.rb +18 -0
  94. data/src/rewrite/rewrite_cell_references_to_include_sheet.rb +56 -0
  95. data/src/rewrite/rewrite_formulae_to_ast.rb +24 -0
  96. data/src/rewrite/rewrite_merge_formulae_and_values.rb +18 -0
  97. data/src/rewrite/rewrite_relationship_id_to_filename.rb +22 -0
  98. data/src/rewrite/rewrite_shared_formulae.rb +38 -0
  99. data/src/rewrite/rewrite_values_to_ast.rb +28 -0
  100. data/src/rewrite/rewrite_whole_row_column_references_to_areas.rb +90 -0
  101. data/src/rewrite/rewrite_worksheet_names.rb +20 -0
  102. data/src/simplify.rb +16 -0
  103. data/src/simplify/count_formula_references.rb +58 -0
  104. data/src/simplify/identify_dependencies.rb +56 -0
  105. data/src/simplify/identify_repeated_formula_elements.rb +37 -0
  106. data/src/simplify/inline_formulae.rb +77 -0
  107. data/src/simplify/map_formulae_to_values.rb +157 -0
  108. data/src/simplify/remove_cells.rb +18 -0
  109. data/src/simplify/replace_arrays_with_single_cells.rb +27 -0
  110. data/src/simplify/replace_blanks.rb +58 -0
  111. data/src/simplify/replace_common_elements_in_formulae.rb +19 -0
  112. data/src/simplify/replace_formulae_with_calculated_values.rb +21 -0
  113. data/src/simplify/replace_indirects_with_references.rb +44 -0
  114. data/src/simplify/replace_named_references.rb +82 -0
  115. data/src/simplify/replace_ranges_with_array_literals.rb +54 -0
  116. data/src/simplify/replace_shared_strings.rb +49 -0
  117. data/src/simplify/replace_table_references.rb +71 -0
  118. data/src/simplify/replace_values_with_constants.rb +47 -0
  119. data/src/simplify/simplify_arithmetic.rb +54 -0
  120. data/src/util.rb +2 -0
  121. data/src/util/not_supported_exception.rb +2 -0
  122. data/src/util/try.rb +9 -0
  123. metadata +207 -0
data/README ADDED
@@ -0,0 +1,41 @@
1
+ # excel_to_code
2
+
3
+ Converts some excel spreadsheets (.xlsx, not .xls) into some other programming languages (currently ruby or c).
4
+ This allows the excel spreadsheets to be run programatically, without excel.
5
+
6
+ Its cannonical source is at http://github.com/tamc/excel2code
7
+
8
+ # Running excel_to_code
9
+
10
+ To just have a go:
11
+
12
+ ./bin/excel_to_c <excel_file_name>
13
+
14
+ NB:For small spreadsheets this will take a minute or so. For large spreadsheets it is best to run it overnight.
15
+
16
+ for more detail:
17
+
18
+ ./bin/excel_to_c --compile --run-tests --settable <name of input worksheet> --prune-except <name of output worksheet> <excel file name>
19
+
20
+ this should work:
21
+
22
+ ./bin/excel_to_c --help
23
+
24
+ # Testing excel_to_code
25
+
26
+ 1. Make sure you have ruby 1.9.2 or later installed
27
+ 2. gem install bundler # May need to use sudo
28
+ 3. bundle
29
+ 4. rspec spec/*
30
+
31
+ # Hacking excel_to_code
32
+
33
+ There are some how to guides in the doc folder.
34
+
35
+ # Limitations
36
+
37
+ 1. Not tested at all on Windows
38
+ 2. INDIRECT formula must be convertable at runtime into a standard formula
39
+ 3. Doesn't implement all functions (see doc/Which_functions_are_implemented.md)
40
+ 4. Doesn't implement references that involve range unions and lists
41
+ 5. Sometimes gives cells as being empty, when excel would give the cell as having a numeric value of zero
data/bin/excel_to_c ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require_relative '../src/commands/excel_to_c'
4
+
5
+ command = ExcelToC.new
6
+
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = <<-END
9
+
10
+ A command to approximately translate excel files into c code.
11
+ Usage: excel_to_c [options] input_excel_file <output_directory>
12
+ input_excel_file is the name of a .xlsx excel file (i.e., produced by Excel 2007+, not an xls file)
13
+ output_directory is the name of a folder in which to place the generated code. If not specified will have the same name as the input_excel_file, without the .xlsx
14
+
15
+ Support: http://github.com/tamc/excel2code
16
+ END
17
+
18
+ opts.separator ""
19
+ opts.separator "Specific options:"
20
+
21
+ opts.on('-s','--settable WORKSHEET','Make it possible to set the values of cells in this worksheet at runtime. By default no values are settable.') do |sheet|
22
+ command.cells_that_can_be_set_at_runtime = { sheet => :all }
23
+ end
24
+
25
+ opts.on('-o','--output-name NAME','Filename to give to c version of code (and associated ruby interface). Defaults to a folder with the same name as the excel file.') do |name|
26
+ command.output_name = name
27
+ end
28
+
29
+ opts.on('-p','--prune-except WORKSHEET',"Remove all cells except those on this worksheet, or that are required to calculate values on that worksheet. By default keeps all cells.") do |sheet|
30
+ command.cells_to_keep = { sheet => :all }
31
+ end
32
+
33
+ opts.on('-c','--compile',"Compile the generated c code") do
34
+ command.actually_compile_c_code = true
35
+ end
36
+
37
+ opts.on('-r','--run-tests',"Compile the generated c code and then run the tests") do
38
+ command.actually_compile_c_code = true
39
+ command.actually_run_tests = true
40
+ end
41
+
42
+ opts.on("-h", "--help", "Show this message") do
43
+ puts opts
44
+ exit
45
+ end
46
+ end
47
+
48
+ begin
49
+ opts.parse!(ARGV)
50
+ rescue OptionParser::ParseError => e
51
+ STDERR.puts e.message, "\n", opts
52
+ exit(-1)
53
+ end
54
+
55
+ unless ARGV.size > 0
56
+ puts opts
57
+ exit(-1)
58
+ end
59
+
60
+ command.excel_file = ARGV[0]
61
+ command.output_directory = ARGV[1] if ARGV[1]
62
+
63
+ command.go!
data/bin/excel_to_ruby ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../src/commands/excel_to_ruby'
3
+ command = ExcelToRuby.new
4
+ command.excel_file = ARGV[0]
5
+ command.compiled_module_name = "ExcelSpreadsheet"
6
+ command.output_directory = ARGV[1]
7
+ command.values_that_can_be_set_at_runtime = { ARGV[2] => :all } if ARGV[2]
8
+ command.outputs_to_keep = { ARGV[3] => :all } if ARGV[3]
9
+ command.go!
data/src/commands.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative "commands/excel_to_ruby"
2
+ require_relative "commands/excel_to_c"
@@ -0,0 +1,858 @@
1
+ # coding: utf-8
2
+
3
+ require 'fileutils'
4
+ require_relative '../util'
5
+ require_relative '../excel'
6
+ require_relative '../extract'
7
+ require_relative '../rewrite'
8
+ require_relative '../simplify'
9
+ require_relative '../compile'
10
+
11
+ # Used to throw normally fatal errors
12
+ class ExcelToCException < Exception; end
13
+
14
+ class ExcelToC
15
+
16
+ # Required attribute. The source excel file. This must be .xlsx not .xls
17
+ attr_accessor :excel_file
18
+
19
+ # Optional attribute. The output directory.
20
+ # If not specified, will be '#{excel_file_name}/c'
21
+ attr_accessor :output_directory
22
+
23
+ # Optional attribute. The name of the resulting c file (and associated ruby ffi module). Defaults to excelspreadsheet
24
+ attr_accessor :output_name
25
+
26
+ # Optional attribute. The excel file will be translated to xml and stored here.
27
+ # If not specified, will be '#{excel_file_name}/xml'
28
+ attr_accessor :xml_directory
29
+
30
+ # Optional attribute. The intermediate workings will be stored here.
31
+ # If not specified, will be '#{excel_file_name}/intermediate'
32
+ attr_accessor :intermediate_directory
33
+
34
+ # Optional attribute. Specifies which cells have setters created in the c code so their values can be altered at runtime.
35
+ # It is a hash. The keys are the sheet names. The values are either the symbol :all to specify that all cells on that sheet
36
+ # should be setable, or an array of cell names on that sheet that should be settable (e.g., A1)
37
+ attr_accessor :cells_that_can_be_set_at_runtime
38
+
39
+ # Optional attribute. Specifies which cells must appear in the final generated code.
40
+ # The default is that all cells in the original spreadsheet appear in the final code.
41
+ #
42
+ # If specified, then any cells that are not:
43
+ # * specified
44
+ # * required to calculate one of the cells that is specified
45
+ # * specified as a cell that can be set at runtime
46
+ # may be excluded from the final generated code.
47
+ #
48
+ # It is a hash. The keys are the sheet names. The values are either the symbol :all to specify that all cells on that sheet
49
+ # should be lept, or an array of cell names on that sheet that should be kept (e.g., A1)
50
+ attr_accessor :cells_to_keep
51
+
52
+ # Optional attribute. Boolean.
53
+ # * true - the generated c code is compiled
54
+ # * false - the generated c code is not compiled (default, unless actuall_run_tests is specified as true)
55
+ attr_accessor :actually_compile_c_code
56
+
57
+ # Optional attribute. Boolean.
58
+ # * true - the generated tests are run
59
+ # * false - the generated tests are not run
60
+ attr_accessor :actually_run_tests
61
+
62
+ def set_defaults
63
+ raise ExcelToCException.new("No excel file has been specified in ExcelToC.excel_file") unless excel_file
64
+
65
+ self.output_directory ||= File.join(File.dirname(excel_file),File.basename(excel_file,".*"),'c')
66
+ self.xml_directory ||= File.join(File.dirname(excel_file),File.basename(excel_file,".*"),'xml')
67
+ self.intermediate_directory ||= File.join(File.dirname(excel_file),File.basename(excel_file,".*"),'intermediate')
68
+
69
+ self.output_name ||= "Excelspreadsheet"
70
+
71
+ self.cells_that_can_be_set_at_runtime ||= {}
72
+
73
+ # Make sure that all the cell names are downcase and don't have any $ in them
74
+ cells_that_can_be_set_at_runtime.keys.each do |sheet|
75
+ next unless cells_that_can_be_set_at_runtime[sheet].is_a?(Array)
76
+ cells_that_can_be_set_at_runtime[sheet] = cells_that_can_be_set_at_runtime[sheet].map { |reference| reference.gsub('$','').downcase }
77
+ end
78
+
79
+ if cells_to_keep
80
+ cells_to_keep.keys.each do |sheet|
81
+ next unless cells_to_keep[sheet].is_a?(Array)
82
+ cells_to_keep[sheet] = cells_to_keep[sheet].map { |reference| reference.gsub('$','').downcase }
83
+ end
84
+ end
85
+
86
+ self.excel_file = File.expand_path(excel_file)
87
+ self.output_directory = File.expand_path(output_directory)
88
+
89
+ end
90
+
91
+ def go!
92
+ # This sorts out the attributes
93
+ set_defaults
94
+
95
+ # These turn the excel into a more accesible format
96
+ sort_out_output_directories
97
+ unzip_excel
98
+
99
+ # These get all the information out of the excel and put
100
+ # into a useful format
101
+ extract_data_from_workbook
102
+ extract_data_from_worksheets
103
+ merge_table_files
104
+ rewrite_worksheets
105
+
106
+ # These perform a series of transformations to the information
107
+ # with the intent of removing any redundant calculations
108
+ # that are in the excel
109
+ simplify_worksheets
110
+ optimise_and_replace_indirect_loop
111
+ replace_blanks
112
+ remove_any_cells_not_needed_for_outputs
113
+ inline_formulae_that_are_only_used_once
114
+ separate_formulae_elements
115
+
116
+ # These actually create the code version of the excel
117
+ write_out_excel_as_code
118
+ write_build_script
119
+ write_fuby_ffi_interface
120
+ write_tests
121
+
122
+ # These compile and run the code version of the excel
123
+ compile_c_code
124
+ run_tests
125
+
126
+ puts
127
+ puts "The generated code is available in #{File.join(output_directory)}"
128
+ end
129
+
130
+ def sort_out_output_directories
131
+ FileUtils.mkdir_p(output_directory)
132
+ FileUtils.mkdir_p(xml_directory)
133
+ FileUtils.mkdir_p(intermediate_directory)
134
+ end
135
+
136
+ def unzip_excel
137
+ puts `rm -fr '#{xml_directory}'`
138
+ puts `unzip -uo '#{excel_file}' -d '#{xml_directory}'`
139
+ end
140
+
141
+ def extract_data_from_workbook
142
+ extract_shared_strings
143
+
144
+ extract ExtractNamedReferences, 'workbook.xml', 'named_references'
145
+ rewrite RewriteFormulaeToAst, 'named_references', 'named_references.ast'
146
+
147
+ extract ExtractWorksheetNames, 'workbook.xml', 'worksheet_names_without_filenames'
148
+ extract ExtractRelationships, File.join('_rels','workbook.xml.rels'), 'workbook_relationships'
149
+ rewrite RewriteWorksheetNames, 'worksheet_names_without_filenames', 'workbook_relationships', 'worksheet_names'
150
+ rewrite MapSheetNamesToCNames, 'worksheet_names', 'worksheet_c_names'
151
+
152
+ extract_dimensions_from_worksheets
153
+ end
154
+
155
+ def extract_shared_strings
156
+ if File.exists?(File.join(xml_directory,'xl','sharedStrings.xml'))
157
+ extract ExtractSharedStrings, 'sharedStrings.xml', 'shared_strings'
158
+ else
159
+ FileUtils.touch(File.join(intermediate_directory,'shared_strings'))
160
+ end
161
+ end
162
+
163
+ def extract_dimensions_from_worksheets
164
+ dimension_file = intermediate('dimensions')
165
+ worksheets("Extracting dimensions") do |name,xml_filename|
166
+ dimension_file.write name
167
+ dimension_file.write "\t"
168
+ extract ExtractWorksheetDimensions, File.open(xml_filename,'r'), dimension_file
169
+ end
170
+ dimension_file.close
171
+ end
172
+
173
+ def extract_data_from_worksheets
174
+ worksheets("Initial data extract") do |name,xml_filename|
175
+ worksheet_directory = File.join(intermediate_directory,name)
176
+ FileUtils.mkdir_p(worksheet_directory)
177
+ worksheet_xml = File.open(xml_filename,'r')
178
+ { ExtractValues => 'values',
179
+ ExtractSimpleFormulae => 'simple_formulae',
180
+ ExtractSharedFormulae => 'shared_formulae',
181
+ ExtractArrayFormulae => 'array_formulae'
182
+ }.each do |_klass,output_filename|
183
+ worksheet_xml.rewind
184
+ extract _klass, worksheet_xml, File.join(name,output_filename)
185
+ if _klass == ExtractValues
186
+ rewrite RewriteValuesToAst, File.join(name,output_filename), File.join(name,"#{output_filename}.ast")
187
+ else
188
+ rewrite RewriteFormulaeToAst, File.join(name,output_filename), File.join(name,"#{output_filename}.ast")
189
+ end
190
+ end
191
+ worksheet_xml.rewind
192
+ extract ExtractWorksheetTableRelationships, worksheet_xml, File.join(name,'table_rids')
193
+ if File.exists?(File.join(xml_directory,'xl','worksheets','_rels',"#{File.basename(xml_filename)}.rels"))
194
+ extract ExtractRelationships, File.join('worksheets','_rels',"#{File.basename(xml_filename)}.rels"), File.join(name,'relationships')
195
+ rewrite RewriteRelationshipIdToFilename, File.join(name,'table_rids'), File.join(name,'relationships'), File.join(name,'table_filenames')
196
+ tables = intermediate(name,'tables')
197
+ table_extractor = ExtractTable.new(name)
198
+ table_filenames = input(name,'table_filenames')
199
+ table_filenames.lines.each do |line|
200
+ extract table_extractor, File.join('worksheets',line.strip), tables
201
+ end
202
+ close(tables,table_filenames)
203
+ else
204
+ FileUtils.touch File.join(intermediate_directory,name,'relationships')
205
+ FileUtils.touch File.join(intermediate_directory,name,'table_filenames')
206
+ FileUtils.touch File.join(intermediate_directory,name,'tables')
207
+ end
208
+ close(worksheet_xml)
209
+ end
210
+ end
211
+
212
+ def rewrite_worksheets
213
+ worksheets("Initial rewrite of references and formulae") do |name,xml_filename|
214
+ rewrite_row_and_column_references(name,xml_filename)
215
+ rewrite_shared_formulae(name,xml_filename)
216
+ rewrite_array_formulae(name,xml_filename)
217
+ combine_formulae_files(name,xml_filename)
218
+ end
219
+ end
220
+
221
+ def rewrite_row_and_column_references(name,xml_filename)
222
+ dimensions = input('dimensions')
223
+ %w{simple_formulae.ast shared_formulae.ast array_formulae.ast}.each do |file|
224
+ dimensions.rewind
225
+ i = File.open(File.join(intermediate_directory,name,file),'r')
226
+ o = File.open(File.join(intermediate_directory,name,"#{file}-nocols"),'w')
227
+ RewriteWholeRowColumnReferencesToAreas.rewrite(i,name, dimensions, o)
228
+ close(i,o)
229
+ end
230
+ dimensions.close
231
+ end
232
+
233
+ def rewrite_shared_formulae(name,xml_filename)
234
+ i = File.open(File.join(intermediate_directory,name,'shared_formulae.ast-nocols'),'r')
235
+ o = File.open(File.join(intermediate_directory,name,"shared_formulae-expanded.ast"),'w')
236
+ RewriteSharedFormulae.rewrite(i,o)
237
+ close(i,o)
238
+ end
239
+
240
+ def rewrite_array_formulae(name,xml_filename)
241
+ r = ReplaceNamedReferences.new
242
+ r.sheet_name = name
243
+ replace r, File.join(name,'array_formulae.ast-nocols'), 'named_references.ast', File.join(name,"array_formulae1.ast")
244
+
245
+ r = ReplaceTableReferences.new
246
+ r.sheet_name = name
247
+ replace r, File.join(name,'array_formulae1.ast'), 'all_tables', File.join(name,"array_formulae2.ast")
248
+ replace SimplifyArithmetic, File.join(name,'array_formulae2.ast'), File.join(name,'array_formulae3.ast')
249
+ replace ReplaceRangesWithArrayLiterals, File.join(name,"array_formulae3.ast"), File.join(name,"array_formulae4.ast")
250
+ rewrite RewriteArrayFormulaeToArrays, File.join(name,"array_formulae4.ast"), File.join(name,"array_formulae5.ast")
251
+ rewrite RewriteArrayFormulae, File.join(name,'array_formulae5.ast'), File.join(name,"array_formulae-expanded.ast")
252
+ end
253
+
254
+ def combine_formulae_files(name,xml_filename)
255
+ values = File.join(name,'values.ast')
256
+ shared_formulae = File.join(name,"shared_formulae-expanded.ast")
257
+ array_formulae = File.join(name,"array_formulae-expanded.ast")
258
+ simple_formulae = File.join(name,"simple_formulae.ast-nocols")
259
+ output = File.join(name,'formulae.ast')
260
+ rewrite RewriteMergeFormulaeAndValues, values, shared_formulae, array_formulae, simple_formulae, output
261
+ end
262
+
263
+ def merge_table_files
264
+ tables = []
265
+ worksheets("Merging table files") do |name,xml_filename|
266
+ tables << File.join(intermediate_directory,name,'tables')
267
+ end
268
+ `sort #{tables.map { |t| " '#{t}' "}.join} > #{File.join(intermediate_directory,'all_tables')}`
269
+ end
270
+
271
+ def simplify_worksheets
272
+ worksheets("Simplifying") do |name,xml_filename|
273
+ simplify_worksheet(name,xml_filename)
274
+ end
275
+ end
276
+
277
+ def simplify_worksheet(name,xml_filename)
278
+ replace SimplifyArithmetic, File.join(name,'formulae.ast'), File.join(name,'formulae_simple_arithmetic.ast')
279
+ replace ReplaceSharedStrings, File.join(name,'formulae_simple_arithmetic.ast'), 'shared_strings', File.join(name,"formulae_no_shared_strings.ast")
280
+ replace ReplaceSharedStrings, File.join(name,'values.ast'), 'shared_strings', File.join(name,"values_no_shared_strings.ast")
281
+ r = ReplaceNamedReferences.new
282
+ r.sheet_name = name
283
+ replace r, File.join(name,'formulae_no_shared_strings.ast'), 'named_references.ast', File.join(name,"formulae_no_named_references.ast")
284
+
285
+ r = ReplaceTableReferences.new
286
+ r.sheet_name = name
287
+ replace r, File.join(name,'formulae_no_named_references.ast'), 'all_tables', File.join(name,"formulae_no_table_references.ast")
288
+ replace ReplaceRangesWithArrayLiterals, File.join(name,"formulae_no_table_references.ast"), File.join(name,"formulae_no_ranges.ast")
289
+ end
290
+
291
+ def replace_blanks
292
+ references = {}
293
+ worksheets("Loading formulae") do |name,xml_filename|
294
+ r = references[name] = {}
295
+ i = input(name,"formulae_no_indirects_optimised.ast")
296
+ i.lines do |line|
297
+ ref = line[/^(.*?)\t/,1]
298
+ r[ref] = true
299
+ end
300
+ end
301
+ worksheets("Replacing blanks") do |name,xml_filename|
302
+ #fork do
303
+ r = ReplaceBlanks.new
304
+ r.references = references
305
+ r.default_sheet_name = name
306
+ replace r, File.join(name,"formulae_no_indirects_optimised.ast"),File.join(name,"formulae_no_blanks.ast")
307
+ #end
308
+ end
309
+ end
310
+
311
+ def optimise_and_replace_indirect_loop
312
+ number_of_loops = 4
313
+ 1.upto(number_of_loops) do |pass|
314
+ puts "Optimise and replace indirects pass #{pass}"
315
+ start = pass == 1 ? "formulae_no_ranges.ast" : "optimse-output-#{pass-1}.ast"
316
+ finish = pass == number_of_loops ? "formulae_no_indirects_optimised.ast" : "optimse-output-#{pass}.ast"
317
+ replace_indirects(start,"replace-indirect-output-#{pass}.ast","replace-indirect-working-#{pass}-")
318
+ optimise_sheets("replace-indirect-output-#{pass}.ast",finish,"optimse-working-#{pass}-")
319
+ end
320
+ end
321
+
322
+ def replace_indirects(start_filename,finish_filename,basename)
323
+ worksheets("Replacing indirects") do |name,xml_filename|
324
+ counter = 1
325
+ replace ReplaceIndirectsWithReferences, File.join(name,start_filename), File.join(name,"#{basename}#{counter+1}.ast")
326
+ counter += 1
327
+
328
+ r = ReplaceNamedReferences.new
329
+ r.sheet_name = name
330
+ replace r, File.join(name,"#{basename}#{counter}.ast"), 'named_references.ast', File.join(name,"#{basename}#{counter+1}.ast")
331
+ counter += 1
332
+
333
+ r = ReplaceTableReferences.new
334
+ r.sheet_name = name
335
+ replace r, File.join(name,"#{basename}#{counter}.ast"), 'all_tables', File.join(name,"#{basename}#{counter+1}.ast")
336
+ counter += 1
337
+
338
+ replace ReplaceRangesWithArrayLiterals, File.join(name,"#{basename}#{counter}.ast"), File.join(name,"#{basename}#{counter+1}.ast")
339
+ counter += 1
340
+ replace ReplaceArraysWithSingleCells, File.join(name,"#{basename}#{counter}.ast"), File.join(name,"#{basename}#{counter+1}.ast")
341
+ counter += 1
342
+
343
+ # Finally, create the output directory
344
+ i = File.join(intermediate_directory,name,"#{basename}#{counter}.ast")
345
+ o = File.join(intermediate_directory,name,finish_filename)
346
+ `cp '#{i}' '#{o}'`
347
+ end
348
+ end
349
+
350
+ def optimise_sheets(start_filename,finish_filename,basename)
351
+ counter = 1
352
+
353
+ # Setup start
354
+ worksheets("Setting up for optimise -#{counter}") do |name|
355
+ i = File.join(intermediate_directory,name,start_filename)
356
+ o = File.join(intermediate_directory,name,"#{basename}#{counter}.ast")
357
+ `cp '#{i}' '#{o}'`
358
+ end
359
+
360
+ worksheets("Replacing with calculated values #{counter}-#{counter+1}") do |name,xml_filename|
361
+ #fork do
362
+ replace ReplaceFormulaeWithCalculatedValues, File.join(name,"#{basename}#{counter}.ast"), File.join(name,"#{basename}#{counter+1}.ast")
363
+ #end
364
+ end
365
+ counter += 1
366
+ Process.waitall
367
+
368
+ references = all_formulae("#{basename}#{counter}.ast")
369
+ inline_ast_decision = lambda do |sheet,cell,references|
370
+ references_to_keep = @cells_that_can_be_set_at_runtime[sheet]
371
+ if references_to_keep && (references_to_keep == :all || references_to_keep.include?(cell))
372
+ false
373
+ else
374
+ ast = references[sheet][cell]
375
+ if ast
376
+ if [:number,:string,:blank,:null,:error,:boolean_true,:boolean_false,:sheet_reference,:cell].include?(ast.first)
377
+ # puts "Inlining #{sheet}.#{cell}: #{ast.inspect}"
378
+ true
379
+ else
380
+ false
381
+ end
382
+ else
383
+ true # Always inline blanks
384
+ end
385
+ end
386
+ end
387
+ r = InlineFormulae.new
388
+ r.references = references
389
+ r.inline_ast = inline_ast_decision
390
+
391
+ worksheets("Inlining formulae #{counter}-#{counter+1}") do |name,xml_filename|
392
+ #fork do
393
+ r.default_sheet_name = name
394
+ replace r, File.join(name,"#{basename}#{counter}.ast"), File.join(name,"#{basename}#{counter+1}.ast")
395
+ #end
396
+ end
397
+ counter += 1
398
+ Process.waitall
399
+
400
+ # Finish
401
+ worksheets("Moving sheets #{counter}-") do |name|
402
+ o = File.join(intermediate_directory,name,finish_filename)
403
+ i = File.join(intermediate_directory,name,"#{basename}#{counter}.ast")
404
+ `cp '#{i}' '#{o}'`
405
+ end
406
+ end
407
+
408
+ def remove_any_cells_not_needed_for_outputs(formula_in = "formulae_no_blanks.ast", formula_out = "formulae_pruned.ast", values_in = "values_no_shared_strings.ast", values_out = "values_pruned.ast")
409
+ if cells_to_keep && !cells_to_keep.empty?
410
+ identifier = IdentifyDependencies.new
411
+ identifier.references = all_formulae(formula_in)
412
+ cells_to_keep.each do |sheet_to_keep,cells_to_keep|
413
+ if cells_to_keep == :all
414
+ identifier.add_depedencies_for(sheet_to_keep)
415
+ elsif cells_to_keep.is_a?(Array)
416
+ cells_to_keep.each do |cell|
417
+ identifier.add_depedencies_for(sheet_to_keep,cell)
418
+ end
419
+ end
420
+ end
421
+ r = RemoveCells.new
422
+ worksheets("Removing cells") do |name,xml_filename|
423
+ #fork do
424
+ r.cells_to_keep = identifier.dependencies[name]
425
+ rewrite r, File.join(name, formula_in), File.join(name, formula_out)
426
+ rewrite r, File.join(name, values_in), File.join(name, values_out)
427
+ #end
428
+ end
429
+ Process.waitall
430
+ else
431
+ worksheets do |name,xml_filename|
432
+ i = File.join(intermediate_directory,name, formula_in)
433
+ o = File.join(intermediate_directory,name, formula_out)
434
+ `cp '#{i}' '#{o}'`
435
+ i = File.join(intermediate_directory,name, values_in)
436
+ o = File.join(intermediate_directory,name, values_out)
437
+ `cp '#{i}' '#{o}'`
438
+ end
439
+ end
440
+ end
441
+
442
+ def inline_formulae_that_are_only_used_once
443
+ references = all_formulae("formulae_pruned.ast")
444
+ counter = CountFormulaReferences.new
445
+ count = counter.count(references)
446
+
447
+ inline_ast_decision = lambda do |sheet,cell,references|
448
+ references_to_keep = @cells_that_can_be_set_at_runtime[sheet]
449
+ if references_to_keep && (references_to_keep == :all || references_to_keep.include?(cell))
450
+ false
451
+ else
452
+ count[sheet][cell] == 1
453
+ end
454
+ end
455
+
456
+ r = InlineFormulae.new
457
+ r.references = references
458
+ r.inline_ast = inline_ast_decision
459
+
460
+ worksheets("Inlining formulae") do |name,xml_filename|
461
+ #fork do
462
+ r.default_sheet_name = name
463
+ replace r, File.join(name,"formulae_pruned.ast"), File.join(name,"formulae_inlined.ast")
464
+ #end
465
+ end
466
+
467
+ remove_any_cells_not_needed_for_outputs("formulae_inlined.ast", "formulae_inlined_pruned.ast", "values_pruned.ast", "values_pruned2.ast")
468
+
469
+ end
470
+
471
+ def separate_formulae_elements
472
+ # First we add the sheet to all references, so that we can then look for common elements accross worksheets
473
+ r = RewriteCellReferencesToIncludeSheet.new
474
+ worksheets("Adding the sheet to all references") do |name,xml_filename|
475
+ r.worksheet = name
476
+ rewrite r, File.join(name,"formulae_inlined_pruned.ast"), File.join(name,"formulae_inlined_pruned_with_sheets.ast")
477
+ end
478
+
479
+ references = all_formulae("formulae_inlined_pruned_with_sheets.ast")
480
+ identifier = IdentifyRepeatedFormulaElements.new
481
+ repeated_elements = identifier.count(references)
482
+ repeated_elements.delete_if do |element,count|
483
+ count < 2
484
+ end
485
+ o = intermediate('common-elements-1.ast')
486
+ i = 0
487
+ repeated_elements.each do |element,count|
488
+ o.puts "common#{i}\t#{element}"
489
+ i = i + 1
490
+ end
491
+ close(o)
492
+
493
+ worksheets("Replacing repeated elements") do |name,xml_filename|
494
+ replace ReplaceCommonElementsInFormulae, File.join(name,"formulae_inlined_pruned_with_sheets.ast"), "common-elements-1.ast", File.join(name,"formulae_inlined_pruned_replaced-1.ast")
495
+ end
496
+
497
+ r = ReplaceValuesWithConstants.new
498
+ worksheets("Replacing values with constants") do |name,xml_filename|
499
+ i = input(name,"formulae_inlined_pruned_replaced-1.ast")
500
+ o = intermediate(name,"formulae_inlined_pruned_replaced.ast")
501
+ r.replace(i,o)
502
+ close(i,o)
503
+ end
504
+
505
+ puts "Replacing values with constants in common elements"
506
+ i = input("common-elements-1.ast")
507
+ o = intermediate("common-elements.ast")
508
+ r.replace(i,o)
509
+ close(i,o)
510
+
511
+ puts "Writing out constants"
512
+ co = intermediate("value_constants.ast")
513
+ r.rewriter.constants.each do |ast,constant|
514
+ co.puts "#{constant}\t#{ast}"
515
+ end
516
+ close(co)
517
+ end
518
+
519
+ def write_out_excel_as_code
520
+
521
+ all_refs = all_formulae("formulae_inlined_pruned_replaced.ast")
522
+
523
+ number_of_refs = 0
524
+
525
+ # Probably a better way of getting the runtime file to be compiled with the created file
526
+ puts `cp #{File.join(File.dirname(__FILE__),'..','compile','c','excel_to_c_runtime.c')} #{File.join(output_directory,'excel_to_c_runtime.c')}`
527
+
528
+ # Output the workbook preamble
529
+ w = input("worksheet_c_names")
530
+ o = output("#{output_name.downcase}.c")
531
+ o.puts "// #{excel_file} approximately translated into C"
532
+ o.puts '#include "excel_to_c_runtime.c"'
533
+ o.puts
534
+
535
+ # Now we have to put all the initial definitions out
536
+ o.puts "// definitions"
537
+
538
+ i = input("common-elements.ast")
539
+ c = CompileToCHeader.new
540
+ c.gettable = lambda { |ref| false }
541
+ c.rewrite(i,w,o)
542
+ i.rewind
543
+ number_of_refs += i.lines.to_a.size
544
+ close(i)
545
+
546
+ worksheets("Compiling definitions") do |name,xml_filename|
547
+ w.rewind
548
+ c = CompileToCHeader.new
549
+ c.settable = settable(name)
550
+ c.gettable = gettable(name)
551
+ c.worksheet = name
552
+ i = input(name,"formulae_inlined_pruned_replaced.ast")
553
+ c.rewrite(i,w,o)
554
+ i.rewind
555
+ number_of_refs += i.lines.to_a.size
556
+ close(i)
557
+ end
558
+
559
+ o.puts "// end of definitions"
560
+ o.puts
561
+ o.puts "// Used to decide whether to recalculate a cell"
562
+ o.puts "static int variable_set[#{number_of_refs}];"
563
+ o.puts "void reset() {"
564
+ o.puts " int i;"
565
+ o.puts " cell_counter = 0;"
566
+ o.puts " for(i = 0; i < #{number_of_refs}; i++) {"
567
+ o.puts " variable_set[i] = 0;"
568
+ o.puts " }"
569
+ o.puts "};"
570
+ o.puts
571
+
572
+ # Output the value constants
573
+ o.puts "// starting the value constants"
574
+ mapper = MapValuesToCStructs.new
575
+ i = input("value_constants.ast")
576
+ i.lines do |line|
577
+ begin
578
+ ref, formula = line.split("\t")
579
+ ast = eval(formula)
580
+ calculation = mapper.map(ast)
581
+ o.puts "static ExcelValue #{ref} = #{calculation};"
582
+ rescue Exception => e
583
+ puts "Exception at line #{line}"
584
+ raise
585
+ end
586
+ end
587
+ close(i)
588
+ o.puts "// ending the value constants"
589
+ o.puts
590
+
591
+ variable_set_counter = 0
592
+
593
+ # output the common elements
594
+ o.puts "// starting common elements"
595
+ w.rewind
596
+ c = CompileToC.new
597
+ c.variable_set_counter = variable_set_counter
598
+ c.gettable = lambda { |ref| false }
599
+ i = input("common-elements.ast")
600
+ c.rewrite(i,w,o)
601
+ close(i)
602
+ o.puts "// ending common elements"
603
+ o.puts
604
+
605
+ variable_set_counter = c.variable_set_counter
606
+
607
+ c = CompileToC.new
608
+ c.variable_set_counter = variable_set_counter
609
+ # Output the elements from each worksheet in turn
610
+ worksheets("Compiling worksheet") do |name,xml_filename|
611
+ w.rewind
612
+ c.settable = settable(name)
613
+ c.gettable = gettable(name)
614
+ c.worksheet = name
615
+
616
+ i = input(name,"formulae_inlined_pruned_replaced.ast")
617
+ ruby_name = c_name_for_worksheet_name(name)
618
+ o.puts "// start #{name}"
619
+ c.rewrite(i,w,o)
620
+ o.puts "// end #{name}"
621
+ o.puts
622
+ close(i)
623
+ end
624
+ close(w,o)
625
+ end
626
+
627
+ def settable(name)
628
+ settable_refs = @cells_that_can_be_set_at_runtime[name]
629
+ if settable_refs
630
+ lambda { |ref| (settable_refs == :all) ? true : settable_refs.include?(ref) }
631
+ else
632
+ lambda { |ref| false }
633
+ end
634
+ end
635
+
636
+ def gettable(name)
637
+ if @cells_to_keep
638
+ gettable_refs = @cells_to_keep[name]
639
+ if gettable_refs
640
+ lambda { |ref| (gettable_refs == :all) ? true : gettable_refs.include?(ref) }
641
+ else
642
+ lambda { |ref| false }
643
+ end
644
+ else
645
+ lambda { |ref| true }
646
+ end
647
+ end
648
+
649
+ def write_build_script
650
+ o = output("Makefile")
651
+ name = output_name.downcase
652
+
653
+ # Target for shared library
654
+ o.puts "lib#{name}.dylib: #{name}.o"
655
+ o.puts "\tgcc -shared -o lib#{name}.dylib #{name}.o"
656
+ o.puts
657
+
658
+ # Target for compiled version
659
+ o.puts "#{name}.o:"
660
+ o.puts "\tgcc -Wall -fPIC -c #{name}.c"
661
+ o.puts
662
+
663
+ # Target for cleaning
664
+ o.puts "clean:"
665
+ o.puts "\trm #{name}.o"
666
+ o.puts "\trm lib#{name}.dylib"
667
+
668
+ close(o)
669
+ end
670
+
671
+ def write_fuby_ffi_interface
672
+ all_formulae = all_formulae('formulae_inlined_pruned_replaced.ast')
673
+ name = output_name.downcase
674
+ o = output("#{name}.rb")
675
+ code = <<END
676
+ require 'ffi'
677
+
678
+ module #{name.capitalize}
679
+ extend FFI::Library
680
+ ffi_lib File.join(File.dirname(__FILE__),'lib#{name}.dylib')
681
+ ExcelType = enum :ExcelEmpty, :ExcelNumber, :ExcelString, :ExcelBoolean, :ExcelError, :ExcelRange
682
+
683
+ class ExcelValue < FFI::Struct
684
+ layout :type, ExcelType,
685
+ :number, :double,
686
+ :string, :string,
687
+ :array, :pointer,
688
+ :rows, :int,
689
+ :columns, :int
690
+ end
691
+
692
+ END
693
+ o.puts code
694
+ o.puts
695
+ o.puts " # use this function to reset all cell values"
696
+ o.puts " attach_function 'reset', [], :void"
697
+
698
+ worksheets("Adding references to ruby shim for") do |name,xml_filename|
699
+ o.puts
700
+ o.puts " # start of #{name}"
701
+ c_name = c_name_for_worksheet_name(name)
702
+
703
+ # Put in place the setters, if any
704
+ settable_refs = @cells_that_can_be_set_at_runtime[name]
705
+ if settable_refs
706
+ settable_refs = all_formulae[name].keys if settable_refs == :all
707
+ settable_refs.each do |ref|
708
+ o.puts " attach_function 'set_#{c_name}_#{ref.downcase}', [ExcelValue.by_value], :void"
709
+ end
710
+ end
711
+
712
+ if !cells_to_keep || cells_to_keep.empty? || cells_to_keep[name] == :all
713
+ getable_refs = all_formulae[name].keys
714
+ elsif !cells_to_keep[name] && settable_refs
715
+ getable_refs = settable_refs
716
+ else
717
+ getable_refs = cells_to_keep[name] || []
718
+ end
719
+
720
+ getable_refs.each do |ref|
721
+ o.puts " attach_function '#{c_name}_#{ref.downcase}', [], ExcelValue.by_value"
722
+ end
723
+
724
+ o.puts " # end of #{name}"
725
+ end
726
+ o.puts "end"
727
+ close(o)
728
+ end
729
+
730
+ def write_tests
731
+ name = output_name.downcase
732
+ o = output("#{name}_test.rb")
733
+ o.puts "# coding: utf-8"
734
+ o.puts "# Test for #{name}"
735
+ o.puts "require 'rubygems'"
736
+ o.puts "gem 'minitest'"
737
+ o.puts "require 'test/unit'"
738
+ o.puts "require_relative '#{output_name.downcase}'"
739
+ o.puts
740
+ o.puts "class Test#{name.capitalize} < Test::Unit::TestCase"
741
+ o.puts " def spreadsheet; @spreadsheet ||= init_spreadsheet; end"
742
+ o.puts " def init_spreadsheet; #{name.capitalize} end"
743
+
744
+ all_formulae = all_formulae('formulae_inlined_pruned_replaced.ast')
745
+
746
+ worksheets("Adding tests for") do |name,xml_filename|
747
+ i = input(name,"values_pruned2.ast")
748
+ o.puts
749
+ o.puts " # start of #{name}"
750
+ c_name = c_name_for_worksheet_name(name)
751
+ if !cells_to_keep || cells_to_keep.empty? || cells_to_keep[name] == :all
752
+ refs_to_test = all_formulae[name].keys
753
+ else
754
+ refs_to_test = cells_to_keep[name]
755
+ end
756
+ if refs_to_test && !refs_to_test.empty?
757
+ CompileToCUnitTest.rewrite(i, c_name, refs_to_test, o)
758
+ end
759
+ close(i)
760
+ end
761
+ o.puts "end"
762
+ close(o)
763
+ end
764
+
765
+ def compile_c_code
766
+ return unless actually_compile_c_code || actually_run_tests
767
+ puts "Compiling the resulting c code"
768
+ puts `cd #{File.join(output_directory)}; make clean; make`
769
+ end
770
+
771
+ def run_tests
772
+ return unless actually_run_tests
773
+ puts "Running the resulting tests"
774
+ puts `cd #{File.join(output_directory)}; ruby "#{output_name.downcase}_test.rb"`
775
+ end
776
+
777
+
778
+ # UTILITY FUNCTIONS
779
+
780
+ def all_formulae(filename)
781
+ references = {}
782
+ worksheets do |name,xml_filename|
783
+ r = references[name] = {}
784
+ i = input(name,filename)
785
+ i.lines do |line|
786
+ line =~ /^(.*?)\t(.*)$/
787
+ ref, ast = $1, $2
788
+ r[$1] = eval($2)
789
+ end
790
+ end
791
+ references
792
+ end
793
+
794
+ def c_name_for_worksheet_name(name)
795
+ unless @worksheet_names
796
+ w = input("worksheet_c_names")
797
+ @worksheet_names = Hash[w.readlines.map { |line| line.split("\t").map { |a| a.strip }}]
798
+ close(w)
799
+ end
800
+ @worksheet_names[name]
801
+ end
802
+
803
+ def worksheets(message = "Processing",&block)
804
+ IO.readlines(File.join(intermediate_directory,'worksheet_names')).each do |line|
805
+ name, filename = *line.split("\t")
806
+ filename = File.expand_path(File.join(xml_directory,'xl',filename.strip))
807
+ puts "#{message} #{name}"
808
+ block.call(name, filename)
809
+ end
810
+ end
811
+
812
+ def extract(_klass,xml_name,output_name)
813
+ i = xml_name.is_a?(String) ? xml(xml_name) : xml_name
814
+ o = output_name.is_a?(String) ? intermediate(output_name) : output_name
815
+ _klass.extract(i,o)
816
+ if xml_name.is_a?(String)
817
+ close(i)
818
+ end
819
+ if output_name.is_a?(String)
820
+ close(o)
821
+ end
822
+ end
823
+
824
+ def rewrite(_klass,*args)
825
+ o = intermediate(args.pop)
826
+ inputs = args.map { |name| input(name) }
827
+ _klass.rewrite(*inputs,o)
828
+ close(*inputs,o)
829
+ end
830
+
831
+ def replace(_klass,*args)
832
+ o = intermediate(args.pop)
833
+ inputs = args.map { |name| input(name) }
834
+ _klass.replace(*inputs,o)
835
+ close(*inputs,o)
836
+ end
837
+
838
+ def xml(*args)
839
+ File.open(File.join(xml_directory,'xl',*args),'r')
840
+ end
841
+
842
+ def input(*args)
843
+ File.open(File.join(intermediate_directory,*args),'r')
844
+ end
845
+
846
+ def intermediate(*args)
847
+ File.open(File.join(intermediate_directory,*args),'w')
848
+ end
849
+
850
+ def output(*args)
851
+ File.open(File.join(output_directory,*args),'w')
852
+ end
853
+
854
+ def close(*args)
855
+ args.map(&:close)
856
+ end
857
+
858
+ end