How to add an array of hashes into a csv file? - ruby

I found this code and it fits me nicely, i transformed it into this one:
def write_in_file(file_name, hash)
column_names = hash.first.keys
s=CSV.generate do |csv|
csv << column_names
hash.each do |x|
csv << x.values
end
end
File.write("#{file_name}.csv", s)
end
This is how my array of hashes looks:
[
{:Name => "John", :Age => 26, :Country => America},
{:Name => "Ivan", :Age => 34, :Country => Russia},
{:Name => "Pablo", :Age => 20, :Country => Columbia}
]
But the problem is that every time I call this method - it rewrites whole file. How to change it, if i want to save this headers and add new information every iteration?

You can append to an existing file by using mode: 'a': (see open modes)
File.write("#{file_name}.csv", s, mode: 'a')
To write the headers only on the first run, you could check whether the file exists. In addition, you should use a fixed header and fetch the hash values in that specific order, e.g.:
CSV.generate do |csv|
csv << %w[Name Age Country] unless File.exist?("#{file_name}.csv")
hash.each do |x|
csv << x.values_at(:Name, :Age, :Country)
end
end
File.write("#{file_name}.csv", s, mode: 'a')
There's also CSV.open which creates the file for you:
CSV.open("#{file_name}.csv", 'a') do |csv|
csv << %w[Name Age Country] if csv.stat.zero?
hash.each do |x|
csv << x.values_at(:Name, :Age, :Country)
end
end
Since the file will always exist when the block gets executed, the header check needs to be changed: csv.stat returns the file's File::Stat and zero? determines whether the file is empty.

Related

How to use a variable for a file path? Ruby

Is there the possibility in Ruby to use a variable / string to define a file path?
For example I would like to use the variable location as follow:
location = 'C:\Users\Private\Documents'
#### some code here ####
class Array
def to_csv(csv_filename)
require 'csv'
CSV.open(csv_filename, "wb") do |csv|
csv << first.keys # adds the attributes name on the first line
self.each do |hash|
csv << hash.values
end
end
end
end
array = [{:color => "orange", :quantity => 3},
{:color => "green", :quantity => 1}]
array.to_csv('location\FileName.csv')
You can use variable inside string, following way:
array.to_csv("#{location}\FileName.csv")
You can use File.join, which accepts variables as arguments.
irb(main):001:0> filename = File.basename('/home/gumby/work/ruby.rb')
=> "ruby.rb"
irb(main):002:0> path = '/home/gumby/work'
=> "/home/gumby/work"
irb(main):003:0> File.join(path, filename)
=> "/home/gumby/work/ruby.rb"
As noted above, if you start embedding slashes in your strings things may get unmanageable in future.

Is it possible to generate a CSV using hashes as rows?

I have a big array of hashes, like this
# note that the key order isn't consistent
data = [
{foo: 1, bar: 2, baz: 3},
{foo: 11, baz: 33, bar: 22}
]
I want to turn this into a CSV
foo,bar,baz
1,2,3
11,22,33
I am doing so like this:
columns = [:foo, :bar, :baz]
csv_string = CSV.generate do |csv|
csv << columns
data.each do |d|
row = []
columns.each do |column|
row << d[column]
end
csv << row
end
end
Is there a better way to do this? What I'd like to do is something like...
csv_string = CSV.generate do |csv|
csv << [:foo, :bar, :baz]
data.each do |row|
csv.add_row_hash row
end
end
With the appropriate options passed to generate, you can achieve what you want. Note that you can add the hash directly to the CSV once the headers are set.
c = CSV.generate(:headers => [:foo, :bar, :baz], :write_headers => true) do |csv|
data.each { |row| csv << row }
end
Output:
foo,bar,baz
1,2,3
11,22,33
If keys can be missing, you need to get all the possible keys
keys = data.map(&:keys).flatten.uniq
Then map each row using those keys.
csv_string = CSV.generate do |csv|
csv << keys
data.each do |row|
csv << row.values_at(keys)
end
end
My first idea: Your data could be used to insert data into a database table.
If you combine this with a csv-output of a DB-table you have another solution.
Example:
data = [
{foo: 1, bar: 2, baz: 3},
{foo: 11, baz: 33, bar: 22},
{foo: 11, baz: 33, bar: 22, xx: 3}, #additional parameters are no problem
]
#Prepare DB as a helper
require 'sequel'
DB = Sequel.sqlite
DB.extension(:sequel_3_dataset_methods) #define to_csv
DB.create_table(:tab){
add_column :foo
add_column :bar
add_column :baz
}
DB[:tab].multi_insert(data) #fill table
#output as csv (the gsub is necessary on Windows, maybe not necessary on other OS
puts DB[:tab].to_csv.gsub("\r\n","\n")
Disadvantage: You need Sequel
Advantage: You can adapt the order quite easy:
puts DB[:tab].select(:bar, :baz).to_csv.gsub("\r\n","\n")

How do I skip headers while writing CSV?

I am writing a CSV file and CSV.dump outputs two header lines which I don't want.
I tried setting :write_headers => false but still it outputs a header:
irb> A = Struct.new(:a, :b)
=> A
irb> a = A.new(1,2)
=> #<struct A a=1, b=2>
irb> require 'csv'
=> true
irb> puts CSV.dump [a], '', :write_headers => false, :headers=>false
class,A
a=,b=
1,2
I don't think you can do it with option parameters. But you can easily accomplish what you want by not using the generate method
irb> arr = [a, a]
=> [#<struct A a=1, b=2>, #<struct A a=1, b=2>]
irb> csv_string = CSV.generate do |csv|
irb* arr.each {|a| csv << a}
irb> end
irb> puts csv_string
1,2
1,2
=> nil
I think the problem is two-fold:
CSV.dump [a]
wraps an instance of the struct a in an array, which then CSV tries to marshall. While that might be useful sometimes, when trying to generate a CSV file for consumption by some other non-Ruby app that recognizes CSV, you're going to end up with values that can't be used. Looking at the output, it isn't CSV:
class,A
a=,b=
1,2
Looking at it in IRB shows:
=> "class,A\na=,b=\n1,2\n"
which, again, isn't going to be accepted by something like a spreadsheet or database. So, another tactic is needed.
Removing the array from a doesn't help:
CSV.dump a
=> "class,Fixnum\n\n\n\n"
Heading off a different way, I looked at a standard way of generating CSV from an array:
puts a.to_a.to_csv
=> 1,2
An alternate way to create it is:
CSV.generate do |csv|
csv << a.to_a
end
=> "1,2\n"

Ruby: Reading Arrays of Hashes from YAML

I have two dads going into my YAML file, but only one family comes out. What happened to Sam? How do I get both out?
## dads.rb
require 'yaml'
require 'pp'
dad=[]
dad[0] = {:name => "Joe", :kids => ["Mary", "John"]}
dad[1] = {:name => "Sam", :kids => ["Sam Jr", "Samantha", "Samizdat"]}
open('dads.yml' , 'w') do |f|
dad.each do |d|
f.write YAML::dump(d)
end
end
family = []
open('dads.yml') do |f|
family << YAML::load(f.read)
end
pp fams
You dump multiple YAML documents but only read back one. Instead, you can just dump and read the whole array:
require 'yaml'
dads = []
dads << {:name => "Joe", :kids => ["Mary", "John"]}
dads << {:name => "Sam", :kids => ["Sam Jr", "Samantha", "Samizdat"]}
open('dads.yml', 'w') { |f| YAML::dump(dads, f) }
family = YAML::load(File.read('dads.yml'))
p family
Your code currently creates separate "documents" within the YAML output. By default, YAML::load will just read in the first document. Niklas' answer is definitely the way you should go, but if you absolutely had to deal with multiple documents, you could use the load_documents method:
family = YAML.load_documents(File.read("dads.yml"))
# => [{:name=>"Joe", :kids=>["Mary", "John"]}, {:name=>"Sam", :kids=>["Sam Jr", "Samantha", "Samizdat"]}]

How to save a hash into a CSV

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.

Resources