diff --git a/bin/excel_to_c b/bin/excel_to_c index 193013f..27558d7 100755 --- a/bin/excel_to_c +++ b/bin/excel_to_c @@ -6,16 +6,16 @@ command = ExcelToC.new opts = OptionParser.new do |opts| CommonCommandLineOptions.set(command: command, options: opts, generates: "C", extension: "c") - + opts.on('-c','--compile',"Compile the C") do command.actually_compile_code = true end - + opts.on('--[no-]makefile', 'Generate a makefile. Default: no.') do |b| command.create_makefile = b end - + opts.on('--[no-]rakefile', 'Generate a rakefile. Default: yes.') do |b| command.create_rakefile = b end diff --git a/src/commands/common_command_line_options.rb b/src/commands/common_command_line_options.rb index e03ab9e..12e160e 100644 --- a/src/commands/common_command_line_options.rb +++ b/src/commands/common_command_line_options.rb @@ -45,6 +45,7 @@ def self.set(command:, options:, generates:, extension:) options.on('-d', '--debug', "Fewer optimisations; the #{generates} should be more similar to the original Excel.") do command.should_inline_formulae_that_are_only_used_once = false + command.replace_reference_to_blanks_with_zeros = false command.extract_repeated_parts_of_formulae = false end diff --git a/src/commands/excel_to_c.rb b/src/commands/excel_to_c.rb index 335e756..0580218 100644 --- a/src/commands/excel_to_c.rb +++ b/src/commands/excel_to_c.rb @@ -45,6 +45,7 @@ def write_out_excel_as_code o.puts "// #{excel_file} approximately translated into C" o.puts "// definitions" o.puts "#define NUMBER_OF_REFS #{number_of_refs}" + o.puts "#define NUMBER_OF_RECURSION_PREVENT_VARS #{number_of_refs}" # This is only known after creating CompileToC, number_of_refs overapproximate it o.puts "#define EXCEL_FILENAME #{excel_file.inspect}" o.puts "// end of definitions" o.puts @@ -86,6 +87,7 @@ def write_out_excel_as_code c.settable = settable c.gettable = gettable c.rewrite(@formulae, @worksheet_c_names, o) + c.reset_sheets(@worksheet_c_names, o) # Output the named references @@ -193,7 +195,15 @@ def write_fuby_ffi_interface name = output_name.downcase o = output("#{name}.rb") - + + sheet_resets = [] + worksheets do |sheet_name, _| + sheet_name = c_name_for_worksheet_name(sheet_name) + sheet_resets.push("def reset_#{sheet_name}") + sheet_resets.push(" C.reset_#{sheet_name}") + sheet_resets.push("end\n") + end + code = < e log.fatal "Exception when simplifying #{ref}: #{ast}" @@ -1072,6 +1082,7 @@ def replace_formulae_with_their_results inline_replacer = InlineFormulaeAst.new inline_replacer.references = @formulae inline_replacer.inline_ast = inline_ast_decision + inline_replacer.named_references = @named_references value_replacer = MapFormulaeToValues.new value_replacer.original_excel_filename = excel_file @@ -1111,9 +1122,11 @@ def replace_formulae_with_their_results if cell_address_replacement.replace(ast) references_that_need_updating[ref] = ast end - # FIXME: Shouldn't need to wrap ref.fist in an array - inline_replacer.current_sheet_name = [ref.first] - inline_replacer.map(ast) + if should_inline_formulae_that_are_only_used_once + # FIXME: Shouldn't need to wrap ref.fist in an array + inline_replacer.current_sheet_name = [ref.first] + inline_replacer.map(ast) + end # If a formula references a cell containing a value, the reference is replaced with the value (e.g., if A1 := 2 and A2 := A1 + 1 then becomes: A2 := 2 + 1) #require 'pry'; binding.pry if ref == [:"Outputs - Summary table", :E77] value_replacer.map(ast) @@ -1127,9 +1140,11 @@ def replace_formulae_with_their_results end end - @named_references.each do |ref, ast| - inline_replacer.current_sheet_name = ref.is_a?(Array) ? [ref.first] : [] - inline_replacer.map(ast) + if should_inline_formulae_that_are_only_used_once + @named_references.each do |ref, ast| + inline_replacer.current_sheet_name = ref.is_a?(Array) ? [ref.first] : [] + inline_replacer.map(ast) + end end simplify(references_that_need_updating) @@ -1328,6 +1343,16 @@ def ensure_there_is_a_good_set_of_cells_that_can_be_set_at_runtime @cells_that_can_be_set_at_runtime = cells_with_settable_values end + def make_blank_referenced_cells_settable(ref, ast) + unless replace_reference_to_blanks_with_zeros + if ast == nil + ast = [:number, 0.0] + @formulae[ref] = ast + end + end + ast + end + # Returns a list of cells that are: # 1. Simple values (e.g., a string or a number) # 2. That are referenced in other formulae @@ -1337,14 +1362,14 @@ def cells_with_settable_values log.info "Generating a good set of cells that should be settable" counter = CountFormulaReferences.new - count = counter.count(@formulae) + countList = counter.count(@formulae) settable_cells = {} settable_types = [:blank,:number,:null,:string,:shared_string,:constant,:percentage,:error,:boolean_true,:boolean_false] - count.each do |ref,count| + countList.each do |ref,count| next unless count >= 1 # No point making a cell that isn't reference settable - ast = @formulae[ref] - next unless ast # Sometimes empty cells are referenced. + ast = make_blank_referenced_cells_settable(ref, @formulae[ref]) + next unless ast # Sometimes empty cells are referenced. next unless settable_types.include?(ast.first) settable_cells[ref.first] ||= [] settable_cells[ref.first] << ref.last.upcase diff --git a/src/compile/c/compile_to_c.rb b/src/compile/c/compile_to_c.rb index 3ee304c..05b1232 100644 --- a/src/compile/c/compile_to_c.rb +++ b/src/compile/c/compile_to_c.rb @@ -1,20 +1,30 @@ require_relative 'map_formulae_to_c' +require 'set' class CompileToC - + attr_accessor :settable attr_accessor :gettable attr_accessor :variable_set_counter + attr_accessor :variable_set_sheet_hash + attr_accessor :recursion_prevention_sheet_hash attr_accessor :allow_unknown_functions - + def self.rewrite(*args) self.new.rewrite(*args) end - + + def init_sheet_hash(sheet_names) + Hash[sheet_names.map {|sheet_name| [sheet_name, Set.new]}] + end + def rewrite(formulae, sheet_names, output) self.settable ||= lambda { |ref| false } self.gettable ||= lambda { |ref| true } @variable_set_counter ||= 0 + @recursion_prevention_counter ||= 0 + @variable_set_sheet_hash ||= init_sheet_hash(sheet_names.values.uniq) + @recursion_prevention_sheet_hash ||= init_sheet_hash(sheet_names.values.uniq) mapper = MapFormulaeToC.new mapper.allow_unknown_functions = self.allow_unknown_functions @@ -27,7 +37,7 @@ def rewrite(formulae, sheet_names, output) worksheet_c_name = mapper.sheet_names[worksheet.to_s] || worksheet.to_s calculation = mapper.map(ast) name = worksheet_c_name.length > 0 ? "#{worksheet_c_name}_#{cell.downcase}" : cell.downcase - + # Declare function as static so it can be inlined where possible static_or_not = gettable.call(ref) ? "" : "static " @@ -54,17 +64,27 @@ def rewrite(formulae, sheet_names, output) output.puts "#{static_or_not}ExcelValue #{name}() {" output.puts " static ExcelValue result;" output.puts " if(variable_set[#{@variable_set_counter}] == 1) { return result;}" + output.puts " if(recursion_prevention[#{@recursion_prevention_counter}] == 1) { return result;}" + output.puts " recursion_prevention[#{@recursion_prevention_counter}] = 1;" + mapper.initializers.each do |i| - output.puts " #{i}" + output.puts " #{i}".gsub("#{worksheet_c_name}_#{cell.downcase}()", "ZERO") end + output.puts " result = #{calculation};" output.puts " variable_set[#{@variable_set_counter}] = 1;" + output.puts " recursion_prevention[#{@recursion_prevention_counter}] = 0;" output.puts " return result;" output.puts "}" output.puts end end - @variable_set_counter += 1 + unless worksheet_c_name.empty? + @variable_set_sheet_hash[worksheet_c_name].add(@variable_set_counter) + @recursion_prevention_sheet_hash[worksheet_c_name].add(@variable_set_counter) + @variable_set_counter += 1 + @recursion_prevention_counter += 1 + end mapper.reset rescue Exception => e puts "Exception at #{ref} #{ast}" @@ -75,8 +95,24 @@ def rewrite(formulae, sheet_names, output) end end raise - end + end + end + end + + def reset_sheets(sheet_names, output) + sheet_names = sheet_names.values.uniq + sheet_names.each do |sheet_name| + output.puts "void reset_#{sheet_name}()\n{" + + @variable_set_sheet_hash[sheet_name].each do |variable| + output.puts " variable_set[#{variable}] = 0;" + end + + @recursion_prevention_sheet_hash[sheet_name].each do |variable| + output.puts " recursion_prevention[#{variable}] = 0;" + end + + output.puts "}\n" end end - end diff --git a/src/compile/c/excel_to_c_runtime.c b/src/compile/c/excel_to_c_runtime.c index e929da5..b4347e5 100644 --- a/src/compile/c/excel_to_c_runtime.c +++ b/src/compile/c/excel_to_c_runtime.c @@ -166,11 +166,13 @@ static void free_all_allocated_memory() { } static int variable_set[NUMBER_OF_REFS]; +static int recursion_prevention[NUMBER_OF_RECURSION_PREVENT_VARS]; // Resets all cached and malloc'd values void reset() { free_all_allocated_memory(); memset(variable_set, 0, sizeof(variable_set)); + memset(recursion_prevention, 0, sizeof(recursion_prevention)); } // Handy macros @@ -3017,11 +3019,18 @@ static ExcelValue hlookup(ExcelValue lookup_value_v,ExcelValue lookup_table_v, E for(i=0; i< columns; i++) { possible_match_v = array[i]; if(lookup_value_v.type != possible_match_v.type) continue; - if(more_than(possible_match_v,lookup_value_v).number == true) { - if(i == 0) return NA; - return array[((((int) row_number_v.number)-1)*columns)+(i-1)]; - } else { - last_good_match = i; + if(lookup_value_v.type == ExcelString && possible_match_v.type == ExcelString) + { + if (strstr(possible_match_v.string, lookup_value_v.string) != NULL) + last_good_match = i; + } else + { + if(more_than(possible_match_v,lookup_value_v).number == true) { + if(i == 0) return NA; + return array[((((int) row_number_v.number)-1)*columns)+(i-1)]; + } else { + last_good_match = i; + } } } return array[((((int) row_number_v.number)-1)*columns)+(last_good_match)]; diff --git a/src/rewrite/ast_copy_formula.rb b/src/rewrite/ast_copy_formula.rb index 5c47866..aa41073 100644 --- a/src/rewrite/ast_copy_formula.rb +++ b/src/rewrite/ast_copy_formula.rb @@ -4,10 +4,12 @@ class AstCopyFormula attr_accessor :rows_to_move attr_accessor :columns_to_move + attr_accessor :named_references - def initialize + def initialize(named_references = {}) self.rows_to_move = 0 self.columns_to_move = 0 + self.named_references = named_references end DO_NOT_MAP = {:number => true, :string => true, :blank => true, :null => true, :error => true, :boolean_true => true, :boolean_false => true, :operator => true, :comparator => true} @@ -26,6 +28,9 @@ def copy(ast) def cell(reference) r = Reference.for(reference) + if self.named_references.key?(reference) + return [:cell, reference] + end [:cell,r.offset(rows_to_move,columns_to_move)] end diff --git a/src/rewrite/rewrite_shared_formulae.rb b/src/rewrite/rewrite_shared_formulae.rb index 2978e15..7d4bb64 100644 --- a/src/rewrite/rewrite_shared_formulae.rb +++ b/src/rewrite/rewrite_shared_formulae.rb @@ -5,7 +5,7 @@ def self.rewrite(*args) new.rewrite(*args) end - def rewrite(formula_shared, formula_shared_targets) + def rewrite(formula_shared, formula_shared_targets, named_references = {}) @output = {} @formula_shared_targets = formula_shared_targets @@ -13,13 +13,13 @@ def rewrite(formula_shared, formula_shared_targets) copy_range = a[0] shared_formula_identifier = a[1] shared_ast = a[2] - share_formula(ref, shared_ast, copy_range, shared_formula_identifier) + share_formula(ref, shared_ast, copy_range, shared_formula_identifier, named_references) end @output end - def share_formula(ref, shared_ast, copy_range, shared_formula_identifier) - copier = AstCopyFormula.new + def share_formula(ref, shared_ast, copy_range, shared_formula_identifier, named_references) + copier = AstCopyFormula.new(named_references) copy_range = Area.for(copy_range) copy_range.calculate_excel_variables diff --git a/src/simplify/inline_formulae.rb b/src/simplify/inline_formulae.rb index 4ed32c5..bab139d 100644 --- a/src/simplify/inline_formulae.rb +++ b/src/simplify/inline_formulae.rb @@ -16,11 +16,11 @@ def replace(new_array) class InlineFormulaeAst - attr_accessor :references, :current_sheet_name, :inline_ast + attr_accessor :references, :current_sheet_name, :inline_ast, :named_references attr_accessor :count_replaced - def initialize(references = nil, current_sheet_name = nil, inline_ast = nil) - @references, @current_sheet_name, @inline_ast = references, [current_sheet_name], inline_ast + def initialize(references = nil, current_sheet_name = nil, inline_ast = nil, named_references = {}) + @references, @current_sheet_name, @inline_ast, @named_references = references, [current_sheet_name], inline_ast, named_references @count_replaced = 0 @inline_ast ||= lambda { |sheet, ref, references| true } # Default is to always inline end @@ -66,9 +66,17 @@ def sheet_reference(ast) return unless ast[2][0] == :cell sheet = ast[1].to_sym ref = ast[2][1].to_s.upcase.gsub('$','').to_sym - # FIXME: Need to check if valid worksheet and return [:error, "#REF!"] if not - # Now check user preference on this - return unless inline_ast.call(sheet,ref, references) + ref_org = ast[2][1].to_s.gsub('$','').to_sym + + if named_references[ref_org] # Fix issue for named_reference that is mapped to blank + sheet = named_references[ref_org][1] + ref = named_references[ref_org][2][1] + else + # FIXME: Need to check if valid worksheet and return [:error, "#REF!"] if not + # Now check user preference on this + return unless inline_ast.call(sheet,ref, references) + end + ast_to_inline = ast_or_blank(sheet, ref) @count_replaced += 1 current_sheet_name.push(sheet) @@ -95,6 +103,11 @@ def cell(ast) def ast_or_blank(sheet, ref) ast_to_inline = references[[sheet, ref]] return ast_to_inline if ast_to_inline + + if named_references.key?(ref.downcase) + return named_references[ref.downcase] + end + # Need to add a new blank cell and return ast for an inlined blank references[[sheet, ref]] = [:blank] [:inlined_blank] diff --git a/src/simplify/replace_references_to_blanks_with_zeros.rb b/src/simplify/replace_references_to_blanks_with_zeros.rb index b06d007..ad50ff5 100644 --- a/src/simplify/replace_references_to_blanks_with_zeros.rb +++ b/src/simplify/replace_references_to_blanks_with_zeros.rb @@ -1,10 +1,10 @@ class ReplaceReferencesToBlanksWithZeros - attr_accessor :references, :current_sheet_name, :inline_ast + attr_accessor :references, :current_sheet_name, :inline_ast, :named_references attr_accessor :count_replaced - def initialize(references = nil, current_sheet_name = nil, inline_ast = nil) - @references, @current_sheet_name, @inline_ast = references, [current_sheet_name], inline_ast + def initialize(references = nil, current_sheet_name = nil, inline_ast = nil, named_references = {}) + @references, @current_sheet_name, @inline_ast, @named_references = references, [current_sheet_name], inline_ast, named_references @count_replaced = 0 @inline_ast ||= lambda { |sheet, ref, references| true } # Default is to always inline end @@ -18,8 +18,13 @@ def map(ast) case ast.first when :sheet_reference return ast unless ast[2][0] == :cell - sheet = ast[1].to_sym - ref = ast[2][1].to_s.upcase.gsub('$','').to_sym + if @named_references.key?(ast[2][1]) + sheet = named_references[ast[2][1]][1] + ref = named_references[ast[2][1]][2].last + else + sheet = ast[1].to_sym + ref = ast[2][1].to_s.upcase.gsub('$','').to_sym + end when :cell sheet = current_sheet_name.last ref = ast[1].to_s.upcase.gsub('$', '').to_sym