How to Print to Different Files on the Fly? - ruby

How can I print the contents of a dynamically generated and sorted array to different files based on their content?
For example, let's say we have the following multi-dimensional array which is sorted by the second column
[ ['Steph', 'Allen', 29], ['Jon', 'Doe', 30], ['Jane', 'Doe', 30], ['Tom', 'Moore', 28] ]
The goal is to have 3 files:
last_name-Allen.txt <-- Contains Steph Allen 29
last_name-Doe.txt <-- Contains Jon Doe 30 Jane Doe 30
last_name-Moore.txt <-- Contains Tom Moore 28

ar = [ ['Steph', 'Allen', 29], ['Jon', 'Doe', 30], ['Jane', 'Doe', 30], ['Tom', 'Moore', 28] ]
grouped = ar.group_by{|el| el[1] }
# {"Allen"=>[["Steph", "Allen", 29]], "Doe"=>[["Jon", "Doe", 30], ["Jane", "Doe", 30]], "Moore"=>[["Tom", "Moore", 28]]}
grouped.each do |last_name, record|
File.open("last_name-#{last_name}.txt",'w') do |f|
f.puts record.join(' ')
end
end

If you wanted to do this in Groovy, you could use the groupBy method to get a map based on surname like so:
// Start with your list
def list = [ ['Steph', 'Allen', 29], ['Jon', 'Doe', 30], ['Jane', 'Doe', 30], ['Tom', 'Moore', 28] ]
// Group it by the second element...
def grouped = list.groupBy { it[ 1 ] }
println grouped
prints
[Allen:[[Steph, Allen, 29]], Doe:[[Jon, Doe, 30], [Jane, Doe, 30]], Moore:[[Tom, Moore, 28]]]
Then, iterate through this map, opening a new file for each surname and writing the content in (tab separated in this example)
grouped.each { surname, contents ->
new File( "last_name-${surname}.txt" ).withWriter { out ->
contents.each { person ->
out.writeLine( person.join( '\t' ) )
}
}
}

In ruby:
array.each{|first, last, age| open("last_name-#{last}.txt", "a"){|io| io.write([first, last, age, nil].join(" ")}}
It adds an extra space at the end of the file. This is to keep the space when there is another entity to be added.

use a hash with last name as the keys, then iterate over the hash and write each key/value pair to its own file.

In Groovy you can do this:
def a = ​[['Steph', 'Allen', 29], ['Jon', 'Doe', 30], ['Jane', 'Doe', 30], ['Tom', 'Moore', 28]]
a.each {
def name = "last_name-${it[1]}.txt"
new File(name) << it.toString()
}
Probably there is shorter (groovier) way to do this.

You can create a hash of with "second column" as key and value as "file handle". If you get the key in hash, just fetch file handle and write, else create new file handle and insert in hash.

This answer is in Ruby:
# hash which opens appropriate file on first access
files = Hash.new { |surname| File.open("last_name-#{surname}.txt", "w") }
list.each do |first, last, age|
files[last].puts [first, last, age].join(" ")
end
# closes all the file handles
files.values.each(&:close)

Related

Method to invert a mapping along a given range

Say I have this collection of objects:
[
{value: 1, contents: "one"},
{value: 2, contents: "two"},
{value: 3, contents: "three"},
{value: 4, contents: "four"},
{value: 5, contents: "five"}
]
And want to invert the relation of values to contents, like so:
[
{value: 5, contents: "one"},
{value: 4, contents: "two"},
{value: 3, contents: "three"},
{value: 2, contents: "four"},
{value: 1, contents: "five"}
]
I was unable to think of an algorithm to accomplish this. I'm using Ruby, but I'm not so concerned about the code as I am about the method of accomplishing this.
a = [
{value: 1, contents: "one"},
{value: 2, contents: "two"},
{value: 3, contents: "three"},
{value: 4, contents: "four"},
{value: 5, contents: "five"}
]
a.map{|h| h[:value]}.reverse.zip(a.map{|h| h[:contents]})
.map{|k, v| {value: k, contents: v}}
# =>
# [
# {:value=>5, :contents=>"one"},
# {:value=>4, :contents=>"two"},
# {:value=>3, :contents=>"three"},
# {:value=>2, :contents=>"four"},
# {:value=>1, :contents=>"five"}
#]
Or,
a.each_index.map{|i| {value: a[-i - 1][:value], contents: a[i][:contents]}}
Letting arr equal your array of hashes, here are a couple of ways you could do it.
Two passes, no indices
value_vals = arr.map {|h| h[:value]}.reverse
#=> [5, 4, 3, 2, 1]
arr.map { |h| {value: value_vals.shift, contents: h[:contents]}}
#=> [{:value=>5, :contents=>"one"},
# {:value=>4, :contents=>"two"},
# {:value=>3, :contents=>"three"},
# {:value=>2, :contents=>"four"},
# {:value=>1, :contents=>"five"}]
One pass, but not pretty
arr.each_index.map {|i,a| {value: arr[-1-i][:value], contents: arr[i][:contents]}}
#=> [{:value=>5, :contents=>"one"},
# {:value=>4, :contents=>"two"},
# {:value=>3, :contents=>"three"},
# {:value=>2, :contents=>"four"},
# {:value=>1, :contents=>"five"}]
TL;DR
arr.zip(arr.reverse).map {|a, b| a.merge(value: b[:value]) }
Since reverse makes a copy of the array, this will take twice as much memory as other methods—which for most data sets probably isn't an issue at all. But if it is, there's an easy way to avoid it. See the "Bonus" section at the end of my answer.
Building an algorithm
The simplest (and probably best) solution is to walk the array, and for each item get the :value from its counterpart at the other end of the array. You can get an item's "counterpart" by subtracting the item's index from the index of the last item (i.e. the size of the array minus 1). So, if you have five items in an array called arr, the steps of the algorithm looks like this:
end_idx = arr.size - 1 # => 4
new_arr = []
new_arr[0] = { value: arr[end_idx - 0][:value], contents: arr[0][:contents] }
new_arr[1] = { value: arr[end_idx - 1][:value], contents: arr[1][:contents] }
new_arr[2] = { value: arr[end_idx - 2][:value], contents: arr[2][:contents] }
new_arr[3] = { value: arr[end_idx - 3][:value], contents: arr[3][:contents] }
new_arr[4] = { value: arr[end_idx - 4][:value], contents: arr[4][:contents] }
As you can see, every step is the same but with one number incremented, so I bet you already know how to turn this into a loop:
end_idx = arr.size - 1 # => 4
new_arr = []
0.upto(end_idx) do |idx|
new_arr[idx] = { value: arr[end_idx - idx][:value],
contents: arr[idx][:contents] }
end
Easy, and to be honest a perfectly good solution. However, it's not very "Rubyish." How do we make it more Rubyish? I'm glad you asked!
Make it more Rubyish
It's a pretty common situation to want, as an output, an array with one item corresponding to each item in an input array. Because it's so common, we have the Enumerable#map method, which does exactly that: It "maps" every item in an input array (or other Enumerable) to an item in an output array.
map walks over the items of the array, which is just what we need, but it's missing one thing we need: The index of the current item. To get that, we can "chain" the with_index method onto the map method, and now, in addition to the array item itself, the block will be passed a second argument, which is its index. Now we have everything we need:
end_idx = vals.size - 1
arr.map.with_index do |hsh, idx|
{ value: arr[end_idx - idx][:value],
contents: hsh[:contents] }
end
Alternatively, if we don't want to explicitly specify the structure of the hash (as we might if the hash comes from, say, user input or a database query and might have keys other than :value and :contents that we want to preserve without having to keep track of changes to the input form or database schema), we could do this:
end_idx = vals.size - 1
arr.map.with_index do |hsh, idx|
hsh.merge(value: arr[end_idx - idx][:value])
end
But I've saved the best for last.
At last...
arr.zip(arr.reverse_each).map do |a, b|
a.merge(value: b[:value])
end
What's going on here? The Array#zip method takes two arrays and "zips" them up, so e.g. [1, 2, 3].zip([:a, :b, :c]) yields [[1, :a], [2, :b], [3, :c]], so we do that with our array and its reverse (or, rather, an Enumerable that yields successive items from the end of the array, which is what reverse_each returns), and then we use map to set the value at :value from the latter on a copy of the former (using merge).
Bonus: Why reverse_each and not just reverse? Because we can make it lazy! Suppose arr has a billion items. If you call arr.zip(arr.reverse), now you have a (two-dimensional) array with two billion items. Maybe that's not a big deal (or maybe you don't have anywhere near a billion items), but if it is, laziness can help us out:
new_enum = arr.lazy.zip(arr.reverse_each).map do |a, b|
a.merge(value: b[:value])
end
# => #<Enumerator::Lazy: ...>
All we've done is added lazy, and now we get an Enumerator back instead of an array. This won't even do any work until we call, say, each or some other Enumerable method on it, and when we do that it will only operate on as many items as we ask it to. For example, say we just want the first three items:
new_enum.take(3).to_a
# => [ { value: 1000000000, contents: "one" },
# { value: 999999999, contents: "two" },
# { value: 999999998, contents: "three" } ]
Thanks to laziness, we never had to make a copy of the whole array and reverse it (and take up the corresponding amount of memory); we only had to deal with three items.
And if you do want all of the items, but still want to avoid making a copy of the whole array, just call new_enum.to_a.

Array of Hashes to CSV with Ruby [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 8 years ago.
Improve this question
I'm working on creating a CSV file of orders from an array of hashes. The name and id in the CSV output need to be the same on each line, but the amount of lines for each order depends on the amount of skus that are in the order.
Is there a simple way to output this orders array?
orders = []
orders << { name:"bob", id:123, sku:[ "123a", "456b", "xyz1" ], qty:[ 2, 4, 1 ] }
orders << { name:"kat", id:987, sku:[ "456b", "aaa0", "xyz1" ], qty:[ 8, 9, 5 ] }
orders << { name:"kat", id:222, sku:[ "123a" ], qty:[ 4 ] }
To a CSV file like this:
name,id,sku,qty
bob,123,123a,2
bob,123,456b,4
bob,123,xyz1,1
kat,987,456b,8
kat,987,aaa0,9
kat,987,xyz1,5
kat,222,123a,4
First of all, let’s prepare the array to dump to CSV:
asarray = orders.map { |e|
[e[:name], e[:id], e[:sku].zip(e[:qty])]
}.map { |e|
e.last.map { |sq| [*e[0..1], *sq] }
}
Now we have raw array ready to be serialized to CSV:
require 'csv'
CSV.open("path/to/file.csv", "wb") do |csv|
csv << ["name", "id", "sku", "qty"]
asarray.each { |order|
order.each { |row|
csv << row
}
}
end
As a variant:
orders = []
orders << { name:"bob", id:123, sku:[ "123a", "456b", "xyz1" ], qty:[ 2, 4, 1 ] }
orders << { name:"kat", id:987, sku:[ "456b", "aaa0", "xyz1" ], qty:[ 8, 9, 5 ] }
orders << { name:"kat", id:222, sku:[ "123a" ], qty:[ 4 ] }
csv = ''
orders.each do |el|
el[:qty].length.times do |idx|
csv += "#{el[:name]},#{el[:id]},#{el[:sku][idx]},#{el[:qty][idx]}\n"
end
end
puts csv
Result:
#> bob,123,123a,2
#> bob,123,456b,4
#> bob,123,xyz1,1
#> kat,987,456b,8
#> kat,987,aaa0,9
#> kat,987,xyz1,5
#> kat,222,123a,4
Try this:
module SKUSeparator
def map_by_skus
inject([]) do |csv, order|
order[:sku].each_with_index do |sku, index|
csv << [order[:name], order[:id], order[:sku][index], order[:qty][index]]
end
csv
end
end
def to_csv
map_by_skus.map { |line| line.join(",") }.join("\n")
end
end
ORDERS = [
{:name=>"bob", :id=>123, :sku=>["123a", "456b", "xyz1"], :qty=>[2, 4, 1]},
{:name=>"kat", :id=>987, :sku=>["456b", "aaa0", "xyz1"], :qty=>[8, 9, 5]},
{:name=>"kat", :id=>222, :sku=>["123a"], :qty=>[4]}
]
ORDERS.extend(SKUSeparator).map_by_skus # =>
# [
# ["bob", 123, "123a", 2],
# ["bob", 123, "456b", 4],
# ["bob", 123, "xyz1", 1],
# ["kat", 987, "456b", 8],
# ["kat", 987, "aaa0", 9],
# ["kat", 987, "xyz1", 5],
# ["kat", 222, "123a", 4]
# ]
ORDERS.extend(SKUSeparator).to_csv # =>
# bob,123,123a,2
# bob,123,456b,4
# bob,123,xyz1,1
# kat,987,456b,8
# kat,987,aaa0,9
# kat,987,xyz1,5
# kat,222,123a,4

Merge multiple arrays using zip

This may be a silly one. But I am not able to figure it out.
names = ['Fred', 'John', 'Mark']
age = [27, 40, 25]
location = ['Sweden', 'Denmark', 'Poland']
names.zip(age)
#Outputs => [["Fred", 27], ["John", 40], ["Mark", 25]]
But I need to output it with the 3rd array (location) with it.
#Expected Output => [["Fred", 27,"Sweden"], ["John", 40,"Denmark"], ["Mark", 25,"Poland"]]
The most important condition here is that, there may be any number of arrays but the output should form from the first element of each array and encapsulate inside another array.
Thanks for any help.
Try passing multiple arguments to zip:
names.zip(age, location)
# => [["Fred", 27, "Sweden"], ["John", 40, "Denmark"], ["Mark", 25, "Poland"]]

Convert Table to Multidimensional Hash in Ruby

I know that there must be some simple and elegant way to do this, but I'm drawing a blank.
I have a table (or group of key value pairs)
id,val
64664,68
64665,65
64666,53
64667,68
64668,6
64668,27
64668,33
64669,12
In most cases there is one value per id. In some cases there are multiples.
I want to end up with each id with multiple values represented as an array of those values
something like this:
[ 64664 => 68,
64665 => 65,
64666 => 53,
64668 =>[6,27,33],
64669 => 12
]
Any brilliant ideas?
You can use Hash#merge to merge two hashes. Using Enumerable#inject, you can get what you want.
tbl = [
[64664, 68],
[64665, 65],
[64666, 53],
[64667, 68],
[64668, 6],
[64668, 27],
[64668, 33],
[64669, 12],
]
# Convert the table to array of hashes
hashes = tbl.map { |id, val|
{id => val}
}
# Merge the hashes
hashes.inject { |h1, h2|
h1.merge(h2) { |key,old,new|
(old.is_a?(Array) ? old : [old]) << new
}
}
# => {64664=>68, 64665=>65, 64666=>53, 64667=>68, 64668=>[6, 27, 33], 64669=>12}
values = [
[64664, 68],
[64665, 65],
[64666, 53],
[64667, 68],
[64668, 6],
[64668, 27],
[64668, 33],
[64669, 12],
]
# When key not present, create new empty array as default value
h = Hash.new{|h,k,v| h[k]=[]}
values.each{|(k,v)| h[k] << v}
p h #=>{64664=>[68], 64665=>[65], 64666=>[53], 64667=>[68], 64668=>[6, 27, 33], 64669=>[12]}

How to remove outside array from joining two separate arrays with collect

I have an object called Grade with two attributes material and strength.
Grade.all.collect { |g| g.material }
#=> [steel, bronze, aluminium]
Grade.all.collect { |g| g.strength }
#=> [75, 22, 45]
Now I would like to combine both to get the following output:
[steel, 75], [bronze, 22], [aluminium, 45]
I currently do this
Grade.all.collect{|e| e.material}.zip(Grade.all.collect{|g| g.strength})
#=> [[steel, 75], [bronze, 22], [aluminium, 45]]
Note: I do not want the outside array [[steel, 75], [bronze, 22], [aluminium, 45]]
Any thoughts?
Splat the array to a mere list.
*Grade.all.collect{ |g| [g.material, g.strength] }

Resources