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.
@@ -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