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)
I have a ugly array that looks like this
["advert 0", "[1404915231, 1404920520]", "advert 4", "[1404915231]", "advert 5", "[1404915231]", "advert 6", "[1404915231]", "advert 7", "[1404915231]", "advert 8", "[1404915231]", "advert 9", "[1404915231]"]
I've tried to get it to look like this but I have been unsuccessful.
{advert1: [1404915231, 1404920520], advert4: [1404915231]}
Thanks
Here is a way(if it helps you) :-
require 'yaml'
arr = [
"advert 0", "[1404915231, 1404920520]",
"advert 4", "[1404915231]", "advert 5",
"[1404915231]", "advert 6", "[1404915231]",
"advert 7", "[1404915231]", "advert 8",
"[1404915231]", "advert 9", "[1404915231]"
]
Hash[arr.each_slice(2).map { |a, b| [a.gsub(/\s+/,'').to_sym, YAML.load(b)] }]
# => {:advert0=>[1404915231, 1404920520],
# :advert4=>[1404915231],
# :advert5=>[1404915231],
# :advert6=>[1404915231],
# :advert7=>[1404915231],
# :advert8=>[1404915231],
# :advert9=>[1404915231]}
Enumerable#each_slice(2) - Is sending 2 items from the collection, to the Enumerable#map block. Now OP wants, string to be converted as symbols. Thus, I used first, to removes the white spaces in between strings. It is done by String#gsub method. I passed the regex, /\s+/ as an Argument to the method #gsub, which as per the regex, will find each white space and replace them with empty strings(''). That means
"advert 0".gsub(/\s+/,'') # => "advert0"
Now as OP wants, all keys to be symbols, I apply String#to_sym.
"advert0".to_sym # => :advert0
So finally, I need to covert all string arrays to array, thus YAML::load will be helpful.
YAML::load "[1404915231, 1404920520]" # => [1404915231, 1404920520]
Till now what I said, will give us -
arr.each_slice(2).map { |a, b| [a.gsub(/\s+/,'').to_sym, YAML.load(b)] }
# => [[:advert0, [1404915231, 1404920520]],
# [:advert4, [1404915231]],
# [:advert5, [1404915231]],
# [:advert6, [1404915231]],
# [:advert7, [1404915231]],
# [:advert8, [1404915231]],
# [:advert9, [1404915231]]]
Now, I will make it a Hash. Look this Hash[ key, value, ... ] → new_hash.
Hash[:a,[1],:b, [2,3]] # => {:a=>[1], :b=>[2, 3]}
Hope it helps.
Arup gives a great answer, but if you're looking for alternatives, here are some thoughts I had:
Break down the problem
Let's break down the problem. We have ugly_array, and we want to use its even elements ("advert 0", "advert 2") as keys in a Hash, and its odd elements as the corresponding values. We want to transform both the keys (into symbols) and the values (into arrays of numbers) on the way.
Create a (temporary) Hash
A useful method in Ruby is Hash[], which will create a Hash using its odd arguments as keys and even arguments as values. For example:
Hash[ :foo, 1, :bar, 2 ]
# => { :foo => 1, :bar => 2 }
In order to use the items in ugly_array as separate arguments, we put * ("splat") before it:
ugly_hash = Hash[ *ugly_array ]
This is equivalent to Hash[ "advert 0", "[1404915231, 1404920520]", "advert 4", ... ], and it gives us this Hash:
{ "advert 0" => "[1404915231, 1404920520]",
"advert 4" => "[1404915231]",
"advert 5" => "[1404915231]",
# ...
}
Transform the keys
Now we need to transform the keys and values. First, the keys. You want to turn the string "advert 0" into the symbol :advert0, which we can do like this:
"advert 0".gsub(" ", "").to_sym
# => :advert0
To clarify gsub(" ", "") replaces all spaces (" ") with nothing, which effectively removes them. We could also use a regular expression like /\s+/, but it looks like the extra flexibility isn't really needed here. Now we have the string "advert0", and to_sym converts it into the symbol :advert0.
Transform the values
We want to turn a string like "[1404915231, 1404920520]" into an array of numbers like [1404915231, 1404920520]. There are a lot of different ways to do this, but one way that doesn't require using a module like JSON or YAML is this:
"[1404915231, 1404920520]".scan(/\d+/).map(&:to_i)
# => [ 1404915231, 1404920520 ]
This does two things. First, scan(/\d+/) uses a regular expression to find sequences of consecutive digits (\d) in the string and returns them as an array, yielding an array of strings: ["1404915231", "1404920520"]. Then we use map(&:to_i) to call the to_i method on each of the strings, yielding an array of numbers.
Transform the temporary Hash
Now that we know how to transform the keys and values, we can take our temporary Hash and build a new, transformed Hash from it. There are other ways to do this, but I'm fond of Enumerable#each_with_object. Suppose we do this:
ugly_hash.each_with_object({}) do |(key, val), hsh|
hsh[key] = val
end
Inside the block, hsh is the new, empty Hash ({}) we gave as the argument to each_with_object, and in each iteration we add val to it with the key key. This yields:
{ "advert 0" => "[1404915231, 1404920520]",
"advert 4" => "[1404915231]",
# ...
}
It looks exactly the same! But you can probably see that since we have key and val inside the block, we can transform them before using them.
Bring it all together
pretty_hash = Hash[ *ugly_array ].each_with_object({}) do |(key, val), hsh|
key = key.tr(" ", "").to_sym
val = val.scan(/\d+/).map(&:to_i)
hsh[key] = val
end
As you can see, this is the same code, except inside the block we're transforming key and val before using them to add an element to the Hash. And it gives us just what we're looking for:
p pretty_hash
# => { :advert0 => [ 1404915231, 1404920520 ],
# :advert4 => [ 1404915231 ],
# :advert5 => [ 1404915231 ],
# ...
# }
Apart from being relatively readable and not requiring external libraries, this method has the distinct advantage of iterating over the original data exactly once and performing only one (composite) operation on each key and value.
If you're so inclined, you can reduce the inside of the block to a single line, but be aware of how it impacts readability:
pretty_hash = Hash[*ugly_array].each_with_object({}) do |(key, val), hsh|
hsh[ key.tr(" ", "").to_sym ] = val.scan(/\d+/).map(&:to_i)
end
I hope that's helpful!
I've assumed you want the result:
{ advert0: [1404915231, 1404920520], advert4: [1404915231] }
That is, I assume you meant advert0 where you wrote advert1; specifically, that you wish to select those pairs "advert x", arr (arr being an array) that correspond to given values of x (here 0 and 4) and convert the collection of those pairs to a hash. If this is not want you want to do, you need read no further.
You could do this as follows:
Code
def doit(arr, *vals_at)
arr.each_slice(2)
.map {|s1,s2|
[s1.split.last.to_i,[s1.tr(' ','').to_sym, s2.scan(/\d+/).map(&:to_i)]]}
.to_h
.values_at(*vals_at)
.to_h
end
Example
arr = ["advert 0", "[1404915231, 1404920520]",
"advert 4", "[1404915231]",
"advert 5", "[1404915231]"
]
(We don't need the rest of arr.)
doit(arr, 0, 4)
#=> {:advert0=>[1404915231, 1404920520], :advert4=>[1404915231]}
Explanation
For the value of arr above, let:
b = a.each_slice(2)
#=> #<Enumerator: ["advert 0", "[1404915231, 1404920520]",
# "advert 4", "[1404915231]",
# "advert 5", "[1404915231]"]:each_slice(2)>
c = b.map
#<Enumerator: #<Enumerator: ["advert 0", "[1404915231, 1404920520]",
# "advert 4", "[1404915231]",
# "advert 5", "[1404915231]"]:each_slice(2)>:map>
c may be thought of as a "compound" enumerator. When we add a block, each will pass each value of the following array into the block:
c.to_a
#=> [["advert 0", "[1404915231, 1404920520]"],
# ["advert 4", "[1404915231]"],
# ["advert 5", "[1404915231]"]]
Note
b.to_a == c.to_a #=> true
Next,
d = c.each { |s1,s2| [s1.split.last.to_i,
[s1.tr(' ','').to_sym, s2.scan(/\d+/).map(&:to_i)]] }
#=> [[0, [:advert0, [1404915231, 1404920520]]],
# [4, [:advert4, [1404915231]]],
# [5, [:advert5, [1404915231]]]]
Note that c.each is equivalent to b.map:
d == b.map { |s1,s2| [s1.split.last.to_i,
[s1.tr(' ','').to_sym, s2.scan(/\d+/).map(&:to_i)]] }
#=> true
Let's pause a moment to see what's going on here. The first value the enumerator c passes into the block (["advert 0", "[1404915231, 1404920520]"]) is split into the two block variables by disambiguation:
s1 #=> "advert 0"
s2 #=> "[1404915231, 1404920520]"
Then:
s1.split.last.to_i
#=> 0
r = s2.scan(/\d+/)
#=> ["1404915231", "1404920520"]
r.map(&:to_i)
#=> [1404915231, 1404920520]
Ergo, the first element passed into the block:
["advert 0", "[1404915231, 1404920520]"]
is mapped to:
[0,[1404915231, 1404920520]]
Similarly, the second and the third elements of c are respectively mapped to:
[4=>[:advert4, [1404915231]]
and
[5=>[:advert5, [1404915231]]
Now that we have d,
e = d.to_h # or Hash[d] for Ruby versions < 2.0
#=> {0=>[:advert0, [1404915231, 1404920520]],
# 4=>[:advert4, [1404915231]],
# 5=>[:advert5, [1404915231]]}
Aha! Now you can see why I constructed e as I did:
f = e.values_at(0,4)
#=> [[:advert0, [1404915231, 1404920520]], [:advert4, [1404915231]]]
f.to_h
#=> {:advert0=>[1404915231, 1404920520], :advert4=>[1404915231]}
If I have a string array that looks like this:
array = ["STRING1", "STRING05", "STRING20", "STRING4", "STRING3"]
or
array = ["STRING: 1", "STRING: 05", "STRING: 20", "STRING: 4", "STRING: 3"]
How can I sort the array by the number in each string (descending)?
I know that If the array consisted of integers and not strings, I could use:
sort_by { |k, v| -k }
I've searched all around but can't come up with a solution
The below would sort by the number in each string and not the string itself
array.sort_by { |x| x[/\d+/].to_i }
=> ["STRING: 1", "STRING: 2", "STRING: 3", "STRING: 4", "STRING: 5"]
descending order:
array.sort_by { |x| -(x[/\d+/].to_i) }
=> ["STRING: 5", "STRING: 4", "STRING: 3", "STRING: 2", "STRING: 1"]
sort the array by the number in each string (descending)
array.sort_by { |x| -x[/\d+/].to_i }
I want to take a array and make it an order list. Currently I'm trying to do it in this way:
r = ["a", "b","c"]
r.each_with_index { |w, index| puts "#{index+1}. #{w}" }.map.to_a
# 1. a
# 2. b
# 3. c
#=> ["a", "b", "c"]
the output should be ["1. a", "2. b", "3. c"].
How do I get the proper output to be the new value for the r array?
a.to_enum.with_index(1).map { |element, index| "#{index}. #{element}" }
or
a.map.with_index(1) { |element, index| "#{index}. #{element}" }
with_index(1) makes the index of the first element 1.
In the first solution the array is converted to an enum, and in the second solution the array is directly mapped.
> => r.each_with_index.map { |w, index| "#{index+1}. #{w}" }
> => ["1. a", "2. b", "3. c"]
You need to map first, then puts:
r = %w[a b c]
r.map.with_index do |w, index|
"#{index + 1}. #{w}"
end.each do |str|
puts str
end
#=> ["1. a", "2. b", "3. c"]
# prints:
# 1. a
# 2. b
# 3. c
This is because each (and each_with_index) simply returns the original array.
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]}]