Sequentially parse array to hash in Ruby - ruby

I have an array that looks like this:
array = [
"timestamp 1",
"data 1",
"data 2",
"data 3",
"timestamp 2",
"data "1",
"timestamp 3",
".."
]
etc
I want to loop through my array, and turn it into a hash data structure that looks like:
hash = {
"timestamp 1" => [ "data 1", " data 2", "data 3" ],
"timestamp 2" => [ "data 1" ],
}
I can't figure out a good "rubyish" way of doing it. I'm looping through the array, and I just quite can't seem to figure out how to keep track of where I am at, and assign to the hash as needed.
# Let's comb through the array, and map the time value to the subsequent lines beneath
array.each do |e|
if timestamp?(e)
hash["#{e}"] == nil
else
# last time stamp here => e
end
EDIT: Here is the timestamp? method
def timestamp?(string)
begin
return true if string =~ /[a-zA-z][a-z][a-z]\s[a-zA-z][a-z][a-z]\s\d\d\s\d\d:\d\d:\d\d\s\d\d\d\d/
false
rescue => msg
puts "Error in timestamp? => #{msg}"
exit
end
end

array = [
"timestamp 1",
"data 1",
"data 2",
"data 3",
"timestamp 2",
"data 1",
"timestamp 3",
"data 2"
]
hsh = {}
ary = []
array.each do |line|
if line.start_with?("timestamp")
ary = Array.new
hsh[line] = ary
else
ary << line
end
end
puts hsh.inspect

I would do as below:
array = [
"timestamp 1",
"data 1",
"data 2",
"data 3",
"timestamp 2",
"data 1",
]
Hash[array.slice_before{|i| i.include? 'timestamp'}.map{|a| [a.first,a[1..-1]]}]
# => {"timestamp 1"=>["data 1", "data 2", "data 3"], "timestamp 2"=>["data 1"]}

Hash[array.slice_before{|e| e.start_with?("timestamp ")}.map{|k, *v| [k, v]}]
Output
{
"timestamp 1" => [
"data 1",
"data 2",
"data 3"
],
"timestamp 2" => ["data 1"],
"timestamp 3" => [".."]
}

You can keep track of the last hash key using an outside variable. It will be persisted across all iterations:
h = {}
last_group = nil
array.each do |e|
if timestamp?(e)
array[e] = []
last_group = e
else
h[last_group] << e
end
end

last_timestamp = nil
array.reduce(Hash.new(){|hsh,k| hsh[k]=[]}) do |hsh, m|
if m =~ /timestamp/
last_timestamp = m
else
hsh[last_timestamp] << m
end
hsh
end

hash = (Hash.new { |this, key| this[key] = [] } ).tap do |hash|
current_timestamp = nil
array.each do |element|
current_timestamp = element if timestamp? element
hash[current_timestamp] << element unless timestamp? element
end
end
Using an outside variable to keep track of the current timestamp, but wrapping it in a closure to avoid polluting the namespace.

I know this has already been answered, but there are so many ways to do this.
I prefer these two ways, they might not be fast but i find them readable:
my_hash = Hash.new
array.slice_before(/timestamp/).each do |array|
key, *values = array
my_hash[key] = values
end
or
one_liner = Hash[array.slice_before(/timestamp/).map{|x|[x.shift, x]}]

Related

Find all values that match a parameter in a nested ruby hash

If I have a multi-nested hash like so
{
"Monday"=>{
"North"=>{
"Group 1"=>[
{:name=>"Event A", :type=>"Private"},
{:name=>"Event B", :type=>"Public"},
]
},
"South"=>{
"Group 1"=>[
{:name=>"Event c", :type=>"Private"},
{:name=>"Event D", :type=>"Public"},
{:name=>"Event E", :type=>"Private"},
]
}
},
"Tuesday"=>{
"North"=>{
"Group 1"=>[
{:name=>"Event F", :type=>"Private"},
{:name=>"Event G", :type=>"Public"},
]
},
"South"=>{
"Group 1"=>[
{:name=>"Event H", :type=>"Private"},
]
}
}
}
I would like to be able to search within the hash for all Events that have a type that is equal to Private
How would I go about doing this without knowing exactly what the values of the keys will be in the hash?
If using the gem is an option, there is iteraptor, that is explicitly about iterating deeply nested structures.
Assuming your original hash is named hash, here we go:
hash.iteraptor.
each(full_parent: true, yield_all: true).
with_object({}) do |(parent, (k, v)), acc|
(acc[parent[0...-1]] ||= []) << k if
parent.last.is_a?(Integer) && v.nil? && k.is_a?(Hash) && k[:type] == "Private"
end
Resulting in:
#⇒ {["Monday", "North", "Group 1"] =>
# [{:name=>"Event A", :type=>"Private"}],
# ["Monday", "South", "Group 1"] =>
# [{:name=>"Event c", :type=>"Private"},
# {:name=>"Event E", :type=>"Private"}],
# ["Tuesday", "North", "Group 1"] =>
# [{:name=>"Event F", :type=>"Private"}],
# ["Tuesday", "South", "Group 1"] =>
# [{:name=>"Event H", :type=>"Private"}]}
In solving this recursively I have made three assumptions:
There can be any number of nested arrays and hashes;
:type is the only known key;
if a hash contains the key :type it contains exactly one other key.
def get_em(obj)
arr = []
case obj
when Hash
obj.values.each do |v|
case v
when "Private"
arr += obj.values-[v]
when Hash, Array
arr += get_em(v)
end
end
when Array
obj.each { |e| arr += get_em(e) if Hash === e || Array === e }
end
arr
end
If h is the hash given in the example,
get_em(h)
#=> ["Event A", "Event C", "Event E", "Event F", "Event H"]
Note Hash === e is equivalent to e.is_a?(Hash).
Try this recursion:
def hash_match(the_hash)
found=false
the_hash.each do |key, value|
if value.is_a?(Hash)
if hash_match(value)
if value.has_key :name
puts value[:name]
end
end
elsif value.is_a?(Array)
value.each do |element|
if element.is_a?(Hash)
if hash_match(element)
if element.has_key? :name
puts element[:name]
end
end
end
end
else
if key==:type && value=="Private"
found=true
end
end
end
return found
end
Then just call hash_match(your_hash)

how do I map one csv to another with ruby

I have two csv's with different headers.
lets say csv 1 has headers one, two, three, four and I want to create a csv with headers five, six, seven, eight.
I'm having a hard time writing the code to open the first CSV and then creating the second CSV.
Here is the current code that I have.
require 'csv'
wmj_headers = [
"Project Number",
"Task ID",
"Task Name",
"Status Comment",
"Act Complete",
"Plan Complete",
"Description"]
jir_headers_hash = {
"Summary" => "Task Name",
"Issue key" => "Status Comment",
"Resolved" => "Act Complete",
"Due date" => "Plan Complete",
"Description" => "Description"
}
puts "Enter path to a directory of .csv files"
dir_path = gets.chomp
csv_file_names = Dir["#{dir_path}*.csv"]
csv_file_names.each do |f_path|
base_name = File.basename(f_path, '.csv')
wmj_name = "#{base_name}_wmj.csv"
arr = []
mycount = 0
CSV.open(wmj_name, "wb") do |row|
row << wmj_headers
CSV.foreach(f_path, :headers => true) do |r|
r.headers.each do |value|
if jir_headers_hash[value].nil? == false
arr << r[value]
end
end
end
row << arr
end
end
People tend to overcomplicate things. You don’t need any CSV processing at all to substitute headers.
$ cat /tmp/src.csv
one,two,three
1,2,3
4,5,6
Let’s substitute the headers and stream everything else untouched.
subst = {"one" => "ONE", "two" => "TWO", "three" => "THREE"}
src, dest = %w[/tmp/src.csv /tmp/dest.csv].map { |f| File.new f, "a+" }
headers = src.readline() # read just headers
dest.write(headers.gsub(/\b(#{Regexp.union(subst.keys)})\b/, )) # write headers
IO.copy_stream(src, dest, -1, headers.length) # stream the rest
[src, dest].each(&:close)
Check it:
$ cat /tmp/dest.csv
ONE,TWO,THREE
1,2,3
4,5,6
If you want to substitute CSV column names, here it is:
require 'csv'
# [["one", "two", "three"], ["1", "2", "3"], ["4", "5", "6"]]
csv = CSV.read('data.csv')
# new keys
ks = ['k1', 'k2', 'k3']
# [["k1", "k2", "k3"], ["1", "2", "3"], ["4", "5", "6"]]
k = csv.transpose.each_with_index.map do |x,i|
x[0] = ks[i]
x
end.transpose
# write new file
CSV.open("myfile.csv", "w") do |csv|
k.each do |row|
csv << row
end
end

Group List of hashes and index the values

I has an array of hashes
Some hashes are duplicate
I want to keep the duplicate, but add counter to the title
For example "TITLE #1" And "TITLE #2"
This is my Array
list = []
#temp = {}
#temp["name"] = "Germany"
#temp["id"] = 1
list << #temp
#temp["name"] = "USA"
#temp["id"] = 2
list << #temp
#temp["name"] = "USA"
#temp["id"] = 3
list << #temp
#temp["name"] = "France"
#temp["id"] = 4
list << #temp
#temp["name"] = "France"
#temp["id"] = 5
list << #temp
#temp["name"] = "France"
#temp["id"] = 6
list << #temp
I Want the result Same as the source but near "USA" add the counter "USA #1" and "USA #2"
And France change to "France #1", "France #2" "France #3"
No change on germany element because there are not multiple items
You need #temp = 0 at the beginning of each block of code.
After executing your code with the modification
list = [{"name"=>"Germany", "id"=>1},
{"name"=>"USA", "id"=>2},
{"name"=>"USA", "id"=>3},
{"name"=>"France", "id"=>4},
{"name"=>"France", "id"=>5},
{"name"=>"France", "id"=>6}]
We can then obtain your desired result as follows.
list.group_by { |h| h["name"] }.values.flat_map do |a|
a.map.with_index(1) do |h,i|
base = h["name"]
h.merge("name"=>base +" #{i}")
end
end
#=> [{"name"=>"Germany 1", "id"=>1},
# {"name"=>"USA 1", "id"=>2},
# {"name"=>"USA 2", "id"=>3},
# {"name"=>"France 1", "id"=>4},
# {"name"=>"France 2", "id"=>5},
# {"name"=>"France 3", "id"=>6}]
Note
arr = list.group_by { |h| h["name"] }.values
#=> [[{"name"=>"Germany", "id"=>1}],
# [{"name"=>"USA", "id"=>2}, {"name"=>"USA", "id"=>3}],
# [{"name"=>"France", "id"=>4}, {"name"=>"France", "id"=>5},
# {"name"=>"France", "id"=>6}]]
Had I used Enumerable#map rather than Enumerable#flat_map, the result would have been
[[{"name"=>"Germany 1", "id"=>1}],
[{"name"=>"USA 1", "id"=>2}, {"name"=>"USA 2", "id"=>3}],
[{"name"=>"France 1", "id"=>4}, {"name"=>"France 2", "id"=>5},
{"name"=>"France 3", "id"=>6}]]
Using flat_map is equivalent to inserting a splat in front of each of this array's elements.
[*[{"name"=>"Germany 1", "id"=>1}],
*[{"name"=>"USA 1", "id"=>2}, {"name"=>"USA 2", "id"=>3}],
*[{"name"=>"France 1", "id"=>4}, {"name"=>"France 2", "id"=>5},
{"name"=>"France 3", "id"=>6}]]

Create hash-of-hashes using structure of the default hash

I have the following code:
default = {:id => 0, :detail =>{:name=>"Default", :id => ""}}
employees = {}
nr = (0..3).to_a
nr.each do |n|
employee = default
employee[:id] = n
employee[:detail][:name] = "Default #{n}"
employee[:detail][:id] = "KEY-#{n}"
employees[n] = employee
end
puts employees
I expect the values for the key :id in :detail hash to be KEY-0, KEY-1, KEY-2.
You will need marshall your default in order to copy
default = {id: 0, detail: {name: "Default", id:""}}
employees = {}
4.times do |n|
employees[n] = Marshal.load(Marshal.dump(default))
employees[n][:id] = n
employees[n][:detail][:name] = "Default #{n}"
employees[n][:detail][:id] = "KEY-#{n}"
end
puts employees
The output is
{0=>{:id=>0, :detail=>{:name=>"Default 0", :id=>"KEY-0"}}, 1=>{:id=>1, :detail=>{:name=>"Default 1", :id=>"KEY-1"}}, 2=>{:id=>2, :detail=>{:name=>"Default 2", :id=>"KEY-2"}}, 3=>{:id=>3, :detail=>{:name=>"Default 3", :id=>"KEY-3"}}}
You can read this post Cloning an array with its content
ADDED
And here you have an reduce version and should be faster if you want.
employees = {}
4.times { |n| employees[n]={id: n, detail: {name: "Default #{n}", id:"KEY-#{n}"}} }
puts employees
You need only change:
default = { :id=>0, :detail=>{ :name=>"Default", :id=>"" } }
to
def default
{}.merge(:id=>0, :detail=>({}.merge(:name=>"Default", :id=>"")))
end
but, hey, while we're at it we may as well Ruby-ize the rest:
employees = (0..3).map do |n|
employee = default
employee[:id] = n
employee[:detail][:name] = "Default #{n}"
employee[:detail][:id] = "KEY-#{n}"
employee
end
#=> [{:id=>0, :detail=>{:name=>"Default 0", :id=>"KEY-0"}},
# {:id=>1, :detail=>{:name=>"Default 1", :id=>"KEY-1"}},
# {:id=>2, :detail=>{:name=>"Default 2", :id=>"KEY-2"}},
# {:id=>3, :detail=>{:name=>"Default 3", :id=>"KEY-3"}}]
Let's confirm we are making deep copies of default:
employees[0][:detail][:id] = "cat"
employees
#=> [{:id=>0, :detail=>{:name=>"Default 0", :id=>"cat"}},
# {:id=>1, :detail=>{:name=>"Default 1", :id=>"KEY-1"}},
# {:id=>2, :detail=>{:name=>"Default 2", :id=>"KEY-2"}},
# {:id=>3, :detail=>{:name=>"Default 3", :id=>"KEY-3"}}]
You'd more commonly see this written:
employees = (0..3).map do |n|
default.merge(:id=>n, :detail=>{:name=>"Default #{n}", :id=>"KEY-#{n}"})
end
#=> [{:id=>0, :detail=>{:name=>"Default 0", :id=>"cat"}},
# {:id=>1, :detail=>{:name=>"Default 1", :id=>"KEY-1"}},
# {:id=>2, :detail=>{:name=>"Default 2", :id=>"KEY-2"}},
# {:id=>3, :detail=>{:name=>"Default 3", :id=>"KEY-3"}}]
As suggested by other answers, you could to this:
class Object
def deep_copy
Marshal.load(Marshal.dump(self))
end
end
Then you could write:
default = { :id=>0, :detail=>{ :name=>"Default", :id=>"" } }
employees = (0..3).map do |n|
default.deep_copy.merge(:id=>n, :detail=>{:name=>"Default #{n}",
:id=>"KEY-#{n}"})
end
#=> [{:id=>0, :detail=>{:name=>"Default 0", :id=>"KEY-0"}},
# {:id=>1, :detail=>{:name=>"Default 1", :id=>"KEY-1"}},
# {:id=>2, :detail=>{:name=>"Default 2", :id=>"KEY-2"}},
# {:id=>3, :detail=>{:name=>"Default 3", :id=>"KEY-3"}}]
This has the advantage that, if you change default, no other changes are needed.
You are making a shallow copy in each iteration, i.e. each time each copy is overridden with the values calculated in the last iteration. You can try the following for you hash-within-hash default pattern to make a deep copy:
employee = Marshal.load(Marshal.dump(default))
Demonstration

Convert an Array of Strings to a Hash in Ruby

I have an Array that contains strings:
["First Name", "Last Name", "Location", "Description"]
I need to convert the Array to a Hash, as in the following:
{"A" => "First Name", "B" => "Last Name", "C" => "Location", "D" => "Description"}
Also, this way too:
{"First Name" => "A", "Last Name" => "B", "Location" => "C", "Description" => "D"}
Any thoughts how to handle this the best way?
You could implement as follows
def string_array_to_hash(a=[],keys=false)
headers = ("A".."Z").to_a
Hash[keys ? a.zip(headers.take(a.count)) : headers.take(a.count).zip(a)]
end
Then to get your initial output it would be
a = ["First Name", "Last Name", "Location", "Description"]
string_array_to_hash a
#=> {"A"=>"First Name", "B"=>"Last Name", "C"=>"Location", "D"=>"Description"}
And second output is
a = ["First Name", "Last Name", "Location", "Description"]
string_array_to_hash a, true
#=> {"First Name"=>"A", "Last Name"=>"B", "Location"=>"C", "Description"=>"D"}
Note: this will work as long as a is less than 27 Objects otherwise you will have to specify a different desired output. This is due to the fact that a) the alphabet only has 26 letters b) Hash objects can only have unique keys.
You could do this:
arr = ["First Name", "Last Name", "Location", "Description"]
letter = Enumerator.new do |y|
l = ('A'.ord-1).chr
loop do
y.yield l=l.next
end
end
#=> #<Enumerator: #<Enumerator::Generator:0x007f9a00878fd8>:each>
h = arr.each_with_object({}) { |s,h| h[letter.next] = s }
#=> {"A"=>"First Name", "B"=>"Last Name", "C"=>"Location", "D"=>"Description"}
h.invert
#=> {"First Name"=>"A", "Last Name"=>"B", "Location"=>"C", "Description"=>"D"}
or
letter = ('A'.ord-1).chr
#=> "#"
h = arr.each_with_object({}) { |s,h| h[letter = letter.next] = s }
#=> {"A"=>"First Name", "B"=>"Last Name", "C"=>"Location", "D"=>"Description"}
When using the enumerator letter, we have
27.times { puts letter.next }
#=> "A"
# "B"
# ...
# "Z"
# "AA"
If you are not being specific about keys name then you could try this out
list = ["First Name", "Last Name", "Location", "Description"]
Hash[list.map.with_index{|*x|x}].invert
Output
{0=>"First Name", 1=>"Last Name", 2=>"Location", 3=>"Description"}
Similar solutions is here.
Or..You also can try this :)
letter = 'A'
arr = ["First Name", "Last Name", "Location", "Description"]
hash = {}
arr.each { |i|
hash[i] = letter
letter = letter.next
}
// => {"First Name"=>"A", "Last Name"=>"B", "Location"=>"C", "Description"=>"D"}
or
letter = 'A'
arr = ["First Name", "Last Name", "Location", "Description"]
hash = {}
arr.each { |i|
hash[letter] = i
letter = letter.next
}
// => {"A"=>"First Name", "B"=>"Last Name", "C"=>"Location", "D"=>"Description"}

Resources