rubyexcel 0.3.9 → 0.4.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.
- checksums.yaml +4 -4
- data/LICENSE.md +9 -9
- data/README.md +782 -779
- data/lib/rubyexcel.rb +264 -260
- data/lib/rubyexcel/address.rb +187 -187
- data/lib/rubyexcel/data.rb +450 -450
- data/lib/rubyexcel/element.rb +226 -226
- data/lib/rubyexcel/excel_tools.rb +287 -261
- data/lib/rubyexcel/rubyexcel_components.rb +70 -70
- data/lib/rubyexcel/section.rb +376 -376
- data/lib/rubyexcel/sheet.rb +720 -720
- metadata +6 -7
data/lib/rubyexcel/sheet.rb
CHANGED
@@ -1,721 +1,721 @@
|
|
1
|
-
#
|
2
|
-
# Namespace for all RubyExcel Classes and Modules
|
3
|
-
#
|
4
|
-
|
5
|
-
module RubyExcel
|
6
|
-
|
7
|
-
#
|
8
|
-
# The front-end class for data manipulation and output.
|
9
|
-
#
|
10
|
-
|
11
|
-
class Sheet
|
12
|
-
include Address
|
13
|
-
|
14
|
-
# The Data underlying the Sheet
|
15
|
-
attr_reader :data
|
16
|
-
|
17
|
-
# The name of the Sheet
|
18
|
-
attr_accessor :name
|
19
|
-
|
20
|
-
# The number of rows treated as headers
|
21
|
-
attr_accessor :header_rows
|
22
|
-
|
23
|
-
# The Workbook parent of this Sheet
|
24
|
-
attr_accessor :workbook
|
25
|
-
|
26
|
-
alias parent workbook; alias parent= workbook=
|
27
|
-
alias headers header_rows; alias headers= header_rows=
|
28
|
-
|
29
|
-
#
|
30
|
-
# Creates a RubyExcel::Sheet instance
|
31
|
-
#
|
32
|
-
# @param [String] name the name of the Sheet
|
33
|
-
# @param [RubyExcel::Workbook] workbook the Workbook which holds this Sheet
|
34
|
-
#
|
35
|
-
|
36
|
-
def initialize( name, workbook )
|
37
|
-
@workbook = workbook
|
38
|
-
@name = name
|
39
|
-
@header_rows = 1
|
40
|
-
@data = Data.new( self, [[]] )
|
41
|
-
end
|
42
|
-
|
43
|
-
#
|
44
|
-
# Read a value by address
|
45
|
-
#
|
46
|
-
# @example
|
47
|
-
# sheet['A1']
|
48
|
-
# #=> "Part"
|
49
|
-
#
|
50
|
-
# @example
|
51
|
-
# sheet['A1:B2']
|
52
|
-
# #=> [["Part", "Ref1"], ["Type1", "QT1"]]
|
53
|
-
#
|
54
|
-
# @param [String] addr the address to access
|
55
|
-
#
|
56
|
-
|
57
|
-
def[]( addr )
|
58
|
-
range( addr ).value
|
59
|
-
end
|
60
|
-
|
61
|
-
#
|
62
|
-
# Write a value by address
|
63
|
-
#
|
64
|
-
# @example
|
65
|
-
# sheet['A1'] = "Bart"
|
66
|
-
# sheet['A1']
|
67
|
-
# #=> "Bart"
|
68
|
-
#
|
69
|
-
# @param (see #[])
|
70
|
-
# @param [Object] val the value to write into the data
|
71
|
-
#
|
72
|
-
|
73
|
-
def []=( addr, val )
|
74
|
-
range( addr ).value = val
|
75
|
-
end
|
76
|
-
|
77
|
-
#
|
78
|
-
# Add data with the Sheet
|
79
|
-
#
|
80
|
-
# @param [Array<Array>, Hash<Hash>, RubyExcel::Sheet] other the data to add
|
81
|
-
# @return [RubyExcel::Sheet] returns a new Sheet
|
82
|
-
# @note When adding another Sheet it won't import the headers unless this Sheet is empty.
|
83
|
-
#
|
84
|
-
|
85
|
-
def +( other )
|
86
|
-
dup << other
|
87
|
-
end
|
88
|
-
|
89
|
-
#
|
90
|
-
# Subtract data from the Sheet
|
91
|
-
#
|
92
|
-
# @param [Array<Array>, RubyExcel::Sheet] other the data to subtract
|
93
|
-
# @return [RubyExcel::Sheet] returns a new Sheet
|
94
|
-
#
|
95
|
-
|
96
|
-
def -( other )
|
97
|
-
case other
|
98
|
-
when Array ; Workbook.new.load( data.all - other )
|
99
|
-
when Sheet ; Workbook.new.load( data.all - other.data.no_headers )
|
100
|
-
else ; fail ArgumentError, "Unsupported class: #{ other.class }"
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
#
|
105
|
-
# Append an object to the Sheet
|
106
|
-
#
|
107
|
-
# @param [Object] other the object to append
|
108
|
-
# @return [self]
|
109
|
-
# @note When adding another Sheet it won't import the headers unless this Sheet is empty.
|
110
|
-
# @note Anything other than an an Array, Hash, Row, Column or Sheet will be appended to the first row
|
111
|
-
#
|
112
|
-
|
113
|
-
def <<( other )
|
114
|
-
data << other
|
115
|
-
self
|
116
|
-
end
|
117
|
-
|
118
|
-
# @deprecated Please use {#filter!} instead
|
119
|
-
# @overload advanced_filter!( header, comparison_operator, search_criteria, ... )
|
120
|
-
# Filter on multiple criteria
|
121
|
-
# @param [String] header a header to search under
|
122
|
-
# @param [Symbol] comparison_operator the operator to compare with
|
123
|
-
# @param [Object] search_criteria the value to filter by
|
124
|
-
# @raise [ArgumentError] 'Number of arguments must be a multiple of 3'
|
125
|
-
# @raise [ArgumentError] 'Operator must be a symbol'
|
126
|
-
# @example Filter to 'Part': 'Type1' and 'Type3', with Qty greater than 1
|
127
|
-
# s.advanced_filter!( 'Part', :=~, /Type[13]/, 'Qty', :>, 1 )
|
128
|
-
# @example Filter to 'Part': 'Type1', with 'Ref1' containing 'X'
|
129
|
-
# s.advanced_filter!( 'Part', :==, 'Type1', 'Ref1', :include?, 'X' )
|
130
|
-
#
|
131
|
-
|
132
|
-
def advanced_filter!( *args )
|
133
|
-
warn "[DEPRECATION] `advanced_filter!` is deprecated. Please use `filter!` instead."
|
134
|
-
data.advanced_filter!( *args ); self
|
135
|
-
end
|
136
|
-
|
137
|
-
#
|
138
|
-
# Average the values in a Column by searching another Column
|
139
|
-
#
|
140
|
-
# @param [String] find_header the header of the Column to yield to the block
|
141
|
-
# @param [String] avg_header the header of the Column to average
|
142
|
-
# @yield yields the find_header column values to the block
|
143
|
-
#
|
144
|
-
|
145
|
-
def averageif( find_header, avg_header )
|
146
|
-
return to_enum( :sumif ) unless block_given?
|
147
|
-
find_col, avg_col = ch( find_header ), ch( avg_header )
|
148
|
-
sum = find_col.each_cell_wh.inject([0,0]) do |sum,ce|
|
149
|
-
if yield( ce.value )
|
150
|
-
sum[0] += avg_col[ ce.row ]
|
151
|
-
sum[1] += 1
|
152
|
-
sum
|
153
|
-
else
|
154
|
-
sum
|
155
|
-
end
|
156
|
-
end
|
157
|
-
sum.first.to_f / sum.last
|
158
|
-
end
|
159
|
-
|
160
|
-
#
|
161
|
-
# Access an Cell by indices.
|
162
|
-
#
|
163
|
-
# @param [Fixnum] row the row index
|
164
|
-
# @param [Fixnum] col the column index
|
165
|
-
# @return [RubyExcel::Cell]
|
166
|
-
# @note Indexing is 1-based like Excel VBA
|
167
|
-
#
|
168
|
-
|
169
|
-
def cell( row, col )
|
170
|
-
Cell.new( self, indices_to_address( row, col ) )
|
171
|
-
end
|
172
|
-
alias cells cell
|
173
|
-
|
174
|
-
#
|
175
|
-
# Delete all data and headers from Sheet
|
176
|
-
#
|
177
|
-
|
178
|
-
def clear_all
|
179
|
-
data.delete_all
|
180
|
-
self
|
181
|
-
end
|
182
|
-
alias delete_all clear_all
|
183
|
-
|
184
|
-
#
|
185
|
-
# Access a Column (Section) by its reference.
|
186
|
-
#
|
187
|
-
# @param [String, Fixnum] index the Column reference
|
188
|
-
# @return [RubyExcel::Column]
|
189
|
-
# @note Index 'A' and 1 both select the 1st Column
|
190
|
-
#
|
191
|
-
|
192
|
-
def column( index )
|
193
|
-
Column.new( self, col_letter( index ) )
|
194
|
-
end
|
195
|
-
|
196
|
-
#
|
197
|
-
# Access a Column (Section) by its header.
|
198
|
-
#
|
199
|
-
# @param [String] header the Column header
|
200
|
-
# @return [RubyExcel::Column]
|
201
|
-
#
|
202
|
-
|
203
|
-
def column_by_header( header )
|
204
|
-
header.is_a?( Column ) ? header : Column.new( self, data.colref_by_header( header ) )
|
205
|
-
end
|
206
|
-
alias ch column_by_header
|
207
|
-
|
208
|
-
#
|
209
|
-
# Yields each Column to the block
|
210
|
-
#
|
211
|
-
# @param [String, Fixnum] start_column the Column to start looping from
|
212
|
-
# @param [String, Fixnum] end_column the Column to end the loop at
|
213
|
-
# @note Iterates to the last Column in the Sheet unless given a second argument.
|
214
|
-
#
|
215
|
-
|
216
|
-
def columns( start_column = 'A', end_column = data.cols )
|
217
|
-
return to_enum( :columns, start_column, end_column ) unless block_given?
|
218
|
-
( col_letter( start_column )..col_letter( end_column ) ).each { |idx| yield column( idx ) }
|
219
|
-
self
|
220
|
-
end
|
221
|
-
|
222
|
-
#
|
223
|
-
# Removes empty Columns and Rows
|
224
|
-
#
|
225
|
-
|
226
|
-
def compact!
|
227
|
-
data.compact!; self
|
228
|
-
end
|
229
|
-
|
230
|
-
#
|
231
|
-
# Removes Sheet from the parent Workbook
|
232
|
-
#
|
233
|
-
|
234
|
-
def delete
|
235
|
-
workbook.delete self
|
236
|
-
end
|
237
|
-
|
238
|
-
#
|
239
|
-
# Deletes each Row where the block is true
|
240
|
-
#
|
241
|
-
|
242
|
-
def delete_rows_if
|
243
|
-
return to_enum( :delete_rows_if ) unless block_given?
|
244
|
-
rows.reverse_each { |r| r.delete if yield r }; self
|
245
|
-
end
|
246
|
-
|
247
|
-
#
|
248
|
-
# Deletes each Column where the block is true
|
249
|
-
#
|
250
|
-
|
251
|
-
def delete_columns_if
|
252
|
-
return to_enum( :delete_columns_if ) unless block_given?
|
253
|
-
columns.reverse_each { |c| c.delete if yield c }; self
|
254
|
-
end
|
255
|
-
|
256
|
-
#
|
257
|
-
# Return a copy of self
|
258
|
-
#
|
259
|
-
# @return [RubyExcel::Sheet]
|
260
|
-
#
|
261
|
-
|
262
|
-
def dup
|
263
|
-
s = Sheet.new( name, workbook )
|
264
|
-
d = data
|
265
|
-
unless d.nil?
|
266
|
-
d = d.dup
|
267
|
-
s.load( d.all, header_rows )
|
268
|
-
d.sheet = s
|
269
|
-
end
|
270
|
-
s
|
271
|
-
end
|
272
|
-
|
273
|
-
#
|
274
|
-
# Check whether the Sheet contains data (not counting headers)
|
275
|
-
#
|
276
|
-
# @return [Boolean] if there is any data
|
277
|
-
#
|
278
|
-
|
279
|
-
def empty?
|
280
|
-
data.empty?
|
281
|
-
end
|
282
|
-
|
283
|
-
#
|
284
|
-
# Export data to a specific WIN32OLE Excel Sheet
|
285
|
-
#
|
286
|
-
# @param win32ole_sheet the Sheet to export to
|
287
|
-
# @return WIN32OLE Sheet
|
288
|
-
#
|
289
|
-
|
290
|
-
def export( win32ole_sheet )
|
291
|
-
parent.dump_to_sheet( to_a, win32ole_sheet )
|
292
|
-
end
|
293
|
-
|
294
|
-
#
|
295
|
-
# Removes all Rows (omitting headers) where the block is falsey
|
296
|
-
#
|
297
|
-
# @param [String, Array] headers splat of the headers for the Columns to filter by
|
298
|
-
# @yield [Array] the values at the intersections of Column and Row
|
299
|
-
# @return [self]
|
300
|
-
#
|
301
|
-
|
302
|
-
def filter!( *headers, &block )
|
303
|
-
return to_enum( :filter!, headers ) unless block_given?
|
304
|
-
data.filter!( *headers, &block ); self
|
305
|
-
end
|
306
|
-
|
307
|
-
#
|
308
|
-
# Select and re-order Columns by a list of headers
|
309
|
-
#
|
310
|
-
# @param [Array<String>] headers the ordered list of headers to keep
|
311
|
-
# @note This method can accept either a list of arguments or an Array
|
312
|
-
# @note Invalid headers will be skipped
|
313
|
-
#
|
314
|
-
|
315
|
-
def get_columns!( *headers )
|
316
|
-
data.get_columns!( *headers ); self
|
317
|
-
end
|
318
|
-
alias gc! get_columns!
|
319
|
-
|
320
|
-
# @overload insert_columns( before, number=1 )
|
321
|
-
# Insert blank Columns into the data
|
322
|
-
#
|
323
|
-
# @param [String, Fixnum] before the Column reference to insert before.
|
324
|
-
# @param [Fixnum] number the number of new Columns to insert
|
325
|
-
#
|
326
|
-
|
327
|
-
def insert_columns( *args )
|
328
|
-
data.insert_columns( *args ); self
|
329
|
-
end
|
330
|
-
|
331
|
-
# @overload insert_rows( before, number=1 )
|
332
|
-
# Insert blank Rows into the data
|
333
|
-
#
|
334
|
-
# @param [Fixnum] before the Row index to insert before.
|
335
|
-
# @param [Fixnum] number the number of new Rows to insert
|
336
|
-
#
|
337
|
-
|
338
|
-
def insert_rows( *args )
|
339
|
-
data.insert_rows( *args ); self
|
340
|
-
end
|
341
|
-
|
342
|
-
#
|
343
|
-
# View the object for debugging
|
344
|
-
#
|
345
|
-
|
346
|
-
def inspect
|
347
|
-
"#{ self.class }:0x#{ '%x' % (object_id << 1) }: #{ name }"
|
348
|
-
end
|
349
|
-
|
350
|
-
#
|
351
|
-
# The last Column in the Sheet
|
352
|
-
#
|
353
|
-
# @return [RubyExcel::Column]
|
354
|
-
#
|
355
|
-
|
356
|
-
def last_column
|
357
|
-
column( maxcol )
|
358
|
-
end
|
359
|
-
alias last_col last_column
|
360
|
-
|
361
|
-
#
|
362
|
-
# The last Row in the Sheet
|
363
|
-
#
|
364
|
-
# @return [RubyExcel::Row]
|
365
|
-
#
|
366
|
-
|
367
|
-
def last_row
|
368
|
-
row( maxrow )
|
369
|
-
end
|
370
|
-
|
371
|
-
#
|
372
|
-
# Populate the Sheet with data (overwrite)
|
373
|
-
#
|
374
|
-
# @param [Array<Array>, Hash<Hash>] input_data the data to fill the Sheet with
|
375
|
-
# @param header_rows [Fixnum] the number of Rows to be treated as headers
|
376
|
-
#
|
377
|
-
|
378
|
-
def load( input_data, header_rows=1 )
|
379
|
-
input_data = _convert_hash(input_data) if input_data.is_a?(Hash)
|
380
|
-
input_data.is_a?(Array) or fail ArgumentError, 'Input must be an Array or Hash'
|
381
|
-
@header_rows = header_rows
|
382
|
-
@data = Data.new( self, input_data ); self
|
383
|
-
end
|
384
|
-
|
385
|
-
#
|
386
|
-
# Find the row number by looking up a value in a Column
|
387
|
-
#
|
388
|
-
# @param [String] header the header of the Column to pass to the block
|
389
|
-
# @yield yields each value in the Column to the block
|
390
|
-
# @return [Fixnum, nil] the row number of the first match or nil if nothing is found
|
391
|
-
#
|
392
|
-
|
393
|
-
def match( header, &block )
|
394
|
-
row_id( column_by_header( header ).find( &block ) ) rescue nil
|
395
|
-
end
|
396
|
-
|
397
|
-
#
|
398
|
-
# The highest currently used row number
|
399
|
-
#
|
400
|
-
|
401
|
-
def maxrow
|
402
|
-
data.rows
|
403
|
-
end
|
404
|
-
alias length maxrow
|
405
|
-
|
406
|
-
#
|
407
|
-
# The highest currently used column number
|
408
|
-
#
|
409
|
-
|
410
|
-
def maxcol
|
411
|
-
data.cols
|
412
|
-
end
|
413
|
-
alias maxcolumn maxcol
|
414
|
-
alias width maxcol
|
415
|
-
|
416
|
-
#
|
417
|
-
# Allow shorthand range references and non-bang versions of bang methods.
|
418
|
-
#
|
419
|
-
|
420
|
-
def method_missing(m, *args, &block)
|
421
|
-
method_name = m.to_s
|
422
|
-
|
423
|
-
if method_name[-1] != '!' && respond_to?( method_name + '!' )
|
424
|
-
|
425
|
-
dup.send( method_name + '!', *args, &block )
|
426
|
-
|
427
|
-
elsif method_name =~ /\A[A-Z]{1,3}\d+=?\z/i
|
428
|
-
|
429
|
-
method_name.upcase!
|
430
|
-
if method_name[-1] == '='
|
431
|
-
range( method_name.chop ).value = ( args.length == 1 ? args.first : args )
|
432
|
-
else
|
433
|
-
range( method_name ).value
|
434
|
-
end
|
435
|
-
|
436
|
-
else
|
437
|
-
super
|
438
|
-
end
|
439
|
-
end
|
440
|
-
|
441
|
-
#
|
442
|
-
# Allow for certain method_missing calls
|
443
|
-
#
|
444
|
-
|
445
|
-
def respond_to?( m, include_private = false )
|
446
|
-
|
447
|
-
if m[-1] != '!' && respond_to?( m.to_s + '!' )
|
448
|
-
true
|
449
|
-
elsif m.to_s.upcase.strip =~ /\A[A-Z]{1,3}\d+=?\z/
|
450
|
-
true
|
451
|
-
else
|
452
|
-
super
|
453
|
-
end
|
454
|
-
|
455
|
-
end
|
456
|
-
|
457
|
-
#
|
458
|
-
# Split the Sheet into two Sheets by evaluating each value in a column
|
459
|
-
#
|
460
|
-
# @param [String] header the header of the Column which contains the yield value
|
461
|
-
# @yield [value] yields the value of each row under the given header
|
462
|
-
# @return [Array<RubyExcel::Sheet, RubyExcel::Sheet>] Two Sheets: true and false. Headers included.
|
463
|
-
#
|
464
|
-
|
465
|
-
def partition( header, &block )
|
466
|
-
data.partition( header, &block ).map { |d| dup.load( d ) }
|
467
|
-
end
|
468
|
-
|
469
|
-
#
|
470
|
-
# Access a Range by address.
|
471
|
-
#
|
472
|
-
# @param [String, Cell, Range] first_cell the first Cell or Address in the Range
|
473
|
-
# @param [String, Cell, Range] last_cell the last Cell or Address in the Range
|
474
|
-
# @return [RubyExcel::Range]
|
475
|
-
# @note These are all valid arguments:
|
476
|
-
# ('A1')
|
477
|
-
# ('A1:B2')
|
478
|
-
# ('A:A')
|
479
|
-
# ('1:1')
|
480
|
-
# ('A1', 'B2')
|
481
|
-
# (cell1)
|
482
|
-
# (cell1, cell2)
|
483
|
-
#
|
484
|
-
|
485
|
-
def range( first_cell, last_cell=nil )
|
486
|
-
addr = to_range_address( first_cell, last_cell )
|
487
|
-
addr.include?(':') ? Range.new( self, addr ) : Cell.new( self, addr )
|
488
|
-
end
|
489
|
-
|
490
|
-
#
|
491
|
-
# Reverse the Sheet Columns
|
492
|
-
#
|
493
|
-
|
494
|
-
def reverse_columns!
|
495
|
-
data.reverse_columns!; self
|
496
|
-
end
|
497
|
-
|
498
|
-
#
|
499
|
-
# Reverse the Sheet Rows (without affecting the headers)
|
500
|
-
#
|
501
|
-
|
502
|
-
def reverse_rows!
|
503
|
-
data.reverse_rows!; self
|
504
|
-
end
|
505
|
-
alias reverse! reverse_rows!
|
506
|
-
|
507
|
-
#
|
508
|
-
# Create a Row from an index
|
509
|
-
#
|
510
|
-
# @param [Fixnum] index the Row index
|
511
|
-
# @return [RubyExcel::Row]
|
512
|
-
#
|
513
|
-
|
514
|
-
def row( index )
|
515
|
-
Row.new( self, index )
|
516
|
-
end
|
517
|
-
|
518
|
-
#
|
519
|
-
# Yields each Row to the block
|
520
|
-
#
|
521
|
-
# @param [Fixnum] start_row the Row to start looping from
|
522
|
-
# @param [Fixnum] end_row the Row to end the loop at
|
523
|
-
# @note Iterates to the last Row in the Sheet unless given a second argument.
|
524
|
-
#
|
525
|
-
|
526
|
-
def rows( start_row = 1, end_row = data.rows )
|
527
|
-
return to_enum(:rows, start_row, end_row) unless block_given?
|
528
|
-
( start_row..end_row ).each { |idx| yield row( idx ) }; self
|
529
|
-
end
|
530
|
-
alias each rows
|
531
|
-
|
532
|
-
#
|
533
|
-
# Save the RubyExcel::Sheet as an Excel Workbook
|
534
|
-
#
|
535
|
-
# @param [String] filename the filename to save as
|
536
|
-
# @param [Boolean] invisible leave Excel invisible if creating a new instance
|
537
|
-
# @return [WIN32OLE::Workbook] the Workbook, saved as filename.
|
538
|
-
#
|
539
|
-
|
540
|
-
def save_excel( filename = nil, invisible = false )
|
541
|
-
workbook.dup.clear_all.add( self.dup ).workbook.save_excel( filename, invisible )
|
542
|
-
end
|
543
|
-
|
544
|
-
#
|
545
|
-
# Sort the data by a column, selected by header(s)
|
546
|
-
#
|
547
|
-
# @param [String, Array<String>] headers the header(s) to sort the Sheet by
|
548
|
-
#
|
549
|
-
|
550
|
-
def sort_by!( *headers )
|
551
|
-
raise ArgumentError, 'Sheet#sort_by! does not support blocks.' if block_given?
|
552
|
-
idx_array = headers.flatten.map { |header| data.index_by_header( header ) - 1 }
|
553
|
-
sort_method = lambda { |array| idx_array.map { |idx| array[idx] } }
|
554
|
-
data.sort_by!( &sort_method )
|
555
|
-
self
|
556
|
-
rescue ArgumentError => err
|
557
|
-
raise( NoMethodError, 'Item not comparable in "' + headers.flatten.map(&:to_s).join(', ') + '"' ) if err.message == 'comparison of Array with Array failed'
|
558
|
-
raise err
|
559
|
-
end
|
560
|
-
|
561
|
-
#
|
562
|
-
# Break the Sheet into a Workbook with multiple Sheets, split by the values under a header.
|
563
|
-
#
|
564
|
-
# @param [String] header the header to split by
|
565
|
-
# @return [RubyExcel::Workbook] a new workbook containing the split Sheets (each with headers)
|
566
|
-
#
|
567
|
-
|
568
|
-
def split( header )
|
569
|
-
wb = Workbook.new
|
570
|
-
ch( header ).each_wh.to_a.uniq.each { |name| wb.add( name ).load( data.headers ) }
|
571
|
-
rows( header_rows+1 ) do |row|
|
572
|
-
wb.sheets( row.val( header ) ) << row
|
573
|
-
end
|
574
|
-
wb
|
575
|
-
end
|
576
|
-
|
577
|
-
#
|
578
|
-
# Sum the values in a Column by searching another Column
|
579
|
-
#
|
580
|
-
# @param [String] find_header the header of the Column to yield to the block
|
581
|
-
# @param [String] sum_header the header of the Column to sum
|
582
|
-
# @yield yields the find_header column values to the block
|
583
|
-
#
|
584
|
-
|
585
|
-
def sumif( find_header, sum_header )
|
586
|
-
return to_enum( :sumif ) unless block_given?
|
587
|
-
find_col, sum_col = ch( find_header ), ch( sum_header )
|
588
|
-
find_col.each_cell.inject(0) { |sum,ce| yield( ce.value ) && ce.row > header_rows ? sum + sum_col[ ce.row ] : sum }
|
589
|
-
end
|
590
|
-
|
591
|
-
#
|
592
|
-
# Return a Hash containing the Column values and the number of times each appears.
|
593
|
-
#
|
594
|
-
# @param [String] header the header of the Column to summarise
|
595
|
-
# @return [Hash]
|
596
|
-
#
|
597
|
-
|
598
|
-
def summarise( header )
|
599
|
-
ch( header ).summarise
|
600
|
-
end
|
601
|
-
alias summarize summarise
|
602
|
-
|
603
|
-
#
|
604
|
-
# Overwrite the sheet with the Summary of a Column
|
605
|
-
#
|
606
|
-
# @param [String] header the header of the Column to summarise
|
607
|
-
#
|
608
|
-
|
609
|
-
def summarise!( header )
|
610
|
-
load( summarise( header ).to_a.unshift [ header, 'Count' ] )
|
611
|
-
end
|
612
|
-
alias summarize! summarise!
|
613
|
-
|
614
|
-
#
|
615
|
-
# The Sheet as a 2D Array
|
616
|
-
#
|
617
|
-
|
618
|
-
def to_a
|
619
|
-
data.all
|
620
|
-
end
|
621
|
-
|
622
|
-
#
|
623
|
-
# The Sheet as a CSV String
|
624
|
-
#
|
625
|
-
|
626
|
-
def to_csv
|
627
|
-
CSV.generate { |csv| to_a.each { |r| csv << r } }
|
628
|
-
end
|
629
|
-
|
630
|
-
#
|
631
|
-
# The Sheet as a WIN32OLE Excel Workbook
|
632
|
-
# @note This requires Windows and MS Excel
|
633
|
-
#
|
634
|
-
|
635
|
-
def to_excel
|
636
|
-
workbook.dup.clear_all.add( self.dup ).workbook.to_excel
|
637
|
-
end
|
638
|
-
|
639
|
-
#
|
640
|
-
# The Sheet as a String containing an HTML Table
|
641
|
-
#
|
642
|
-
|
643
|
-
def to_html
|
644
|
-
%Q|<table border=1>\n<caption>#@name</caption>\n| + data.map { |row| '<tr>' + row.map { |v| '<td>' + CGI.escapeHTML(v.to_s) }.join }.join("\n") + "\n</table>"
|
645
|
-
end
|
646
|
-
|
647
|
-
#
|
648
|
-
# The Sheet as a Tab Seperated Value String (Strips extra whitespace)
|
649
|
-
#
|
650
|
-
|
651
|
-
def to_s
|
652
|
-
data.map { |ar| ar.map { |v| v.to_s.gsub(/\t|\n|\r/,' ') }.join "\t" }.join( $/ )
|
653
|
-
end
|
654
|
-
|
655
|
-
# {Sheet#to_safe_format!}
|
656
|
-
|
657
|
-
def to_safe_format
|
658
|
-
dup.to_safe_format!
|
659
|
-
end
|
660
|
-
|
661
|
-
#
|
662
|
-
# Standardise the data for safe export to Excel.
|
663
|
-
# Set each cell contents to a string and remove leading equals signs.
|
664
|
-
#
|
665
|
-
|
666
|
-
def to_safe_format!
|
667
|
-
rows { |r| r.map! { |v|
|
668
|
-
if v.is_a?( String )
|
669
|
-
v[0] == '=' ? v.sub( /\A=/,"'=" ) : v
|
670
|
-
else
|
671
|
-
v.to_s
|
672
|
-
end
|
673
|
-
} }; self
|
674
|
-
end
|
675
|
-
|
676
|
-
#
|
677
|
-
# the Sheet as a TSV String
|
678
|
-
#
|
679
|
-
|
680
|
-
def to_tsv
|
681
|
-
CSV.generate( :col_sep => "\t" ) { |csv| to_a.each { |r| csv << r } }
|
682
|
-
end
|
683
|
-
|
684
|
-
#
|
685
|
-
# Remove any Rows with duplicate values within a Column
|
686
|
-
#
|
687
|
-
# @param [String] header the header of the Column to check for duplicates
|
688
|
-
#
|
689
|
-
|
690
|
-
def uniq!( header )
|
691
|
-
data.uniq!( header ); self
|
692
|
-
end
|
693
|
-
alias unique! uniq!
|
694
|
-
|
695
|
-
#
|
696
|
-
# Select the used Range in the Sheet
|
697
|
-
#
|
698
|
-
# @return [Range] the Sheet's contents in Range
|
699
|
-
#
|
700
|
-
|
701
|
-
def usedrange
|
702
|
-
raise NoMethodError, 'Sheet is empty' if empty?
|
703
|
-
Range.new( self, 'A1:' + indices_to_address( maxrow, maxcol ) )
|
704
|
-
end
|
705
|
-
|
706
|
-
#
|
707
|
-
# Find a value within a Column by searching another Column
|
708
|
-
#
|
709
|
-
# @param [String] find_header the header of the Column to search
|
710
|
-
# @param [String] return_header the header of the return value Column
|
711
|
-
# @yield the first matching value
|
712
|
-
#
|
713
|
-
|
714
|
-
def vlookup( find_header, return_header, &block )
|
715
|
-
find_col, return_col = ch( find_header ), ch( return_header )
|
716
|
-
return_col[ row_id( find_col.find( &block ) ) ] rescue nil
|
717
|
-
end
|
718
|
-
|
719
|
-
end # Sheet
|
720
|
-
|
1
|
+
#
|
2
|
+
# Namespace for all RubyExcel Classes and Modules
|
3
|
+
#
|
4
|
+
|
5
|
+
module RubyExcel
|
6
|
+
|
7
|
+
#
|
8
|
+
# The front-end class for data manipulation and output.
|
9
|
+
#
|
10
|
+
|
11
|
+
class Sheet
|
12
|
+
include Address
|
13
|
+
|
14
|
+
# The Data underlying the Sheet
|
15
|
+
attr_reader :data
|
16
|
+
|
17
|
+
# The name of the Sheet
|
18
|
+
attr_accessor :name
|
19
|
+
|
20
|
+
# The number of rows treated as headers
|
21
|
+
attr_accessor :header_rows
|
22
|
+
|
23
|
+
# The Workbook parent of this Sheet
|
24
|
+
attr_accessor :workbook
|
25
|
+
|
26
|
+
alias parent workbook; alias parent= workbook=
|
27
|
+
alias headers header_rows; alias headers= header_rows=
|
28
|
+
|
29
|
+
#
|
30
|
+
# Creates a RubyExcel::Sheet instance
|
31
|
+
#
|
32
|
+
# @param [String] name the name of the Sheet
|
33
|
+
# @param [RubyExcel::Workbook] workbook the Workbook which holds this Sheet
|
34
|
+
#
|
35
|
+
|
36
|
+
def initialize( name, workbook )
|
37
|
+
@workbook = workbook
|
38
|
+
@name = name
|
39
|
+
@header_rows = 1
|
40
|
+
@data = Data.new( self, [[]] )
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Read a value by address
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# sheet['A1']
|
48
|
+
# #=> "Part"
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# sheet['A1:B2']
|
52
|
+
# #=> [["Part", "Ref1"], ["Type1", "QT1"]]
|
53
|
+
#
|
54
|
+
# @param [String] addr the address to access
|
55
|
+
#
|
56
|
+
|
57
|
+
def[]( addr )
|
58
|
+
range( addr ).value
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Write a value by address
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# sheet['A1'] = "Bart"
|
66
|
+
# sheet['A1']
|
67
|
+
# #=> "Bart"
|
68
|
+
#
|
69
|
+
# @param (see #[])
|
70
|
+
# @param [Object] val the value to write into the data
|
71
|
+
#
|
72
|
+
|
73
|
+
def []=( addr, val )
|
74
|
+
range( addr ).value = val
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Add data with the Sheet
|
79
|
+
#
|
80
|
+
# @param [Array<Array>, Hash<Hash>, RubyExcel::Sheet] other the data to add
|
81
|
+
# @return [RubyExcel::Sheet] returns a new Sheet
|
82
|
+
# @note When adding another Sheet it won't import the headers unless this Sheet is empty.
|
83
|
+
#
|
84
|
+
|
85
|
+
def +( other )
|
86
|
+
dup << other
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Subtract data from the Sheet
|
91
|
+
#
|
92
|
+
# @param [Array<Array>, RubyExcel::Sheet] other the data to subtract
|
93
|
+
# @return [RubyExcel::Sheet] returns a new Sheet
|
94
|
+
#
|
95
|
+
|
96
|
+
def -( other )
|
97
|
+
case other
|
98
|
+
when Array ; Workbook.new.load( data.all - other )
|
99
|
+
when Sheet ; Workbook.new.load( data.all - other.data.no_headers )
|
100
|
+
else ; fail ArgumentError, "Unsupported class: #{ other.class }"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Append an object to the Sheet
|
106
|
+
#
|
107
|
+
# @param [Object] other the object to append
|
108
|
+
# @return [self]
|
109
|
+
# @note When adding another Sheet it won't import the headers unless this Sheet is empty.
|
110
|
+
# @note Anything other than an an Array, Hash, Row, Column or Sheet will be appended to the first row
|
111
|
+
#
|
112
|
+
|
113
|
+
def <<( other )
|
114
|
+
data << other
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
# @deprecated Please use {#filter!} instead
|
119
|
+
# @overload advanced_filter!( header, comparison_operator, search_criteria, ... )
|
120
|
+
# Filter on multiple criteria
|
121
|
+
# @param [String] header a header to search under
|
122
|
+
# @param [Symbol] comparison_operator the operator to compare with
|
123
|
+
# @param [Object] search_criteria the value to filter by
|
124
|
+
# @raise [ArgumentError] 'Number of arguments must be a multiple of 3'
|
125
|
+
# @raise [ArgumentError] 'Operator must be a symbol'
|
126
|
+
# @example Filter to 'Part': 'Type1' and 'Type3', with Qty greater than 1
|
127
|
+
# s.advanced_filter!( 'Part', :=~, /Type[13]/, 'Qty', :>, 1 )
|
128
|
+
# @example Filter to 'Part': 'Type1', with 'Ref1' containing 'X'
|
129
|
+
# s.advanced_filter!( 'Part', :==, 'Type1', 'Ref1', :include?, 'X' )
|
130
|
+
#
|
131
|
+
|
132
|
+
def advanced_filter!( *args )
|
133
|
+
warn "[DEPRECATION] `advanced_filter!` is deprecated. Please use `filter!` instead."
|
134
|
+
data.advanced_filter!( *args ); self
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# Average the values in a Column by searching another Column
|
139
|
+
#
|
140
|
+
# @param [String] find_header the header of the Column to yield to the block
|
141
|
+
# @param [String] avg_header the header of the Column to average
|
142
|
+
# @yield yields the find_header column values to the block
|
143
|
+
#
|
144
|
+
|
145
|
+
def averageif( find_header, avg_header )
|
146
|
+
return to_enum( :sumif ) unless block_given?
|
147
|
+
find_col, avg_col = ch( find_header ), ch( avg_header )
|
148
|
+
sum = find_col.each_cell_wh.inject([0,0]) do |sum,ce|
|
149
|
+
if yield( ce.value )
|
150
|
+
sum[0] += avg_col[ ce.row ]
|
151
|
+
sum[1] += 1
|
152
|
+
sum
|
153
|
+
else
|
154
|
+
sum
|
155
|
+
end
|
156
|
+
end
|
157
|
+
sum.first.to_f / sum.last
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# Access an Cell by indices.
|
162
|
+
#
|
163
|
+
# @param [Fixnum] row the row index
|
164
|
+
# @param [Fixnum] col the column index
|
165
|
+
# @return [RubyExcel::Cell]
|
166
|
+
# @note Indexing is 1-based like Excel VBA
|
167
|
+
#
|
168
|
+
|
169
|
+
def cell( row, col )
|
170
|
+
Cell.new( self, indices_to_address( row, col ) )
|
171
|
+
end
|
172
|
+
alias cells cell
|
173
|
+
|
174
|
+
#
|
175
|
+
# Delete all data and headers from Sheet
|
176
|
+
#
|
177
|
+
|
178
|
+
def clear_all
|
179
|
+
data.delete_all
|
180
|
+
self
|
181
|
+
end
|
182
|
+
alias delete_all clear_all
|
183
|
+
|
184
|
+
#
|
185
|
+
# Access a Column (Section) by its reference.
|
186
|
+
#
|
187
|
+
# @param [String, Fixnum] index the Column reference
|
188
|
+
# @return [RubyExcel::Column]
|
189
|
+
# @note Index 'A' and 1 both select the 1st Column
|
190
|
+
#
|
191
|
+
|
192
|
+
def column( index )
|
193
|
+
Column.new( self, col_letter( index ) )
|
194
|
+
end
|
195
|
+
|
196
|
+
#
|
197
|
+
# Access a Column (Section) by its header.
|
198
|
+
#
|
199
|
+
# @param [String] header the Column header
|
200
|
+
# @return [RubyExcel::Column]
|
201
|
+
#
|
202
|
+
|
203
|
+
def column_by_header( header )
|
204
|
+
header.is_a?( Column ) ? header : Column.new( self, data.colref_by_header( header ) )
|
205
|
+
end
|
206
|
+
alias ch column_by_header
|
207
|
+
|
208
|
+
#
|
209
|
+
# Yields each Column to the block
|
210
|
+
#
|
211
|
+
# @param [String, Fixnum] start_column the Column to start looping from
|
212
|
+
# @param [String, Fixnum] end_column the Column to end the loop at
|
213
|
+
# @note Iterates to the last Column in the Sheet unless given a second argument.
|
214
|
+
#
|
215
|
+
|
216
|
+
def columns( start_column = 'A', end_column = data.cols )
|
217
|
+
return to_enum( :columns, start_column, end_column ) unless block_given?
|
218
|
+
( col_letter( start_column )..col_letter( end_column ) ).each { |idx| yield column( idx ) }
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
#
|
223
|
+
# Removes empty Columns and Rows
|
224
|
+
#
|
225
|
+
|
226
|
+
def compact!
|
227
|
+
data.compact!; self
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# Removes Sheet from the parent Workbook
|
232
|
+
#
|
233
|
+
|
234
|
+
def delete
|
235
|
+
workbook.delete self
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
# Deletes each Row where the block is true
|
240
|
+
#
|
241
|
+
|
242
|
+
def delete_rows_if
|
243
|
+
return to_enum( :delete_rows_if ) unless block_given?
|
244
|
+
rows.reverse_each { |r| r.delete if yield r }; self
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Deletes each Column where the block is true
|
249
|
+
#
|
250
|
+
|
251
|
+
def delete_columns_if
|
252
|
+
return to_enum( :delete_columns_if ) unless block_given?
|
253
|
+
columns.reverse_each { |c| c.delete if yield c }; self
|
254
|
+
end
|
255
|
+
|
256
|
+
#
|
257
|
+
# Return a copy of self
|
258
|
+
#
|
259
|
+
# @return [RubyExcel::Sheet]
|
260
|
+
#
|
261
|
+
|
262
|
+
def dup
|
263
|
+
s = Sheet.new( name, workbook )
|
264
|
+
d = data
|
265
|
+
unless d.nil?
|
266
|
+
d = d.dup
|
267
|
+
s.load( d.all, header_rows )
|
268
|
+
d.sheet = s
|
269
|
+
end
|
270
|
+
s
|
271
|
+
end
|
272
|
+
|
273
|
+
#
|
274
|
+
# Check whether the Sheet contains data (not counting headers)
|
275
|
+
#
|
276
|
+
# @return [Boolean] if there is any data
|
277
|
+
#
|
278
|
+
|
279
|
+
def empty?
|
280
|
+
data.empty?
|
281
|
+
end
|
282
|
+
|
283
|
+
#
|
284
|
+
# Export data to a specific WIN32OLE Excel Sheet
|
285
|
+
#
|
286
|
+
# @param win32ole_sheet the Sheet to export to
|
287
|
+
# @return WIN32OLE Sheet
|
288
|
+
#
|
289
|
+
|
290
|
+
def export( win32ole_sheet )
|
291
|
+
parent.dump_to_sheet( to_a, win32ole_sheet )
|
292
|
+
end
|
293
|
+
|
294
|
+
#
|
295
|
+
# Removes all Rows (omitting headers) where the block is falsey
|
296
|
+
#
|
297
|
+
# @param [String, Array] headers splat of the headers for the Columns to filter by
|
298
|
+
# @yield [Array] the values at the intersections of Column and Row
|
299
|
+
# @return [self]
|
300
|
+
#
|
301
|
+
|
302
|
+
def filter!( *headers, &block )
|
303
|
+
return to_enum( :filter!, headers ) unless block_given?
|
304
|
+
data.filter!( *headers, &block ); self
|
305
|
+
end
|
306
|
+
|
307
|
+
#
|
308
|
+
# Select and re-order Columns by a list of headers
|
309
|
+
#
|
310
|
+
# @param [Array<String>] headers the ordered list of headers to keep
|
311
|
+
# @note This method can accept either a list of arguments or an Array
|
312
|
+
# @note Invalid headers will be skipped
|
313
|
+
#
|
314
|
+
|
315
|
+
def get_columns!( *headers )
|
316
|
+
data.get_columns!( *headers ); self
|
317
|
+
end
|
318
|
+
alias gc! get_columns!
|
319
|
+
|
320
|
+
# @overload insert_columns( before, number=1 )
|
321
|
+
# Insert blank Columns into the data
|
322
|
+
#
|
323
|
+
# @param [String, Fixnum] before the Column reference to insert before.
|
324
|
+
# @param [Fixnum] number the number of new Columns to insert
|
325
|
+
#
|
326
|
+
|
327
|
+
def insert_columns( *args )
|
328
|
+
data.insert_columns( *args ); self
|
329
|
+
end
|
330
|
+
|
331
|
+
# @overload insert_rows( before, number=1 )
|
332
|
+
# Insert blank Rows into the data
|
333
|
+
#
|
334
|
+
# @param [Fixnum] before the Row index to insert before.
|
335
|
+
# @param [Fixnum] number the number of new Rows to insert
|
336
|
+
#
|
337
|
+
|
338
|
+
def insert_rows( *args )
|
339
|
+
data.insert_rows( *args ); self
|
340
|
+
end
|
341
|
+
|
342
|
+
#
|
343
|
+
# View the object for debugging
|
344
|
+
#
|
345
|
+
|
346
|
+
def inspect
|
347
|
+
"#{ self.class }:0x#{ '%x' % (object_id << 1) }: #{ name }"
|
348
|
+
end
|
349
|
+
|
350
|
+
#
|
351
|
+
# The last Column in the Sheet
|
352
|
+
#
|
353
|
+
# @return [RubyExcel::Column]
|
354
|
+
#
|
355
|
+
|
356
|
+
def last_column
|
357
|
+
column( maxcol )
|
358
|
+
end
|
359
|
+
alias last_col last_column
|
360
|
+
|
361
|
+
#
|
362
|
+
# The last Row in the Sheet
|
363
|
+
#
|
364
|
+
# @return [RubyExcel::Row]
|
365
|
+
#
|
366
|
+
|
367
|
+
def last_row
|
368
|
+
row( maxrow )
|
369
|
+
end
|
370
|
+
|
371
|
+
#
|
372
|
+
# Populate the Sheet with data (overwrite)
|
373
|
+
#
|
374
|
+
# @param [Array<Array>, Hash<Hash>] input_data the data to fill the Sheet with
|
375
|
+
# @param header_rows [Fixnum] the number of Rows to be treated as headers
|
376
|
+
#
|
377
|
+
|
378
|
+
def load( input_data, header_rows=1 )
|
379
|
+
input_data = _convert_hash(input_data) if input_data.is_a?(Hash)
|
380
|
+
input_data.is_a?(Array) or fail ArgumentError, 'Input must be an Array or Hash'
|
381
|
+
@header_rows = header_rows
|
382
|
+
@data = Data.new( self, input_data ); self
|
383
|
+
end
|
384
|
+
|
385
|
+
#
|
386
|
+
# Find the row number by looking up a value in a Column
|
387
|
+
#
|
388
|
+
# @param [String] header the header of the Column to pass to the block
|
389
|
+
# @yield yields each value in the Column to the block
|
390
|
+
# @return [Fixnum, nil] the row number of the first match or nil if nothing is found
|
391
|
+
#
|
392
|
+
|
393
|
+
def match( header, &block )
|
394
|
+
row_id( column_by_header( header ).find( &block ) ) rescue nil
|
395
|
+
end
|
396
|
+
|
397
|
+
#
|
398
|
+
# The highest currently used row number
|
399
|
+
#
|
400
|
+
|
401
|
+
def maxrow
|
402
|
+
data.rows
|
403
|
+
end
|
404
|
+
alias length maxrow
|
405
|
+
|
406
|
+
#
|
407
|
+
# The highest currently used column number
|
408
|
+
#
|
409
|
+
|
410
|
+
def maxcol
|
411
|
+
data.cols
|
412
|
+
end
|
413
|
+
alias maxcolumn maxcol
|
414
|
+
alias width maxcol
|
415
|
+
|
416
|
+
#
|
417
|
+
# Allow shorthand range references and non-bang versions of bang methods.
|
418
|
+
#
|
419
|
+
|
420
|
+
def method_missing(m, *args, &block)
|
421
|
+
method_name = m.to_s
|
422
|
+
|
423
|
+
if method_name[-1] != '!' && respond_to?( method_name + '!' )
|
424
|
+
|
425
|
+
dup.send( method_name + '!', *args, &block )
|
426
|
+
|
427
|
+
elsif method_name =~ /\A[A-Z]{1,3}\d+=?\z/i
|
428
|
+
|
429
|
+
method_name.upcase!
|
430
|
+
if method_name[-1] == '='
|
431
|
+
range( method_name.chop ).value = ( args.length == 1 ? args.first : args )
|
432
|
+
else
|
433
|
+
range( method_name ).value
|
434
|
+
end
|
435
|
+
|
436
|
+
else
|
437
|
+
super
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
#
|
442
|
+
# Allow for certain method_missing calls
|
443
|
+
#
|
444
|
+
|
445
|
+
def respond_to?( m, include_private = false )
|
446
|
+
|
447
|
+
if m[-1] != '!' && respond_to?( m.to_s + '!' )
|
448
|
+
true
|
449
|
+
elsif m.to_s.upcase.strip =~ /\A[A-Z]{1,3}\d+=?\z/
|
450
|
+
true
|
451
|
+
else
|
452
|
+
super
|
453
|
+
end
|
454
|
+
|
455
|
+
end
|
456
|
+
|
457
|
+
#
|
458
|
+
# Split the Sheet into two Sheets by evaluating each value in a column
|
459
|
+
#
|
460
|
+
# @param [String] header the header of the Column which contains the yield value
|
461
|
+
# @yield [value] yields the value of each row under the given header
|
462
|
+
# @return [Array<RubyExcel::Sheet, RubyExcel::Sheet>] Two Sheets: true and false. Headers included.
|
463
|
+
#
|
464
|
+
|
465
|
+
def partition( header, &block )
|
466
|
+
data.partition( header, &block ).map { |d| dup.load( d ) }
|
467
|
+
end
|
468
|
+
|
469
|
+
#
|
470
|
+
# Access a Range by address.
|
471
|
+
#
|
472
|
+
# @param [String, Cell, Range] first_cell the first Cell or Address in the Range
|
473
|
+
# @param [String, Cell, Range] last_cell the last Cell or Address in the Range
|
474
|
+
# @return [RubyExcel::Range]
|
475
|
+
# @note These are all valid arguments:
|
476
|
+
# ('A1')
|
477
|
+
# ('A1:B2')
|
478
|
+
# ('A:A')
|
479
|
+
# ('1:1')
|
480
|
+
# ('A1', 'B2')
|
481
|
+
# (cell1)
|
482
|
+
# (cell1, cell2)
|
483
|
+
#
|
484
|
+
|
485
|
+
def range( first_cell, last_cell=nil )
|
486
|
+
addr = to_range_address( first_cell, last_cell )
|
487
|
+
addr.include?(':') ? Range.new( self, addr ) : Cell.new( self, addr )
|
488
|
+
end
|
489
|
+
|
490
|
+
#
|
491
|
+
# Reverse the Sheet Columns
|
492
|
+
#
|
493
|
+
|
494
|
+
def reverse_columns!
|
495
|
+
data.reverse_columns!; self
|
496
|
+
end
|
497
|
+
|
498
|
+
#
|
499
|
+
# Reverse the Sheet Rows (without affecting the headers)
|
500
|
+
#
|
501
|
+
|
502
|
+
def reverse_rows!
|
503
|
+
data.reverse_rows!; self
|
504
|
+
end
|
505
|
+
alias reverse! reverse_rows!
|
506
|
+
|
507
|
+
#
|
508
|
+
# Create a Row from an index
|
509
|
+
#
|
510
|
+
# @param [Fixnum] index the Row index
|
511
|
+
# @return [RubyExcel::Row]
|
512
|
+
#
|
513
|
+
|
514
|
+
def row( index )
|
515
|
+
Row.new( self, index )
|
516
|
+
end
|
517
|
+
|
518
|
+
#
|
519
|
+
# Yields each Row to the block
|
520
|
+
#
|
521
|
+
# @param [Fixnum] start_row the Row to start looping from
|
522
|
+
# @param [Fixnum] end_row the Row to end the loop at
|
523
|
+
# @note Iterates to the last Row in the Sheet unless given a second argument.
|
524
|
+
#
|
525
|
+
|
526
|
+
def rows( start_row = 1, end_row = data.rows )
|
527
|
+
return to_enum(:rows, start_row, end_row) unless block_given?
|
528
|
+
( start_row..end_row ).each { |idx| yield row( idx ) }; self
|
529
|
+
end
|
530
|
+
alias each rows
|
531
|
+
|
532
|
+
#
|
533
|
+
# Save the RubyExcel::Sheet as an Excel Workbook
|
534
|
+
#
|
535
|
+
# @param [String] filename the filename to save as
|
536
|
+
# @param [Boolean] invisible leave Excel invisible if creating a new instance
|
537
|
+
# @return [WIN32OLE::Workbook] the Workbook, saved as filename.
|
538
|
+
#
|
539
|
+
|
540
|
+
def save_excel( filename = nil, invisible = false )
|
541
|
+
workbook.dup.clear_all.add( self.dup ).workbook.save_excel( filename, invisible )
|
542
|
+
end
|
543
|
+
|
544
|
+
#
|
545
|
+
# Sort the data by a column, selected by header(s)
|
546
|
+
#
|
547
|
+
# @param [String, Array<String>] headers the header(s) to sort the Sheet by
|
548
|
+
#
|
549
|
+
|
550
|
+
def sort_by!( *headers )
|
551
|
+
raise ArgumentError, 'Sheet#sort_by! does not support blocks.' if block_given?
|
552
|
+
idx_array = headers.flatten.map { |header| data.index_by_header( header ) - 1 }
|
553
|
+
sort_method = lambda { |array| idx_array.map { |idx| array[idx] } }
|
554
|
+
data.sort_by!( &sort_method )
|
555
|
+
self
|
556
|
+
rescue ArgumentError => err
|
557
|
+
raise( NoMethodError, 'Item not comparable in "' + headers.flatten.map(&:to_s).join(', ') + '"' ) if err.message == 'comparison of Array with Array failed'
|
558
|
+
raise err
|
559
|
+
end
|
560
|
+
|
561
|
+
#
|
562
|
+
# Break the Sheet into a Workbook with multiple Sheets, split by the values under a header.
|
563
|
+
#
|
564
|
+
# @param [String] header the header to split by
|
565
|
+
# @return [RubyExcel::Workbook] a new workbook containing the split Sheets (each with headers)
|
566
|
+
#
|
567
|
+
|
568
|
+
def split( header )
|
569
|
+
wb = Workbook.new
|
570
|
+
ch( header ).each_wh.to_a.uniq.each { |name| wb.add( name ).load( data.headers ) }
|
571
|
+
rows( header_rows+1 ) do |row|
|
572
|
+
wb.sheets( row.val( header ) ) << row
|
573
|
+
end
|
574
|
+
wb
|
575
|
+
end
|
576
|
+
|
577
|
+
#
|
578
|
+
# Sum the values in a Column by searching another Column
|
579
|
+
#
|
580
|
+
# @param [String] find_header the header of the Column to yield to the block
|
581
|
+
# @param [String] sum_header the header of the Column to sum
|
582
|
+
# @yield yields the find_header column values to the block
|
583
|
+
#
|
584
|
+
|
585
|
+
def sumif( find_header, sum_header )
|
586
|
+
return to_enum( :sumif ) unless block_given?
|
587
|
+
find_col, sum_col = ch( find_header ), ch( sum_header )
|
588
|
+
find_col.each_cell.inject(0) { |sum,ce| yield( ce.value ) && ce.row > header_rows ? sum + sum_col[ ce.row ] : sum }
|
589
|
+
end
|
590
|
+
|
591
|
+
#
|
592
|
+
# Return a Hash containing the Column values and the number of times each appears.
|
593
|
+
#
|
594
|
+
# @param [String] header the header of the Column to summarise
|
595
|
+
# @return [Hash]
|
596
|
+
#
|
597
|
+
|
598
|
+
def summarise( header )
|
599
|
+
ch( header ).summarise
|
600
|
+
end
|
601
|
+
alias summarize summarise
|
602
|
+
|
603
|
+
#
|
604
|
+
# Overwrite the sheet with the Summary of a Column
|
605
|
+
#
|
606
|
+
# @param [String] header the header of the Column to summarise
|
607
|
+
#
|
608
|
+
|
609
|
+
def summarise!( header )
|
610
|
+
load( summarise( header ).to_a.unshift [ header, 'Count' ] )
|
611
|
+
end
|
612
|
+
alias summarize! summarise!
|
613
|
+
|
614
|
+
#
|
615
|
+
# The Sheet as a 2D Array
|
616
|
+
#
|
617
|
+
|
618
|
+
def to_a
|
619
|
+
data.all
|
620
|
+
end
|
621
|
+
|
622
|
+
#
|
623
|
+
# The Sheet as a CSV String
|
624
|
+
#
|
625
|
+
|
626
|
+
def to_csv
|
627
|
+
CSV.generate { |csv| to_a.each { |r| csv << r } }
|
628
|
+
end
|
629
|
+
|
630
|
+
#
|
631
|
+
# The Sheet as a WIN32OLE Excel Workbook
|
632
|
+
# @note This requires Windows and MS Excel
|
633
|
+
#
|
634
|
+
|
635
|
+
def to_excel
|
636
|
+
workbook.dup.clear_all.add( self.dup ).workbook.to_excel
|
637
|
+
end
|
638
|
+
|
639
|
+
#
|
640
|
+
# The Sheet as a String containing an HTML Table
|
641
|
+
#
|
642
|
+
|
643
|
+
def to_html
|
644
|
+
%Q|<table border=1>\n<caption>#@name</caption>\n| + data.map { |row| '<tr>' + row.map { |v| '<td>' + CGI.escapeHTML(v.to_s) }.join }.join("\n") + "\n</table>"
|
645
|
+
end
|
646
|
+
|
647
|
+
#
|
648
|
+
# The Sheet as a Tab Seperated Value String (Strips extra whitespace)
|
649
|
+
#
|
650
|
+
|
651
|
+
def to_s
|
652
|
+
data.map { |ar| ar.map { |v| v.to_s.gsub(/\t|\n|\r/,' ') }.join "\t" }.join( $/ )
|
653
|
+
end
|
654
|
+
|
655
|
+
# {Sheet#to_safe_format!}
|
656
|
+
|
657
|
+
def to_safe_format
|
658
|
+
dup.to_safe_format!
|
659
|
+
end
|
660
|
+
|
661
|
+
#
|
662
|
+
# Standardise the data for safe export to Excel.
|
663
|
+
# Set each cell contents to a string and remove leading equals signs.
|
664
|
+
#
|
665
|
+
|
666
|
+
def to_safe_format!
|
667
|
+
rows { |r| r.map! { |v|
|
668
|
+
if v.is_a?( String )
|
669
|
+
v[0] == '=' ? v.sub( /\A=/,"'=" ) : v
|
670
|
+
else
|
671
|
+
v.to_s
|
672
|
+
end
|
673
|
+
} }; self
|
674
|
+
end
|
675
|
+
|
676
|
+
#
|
677
|
+
# the Sheet as a TSV String
|
678
|
+
#
|
679
|
+
|
680
|
+
def to_tsv
|
681
|
+
CSV.generate( :col_sep => "\t" ) { |csv| to_a.each { |r| csv << r } }
|
682
|
+
end
|
683
|
+
|
684
|
+
#
|
685
|
+
# Remove any Rows with duplicate values within a Column
|
686
|
+
#
|
687
|
+
# @param [String] header the header of the Column to check for duplicates
|
688
|
+
#
|
689
|
+
|
690
|
+
def uniq!( header )
|
691
|
+
data.uniq!( header ); self
|
692
|
+
end
|
693
|
+
alias unique! uniq!
|
694
|
+
|
695
|
+
#
|
696
|
+
# Select the used Range in the Sheet
|
697
|
+
#
|
698
|
+
# @return [Range] the Sheet's contents in Range
|
699
|
+
#
|
700
|
+
|
701
|
+
def usedrange
|
702
|
+
raise NoMethodError, 'Sheet is empty' if empty?
|
703
|
+
Range.new( self, 'A1:' + indices_to_address( maxrow, maxcol ) )
|
704
|
+
end
|
705
|
+
|
706
|
+
#
|
707
|
+
# Find a value within a Column by searching another Column
|
708
|
+
#
|
709
|
+
# @param [String] find_header the header of the Column to search
|
710
|
+
# @param [String] return_header the header of the return value Column
|
711
|
+
# @yield the first matching value
|
712
|
+
#
|
713
|
+
|
714
|
+
def vlookup( find_header, return_header, &block )
|
715
|
+
find_col, return_col = ch( find_header ), ch( return_header )
|
716
|
+
return_col[ row_id( find_col.find( &block ) ) ] rescue nil
|
717
|
+
end
|
718
|
+
|
719
|
+
end # Sheet
|
720
|
+
|
721
721
|
end # RubyExcel
|