rubyexcel 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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