0

The Problem:

I have the following hash:

hash = {1=>{:price=>"400", :brand=>"Primark"}, 2=>{:price=>"1000", :brand=>"pull&bear"}, 3=>{:price=>"1700", :brand=>""}, 4=>{:price=>"500", :brand=>"H&M"}, 5=>{:price=>"500", :brand=>"Mango"}}

and I want to convert it into a csv file.


Approach so far:

My closest solution (inspired by) seems to overwrite itself constantly:

require 'csv'

(1..5).each do |id|
   column_names = hash[id].keys
   column_values = hash[id].values
   s=CSV.generate do |csv|
      csv << column_names
      csv << column_values
   end
   File.write('the_file.csv', s)
end

Current csv file:

+---+------------+-------------+
|   |     A      |      B      |
+---+------------+-------------+
| 1 | price,brand|             |
| 2 | 500, Mango |             |
+---+------------+-------------+

Desired csv output:

+---+------------+-------------+
|   |     A      |      B      |
+---+------------+-------------+
| 1 | price      | brand       |
| 2 | 400        | Primark     |
| 3 | 1000       | pull&bear   |
| 4 | 1700       |             |
| 5 | 500        | H&M         |
| 6 | 500        | mango       |
+---+------------+-------------+

There are several questions on here that either deal with the inverse (converting a csv into a nested hash) or only convert simple hashes into csv files, but not with nested hashes. I'm fairly new to Ruby and can't connect the dots here yet. Help would be greatly appreciated!

F7777
  • 3
  • 3
  • It would be very useful to say what output you desire and to also show what output you are actually getting. – Peter Camilleri Mar 11 '19 at 15:49
  • I added the desired/current output, thanks! – F7777 Mar 11 '19 at 16:13
  • Note that two of the solutions to date employ the method `Hash#values_at`. If it's not evident, that was used so that no assumption need be made about the order of the hash keys (`:price` and `:brand`). – Cary Swoveland Mar 11 '19 at 20:39

3 Answers3

4

It seems that you are only interested in the values of your nested hash. In that case you can simply do

titles = hash.values.map(&:keys).flatten.uniq
rows = hash.values.map { |data| data.values_at(*titles) }

s = CSV.generate do |csv|
  csv << titles
  rows.each do |row|
    csv << row
  end
end

(update)

Titles should be flattened to a simple array.

rewritten
  • 16,280
  • 2
  • 47
  • 50
  • Thank you for your solution! Unfortunately, it doesnt work. Here are the terminal outputs: `irb(main):170:0> titles = hash.values.map(&:keys).uniq => [[:price, :brand]]` `irb(main):171:0> rows = hash.values.map { |data| data.values_at(*titles) } => [[nil], [nil], [nil], [nil], [nil]]` `irb(main):172:0> s = CSV.generate do |csv| irb(main):173:1* csv << titles irb(main):174:1> rows.each do |row| irb(main):175:2* csv << row irb(main):176:2> end irb(main):177:1> end => "\"[:price, :brand]\"\n\n\n\n\n\n"` The exported csv file then only contains "[:price, :brand]" in one row – F7777 Mar 12 '19 at 11:40
  • yep I forgot to flatten the titles list. Titles should have been `[:price, :brand]` and not `[[:price, :brand]]`. – rewritten Mar 13 '19 at 10:47
4

I would do something like this:

require 'csv'

CSV.open('the_file.csv', 'w') do |csv|
  hash.each do |id, attributes|    
    csv << [id, *attributes.values_at(:price, :brand)]
  end
end
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • Thank you, spickermann! Your solution works fine. Including `headers = hash.values[0].keys` and `csv << headers` from @Grzegorz code adds the headers. – F7777 Mar 12 '19 at 11:48
1

CSV format is essentially an array of arrays. Each element of the main array is a single row. A single row is an array of cells. So what you want basically is something like this:

[
  [:price, :brand],
  ["400", "Primark"],
  ["1000", "Pull&Bear"]
]

You can achieve it in a following way:

headers = hash.values[0].keys # this will return [:price, :brand] and we'll use it as a header
data = hash.values.map(&:values)

csv_output = CSV.generate do |csv|
  csv << headers # we need headers only once, we don't need them in every row
  data.each do |single_row| # iterate over each row
    csv << single_row # add the row to csv file
  end
end
File.write('the_file.csv', csv_output)

This code assumes that for every row you'll have all data available (i.e. each row will have a price and brand provided). Code above provided by user rewritten is more flexible.

Gregory Witek
  • 1,216
  • 8
  • 10