rubyexcel 0.3.9 → 0.4.0

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