I've stolen Dimitrius' idea of using a reserved character to denote an array and expanded on that by creating a custom data converter to convert strings containing the reserved character to arrays, the elements of which are then converted to appropriate data types.
Data
arr = [{ name: 'Dimitrius', errors: ['e1', 3, 4.1] },
{ name: 'Wilma', errors: ['f1', 7, 9.5] }]
Write the CSV file
require 'csv'
FName = 't.csv'
I've assumed that if a substring between commas in the CSV file contains a specified reserved character it denotes an array, with the reserved character separating elements of the array.
RESERVED_CHAR = '|'
keys = arr.first.keys
#=> [:name, :errors]
rows = arr.map(&:values)
#=> [["Dimitrius", ["e1", 3, 4.1]],
# ["Wilma", ["f1", 7, 9.5]]]
CSV.open(FName, "wb") do |csv|
csv << keys
rows.each { |name,arr| csv << [name, arr.join(RESERVED_CHAR)] }
end
#=> [["Dimitrius", ["e1", 3, 4.1]], ["Wilma", ["f1", 7, 9.5]]]
Let's see what was written.
File.read(FName).each_line { |line| p line }
"name,errors\n"
"Dimitrius,e1|3|4.1\n"
"Wilma,f1|7|9.5\n"
Read the CSV file
The CSV class has six built-in data converters (look under "Constants"). One may, however, add additional converters as needed. These convert values in the rows, but not the headers.
require 'csv'
FName = 't.csv'
RESERVED_CHAR = '|'
CSV::Converters[:array] = ->(v) do
if v.include?(RESERVED_CHAR)
v.split(RESERVED_CHAR).map do |e|
case e
when /\A\-?\d+\z/
e.to_i
when /\A\-?\d+\.\d+\z/
e.to_f
else
e
end
end
else
v
end
end
We may now read the CSV file into a CSV::Table object:
tbl = CSV.read(FName, headers: true, converters: :array)
#=> #<CSV::Table mode:col_or_row row_count:3>
We can use this to extract the headers and rows:
headers, *body = tbl.to_a
#=> [["name", "errors"],
# ["Dimitrius", ["e1", 3, 4.1]],
# ["Wilma", ["f1", 7, 9.5]]]
headers
#=> ["name", "errors"]
body
#=> [["Dimitrius", ["e1", 3, 4.1]],
# ["Wilma", ["f1", 7, 9.5]]]
or individual columns:
tbl["name"]
#=> ["Dimitrius", "Wilma"]
tbl["errors"]
#=> [["e1", 3, 4.1],
# ["f1", 7, 9.5]]
Note that the values in the arrays have been converted as desired, and the headers are now strings. If they are to be symbols that would require a separate step at the end.