rexcel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|