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