rubyexcel 0.1.2 → 0.1.3

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