rubyexcel 0.3.9 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,188 +1,188 @@
1
- module RubyExcel
2
-
3
- #
4
- #Provides address translation methods to RubyExcel's classes
5
- #
6
-
7
- module Address
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
-
16
- def address_to_col_index( address )
17
- col_index( column_id( address ) )
18
- end
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
-
27
- def address_to_indices( address )
28
- [ row_id( address ), address_to_col_index( address ) ]
29
- end
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
-
38
- def col_index( letter )
39
- return letter if letter.is_a? Fixnum
40
- letter !~ /[^A-Z]/ && [1,2,3].include?( letter.length ) or fail ArgumentError, "Invalid column reference: #{ letter }"
41
- idx, a = 1, 'A'
42
- loop { return idx if a == letter; idx+=1; a.next! }
43
- end
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
-
52
- def col_letter( index, start='A' )
53
- return index if index.is_a? String
54
- index > 0 or fail ArgumentError, 'Indexing is 1-based'
55
- a = start.dup; ( index - 1 ).times { a.next! }; a
56
- end
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
-
65
- def column_id( address )
66
- address[/[A-Z]+/i].upcase
67
- end
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
-
76
- def expand( address )
77
- return [[address]] unless address.include? ':'
78
-
79
- #Extract the relevant boundaries
80
- case address
81
-
82
- # Row
83
- when /\A(\d+):(\d+)\z/
84
-
85
- start_col, end_col, start_row, end_row = [ 'A', col_letter( sheet.maxcol ) ] + [ $1.to_i, $2.to_i ].sort
86
-
87
- # Column
88
- when /\A([A-Z]+):([A-Z]+)\z/
89
-
90
- start_col, end_col, start_row, end_row = [ $1, $2 ].sort + [ 1, sheet.maxrow ]
91
-
92
- # Range
93
- when /([A-Z]+)(\d+):([A-Z]+)(\d+)/
94
-
95
- start_col, end_col, start_row, end_row = [ $1, $3 ].sort + [ $2.to_i, $4.to_i ].sort
96
-
97
- # Invalid
98
- else
99
- fail ArgumentError, 'Invalid address: ' + address
100
- end
101
-
102
- # Return the array of addresses
103
- ( start_row..end_row ).map { |r| ( start_col..end_col ).map { |c| c + r.to_s } }
104
-
105
- end
106
-
107
- #
108
- # Translates indices to an address
109
- #
110
- # @param [Fixnum] row_idx the row index
111
- # @param [Fixnum] column_idx the column index
112
- # @return [String] the corresponding address
113
- #
114
-
115
- def indices_to_address( row_idx, column_idx )
116
- [ row_idx, column_idx ].all? { |a| a.is_a?( Fixnum ) } or fail ArgumentError, 'Input must be Fixnum'
117
- col_letter( column_idx ) + row_idx.to_s
118
- end
119
-
120
- #
121
- # Checks whether an object is a multidimensional Array
122
- #
123
- # @param [Object] obj the object to test
124
- # @return [Boolean] whether the object is a multidimensional Array
125
- #
126
-
127
- def multi_array?( obj )
128
- obj.all? { |el| el.is_a?( Array ) } && obj.is_a?( Array ) rescue false
129
- end
130
-
131
- #
132
- # Offsets an address by row and column
133
- #
134
- # @param [String] address the address to offset
135
- # @param [Fixnum] row the number of rows to offset by
136
- # @param [Fixnum] col the number of columns to offset by
137
- # @return [String] the new address
138
- #
139
-
140
- def offset(address, row, col)
141
- ( col_letter( address_to_col_index( address ) + col ) ) + ( row_id( address ) + row ).to_s
142
- end
143
-
144
- #
145
- # Translates an address to a row id
146
- #
147
- # @param [String] address the address to translate
148
- # @return [Fixnum] the row id
149
- #
150
-
151
- def row_id( address )
152
- Integer( address[/\d+/] )
153
- end
154
-
155
- #
156
- # Step an index forward for an Array-style slice
157
- #
158
- # @param [Fixnum, String] start the index to start at
159
- # @param [Fixnum] slice the amount to advance to (1 means keep the same index)
160
- #
161
-
162
- def step_index( start, slice )
163
- if start.is_a?( Fixnum )
164
- start + slice - 1
165
- else
166
- x = start.dup
167
- ( slice - 1 ).times { x.next! }
168
- x
169
- end
170
- end
171
-
172
- #
173
- # Translates two objects to a range address
174
- #
175
- # @param [String, RubyExcel::Element] obj1 the first address element
176
- # @param [String, RubyExcel::Element] obj2 the second address element
177
- # @return [String] the new address
178
- #
179
-
180
- def to_range_address( obj1, obj2 )
181
- addr = obj1.respond_to?( :address ) ? obj1.address : obj1.to_s
182
- addr << ':' + ( obj2.respond_to?( :address ) ? obj2.address : obj2.to_s ) if obj2
183
- addr
184
- end
185
-
186
- end
187
-
1
+ module RubyExcel
2
+
3
+ #
4
+ #Provides address translation methods to RubyExcel's classes
5
+ #
6
+
7
+ module Address
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
+
16
+ def address_to_col_index( address )
17
+ col_index( column_id( address ) )
18
+ end
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
+
27
+ def address_to_indices( address )
28
+ [ row_id( address ), address_to_col_index( address ) ]
29
+ end
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
+
38
+ def col_index( letter )
39
+ return letter if letter.is_a? Fixnum
40
+ letter !~ /[^A-Z]/ && [1,2,3].include?( letter.length ) or fail ArgumentError, "Invalid column reference: #{ letter }"
41
+ idx, a = 1, 'A'
42
+ loop { return idx if a == letter; idx+=1; a.next! }
43
+ end
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
+
52
+ def col_letter( index, start='A' )
53
+ return index if index.is_a? String
54
+ index > 0 or fail ArgumentError, 'Indexing is 1-based'
55
+ a = start.dup; ( index - 1 ).times { a.next! }; a
56
+ end
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
+
65
+ def column_id( address )
66
+ address[/[A-Z]+/i].upcase
67
+ end
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
+
76
+ def expand( address )
77
+ return [[address]] unless address.include? ':'
78
+
79
+ #Extract the relevant boundaries
80
+ case address
81
+
82
+ # Row
83
+ when /\A(\d+):(\d+)\z/
84
+
85
+ start_col, end_col, start_row, end_row = [ 'A', col_letter( sheet.maxcol ) ] + [ $1.to_i, $2.to_i ].sort
86
+
87
+ # Column
88
+ when /\A([A-Z]+):([A-Z]+)\z/
89
+
90
+ start_col, end_col, start_row, end_row = [ $1, $2 ].sort + [ 1, sheet.maxrow ]
91
+
92
+ # Range
93
+ when /([A-Z]+)(\d+):([A-Z]+)(\d+)/
94
+
95
+ start_col, end_col, start_row, end_row = [ $1, $3 ].sort + [ $2.to_i, $4.to_i ].sort
96
+
97
+ # Invalid
98
+ else
99
+ fail ArgumentError, 'Invalid address: ' + address
100
+ end
101
+
102
+ # Return the array of addresses
103
+ ( start_row..end_row ).map { |r| ( start_col..end_col ).map { |c| c + r.to_s } }
104
+
105
+ end
106
+
107
+ #
108
+ # Translates indices to an address
109
+ #
110
+ # @param [Fixnum] row_idx the row index
111
+ # @param [Fixnum] column_idx the column index
112
+ # @return [String] the corresponding address
113
+ #
114
+
115
+ def indices_to_address( row_idx, column_idx )
116
+ [ row_idx, column_idx ].all? { |a| a.is_a?( Fixnum ) } or fail ArgumentError, 'Input must be Fixnum'
117
+ col_letter( column_idx ) + row_idx.to_s
118
+ end
119
+
120
+ #
121
+ # Checks whether an object is a multidimensional Array
122
+ #
123
+ # @param [Object] obj the object to test
124
+ # @return [Boolean] whether the object is a multidimensional Array
125
+ #
126
+
127
+ def multi_array?( obj )
128
+ obj.all? { |el| el.is_a?( Array ) } && obj.is_a?( Array ) rescue false
129
+ end
130
+
131
+ #
132
+ # Offsets an address by row and column
133
+ #
134
+ # @param [String] address the address to offset
135
+ # @param [Fixnum] row the number of rows to offset by
136
+ # @param [Fixnum] col the number of columns to offset by
137
+ # @return [String] the new address
138
+ #
139
+
140
+ def offset(address, row, col)
141
+ ( col_letter( address_to_col_index( address ) + col ) ) + ( row_id( address ) + row ).to_s
142
+ end
143
+
144
+ #
145
+ # Translates an address to a row id
146
+ #
147
+ # @param [String] address the address to translate
148
+ # @return [Fixnum] the row id
149
+ #
150
+
151
+ def row_id( address )
152
+ Integer( address[/\d+/] )
153
+ end
154
+
155
+ #
156
+ # Step an index forward for an Array-style slice
157
+ #
158
+ # @param [Fixnum, String] start the index to start at
159
+ # @param [Fixnum] slice the amount to advance to (1 means keep the same index)
160
+ #
161
+
162
+ def step_index( start, slice )
163
+ if start.is_a?( Fixnum )
164
+ start + slice - 1
165
+ else
166
+ x = start.dup
167
+ ( slice - 1 ).times { x.next! }
168
+ x
169
+ end
170
+ end
171
+
172
+ #
173
+ # Translates two objects to a range address
174
+ #
175
+ # @param [String, RubyExcel::Element] obj1 the first address element
176
+ # @param [String, RubyExcel::Element] obj2 the second address element
177
+ # @return [String] the new address
178
+ #
179
+
180
+ def to_range_address( obj1, obj2 )
181
+ addr = obj1.respond_to?( :address ) ? obj1.address : obj1.to_s
182
+ addr << ':' + ( obj2.respond_to?( :address ) ? obj2.address : obj2.to_s ) if obj2
183
+ addr
184
+ end
185
+
186
+ end
187
+
188
188
  end
@@ -1,451 +1,451 @@
1
- module RubyExcel
2
-
3
- require_relative 'address.rb'
4
-
5
- #
6
- # The class which holds a Sheet's data
7
- #
8
- # @note This class is exposed to the API purely for debugging.
9
- #
10
-
11
- class Data
12
- include Address
13
- include Enumerable
14
-
15
- #The number of rows in the data
16
- attr_reader :rows
17
-
18
- #The number of columns in the data
19
- attr_reader :cols
20
-
21
- #The parent Sheet
22
- attr_accessor :sheet
23
- alias parent sheet
24
-
25
- #
26
- # Creates a RubyExcel::Data instance
27
- #
28
- # @param [RubyExcel::Sheet] sheet the parent Sheet
29
- # @param [Array<Array>] input_data the multidimensional Array which holds the data
30
- #
31
-
32
- def initialize( sheet, input_data )
33
- ( input_data.kind_of?( Array ) && input_data.all? { |el| el.kind_of?( Array ) } ) or fail ArgumentError, 'Input must be Array of Arrays'
34
- @sheet = sheet
35
- @data = input_data.dup
36
- calc_dimensions
37
- end
38
-
39
- #
40
- # Append an object to Data
41
- #
42
- # @param [Object] other the data to append
43
- # @return [self]
44
- #
45
-
46
- def <<( other )
47
- case other
48
- when Array
49
- if multi_array?( other )
50
- all.all?(&:empty?) ? @data = other : @data += other
51
- else
52
- all.all?(&:empty?) ? @data = [ other ] : @data << other
53
- end
54
- when Hash ; @data += _convert_hash( other )
55
- when Sheet ; empty? ? @data = other.data.all.dup : @data += other.data.dup.no_headers
56
- when Row ; @data << other.to_a.dup
57
- when Column ; @data.map!.with_index { |row, i| row << other[ i+1 ] }
58
- else ; @data[0] << other
59
- end
60
- calc_dimensions
61
- self
62
- end
63
-
64
- # @overload advanced_filter!( header, comparison_operator, search_criteria, ... )
65
- # Filter on multiple criteria
66
- #
67
- # @example Filter to 'Part': 'Type1' and 'Type3', with 'Qty' greater than 1
68
- # s.advanced_filter!( 'Part', :=~, /Type[13]/, 'Qty', :>, 1 )
69
- #
70
- # @example Filter to 'Part': 'Type1', with 'Ref1' containing 'X'
71
- # s.advanced_filter!( 'Part', :==, 'Type1', 'Ref1', :include?, 'X' )
72
- #
73
- # @param [String] header a header to search under
74
- # @param [Symbol] comparison_operator the operator to compare with
75
- # @param [Object] search_criteria the value to filter by
76
- # @raise [ArgumentError] 'Number of arguments must be a multiple of 3'
77
- # @raise [ArgumentError] 'Operator must be a symbol'
78
- #
79
-
80
- def advanced_filter!( *args )
81
- hrows = sheet.header_rows
82
- args.length % 3 == 0 or fail ArgumentError, 'Number of arguments must be a multiple of 3'
83
- 1.step( args.length - 2, 3 ) { |i| args[i].is_a?( Symbol ) or fail ArgumentError, 'Operator must be a symbol: ' + args[i].to_s }
84
- 0.step( args.length - 3, 3 ) { |i| index_by_header( args[i] ) }
85
-
86
- @data = @data.select.with_index do |row, i|
87
- if hrows > i
88
- true
89
- else
90
- args.each_slice(3).map do |h, op, crit|
91
- row[ index_by_header( h ) - 1 ].send( op, crit )
92
- end.all?
93
- end
94
- end
95
- calc_dimensions
96
- end
97
-
98
- #
99
- # Returns a copy of the data
100
- #
101
- # @return [Array<Array>]
102
- #
103
-
104
- def all
105
- @data.dup
106
- end
107
-
108
- #
109
- # Finds a Column reference by a header
110
- #
111
- # @param [String] header the header to search for
112
- # @return [String] the Column reference
113
- # @raise [NoMethodError] 'No header rows present'
114
- # @raise [IndexError] header.to_s + ' is not a valid header'
115
- #
116
-
117
- def colref_by_header( header )
118
- return header.idx if header.is_a?( Column )
119
- sheet.header_rows > 0 or fail NoMethodError, 'No header rows present'
120
- @data[ 0..sheet.header_rows-1 ].each { |r| idx = r.index( header ); return col_letter( idx+1 ) if idx }
121
- fail IndexError, header.to_s + ' is not a valid header'
122
- end
123
-
124
- #
125
- # Removes empty rows and columns from the data
126
- #
127
-
128
- def compact!
129
- compact_columns!
130
- compact_rows!
131
- end
132
-
133
- #
134
- # Removes empty columns from the data
135
- #
136
-
137
- def compact_columns!
138
- ensure_shape
139
- @data = @data.transpose.delete_if { |ar| ar.all? { |el| el.to_s.empty? } || ar.empty? }.transpose
140
- calc_dimensions
141
- end
142
-
143
- #
144
- # Removes empty rows from the data
145
- #
146
-
147
- def compact_rows!
148
- @data.delete_if { |ar| ar.all? { |el| el.to_s.empty? } || ar.empty? }
149
- calc_dimensions
150
- end
151
-
152
- #
153
- # Deletes the data referenced by an object
154
- #
155
- # @param [RubyExcel::Column, RubyExcel::Element, RubyExcel::Row] object the object to delete
156
- # @raise [NoMethodError] object.class.to_s + ' is not supported"
157
- #
158
-
159
- def delete( object )
160
- case object
161
- when Row
162
- @data.slice!( object.idx - 1 )
163
- when Column
164
- idx = col_index( object.idx ) - 1
165
- @data.each { |r| r.slice! idx }
166
- when Element
167
- addresses = expand( object.address )
168
- indices = [ address_to_indices( addresses.first.first ), address_to_indices( addresses.last.last ) ].flatten.map { |n| n-1 }
169
- @data[ indices[0]..indices[2] ].each { |r| r.slice!( indices[1], indices[3] - indices[1] + 1 ) }
170
- @data.delete_if.with_index { |r,i| r.empty? && i.between?( indices[0], indices[2] ) }
171
- else
172
- fail NoMethodError, object.class.to_s + ' is not supported'
173
- end
174
- calc_dimensions
175
- end
176
-
177
- #
178
- # Wipe all data
179
- #
180
-
181
- def delete_all
182
- @data = [[]]
183
- end
184
-
185
- #
186
- # Deletes the data referenced by a column id
187
- #
188
-
189
- def delete_column( ref )
190
- delete( Column.new( sheet, ref ) )
191
- end
192
-
193
- #
194
- # Deletes the data referenced by a row id
195
- #
196
-
197
- def delete_row( ref )
198
- delete( Row.new( sheet, ref ) )
199
- end
200
-
201
- #
202
- # Deletes the data referenced by an address
203
- #
204
-
205
- def delete_range( ref )
206
- delete( Element.new( sheet, ref ) )
207
- end
208
-
209
- #
210
- # Return a copy of self
211
- #
212
- # @return [RubyExcel::Data]
213
- #
214
-
215
- def dup
216
- Data.new( sheet, @data.map(&:dup) )
217
- end
218
-
219
- #
220
- # Check whether the data (without headers) is empty
221
- #
222
- # @return [Boolean]
223
- #
224
-
225
- def empty?
226
- no_headers.empty? rescue true
227
- end
228
-
229
- #
230
- # Yields each "Row" as an Array
231
- #
232
-
233
- def each
234
- return to_enum( :each ) unless block_given?
235
- @data.each { |ar| yield ar }
236
- end
237
-
238
- #
239
- # Removes all Rows (omitting headers) where the block is falsey
240
- #
241
- # @param [String, Array] headers splat of the headers for the Columns to filter by
242
- # @yield [Array] the values at the intersections of Column and Row
243
- # @return [self]
244
- #
245
-
246
- def filter!( *headers )
247
- hrows = sheet.header_rows
248
- idx_array = headers.flatten.map { |header| index_by_header( header ) }.compact
249
- @data = @data.select.with_index { |row, i| hrows > i || yield( idx_array.length == 1 ? row[ idx_array[0] - 1 ] : idx_array.map { |idx| row[ idx -1 ] } ) }
250
- calc_dimensions
251
- end
252
-
253
- #
254
- # Select and re-order Columns by a list of headers
255
- #
256
- # @param [Array<String>] headers the ordered list of headers to keep
257
- # @note This method can accept either a list of arguments or an Array
258
- # @note Invalid headers will be skipped
259
- #
260
-
261
- def get_columns!( *headers )
262
- headers = headers.flatten
263
- hrow = sheet.header_rows - 1
264
- ensure_shape
265
- @data = @data.transpose.select{ |col| col[0..hrow].any?{ |val| headers.include?( val ) } }
266
- @data = @data.sort_by{ |col| headers.index( col[0..hrow].select { |val| headers.include?( val ) }.first ) || headers.length }.transpose
267
- calc_dimensions
268
- end
269
-
270
- #
271
- # Return the header section of the data
272
- #
273
-
274
- def headers
275
- return nil if sheet.header_rows.nil? || sheet.header_rows.zero?
276
- @data[ 0..sheet.header_rows-1 ]
277
- end
278
-
279
- #
280
- # Find a Column index by header
281
- #
282
- # @param [String] header the Column header to search for
283
- # @return [Fixnum] the index of the given header
284
- #
285
-
286
- def index_by_header( header )
287
- sheet.header_rows > 0 or fail NoMethodError, 'No header rows present'
288
- col_index( colref_by_header( header ) )
289
- end
290
-
291
- #
292
- # Insert blank Columns into the data
293
- #
294
- # @param [String, Fixnum] before the Column reference to insert before.
295
- # @param [Fixnum] number the number of new Columns to insert
296
- #
297
-
298
- def insert_columns( before, number=1 )
299
- a = Array.new( number, nil )
300
- before = col_index( before ) - 1
301
- @data.map! { |row| row.insert( before, *a ) }
302
- calc_dimensions
303
- end
304
-
305
- #
306
- # Insert blank Rows into the data
307
- #
308
- # @param [Fixnum] before the Row index to insert before.
309
- # @param [Fixnum] number the number of new Rows to insert
310
- #
311
-
312
- def insert_rows( before, number=1 )
313
- @data = @data.insert( ( col_index( before ) - 1 ), *Array.new( number, [nil] ) )
314
- calc_dimensions
315
- end
316
-
317
- #
318
- # Return the data without headers
319
- #
320
-
321
- def no_headers
322
- return @data unless sheet.header_rows
323
- @data[ sheet.header_rows..-1 ]
324
- end
325
-
326
- #
327
- # Split the data into two sections by evaluating each value in a column
328
- #
329
- # @param [String] header the header of the Column which contains the yield value
330
- # @yield [value] yields the value of each row under the given header
331
- #
332
-
333
- def partition( header, &block )
334
- copy = dup
335
- idx = index_by_header( header )
336
- d1, d2 = copy.no_headers.partition { |row| yield row[ idx -1 ] }
337
- [ copy.headers + d1, copy.headers.map(&:dup) + d2 ] if headers
338
- end
339
-
340
- #
341
- # Read a value by address
342
- #
343
-
344
- def read( addr )
345
- row_idx, col_idx = address_to_indices( addr )
346
- return nil if row_idx > rows
347
- @data[ row_idx-1 ][ col_idx-1 ]
348
- end
349
- alias [] read
350
-
351
- #
352
- # Reverse the data Columns
353
- #
354
-
355
- def reverse_columns!
356
- ensure_shape
357
- @data = @data.transpose.reverse.transpose
358
- end
359
-
360
- #
361
- # Reverse the data Rows (without affecting the headers)
362
- #
363
-
364
- def reverse_rows!
365
- @data = skip_headers &:reverse
366
- end
367
-
368
- #
369
- # Perform an operation on the data without affecting the headers
370
- #
371
- # @yield [data] yield the data without the headers
372
- # @return [Array<Array>] returns the data with the block operation performed on it, and the headers back in place
373
- #
374
-
375
- def skip_headers
376
- return to_enum(:skip_headers) unless block_given?
377
- hr = sheet.header_rows
378
- if hr > 0
379
- @data[ 0..hr - 1 ] + yield( @data[ hr..-1 ] )
380
- else
381
- yield( @data )
382
- end
383
- end
384
-
385
- #
386
- # Sort the data according to the block
387
- #
388
-
389
- def sort!( &block )
390
- @data = skip_headers { |d| d.sort( &block ) }; self
391
- end
392
-
393
- #
394
- # Sort the data according to the block value
395
- #
396
-
397
- def sort_by!( &block )
398
- @data = skip_headers { |d| d.sort_by( &block ) }; self
399
- end
400
-
401
- #
402
- # Unique the rows according to the values within a Column, selected by header
403
- #
404
-
405
- def uniq!( header )
406
- column = col_index( colref_by_header( header ) )
407
- @data = skip_headers { |d| d.uniq { |row| row[ column - 1 ] } }
408
- calc_dimensions
409
- end
410
- alias unique! uniq!
411
-
412
- #
413
- # Write a value into the data
414
- #
415
- # @param [String] addr the address to write the value to
416
- # @param val the value to write to the address
417
- #
418
-
419
- def write( addr, val )
420
- row_idx, col_idx = address_to_indices( addr )
421
- ( row_idx - rows ).times { @data << [] }
422
- @data[ row_idx-1 ][ col_idx-1 ] = val
423
- calc_dimensions if row_idx > rows || col_idx > cols
424
- val
425
- end
426
- alias []= write
427
-
428
- private
429
-
430
- def calc_dimensions
431
- @rows = ( @data.length rescue 0 )
432
- @cols = ( @data.max_by { |row| row.length }.length rescue 0 )
433
- self
434
- end
435
-
436
- def ensure_shape
437
- calc_dimensions
438
- @data = @data.map { |ar| ar.length == cols ? ar : ar + Array.new( cols - ar.length, nil) }
439
- end
440
-
441
- def _convert_hash(h)
442
- _hash_to_a(h).each_slice(2).map { |a1,a2| a1 << a2.last }
443
- end
444
-
445
- def _hash_to_a(h)
446
- h.map { |k,v| v.is_a?(Hash) ? _hash_to_a(v).map { |val| ([ k ] + [ val ]).flatten(1) } : [ k, v ] }.flatten(1)
447
- end
448
-
449
- end
450
-
1
+ module RubyExcel
2
+
3
+ require_relative 'address.rb'
4
+
5
+ #
6
+ # The class which holds a Sheet's data
7
+ #
8
+ # @note This class is exposed to the API purely for debugging.
9
+ #
10
+
11
+ class Data
12
+ include Address
13
+ include Enumerable
14
+
15
+ #The number of rows in the data
16
+ attr_reader :rows
17
+
18
+ #The number of columns in the data
19
+ attr_reader :cols
20
+
21
+ #The parent Sheet
22
+ attr_accessor :sheet
23
+ alias parent sheet
24
+
25
+ #
26
+ # Creates a RubyExcel::Data instance
27
+ #
28
+ # @param [RubyExcel::Sheet] sheet the parent Sheet
29
+ # @param [Array<Array>] input_data the multidimensional Array which holds the data
30
+ #
31
+
32
+ def initialize( sheet, input_data )
33
+ ( input_data.kind_of?( Array ) && input_data.all? { |el| el.kind_of?( Array ) } ) or fail ArgumentError, 'Input must be Array of Arrays'
34
+ @sheet = sheet
35
+ @data = input_data.dup
36
+ calc_dimensions
37
+ end
38
+
39
+ #
40
+ # Append an object to Data
41
+ #
42
+ # @param [Object] other the data to append
43
+ # @return [self]
44
+ #
45
+
46
+ def <<( other )
47
+ case other
48
+ when Array
49
+ if multi_array?( other )
50
+ all.all?(&:empty?) ? @data = other : @data += other
51
+ else
52
+ all.all?(&:empty?) ? @data = [ other ] : @data << other
53
+ end
54
+ when Hash ; @data += _convert_hash( other )
55
+ when Sheet ; empty? ? @data = other.data.all.dup : @data += other.data.dup.no_headers
56
+ when Row ; @data << other.to_a.dup
57
+ when Column ; @data.map!.with_index { |row, i| row << other[ i+1 ] }
58
+ else ; @data[0] << other
59
+ end
60
+ calc_dimensions
61
+ self
62
+ end
63
+
64
+ # @overload advanced_filter!( header, comparison_operator, search_criteria, ... )
65
+ # Filter on multiple criteria
66
+ #
67
+ # @example Filter to 'Part': 'Type1' and 'Type3', with 'Qty' greater than 1
68
+ # s.advanced_filter!( 'Part', :=~, /Type[13]/, 'Qty', :>, 1 )
69
+ #
70
+ # @example Filter to 'Part': 'Type1', with 'Ref1' containing 'X'
71
+ # s.advanced_filter!( 'Part', :==, 'Type1', 'Ref1', :include?, 'X' )
72
+ #
73
+ # @param [String] header a header to search under
74
+ # @param [Symbol] comparison_operator the operator to compare with
75
+ # @param [Object] search_criteria the value to filter by
76
+ # @raise [ArgumentError] 'Number of arguments must be a multiple of 3'
77
+ # @raise [ArgumentError] 'Operator must be a symbol'
78
+ #
79
+
80
+ def advanced_filter!( *args )
81
+ hrows = sheet.header_rows
82
+ args.length % 3 == 0 or fail ArgumentError, 'Number of arguments must be a multiple of 3'
83
+ 1.step( args.length - 2, 3 ) { |i| args[i].is_a?( Symbol ) or fail ArgumentError, 'Operator must be a symbol: ' + args[i].to_s }
84
+ 0.step( args.length - 3, 3 ) { |i| index_by_header( args[i] ) }
85
+
86
+ @data = @data.select.with_index do |row, i|
87
+ if hrows > i
88
+ true
89
+ else
90
+ args.each_slice(3).map do |h, op, crit|
91
+ row[ index_by_header( h ) - 1 ].send( op, crit )
92
+ end.all?
93
+ end
94
+ end
95
+ calc_dimensions
96
+ end
97
+
98
+ #
99
+ # Returns a copy of the data
100
+ #
101
+ # @return [Array<Array>]
102
+ #
103
+
104
+ def all
105
+ @data.dup
106
+ end
107
+
108
+ #
109
+ # Finds a Column reference by a header
110
+ #
111
+ # @param [String] header the header to search for
112
+ # @return [String] the Column reference
113
+ # @raise [NoMethodError] 'No header rows present'
114
+ # @raise [IndexError] header.to_s + ' is not a valid header'
115
+ #
116
+
117
+ def colref_by_header( header )
118
+ return header.idx if header.is_a?( Column )
119
+ sheet.header_rows > 0 or fail NoMethodError, 'No header rows present'
120
+ @data[ 0..sheet.header_rows-1 ].each { |r| idx = r.index( header ); return col_letter( idx+1 ) if idx }
121
+ fail IndexError, header.to_s + ' is not a valid header'
122
+ end
123
+
124
+ #
125
+ # Removes empty rows and columns from the data
126
+ #
127
+
128
+ def compact!
129
+ compact_columns!
130
+ compact_rows!
131
+ end
132
+
133
+ #
134
+ # Removes empty columns from the data
135
+ #
136
+
137
+ def compact_columns!
138
+ ensure_shape
139
+ @data = @data.transpose.delete_if { |ar| ar.all? { |el| el.to_s.empty? } || ar.empty? }.transpose
140
+ calc_dimensions
141
+ end
142
+
143
+ #
144
+ # Removes empty rows from the data
145
+ #
146
+
147
+ def compact_rows!
148
+ @data.delete_if { |ar| ar.all? { |el| el.to_s.empty? } || ar.empty? }
149
+ calc_dimensions
150
+ end
151
+
152
+ #
153
+ # Deletes the data referenced by an object
154
+ #
155
+ # @param [RubyExcel::Column, RubyExcel::Element, RubyExcel::Row] object the object to delete
156
+ # @raise [NoMethodError] object.class.to_s + ' is not supported"
157
+ #
158
+
159
+ def delete( object )
160
+ case object
161
+ when Row
162
+ @data.slice!( object.idx - 1 )
163
+ when Column
164
+ idx = col_index( object.idx ) - 1
165
+ @data.each { |r| r.slice! idx }
166
+ when Element
167
+ addresses = expand( object.address )
168
+ indices = [ address_to_indices( addresses.first.first ), address_to_indices( addresses.last.last ) ].flatten.map { |n| n-1 }
169
+ @data[ indices[0]..indices[2] ].each { |r| r.slice!( indices[1], indices[3] - indices[1] + 1 ) }
170
+ @data.delete_if.with_index { |r,i| r.empty? && i.between?( indices[0], indices[2] ) }
171
+ else
172
+ fail NoMethodError, object.class.to_s + ' is not supported'
173
+ end
174
+ calc_dimensions
175
+ end
176
+
177
+ #
178
+ # Wipe all data
179
+ #
180
+
181
+ def delete_all
182
+ @data = [[]]
183
+ end
184
+
185
+ #
186
+ # Deletes the data referenced by a column id
187
+ #
188
+
189
+ def delete_column( ref )
190
+ delete( Column.new( sheet, ref ) )
191
+ end
192
+
193
+ #
194
+ # Deletes the data referenced by a row id
195
+ #
196
+
197
+ def delete_row( ref )
198
+ delete( Row.new( sheet, ref ) )
199
+ end
200
+
201
+ #
202
+ # Deletes the data referenced by an address
203
+ #
204
+
205
+ def delete_range( ref )
206
+ delete( Element.new( sheet, ref ) )
207
+ end
208
+
209
+ #
210
+ # Return a copy of self
211
+ #
212
+ # @return [RubyExcel::Data]
213
+ #
214
+
215
+ def dup
216
+ Data.new( sheet, @data.map(&:dup) )
217
+ end
218
+
219
+ #
220
+ # Check whether the data (without headers) is empty
221
+ #
222
+ # @return [Boolean]
223
+ #
224
+
225
+ def empty?
226
+ no_headers.empty? rescue true
227
+ end
228
+
229
+ #
230
+ # Yields each "Row" as an Array
231
+ #
232
+
233
+ def each
234
+ return to_enum( :each ) unless block_given?
235
+ @data.each { |ar| yield ar }
236
+ end
237
+
238
+ #
239
+ # Removes all Rows (omitting headers) where the block is falsey
240
+ #
241
+ # @param [String, Array] headers splat of the headers for the Columns to filter by
242
+ # @yield [Array] the values at the intersections of Column and Row
243
+ # @return [self]
244
+ #
245
+
246
+ def filter!( *headers )
247
+ hrows = sheet.header_rows
248
+ idx_array = headers.flatten.map { |header| index_by_header( header ) }.compact
249
+ @data = @data.select.with_index { |row, i| hrows > i || yield( idx_array.length == 1 ? row[ idx_array[0] - 1 ] : idx_array.map { |idx| row[ idx -1 ] } ) }
250
+ calc_dimensions
251
+ end
252
+
253
+ #
254
+ # Select and re-order Columns by a list of headers
255
+ #
256
+ # @param [Array<String>] headers the ordered list of headers to keep
257
+ # @note This method can accept either a list of arguments or an Array
258
+ # @note Invalid headers will be skipped
259
+ #
260
+
261
+ def get_columns!( *headers )
262
+ headers = headers.flatten
263
+ hrow = sheet.header_rows - 1
264
+ ensure_shape
265
+ @data = @data.transpose.select{ |col| col[0..hrow].any?{ |val| headers.include?( val ) } }
266
+ @data = @data.sort_by{ |col| headers.index( col[0..hrow].select { |val| headers.include?( val ) }.first ) || headers.length }.transpose
267
+ calc_dimensions
268
+ end
269
+
270
+ #
271
+ # Return the header section of the data
272
+ #
273
+
274
+ def headers
275
+ return nil if sheet.header_rows.nil? || sheet.header_rows.zero?
276
+ @data[ 0..sheet.header_rows-1 ]
277
+ end
278
+
279
+ #
280
+ # Find a Column index by header
281
+ #
282
+ # @param [String] header the Column header to search for
283
+ # @return [Fixnum] the index of the given header
284
+ #
285
+
286
+ def index_by_header( header )
287
+ sheet.header_rows > 0 or fail NoMethodError, 'No header rows present'
288
+ col_index( colref_by_header( header ) )
289
+ end
290
+
291
+ #
292
+ # Insert blank Columns into the data
293
+ #
294
+ # @param [String, Fixnum] before the Column reference to insert before.
295
+ # @param [Fixnum] number the number of new Columns to insert
296
+ #
297
+
298
+ def insert_columns( before, number=1 )
299
+ a = Array.new( number, nil )
300
+ before = col_index( before ) - 1
301
+ @data.map! { |row| row.insert( before, *a ) }
302
+ calc_dimensions
303
+ end
304
+
305
+ #
306
+ # Insert blank Rows into the data
307
+ #
308
+ # @param [Fixnum] before the Row index to insert before.
309
+ # @param [Fixnum] number the number of new Rows to insert
310
+ #
311
+
312
+ def insert_rows( before, number=1 )
313
+ @data = @data.insert( ( col_index( before ) - 1 ), *Array.new( number, [nil] ) )
314
+ calc_dimensions
315
+ end
316
+
317
+ #
318
+ # Return the data without headers
319
+ #
320
+
321
+ def no_headers
322
+ return @data unless sheet.header_rows
323
+ @data[ sheet.header_rows..-1 ]
324
+ end
325
+
326
+ #
327
+ # Split the data into two sections by evaluating each value in a column
328
+ #
329
+ # @param [String] header the header of the Column which contains the yield value
330
+ # @yield [value] yields the value of each row under the given header
331
+ #
332
+
333
+ def partition( header, &block )
334
+ copy = dup
335
+ idx = index_by_header( header )
336
+ d1, d2 = copy.no_headers.partition { |row| yield row[ idx -1 ] }
337
+ [ copy.headers + d1, copy.headers.map(&:dup) + d2 ] if headers
338
+ end
339
+
340
+ #
341
+ # Read a value by address
342
+ #
343
+
344
+ def read( addr )
345
+ row_idx, col_idx = address_to_indices( addr )
346
+ return nil if row_idx > rows
347
+ @data[ row_idx-1 ][ col_idx-1 ]
348
+ end
349
+ alias [] read
350
+
351
+ #
352
+ # Reverse the data Columns
353
+ #
354
+
355
+ def reverse_columns!
356
+ ensure_shape
357
+ @data = @data.transpose.reverse.transpose
358
+ end
359
+
360
+ #
361
+ # Reverse the data Rows (without affecting the headers)
362
+ #
363
+
364
+ def reverse_rows!
365
+ @data = skip_headers &:reverse
366
+ end
367
+
368
+ #
369
+ # Perform an operation on the data without affecting the headers
370
+ #
371
+ # @yield [data] yield the data without the headers
372
+ # @return [Array<Array>] returns the data with the block operation performed on it, and the headers back in place
373
+ #
374
+
375
+ def skip_headers
376
+ return to_enum(:skip_headers) unless block_given?
377
+ hr = sheet.header_rows
378
+ if hr > 0
379
+ @data[ 0..hr - 1 ] + yield( @data[ hr..-1 ] )
380
+ else
381
+ yield( @data )
382
+ end
383
+ end
384
+
385
+ #
386
+ # Sort the data according to the block
387
+ #
388
+
389
+ def sort!( &block )
390
+ @data = skip_headers { |d| d.sort( &block ) }; self
391
+ end
392
+
393
+ #
394
+ # Sort the data according to the block value
395
+ #
396
+
397
+ def sort_by!( &block )
398
+ @data = skip_headers { |d| d.sort_by( &block ) }; self
399
+ end
400
+
401
+ #
402
+ # Unique the rows according to the values within a Column, selected by header
403
+ #
404
+
405
+ def uniq!( header )
406
+ column = col_index( colref_by_header( header ) )
407
+ @data = skip_headers { |d| d.uniq { |row| row[ column - 1 ] } }
408
+ calc_dimensions
409
+ end
410
+ alias unique! uniq!
411
+
412
+ #
413
+ # Write a value into the data
414
+ #
415
+ # @param [String] addr the address to write the value to
416
+ # @param val the value to write to the address
417
+ #
418
+
419
+ def write( addr, val )
420
+ row_idx, col_idx = address_to_indices( addr )
421
+ ( row_idx - rows ).times { @data << [] }
422
+ @data[ row_idx-1 ][ col_idx-1 ] = val
423
+ calc_dimensions if row_idx > rows || col_idx > cols
424
+ val
425
+ end
426
+ alias []= write
427
+
428
+ private
429
+
430
+ def calc_dimensions
431
+ @rows = ( @data.length rescue 0 )
432
+ @cols = ( @data.max_by { |row| row.length }.length rescue 0 )
433
+ self
434
+ end
435
+
436
+ def ensure_shape
437
+ calc_dimensions
438
+ @data = @data.map { |ar| ar.length == cols ? ar : ar + Array.new( cols - ar.length, nil) }
439
+ end
440
+
441
+ def _convert_hash(h)
442
+ _hash_to_a(h).each_slice(2).map { |a1,a2| a1 << a2.last }
443
+ end
444
+
445
+ def _hash_to_a(h)
446
+ h.map { |k,v| v.is_a?(Hash) ? _hash_to_a(v).map { |val| ([ k ] + [ val ]).flatten(1) } : [ k, v ] }.flatten(1)
447
+ end
448
+
449
+ end
450
+
451
451
  end