How do you convert an array of hashes to a .csv file?
I have tried
CSV.open("data.csv", "wb") do |csv|
#data.to_csv
end
but it is blank
Try this:
CSV.open("data.csv", "wb") do |csv|
#data.each do |hash|
csv << hash.values
end
end
If you want the first line of the CSV to contain the keys of the hash (a header row), simply do:
CSV.open("data.csv", "wb") do |csv|
csv << #data.first.keys # adds the attributes name on the first line
#data.each do |hash|
csv << hash.values
end
end
Please read the comment of #cgenco below: He wrote a monkey patch for the Array class.
CSV is smart enough to deal with the non-uniform hashes for you. See the code for CSV::Writer#<<
So, this works, and is a bit simpler than the above examples:
CSV.open("data.csv", "wb", {headers: #data.first.keys} ) do |csv|
#data.each do |hash|
csv << hash
end
end
If the keys are not the same in all rows, the current answers fail. This is the safest approach:
data = [{a: 1, b: 2}, {b: 3, c: 4}]
CSV.open("data.csv", "w") { |csv|
headers = data.flat_map(&:keys).uniq
csv << headers
data.each { |row|
csv << row.values_at(*headers)
}
}
All keys will be present in the CSV, even if they don't appear in the first row:
a
b
c
1
2
3
4
If the hashes aren't uniform then you will end up with data in the wrong columns. You should use values_at instead:
CSV.open("data.csv", "wb") do |csv|
keys = #data.first.keys
csv << keys
#data.each do |hash|
csv << hash.values_at(*keys)
end
end
None of the other answers worked for me for one reason or another, so I'll throw in my contribution as well. Here's what worked for me for my array of hashes with ruby 2.7:
headers = data.map(&:keys).flatten.uniq
CSV.open("data.csv", "wb", {headers: headers} ) do |csv|
csv << headers
data.each do |hash|
csv << hash
end
end
Related
I want to write header only 1 time in first row when import data to csv in ruby, but the header is written many time on output file.
job_datas.each do |job_data|
#company_job = job data coverted etc....
save_job_to_csv(#company_job)
end
def save_job_to_csv(job_data)
filepath = "tmp/jobs/jobs.csv"
CSV.open(filepath, "a", :headers => true) do |csv|
if csv.blank?
csv << CompanyJob.attribute_names
end
csv << job_data.attributes.values
end
end
Any one can give me solution? Thank you so much!
You are calling save_job_to_csv the method for each job_data and pushing header every time csv << CompanyJob.attribute_names
filepath = "tmp/jobs/jobs.csv"
CSV.open(filepath, "a", :headers => true) do |csv|
# push header once
csv << CompanyJob.attribute_names
# push every job record
job_datas.each do |job_data|
#company_job = job data coverted etc....
csv << #company_job.attributes.values
end
end
The above script can be created wrapped a method but if you like to write a separate method that just saves the CSV, then you need to refactor the script when you first prepare an array of values holding header and pass it to a method that just saves to CSV.
You could do something similar to this:
def save_job_to_csv(job_data)
filepath = "tmp/jobs/jobs.csv"
unless File.file?(filepath)
File.open(filepath, 'w') do |file|
file.puts(job_data.attribute_names.join(','))
end
end
CSV.open(filepath, "a", :headers => true) do |csv|
csv << job_data.attributes.values
end
end
It just checks beforehand if the file exists and if not it adds the header. If you want tabs as column separators, you just have to change the value for the join function and add the col_sep parameter to CSV.open():
file.puts(job_data.attribute_names.join("\t"))
CSV.open(filepath, "a", :headers => true, col_sep: "\t") do |csv|
I have a array of data which is
user: [
{name: 'foo', email: 'foo#gmail.com'},
{name: 'foo', email: 'foo#gmail.com', phone: '76767676'},
{name: 'foo', email: 'foo#gmail.com', pin_code: '22526'},
]
and I am changing it into csv by this code
CSV.open(file, 'w') do |csv|
user_hash = JSON.parse(user.to_json)
csv << user_hash.first.keys
user_hash.each do |hash|
csv << hash.values
end
end
in above code there is a problem that i am taking user_hash.first.keys so first keys name and email will be there for header.
I want to know that how can we add new column in header if a new attribute arrives which is happening in other two.
because I am taking the user_hash.first.keys so name and email header columns will be there not phone and pin_code.
Thanks.
You will need to collect all available keys first and then get the values for those keys in the same order for each hash.
user_hash = JSON.parse(user.to_json)
keys = user_hash.flat_map(&:keys).uniq
CSV.open(file, 'w') do |csv|
csv << keys
user_hash.each { |hash| csv << hash.values_at(*keys) }
end
I have a block in a rails controller that looks like this:
CSV.generate do |csv|
csv << ["Name", "Other Field"]
csv << [#app.name, #app.other_field]
send_data csv, filename: 'test.csv'
end
But then the contents of the downloaded file are simply this:
#<CSV:0x007fe518414bc8>
What am I doing wrong?
CSV.generate returns the generated CSV string, for example:
str = CSV.generate { |csv| csv << %w[a b c]; csv << [1,2,3] }
puts str
gives you:
a,b,c
1,2,3
as output.
You're passing a CSV instance to send_data and it is trying to convert that CSV instance to a string by, apparently, calling to_s on it.
You want to say something more like this:
csv_string = CSV.generate do |csv|
csv << ["Name", "Other Field"]
csv << [#app.name, #app.other_field]
end
send_data csv_string, filename: 'test.csv'
I am currently exporting a CSV file from my rails app and it is working fine but i would like to add a bit more data to the csv.
Currently i'm using:
CSV.generate do |csv|
csv << column_names
all.each do |item|
csv << item.attributes.values_at(*column_names)
end
end
To generate a csv with all of the data from the target model but would like to add an additional column, manufacturer_name which will be taken from a parent model.. Something like:
CSV.generate do |csv|
csv << column_names,
csv << "manufacturer_name"
all.each do |item|
csv << item.attributes.values_at(*column_names),
csv << Manufacturer.find(item.manufacturer_id).first().name
end
end
How would i write this correctly so that the "manufacturer_name" get set to a new column header and the manufacturer name of each item is pulled in and put in the correct column?
CSV.generate do |csv|
names = column_names << 'manufacturer_name'
csv << names
all.each do |item|
row = item.attributes.values_at(*column_names)
row << Manufacturer.find(item.manufacturer_id).first.name
csv << row
end
end
I'm currently using this code:
CSV.open "application.csv", "a+" do |csv|
csv << [ "#{params[:first_name]}", "#{params[:last_name]}","#{params[:company]}","#{params[:email]}", "#{params[:phone]}", "#{params[:business]}", "#{params[:services]}", "#{params[:employees]}", "#{params[:turnover]}" ]
end
Which writes an extra row to the csv each time, what can I put instead of "a+" that will overwrite the entire file each time, so it always only has one row?
You should use 'w' mode. BTW, why do you write "#{params[:first_name]}" where params[:first_name] would be enough? The code should look:
CSV.open 'application.csv', 'w' do |csv|
csv << [params[:first_name], params[:last_name], params[:company], params[:email], params[:phone], params[:business], params[:services], params[:employees], params[:turnover], params[:c4l_services]]
end