axlsx 1.1.8 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.yardopts +5 -2
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +48 -46
  4. data/Rakefile +3 -3
  5. data/examples/basic_charts.rb +8 -0
  6. data/examples/example.rb +7 -1
  7. data/examples/example.xlsx +0 -0
  8. data/examples/example_streamed.xlsx +0 -0
  9. data/examples/no-use_autowidth.xlsx +0 -0
  10. data/examples/scraping_html.rb +91 -0
  11. data/examples/shared_strings_example.xlsx +0 -0
  12. data/lib/axlsx.rb +14 -8
  13. data/lib/axlsx/drawing/bar_3D_chart.rb +2 -8
  14. data/lib/axlsx/drawing/chart.rb +29 -25
  15. data/lib/axlsx/drawing/d_lbls.rb +100 -0
  16. data/lib/axlsx/drawing/drawing.rb +2 -0
  17. data/lib/axlsx/drawing/line_3D_chart.rb +2 -9
  18. data/lib/axlsx/drawing/pie_3D_chart.rb +3 -0
  19. data/lib/axlsx/drawing/scatter_chart.rb +2 -8
  20. data/lib/axlsx/drawing/two_cell_anchor.rb +38 -1
  21. data/lib/axlsx/util/simple_typed_list.rb +13 -6
  22. data/lib/axlsx/version.rb +2 -7
  23. data/lib/axlsx/workbook/defined_name.rb +174 -0
  24. data/lib/axlsx/workbook/defined_names.rb +21 -0
  25. data/lib/axlsx/workbook/workbook.rb +39 -13
  26. data/lib/axlsx/workbook/worksheet/auto_filter.rb +34 -0
  27. data/lib/axlsx/workbook/worksheet/cell.rb +24 -1
  28. data/lib/axlsx/workbook/worksheet/col.rb +15 -0
  29. data/lib/axlsx/workbook/worksheet/cols.rb +20 -0
  30. data/lib/axlsx/workbook/worksheet/comments.rb +8 -0
  31. data/lib/axlsx/workbook/worksheet/conditional_formattings.rb +25 -0
  32. data/lib/axlsx/workbook/worksheet/data_validations.rb +28 -0
  33. data/lib/axlsx/workbook/worksheet/dimension.rb +65 -0
  34. data/lib/axlsx/workbook/worksheet/merged_cells.rb +35 -0
  35. data/lib/axlsx/workbook/worksheet/protected_ranges.rb +34 -0
  36. data/lib/axlsx/workbook/worksheet/row.rb +1 -1
  37. data/lib/axlsx/workbook/worksheet/sheet_data.rb +25 -0
  38. data/lib/axlsx/workbook/worksheet/sheet_pr.rb +24 -0
  39. data/lib/axlsx/workbook/worksheet/tables.rb +31 -0
  40. data/lib/axlsx/workbook/worksheet/worksheet.rb +263 -380
  41. data/lib/axlsx/workbook/worksheet/worksheet_comments.rb +57 -0
  42. data/lib/axlsx/workbook/worksheet/worksheet_drawing.rb +64 -0
  43. data/test/drawing/tc_bar_series.rb +1 -1
  44. data/test/drawing/tc_chart.rb +7 -1
  45. data/test/drawing/tc_d_lbls.rb +47 -0
  46. data/test/drawing/tc_drawing.rb +5 -4
  47. data/test/drawing/tc_line_series.rb +1 -1
  48. data/test/drawing/tc_pie_3D_chart.rb +1 -1
  49. data/test/drawing/tc_pie_series.rb +1 -1
  50. data/test/drawing/tc_scatter_series.rb +1 -1
  51. data/test/drawing/tc_series.rb +1 -1
  52. data/test/tc_package.rb +16 -1
  53. data/test/workbook/tc_defined_name.rb +41 -0
  54. data/test/workbook/tc_workbook.rb +5 -3
  55. data/test/workbook/worksheet/table/tc_table.rb +0 -8
  56. data/test/workbook/worksheet/tc_cell.rb +2 -4
  57. data/test/workbook/worksheet/tc_protected_range.rb +0 -1
  58. data/test/workbook/worksheet/tc_row.rb +2 -2
  59. data/test/workbook/worksheet/tc_worksheet.rb +19 -21
  60. metadata +48 -7
@@ -134,7 +134,7 @@ module Axlsx
134
134
  # @return [Cell]
135
135
  def add_cell(value="", options={})
136
136
  c = Cell.new(self, value, options)
137
- worksheet.send(:update_column_info, self.cells, [], self.cells.map(&:style))
137
+ worksheet.send(:update_column_info, self.cells, [])
138
138
  c
139
139
  end
140
140
 
@@ -0,0 +1,25 @@
1
+ module Axlsx
2
+
3
+ # This class manages the serialization of rows for worksheets
4
+ class SheetData
5
+
6
+ # Creates a new SheetData object
7
+ # @param [Worksheet] worksheet The worksheet that owns this sheet data.
8
+ def initialize(worksheet)
9
+ raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
10
+ @worksheet = worksheet
11
+ end
12
+
13
+ attr_reader :worksheet
14
+
15
+ # Serialize the sheet data
16
+ # @param [String] str the string this objects serializaton will be concacted to.
17
+ # @return [String]
18
+ def to_xml_string(str = '')
19
+ str << '<sheetData>'
20
+ worksheet.rows.each_with_index{ |row, index| row.to_xml_string(index, str) }
21
+ str << '</sheetData>'
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Axlsx
2
+
3
+ # The SheetPr class manages serialization fo a worksheet's sheetPr element.
4
+ # Only fit_to_page is implemented
5
+ class SheetPr
6
+
7
+ # Creates a new SheetPr object
8
+ # @param [Worksheet] worksheet The worksheet that owns this SheetPr object
9
+ def initialize(worksheet)
10
+ raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
11
+ @worksheet = worksheet
12
+ end
13
+
14
+ attr_reader :worksheet
15
+
16
+ # Serialize the object
17
+ # @param [String] str serialized output will be appended to this object if provided.
18
+ # @return [String]
19
+ def to_xml_string(str = '')
20
+ return unless worksheet.fit_to_page?
21
+ str << "<sheetPr><pageSetUpPr fitToPage=\"%s\"></pageSetUpPr></sheetPr>" % worksheet.fit_to_page?
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module Axlsx
2
+
3
+ # A simple, self serializing class for storing tables
4
+ class Tables < SimpleTypedList
5
+
6
+ # creates a new Tables object
7
+ def initialize(worksheet)
8
+ raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
9
+ super Table
10
+ @worksheet = worksheet
11
+ end
12
+
13
+ # The worksheet that owns this collection of tables
14
+ # @return [Worksheet]
15
+ attr_reader :worksheet
16
+
17
+ # returns the relationships required by this collection
18
+ def relationships
19
+ return [] if empty?
20
+ map{ |table| Relationship.new(TABLE_R, "../#{table.pn}") }
21
+ end
22
+
23
+ def to_xml_string(str = "")
24
+ return if empty?
25
+ str << "<tableParts count='#{size}'>"
26
+ @list.each { |table| str << "<tablePart r:id='#{table.rId}'/>" }
27
+ str << '</tableParts>'
28
+ end
29
+ end
30
+
31
+ end
@@ -4,9 +4,44 @@ module Axlsx
4
4
  # The Worksheet class represents a worksheet in the workbook.
5
5
  class Worksheet
6
6
 
7
+ # definition of characters which are less than the maximum width of 0-9 in the default font for use in String#count.
8
+ # This is used for autowidth calculations
9
+ # @return [String]
10
+ def self.thin_chars
11
+ @thin_chars ||= "^.acefijklrstxyzFIJL()-"
12
+ end
13
+
14
+ # Creates a new worksheet.
15
+ # @note the recommended way to manage worksheets is Workbook#add_worksheet
16
+ # @see Workbook#add_worksheet
17
+ # @option options [String] name The name of this worksheet.
18
+ # @option options [Hash] page_margins A hash containing page margins for this worksheet. @see PageMargins
19
+ # @option options [Hash] print_options A hash containing print options for this worksheet. @see PrintOptions
20
+ # @option options [Boolean] show_gridlines indicates if gridlines should be shown for this sheet.
21
+ def initialize(wb, options={})
22
+ self.workbook = wb
23
+ @workbook.worksheets << self
24
+ @sheet_protection = nil
25
+
26
+ initialize_page_options(options)
27
+ options.each do |o|
28
+ self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
29
+ end
30
+ end
31
+
32
+ # Initalizes page margin, setup and print options
33
+ # @param [Hash] options Options passed in from the initializer
34
+ def initialize_page_options(options)
35
+ @page_margins = PageMargins.new options[:page_margins] if options[:page_margins]
36
+ @page_setup = PageSetup.new options[:page_setup] if options[:page_setup]
37
+ @print_options = PrintOptions.new options[:print_options] if options[:print_options]
38
+ end
39
+
7
40
  # The name of the worksheet
8
41
  # @return [String]
9
- attr_reader :name
42
+ def name
43
+ @name ||= "Sheet" + (index+1).to_s
44
+ end
10
45
 
11
46
  # The sheet protection object for this workbook
12
47
  # @return [SheetProtection]
@@ -17,12 +52,6 @@ module Axlsx
17
52
  @sheet_protection
18
53
  end
19
54
 
20
- # A collection of protected ranges in the worksheet
21
- # @note The recommended way to manage protected ranges is with Worksheet#protect_range
22
- # @see Worksheet#protect_range
23
- # @return [SimpleTypedList] The protected ranges for this worksheet
24
- attr_reader :protected_ranges
25
-
26
55
  # The sheet view object for this worksheet
27
56
  # @return [SheetView]
28
57
  # @see [SheetView]
@@ -38,50 +67,35 @@ module Axlsx
38
67
 
39
68
  # The tables in this worksheet
40
69
  # @return [Array] of Table
41
- attr_reader :tables
42
-
43
- # The comments associated with this worksheet
44
- # @return [SimpleTypedList]
45
- attr_reader :comments
70
+ def tables
71
+ @tables ||= Tables.new self
72
+ end
73
+
74
+ # The a shortcut to the worksheet_comments list of comments
75
+ # @return [Array|SimpleTypedList]
76
+ def comments
77
+ worksheet_comments.comments if worksheet_comments.has_comments?
78
+ end
46
79
 
47
80
  # The rows in this worksheet
48
81
  # @note The recommended way to manage rows is Worksheet#add_row
49
82
  # @return [SimpleTypedList]
50
83
  # @see Worksheet#add_row
51
- attr_reader :rows
52
-
53
- # An array of content based calculated column widths.
54
- # @note a single auto fit data item is a hash with :longest => [String] and :sz=> [Integer] members.
55
- # @return [Array] of Hash
56
- attr_reader :auto_fit_data
84
+ def rows
85
+ @rows ||= SimpleTypedList.new Row
86
+ end
57
87
 
58
- # An array of merged cell ranges e.d "A1:B3"
59
- # Content and formatting is read from the first cell.
60
- # @return Array
61
- attr_reader :merged_cells
88
+ # returns the sheet data as columnw
89
+ def cols
90
+ @rows.transpose
91
+ end
62
92
 
63
93
  # An range that excel will apply an autfilter to "A1:B3"
64
94
  # This will turn filtering on for the cells in the range.
65
95
  # The first row is considered the header, while subsequent rows are considerd to be data.
66
- # @return Array
67
- attr_reader :auto_filter
68
-
69
- # Indicates if the worksheet should show gridlines or not
70
- # @return Boolean
71
- # @deprecated Use {SheetView#show_grid_lines} instead.
72
- def show_gridlines
73
- warn('axlsx::DEPRECIATED: Worksheet#show_gridlines has been depreciated. This value can get over SheetView#show_grid_lines.')
74
- sheet_view.show_grid_lines
75
- end
76
-
77
- # Indicates if the worksheet is selected in the workbook
78
- # It is possible to have more than one worksheet selected, however it might cause issues
79
- # in some older versions of excel when using copy and paste.
80
- # @return Boolean
81
- # @deprecated Use {SheetView#tab_selected} instead.
82
- def selected
83
- warn('axlsx::DEPRECIATED: Worksheet#selected has been depreciated. This value can get over SheetView#tab_selected.')
84
- sheet_view.tab_selected
96
+ # @return String
97
+ def auto_filter
98
+ @auto_filter ||= AutoFilter.new self
85
99
  end
86
100
 
87
101
  # Indicates if the worksheet will be fit by witdh or height to a specific number of pages.
@@ -90,14 +104,16 @@ module Axlsx
90
104
  # @return Boolean
91
105
  # @see #page_setup
92
106
  def fit_to_page?
93
- return false unless @page_setup
94
- @page_setup.fit_to_page?
107
+ return false unless self.instance_values.keys.include?('page_setup')
108
+ page_setup.fit_to_page?
95
109
  end
96
110
 
97
111
 
98
112
  # Column info for the sheet
99
113
  # @return [SimpleTypedList]
100
- attr_reader :column_info
114
+ def column_info
115
+ @column_info ||= Cols.new self
116
+ end
101
117
 
102
118
  # Page margins for printing the worksheet.
103
119
  # @example
@@ -166,77 +182,12 @@ module Axlsx
166
182
  @print_options
167
183
  end
168
184
 
169
- # definition of characters which are less than the maximum width of 0-9 in the default font for use in String#count.
170
- # This is used for autowidth calculations
171
- # @return [String]
172
- def self.thin_chars
173
- @thin_chars ||= "^.acefijklrstxyzFIJL()-"
174
- end
175
-
176
- # Creates a new worksheet.
177
- # @note the recommended way to manage worksheets is Workbook#add_worksheet
178
- # @see Workbook#add_worksheet
179
- # @option options [String] name The name of this worksheet.
180
- # @option options [Hash] page_margins A hash containing page margins for this worksheet. @see PageMargins
181
- # @option options [Hash] print_options A hash containing print options for this worksheet. @see PrintOptions
182
- # @option options [Boolean] show_gridlines indicates if gridlines should be shown for this sheet.
183
- def initialize(wb, options={})
184
- self.workbook = wb
185
- @workbook.worksheets << self
186
- @page_marging = @page_setup = @print_options = nil
187
- @drawing = @page_margins = @auto_filter = @sheet_protection = @sheet_view = nil
188
- @merged_cells = []
189
- @auto_fit_data = []
190
- @conditional_formattings = []
191
- @data_validations = []
192
- @comments = Comments.new(self)
193
- self.name = "Sheet" + (index+1).to_s
194
- @page_margins = PageMargins.new options[:page_margins] if options[:page_margins]
195
- @page_setup = PageSetup.new options[:page_setup] if options[:page_setup]
196
- @print_options = PrintOptions.new options[:print_options] if options[:print_options]
197
- @rows = SimpleTypedList.new Row
198
- @column_info = SimpleTypedList.new Col
199
- @protected_ranges = SimpleTypedList.new ProtectedRange
200
- @tables = SimpleTypedList.new Table
201
-
202
- options.each do |o|
203
- self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
204
- end
205
- end
206
-
207
185
  # convinience method to access all cells in this worksheet
208
186
  # @return [Array] cells
209
187
  def cells
210
188
  rows.flatten
211
189
  end
212
190
 
213
- # Add conditional formatting to this worksheet.
214
- #
215
- # @param [String] cells The range to apply the formatting to
216
- # @param [Array|Hash] rules An array of hashes (or just one) to create Conditional formatting rules from.
217
- # @example This would format column A whenever it is FALSE.
218
- # # for a longer example, see examples/example_conditional_formatting.rb (link below)
219
- # worksheet.add_conditional_formatting( "A1:A1048576", { :type => :cellIs, :operator => :equal, :formula => "FALSE", :dxfId => 1, :priority => 1 }
220
- #
221
- # @see ConditionalFormattingRule#initialize
222
- # @see file:examples/example_conditional_formatting.rb
223
- def add_conditional_formatting(cells, rules)
224
- cf = ConditionalFormatting.new( :sqref => cells )
225
- cf.add_rules rules
226
- @conditional_formattings << cf
227
- end
228
-
229
- # Add data validation to this worksheet.
230
- #
231
- # @param [String] cells The cells the validation will apply to.
232
- # @param [hash] data_validation options defining the validation to apply.
233
- # @see examples/data_validation.rb for an example
234
- def add_data_validation(cells, data_validation)
235
- dv = DataValidation.new(data_validation)
236
- dv.sqref = cells
237
- @data_validations << dv
238
- end
239
-
240
191
  # Creates merge information for this worksheet.
241
192
  # Cells can be merged by calling the merge_cells method on a worksheet.
242
193
  # @example This would merge the three cells C1..E1 #
@@ -247,12 +198,7 @@ module Axlsx
247
198
  # worksheet["C1"].merge worksheet["E1"]
248
199
  # @param [Array, string] cells
249
200
  def merge_cells(cells)
250
- @merged_cells << if cells.is_a?(String)
251
- cells
252
- elsif cells.is_a?(Array)
253
- cells = cells.sort { |x, y| [x.index, x.row.index] <=> [y.index, y.row.index] }
254
- "#{cells.first.r}:#{cells.last.r}"
255
- end
201
+ merged_cells.add cells
256
202
  end
257
203
 
258
204
  # Adds a new protected cell range to the worksheet. Note that protected ranges are only in effect when sheet protection is enabled.
@@ -260,23 +206,21 @@ module Axlsx
260
206
  # @return [ProtectedRange]
261
207
  # @note When using an array of cells, a contiguous range is created from the minimum top left to the maximum top bottom of the cells provided.
262
208
  def protect_range(cells)
263
- sqref = if cells.is_a?(String)
264
- cells
265
- elsif cells.is_a?(SimpleTypedList)
266
- cells = cells.sort { |x, y| [x.index, x.row.index] <=> [y.index, y.row.index] }
267
- "#{cells.first.r}:#{cells.last.r}"
268
- end
269
- @protected_ranges << ProtectedRange.new(:sqref => sqref, :name => 'Range#{@protected_ranges.size}')
270
- @protected_ranges.last
209
+ protected_ranges.add_range(cells)
271
210
  end
272
211
 
273
- # The demensions of a worksheet. This is not actually a required element by the spec,
212
+ # The dimensions of a worksheet. This is not actually a required element by the spec,
274
213
  # but at least a few other document readers expect this for conversion
275
- # @return [String] the A1:B2 style reference for the first and last row column intersection in the workbook
214
+ # @return [Dimension]
276
215
  def dimension
277
- dim_start = rows.first.cells.first == nil ? 'A1' : rows.first.cells.first.r
278
- dim_end = rows.last.cells.last == nil ? 'AA200' : rows.last.cells.last.r
279
- "#{dim_start}:#{dim_end}"
216
+ @dimension ||= Dimension.new self
217
+ end
218
+
219
+ # The sheet properties for this workbook.
220
+ # Currently only pageSetUpPr -> fitToPage is implemented
221
+ # @return [SheetPr]
222
+ def sheet_pr
223
+ @sheet_pr ||= SheetPr.new self
280
224
  end
281
225
 
282
226
  # Indicates if gridlines should be shown in the sheet.
@@ -298,6 +242,23 @@ module Axlsx
298
242
  sheet_view.tab_selected = v
299
243
  end
300
244
 
245
+ # Indicates if the worksheet should show gridlines or not
246
+ # @return Boolean
247
+ # @deprecated Use {SheetView#show_grid_lines} instead.
248
+ def show_gridlines
249
+ warn('axlsx::DEPRECIATED: Worksheet#show_gridlines has been depreciated. This value can get over SheetView#show_grid_lines.')
250
+ sheet_view.show_grid_lines
251
+ end
252
+
253
+ # Indicates if the worksheet is selected in the workbook
254
+ # It is possible to have more than one worksheet selected, however it might cause issues
255
+ # in some older versions of excel when using copy and paste.
256
+ # @return Boolean
257
+ # @deprecated Use {SheetView#tab_selected} instead.
258
+ def selected
259
+ warn('axlsx::DEPRECIATED: Worksheet#selected has been depreciated. This value can get over SheetView#tab_selected.')
260
+ sheet_view.tab_selected
261
+ end
301
262
 
302
263
  # (see #fit_to_page)
303
264
  # @return [Boolean]
@@ -306,33 +267,12 @@ module Axlsx
306
267
  fit_to_page?
307
268
  end
308
269
 
309
-
310
- # returns the column and row index for a named based cell
311
- # @param [String] name The cell or cell range to return. "A1" will return the first cell of the first row.
312
- # @return [Cell]
313
- def name_to_cell(name)
314
- col_index, row_index = *Axlsx::name_to_indices(name)
315
- r = rows[row_index]
316
- r.cells[col_index] if r
317
- end
318
-
319
270
  # The name of the worksheet
320
271
  # The name of a worksheet must be unique in the workbook, and must not exceed 31 characters
321
- # @param [String] v
322
- def name=(v)
323
- DataTypeValidator.validate "Worksheet.name", String, v
324
- raise ArgumentError, (ERR_SHEET_NAME_TOO_LONG % v) if v.size > 31
325
- raise ArgumentError, (ERR_SHEET_NAME_COLON_FORBIDDEN % v) if v.include? ':'
326
- v = Axlsx::coder.encode(v)
327
- sheet_names = @workbook.worksheets.map { |s| s.name }
328
- raise ArgumentError, (ERR_DUPLICATE_SHEET_NAME % v) if sheet_names.include?(v)
329
- @name=v
330
- end
331
-
332
- # The absolute auto filter range
333
- # @see auto_filter
334
- def abs_auto_filter
335
- Axlsx.cell_range(@auto_filter.split(':').collect { |name| name_to_cell(name)}) if @auto_filter
272
+ # @param [String] name
273
+ def name=(name)
274
+ validate_sheet_name name
275
+ @name=Axlsx::coder.encode(name)
336
276
  end
337
277
 
338
278
  # The auto filter range for the worksheet
@@ -340,7 +280,7 @@ module Axlsx
340
280
  # @see auto_filter
341
281
  def auto_filter=(v)
342
282
  DataTypeValidator.validate "Worksheet.auto_filter", String, v
343
- @auto_filter = v
283
+ auto_filter.range = v
344
284
  end
345
285
 
346
286
  # The part name of this worksheet
@@ -372,7 +312,7 @@ module Axlsx
372
312
  # @return [Drawing]
373
313
  # @see Worksheet#add_chart
374
314
  def drawing
375
- @drawing || @drawing = Axlsx::Drawing.new(self)
315
+ worksheet_drawing.drawing
376
316
  end
377
317
 
378
318
  # Adds a row to the worksheet and updates auto fit data.
@@ -416,73 +356,39 @@ module Axlsx
416
356
  # @option options [Float] height the row's height (in points)
417
357
  def add_row(values=[], options={})
418
358
  Row.new(self, values, options)
419
- update_column_info @rows.last.cells, options.delete(:widths) ||[], options.delete(:style) || []
420
- # update_auto_fit_data @rows.last.cells, options.delete(:widths) || []
359
+ update_column_info @rows.last.cells, options.delete(:widths) || []
421
360
  yield @rows.last if block_given?
422
361
  @rows.last
423
362
  end
424
363
 
425
364
  alias :<< :add_row
426
365
 
427
- # Set the style for cells in a specific row
428
- # @param [Integer] index or range of indexes in the table
429
- # @param [Integer] style the cellXfs index
430
- # @param [Hash] options the options used when applying the style
431
- # @option [Integer] :col_offset only cells after this column will be updated.
432
- # @note You can also specify the style in the add_row call
433
- # @see Worksheet#add_row
434
- # @see README.md for an example
435
- def row_style(index, style, options={})
436
- offset = options.delete(:col_offset) || 0
437
- rs = @rows[index]
438
- if rs.is_a?(Array)
439
- rs.each { |r| r.cells[(offset..-1)].each { |c| c.style = style } }
440
- else
441
- rs.cells[(offset..-1)].each { |c| c.style = style }
442
- end
443
- end
444
-
445
- # returns the sheet data as columnw
446
- def cols
447
- @rows.transpose
448
- end
449
-
450
-
451
- # Set the style for cells in a specific column
452
- # @param [Integer] index the index of the column
453
- # @param [Integer] style the cellXfs index
454
- # @param [Hash] options
455
- # @option [Integer] :row_offset only cells after this column will be updated.
456
- # @note You can also specify the style for specific columns in the call to add_row by using an array for the :styles option
457
- # @see Worksheet#add_row
458
- # @see README.md for an example
459
- def col_style(index, style, options={})
460
- offset = options.delete(:row_offset) || 0
461
- @rows[(offset..-1)].each do |r|
462
- cells = r.cells[index]
463
- next unless cells
464
- if cells.is_a?(Array)
465
- cells.each { |c| c.style = style }
466
- else
467
- cells.style = style
468
- end
469
- end
366
+ # Add conditional formatting to this worksheet.
367
+ #
368
+ # @param [String] cells The range to apply the formatting to
369
+ # @param [Array|Hash] rules An array of hashes (or just one) to create Conditional formatting rules from.
370
+ # @example This would format column A whenever it is FALSE.
371
+ # # for a longer example, see examples/example_conditional_formatting.rb (link below)
372
+ # worksheet.add_conditional_formatting( "A1:A1048576", { :type => :cellIs, :operator => :equal, :formula => "FALSE", :dxfId => 1, :priority => 1 }
373
+ #
374
+ # @see ConditionalFormattingRule#initialize
375
+ # @see file:examples/example_conditional_formatting.rb
376
+ def add_conditional_formatting(cells, rules)
377
+ cf = ConditionalFormatting.new( :sqref => cells )
378
+ cf.add_rules rules
379
+ conditional_formattings << cf
380
+ conditional_formattings
470
381
  end
471
382
 
472
- # This is a helper method that Lets you specify a fixed width for multiple columns in a worksheet in one go.
473
- # Axlsx is sparse, so if you have not set data for a column, you cannot set the width.
474
- # Setting a fixed column width to nil will revert the behaviour back to calculating the width for you.
475
- # @example This would set the first and third column widhts but leave the second column in autofit state.
476
- # ws.column_widths 7.2, nil, 3
477
- # @note For updating only a single column it is probably easier to just set the width of the ws.column_info[col_index].width directly
478
- # @param [Integer|Float|Fixnum|nil] widths
479
- def column_widths(*widths)
480
- widths.each_with_index do |value, index|
481
- next if value == nil
482
- Axlsx::validate_unsigned_numeric(value) unless value == nil
483
- @column_info[index] ||= Col.new index+1, index+1
484
- @column_info[index].width = value
485
- end
383
+ # Add data validation to this worksheet.
384
+ #
385
+ # @param [String] cells The cells the validation will apply to.
386
+ # @param [hash] data_validation options defining the validation to apply.
387
+ # @see examples/data_validation.rb for an example
388
+ def add_data_validation(cells, data_validation)
389
+ dv = DataValidation.new(data_validation)
390
+ dv.sqref = cells
391
+ data_validations << dv
486
392
  end
487
393
 
488
394
  # Adds a chart to this worksheets drawing. This is the recommended way to create charts for your worksheet. This method wraps the complexity of dealing with ooxml drawing, anchors, markers graphic frames chart objects and all the other dirty details.
@@ -499,68 +405,84 @@ module Axlsx
499
405
  # @see Line3DChart
500
406
  # @see README for examples
501
407
  def add_chart(chart_type, options={})
502
- chart = drawing.add_chart(chart_type, options)
408
+ chart = worksheet_drawing.add_chart(chart_type, options)
503
409
  yield chart if block_given?
504
410
  chart
505
411
  end
506
412
 
507
413
  # needs documentation
508
414
  def add_table(ref, options={})
509
- table = Table.new(ref, self, options)
510
- @tables << table
511
- yield table if block_given?
512
- table
415
+ tables << Table.new(ref, self, options)
416
+ yield tables.last if block_given?
417
+ tables.last
513
418
  end
514
419
 
515
-
516
- # Shortcut to comments#add_comment
420
+ # Shortcut to worsksheet_comments#add_comment
517
421
  def add_comment(options={})
518
- @comments.add_comment(options)
422
+ worksheet_comments.add_comment(options)
519
423
  end
520
424
 
521
425
  # Adds a media item to the worksheets drawing
522
426
  # @option [Hash] options options passed to drawing.add_image
523
427
  def add_image(options={})
524
- image = drawing.add_image(options)
428
+ image = worksheet_drawing.add_image(options)
525
429
  yield image if block_given?
526
430
  image
527
431
  end
528
432
 
433
+ # This is a helper method that Lets you specify a fixed width for multiple columns in a worksheet in one go.
434
+ # Axlsx is sparse, so if you have not set data for a column, you cannot set the width.
435
+ # Setting a fixed column width to nil will revert the behaviour back to calculating the width for you on the next call to add_row.
436
+ # @example This would set the first and third column widhts but leave the second column in autofit state.
437
+ # ws.column_widths 7.2, nil, 3
438
+ # @note For updating only a single column it is probably easier to just set the width of the ws.column_info[col_index].width directly
439
+ # @param [Integer|Float|Fixnum|nil] widths
440
+ def column_widths(*widths)
441
+ widths.each_with_index do |value, index|
442
+ next if value == nil
443
+ Axlsx::validate_unsigned_numeric(value) unless value == nil
444
+ find_or_create_column_info(index).width = value
445
+ end
446
+ end
447
+
448
+ # Set the style for cells in a specific column
449
+ # @param [Integer] index the index of the column
450
+ # @param [Integer] style the cellXfs index
451
+ # @param [Hash] options
452
+ # @option [Integer] :row_offset only cells after this column will be updated.
453
+ # @note You can also specify the style for specific columns in the call to add_row by using an array for the :styles option
454
+ # @see Worksheet#add_row
455
+ # @see README.md for an example
456
+ def col_style(index, style, options={})
457
+ offset = options.delete(:row_offset) || 0
458
+ cells = @rows[(offset..-1)].map { |row| row.cells[index] }.flatten.compact
459
+ cells.each { |cell| cell.style = style }
460
+ end
461
+
462
+ # Set the style for cells in a specific row
463
+ # @param [Integer] index or range of indexes in the table
464
+ # @param [Integer] style the cellXfs index
465
+ # @param [Hash] options the options used when applying the style
466
+ # @option [Integer] :col_offset only cells after this column will be updated.
467
+ # @note You can also specify the style in the add_row call
468
+ # @see Worksheet#add_row
469
+ # @see README.md for an example
470
+ def row_style(index, style, options={})
471
+ offset = options.delete(:col_offset) || 0
472
+ cells = cols[(offset..-1)].map { |column| column[index] }.flatten.compact
473
+ cells.each { |cell| cell.style = style }
474
+ end
475
+
529
476
  # Serializes the worksheet object to an xml string
530
477
  # This intentionally does not use nokogiri for performance reasons
531
478
  # @return [String]
532
479
  def to_xml_string
533
480
  str = '<?xml version="1.0" encoding="UTF-8"?>'
534
481
  str << worksheet_node
535
- str << sheet_pr_node
536
- str << dimension_node
537
- @sheet_view.to_xml_string(str) if @sheet_view
538
- str << cols_node
539
- str << sheet_data_node
540
-
541
- str << auto_filter_node
542
- @sheet_protection.to_xml_string(str) if @sheet_protection
543
- str << protected_ranges_node
544
- str << merged_cells_node
545
- @print_options.to_xml_string(str) if @print_options
546
- page_margins.to_xml_string(str) if @page_margins
547
- page_setup.to_xml_string(str) if @page_setup
548
- str << drawing_node
549
- str << legacy_drawing_node
550
- str << table_parts_node
551
- str << conditional_formattings_node
552
- str << data_validations_node
482
+ serializable_parts.each do |item|
483
+ item.to_xml_string(str) if item
484
+ end
553
485
  str << '</worksheet>'
554
- # User reported that when parsing some old data that had control characters excel chokes.
555
- # All of the following are defined as illegal xml characters in the xml spec, but for now I am only dealing with control
556
- # characters. Thanks to asakusarb and @hsbt's flash of code on the screen!
557
- # [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
558
- # [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
559
- # [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
560
- # [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
561
- # [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
562
- # [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
563
- # [#x10FFFE-#x10FFFF].
564
486
  str.gsub(/[[:cntrl:]]/,'')
565
487
  end
566
488
 
@@ -568,15 +490,9 @@ module Axlsx
568
490
  # @return [Relationships]
569
491
  def relationships
570
492
  r = Relationships.new
571
- @tables.each do |table|
572
- r << Relationship.new(TABLE_R, "../#{table.pn}")
573
- end
574
-
575
- r << Relationship.new(VML_DRAWING_R, "../#{@comments.vml_drawing.pn}") if @comments.size > 0
576
- r << Relationship.new(COMMENT_R, "../#{@comments.pn}") if @comments.size > 0
577
- r << Relationship.new(COMMENT_R_NULL, "NULL") if @comments.size > 0
578
-
579
- r << Relationship.new(DRAWING_R, "../#{@drawing.pn}") if @drawing
493
+ r + [tables.relationships,
494
+ worksheet_comments.relationships,
495
+ worksheet_drawing.relationship].flatten.compact || []
580
496
  r
581
497
  end
582
498
 
@@ -585,163 +501,130 @@ module Axlsx
585
501
  # @return [Cell, Array]
586
502
  def [] (cell_def)
587
503
  return rows[cell_def] if cell_def.is_a?(Integer)
588
-
589
- parts = cell_def.split(':')
590
- first = name_to_cell parts[0]
504
+ parts = cell_def.split(':').map{ |part| name_to_cell part }
591
505
  if parts.size == 1
592
- first
506
+ parts.first
593
507
  else
594
- cells = []
595
- last = name_to_cell(parts[1])
596
- rows[(first.row.index..last.row.index)].each do |r|
597
- r.cells[(first.index..last.index)].each do |c|
598
- cells << c
599
- end
600
- end
601
- cells
508
+ range(*parts)
602
509
  end
603
510
  end
604
511
 
605
- private
606
-
607
- # Helper method for parsingout the root node for worksheet
608
- # @return [String]
609
- def worksheet_node
610
- "<worksheet xmlns=\"%s\" xmlns:r=\"%s\">" % [XML_NS, XML_NS_R]
512
+ # returns the column and row index for a named based cell
513
+ # @param [String] name The cell or cell range to return. "A1" will return the first cell of the first row.
514
+ # @return [Cell]
515
+ def name_to_cell(name)
516
+ col_index, row_index = *Axlsx::name_to_indices(name)
517
+ r = rows[row_index]
518
+ r.cells[col_index] if r
611
519
  end
612
520
 
613
- # Helper method fo parsing out the sheetPr node
614
- # @return [String]
615
- def sheet_pr_node
616
- return '' unless fit_to_page?
617
- "<sheetPr><pageSetUpPr fitToPage=\"%s\"></pageSetUpPr></sheetPr>" % fit_to_page?
521
+ # shortcut method to access styles direclty from the worksheet
522
+ # This lets us do stuff like:
523
+ # @example
524
+ # p = Axlsx::Package.new
525
+ # p.workbook.add_worksheet(:name => 'foo') do |sheet|
526
+ # my_style = sheet.styles.add_style { :bg_color => "FF0000" }
527
+ # sheet.add_row ['Oh No!'], :styles => my_style
528
+ # end
529
+ # p.serialize 'foo.xlsx'
530
+ def styles
531
+ @styles ||= self.workbook.styles
618
532
  end
619
533
 
620
- # Helper method for parsing out the demension node
621
- # @return [String]
622
- def dimension_node
623
- return '' if rows.size == 0
624
- "<dimension ref=\"%s\"></dimension>" % dimension
534
+
535
+ private
536
+
537
+ def validate_sheet_name(name)
538
+ DataTypeValidator.validate "Worksheet.name", String, name
539
+ raise ArgumentError, (ERR_SHEET_NAME_TOO_LONG % name) if name.size > 31
540
+ raise ArgumentError, (ERR_SHEET_NAME_COLON_FORBIDDEN % name) if name.include? ':'
541
+ name = Axlsx::coder.encode(name)
542
+ sheet_names = @workbook.worksheets.map { |s| s.name }
543
+ raise ArgumentError, (ERR_DUPLICATE_SHEET_NAME % name) if sheet_names.include?(name)
625
544
  end
626
545
 
627
- # Helper method for parsing out the sheetData node
628
- # @return [String]
629
- def sheet_data_node
630
- str = '<sheetData>'
631
- @rows.each_with_index { |row, index| row.to_xml_string(index, str) }
632
- str << '</sheetData>'
546
+
547
+ def serializable_parts
548
+ [sheet_pr, dimension, sheet_view, column_info,
549
+ sheet_data, @sheet_protection, protected_ranges,
550
+ auto_filter, merged_cells, conditional_formattings,
551
+ data_validations, print_options, page_margins,
552
+ page_setup, worksheet_drawing, worksheet_comments,
553
+ tables]
633
554
  end
634
555
 
635
- # Helper method for parsing out the autoFilter node
636
- # @return [String]
637
- def auto_filter_node
638
- return '' unless @auto_filter
639
- "<autoFilter ref='%s'></autoFilter>" % @auto_filter
556
+ def range(*cell_def)
557
+ first, last = cell_def
558
+ cells = []
559
+ rows[(first.row.index..last.row.index)].each do |r|
560
+ r.cells[(first.index..last.index)].each do |c|
561
+ cells << c
562
+ end
563
+ end
564
+ cells
640
565
  end
641
566
 
642
- # Helper method for parsing out the cols node
643
- # @return [String]
644
- def cols_node
645
- return '' if @column_info.empty?
646
- str = "<cols>"
647
- @column_info.each { |col| col.to_xml_string(str) }
648
- str << '</cols>'
649
- end
650
-
651
- # Helper method for parsing out the protectedRanges node
652
- # @return [String]
653
- def protected_ranges_node
654
- return '' if @protected_ranges.empty?
655
- str = '<protectedRanges>'
656
- @protected_ranges.each { |pr| pr.to_xml_string(str) }
657
- str << '</protectedRanges>'
567
+ # A collection of protected ranges in the worksheet
568
+ # @note The recommended way to manage protected ranges is with Worksheet#protect_range
569
+ # @see Worksheet#protect_range
570
+ # @return [SimpleTypedList] The protected ranges for this worksheet
571
+ def protected_ranges
572
+ @protected_ranges ||= ProtectedRanges.new self
573
+ # SimpleTypedList.new ProtectedRange
658
574
  end
659
575
 
660
- # Helper method for parsing out the mergedCells node
661
- # @return [String]
662
- def merged_cells_node
663
- return '' if @merged_cells.size == 0
664
- str = "<mergeCells count='#{@merged_cells.size}'>"
665
- @merged_cells.each { |merged_cell| str << "<mergeCell ref='#{merged_cell}'></mergeCell>" }
666
- str << '</mergeCells>'
576
+ # conditional formattings
577
+ # @return [Array]
578
+ def conditional_formattings
579
+ @conditional_formattings ||= ConditionalFormattings.new self
667
580
  end
668
581
 
669
- # Helper method for parsing out the drawing node
670
- # @return [String]
671
- def drawing_node
672
- return '' unless @drawing
673
- "<drawing r:id='rId" << (relationships.index{ |r| r.Type == DRAWING_R } + 1).to_s << "'/>"
582
+ # data validations array
583
+ # @return [Array]
584
+ def data_validations
585
+ @data_validations ||= DataValidations.new self
674
586
  end
675
587
 
676
- # Helper method for parsing out the legacyDrawing node required for comments
677
- # @return [String]
678
- def legacy_drawing_node
679
- return '' if @comments.empty?
680
- "<legacyDrawing r:id='rId" << (relationships.index{ |r| r.Type == VML_DRAWING_R } + 1).to_s << "'/>"
588
+ # merged cells array
589
+ # @return [Array]
590
+ def merged_cells
591
+ @merged_cells ||= MergedCells.new self
681
592
  end
682
593
 
683
- # Helper method for parsing out the tableParts node
594
+
595
+ # Helper method for parsingout the root node for worksheet
684
596
  # @return [String]
685
- def table_parts_node
686
- return '' if @tables.empty?
687
- str = "<tableParts count='#{@tables.size}'>"
688
- @tables.each { |table| str << "<tablePart r:id='#{table.rId}'/>" }
689
- str << '</tableParts>'
597
+ def worksheet_node
598
+ "<worksheet xmlns=\"%s\" xmlns:r=\"%s\">" % [XML_NS, XML_NS_R]
690
599
  end
691
600
 
692
- # Helper method for parsing out the conditional formattings
693
- # @return [String]
694
- def conditional_formattings_node
695
- return '' if @conditional_formattings.size == 0
696
- str = ''
697
- @conditional_formattings.each { |conditional_formatting| str << conditional_formatting.to_xml_string }
698
- str
601
+ def sheet_data
602
+ @sheet_data ||= SheetData.new self
699
603
  end
700
604
 
701
- # Helper method for parsing out the dataValidations node
702
- # @return [String]
703
- def data_validations_node
704
- return '' if @data_validations.size == 0
705
- str = "<dataValidations count='#{@data_validations.size}'>"
706
- @data_validations.each { |data_validation| str << data_validation.to_xml_string }
707
- str << '</dataValidations>'
605
+ def worksheet_drawing
606
+ @worksheet_drawing ||= WorksheetDrawing.new self
708
607
  end
709
608
 
609
+ # The comments associated with this worksheet
610
+ # @return [SimpleTypedList]
611
+ def worksheet_comments
612
+ @worksheet_comments ||= WorksheetComments.new self
613
+ end
710
614
 
711
- # assigns the owner workbook for this worksheet
712
615
  def workbook=(v) DataTypeValidator.validate "Worksheet.workbook", Workbook, v; @workbook = v; end
713
616
 
714
-
715
- # TODO this needs cleanup!
716
- def update_column_info(cells, widths=[], style=[])
717
- styles = self.workbook.styles
718
- cellXfs, fonts = styles.cellXfs, styles.fonts
719
- sz = 11
720
-
617
+ def update_column_info(cells, widths=[])
721
618
  cells.each_with_index do |cell, index|
722
- @column_info[index] ||= Col.new index+1, index+1
723
- col = @column_info[index]
724
- width = widths[index]
725
- col.width = width if [Integer, Float, Fixnum].include?(width.class)
726
- c_style = style[index] if [Integer, Fixnum].include?(style[index].class)
727
- next if width == :ignore || (cell.value.is_a?(String) && cell.value.start_with?('=') || cell.value == nil)
728
- if self.workbook.use_autowidth
729
- cell_xf = cellXfs[(c_style || 0)]
730
- font = fonts[(cell_xf.fontId || 0)]
731
- sz = cell.sz || font.sz || sz
732
- col.width = [(col.width || 0), calculate_width(cell.value.to_s, sz)].max
733
- end
619
+ col = find_or_create_column_info(index)
620
+ next if widths[index] == :ignore
621
+ col.update_width(cell, widths[index], workbook.use_autowidth)
734
622
  end
735
623
  end
736
624
 
737
-
738
- # This is still not perfect...
739
- # - scaling is not linear as font sizes increst
740
- # - different fonts have different mdw and char widths
741
- def calculate_width(text, sz)
742
- mdw = 1.78 #This is the widest width of 0..9 in arial@10px)
743
- font_scale = (sz/10.0).to_f
744
- ((text.count(Worksheet.thin_chars) * mdw + 5) / mdw * 256) / 256.0 * font_scale
625
+ def find_or_create_column_info(index)
626
+ column_info[index] ||= Col.new(index + 1, index + 1)
745
627
  end
628
+
746
629
  end
747
630
  end