I am new in ruby so please forgive the noobishness.
I have a CSV with two columns. One for animal name and one for animal type.
I have a hash with all the keys being animal names and the values being animal type. I would like to write the hash to the CSV without using fasterCSV. I have thought of several ideas what would be easiest.. here is the basic layout.
require "csv"
def write_file
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
CSV.open("data.csv", "wb") do |csv|
csv << [???????????]
end
end
When I opened the file to read from it I opened it File.open("blabla.csv", headers: true)
Would it be possible to write back to the file the same way?
If you want column headers and you have multiple hashes:
require 'csv'
hashes = [{'a' => 'aaaa', 'b' => 'bbbb'}]
column_names = hashes.first.keys
s=CSV.generate do |csv|
csv << column_names
hashes.each do |x|
csv << x.values
end
end
File.write('the_file.csv', s)
(tested on Ruby 1.9.3-p429)
Try this:
require 'csv'
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
CSV.open("data.csv", "wb") {|csv| h.to_a.each {|elem| csv << elem} }
Will result:
1.9.2-p290:~$ cat data.csv
dog,canine
cat,feline
donkey,asinine
I think the simplest solution to your original question:
def write_file
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
CSV.open("data.csv", "w", headers: h.keys) do |csv|
csv << h.values
end
end
With multiple hashes that all share the same keys:
def write_file
hashes = [ { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' },
{ 'dog' => 'rover', 'cat' => 'kitty', 'donkey' => 'ass' } ]
CSV.open("data.csv", "w", headers: hashes.first.keys) do |csv|
hashes.each do |h|
csv << h.values
end
end
end
CSV can take a hash in any order, exclude elements, and omit a params not in the HEADERS
require "csv"
HEADERS = [
'dog',
'cat',
'donkey'
]
def write_file
CSV.open("data.csv", "wb", :headers => HEADERS, :write_headers => true) do |csv|
csv << { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
csv << { 'dog' => 'canine'}
csv << { 'cat' => 'feline', 'dog' => 'canine', 'donkey' => 'asinine' }
csv << { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine', 'header not provided in the options to #open' => 'not included in output' }
end
end
write_file # =>
# dog,cat,donkey
# canine,feline,asinine
# canine,,
# canine,feline,asinine
# canine,feline,asinine
This makes working with the CSV class more flexible and readable.
I tried the solutions here but got an incorrect result (values in wrong columns) since my source is a LDIF file that not always has all the values for a key. I ended up using the following.
First, when building up the hash I remember the keys in a separate array which I extend with the keys that are not allready there.
# building up the array of hashes
File.read(ARGV[0]).each_line do |lijn|
case
when lijn[0..2] == "dn:" # new record
record = {}
when lijn.chomp == '' # end record
if record['telephonenumber'] # valid record ?
hashes << record
keys = keys.concat(record.keys).uniq
end
when ...
end
end
The important line here is keys = keys.concat(record.keys).uniq which extends the array of keys when new keys (headers) are found.
Now the most important: converting our hashes to a CSV
CSV.open("export.csv", "w", {headers: keys, col_sep: ";"}) do |row|
row << keys # add the headers
hashes.each do |hash|
row << hash # the whole hash, not just the array of values
end
end
[BEWARE] All the answers in this thread are assuming that the order of the keys defined in the hash will be constant amongst all rows.
To prevent problems (that I am facing right now) where some values are assigned to the wrong keys in the csv (Ex:)
hahes = [
{:cola => "hello", :colb => "bye"},
{:colb => "bye", :cola => "hello"}
]
producing the following table using the code from the majority (including best answer) of the answers on this thread:
cola | colb
-------------
hello | bye
-------------
bye | hello
You should do this instead:
require "csv"
csv_rows = [
{:cola => "hello", :colb => "bye"},
{:colb => "bye", :cola => "hello"}
]
column_names = csv_rows.first.keys
s=CSV.generate do |csv|
csv << column_names
csv_rows.each do |row|
csv << column_names.map{|column_name| row[column_name]} #To be explicit
end
end
Try this:
require 'csv'
data = { 'one' => '1', 'two' => '2', 'three' => '3' }
CSV.open("data.csv", "a+") do |csv|
csv << data.keys
csv << data.values
end
Lets we have a hash,
hash_1 = {1=>{:rev=>400, :d_odr=>3}, 2=>{:rev=>4003, :d_price=>300}}
The above hash_1 having keys as some id 1,2,.. and values to those are again hash with some keys as (:rev, :d_odr, :d_price).
Suppose we want a CSV file with headers,
headers = ['Designer_id','Revenue','Discount_price','Impression','Designer ODR']
Then make a new array for each value of hash_1 and insert it in CSV file,
CSV.open("design_performance_data_temp.csv", "w") do |csv|
csv << headers
csv_data = []
result.each do |design_data|
csv_data << design_data.first
csv_data << design_data.second[:rev] || 0
csv_data << design_data.second[:d_price] || 0
csv_data << design_data.second[:imp] || 0
csv_data << design_data.second[:d_odr] || 0
csv << csv_data
csv_data = []
end
end
Now you are having design_performance_data_temp.csv file saved in your corresponding directory.
Above code can further be optimized.
Related
I have an array of objects. I am trying to create CSV data and allow the user to download that file but I get the following error:
Undefined method 'first_name' for Hash:0x007f946fc76590
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
This is the button that allows a user to download the CSV:
<%= link_to "Download Employee CSV", download_employee_csv_path %>
Controller:
def download_employee_csv
employee_csv_data = []
employees.each do |employee|
employee_csv_data << {
first_name: employee[:first_name],
last_name: employee[:last_name],
email: employee_email,
phone1: employee[:phone1],
gender: employee[:gender],
veteran: employee[:veteran].to_s,
dob: employee[:dob],
core_score: service_score,
performance_rank: rank,
industry_modules_passed: industry_modules_passed
}
end
respond_to do |format|
format.html
format.csv { send_data Employer.to_csv(employee_csv_data), filename: "download_employee_csv.csv" }
end
end
employee_csv_data:
=> [{:first_name=>"Christopher",
:last_name=>"Pelnar",
:email=>"pelnar#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1988-09-09",
:core_score=>"No Score",
:performance_rank=>"No Rank",
:industry_modules_passed=>"No Industry Modules Passed"},
{:first_name=>"chris",
:last_name=>"pelnar",
:email=>"chris#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1998-09-09",
:core_score=>"729",
:performance_rank=>"Good",
:industry_modules_passed=>"Entry-Service, Entry-Tech"}]
Model:
def self.to_csv(employee_csv_data)
attributes = %w(first_name last_name email phone gender veteran dob core_score performance_rank industry_modules_passed)
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
When I click the button, it takes me to the blank HTML page without any problem. When I add .csv to the filename in the URL on that page I get the error.
It looks like it's an array of Hashes. To access properties of a hash in Ruby you need to use brackets. Try updating your code to this:
csv << attributes.map{ |attr| obj.send([], attr) }
or more concisely:
csv << attributes.map{ |attr| obj[attr] }
One more thing, in the example you provided, the keys in the hash are symbols which means you may need to convert your attributes to symbols when trying to access them, like this:
csv << attributes.map{ |attr| obj[attr.to_sym] }
I adapted #Ctpelnar1988's answer to determine the attributes dynamically and allow each array item to have different columns:
def array_of_hashes_to_csv(array)
array_keys = array.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << array_keys
array.each do |obj|
csv << array_keys.map{ |attr| obj[attr] }
end
end
end
Example:
puts array_of_hashes_to_csv([
{attr_a: 1, attr_b: 2},
{attr_a: 3, attr_c: 4}
])
attr_a,attr_b,attr_c
1,2,
3,,4
In the more specific "employee_csv_data" context, I think it'd look like this:
def self.to_csv(employee_csv_data)
attributes = employee_csv_data.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map { |attr| obj[attr] }
end
end
end
I have the following method on on my action to export to excel a list of user:
def users_report
#users = Kid.where(confirmation_token: nil).paginate(:page => params[:page], :per_page => 30)
#userxls = Kid.where(confirmation_token: nil)
respond_to do |format|
format.html
format.xls { send_data #userxls.to_csv({col_sep: "\t"}) }
end
end
On my model the to_csv method:
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << ["Name", "Surname", "E-mail", "Age", "School", "Class", "Native Language", "Practised Language", "Conversations same Native", "Convserations different Native", "Message same Native", "Message different Native", "Posts", "Clossed/Finished calls", "Missed calls", "Connections per Week", "Nb of foreign friends", "Nb of friends same country", "Activation Date", "Email Parent", "Parent Activated"]
kids = Array.new
all.each do |kid|
if kid.user_role != "admin"
k = Array.new
kid.name = kid.name rescue "No name"
k << kid.name
kid.surname = kid.surname rescue "No surname"
k << kid.surname
kid.email = kid.email rescue "No email"
k << kid.email
k << kid.age rescue "No age"
if !kid.school.nil?
k << kid.school.name
else
k << "No School"
end
if kid.courses.empty?
k << "No Courses"
else
k << kid.courses.first.name
end
if !kid.native_languages.empty?
languages = Array.new
kid.native_languages.each do |lang|
languages << Language.find(lang).name
end
k << languages
else
k << "No native language"
end
if !kid.practice_languages.empty?
languages = Array.new
kid.practice_languages.each do |lang|
languages << Language.find(lang).name
end
k << languages
else
k << "No practise language"
end
k << kid.number_of_native_conversations rescue "0"
k << kid.number_of_foreign_conversations rescue "0"
k << kid.number_of_native_messages rescue "0"
k << kid.number_of_foreign_messages rescue "0"
k << kid.number_of_activity_posts rescue "0"
k << kid.number_of_finished_closed_calls rescue "0"
k << kid.number_of_missed_calls rescue "0"
k << kid.avg_of_connections_week rescue "0"
k << kid.number_of_foreign_friends rescue "0"
k << kid.number_of_friends_same_country rescue "0"
k << kid.confirmed_at.try(:strftime, "%d/%m/%Y") rescue "0"
k << kid.tutor.email rescue "No parent email"
k << kid.tutor.confirmed? rescue "No parent email"
kids << k
end
end
kids.each do |k|
csv << k
end
end
end
But on my excel file I'm getting names like Jérôme instead of Jérôme. I tried:
# encoding: utf-8
on my view also tried for every field
.force_encoding("UTF-8")
But I still have this problem. Please I really need help with this.
Thanks in advance
CVS::generate understands an option :encoding (see Ruby API).
So use
format.xls { send_data #userxls.to_csv({col_sep: "\t", encoding: 'UTF-8'}) }
You may also think about separating representation and business logic. I use csv_builder that provides views like user_report.csv.csvbuilder to define the csv output.
cvs_builder uses the instance variable #encoding to specify the output character encoding.
Edit
It seems, like your generated csv is already encoded in UTF-8 but you read it as if it were ISO-8859-1 aka. LATIN-1.
You may want to try to generate the csv in LATIN-1 as excel has issues importing UTF-8 csv files.
Depending on the Ruby or Rails version, you have to use ISO-8859-1 instead of LATIN-1.
I would like to create one CSV file containing two models with comma gem in my ruby 3.2.8 application. Maybe the answer to the question is trivial, but this is the first time I use this gem. I know how create the file it based on a model, but I don' t know how make matches of two.
I have a views\participants\index with :
<%= link_to 'Download CSV', '/participants.csv' %>
the controller :
def index
#participants = Participant.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #participants }
format.csv { send_data #participants.to_comma }
end
end
participant Model:
require 'comma'
class Participant < ActiveRecord::Base
comma do
id
token
end
end
and field Model:
require 'comma'
class Field < ActiveRecord::Base
comma do
name
value
id_participant
end
end
in the db i have:
Participant1 = ["id" => 1 , "token" => "a"]
Participant2 = ["id" => 2 , "token" => "b"]
Field1= ["id_participant" => 1, "name" => "p1_exams1", "value" =>5]
Field2= ["id_participant" => 1, "name" => "p1_exams2", "value" =>3]
Field3= ["id_participant" => 2, "name" => "p2_exams1", "value" =>2]
Field4= ["id_participant" => 2, "name" => "p2_exams2", "value" =>3]
I would like to have a file like this:
id token
1 a
id_p name value
1 p1_c1_exams1 5
1 p1_c1_exams2 3
id token
2 b
id_p name value
2 p1_c1_exams1 2
2 p1_c1_exams2 3
I tried with this in controller:
def index
#participants = Participant.all
#fields = Field.all
require 'csv'
csv_string = CSV.generate do |csv|
#participants.each do |p|
csv << ["id","token","last_ip_address","start_date","last_transition_date","completion_date","completed","total_time_survey","created_at"]
csv << [ p.id, p.token , p.last_ip_address, p.start_date, p.last_transition_date, p.completion_date, p.completed, p.total_time_survey, p.created_at]
#fields.each do |f|
if String(f.id_participant) == String(p.id)
csv << ["id","name","value","id_participant","id_survey","created_at"]
csv << [f.id,f.name, f.insert_value, f.id_participant, f.id_survey, f.created_at]
end
end
end
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: #participants }
format.csv { send_data csv_string,
:type => "text/csv; charset=iso-8859-1; header=present",
:disposition => "attachment; filename=Database.csv" }
end
end
You can also use the fastercsv for this
i think this will help u what i am understanding that u have has many relationship between Participant and Field regarding this i have write some piece of code u can customize it as ur need
#participants = Participant.all
csv_string = FasterCSV.generate do |csv|
#participants.each do |i|
csv << ["id","token"]
csv << [ i.id, i.token ]
i.fields.each do |j|
csv << ["id_p","name", "value"]
csv << [i.id,j.name, j.value]
end
end
end
send_data csv_string,
:type => "text/csv; charset=iso-8859-1; header=present",
:disposition => "attachment; filename=anyName.csv"
I am using Ruby on Rails 3.2.2. I have the following scenario:
# hash_params.class
# => Hash
# hash_params.inspect
# => { :key1 => :value1, :key2 => value2, ... => ... }
#
def self.method_1(hash_params)
hash_params.each do { |hash_param| self.method_2(hash_param) }
end
# param.class
# => Array
# param.inspect
# => [:key1, value1] # or '[:key2, value2]' or '[..., ...]', depending on cases.
#
def self.method_2(param)
logger.debug "Key => #{param[0])"
logger.debug "Value => #{param[1])"
end
Given outputs commented out in the above code, when I run the method_1 then in the logger file I have the following:
Key => :key1
Value => :value1
Key => :key2
Value => :value2
Key => ...
Value => ...
I would like to treat the param variable in method_2 as-like a key / value pair (not as an Array), for example by making something like the following
def self.method_2(param)
param do |key, value| # Note: This code line doesn't work. It is just a sample code to clarify the question.
logger.debug "Key => #{key.inspect)"
logger.debug "Value => #{value.inspect)"
end
end
? Is it possible? If so, how? What do you advice about?
Use Hash[]:
param = [:key1, 'value1']
h = Hash[*param]
puts h[:key1]
Output:
value1
How about
def self.method_1(hash_params)
hash_params.each do { |key, value| self.method_2(key, value) }
end
def self.method_2(key, value)
logger.debug "Key => #{key)"
logger.debug "Value => #{value)"
end
Otherwise you can still pass a hash in param like
def self.method_1(hash_params)
hash_params.keys.each do { |key| self.method_2(hash_params.slice(key)) }
end
edit: if you want a hash as parameter you could just do
def self.method_1(hash_params)
hash_params.each do { |key, value| self.method_2({key => value}) }
end
I need to layout my CSV into columns not rows. So going down the spreadsheet not across. For example:
Header 1, value1.1, value2.1
Header 2, value1.2, value2.2
Header 3, value1.3, value2.3
Does anyone know how to do this? I've been through the documentation and can't find anything about changing the layout to columns.
EDIT:
row_data = [];
csv_string = FasterCSV.generate do |csv|
# header row
row_data << ["id", "Name", "Age"]
# data rows
Playerapplication.find_each do |player|
row_data << [player.id, player.name, player.age]
end
row_data.transpose
csv << row_data
end
# send it to the browser
send_data csv_string,
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename=players_application.csv"
Simply use Array#transpose on your data before writing to CSV.
If you modify your code like this:
row_data = [];
csv_string = FasterCSV.generate do |csv|
# header row
row_data << ["id", "Name", "Age"]
# data rows
Playerapplication.find_each do |player|
row_data << [player.id, player.name, player.age]
end
row_data.transpose.each do |row|
csv << row
end
end
it works for me.