rubyexcel 0.3.9 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.md +9 -9
- data/README.md +782 -779
- data/lib/rubyexcel.rb +264 -260
- data/lib/rubyexcel/address.rb +187 -187
- data/lib/rubyexcel/data.rb +450 -450
- data/lib/rubyexcel/element.rb +226 -226
- data/lib/rubyexcel/excel_tools.rb +287 -261
- data/lib/rubyexcel/rubyexcel_components.rb +70 -70
- data/lib/rubyexcel/section.rb +376 -376
- data/lib/rubyexcel/sheet.rb +720 -720
- metadata +6 -7
data/lib/rubyexcel/address.rb
CHANGED
@@ -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
|
data/lib/rubyexcel/data.rb
CHANGED
@@ -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
|