vfcsv 1.0.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 +7 -0
- data/.tool-versions +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +65 -0
- data/LICENSE +21 -0
- data/README.md +268 -0
- data/Rakefile +37 -0
- data/bench/run_all_jit.sh +20 -0
- data/bench/vs_competitors.rb +253 -0
- data/bench/vs_stdlib.rb +137 -0
- data/ext/vfcsv_rust/Cargo.lock +289 -0
- data/ext/vfcsv_rust/Cargo.toml +27 -0
- data/ext/vfcsv_rust/extconf.rb +6 -0
- data/ext/vfcsv_rust/src/lib.rs +476 -0
- data/lib/vfcsv/row.rb +296 -0
- data/lib/vfcsv/table.rb +270 -0
- data/lib/vfcsv/version.rb +5 -0
- data/lib/vfcsv.rb +568 -0
- data/vfcsv.gemspec +43 -0
- metadata +149 -0
data/lib/vfcsv/row.rb
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class VFCSV
|
|
4
|
+
# A CSV::Row-compatible class representing a single row of CSV data.
|
|
5
|
+
# Supports both header-based (Hash-like) and index-based (Array-like) access.
|
|
6
|
+
class Row
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :row
|
|
10
|
+
|
|
11
|
+
# Create a new Row
|
|
12
|
+
# @param headers [Array] Column headers
|
|
13
|
+
# @param fields [Array] Field values
|
|
14
|
+
# @param header_row [Boolean] Whether this is a header row (default: false)
|
|
15
|
+
def initialize(headers, fields, header_row: false)
|
|
16
|
+
@row = headers.zip(fields).map { |pair| pair }
|
|
17
|
+
@header_row = header_row
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get headers
|
|
21
|
+
# @return [Array] Column headers
|
|
22
|
+
def headers
|
|
23
|
+
@row.map(&:first)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get field values
|
|
27
|
+
# @return [Array] Field values
|
|
28
|
+
def fields
|
|
29
|
+
@row.map(&:last)
|
|
30
|
+
end
|
|
31
|
+
alias_method :values, :fields
|
|
32
|
+
|
|
33
|
+
# Access a field by header name or index
|
|
34
|
+
# @param header_or_index [String, Integer] Header name or column index
|
|
35
|
+
# @param minimum_index [Integer] For duplicate headers, start searching from this index
|
|
36
|
+
# @return [String, nil] Field value
|
|
37
|
+
def [](header_or_index, minimum_index = 0)
|
|
38
|
+
if header_or_index.is_a?(Integer)
|
|
39
|
+
pair = @row[header_or_index]
|
|
40
|
+
pair&.last
|
|
41
|
+
else
|
|
42
|
+
# Search by header name
|
|
43
|
+
@row.each_with_index do |(header, value), index|
|
|
44
|
+
next if index < minimum_index
|
|
45
|
+
return value if header == header_or_index
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
alias_method :field, :[]
|
|
51
|
+
|
|
52
|
+
# Set a field by header name or index
|
|
53
|
+
# @param header_or_index [String, Integer] Header name or column index
|
|
54
|
+
# @param value [Object] New value
|
|
55
|
+
def []=(header_or_index, value)
|
|
56
|
+
if header_or_index.is_a?(Integer)
|
|
57
|
+
if @row[header_or_index]
|
|
58
|
+
@row[header_or_index][1] = value
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
@row.each do |pair|
|
|
62
|
+
if pair[0] == header_or_index
|
|
63
|
+
pair[1] = value
|
|
64
|
+
return value
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
# Header not found, add new pair
|
|
68
|
+
@row << [header_or_index, value]
|
|
69
|
+
end
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Append a field
|
|
74
|
+
# @param arg [Array] [header, value] pair
|
|
75
|
+
# @return [self]
|
|
76
|
+
def <<(arg)
|
|
77
|
+
if arg.is_a?(Array) && arg.size == 2
|
|
78
|
+
@row << arg.dup
|
|
79
|
+
else
|
|
80
|
+
@row << [nil, arg]
|
|
81
|
+
end
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Push a header and value
|
|
86
|
+
# @param header [String] Header name
|
|
87
|
+
# @param value [Object] Field value
|
|
88
|
+
# @return [self]
|
|
89
|
+
def push(header, value)
|
|
90
|
+
@row << [header, value]
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Delete a field by header or index
|
|
95
|
+
# @param header_or_index [String, Integer] Header name or index
|
|
96
|
+
# @return [Array, nil] Deleted [header, value] pair
|
|
97
|
+
def delete(header_or_index)
|
|
98
|
+
if header_or_index.is_a?(Integer)
|
|
99
|
+
@row.delete_at(header_or_index)
|
|
100
|
+
else
|
|
101
|
+
index = @row.index { |pair| pair[0] == header_or_index }
|
|
102
|
+
index ? @row.delete_at(index) : nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Delete fields matching a condition
|
|
107
|
+
# @yield [header, value] Block that returns true for fields to delete
|
|
108
|
+
# @return [self]
|
|
109
|
+
def delete_if(&block)
|
|
110
|
+
@row.delete_if { |pair| block.call(*pair) }
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Iterate over header/value pairs
|
|
115
|
+
# @yield [header, value]
|
|
116
|
+
# @return [Enumerator] if no block given
|
|
117
|
+
def each(&block)
|
|
118
|
+
return to_enum(__method__) unless block_given?
|
|
119
|
+
@row.each { |pair| block.call(*pair) }
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
alias_method :each_pair, :each
|
|
123
|
+
|
|
124
|
+
# Check if row is empty
|
|
125
|
+
# @return [Boolean]
|
|
126
|
+
def empty?
|
|
127
|
+
@row.empty?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get number of fields
|
|
131
|
+
# @return [Integer]
|
|
132
|
+
def size
|
|
133
|
+
@row.size
|
|
134
|
+
end
|
|
135
|
+
alias_method :length, :size
|
|
136
|
+
|
|
137
|
+
# Fetch a field with optional default
|
|
138
|
+
# @param header_or_index [String, Integer] Header or index
|
|
139
|
+
# @param default [Object] Default value if not found
|
|
140
|
+
# @yield Block to call if not found
|
|
141
|
+
# @return [Object] Field value or default
|
|
142
|
+
def fetch(header_or_index, *args, &block)
|
|
143
|
+
value = self[header_or_index]
|
|
144
|
+
return value unless value.nil?
|
|
145
|
+
|
|
146
|
+
# Check if header actually exists with nil value
|
|
147
|
+
found = if header_or_index.is_a?(Integer)
|
|
148
|
+
header_or_index >= 0 && header_or_index < @row.size
|
|
149
|
+
else
|
|
150
|
+
@row.any? { |pair| pair[0] == header_or_index }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return value if found
|
|
154
|
+
|
|
155
|
+
if block_given?
|
|
156
|
+
block.call(header_or_index)
|
|
157
|
+
elsif args.size > 0
|
|
158
|
+
args[0]
|
|
159
|
+
else
|
|
160
|
+
raise KeyError, "key not found: #{header_or_index.inspect}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if a value exists in the fields
|
|
165
|
+
# @param value [Object] Value to find
|
|
166
|
+
# @return [Boolean]
|
|
167
|
+
def field?(value)
|
|
168
|
+
fields.include?(value)
|
|
169
|
+
end
|
|
170
|
+
alias_method :include?, :field?
|
|
171
|
+
alias_method :member?, :field?
|
|
172
|
+
|
|
173
|
+
# Check if a header exists
|
|
174
|
+
# @param header [String] Header to find
|
|
175
|
+
# @return [Boolean]
|
|
176
|
+
def header?(header)
|
|
177
|
+
headers.include?(header)
|
|
178
|
+
end
|
|
179
|
+
alias_method :has_key?, :header?
|
|
180
|
+
alias_method :key?, :header?
|
|
181
|
+
|
|
182
|
+
# Find index of a header or value
|
|
183
|
+
# @param header [String] Header to find
|
|
184
|
+
# @param minimum_index [Integer] Start index
|
|
185
|
+
# @return [Integer, nil]
|
|
186
|
+
def index(header, minimum_index = 0)
|
|
187
|
+
@row.each_with_index do |(h, _v), i|
|
|
188
|
+
next if i < minimum_index
|
|
189
|
+
return i if h == header
|
|
190
|
+
end
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Dig into nested data
|
|
195
|
+
# @param index_or_header [Integer, String] First key
|
|
196
|
+
# @param identifiers [Array] Additional keys for nested access
|
|
197
|
+
# @return [Object]
|
|
198
|
+
def dig(index_or_header, *identifiers)
|
|
199
|
+
value = self[index_or_header]
|
|
200
|
+
return nil if value.nil?
|
|
201
|
+
return value if identifiers.empty?
|
|
202
|
+
|
|
203
|
+
if value.respond_to?(:dig)
|
|
204
|
+
value.dig(*identifiers)
|
|
205
|
+
else
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Get values at specified indices or headers
|
|
211
|
+
# @param indices_or_headers [Array] List of indices or headers
|
|
212
|
+
# @return [Array] Values at specified positions
|
|
213
|
+
def values_at(*indices_or_headers)
|
|
214
|
+
indices_or_headers.map { |i| self[i] }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check if this is a header row
|
|
218
|
+
# @return [Boolean]
|
|
219
|
+
def header_row?
|
|
220
|
+
@header_row
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Check if this is a field (data) row
|
|
224
|
+
# @return [Boolean]
|
|
225
|
+
def field_row?
|
|
226
|
+
!@header_row
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Compare with another row
|
|
230
|
+
# @param other [Row] Other row
|
|
231
|
+
# @return [Boolean]
|
|
232
|
+
def ==(other)
|
|
233
|
+
return false unless other.is_a?(Row)
|
|
234
|
+
@row == other.row
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Convert to hash
|
|
238
|
+
# @return [Hash]
|
|
239
|
+
def to_h
|
|
240
|
+
result = {}
|
|
241
|
+
@row.each { |header, value| result[header] = value }
|
|
242
|
+
result
|
|
243
|
+
end
|
|
244
|
+
alias_method :to_hash, :to_h
|
|
245
|
+
|
|
246
|
+
# Convert to array of [header, value] pairs
|
|
247
|
+
# @return [Array<Array>]
|
|
248
|
+
def to_a
|
|
249
|
+
@row.map(&:dup)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Allow implicit conversion to array (for splat operations)
|
|
253
|
+
# @return [Array] Field values only
|
|
254
|
+
def to_ary
|
|
255
|
+
fields
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Convert to CSV string
|
|
259
|
+
# @return [String]
|
|
260
|
+
def to_csv(**options)
|
|
261
|
+
col_sep = options[:col_sep] || ","
|
|
262
|
+
quote_char = options[:quote_char] || '"'
|
|
263
|
+
row_sep = options[:row_sep]
|
|
264
|
+
row_sep = "\n" if row_sep.nil? || row_sep == :auto
|
|
265
|
+
|
|
266
|
+
field_strings = fields.map do |field|
|
|
267
|
+
field_str = field.to_s
|
|
268
|
+
if needs_quoting?(field_str, col_sep, quote_char)
|
|
269
|
+
quote_field(field_str, quote_char)
|
|
270
|
+
else
|
|
271
|
+
field_str
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
field_strings.join(col_sep) + row_sep
|
|
276
|
+
end
|
|
277
|
+
alias_method :to_s, :to_csv
|
|
278
|
+
|
|
279
|
+
# Inspection string
|
|
280
|
+
# @return [String]
|
|
281
|
+
def inspect
|
|
282
|
+
"#<#{self.class} #{to_h.inspect}>"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def needs_quoting?(str, col_sep, quote_char)
|
|
288
|
+
str.include?(col_sep) || str.include?(quote_char) || str.include?("\n") || str.include?("\r")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def quote_field(str, quote_char)
|
|
292
|
+
escaped = str.gsub(quote_char, quote_char + quote_char)
|
|
293
|
+
"#{quote_char}#{escaped}#{quote_char}"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
data/lib/vfcsv/table.rb
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class VFCSV
|
|
4
|
+
# A CSV::Table-compatible class representing a collection of CSV rows.
|
|
5
|
+
# Supports multiple access modes: :row, :col, and :col_or_row (default).
|
|
6
|
+
class Table
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :table, :mode
|
|
10
|
+
|
|
11
|
+
# Create a new Table
|
|
12
|
+
# @param rows [Array<Row>] Array of Row objects
|
|
13
|
+
# @param headers [Array] Column headers (optional, derived from first row if not provided)
|
|
14
|
+
def initialize(rows = [], headers: nil)
|
|
15
|
+
@table = rows
|
|
16
|
+
@headers = headers || (rows.first&.headers || [])
|
|
17
|
+
@mode = :col_or_row
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get column headers
|
|
21
|
+
# @return [Array]
|
|
22
|
+
def headers
|
|
23
|
+
@headers.dup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Access rows or columns depending on mode
|
|
27
|
+
# @param index_or_header [Integer, String] Row index or column header
|
|
28
|
+
# @return [Row, Array] Row object or array of column values
|
|
29
|
+
def [](index_or_header)
|
|
30
|
+
case @mode
|
|
31
|
+
when :row
|
|
32
|
+
@table[index_or_header]
|
|
33
|
+
when :col
|
|
34
|
+
column_values(index_or_header)
|
|
35
|
+
when :col_or_row
|
|
36
|
+
if index_or_header.is_a?(Integer)
|
|
37
|
+
@table[index_or_header]
|
|
38
|
+
else
|
|
39
|
+
column_values(index_or_header)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Set row or column value depending on mode
|
|
45
|
+
# @param index_or_header [Integer, String] Row index or column header
|
|
46
|
+
# @param value [Row, Array] New row or column values
|
|
47
|
+
def []=(index_or_header, value)
|
|
48
|
+
case @mode
|
|
49
|
+
when :row
|
|
50
|
+
@table[index_or_header] = value
|
|
51
|
+
when :col
|
|
52
|
+
set_column(index_or_header, value)
|
|
53
|
+
when :col_or_row
|
|
54
|
+
if index_or_header.is_a?(Integer)
|
|
55
|
+
@table[index_or_header] = value
|
|
56
|
+
else
|
|
57
|
+
set_column(index_or_header, value)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Append a row
|
|
63
|
+
# @param row [Row, Array] Row to append
|
|
64
|
+
# @return [self]
|
|
65
|
+
def <<(row)
|
|
66
|
+
if row.is_a?(Row)
|
|
67
|
+
@table << row
|
|
68
|
+
else
|
|
69
|
+
@table << Row.new(@headers, row)
|
|
70
|
+
end
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
alias_method :push, :<<
|
|
74
|
+
|
|
75
|
+
# Delete a row or column
|
|
76
|
+
# @param index_or_header [Integer, String] Row index or column header
|
|
77
|
+
# @return [Row, Array, nil] Deleted row or column values
|
|
78
|
+
def delete(index_or_header)
|
|
79
|
+
case @mode
|
|
80
|
+
when :row
|
|
81
|
+
@table.delete_at(index_or_header)
|
|
82
|
+
when :col
|
|
83
|
+
delete_column(index_or_header)
|
|
84
|
+
when :col_or_row
|
|
85
|
+
if index_or_header.is_a?(Integer)
|
|
86
|
+
@table.delete_at(index_or_header)
|
|
87
|
+
else
|
|
88
|
+
delete_column(index_or_header)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Delete rows/columns matching condition
|
|
94
|
+
# @yield [row_or_col] Block that returns true for items to delete
|
|
95
|
+
# @return [self]
|
|
96
|
+
def delete_if(&block)
|
|
97
|
+
@table.delete_if(&block)
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Iterate over rows
|
|
102
|
+
# @yield [Row]
|
|
103
|
+
# @return [Enumerator] if no block given
|
|
104
|
+
def each(&block)
|
|
105
|
+
return to_enum(__method__) unless block_given?
|
|
106
|
+
@table.each(&block)
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check if table is empty
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def empty?
|
|
113
|
+
@table.empty?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get number of rows
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
def size
|
|
119
|
+
@table.size
|
|
120
|
+
end
|
|
121
|
+
alias_method :length, :size
|
|
122
|
+
|
|
123
|
+
# Dig into nested data
|
|
124
|
+
# @param index [Integer] Row index
|
|
125
|
+
# @param args [Array] Additional keys
|
|
126
|
+
# @return [Object]
|
|
127
|
+
def dig(index, *args)
|
|
128
|
+
row = @table[index]
|
|
129
|
+
return nil if row.nil?
|
|
130
|
+
return row if args.empty?
|
|
131
|
+
row.dig(*args)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get rows at specified indices
|
|
135
|
+
# @param indices [Array<Integer>] Row indices
|
|
136
|
+
# @return [Array<Row>]
|
|
137
|
+
def values_at(*indices)
|
|
138
|
+
indices.map { |i| @table[i] }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Switch to row access mode (returns new table with mode set)
|
|
142
|
+
# @return [Table]
|
|
143
|
+
def by_row
|
|
144
|
+
dup_with_mode(:row)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Switch to row access mode (mutates self)
|
|
148
|
+
# @return [self]
|
|
149
|
+
def by_row!
|
|
150
|
+
@mode = :row
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Switch to column access mode (returns new table with mode set)
|
|
155
|
+
# @return [Table]
|
|
156
|
+
def by_col
|
|
157
|
+
dup_with_mode(:col)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Switch to column access mode (mutates self)
|
|
161
|
+
# @return [self]
|
|
162
|
+
def by_col!
|
|
163
|
+
@mode = :col
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Switch to column-or-row access mode (returns new table with mode set)
|
|
168
|
+
# @return [Table]
|
|
169
|
+
def by_col_or_row
|
|
170
|
+
dup_with_mode(:col_or_row)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Switch to column-or-row access mode (mutates self)
|
|
174
|
+
# @return [self]
|
|
175
|
+
def by_col_or_row!
|
|
176
|
+
@mode = :col_or_row
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Compare with another table
|
|
181
|
+
# @param other [Table] Other table
|
|
182
|
+
# @return [Boolean]
|
|
183
|
+
def ==(other)
|
|
184
|
+
return false unless other.is_a?(Table)
|
|
185
|
+
@table == other.table && @headers == other.headers
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Convert to array (includes headers as first row)
|
|
189
|
+
# @return [Array<Array>]
|
|
190
|
+
def to_a
|
|
191
|
+
[@headers] + @table.map(&:fields)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Convert to CSV string
|
|
195
|
+
# @param options [Hash] CSV options
|
|
196
|
+
# @return [String]
|
|
197
|
+
def to_csv(**options)
|
|
198
|
+
write_headers = options.fetch(:write_headers, true)
|
|
199
|
+
col_sep = options[:col_sep] || ","
|
|
200
|
+
quote_char = options[:quote_char] || '"'
|
|
201
|
+
row_sep = options[:row_sep]
|
|
202
|
+
row_sep = "\n" if row_sep.nil? || row_sep == :auto
|
|
203
|
+
|
|
204
|
+
result = +""
|
|
205
|
+
|
|
206
|
+
if write_headers
|
|
207
|
+
result << generate_line(@headers, col_sep, quote_char, row_sep)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
@table.each do |row|
|
|
211
|
+
result << generate_line(row.fields, col_sep, quote_char, row_sep)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
result
|
|
215
|
+
end
|
|
216
|
+
alias_method :to_s, :to_csv
|
|
217
|
+
|
|
218
|
+
# Inspection string
|
|
219
|
+
# @return [String]
|
|
220
|
+
def inspect
|
|
221
|
+
"#<#{self.class} mode:#{@mode} row_count:#{size}>"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def column_values(header)
|
|
227
|
+
@table.map { |row| row[header] }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def set_column(header, values)
|
|
231
|
+
@table.each_with_index do |row, i|
|
|
232
|
+
row[header] = values[i] if values[i]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def delete_column(header)
|
|
237
|
+
values = column_values(header)
|
|
238
|
+
@table.each { |row| row.delete(header) }
|
|
239
|
+
@headers.delete(header)
|
|
240
|
+
values
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def dup_with_mode(new_mode)
|
|
244
|
+
new_table = self.class.new(@table.dup, headers: @headers.dup)
|
|
245
|
+
new_table.instance_variable_set(:@mode, new_mode)
|
|
246
|
+
new_table
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def generate_line(row, col_sep, quote_char, row_sep)
|
|
250
|
+
fields = row.map do |field|
|
|
251
|
+
field_str = field.to_s
|
|
252
|
+
if needs_quoting?(field_str, col_sep, quote_char)
|
|
253
|
+
quote_field(field_str, quote_char)
|
|
254
|
+
else
|
|
255
|
+
field_str
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
fields.join(col_sep) + row_sep
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def needs_quoting?(str, col_sep, quote_char)
|
|
262
|
+
str.include?(col_sep) || str.include?(quote_char) || str.include?("\n") || str.include?("\r")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def quote_field(str, quote_char)
|
|
266
|
+
escaped = str.gsub(quote_char, quote_char + quote_char)
|
|
267
|
+
"#{quote_char}#{escaped}#{quote_char}"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|