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

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)

Related

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

Searching through two multidimensional arrays and grouping together similar subarrays

I am trying to search through two multidimensional arrays to find any elements in common in a given subarray and then put the results in a third array where the entire subarrays with similar elements are grouped together (not just the similar elements).
The data is imported from two CSVs:
require 'csv'
array = CSV.read('primary_csv.csv')
#=> [["account_num", "account_name", "primary_phone", "second_phone", "status],
#=> ["11111", "John Smith", "8675309", " ", "active"],
#=> ["11112", "Tina F.", "5551234", "5555678" , "disconnected"],
#=> ["11113", "Troy P.", "9874321", " ", "active"]]
# and so on...
second_array = CSV.read('customer_service.csv')
#=> [["date", "name", "agent", "call_length", "phone", "second_phone", "complaint"],
#=> ["3/1/15", "Mary ?", "Bob X", "5:00", "5551234", " ", "rude"],
#=> ["3/2/15", "Mrs. Smith", "Stew", "1:45", "9995678", "8675309" , "says shes not a customer"]]
# and so on...
If any number is present as an element in a subarray on both primary.csv and customer_service.csv, I want that entire subarray (as opposed to just the common elements), put into a third array, results_array. The desire output based upon the above sample is:
results_array = [["11111", "John Smith", "8675309", " ", "active"],
["3/2/15", "Mrs. Smith", "Stew", "1:45", "9995678", "8675309" , "says shes not a customer"]] # and so on...
I then want to export the array into a new CSV, where each subarray is its own row of the CSV. I intend to iterate over each subarray by joining it with a , to make it comma delimited and then put the results into a new CSV:
results_array.each do {|j| j.join(",")}
File.open("results.csv", "w") {|f| f.puts results_array}
#=> 11111,John Smith,8675309, ,active
#=> 3/2/15,Mrs. Smith,Stew,1:45,9995678,8675309,says shes not a customer
# and so on...
How can I achieve the desired output? I am aware that the final product will look messy because similar data (for example, phone number) will be in different columns. But I need to find a way to generally group the data together.
Suppose a1 and a2 are the two arrays (excluding header rows).
Code
def combine(a1, a2)
h2 = a2.each_with_index
.with_object(Hash.new { |h,k| h[k] = [] }) { |(arr,i),h|
arr.each { |e| es = e.strip; h[es] << i if number?(es) } }
a1.each_with_object([]) do |arr, b|
d = arr.each_with_object([]) do |str, d|
s = str.strip
d.concat(a2.values_at(*h2[s])) if number?(s) && h2.key?(s)
end
b << d.uniq.unshift(arr) if d.any?
end
end
def number?(str)
str =~ /^\d+$/
end
Example
Here is your example, modified somewhat:
a1 = [
["11111", "John Smith", "8675309", "", "active" ],
["11112", "Tina F.", "5551234", "5555678", "disconnected"],
["11113", "Troy P.", "9874321", "", "active" ]
]
a2 = [
["3/1/15", "Mary ?", "Bob X", "5:00", "5551234", "", "rude"],
["3/2/15", "Mrs. Smith", "Stew", "1:45", "9995678", "8675309", "surly"],
["3/7/15", "Cher", "Sonny", "7:45", "9874321", "8675309", "Hey Jude"]
]
combine(a1, a2)
#=> [[["11111", "John Smith", "8675309", "",
# "active"],
# ["3/2/15", "Mrs. Smith", "Stew", "1:45",
# "9995678", "8675309", "surly"],
# ["3/7/15", "Cher", "Sonny", "7:45",
# "9874321", "8675309", "Hey Jude"]
# ],
# [["11112", "Tina F.", "5551234", "5555678",
# "disconnected"],
# ["3/1/15", "Mary ?", "Bob X", "5:00",
# "5551234", "", "rude"]
# ],
# [["11113", "Troy P.", "9874321", "",
# "active"],
# ["3/7/15", "Cher", "Sonny", "7:45",
# "9874321", "8675309", "Hey Jude"]
# ]
# ]
Explanation
First, we define a helper:
def number?(str)
str =~ /^\d+$/
end
For example:
number?("8675309") #=> 0 ("truthy)
number?("3/1/15") #=> nil
Now index a2 on the values that represent numbers:
h2 = a2.each_with_index
.with_object(Hash.new { |h,k| h[k] = [] }) { |(arr,i),h|
arr.each { |e| es = e.strip; h[es] << i if number?(es) } }
#=> {"5551234"=>[0], "9995678"=>[1], "8675309"=>[1, 2], "9874321"=>[2]}
This says, for example, that the "numeric" field "8675309" is contained in elements at offsets 1 and 2 of a2 (i.e, for Mrs. Smith and Cher).
We can now simply run through the elements of a1 looking for matches.
The code:
arr.each_with_object([]) do |str, d|
s = str.strip
d.concat(a2.values_at(*h2[s])) if number?(s) && h2.key?(s)
end
steps through the elements of arr, assigning each to the block variable str. For example, if arr holds the first element of a1 str will in turn equals "11111", "John Smith", and so on. After s = str.strip, this says that if a s has a numerical representation and there is a matching key in h2, the (initially empty) array d is concatenated with the elements of a2 given by the value of h2[s].
After completing this loop we see if d contains any elements of a2:
b << d.uniq.unshift(arr) if d.any?
If it does, we remove duplicates, prepend the array with arr and save it to b.
Note that this allows one element of a2 to match multiple elements of a1.

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"}

Convert Nested hashes in Ruby into a table format

I have a nested hash in ruby:
{"Table1"=>{"Acct type"=>{"Expected"=>"ACC", "Actual"=>"ACC"}, "Seq No"=>{"Expected"=>"100.0", "Actual"=>#<BigDecimal:56d0b28,'0.1E3',9(18)>}, "Class"=>{"Expected"=>"AC", "Actual"=>"AC"}}, "Table 2"=>{"Date"=>{"Expected"=>"20140606", "Actual"=>"20130606"}}}
I need to display the above nested hash in a table format -
Table Field Expected Value Actual Value
Table 1 Acct type ACC ACC
Table 1 Seq No 100.0 100.0
Table 1 Class AC AC
Table 2 Date 20140606 20130606
Any suggestions/pointers would be really helpful. I have tried using 'tableprint' and 'text-table' gems but couldn't get the desired result. The above data was fetched from DB2 tables using ActiveRecord.
Here is one solution:
x = {
"Table1"=>{
"Acct type"=>{"Expected"=>"ACC", "Actual"=>"ACC"},
"Seq No"=>{"Expected"=>"100.0", "Actual"=> 100.00},
"Class"=>{"Expected"=>"AC", "Actual"=>"AC"}
},
"Table 2"=>{
"Date"=> {"Expected"=>"20140606",
"Actual"=>"20130606"}}}
def table_row(row_name, row_values)
[row_name, row_values["Expected"], row_values["Actual"]]
end
def table_rows(table_hash)
table_hash.map do |field_name, field_values|
table_row(field_name, field_values)
end
end
def widest_string_lengths(visual_table)
table_columns_count = visual_table.first.size - 1
0.upto(table_columns_count).map do |index|
columns = visual_table.map { |row| row[index].to_s }
columns.max_by(&:length).length + 2
end
end
def aligned_visual_table(visual_table)
widest_string_items = widest_string_lengths(visual_table)
visual_table.map do |row|
row.each_with_index.map do |cell, index|
cell.to_s.ljust(widest_string_items[index] + 2)
end
end
end
table_headers = ['Table', 'Field', 'Value', 'Actual Value']
# This just turns the hash to array of arrays
visual_table = x.map do |table_name, table_hash|
table_rows(table_hash).map do |table_rows|
table_rows.insert(0, table_name)
end
end.flatten(1).insert(0, table_headers)
puts "*********"
puts aligned_visual_table(visual_table).map(&:join).join("\n")
You example suggests there are unspecified parameters affecting the appearance of the table. For example, the column labels "Table" and "Field" are left-adjusted, whereas "Expected Value" and "Actual Value" are not. Also, "Expected Value" overlaps "Acct type". Below I suggest how you might construct the table, but I've made compromises: left-adjusting all column labels and not overlapping vertically.
Code
def print_table(h, column_labels_and_spacing, table_label)
column_labels = column_labels_and_spacing.map(&:first)
column_spacing = column_labels_and_spacing[0..-2].map(&:last) << 0
rows = h.map { |k,f|
[table_label[k]].product(f.map { |field, g|
[[field, g.values.map(&:to_s)].flatten] }) }
.flat_map { |e| e.map(&:flatten) }.unshift(column_labels)
column_widths = rows.transpose.map { |c| c.max_by(&:size).size }
.zip(column_spacing).map { |a,b| a+b }
rows.each do |row|
row.zip(column_widths).each { |s, width| print s.ljust(width) }
puts
end
end
Example
Input
h = {"Table1"=>
{"Acct type"=>{"Expected"=>"ACC", "Actual"=>"ACC"},
"Seq No" =>{"Expected"=>"100.0", "Actual"=>100},
"Class" =>{"Expected"=>"AC", "Actual"=>"AC"}},
"Table 2"=>
{"Date" =>{"Expected"=>"20140606", "Actual"=>"20130606"}}}
column_labels_and_spacing = [["Table", 1], ["Field", 2],
["Expected", 3], ["Actual"]]
table_label = { "Table1"=>"Table 1", "Table 2"=>"Table 2" }
To simplify the explanation I converted the BigDecimal value to a Fixnum.
Invoke method
print_table(h, column_labels_and_spacing, table_label)
#-> Table Field Expected Actual
# Table 1 Acct type ACC ACC
# Table 1 Seq No 100.0 100
# Table 1 Class AC AC
# Table 2 Date 20140606 20130606
Explanation
For the above example:
column_labels = column_labels_and_spacing.map(&:first)
#=> ["Table", "Field", "Expected", "Actual"]
column_spacing = column_labels_and_spacing[0..-2].map(&:last) << 0
#=> [1, 2, 3, 0]
a = h.map { |k,f|
[table_label[k]].product(f.map { |field, g|
[[field, g.values.map(&:to_s)].flatten] }) }
# => [[["Table 1", [["Acct type", "ACC", "ACC"]]],
# ["Table 1", [["Seq No", "100.0", "100"]]],
# ["Table 1", [["Class", "AC", "AC"]]]],
# [["Table 2", [["Date", "20140606", "20130606"]]]]]
To see how a is calculated, consider the first key-value pair of h passed to the block1, the key being assigned to the block variable k and the value assigned to f:
k = "Table1"
f = { "Acct type"=>{ "Expected"=>"ACC", "Actual"=>"ACC" } }
The block then performs the following calculations:
b = f.map { |field, g| [[field, g.values.map(&:to_s)].flatten] }
# => [["Acct type", "ACC", "ACC"]]
c = ["Table 1"].product([[["Acct type", "ACC", "ACC"]]])
#=> [["Table 1", [["Acct type", "ACC", "ACC"]]]]
d = c.flatten
#=> ["Table 1", "Acct type", "ACC", "ACC"]
Now let's carry on, having computed a. Compute the rows of the body of the table:
e = a.flat_map { |e| e.map(&:flatten) }
#=> [["Table 1", "Acct type", "ACC", "ACC"],
# ["Table 1", "Seq No", "100.0", "100"],
# ["Table 1", "Class", "AC", "AC"],
# ["Table 2", "Date", "20140606", "20130606"]]
and then add the column labels:
rows = e.unshift(column_labels)
#=> [["Table", "Field", "Expected", "Actual"],
# ["Table 1", "Acct type", "ACC", "ACC"],
# ["Table 1", "Seq No", "100.0", "100"],
# ["Table 1", "Class", "AC", "AC"],
# ["Table 2", "Date", "20140606", "20130606"]]
Next, compute column widths:
f = rows.transpose
#=> [["Table", "Table 1", "Table 1", "Table 1", "Table 2"],
# ["Field", "Acct type", "Seq No", "Class", "Date"],
# ["Expected", "ACC", "100.0", "AC", "20140606"],
# ["Actual", "ACC", "100", "AC", "20130606"]]
g = f.map { |c| c.max_by(&:size).size }
#=> [7, 9, 8, 8]
i = g.zip(column_spacing)
# => [[7, 1], [9, 2], [8, 3], [8, 0]]
column_widths = i.map { |a,b| a+b }
#=> [8, 11, 11, 8]
rows.each do |row|
row.zip(column_widths).each { |s, width| print s.ljust(width) }
puts
end
Consider the printing of the second row of the table (the first row after the column labels):
row = ["Table 1", "Acct type", "ACC", "ACC"]
row.zip(column_widths)
# => [["Table 1", 8], ["Acct type", 11], ["ACC", 11], ["ACC", 8]]
The first field is printed:
s = "Table 1"
width = 8
print s.ljust(width); puts ?:
#Table 1 :
where I printed the colon just to show the field width. Other fields are printed similarly.
1 Ruby 1.9+ is required to know which key-value pair is passed to the block first.

Sequentially parse array to hash in 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]}]

Resources