rubyexcel 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/README.md CHANGED
@@ -137,20 +137,28 @@ Common Operations
137
137
  --------
138
138
 
139
139
  ```ruby
140
+ #Some data to play with
141
+ s = RubyExcel.sample_sheet
142
+
143
+ #Have a look at the data
144
+ puts s
145
+
140
146
  #Append a Column by adding a header
141
- s << 'Numbers'
142
- x = 1
147
+ s << 'Number'
143
148
 
144
149
  #Iterate through the rest of the rows while appending data
150
+ x = 1
145
151
  s.rows(2) { |row| row << x; x+=1 }
146
152
 
147
- #
153
+ #Filter to specific part numbers
154
+ s.filter!( 'Part', &/Type[1-3]/ )
155
+
156
+ #Add the Number to the Cost in each row.
157
+ s.rows(2) { |row| row.cell_h('Cost').value += row.cell_h('Number').value }
148
158
 
149
159
  #Split the data into multiple sheets by part number
150
160
  wb = s.split( 'Part' )
151
161
 
152
- #Will add more examples here later.
153
-
154
162
  ```
155
163
 
156
164
  Workbook
@@ -284,9 +292,8 @@ type_1_and_3, other = s.partition( 'Part', &/Type[13]/ )
284
292
  s.reverse_rows!
285
293
  s.reverse_columns!
286
294
 
287
- #Sort the rows by criteria (ignores headers)
288
- s.sort! { |r1,r2| r1['A'] <=> r2['A'] }
289
- s.sort_by! { |r| r['A'] }
295
+ #Sort the rows by a header (ignores header rows)
296
+ s.sort_by!( 'Part' )
290
297
 
291
298
  #Split a Sheet into a Workbook of Sheets by a column (selected by header)
292
299
  wb = s.split( 'Part' )
@@ -334,6 +341,10 @@ s.rows(2) { |r| r << 'Column' }
334
341
  s.column(1) << 'New'
335
342
  s.columns(2) { |c| c << 'Row' }
336
343
 
344
+ #Access a cell by column header (Row only)
345
+ s.row(2).cell_by_header( 'Part' ) #=> Element A2
346
+ s.row(2).cell_h( 'Cost' ) #=> Element E2
347
+
337
348
  #Delete the data referenced by self.
338
349
  row.delete
339
350
  col.delete
@@ -1,7 +1,9 @@
1
1
  require_relative 'address.rb'
2
2
  require_relative 'data.rb'
3
3
  require_relative 'element.rb'
4
+ require_relative 'excel_tools.rb'
4
5
  require_relative 'section.rb'
6
+ require_relative 'sheet.rb'
5
7
 
6
8
  module RubyExcel
7
9
 
@@ -35,11 +35,7 @@ module RubyExcel
35
35
  #
36
36
 
37
37
  def <<( value )
38
- case self
39
- when Row ; lastone = ( idx == 1 ? data.cols + 1 : data.cols )
40
- else ; lastone = ( idx == 'A' ? data.rows + 1 : data.rows )
41
- end
42
- data[ translate_address( lastone ) ] = value
38
+ data[ translate_address( ( col_index( idx ) == 1 ? data.cols + 1 : data.cols ) ) ] = value
43
39
  end
44
40
 
45
41
  #
@@ -58,7 +54,7 @@ module RubyExcel
58
54
  data.delete( self ); self
59
55
  end
60
56
 
61
- #
57
+ #
62
58
  # Yields each value
63
59
  #
64
60
 
@@ -73,7 +69,7 @@ module RubyExcel
73
69
 
74
70
  def each_without_headers
75
71
  return to_enum( :each_without_headers ) unless block_given?
76
- each_address_without_headers { |addr| yield data[ addr ] }
72
+ each_address( false ) { |addr| yield data[ addr ] }
77
73
  end
78
74
  alias each_wh each_without_headers
79
75
 
@@ -92,7 +88,7 @@ module RubyExcel
92
88
 
93
89
  def each_cell_without_headers
94
90
  return to_enum( :each_cell_without_headers ) unless block_given?
95
- each_address_without_headers { |addr| yield Element.new( sheet, addr ) }
91
+ each_address( false ) { |addr| yield Element.new( sheet, addr ) }
96
92
  end
97
93
  alias each_cell_wh each_cell_without_headers
98
94
 
@@ -149,7 +145,7 @@ module RubyExcel
149
145
 
150
146
  def map_without_headers!
151
147
  return to_enum( :map_without_headers! ) unless block_given?
152
- each_address_without_headers { |addr| data[addr] = ( yield data[addr] ) }
148
+ each_address( false ) { |addr| data[addr] = ( yield data[addr] ) }
153
149
  end
154
150
 
155
151
  #
@@ -194,19 +190,6 @@ module RubyExcel
194
190
  end
195
191
  alias []= write
196
192
 
197
- private
198
-
199
- def translate_address( addr )
200
- case self
201
- when Row
202
- col_letter( addr ) + idx.to_s
203
- when Column
204
- addr = addr.to_s unless addr.is_a?( String )
205
- fail ArgumentError, "Invalid address : #{ addr }" if addr =~ /[^\d]/
206
- idx + addr
207
- end
208
- end
209
-
210
193
  end
211
194
 
212
195
  #
@@ -252,12 +235,16 @@ module RubyExcel
252
235
 
253
236
  def getref( header )
254
237
  sheet.header_rows.times do |t|
255
- res = sheet.row( t + 1 ).find &/^#{header}$/
238
+ res = sheet.row( t + 1 ).find { |v| v == header }
256
239
  return column_id( res ) if res
257
240
  end
258
241
  fail ArgumentError, 'Invalid header: ' + header.to_s
259
242
  end
260
243
 
244
+ #
245
+ # The number of Columns in the Row
246
+ #
247
+
261
248
  def length
262
249
  data.cols
263
250
  end
@@ -276,12 +263,15 @@ module RubyExcel
276
263
 
277
264
  private
278
265
 
279
- def each_address
266
+ def each_address( unused=nil )
280
267
  return to_enum( :each_address ) unless block_given?
281
- ( 'A'..col_letter( data.cols ) ).each { |col_id| yield "#{col_id}#{idx}" }
268
+ ( 'A'..col_letter( data.cols ) ).each { |col_id| yield translate_address( col_id ) }
282
269
  end
283
- alias each_address_without_headers each_address
284
270
 
271
+ def translate_address( addr )
272
+ col_letter( addr ) + idx.to_s
273
+ end
274
+
285
275
  end
286
276
 
287
277
  #
@@ -307,22 +297,27 @@ module RubyExcel
307
297
  super( sheet )
308
298
  end
309
299
 
300
+ #
301
+ # The number of Rows in the Column
302
+ #
303
+
310
304
  def length
311
305
  data.rows
312
306
  end
313
307
 
314
308
  private
315
309
 
316
- def each_address
310
+ def each_address( headers=true )
317
311
  return to_enum( :each_address ) unless block_given?
318
- ( 1..data.rows ).each { |row_id| yield idx + row_id.to_s }
312
+ ( headers ? 1 : sheet.header_rows + 1 ).upto( data.rows ) { |row_id| yield translate_address( row_id ) }
319
313
  end
320
314
 
321
- def each_address_without_headers
322
- return to_enum( :each_address_without_headers ) unless block_given?
323
- ( sheet.header_rows+1 ).upto( data.rows ) { |row_id| yield idx + row_id.to_s }
315
+ def translate_address( addr )
316
+ addr = addr.to_s unless addr.is_a?( String )
317
+ fail ArgumentError, "Invalid address : #{ addr }" if addr =~ /[^\d]/
318
+ idx + addr
324
319
  end
325
-
320
+
326
321
  end
327
322
 
328
323
  end
@@ -0,0 +1,582 @@
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 = nil
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
+ # @overload advanced_filter!( header, comparison_operator, search_criteria, ... )
119
+ # Filter on multiple criteria
120
+ # @param [String] header a header to search under
121
+ # @param [Symbol] comparison_operator the operator to compare with
122
+ # @param [Object] search_criteria the value to filter by
123
+ # @raise [ArgumentError] 'Number of arguments must be a multiple of 3'
124
+ # @raise [ArgumentError] 'Operator must be a symbol'
125
+ # @example Filter to 'Part': 'Type1' and 'Type3', with Qty greater than 1
126
+ # s.advanced_filter!( 'Part', :=~, /Type[13]/, 'Qty', :>, 1 )
127
+ # @example Filter to 'Part': 'Type1', with 'Ref1' containing 'X'
128
+ # s.advanced_filter!( 'Part', :==, 'Type1', 'Ref1', :include?, 'X' )
129
+ #
130
+
131
+ def advanced_filter!( *args )
132
+ data.advanced_filter!( *args ); self
133
+ end
134
+
135
+ #
136
+ # Access an Element by indices.
137
+ #
138
+ # @param [Fixnum] row the row index
139
+ # @param [Fixnum] col the column index
140
+ # @return [RubyExcel::Element]
141
+ # @note Indexing is 1-based like Excel VBA
142
+ #
143
+
144
+ def cell( row, col )
145
+ Element.new( self, indices_to_address( row, col ) )
146
+ end
147
+ alias cells cell
148
+
149
+ #
150
+ # Access a Column (Section) by its reference.
151
+ #
152
+ # @param [String, Fixnum] index the Column reference
153
+ # @return [RubyExcel::Column]
154
+ # @note Index 'A' and 1 both select the 1st Column
155
+ #
156
+
157
+ def column( index )
158
+ Column.new( self, col_letter( index ) )
159
+ end
160
+
161
+ #
162
+ # Access a Column (Section) by its header.
163
+ #
164
+ # @param [String] header the Column header
165
+ # @return [RubyExcel::Column]
166
+ #
167
+
168
+ def column_by_header( header )
169
+ header.is_a?( Column ) ? header : Column.new( self, data.colref_by_header( header ) )
170
+ end
171
+ alias ch column_by_header
172
+
173
+ #
174
+ # Yields each Column to the block
175
+ #
176
+ # @param [String, Fixnum] start_column the Column to start looping from
177
+ # @param [String, Fixnum] end_column the Column to end the loop at
178
+ # @note Iterates to the last Column in the Sheet unless given a second argument.
179
+ #
180
+
181
+ def columns( start_column = 'A', end_column = data.cols )
182
+ return to_enum( :columns, start_column, end_column ) unless block_given?
183
+ ( col_letter( start_column )..col_letter( end_column ) ).each { |idx| yield column( idx ) }
184
+ self
185
+ end
186
+
187
+ #
188
+ # Removes empty Columns and Rows
189
+ #
190
+
191
+ def compact!
192
+ data.compact!; self
193
+ end
194
+
195
+ #
196
+ # Removes Sheet from the parent Workbook
197
+ #
198
+
199
+ def delete
200
+ workbook.delete self
201
+ end
202
+
203
+ #
204
+ # Deletes each Row where the block is true
205
+ #
206
+
207
+ def delete_rows_if
208
+ return to_enum( :delete_rows_if ) unless block_given?
209
+ rows.reverse_each { |r| r.delete if yield r }; self
210
+ end
211
+
212
+ #
213
+ # Deletes each Column where the block is true
214
+ #
215
+
216
+ def delete_columns_if
217
+ return to_enum( :delete_columns_if ) unless block_given?
218
+ columns.reverse_each { |c| c.delete if yield c }; self
219
+ end
220
+
221
+ #
222
+ # Return a copy of self
223
+ #
224
+ # @return [RubyExcel::Sheet]
225
+ #
226
+
227
+ def dup
228
+ s = Sheet.new( name, workbook )
229
+ d = data
230
+ unless d.nil?
231
+ d = d.dup
232
+ s.load( d.all, header_rows )
233
+ d.sheet = s
234
+ end
235
+ s
236
+ end
237
+
238
+ #
239
+ # Check whether the Sheet contains data
240
+ #
241
+ # @return [Boolean] if there is any data
242
+ #
243
+
244
+ def empty?
245
+ data.empty?
246
+ end
247
+
248
+ #
249
+ # Removes all Rows (omitting headers) where the block is false
250
+ #
251
+ # @param [String] header the header of the Column to pass to the block
252
+ # @yield [Object] the value at the intersection of Column and Row
253
+ # @return [self]
254
+ #
255
+
256
+ def filter!( header, &block )
257
+ data.filter!( header, &block ); self
258
+ end
259
+
260
+ #
261
+ # Select and re-order Columns by a list of headers
262
+ #
263
+ # @param [Array<String>] headers the ordered list of headers to keep
264
+ # @note This method can accept either a list of arguments or an Array
265
+ # @note Invalid headers will be skipped
266
+ #
267
+
268
+ def get_columns!( *headers )
269
+ data.get_columns!( *headers ); self
270
+ end
271
+ alias gc! get_columns!
272
+
273
+ # @overload insert_columns( before, number=1 )
274
+ # Insert blank Columns into the data
275
+ #
276
+ # @param [String, Fixnum] before the Column reference to insert before.
277
+ # @param [Fixnum] number the number of new Columns to insert
278
+ #
279
+
280
+ def insert_columns( *args )
281
+ data.insert_columns( *args ); self
282
+ end
283
+
284
+ # @overload insert_rows( before, number=1 )
285
+ # Insert blank Rows into the data
286
+ #
287
+ # @param [Fixnum] before the Row index to insert before.
288
+ # @param [Fixnum] number the number of new Rows to insert
289
+ #
290
+
291
+ def insert_rows( *args )
292
+ data.insert_rows( *args ); self
293
+ end
294
+
295
+ #
296
+ # View the object for debugging
297
+ #
298
+
299
+ def inspect
300
+ "#{ self.class }:0x#{ '%x' % (object_id << 1) }: #{ name }"
301
+ end
302
+
303
+ #
304
+ # Populate the Sheet with data (overwrite)
305
+ #
306
+ # @param [Array<Array>, Hash<Hash>] input_data the data to fill the Sheet with
307
+ # @param header_rows [Fixnum] the number of Rows to be treated as headers
308
+ #
309
+
310
+ def load( input_data, header_rows=1 )
311
+ input_data = _convert_hash(input_data) if input_data.is_a?(Hash)
312
+ input_data.is_a?(Array) or fail ArgumentError, 'Input must be an Array or Hash'
313
+ @header_rows = header_rows
314
+ @data = Data.new( self, input_data ); self
315
+ end
316
+
317
+ #
318
+ # Find the row number by looking up a value in a Column
319
+ #
320
+ # @param [String] header the header of the Column to pass to the block
321
+ # @yield yields each value in the Column to the block
322
+ # @return [Fixnum, nil] the row number of the first match or nil if nothing is found
323
+ #
324
+
325
+ def match( header, &block )
326
+ row_id( column_by_header( header ).find( &block ) )
327
+ end
328
+
329
+ #
330
+ # The highest currently used row number
331
+ #
332
+
333
+ def maxrow
334
+ data.rows
335
+ end
336
+
337
+ #
338
+ # The highest currently used column number
339
+ #
340
+
341
+ def maxcol
342
+ data.cols
343
+ end
344
+ alias maxcolumn maxcol
345
+
346
+ #
347
+ # Allow shorthand range references and non-bang versions of bang methods.
348
+ #
349
+
350
+ def method_missing(m, *args, &block)
351
+ method_name = m.to_s
352
+
353
+ if method_name[-1] != '!' && respond_to?( method_name + '!' )
354
+
355
+ dup.send( method_name + '!', *args, &block )
356
+
357
+ elsif method_name =~ /\A[A-Z]{1,3}\d+=?\z/i
358
+
359
+ method_name.upcase!
360
+ if method_name[-1] == '='
361
+ range( method_name.chop ).value = ( args.length == 1 ? args.first : args )
362
+ else
363
+ range( method_name ).value
364
+ end
365
+
366
+ else
367
+ super
368
+ end
369
+ end
370
+
371
+ #
372
+ # Allow for certain method_missing calls
373
+ #
374
+
375
+ def respond_to?(m)
376
+
377
+ if m[-1] != '!' && respond_to?( m.to_s + '!' )
378
+ true
379
+ elsif m.to_s.upcase.strip =~ /\A[A-Z]{1,3}\d+=?\z/
380
+ true
381
+ else
382
+ super
383
+ end
384
+
385
+ end
386
+
387
+ #
388
+ # Split the Sheet into two Sheets by evaluating each value in a column
389
+ #
390
+ # @param [String] header the header of the Column which contains the yield value
391
+ # @yield [value] yields the value of each row under the given header
392
+ # @return [Array<RubyExcel::Sheet, RubyExcel::Sheet>] Two Sheets: true and false. Headers included.
393
+ #
394
+
395
+ def partition( header, &block )
396
+ data.partition( header, &block ).map { |d| dup.load( d ) }
397
+ end
398
+
399
+ #
400
+ # Access an Element by address.
401
+ #
402
+ # @param [String, Element] first_cell the first Cell or Address in the Range
403
+ # @param [String, Element] last_cell the last Cell or Address in the Range
404
+ # @return [RubyExcel::Element]
405
+ # @note These are all valid arguments:
406
+ # ('A1')
407
+ # ('A1:B2')
408
+ # ('A:A')
409
+ # ('1:1')
410
+ # ('A1', 'B2')
411
+ # (cell1)
412
+ # (cell1, cell2)
413
+ #
414
+
415
+ def range( first_cell, last_cell=nil )
416
+ Element.new( self, to_range_address( first_cell, last_cell ) )
417
+ end
418
+
419
+ #
420
+ # Reverse the Sheet Columns
421
+ #
422
+
423
+ def reverse_columns!
424
+ data.reverse_columns!; self
425
+ end
426
+
427
+ #
428
+ # Reverse the Sheet Rows (without affecting the headers)
429
+ #
430
+
431
+ def reverse_rows!
432
+ data.reverse_rows!; self
433
+ end
434
+
435
+ #
436
+ # Create a Row from an index
437
+ #
438
+ # @param [Fixnum] index the Row index
439
+ # @return [RubyExcel::Row]
440
+ #
441
+
442
+ def row( index )
443
+ Row.new( self, index )
444
+ end
445
+
446
+ #
447
+ # Yields each Row to the block
448
+ #
449
+ # @param [Fixnum] start_row the Row to start looping from
450
+ # @param [Fixnum] end_row the Row to end the loop at
451
+ # @note Iterates to the last Row in the Sheet unless given a second argument.
452
+ #
453
+
454
+ def rows( start_row = 1, end_row = data.rows )
455
+ return to_enum(:rows, start_row, end_row) unless block_given?
456
+ ( start_row..end_row ).each { |idx| yield row( idx ) }; self
457
+ end
458
+ alias each rows
459
+
460
+ #
461
+ # Sort the data according to a block (avoiding headers)
462
+ #
463
+
464
+ def sort!( &block )
465
+ data.sort!( &block ); self
466
+ end
467
+
468
+ #
469
+ # Sort the data by a column, selected by header
470
+ #
471
+ # @param [String] header the header to sort the Sheet by
472
+ #
473
+
474
+ def sort_by!( header )
475
+ raise ArgumentError, 'Sheet#sort_by! does not support blocks.' if block_given?
476
+ idx = data.index_by_header( header ) - 1
477
+ sort_method = lambda { |array| array[idx] }
478
+ data.sort_by!( &sort_method )
479
+ self
480
+ end
481
+
482
+ #
483
+ # Break the Sheet into a Workbook with multiple Sheets, split by the values under a header.
484
+ #
485
+ # @param [String] header the header to split by
486
+ # @return [RubyExcel::Workbook] a new workbook containing the split Sheets (each with headers)
487
+ #
488
+
489
+ def split( header )
490
+ wb = Workbook.new
491
+ ch( header ).each_wh.to_a.uniq.each { |name| wb.add( name ).load( data.headers ) }
492
+ rows( header_rows+1 ) do |row|
493
+ wb.sheets( row.val( header ) ) << row
494
+ end
495
+ wb
496
+ end
497
+
498
+ #
499
+ # Sum the values in a Column by searching another Column
500
+ #
501
+ # @param [String] find_header the header of the Column to yield to the block
502
+ # @param [String] sum_header the header of the Column to sum
503
+ # @yield yields the find_header column values to the block
504
+ #
505
+
506
+ def sumif( find_header, sum_header )
507
+ return to_enum( :sumif ) unless block_given?
508
+ find_col, sum_col = ch( find_header ), ch( sum_header )
509
+ find_col.each_cell.inject(0) { |sum,ce| yield( ce.value ) && ce.row > header_rows ? sum + sum_col[ ce.row ] : sum }
510
+ end
511
+
512
+ #
513
+ # Summarise the values of a Column into a Hash
514
+ #
515
+ # @param [String] header the header of the Column to summarise
516
+ # @return [Hash]
517
+ #
518
+
519
+ def summarise( header )
520
+ ch( header ).summarise
521
+ end
522
+
523
+ #
524
+ # The Sheet as a 2D Array
525
+ #
526
+
527
+ def to_a
528
+ data.all
529
+ end
530
+
531
+ #
532
+ # The Sheet as a WIN32OLE Excel Workbook
533
+ # @note This requires Windows and MS Excel
534
+ #
535
+
536
+ def to_excel
537
+ workbook.dup.clear_all.add( self.dup ).workbook.to_excel
538
+ end
539
+
540
+ #
541
+ # The Sheet as a String containing an HTML Table
542
+ #
543
+
544
+ def to_html
545
+ "<table>\n" + data.map { |row| '<tr>' + row.map { |v| '<td>' + CGI.escapeHTML(v.to_s) }.join() + "\n" }.join() + '</table>'
546
+ end
547
+
548
+ #
549
+ # The Sheet as a Tab Seperated Value String
550
+ #
551
+
552
+ def to_s
553
+ data.map { |ar| ar.map { |v| v.to_s.gsub(/\t|\n/,' ') }.join "\t" }.join( $/ )
554
+ end
555
+
556
+ #
557
+ # Remove any Rows with duplicate values within a Column
558
+ #
559
+ # @param [String] header the header of the Column to check for duplicates
560
+ #
561
+
562
+ def uniq!( header )
563
+ data.uniq!( header ); self
564
+ end
565
+ alias unique! uniq!
566
+
567
+ #
568
+ # Find a value within a Column by searching another Column
569
+ #
570
+ # @param [String] find_header the header of the Column to search
571
+ # @param [String] return_header the header of the return value Column
572
+ # @yield the first matching value
573
+ #
574
+
575
+ def vlookup( find_header, return_header, &block )
576
+ find_col, return_col = ch( find_header ), ch( return_header )
577
+ return_col[ row_id( find_col.find( &block ) ) ] rescue nil
578
+ end
579
+
580
+ end # Sheet
581
+
582
+ end # RubyExcel
data/lib/rubyexcel.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require_relative 'rubyexcel/rubyexcel_components.rb'
2
- require_relative 'rubyexcel/excel_tools.rb'
3
2
  require 'cgi'
4
3
 
5
4
  #
@@ -185,612 +184,5 @@ module RubyExcel
185
184
  end
186
185
 
187
186
  end # Workbook
188
-
189
- #
190
- # The front-end class for data manipulation and output.
191
- #
192
-
193
- class Sheet
194
- include Address
195
- include Enumerable
196
-
197
- # The Data underlying the Sheet
198
- attr_reader :data
199
-
200
- # The name of the Sheet
201
- attr_accessor :name
202
-
203
- # The number of rows treated as headers
204
- attr_accessor :header_rows
205
-
206
- # The Workbook parent of this Sheet
207
- attr_accessor :workbook
208
-
209
- alias parent workbook; alias parent= workbook=
210
- alias headers header_rows; alias headers= header_rows=
211
-
212
- #
213
- # Creates a RubyExcel::Sheet instance
214
- #
215
- # @param [String] name the name of the Sheet
216
- # @param [RubyExcel::Workbook] workbook the Workbook which holds this Sheet
217
- #
218
-
219
- def initialize( name, workbook )
220
- @workbook = workbook
221
- @name = name
222
- @header_rows = nil
223
- @data = Data.new( self, [[]] )
224
- end
225
-
226
- #
227
- # Read a value by address
228
- #
229
- # @example
230
- # sheet['A1']
231
- # #=> "Part"
232
- #
233
- # @example
234
- # sheet['A1:B2']
235
- # #=> [["Part", "Ref1"], ["Type1", "QT1"]]
236
- #
237
- # @param [String] addr the address to access
238
- #
239
-
240
- def[]( addr )
241
- range( addr ).value
242
- end
243
-
244
- #
245
- # Write a value by address
246
- #
247
- # @example
248
- # sheet['A1'] = "Bart"
249
- # sheet['A1']
250
- # #=> "Bart"
251
- #
252
- # @param (see #[])
253
- # @param [Object] val the value to write into the data
254
- #
255
-
256
- def []=( addr, val )
257
- range( addr ).value = val
258
- end
259
-
260
- #
261
- # Add data with the Sheet
262
- #
263
- # @param [Array<Array>, Hash<Hash>, RubyExcel::Sheet] other the data to add
264
- # @return [RubyExcel::Sheet] returns a new Sheet
265
- # @note When adding another Sheet it won't import the headers unless this Sheet is empty.
266
- #
267
-
268
- def +( other )
269
- dup << other
270
- end
271
-
272
- #
273
- # Subtract data from the Sheet
274
- #
275
- # @param [Array<Array>, RubyExcel::Sheet] other the data to subtract
276
- # @return [RubyExcel::Sheet] returns a new Sheet
277
- #
278
-
279
- def -( other )
280
- case other
281
- when Array ; Workbook.new.load( data.all - other )
282
- when Sheet ; Workbook.new.load( data.all - other.data.no_headers )
283
- else ; fail ArgumentError, "Unsupported class: #{ other.class }"
284
- end
285
- end
286
-
287
- #
288
- # Append an object to the Sheet
289
- #
290
- # @param [Object] other the object to append
291
- # @return [self]
292
- # @note When adding another Sheet it won't import the headers unless this Sheet is empty.
293
- # @note Anything other than an an Array, Hash, Row, Column or Sheet will be appended to the first row
294
- #
295
-
296
- def <<( other )
297
- data << other
298
- self
299
- end
300
-
301
- # {Sheet#advanced_filter!}
302
-
303
- def advanced_filter( *args )
304
- dup.advanced_filter!( *args )
305
- end
306
-
307
- # @overload advanced_filter!( header, comparison_operator, search_criteria, ... )
308
- # Filter on multiple criteria
309
- # @example Filter to 'Part': 'Type1' and 'Type3', with Qty greater than 1
310
- # s.advanced_filter!( 'Part', :=~, /Type[13]/, 'Qty', :>, 1 )
311
- # @example Filter to 'Part': 'Type1', with 'Ref1' containing 'X'
312
- # s.advanced_filter!( 'Part', :==, 'Type1', 'Ref1', :include?, 'X' )
313
- #
314
- # @param [String] header a header to search under
315
- # @param [Symbol] comparison_operator the operator to compare with
316
- # @param [Object] search_criteria the value to filter by
317
- # @raise [ArgumentError] 'Number of arguments must be a multiple of 3'
318
- # @raise [ArgumentError] 'Operator must be a symbol'
319
- #
320
-
321
- def advanced_filter!( *args )
322
- data.advanced_filter!( *args ); self
323
- end
324
-
325
- #
326
- # Access an Element by indices.
327
- #
328
- # @param [Fixnum] row the row index
329
- # @param [Fixnum] col the column index
330
- # @return [RubyExcel::Element]
331
- # @note Indexing is 1-based like Excel VBA
332
- #
333
-
334
- def cell( row, col )
335
- Element.new( self, indices_to_address( row, col ) )
336
- end
337
- alias cells cell
338
-
339
- #
340
- # Access a Column (Section) by its reference.
341
- #
342
- # @param [String, Fixnum] index the Column reference
343
- # @return [RubyExcel::Column]
344
- # @note Index 'A' and 1 both select the 1st Column
345
- #
346
-
347
- def column( index )
348
- Column.new( self, col_letter( index ) )
349
- end
350
-
351
- #
352
- # Access a Column (Section) by its header.
353
- #
354
- # @param [String] header the Column header
355
- # @return [RubyExcel::Column]
356
- #
357
-
358
- def column_by_header( header )
359
- header.is_a?( Column ) ? header : Column.new( self, data.colref_by_header( header ) )
360
- end
361
- alias ch column_by_header
362
-
363
- #
364
- # Yields each Column to the block
365
- #
366
- # @param [String, Fixnum] start_column the Column to start looping from
367
- # @param [String, Fixnum] end_column the Column to end the loop at
368
- # @note Iterates to the last Column in the Sheet unless given a second argument.
369
- #
370
-
371
- def columns( start_column = 'A', end_column = data.cols )
372
- return to_enum( :columns, start_column, end_column ) unless block_given?
373
- ( col_letter( start_column )..col_letter( end_column ) ).each { |idx| yield column( idx ) }
374
- self
375
- end
376
-
377
- # {Sheet#compact!}
378
-
379
- def compact
380
- dup.compact!
381
- end
382
-
383
- #
384
- # Removes empty Columns and Rows
385
- #
386
-
387
- def compact!
388
- data.compact!; self
389
- end
390
-
391
- #
392
- # Removes Sheet from the parent Workbook
393
- #
394
-
395
- def delete
396
- workbook.delete self
397
- end
398
-
399
- #
400
- # Deletes each Row where the block is true
401
- #
402
-
403
- def delete_rows_if
404
- return to_enum( :delete_rows_if ) unless block_given?
405
- rows.reverse_each { |r| r.delete if yield r }; self
406
- end
407
-
408
- #
409
- # Deletes each Column where the block is true
410
- #
411
-
412
- def delete_columns_if
413
- return to_enum( :delete_columns_if ) unless block_given?
414
- columns.reverse_each { |c| c.delete if yield c }; self
415
- end
416
-
417
- #
418
- # Return a copy of self
419
- #
420
- # @return [RubyExcel::Sheet]
421
- #
422
-
423
- def dup
424
- s = Sheet.new( name, workbook )
425
- d = data
426
- unless d.nil?
427
- d = d.dup
428
- s.load( d.all, header_rows )
429
- d.sheet = s
430
- end
431
- s
432
- end
433
-
434
- #
435
- # Check whether the Sheet contains data
436
- #
437
- # @return [Boolean] if there is any data
438
- #
439
-
440
- def empty?
441
- data.empty?
442
- end
443
-
444
- # {Sheet#filter!}
445
-
446
- def filter( header, &block )
447
- dup.filter!( header, &block )
448
- end
449
-
450
- #
451
- # Removes all Rows (omitting headers) where the block is false
452
- #
453
- # @param [String] header the header of the Column to pass to the block
454
- # @yield [Object] the value at the intersection of Column and Row
455
- # @return [self]
456
- #
457
-
458
- def filter!( header, &block )
459
- data.filter!( header, &block ); self
460
- end
461
-
462
- # {Sheet#get_columns!}
463
-
464
- def get_columns( *headers )
465
- dup.get_columns!( *headers )
466
- end
467
- alias gc get_columns
468
-
469
- #
470
- # Select and re-order Columns by a list of headers
471
- #
472
- # @param [Array<String>] headers the ordered list of headers to keep
473
- # @note This method can accept either a list of arguments or an Array
474
- # @note Invalid headers will be skipped
475
- #
476
-
477
- def get_columns!( *headers )
478
- data.get_columns!( *headers ); self
479
- end
480
- alias gc! get_columns!
481
-
482
- # @overload insert_columns( before, number=1 )
483
- # Insert blank Columns into the data
484
- #
485
- # @param [String, Fixnum] before the Column reference to insert before.
486
- # @param [Fixnum] number the number of new Columns to insert
487
- #
488
-
489
- def insert_columns( *args )
490
- data.insert_columns( *args ); self
491
- end
492
-
493
- # @overload insert_rows( before, number=1 )
494
- # Insert blank Rows into the data
495
- #
496
- # @param [Fixnum] before the Row index to insert before.
497
- # @param [Fixnum] number the number of new Rows to insert
498
- #
499
-
500
- def insert_rows( *args )
501
- data.insert_rows( *args ); self
502
- end
503
-
504
- #
505
- # View the object for debugging
506
- #
507
-
508
- def inspect
509
- "#{ self.class }:0x#{ '%x' % (object_id << 1) }: #{ name }"
510
- end
511
-
512
- #
513
- # Populate the Sheet with data (overwrite)
514
- #
515
- # @param [Array<Array>, Hash<Hash>] input_data the data to fill the Sheet with
516
- # @param header_rows [Fixnum] the number of Rows to be treated as headers
517
- #
518
-
519
- def load( input_data, header_rows=1 )
520
- input_data = _convert_hash(input_data) if input_data.is_a?(Hash)
521
- input_data.is_a?(Array) or fail ArgumentError, 'Input must be an Array or Hash'
522
- @header_rows = header_rows
523
- @data = Data.new( self, input_data ); self
524
- end
525
-
526
- #
527
- # Find the row number by looking up a value in a Column
528
- #
529
- # @param [String] header the header of the Column to pass to the block
530
- # @yield yields each value in the Column to the block
531
- # @return [Fixnum, nil] the row number of the first match or nil if nothing is found
532
- #
533
-
534
- def match( header, &block )
535
- row_id( column_by_header( header ).find( &block ) )
536
- end
537
-
538
- #
539
- # The highest currently used row number
540
- #
541
-
542
- def maxrow
543
- data.rows
544
- end
545
-
546
- #
547
- # The highest currently used column number
548
- #
549
-
550
- def maxcol
551
- data.cols
552
- end
553
- alias maxcolumn maxcol
554
-
555
- #
556
- # Allow shorthand range references
557
- #
558
-
559
- def method_missing(m, *args, &block)
560
- method_name = m.to_s.upcase.strip
561
- if method_name =~ /\A[A-Z]{1,3}\d+=?\z/
562
- if method_name[-1] == '='
563
- range( method_name.chop ).value = ( args.length == 1 ? args.first : args )
564
- else
565
- range( method_name ).value
566
- end
567
- else
568
- super
569
- end
570
- end
571
-
572
- #
573
- # Allow for certain method_missing calls
574
- #
575
-
576
- def respond_to?(meth)
577
- if meth.to_s.upcase.strip =~ /\A[A-Z]{1,3}\d+=?\z/
578
- true
579
- else
580
- super
581
- end
582
- end
583
-
584
- #
585
- # Split the Sheet into two Sheets by evaluating each value in a column
586
- #
587
- # @param [String] header the header of the Column which contains the yield value
588
- # @yield [value] yields the value of each row under the given header
589
- # @return [Array<RubyExcel::Sheet, RubyExcel::Sheet>] Two Sheets: true and false. Headers included.
590
- #
591
-
592
- def partition( header, &block )
593
- data.partition( header, &block ).map { |d| dup.load( d ) }
594
- end
595
-
596
- #
597
- # Access an Element by address.
598
- #
599
- # @param [String, Element] first_cell the first Cell or Address in the Range
600
- # @param [String, Element] last_cell the last Cell or Address in the Range
601
- # @return [RubyExcel::Element]
602
- # @note These are all valid arguments:
603
- # ('A1')
604
- # ('A1:B2')
605
- # ('A:A')
606
- # ('1:1')
607
- # ('A1', 'B2')
608
- # (cell1)
609
- # (cell1, cell2)
610
- #
611
-
612
- def range( first_cell, last_cell=nil )
613
- Element.new( self, to_range_address( first_cell, last_cell ) )
614
- end
615
-
616
- #
617
- # Reverse the Sheet Columns
618
- #
619
-
620
- def reverse_columns!
621
- data.reverse_columns!; self
622
- end
623
-
624
- #
625
- # Reverse the Sheet Rows (without affecting the headers)
626
- #
627
-
628
- def reverse_rows!
629
- data.reverse_rows!; self
630
- end
631
-
632
- #
633
- # Create a Row from an index
634
- #
635
- # @param [Fixnum] index the Row index
636
- # @return [RubyExcel::Row]
637
- #
638
187
 
639
- def row( index )
640
- Row.new( self, index )
641
- end
642
-
643
- #
644
- # Yields each Row to the block
645
- #
646
- # @param [Fixnum] start_row the Row to start looping from
647
- # @param [Fixnum] end_row the Row to end the loop at
648
- # @note Iterates to the last Row in the Sheet unless given a second argument.
649
- #
650
-
651
- def rows( start_row = 1, end_row = data.rows )
652
- return to_enum(:rows, start_row, end_row) unless block_given?
653
- ( start_row..end_row ).each { |idx| yield row( idx ) }; self
654
- end
655
- alias each rows
656
-
657
- # {Sheet#sort!}
658
-
659
- def sort( &block )
660
- dup.sort!( &block )
661
- end
662
-
663
- #
664
- # Sort the data according to a block (avoiding headers)
665
- #
666
-
667
- def sort!( &block )
668
- data.sort!( &block ); self
669
- end
670
-
671
- # {Sheet#sort_by!}
672
-
673
- def sort_by( header )
674
- dup.sort_by!( header )
675
- end
676
-
677
- #
678
- # Sort the data by a column, selected by header
679
- #
680
- # @param [String] header the header to sort the Sheet by
681
- #
682
-
683
- def sort_by!( header )
684
- idx = data.index_by_header( header ) - 1
685
- sort_method = lambda { |array| array[idx] }
686
- data.sort_by!( &sort_method )
687
- self
688
- end
689
-
690
- #
691
- # Break the Sheet into a Workbook with multiple Sheets, split by the values under a header.
692
- #
693
- # @param [String] header the header to split by
694
- # @return [RubyExcel::Workbook] a new workbook containing the split Sheets (each with headers)
695
- #
696
-
697
- def split( header )
698
- wb = Workbook.new
699
- ch( header ).each_wh.to_a.uniq.each { |name| wb.add( name ).load( data.headers ) }
700
- rows( header_rows+1 ) do |row|
701
- wb.sheets( row.val( header ) ) << row
702
- end
703
- wb
704
- end
705
-
706
- #
707
- # Sum the values in a Column by searching another Column
708
- #
709
- # @param [String] find_header the header of the Column to yield to the block
710
- # @param [String] sum_header the header of the Column to sum
711
- # @yield yields the find_header column values to the block
712
- #
713
-
714
- def sumif( find_header, sum_header )
715
- return to_enum( :sumif ) unless block_given?
716
- find_col, sum_col = ch( find_header ), ch( sum_header )
717
- find_col.each_cell.inject(0) { |sum,ce| yield( ce.value ) && ce.row > header_rows ? sum + sum_col[ ce.row ] : sum }
718
- end
719
-
720
- #
721
- # Summarise the values of a Column into a Hash
722
- #
723
- # @param [String] header the header of the Column to summarise
724
- # @return [Hash]
725
- #
726
-
727
- def summarise( header )
728
- ch( header ).summarise
729
- end
730
-
731
- #
732
- # The Sheet as a 2D Array
733
- #
734
-
735
- def to_a
736
- data.all
737
- end
738
-
739
- #
740
- # The Sheet as a WIN32OLE Excel Workbook
741
- # @note This requires Windows and MS Excel
742
- #
743
-
744
- def to_excel
745
- workbook.dup.clear_all.add( self.dup ).workbook.to_excel
746
- end
747
-
748
- #
749
- # The Sheet as a String containing an HTML Table
750
- #
751
-
752
- def to_html
753
- "<table>\n" + data.map { |row| '<tr>' + row.map { |v| '<td>' + CGI.escapeHTML(v.to_s) }.join() + "\n" }.join() + '</table>'
754
- end
755
-
756
- #
757
- # The Sheet as a Tab Seperated Value String
758
- #
759
-
760
- def to_s
761
- data.map { |ar| ar.map { |v| v.to_s.gsub(/\t|\n/,' ') }.join "\t" }.join( $/ )
762
- end
763
-
764
- # {Sheet#uniq!}
765
-
766
- def uniq( header )
767
- dup.uniq!( header )
768
- end
769
-
770
- #
771
- # Remove any Rows with duplicate values within a Column
772
- #
773
- # @param [String] header the header of the Column to check for duplicates
774
- #
775
-
776
- def uniq!( header )
777
- data.uniq!( header ); self
778
- end
779
- alias unique! uniq!
780
-
781
- #
782
- # Find a value within a Column by searching another Column
783
- #
784
- # @param [String] find_header the header of the Column to search
785
- # @param [String] return_header the header of the return value Column
786
- # @yield the first matching value
787
- #
788
-
789
- def vlookup( find_header, return_header, &block )
790
- find_col, return_col = ch( find_header ), ch( return_header )
791
- return_col[ row_id( find_col.find( &block ) ) ] rescue nil
792
- end
793
-
794
- end # Sheet
795
-
796
188
  end # RubyExcel
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyexcel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-03 00:00:00.000000000 Z
12
+ date: 2013-05-07 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: A tabular data structure in Ruby, with header-based helper methods and
15
15
  some of Excel's API style.
@@ -24,6 +24,7 @@ files:
24
24
  - lib/rubyexcel/excel_tools.rb
25
25
  - lib/rubyexcel/rubyexcel_components.rb
26
26
  - lib/rubyexcel/section.rb
27
+ - lib/rubyexcel/sheet.rb
27
28
  - lib/rubyexcel.rb
28
29
  - lib/README.md
29
30
  homepage: https://github.com/VirtuosoJoel