rexcel 0.1.0
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.
- data/benchmark/rexcel_benchmark.rb +70 -0
- data/examples/example.rb +29 -0
- data/examples/example_color.rb +43 -0
- data/examples/example_format.rb +23 -0
- data/lib/rexcel.rb +166 -0
- data/lib/rexcel/cell.rb +130 -0
- data/lib/rexcel/row.rb +73 -0
- data/lib/rexcel/style.rb +224 -0
- data/lib/rexcel/workbook.rb +229 -0
- data/lib/rexcel/worksheet.rb +92 -0
- data/unittest/expected/Test_Style_xlm-test_backgroundcolor-gray.xml +34 -0
- data/unittest/expected/Test_Style_xlm-test_bold-bold.xml +34 -0
- data/unittest/expected/Test_Style_xlm-test_color-gray.xml +34 -0
- data/unittest/expected/Test_Style_xlm-test_italic-italic.xml +34 -0
- data/unittest/expected/Test_workbook-test_save_xml.xml +29 -0
- data/unittest/test_rexcel.rb +725 -0
- metadata +139 -0
data/lib/rexcel/row.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Excel
|
2
|
+
=begin rdoc
|
3
|
+
A Row in the spreadsheet.
|
4
|
+
=end
|
5
|
+
class Row
|
6
|
+
=begin rdoc
|
7
|
+
Define a new row.
|
8
|
+
=end
|
9
|
+
def initialize( options = {})
|
10
|
+
@log = options[:log] || LOGGER
|
11
|
+
@columns = []
|
12
|
+
options.each{|key,value|
|
13
|
+
case key
|
14
|
+
when :log
|
15
|
+
when :style
|
16
|
+
@style = value
|
17
|
+
raise ArgumentError, "Style is no Excel::Style" unless @style.is_a?(Style)
|
18
|
+
else
|
19
|
+
@log.warn("Excel::Row: undefined option #{option}")
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
#Array with columns of this row
|
24
|
+
attr_reader :columns
|
25
|
+
#Style for the row. Is inherited to cells.
|
26
|
+
attr_reader :style
|
27
|
+
=begin rdoc
|
28
|
+
Add content to the Row.
|
29
|
+
=end
|
30
|
+
def << (insertion)
|
31
|
+
case insertion
|
32
|
+
when Cell
|
33
|
+
@columns << insertion
|
34
|
+
when Array
|
35
|
+
insertion.each{|value|
|
36
|
+
@columns << Cell.new(value)
|
37
|
+
}
|
38
|
+
when Hash
|
39
|
+
@log.error("Excel::Row: Hashs not supported")
|
40
|
+
raise ArgumentError, "Excel::Row#<<: Hashs not supported"
|
41
|
+
#fixme: if connectuion to worksheet, use columns.
|
42
|
+
when Row, Worksheet, Workbook
|
43
|
+
raise ArgumentError, "Excel::Row#<<: #{insertion.class} not supported"
|
44
|
+
else
|
45
|
+
@columns << Cell.new(insertion)
|
46
|
+
end
|
47
|
+
self #for usage like " << (Excel::Row.new() << 'a')"
|
48
|
+
end
|
49
|
+
=begin rdoc
|
50
|
+
Build the xml a work sheet row,
|
51
|
+
|
52
|
+
ns must be a method-object to implement the namespace definitions.
|
53
|
+
|
54
|
+
Format options (bold, italic, colors) are forwarded to cells.
|
55
|
+
=end
|
56
|
+
def to_xml(xmlbuilder, ns)
|
57
|
+
raise EmptyError, "Row without content" if @columns.empty?
|
58
|
+
|
59
|
+
#Build options
|
60
|
+
row_options = {}
|
61
|
+
if @style
|
62
|
+
row_options[ns.call('StyleID')] = @style.style_id
|
63
|
+
end
|
64
|
+
|
65
|
+
xmlbuilder[ns.call].Row( row_options ){
|
66
|
+
@columns.each{|column|
|
67
|
+
column.to_xml(xmlbuilder, ns, self)
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end #to_xml
|
71
|
+
|
72
|
+
end #class Row
|
73
|
+
end #module Excel
|
data/lib/rexcel/style.rb
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
module Excel
|
2
|
+
=begin rdoc
|
3
|
+
Style definition for Excel.
|
4
|
+
The styles can be assigned to the Cell and Row.
|
5
|
+
|
6
|
+
You can define:
|
7
|
+
* Font characteristic: bold/italic
|
8
|
+
* Color and background color (Based on ColorIndex)
|
9
|
+
|
10
|
+
You can (yet) not define:
|
11
|
+
* Font type
|
12
|
+
|
13
|
+
=end
|
14
|
+
class Style
|
15
|
+
#
|
16
|
+
#Some colors.
|
17
|
+
#
|
18
|
+
#Colors are adminstrated by a ColorIndex.
|
19
|
+
#This Hash is a reconversion.
|
20
|
+
#
|
21
|
+
#See also http://www.mvps.org/dmcritchie/excel/colors.htm
|
22
|
+
ColorIndex = {
|
23
|
+
1=> '#000000',
|
24
|
+
2=> '#FFFFFF',
|
25
|
+
3=> '#FF0000',
|
26
|
+
4=> '#00FF00',
|
27
|
+
5=> '#0000FF',
|
28
|
+
6=> '#FFFF00',
|
29
|
+
7=> '#FF00FF',
|
30
|
+
8=> '#00FFFF',
|
31
|
+
9=> '#800000',
|
32
|
+
10=> '#008000',
|
33
|
+
11=> '#000080',
|
34
|
+
12=> '#808000',
|
35
|
+
13=> '#800080',
|
36
|
+
14=> '#008080',
|
37
|
+
15=> '#C0C0C0', #gray
|
38
|
+
16=> '#808080',
|
39
|
+
17=> '#9999FF',
|
40
|
+
18=> '#993366',
|
41
|
+
19=> '#FFFFCC',
|
42
|
+
20=> '#CCFFFF',
|
43
|
+
21=> '#660066',
|
44
|
+
22=> '#FF8080',
|
45
|
+
23=> '#0066CC',
|
46
|
+
24=> '#CCCCFF',
|
47
|
+
25=> '#000080',
|
48
|
+
26=> '#FF00FF',
|
49
|
+
27=> '#FFFF00',
|
50
|
+
28=> '#00FFFF',
|
51
|
+
29=> '#800080',
|
52
|
+
30=> '#800000',
|
53
|
+
31=> '#008080',
|
54
|
+
32=> '#0000FF',
|
55
|
+
33=> '#00CCFF',
|
56
|
+
34=> '#CCFFFF',
|
57
|
+
35=> '#CCFFCC',
|
58
|
+
36=> '#FFFF99',
|
59
|
+
37=> '#99CCFF',
|
60
|
+
38=> '#FF99CC',
|
61
|
+
39=> '#CC99FF',
|
62
|
+
40=> '#FFCC99',
|
63
|
+
41=> '#3366FF',
|
64
|
+
42=> '#33CCCC',
|
65
|
+
43=> '#99CC00',
|
66
|
+
44=> '#FFCC00',
|
67
|
+
45=> '#FF9900',
|
68
|
+
46=> '#FF6600',
|
69
|
+
47=> '#666699',
|
70
|
+
48=> '#969696',
|
71
|
+
49=> '#003366',
|
72
|
+
50=> '#339966',
|
73
|
+
51=> '#003300',
|
74
|
+
52=> '#333300',
|
75
|
+
53=> '#993300',
|
76
|
+
54=> '#993366',
|
77
|
+
55=> '#333399',
|
78
|
+
56=> '#333333',
|
79
|
+
}
|
80
|
+
=begin rdoc
|
81
|
+
Create a new style.
|
82
|
+
|
83
|
+
Options:
|
84
|
+
* bold
|
85
|
+
* italic
|
86
|
+
* color (not supported yet)
|
87
|
+
* backgroundcolor (not supported yet)
|
88
|
+
|
89
|
+
Not implemented (yet)
|
90
|
+
* Fontsize
|
91
|
+
* Font
|
92
|
+
=end
|
93
|
+
def initialize(name, options = {})
|
94
|
+
@name = name
|
95
|
+
@log = options[:log] || LOGGER
|
96
|
+
@log.debug( "Create Style #{name}")
|
97
|
+
|
98
|
+
options.each{|key,value|
|
99
|
+
case key
|
100
|
+
when :log
|
101
|
+
when :bold
|
102
|
+
self.bold = value
|
103
|
+
@log.debug( "Style #{name}: Set #{key}")
|
104
|
+
when :italic
|
105
|
+
self.italic = value
|
106
|
+
@log.debug( "Style #{name}: Set #{key}")
|
107
|
+
when :color, :colour
|
108
|
+
if ColorIndex[value]
|
109
|
+
self.color = value
|
110
|
+
@log.debug( "Style #{name}: Set color #{key}")
|
111
|
+
else
|
112
|
+
@log.error( "Style #{name}: Usage of undefined color #{value}")
|
113
|
+
end
|
114
|
+
when :backgroundcolor, :backgroundcolour
|
115
|
+
if ColorIndex[value]
|
116
|
+
self.backgroundcolor = value
|
117
|
+
@log.debug( "Style #{name}: Set backgroundcolor #{key}")
|
118
|
+
else
|
119
|
+
@log.error( "Style #{name}: Usage of undefined color #{value}")
|
120
|
+
end
|
121
|
+
else
|
122
|
+
@log.warn("Excel::Style: undefined option #{option}")
|
123
|
+
end
|
124
|
+
}
|
125
|
+
|
126
|
+
@style_id = nil #Set by Workbook
|
127
|
+
end
|
128
|
+
#Name of the Sytle. for usage inside ruby
|
129
|
+
attr_reader :name
|
130
|
+
|
131
|
+
=begin rdoc
|
132
|
+
Define Style ID. For usage in XML/XLS.
|
133
|
+
|
134
|
+
(Set by Workbook#<<)
|
135
|
+
=end
|
136
|
+
def style_id=(id)
|
137
|
+
raise ArgumentError, "Second id for style #{@name}" if @style_id
|
138
|
+
@log.debug( "Set Style-id #{id} for #{name}")
|
139
|
+
|
140
|
+
@style_id = id
|
141
|
+
end
|
142
|
+
#Style ID. For usage in XML/XLS.
|
143
|
+
attr_reader :style_id
|
144
|
+
|
145
|
+
#Bold
|
146
|
+
attr_accessor :bold
|
147
|
+
#Italic
|
148
|
+
attr_accessor :italic
|
149
|
+
#Color. Must be defined in ColorIndex
|
150
|
+
attr_accessor :color
|
151
|
+
#Background Color. Must be defined in ColorIndex
|
152
|
+
attr_accessor :backgroundcolor
|
153
|
+
=begin rdoc
|
154
|
+
Build the xml a work sheet style definition,
|
155
|
+
|
156
|
+
ns must be a method-object to implement the namespace definitions.
|
157
|
+
=end
|
158
|
+
def to_xml(xmlbuilder, ns)
|
159
|
+
xmlbuilder[ns.call].Style( ns.call(:ID) => @style_id ){
|
160
|
+
fontoption = {}
|
161
|
+
fontoption[ns.call(:Bold)] = "1" if bold
|
162
|
+
fontoption[ns.call(:Italic)] = "1" if italic
|
163
|
+
if color #Set font color
|
164
|
+
if ColorIndex[color]
|
165
|
+
fontoption[ns.call(:Color)] = ColorIndex[color]
|
166
|
+
else
|
167
|
+
@log.error("Color #{color.inspect} not defined in ColorIndex")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
xmlbuilder[ns.call].Font(fontoption) unless fontoption.empty?
|
171
|
+
|
172
|
+
if backgroundcolor
|
173
|
+
if ColorIndex[backgroundcolor]
|
174
|
+
xmlbuilder[ns.call].Interior(
|
175
|
+
ns.call(:Color) => ColorIndex[backgroundcolor],
|
176
|
+
ns.call(:Pattern) => "Solid"
|
177
|
+
)
|
178
|
+
else
|
179
|
+
@log.error("ColorIndex #{backgroundcolor.inspect} not defined")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
}
|
183
|
+
end #to_xml
|
184
|
+
=begin rdoc
|
185
|
+
Define Excel style.
|
186
|
+
|
187
|
+
Receives a OLE-work book
|
188
|
+
|
189
|
+
How to do it??
|
190
|
+
|
191
|
+
Unless this function is working, use #to_xls_direct
|
192
|
+
=end
|
193
|
+
def to_xls(wb)
|
194
|
+
#~ @log.fatal("#{self.class}##{__method__} not supported.") unless @@xls_warning_made
|
195
|
+
@@xls_warning_made = true #only once
|
196
|
+
@log.info("Cells include format options directly")
|
197
|
+
end #to_xls
|
198
|
+
@@xls_warning_made = false
|
199
|
+
=begin rdoc
|
200
|
+
Attach style information directly to the OLE-Object (row/cell)
|
201
|
+
|
202
|
+
Receives a OLE-object
|
203
|
+
|
204
|
+
This method is only a interim solution until I found the way to
|
205
|
+
use styles with ole. (#to_xls).
|
206
|
+
=end
|
207
|
+
def to_xls_direct(cell)
|
208
|
+
|
209
|
+
#if the cell has bold=false, the row-setting is overwritten
|
210
|
+
if bold
|
211
|
+
cell.Font.Bold = true
|
212
|
+
end
|
213
|
+
if italic
|
214
|
+
cell.Font.Italic = true
|
215
|
+
end
|
216
|
+
if color
|
217
|
+
cell.Font.ColorIndex = color
|
218
|
+
end
|
219
|
+
if backgroundcolor
|
220
|
+
cell.Interior.ColorIndex = backgroundcolor
|
221
|
+
end
|
222
|
+
end #to_xls
|
223
|
+
end #class Stylwe
|
224
|
+
end #module Excel
|
@@ -0,0 +1,229 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
Workbook definitions.
|
3
|
+
=end
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
module Excel
|
7
|
+
=begin rdoc
|
8
|
+
This Workbook will become an Excel-File.
|
9
|
+
|
10
|
+
Workbooks contains one or more Worksheet.
|
11
|
+
=end
|
12
|
+
class Workbook
|
13
|
+
=begin rdoc
|
14
|
+
Create a workbook.
|
15
|
+
|
16
|
+
Container for
|
17
|
+
* Style #styles
|
18
|
+
* Worksheets
|
19
|
+
=end
|
20
|
+
def initialize( logger = nil)
|
21
|
+
|
22
|
+
@log = logger || LOGGER
|
23
|
+
raise ArgumentError, "Workbook: No logger" unless @log.is_a?(Log4r::Logger)
|
24
|
+
@log.info("Create Workbook")
|
25
|
+
|
26
|
+
@styles = {}
|
27
|
+
@worksheets = []
|
28
|
+
@active_worksheet = nil
|
29
|
+
#~ @header = []
|
30
|
+
#~ @content = []
|
31
|
+
end
|
32
|
+
#Worksheets
|
33
|
+
attr_reader :worksheets
|
34
|
+
#Predefined styles.
|
35
|
+
attr_reader :styles
|
36
|
+
#Active worksheet
|
37
|
+
attr_reader :active_worksheet
|
38
|
+
=begin rdoc
|
39
|
+
Add content to the workbook.
|
40
|
+
* Excel#Worksheet
|
41
|
+
* Array: Added to the actual worksheet.
|
42
|
+
* Hash: Added to the actual worksheet.
|
43
|
+
* Row: Added the actual worksheet.
|
44
|
+
|
45
|
+
If no actual worksheet is available, a new worksheet i created.
|
46
|
+
=end
|
47
|
+
def << (insertion)
|
48
|
+
case insertion
|
49
|
+
when Style #Excel::Style
|
50
|
+
if @styles[insertion.name]
|
51
|
+
@log.warn("Duplicate insertion #{@name} for Style")
|
52
|
+
else
|
53
|
+
@styles[insertion.name] = insertion
|
54
|
+
insertion.style_id = @styles.size
|
55
|
+
end
|
56
|
+
when Worksheet
|
57
|
+
@worksheets << insertion
|
58
|
+
@active_worksheet = insertion
|
59
|
+
when Array, Hash, Row
|
60
|
+
self << Worksheet.new("Sheet#{@worksheets.size}", :log => @log) unless @active_worksheet
|
61
|
+
@active_worksheet << insertion
|
62
|
+
else
|
63
|
+
raise ArgumentError, "#{Workbook}: Wrong insertion type #{insertion.inspect}"
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
=begin rdoc
|
69
|
+
Build the workbook via OLE.
|
70
|
+
|
71
|
+
If a xml-source is available, it is used to build the Excel Workbook.
|
72
|
+
|
73
|
+
If no xml-source is available, the internal data are taken to
|
74
|
+
build the xls.
|
75
|
+
This may take some time...
|
76
|
+
|
77
|
+
For big files, it is recommended to use the way via xml.
|
78
|
+
|
79
|
+
=end
|
80
|
+
def prepare_xls(xml_source = nil)
|
81
|
+
|
82
|
+
wb = nil
|
83
|
+
if xml_source
|
84
|
+
if File.exist?(xml_source)
|
85
|
+
@log.info("Use existing #{xml_source}")
|
86
|
+
wb = Excel.instance.xl.Workbooks.OpenXML(File.expand_path(xml_source))
|
87
|
+
else
|
88
|
+
@log.fatal("#{__method__} #{xml_source} missing")
|
89
|
+
raise ArgumentError, 'Source data missing'
|
90
|
+
end
|
91
|
+
return wb
|
92
|
+
end
|
93
|
+
|
94
|
+
@log.info("Create xls-Workbook")
|
95
|
+
wb = Excel.instance.xl.Workbooks.Add #Includes 3 worksheets
|
96
|
+
#Delete unused sheets.
|
97
|
+
wb.ActiveSheet.delete
|
98
|
+
wb.ActiveSheet.delete
|
99
|
+
|
100
|
+
@log.info("Add styles to document") unless @styles.empty?
|
101
|
+
@styles.each{|name, style|
|
102
|
+
@log.debug("Add style #{name} to document")
|
103
|
+
style.to_xls(wb)
|
104
|
+
}
|
105
|
+
|
106
|
+
first = true
|
107
|
+
@worksheets.each{|worksheet|
|
108
|
+
if first
|
109
|
+
worksheet.to_xls(wb.ActiveSheet)
|
110
|
+
first = false
|
111
|
+
else
|
112
|
+
@log.error("#{__method__}: Wrong worksheet sequence")
|
113
|
+
worksheet.to_xls(wb.Worksheets.Add)
|
114
|
+
end
|
115
|
+
}
|
116
|
+
wb
|
117
|
+
end
|
118
|
+
|
119
|
+
=begin rdoc
|
120
|
+
Save the workbook.
|
121
|
+
|
122
|
+
Filename must end with
|
123
|
+
* xls
|
124
|
+
* xlsx
|
125
|
+
* xlm (Microsoft Office Word 2003 XML Format)
|
126
|
+
|
127
|
+
The optional XML-file is used to create the workbook.
|
128
|
+
|
129
|
+
The way Ruby Workbook -> xml -> xls[x] is faster then the
|
130
|
+
direct xls[x] generation.
|
131
|
+
=end
|
132
|
+
def save(path, xml_file = nil)
|
133
|
+
|
134
|
+
@log.info("Save #{path}")
|
135
|
+
expath = File.expand_path(path) #Excel needs absolute path
|
136
|
+
expath.gsub!(/\//, '\\')# Save the workbook. / must be \
|
137
|
+
begin
|
138
|
+
#different formats, see http://msdn.microsoft.com/en-us/library/microsoft.office.interop.excel.xlfileformat.aspx
|
139
|
+
case path
|
140
|
+
when /xlsx$/
|
141
|
+
wb = prepare_xls(xml_file)
|
142
|
+
@log.info("Save #{path}")
|
143
|
+
wb.SaveAs(expath, 51)
|
144
|
+
wb.Close
|
145
|
+
when /xls$/
|
146
|
+
wb = prepare_xls(xml_file)
|
147
|
+
@log.info("Save #{path}")
|
148
|
+
wb.SaveAs(expath, -4143 ) #excel97_2003_format
|
149
|
+
wb.Close
|
150
|
+
when /xml$/
|
151
|
+
=begin rdoc
|
152
|
+
Save "Microsoft Office Word 2003 XML Format".
|
153
|
+
|
154
|
+
Special rules:
|
155
|
+
* The last CR must be deleted. Else Excel doesn't accept the xml
|
156
|
+
* There must be a name space.
|
157
|
+
The namespace must be used before its definition (Tag Workbook)
|
158
|
+
=end
|
159
|
+
ns = 'ss' #namespace
|
160
|
+
File.open(path, 'w'){|f|
|
161
|
+
f << build_excel_xml(ns)
|
162
|
+
}
|
163
|
+
else
|
164
|
+
@log.fatal("Wrong filename, no xls/xlsx (#{path})")
|
165
|
+
raise ArgumentError, "Wrong filename, no xls/xlsx (#{path})"
|
166
|
+
end
|
167
|
+
rescue WIN32OLERuntimeError => err
|
168
|
+
@log.error("Error #{path} (Opened in Excel?) #{err}")
|
169
|
+
end
|
170
|
+
# Close the workbook
|
171
|
+
|
172
|
+
end #save_workbook
|
173
|
+
|
174
|
+
|
175
|
+
=begin rdoc
|
176
|
+
Build the XML for Excel ("Microsoft Office Word 2003 XML Format")
|
177
|
+
|
178
|
+
Requires a namespace for the xml.
|
179
|
+
|
180
|
+
Special rules:
|
181
|
+
* The last CR must be deleted. Else Excel doesn't accept the xml
|
182
|
+
* There must be a name space.
|
183
|
+
The namespace must be used before its definition (Tag Workbook)
|
184
|
+
|
185
|
+
Example:
|
186
|
+
* http://blogs.msdn.com/b/brian_jones/archive/2005/06/27/433152.aspx
|
187
|
+
|
188
|
+
XSD:
|
189
|
+
* http://www.microsoft.com/downloads/en/details.aspx?familyid=fe118952-3547-420a-a412-00a2662442d9&displaylang=en
|
190
|
+
|
191
|
+
Descriptions and examples:
|
192
|
+
http://en.wikipedia.org/wiki/Microsoft_Office_XML_formats
|
193
|
+
=end
|
194
|
+
def build_excel_xml(namespace)
|
195
|
+
|
196
|
+
#Define method ns
|
197
|
+
self.class.class_eval(%{
|
198
|
+
def ns(attr=nil)
|
199
|
+
attr ? "%s:%s" % ['#{namespace}', attr] : '#{namespace}'
|
200
|
+
end
|
201
|
+
})
|
202
|
+
|
203
|
+
@log.debug("Prepare XML")
|
204
|
+
builder = Nokogiri::XML::Builder.new() { |xmlbuilder|
|
205
|
+
#~ xmlbuilder.Workbook( 'xmlns'=>"urn:schemas-microsoft-com:office:spreadsheet"){
|
206
|
+
xmlbuilder.Workbook( "xmlns:#{namespace}" =>"urn:schemas-microsoft-com:office:spreadsheet"){
|
207
|
+
#Add Styles
|
208
|
+
xmlbuilder[ns].Styles{
|
209
|
+
@styles.each{|name,style|
|
210
|
+
style.to_xml(xmlbuilder, method(:ns))
|
211
|
+
}
|
212
|
+
} unless @styles.empty?
|
213
|
+
#Add worksheets
|
214
|
+
@worksheets.each{|worksheet|
|
215
|
+
worksheet.to_xml(xmlbuilder, method(:ns))
|
216
|
+
}
|
217
|
+
} #Workbook
|
218
|
+
} #@builder
|
219
|
+
|
220
|
+
builder.to_xml(
|
221
|
+
#~ :encoding => 'utf-8',
|
222
|
+
:indent => 4,
|
223
|
+
:save_with => Nokogiri::XML::Node::SaveOptions::FORMAT
|
224
|
+
).strip.gsub(/<(\/?)Workbook/, '<\1%s:Workbook' % namespace )
|
225
|
+
|
226
|
+
end #build_excel_xml
|
227
|
+
|
228
|
+
end #class Workbook
|
229
|
+
end #module Excel
|