Ruby CSV foreach write to csv using Row object

Viewed 43

I want to loop over a csv file using CSV.foreach, read the data, perform some operation with it, and write the result to the last column of that row, using the Row object.

So let's say I have a csv with data I need to save to a database using Rails ActiveRecord, I validate the record, if it is valid, I write true in the last column, if not I write the errors.

Example csv:

id,title
1,some title
2,another title
3,yet another title
CSV.foreach(path, "r+", headers: true) do |row|
  archive = Archive.new(
    title: row["title"]
  )
  archive.save!
  row["valid"] = true
  
rescue ActiveRecord::RecordInvalid => e
  row["valid"] = archive.errors.full_messages.join(";")
end

When I run the code it reads the data, but it does not write anything to the csv. Is this possible?

Is it possible to write in the same csv file?

Using:

  • Ruby 3.0.4
2 Answers

The row variable in your iterator exists only in memory. You need to write the information back to the file like this:

new_csv = ["id,title,valid\n"]

CSV.foreach(path, 'r+', headers: true) do |row|  # error here, see edit note below
  row["valid"] = 'foo'
  new_csv << row.to_s
end

File.open(path, 'w+') do |f|
  f.write new_csv
end

[EDIT] the 'r+' option to foreach is not valid, it should be 'r'

Maybe this is over-engineering things a bit. But I would do the following:

  1. Read the original CSV file.
  2. Create a temporary CSV file.
  3. Insert the updated headers into the temporary CSV file.
  4. Insert the updated records into the temporary CSV file.
  5. Replace the original CSV file with the temporary CSV file.
csv_path = 'archives.csv'
input_csv = CSV.read(csv_path, headers: true)
input_headers = input_csv.headers

# using an UUID to prevent file conflicts
tmp_csv_path = "#{csv_path}.#{SecureRandom.uuid}.tmp"
output_headers = input_headers + %w[errors]

CSV.open(tmp_csv_path, 'w', write_headers: true, headers: output_headers) do |output_csv|
  input_csv.each do |archive_data|
    values  = archive_data.values_at(*input_headers)
    archive = Archive.new(archive_data.to_h)

    archive.valid?
    # error_messages is an empty string if there are no errors
    error_messages = archive.errors.full_messages.join(';')

    output_csv << values + [error_messages]
  end
end

FileUtils.move(tmp_csv_path, csv_path)
Related