rubyexcel 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,40 @@
1
1
  module RubyExcel
2
2
 
3
+ #
4
+ #Provides address translation methods to RubyExcel's classes
5
+ #
6
+
3
7
  module Address
4
8
 
9
+ #
10
+ # Translates an address to a column index
11
+ #
12
+ # @param [String] address the address to translate
13
+ # @return [Fixnum] the column index
14
+ #
15
+
5
16
  def address_to_col_index( address )
6
17
  col_index( column_id( address ) )
7
18
  end
8
19
 
20
+ #
21
+ # Translates an address to indices
22
+ #
23
+ # @param [String] address the address to translate
24
+ # @return [Array<Fixnum>] row index, column index
25
+ #
26
+
9
27
  def address_to_indices( address )
10
28
  [ row_id( address ), address_to_col_index( address ) ]
11
29
  end
12
30
 
31
+ #
32
+ # Translates a column id to an index
33
+ #
34
+ # @param [String] letter the column id to translate
35
+ # @return [Fixnum] the corresponding index
36
+ #
37
+
13
38
  def col_index( letter )
14
39
  return letter if letter.is_a? Fixnum
15
40
  letter !~ /[^A-Z]/ && [1,2,3].include?( letter.length ) or fail ArgumentError, "Invalid column reference: #{ letter }"
@@ -17,16 +42,37 @@ module RubyExcel
17
42
  loop { return idx if a == letter; idx+=1; a.next! }
18
43
  end
19
44
 
45
+ #
46
+ # Translates an index to a column letter
47
+ #
48
+ # @param [Fixnum] index the index to translate
49
+ # @return [String] the column letter
50
+ #
51
+
20
52
  def col_letter( index )
21
53
  return index if index.is_a? String
22
54
  index > 0 or fail ArgumentError, 'Indexing is 1-based'
23
55
  a = 'A'; ( index - 1 ).times { a.next! }; a
24
56
  end
25
57
 
58
+ #
59
+ # Translates an address to a column id
60
+ #
61
+ # @param [String] address the address to translate
62
+ # @return [String] the column id
63
+ #
64
+
26
65
  def column_id( address )
27
66
  address[/[A-Z]+/]
28
67
  end
29
68
 
69
+ #
70
+ # Expands an address to all contained addresses
71
+ #
72
+ # @param [String] address the address to translate
73
+ # @return [Array<String>] all addresses included within the given address
74
+ #
75
+
30
76
  def expand( address )
31
77
  return [[address]] unless address.include? ':'
32
78
  address.upcase.match( /([A-Z]+)(\d+):([A-Z]+)(\d+)/i )
@@ -34,27 +80,64 @@ module RubyExcel
34
80
  ( start_row..end_row ).map { |r| ( start_col..end_col ).map { |c| c + r.to_s } }
35
81
  end
36
82
 
83
+ #
84
+ # Translates indices to an address
85
+ #
86
+ # @param [Fixnum] row_idx the row index
87
+ # @param [Fixnum] column_idx the column index
88
+ # @return [String] the corresponding address
89
+ #
90
+
37
91
  def indices_to_address( row_idx, column_idx )
38
92
  [ row_idx, column_idx ].all? { |a| a.is_a?( Fixnum ) } or fail ArgumentError, 'Input must be Fixnum'
39
93
  col_letter( column_idx ) + row_idx.to_s
40
94
  end
41
95
 
96
+ #
97
+ # Checks whether an object is a multidimensional Array
98
+ #
99
+ # @param [Object] obj the object to test
100
+ # @return [Boolean] whether the object is a multidimensional Array
101
+ #
102
+
42
103
  def multi_array?( obj )
43
104
  obj.all? { |el| el.is_a?( Array ) } && obj.is_a?( Array ) rescue false
44
105
  end
45
106
 
107
+ #
108
+ # Offsets an address by row and column
109
+ #
110
+ # @param [String] address the address to offset
111
+ # @param [Fixnum] row the number of rows to offset by
112
+ # @param [Fixnum] col the number of columns to offset by
113
+ # @return [String] the new address
114
+ #
115
+
46
116
  def offset(address, row, col)
47
117
  ( col_letter( address_to_col_index( address ) + col ) ) + ( row_id( address ) + row ).to_s
48
118
  end
49
119
 
120
+ #
121
+ # Translates two objects to a range address
122
+ #
123
+ # @param [String, RubyExcel::Element] obj1 the first address element
124
+ # @param [String, RubyExcel::Element] obj2 the second address element
125
+ # @return [String] the new address
126
+ #
127
+
50
128
  def to_range_address( obj1, obj2 )
51
- if obj2
52
- obj2.is_a?( String ) ? ( obj1 + ':' + obj2 ) : "#{ obj1.address }:#{ obj2.address }"
53
- else
54
- obj1.is_a?( String ) ? obj1 : obj1.address
55
- end
129
+ addr = obj1.respond_to?( :address ) ? obj1.address : obj1.to_s
130
+ addr << ':' + ( obj2.respond_to?( :address ) ? obj2.address : obj2.to_s ) if obj2
131
+ addr
56
132
  end
57
133
 
134
+ #
135
+ # Translates an address to a row id
136
+ #
137
+ # @param [String] address the address to translate
138
+ # @return [Fixnum] the row id
139
+ #
140
+
58
141
  def row_id( address )
59
142
  address[/\d+/].to_i
60
143
  end
@@ -2,12 +2,30 @@ module RubyExcel
2
2
 
3
3
  require_relative 'address.rb'
4
4
 
5
+ #
6
+ # The class which holds a Sheet's data
7
+ #
8
+
5
9
  class Data
6
- attr_reader :rows, :cols
10
+ include Address
11
+ include Enumerable
12
+
13
+ #The number of rows in the data
14
+ attr_reader :rows
15
+
16
+ #The number of columns in the data
17
+ attr_reader :cols
18
+
19
+ #The parent Sheet
7
20
  attr_accessor :sheet
8
21
  alias parent sheet
9
22
 
10
- include Address
23
+ #
24
+ # Creates a RubyExcel::Data instance
25
+ #
26
+ # @param [RubyExcel::Sheet] sheet the parent Sheet
27
+ # @param [Array<Array>] input_data the multidimensional Array which holds the data
28
+ #
11
29
 
12
30
  def initialize( sheet, input_data )
13
31
  ( input_data.kind_of?( Array ) && input_data.all? { |el| el.kind_of?( Array ) } ) or fail ArgumentError, 'Input must be Array of Arrays'
@@ -16,37 +34,76 @@ require_relative 'address.rb'
16
34
  calc_dimensions
17
35
  end
18
36
 
37
+ #
38
+ # Returns a copy of the data
39
+ #
40
+
19
41
  def all
20
42
  @data.dup
21
43
  end
22
44
 
45
+ #
46
+ # Appends a multidimensional Array to the end of the data
47
+ #
48
+ # @param [Array<Array>] multi_array the multidimensional Array to add to the data
49
+ #
50
+
23
51
  def append( multi_array )
24
52
  @data << multi_array
25
53
  calc_dimensions
26
54
  end
27
55
 
56
+ #
57
+ # Finds a Column reference bu a header
58
+ #
59
+ # @param [String] header the header to search for
60
+ # @return [String] the Column reference
61
+ # @raise [NoMethodError] 'No header rows present'
62
+ # @raise [IndexError] header.to_s + ' is not a valid header'
63
+ #
64
+
28
65
  def colref_by_header( header )
66
+ return header.idx if header.is_a?( Column )
29
67
  sheet.header_rows > 0 or fail NoMethodError, 'No header rows present'
30
68
  @data[ 0..sheet.header_rows-1 ].each { |r| idx = r.index( header ); return col_letter( idx+1 ) if idx }
31
- fail IndexError, "#{ header } is not a valid header"
69
+ fail IndexError, header.to_s + ' is not a valid header'
32
70
  end
33
71
 
72
+ #
73
+ # Removes empty rows and columns from the data
74
+ #
75
+
34
76
  def compact!
35
77
  compact_columns!
36
78
  compact_rows!
37
79
  end
38
80
 
81
+ #
82
+ # Removes empty columns from the data
83
+ #
84
+
39
85
  def compact_columns!
40
86
  ensure_shape
41
87
  @data = @data.transpose.delete_if { |ar| ar.all? { |el| el.to_s.empty? } || ar.empty? }.transpose
42
88
  calc_dimensions
43
89
  end
44
90
 
91
+ #
92
+ # Removes empty rows from the data
93
+ #
94
+
45
95
  def compact_rows!
46
96
  @data.delete_if { |ar| ar.all? { |el| el.to_s.empty? } || ar.empty? }
47
97
  calc_dimensions
48
98
  end
49
99
 
100
+ #
101
+ # Deletes the data referenced by an object
102
+ #
103
+ # @param [RubyExcel::Column, RubyExcel::Element, RubyExcel::Row] object the object to delete
104
+ # @raise [NoMethodError] object.class.to_s + ' is not supported"
105
+ #
106
+
50
107
  def delete( object )
51
108
  case object
52
109
  when Row
@@ -60,41 +117,89 @@ require_relative 'address.rb'
60
117
  @data[ indices[0]..indices[2] ].each { |r| r.slice!( indices[1], indices[3] - indices[1] + 1 ) }
61
118
  @data.delete_if.with_index { |r,i| r.empty? && i.between?( indices[0], indices[2] ) }
62
119
  else
63
- fail NoMethodError, "#{ object.class } is not supported"
120
+ fail NoMethodError, object.class.to_s + ' is not supported'
64
121
  end
65
122
  calc_dimensions
66
123
  end
67
124
 
125
+ #
126
+ # Deletes the data referenced by a column id
127
+ #
128
+
68
129
  def delete_column( ref )
69
130
  delete( Column.new( sheet, ref ) )
70
131
  calc_dimensions
71
132
  end
72
133
 
134
+ #
135
+ # Deletes the data referenced by a row id
136
+ #
137
+
73
138
  def delete_row( ref )
74
139
  delete( Row.new( sheet, ref ) )
75
140
  calc_dimensions
76
141
  end
77
142
 
143
+ #
144
+ # Deletes the data referenced by an address
145
+ #
146
+
78
147
  def delete_range( ref )
79
148
  delete( Element.new( sheet, ref ) )
80
149
  calc_dimensions
81
150
  end
82
151
 
152
+ #
153
+ # Return a copy of self
154
+ #
155
+ # @return [RubyExcel::Data]
156
+ #
157
+
83
158
  def dup
84
159
  Data.new( sheet, @data.map(&:dup) )
85
160
  end
86
161
 
162
+ #
163
+ # Check whether the data is empty
164
+ #
165
+ # @return [Boolean]
166
+ #
167
+
87
168
  def empty?
88
169
  no_headers.empty?
89
170
  end
90
171
 
172
+ #
173
+ # Yields each "Row" as an Array
174
+ #
175
+
176
+ def each
177
+ @data.each { |ar| yield ar }
178
+ end
179
+
180
+ #
181
+ # Removes all Rows (omitting headers) where the block is false
182
+ #
183
+ # @param [String] header the header of the Column to pass to the block
184
+ # @yield [Object] the value at the intersection of Column and Row
185
+ # @return [self]
186
+ #
187
+
91
188
  def filter!( header )
92
189
  hrows = sheet.header_rows
93
- idx = col_index( hrows > 0 ? colref_by_header( header ) : header )
190
+ idx = index_by_header( header )
94
191
  @data = @data.select.with_index { |row, i| hrows > i || yield( row[ idx -1 ] ) }
95
192
  calc_dimensions
193
+ self
96
194
  end
97
195
 
196
+ #
197
+ # Select and re-order Columns by a list of headers
198
+ #
199
+ # @param [Array<String>] headers the ordered list of headers to keep
200
+ # @note Invalid headers will be skipped
201
+ #
202
+
98
203
  def get_columns!( *headers )
99
204
  headers = headers.flatten
100
205
  hrow = sheet.header_rows - 1
@@ -104,6 +209,32 @@ require_relative 'address.rb'
104
209
  calc_dimensions
105
210
  end
106
211
 
212
+ #
213
+ # Return the header section of the data
214
+ #
215
+
216
+ def headers
217
+ sheet.header_rows > 0 ? @data[ 0..sheet.header_rows-1 ] : nil
218
+ end
219
+
220
+ #
221
+ # Find a Column index by header
222
+ #
223
+ # @param [String] header the Column header to search for
224
+ # @return [Fixnum] the index of the given header
225
+ #
226
+
227
+ def index_by_header( header )
228
+ col_index( sheet.header_rows > 0 ? colref_by_header( header ) : header )
229
+ end
230
+
231
+ #
232
+ # Insert blank Columns into the data
233
+ #
234
+ # @param [String, Fixnum] before the Column reference to insert before.
235
+ # @param [Fixnum] number the number of new Columns to insert
236
+ #
237
+
107
238
  def insert_columns( before, number=1 )
108
239
  a = Array.new( number, nil )
109
240
  before = col_index( before ) - 1
@@ -111,45 +242,117 @@ require_relative 'address.rb'
111
242
  calc_dimensions
112
243
  end
113
244
 
245
+ #
246
+ # Insert blank Rows into the data
247
+ #
248
+ # @param [Fixnum] before the Row index to insert before.
249
+ # @param [Fixnum] number the number of new Rows to insert
250
+ #
251
+
114
252
  def insert_rows( before, number=1 )
115
253
  @data = @data.insert( ( col_index( before ) - 1 ), *Array.new( number, [nil] ) )
116
254
  calc_dimensions
117
255
  end
118
256
 
257
+ #
258
+ # Return the data without headers
259
+ #
260
+
119
261
  def no_headers
120
262
  @data[ sheet.header_rows..-1 ]
121
263
  end
122
264
 
265
+ #
266
+ # Split the data into two sections by evaluating each value in a column
267
+ #
268
+ # @param [String] header the header of the Column which contains the yield value
269
+ # @yield [value] yields the value of each row under the given header
270
+ #
271
+
272
+ def partition( header, &block )
273
+ idx = index_by_header( header )
274
+ d1, d2 = no_headers.partition { |row| yield row[ idx -1 ] }
275
+ [ headers + d1, headers + d2 ] if headers
276
+ end
277
+
278
+ #
279
+ # Read a value by address
280
+ #
281
+
123
282
  def read( addr )
124
283
  row_idx, col_idx = address_to_indices( addr )
125
284
  @data[ row_idx-1 ][ col_idx-1 ]
126
285
  end
127
286
  alias [] read
128
287
 
288
+ #
289
+ # Reverse the data Columns
290
+ #
291
+
129
292
  def reverse_columns!
130
293
  ensure_shape
131
294
  @data = @data.transpose.reverse.transpose
132
295
  end
296
+
297
+ #
298
+ # Reverse the data Rows (without affecting the headers)
299
+ #
133
300
 
134
301
  def reverse_rows!
135
302
  @data = skip_headers &:reverse
136
303
  end
304
+
305
+ #
306
+ # Perform an operation on the data without affecting the headers
307
+ #
308
+ # @yield [data] yield the data without the headers
309
+ # @return [Array<Array>] returns the data with the block operation performed on it, and the headers back in place
310
+ #
137
311
 
312
+ def skip_headers
313
+ return to_enum(:skip_headers) unless block_given?
314
+ hr = sheet.header_rows
315
+ if hr > 0
316
+ @data[ 0..hr - 1 ] + yield( @data[ hr..-1 ] )
317
+ else
318
+ yield( @data )
319
+ end
320
+ end
321
+
322
+ #
323
+ # Sort the data according to the block
324
+ #
325
+
138
326
  def sort!( &block )
139
327
  @data = skip_headers { |d| d.sort( &block ) }; self
140
328
  end
141
329
 
330
+ #
331
+ # Sort the data according to the block value
332
+ #
333
+
142
334
  def sort_by!( &block )
143
335
  @data = skip_headers { |d| d.sort_by( &block ) }
144
336
  end
145
337
 
338
+ #
339
+ # Unique the rows according to the values within a Column, selected by header
340
+ #
341
+
146
342
  def uniq!( header )
147
343
  column = col_index( colref_by_header( header ) )
148
- @data = @data.uniq { |row| row[ column - 1 ] }
344
+ @data = skip_headers { |d| d.uniq { |row| row[ column - 1 ] } }
149
345
  calc_dimensions
150
346
  end
151
347
  alias unique! uniq!
152
348
 
349
+ #
350
+ # Write a value into the data
351
+ #
352
+ # @param [String] addr the address to write the value to
353
+ # @param val the value to write to the address
354
+ #
355
+
153
356
  def write( addr, val )
154
357
  row_idx, col_idx = address_to_indices( addr )
155
358
  ( row_idx - rows ).times { @data << [] }
@@ -157,12 +360,6 @@ require_relative 'address.rb'
157
360
  calc_dimensions
158
361
  end
159
362
  alias []= write
160
-
161
- include Enumerable
162
-
163
- def each
164
- @data.each { |ar| yield ar }
165
- end
166
363
 
167
364
  private
168
365
 
@@ -175,15 +372,6 @@ require_relative 'address.rb'
175
372
  @data = @data.map { |ar| ar.length == cols ? ar : ar + Array.new( cols - ar.length, nil) }
176
373
  end
177
374
 
178
- def skip_headers
179
- hr = sheet.header_rows
180
- if hr > 0
181
- block_given? ? @data[ 0..hr - 1 ] + yield( @data[ hr..-1 ] ) : @data[ hr..-1 ]
182
- else
183
- block_given? ? yield( @data ) : @data
184
- end
185
- end
186
-
187
375
  end
188
376
 
189
377
  end
@@ -1,10 +1,32 @@
1
1
  module RubyExcel
2
2
 
3
3
  class Element
4
+ include Address
5
+ include Enumerable
4
6
 
5
- attr_reader :sheet, :address, :data, :column, :row
7
+ #The parent Sheet
8
+ attr_reader :sheet
6
9
  alias parent sheet
7
-
10
+
11
+ #The address
12
+ attr_reader :address
13
+
14
+ #The Data underlying the Sheet
15
+ attr_reader :data
16
+
17
+ #The first Column id in the address
18
+ attr_reader :column
19
+
20
+ #The first Row id in the address
21
+ attr_reader :row
22
+
23
+ #
24
+ # Creates a RubyExcel::Element instance
25
+ #
26
+ # @param [RubyExcel::Sheet] sheet the parent Sheet
27
+ # @param [String] addr the address to reference
28
+ #
29
+
8
30
  def initialize( sheet, addr )
9
31
  fail ArgumentError, "Invalid range: #{ addr }" unless addr =~ /\A[A-Z]+\d+:[A-Z]+\d+\z|\A[A-Z]+\d+\z/
10
32
  @sheet = sheet
@@ -13,17 +35,31 @@ module RubyExcel
13
35
  @column = column_id( addr )
14
36
  @row = row_id( addr )
15
37
  end
38
+
39
+ #
40
+ # Delete the data referenced by self.address
41
+ #
16
42
 
17
43
  def delete
18
44
  data.delete( self )
19
45
  end
20
-
21
- include Address
46
+
47
+ #
48
+ # Return the value at this Element's address
49
+ #
50
+ # @return [Object, Array<Object>] the Object or Array of Objects within the data, referenced by the address
51
+ #
22
52
 
23
53
  def value
24
54
  address.include?( ':' ) ? expand( address ).map { |ar| ar.map { |addr| data[ addr ] } } : data[ address ]
25
55
  end
26
56
 
57
+ #
58
+ # Set the value at this Element's address
59
+ #
60
+ # @param [Object, Array<Object>] val the Object or Array of Objects to write into the data
61
+ #
62
+
27
63
  def value=( val )
28
64
  if address.include? ':'
29
65
  if multi_array?( val )
@@ -36,30 +72,55 @@ module RubyExcel
36
72
  end
37
73
  self
38
74
  end
75
+
76
+ #
77
+ # The data at address as a TSV String
78
+ #
39
79
 
40
80
  def to_s
41
81
  value.is_a?( Array ) ? value.map { |ar| ar.join "\t" }.join($/) : value.to_s
42
82
  end
43
83
 
84
+ #
85
+ # View the object for debugging
86
+ #
87
+
44
88
  def inspect
45
- "#{ self.class }:0x#{ '%x' % (object_id << 1) }: #{ address }"
89
+ "#{ self.class }:0x#{ '%x' % ( object_id << 1 ) }: #{ address }"
46
90
  end
47
91
 
48
- include Enumerable
92
+ #
93
+ # Yields each value in the data referenced by the address
94
+ #
49
95
 
50
96
  def each
97
+ return to_enum( :each ) unless block_given?
51
98
  expand( address ).flatten.each { |addr| yield data[ addr ] }
52
99
  end
53
100
 
101
+ #
102
+ # Yields each Element referenced by the address
103
+ #
104
+
54
105
  def each_cell
106
+ return to_enum( :each_cell ) unless block_given?
55
107
  expand( address ).flatten.each { |addr| yield Element.new( sheet, addr ) }
56
108
  end
57
109
 
110
+ #
111
+ # Checks whether the data referenced by the address is empty
112
+ #
113
+
58
114
  def empty?
59
115
  all? { |v| v.to_s.empty? }
60
116
  end
61
117
 
118
+ #
119
+ # Replaces each value with the result of the block
120
+ #
121
+
62
122
  def map!
123
+ return to_enum( :map! ) unless block_given?
63
124
  expand( address ).flatten.each { |addr| data[ addr ] = yield data[ addr ] }
64
125
  end
65
126