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
@@ -0,0 +1,157 @@
1
+ require_relative '../compile'
2
+ require_relative '../excel/excel_functions'
3
+ require_relative '../util'
4
+
5
+ class FormulaeCalculator
6
+ include ExcelFunctions
7
+ end
8
+
9
+ class MapFormulaeToValues
10
+
11
+ def initialize
12
+ @value_for_ast = MapValuesToRuby.new
13
+ @calculator = FormulaeCalculator.new
14
+ end
15
+
16
+ def map(ast)
17
+ return ast unless ast.is_a?(Array)
18
+ operator = ast[0]
19
+ if respond_to?(operator)
20
+ send(operator,*ast[1..-1])
21
+ else
22
+ [operator,*ast[1..-1].map {|a| map(a) }]
23
+ end
24
+ end
25
+
26
+ def prefix(operator,argument)
27
+ argument_value = value(map(argument))
28
+ return [:prefix, operator, map(argument)] if argument_value == :not_a_value
29
+ return ast_for_value(argument_value || 0) if operator == "+"
30
+ ast_for_value(@calculator.negative(argument_value))
31
+ end
32
+
33
+ def arithmetic(left,operator,right)
34
+ l = value(map(left))
35
+ r = value(map(right))
36
+ if (l != :not_a_value) && (r != :not_a_value)
37
+ formula_value(operator.last,l,r)
38
+ else
39
+ [:arithmetic,map(left),operator,map(right)]
40
+ end
41
+ end
42
+
43
+ alias :comparison :arithmetic
44
+
45
+ def percentage(number)
46
+ ast_for_value(value([:percentage, number]))
47
+ end
48
+
49
+ def string_join(*args)
50
+ values = args.map { |a| value(map(a)) } # FIXME: These eval statements are really bugging me. Must find a better solution
51
+ if values.any? { |a| a == :not_a_value }
52
+ [:string_join,*args.map { |a| map(a) }]
53
+ else
54
+ ast_for_value(@calculator.string_join(*values))
55
+ end
56
+ end
57
+
58
+ FUNCTIONS_THAT_SHOULD_NOT_BE_CONVERTED = %w{TODAY RAND RANDBETWEEN INDIRECT}
59
+
60
+ def function(name,*args)
61
+ if FUNCTIONS_THAT_SHOULD_NOT_BE_CONVERTED.include?(name)
62
+ [:function,name,*args.map { |a| map(a) }]
63
+ elsif respond_to?("map_#{name.downcase}")
64
+ send("map_#{name.downcase}",*args)
65
+ else
66
+ values = args.map { |a| value(map(a)) }
67
+ if values.any? { |a| a == :not_a_value }
68
+ [:function,name,*args.map { |a| map(a) }]
69
+ else
70
+ formula_value(name,*values)
71
+ end
72
+ end
73
+ end
74
+
75
+ def map_index(array,row_number,column_number = :not_specified)
76
+ return map_index_with_only_two_arguments(array,row_number) if column_number == :not_specified
77
+
78
+ array_mapped = map(array)
79
+ row_as_number = value(map(row_number))
80
+ column_as_number = value(map(column_number))
81
+
82
+ return [:function, "INDEX", array_mapped, map(row_number), map(column_number)] if row_as_number == :not_a_value || column_as_number == :not_a_value
83
+
84
+ array_as_values = array_as_values(array)
85
+ return [:function, "INDEX", array_mapped, map(row_number), map(column_number)] unless array_as_values
86
+
87
+ result = @calculator.send(MapFormulaeToRuby::FUNCTIONS["INDEX"],array_as_values,row_as_number,column_as_number)
88
+ result = ast_for_value(result) unless result.is_a?(Array)
89
+ result
90
+ end
91
+
92
+ def map_index_with_only_two_arguments(array,row_number)
93
+ array_mapped = map(array)
94
+ row_as_number = value(map(row_number))
95
+ return [:function, "INDEX", array_mapped, map(row_number)] if row_as_number == :not_a_value
96
+ array_as_values = array_as_values(array)
97
+ return [:function, "INDEX", array_mapped, map(row_number)] unless array_as_values
98
+ result = @calculator.send(MapFormulaeToRuby::FUNCTIONS["INDEX"],array_as_values,row_as_number)
99
+ result = ast_for_value(result) unless result.is_a?(Array)
100
+ result
101
+ end
102
+
103
+ def array_as_values(array_mapped)
104
+ case array_mapped.first
105
+ when :array
106
+ array_mapped[1..-1].map do |row|
107
+ row[1..-1].map do |cell|
108
+ cell
109
+ end
110
+ end
111
+ when :cell, :sheet_reference, :blank, :number, :percentage, :string, :error, :boolean_true, :boolean_false
112
+ [[array_mapped]]
113
+ else
114
+ nil
115
+ end
116
+ end
117
+
118
+
119
+
120
+ def value(ast)
121
+ return extract_values_from_array(ast) if ast.first == :array
122
+ return :not_a_value unless @value_for_ast.respond_to?(ast.first)
123
+ eval(@value_for_ast.send(*ast))
124
+ end
125
+
126
+ def extract_values_from_array(ast)
127
+ ast[1..-1].map do |row|
128
+ row[1..-1].map do |cell|
129
+ v = value(cell)
130
+ return :not_a_value if v == :not_a_value
131
+ v
132
+ end
133
+ end
134
+ end
135
+
136
+ def formula_value(ast_name,*arguments)
137
+ raise NotSupportedException.new("#{ast_name.inspect} function not recognised in #{MapFormulaeToRuby::FUNCTIONS.inspect}") unless MapFormulaeToRuby::FUNCTIONS.has_key?(ast_name)
138
+ ast_for_value(@calculator.send(MapFormulaeToRuby::FUNCTIONS[ast_name],*arguments))
139
+ end
140
+
141
+ def ast_for_value(value)
142
+ case value
143
+ when Numeric; [:number,value.inspect]
144
+ when true; [:boolean_true]
145
+ when false; [:boolean_false]
146
+ when Symbol;
147
+ raise NotSupportedException.new("Error #{value.inspect} not recognised") unless MapFormulaeToRuby::REVERSE_ERRORS[value.inspect]
148
+ [:error,MapFormulaeToRuby::REVERSE_ERRORS[value.inspect]]
149
+ when String; [:string,value]
150
+ when Array; [:array,*value.map { |row| [:row, *row.map { |c| ast_for_value(c) }]}]
151
+ when nil; [:blank]
152
+ else
153
+ raise NotSupportedException.new("Ast for #{value.inspect} of class #{value.class} not recognised")
154
+ end
155
+ end
156
+
157
+ end
@@ -0,0 +1,18 @@
1
+
2
+ class RemoveCells
3
+
4
+ attr_accessor :cells_to_keep
5
+
6
+ def self.rewrite(*args)
7
+ self.new.rewrite(*args)
8
+ end
9
+
10
+ def rewrite(input,output)
11
+ input.lines do |line|
12
+ ref = line[/^(.*?)\t/,1]
13
+ if cells_to_keep.has_key?(ref)
14
+ output.puts line
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ require_relative '../excel'
2
+
3
+ class ReplaceArraysWithSingleCells
4
+
5
+ def self.replace(*args)
6
+ self.new.replace(*args)
7
+ end
8
+
9
+ def replace(input,output)
10
+
11
+ input.lines do |line|
12
+ # Looks to match shared string lines
13
+ if line =~ /\[:array/
14
+ content = line.split("\t")
15
+ ast = eval(content.pop)
16
+ if ast.first == :array
17
+ output.puts "#{content.join("\t")}\t#{ast[1][1].inspect}"
18
+ else
19
+ output.puts line
20
+ end
21
+ else
22
+ output.puts line
23
+ end
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,58 @@
1
+ class ReplaceBlanksAst
2
+
3
+ attr_accessor :references, :default_sheet_name
4
+
5
+ def initialize(references, default_sheet_name)
6
+ @references, @default_sheet_name = references, default_sheet_name
7
+ end
8
+
9
+ def map(ast)
10
+ return ast unless ast.is_a?(Array)
11
+ operator = ast[0]
12
+ if respond_to?(operator)
13
+ send(operator,*ast[1..-1])
14
+ else
15
+ [operator,*ast[1..-1].map {|a| map(a) }]
16
+ end
17
+ end
18
+
19
+ def sheet_reference(sheet,reference)
20
+ if references[sheet].has_key?(reference.last.gsub('$',''))
21
+ [:sheet_reference,sheet,reference]
22
+ else
23
+ [:blank]
24
+ end
25
+ end
26
+
27
+ def cell(reference)
28
+ if references[default_sheet_name].has_key?(reference.gsub('$',''))
29
+ [:cell,reference]
30
+ else
31
+ [:blank]
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+
38
+ class ReplaceBlanks
39
+
40
+ attr_accessor :references, :default_sheet_name
41
+
42
+ def self.replace(*args)
43
+ self.new.replace(*args)
44
+ end
45
+
46
+ def replace(input,output)
47
+ rewriter = ReplaceBlanksAst.new(references,default_sheet_name)
48
+ input.lines do |line|
49
+ # Looks to match lines with references
50
+ if line =~ /\[:cell/
51
+ ref, ast = line.split("\t")
52
+ output.puts "#{ref}\t#{rewriter.map(eval(ast)).inspect}"
53
+ else
54
+ output.puts line
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,19 @@
1
+ class ReplaceCommonElementsInFormulae
2
+
3
+ def self.replace(*args)
4
+ self.new.replace(*args)
5
+ end
6
+
7
+ def replace(input,common,output)
8
+ common = common.readlines.map do |a|
9
+ ref, element = a.split("\t")
10
+ [element.strip,"[:cell, \"#{ref}\"]",ref]
11
+ end.sort
12
+ input.lines do |line|
13
+ common.each do |element,cell,ref|
14
+ line.gsub!(element,cell)
15
+ end
16
+ output.puts line
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'map_formulae_to_values'
2
+
3
+ class ReplaceFormulaeWithCalculatedValues
4
+
5
+ def self.replace(*args)
6
+ self.new.replace(*args)
7
+ end
8
+
9
+ def replace(input,output)
10
+ rewriter = MapFormulaeToValues.new
11
+ input.lines do |line|
12
+ begin
13
+ ref, ast = line.split("\t")
14
+ output.puts "#{ref}\t#{rewriter.map(eval(ast)).inspect}"
15
+ rescue Exception => e
16
+ puts "Exception at line #{line}"
17
+ raise
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../excel/formula_peg'
2
+
3
+ class ReplaceIndirectsWithReferencesAst
4
+
5
+ def map(ast)
6
+ return ast unless ast.is_a?(Array)
7
+ operator = ast[0]
8
+ if respond_to?(operator)
9
+ send(operator,*ast[1..-1])
10
+ else
11
+ [operator,*ast[1..-1].map {|a| map(a) }]
12
+ end
13
+ end
14
+
15
+ def function(name,*args)
16
+ if name == "INDIRECT" && args.size == 1 && args[0][0] == :string
17
+ Formula.parse(args[0][1]).to_ast[1]
18
+ else
19
+ puts "indirect #{[:function,name,*args.map { |a| map(a) }].inspect} not replaced" if name == "INDIRECT"
20
+ [:function,name,*args.map { |a| map(a) }]
21
+ end
22
+ end
23
+ end
24
+
25
+
26
+ class ReplaceIndirectsWithReferences
27
+
28
+ def self.replace(*args)
29
+ self.new.replace(*args)
30
+ end
31
+
32
+ def replace(input,output)
33
+ rewriter = ReplaceIndirectsWithReferencesAst.new
34
+ input.lines do |line|
35
+ # Looks to match lines with references
36
+ if line =~ /"INDIRECT"/
37
+ ref, ast = line.split("\t")
38
+ output.puts "#{ref}\t#{rewriter.map(eval(ast)).inspect}"
39
+ else
40
+ output.puts line
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,82 @@
1
+ class NamedReferences
2
+
3
+ attr_accessor :named_references
4
+
5
+ def initialize(refs)
6
+ @named_references = {}
7
+ refs.each do |line|
8
+ sheet, name, reference = line.split("\t")
9
+ @named_references[sheet.downcase] ||= {}
10
+ @named_references[sheet.downcase][name.downcase] = eval(reference)
11
+ end
12
+ end
13
+
14
+ def reference_for(sheet,named_reference)
15
+ sheet = sheet.downcase
16
+ named_reference = named_reference.downcase
17
+ if @named_references.has_key?(sheet)
18
+ @named_references[sheet][named_reference] || @named_references[""][named_reference] || [:error, "#NAME?"]
19
+ else
20
+ @named_references[""][named_reference] || [:error, "#NAME?"]
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ class ReplaceNamedReferencesAst
27
+
28
+ attr_accessor :named_references, :default_sheet_name
29
+
30
+ def initialize(named_references, default_sheet_name)
31
+ @named_references, @default_sheet_name = named_references, default_sheet_name
32
+ end
33
+
34
+ def map(ast)
35
+ return ast unless ast.is_a?(Array)
36
+ operator = ast[0]
37
+ if respond_to?(operator)
38
+ send(operator,*ast[1..-1])
39
+ else
40
+ [operator,*ast[1..-1].map {|a| map(a) }]
41
+ end
42
+ end
43
+
44
+ def sheet_reference(sheet,reference)
45
+ if reference.first == :named_reference
46
+ named_references.reference_for(sheet,reference.last)
47
+ else
48
+ [:sheet_reference,sheet,reference]
49
+ end
50
+ end
51
+
52
+ def named_reference(name)
53
+ named_references.reference_for(default_sheet_name,name)
54
+ end
55
+
56
+ end
57
+
58
+
59
+ class ReplaceNamedReferences
60
+
61
+ attr_accessor :sheet_name
62
+
63
+ def self.replace(values,sheet_name,named_references,output)
64
+ self.new.replace(values,sheet_name,named_references,output)
65
+ end
66
+
67
+ # Rewrites ast with named references
68
+ def replace(values,named_references,output)
69
+ named_references = NamedReferences.new(named_references.readlines)
70
+ rewriter = ReplaceNamedReferencesAst.new(named_references,sheet_name)
71
+ values.lines do |line|
72
+ # Looks to match shared string lines
73
+ if line =~ /\[:named_reference/
74
+ cols = line.split("\t")
75
+ ast = cols.pop
76
+ output.puts "#{cols.join("\t")}\t#{rewriter.map(eval(ast)).inspect}"
77
+ else
78
+ output.puts line
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ require_relative '../excel'
2
+
3
+ class ReplaceRangesWithArrayLiteralsAst
4
+ def map(ast)
5
+ if ast.is_a?(Array)
6
+ operator = ast.shift
7
+ if respond_to?(operator)
8
+ send(operator,*ast)
9
+ else
10
+ [operator,*ast.map {|a| map(a) }]
11
+ end
12
+ else
13
+ return ast
14
+ end
15
+ end
16
+
17
+ def sheet_reference(sheet,reference)
18
+ if reference.first == :area
19
+ area = Area.for("#{reference[1]}:#{reference[2]}")
20
+ area.to_array_literal(sheet)
21
+ else
22
+ [:sheet_reference,sheet,reference]
23
+ end
24
+ end
25
+
26
+ def area(start,finish)
27
+ area = Area.for("#{start}:#{finish}")
28
+ area.to_array_literal
29
+ end
30
+
31
+ end
32
+
33
+ class ReplaceRangesWithArrayLiterals
34
+
35
+ def self.replace(*args)
36
+ self.new.replace(*args)
37
+ end
38
+
39
+ def replace(input,output)
40
+ rewriter = ReplaceRangesWithArrayLiteralsAst.new
41
+
42
+ input.lines do |line|
43
+ # Looks to match shared string lines
44
+ if line =~ /\[:area/
45
+ content = line.split("\t")
46
+ ast = eval(content.pop)
47
+ output.puts "#{content.join("\t")}\t#{rewriter.map(ast).inspect}"
48
+ else
49
+ output.puts line
50
+ end
51
+ end
52
+ end
53
+
54
+ end