Find key of value within array in hash - ruby

I have a hash categories as following:
categories = {"horeca" => ["bar", "waiter", "kitchen"],
"retail" => ["eerste", "tweede"]}
I want to find they key if the value is included in the array of values.
Something like following
categories.key("bar")
which would return "horeca"
as of now I can only get "horeca" if I do
categories.key(["bar", "waiter", "kitchen"])

Try Enumberable#find:
categories.find { |key, values|
values.include?("bar")
}.first

As Máté mentioned, you can use find if you want to find the first matching element. Use select if you want all matching elements. To just get the keys you would do:
categories.select { |key, values| values.include?("bar") }.map(&:first)
See https://ruby-doc.org/core-2.2.3/Enumerable.html#method-i-select

Creating intermediate array and then calling first on it is all unnecessary, also if the hash is large and you want first matched value, following solution is better
categories.each{ |k,v| break k if v.include?('bar') }
#=> "horeca"

Md. Farhan Memon's solution is generally the preferable solution but it has one downside: If there's no match in the collection, it returns the collection itself – which probably isn't a desirable result. You can fix this with a simple adjustment that combines both detect/find and break:
categories.detect { |key, values| break key if values.include?('bar') }
This breaks and returns the value if it finds it and otherwise returns nil (which I assume to be the preferable behavior).
If your collection may also contain nil values and/or non-arrays, you can improve it further:
categories.detect { |key, values| break key if Array(values).include?('bar') }
The only downside of this general approach is that it's not particularly intuitive to newcomers: You have to know a bit more than just basic Ruby to understand what's going on without running the code first.

Related

How would I remove multiple nested values from a hash

I've got a really confusing question that I can't seem to figure out.
Say I have this hash
hash = {
"lock_version"=>1,
"exhibition_quality"=>false,
"within"=>["FID6", "S5"],
"representation_file"=>{
"lock_version"=>0,
"created_by"=>"admin",
"within"=>["FID6", "S5"]
}
}
How can I delete from "within"=>["FID6", "S5"] a value with the pattern FID<Number> (in this example FID6)?
I've thought about it a bit and used the .delete, .reject! the within but I realised this was deleting the whole key value pair which is not what I want. Thanks a lot for any help that can put me on the right path.
You could use a method or proc/lambda to achieve the result. This solutions splits up the logic in two parts. Removing the actual FID<Number> string from the array and recursively calling the former on the correct key.
remove_fid = ->(array) { array.grep_v(/\AFID\d+\z/) }
remove_nested_fid = lambda do |hash|
hash.merge(
hash.slice('within').transform_values(&remove_fid),
hash.slice('representation_file').transform_values(&remove_nested_fid)
)
end
pp hash.then(&remove_nested_fid) # or remove_nested_fid.call(hash)
# {"lock_version"=>1,
# "exhibition_quality"=>false,
# "within"=>["S5"],
# "representation_file"=>
# {"lock_version"=>0, "created_by"=>"admin", "within"=>["S5"]}}
grep_v removes all strings from the array that do not match the given regex.
slice creates a new hash only containing the given keys. If a key is missing it will not be present in the resulting hash.
transform_values transforms the values of a hash into a new value (similar to map for Array), returning a hash.
merge creates a new hash, merging the hashes together.
This solution does not mutate the original hash structure.
You're going to need to recurse over the Hash and, in cases where the value is a Hash, process it using the same function, repeatedly. This can be problematic in Ruby -- which doesn't handle recursion very well -- if the depth of the tree is too deep, but it's the most natural way to express this type of issue.
def filter_fids(h)
h.each_pair do |k, v|
if v.is_a? Hash
filter_fids v
elsif k == "within"
v.reject! { |x| x.start_with? "FID" }
end
end
end
This will mutate the original data structure in place.

How to get the index of a key in a hash?

I'm trying to get the index of a key in a hash.
I know how to do this in an array:
arr = ['Done', 13, 0.4, true]
a = arr.index('Done')
puts a
Is there a method or some sort of way to do this something like this with a key in a hash? Thanks!
Hashes aren't usually treated as ordered structures, they simply have a list of keys and values corresponding to those keys.
It's true that in Ruby hashes are technically ordered, but there's very rarely an actual use case for treating them as such.
If what you want to do is find the key corresponding to a value in a hash, you can simply use the Hash#key method:
hash = { a: 1, b: 2 }
hash.key(1) # => :a
I suppose you could use hash.keys.index(hash.key(1)) to get 0 since it's the first value, but again, I wouldn't advise doing this because it's not typical use of the data structure
There are at least a couple ways you can get this information, the 2 that come to mind are Enumerable's find_index method to pass each element to a block and check for your key:
hash.find_index { |key, _| key == 'Done' }
or you could get all the keys from your hash as an array and then look up the index as you've been doing:
hash.keys.index('Done')

Both for & each loops stop after first match when iterating through a hash in ruby

I'm trying to print out the values of a hash where their corresponding keys match a certain condition (if they're odd).
I'm having an issue that's causing my loop to stop working after
The following code returns only the first matching value.
if mycondition
for key,value in myhash do
if key.odd?
return "#{value}"
end
end
end
After reading some other questions, I thought return was my issue, but when I take it out, the code returns the entire hash instead of just one matching value.
Anyone know what might be up?
This is a much more Ruby way of doing it:
myhash.select do |key, value|
key.odd?
end.values
Here it finds all the odd keys and returns the associated values.
The problem with your return is that forces the whole method to exit right then and there. If you're used to JavaScript that might be a little confusing, as you're not just exiting the do block.
As a note the for construct is hardly ever used in Ruby. A more conventional way of expressing your original code is:
myhash.each do |key, value|
# ...
end
Once you return, the function is over, so that's why you get only one key.
If you don't return anything, the function returns its final expression, which in this case is the for loop. for loops evaluate to the thing you looped over (myhash), so that's why removing the return gives you everything.
In your case you need to build up the list of things you want to return, and that return that whole list:
if mycondition
ret = []
for key, value in myhash do
if key.odd?
ret << [key, value]
end
end
return Hash[ret]
end
But Ruby has much nicer ways to pull out a subset of a hash, for example:
Hash[myhash.select{|k,v| k.odd?}]

Ruby Hash destructive vs. non-destructive method

Could not find a previous post that answers my question...I'm learning how to use destructive vs. non-destructive methods in Ruby. I found an answer to the exercise I'm working on (destructively adding a number to hash values), but I want to be clear on why some earlier solutions of mine did not work. Here's the answer that works:
def modify_a_hash(the_hash, number_to_add_to_each_value)
the_hash.each { |k, v| the_hash[k] = v + number_to_add_to_each_value}
end
These two solutions come back as non-destructive (since they all use "each" I cannot figure out why. To make something destructive is it the equals sign above that does the trick?):
def modify_a_hash(the_hash, number_to_add_to_each_value)
the_hash.each_value { |v| v + number_to_add_to_each_value}
end
def modify_a_hash(the_hash, number_to_add_to_each_value)
the_hash.each { |k, v| v + number_to_add_to_each_value}
end
The terms "destructive" and "non-destructive" are a bit misleading here. Better is to use the conventional "in-place modification" vs. "returns a copy" terminology.
Generally methods that modify in-place have ! at the end of their name to serve as a warning, like gsub! for String. Some methods that pre-date this convention do not have them, like push for Array.
The = performs an assignment within the loop. Your other examples don't actually do anything useful since each returns the original object being iterated over regardless of any results produced.
If you wanted to return a copy you'd do this:
def modify_a_hash(the_hash, number_to_add)
Hash[
the_hash.collect do |k, v|
[ k, v + number_to_add ]
end
]
end
That would return a copy. The inner operation collect transforms key-value pairs into new key-value pairs with the adjustment applied. No = is required since there's no assignment.
The outer method Hash[] transforms those key-value pairs into a proper Hash object. This is then returned and is independent of the original.
Generally a non-destructive or "return a copy" method needs to create a new, independent version of the thing it's manipulating for the purpose of storing the results. This applies to String, Array, Hash, or any other class or container you might be working with.
Maybe this slightly different example will be helpful.
We have a hash:
2.0.0-p481 :014 > hash
=> {1=>"ann", 2=>"mary", 3=>"silvia"}
Then we iterate over it and change all the letters to the uppercase:
2.0.0-p481 :015 > hash.each { |key, value| value.upcase! }
=> {1=>"ANN", 2=>"MARY", 3=>"SILVIA"}
The original hash has changed because we used upcase! method.
Compare to method without ! sign, that doesn't modify hash values:
2.0.0-p481 :017 > hash.each { |key, value| value.downcase }
=> {1=>"ANN", 2=>"MARY", 3=>"SILVIA"}

Ruby regex selecting multiple words at the same time

I have a hash that I am using regex on to select what key/value pairs I want. Here is the method I have written:
def extract_gender_race_totals(gender, race)
totals = #data.select {|k,v| k.to_s.match(/(#{gender})(#{race})/)}
temp = 0
totals.each {|key, value| temp += value}
temp
end
the hash looks like this:
#data = {
:number_of_african_male_senior_managers=>2,
:number_of_coloured_male_senior_managers=>0,
:number_of_indian_male_senior_managers=>0,
:number_of_white_male_senior_managers=>0,
:number_of_african_female_senior_managers=>0,
:number_of_coloured_female_senior_managers=>0,
:number_of_indian_female_senior_managers=>0,
:number_of_white_female_senior_managers=>0,
:number_of_african_male_middle_managers=>2,
:number_of_coloured_male_middle_managers=>0,
:number_of_indian_male_middle_managers=>0,
:number_of_white_male_middle_managers=>0,
:number_of_african_female_middle_managers=>0,
:number_of_coloured_female_middle_managers=>0,
:number_of_indian_female_middle_managers=>0,
:number_of_white_female_middle_managers=>0,
:number_of_african_male_junior_managers=>0,
:number_of_coloured_male_junior_managers=>0,
:number_of_indian_male_junior_managers=>0,
:number_of_white_male_junior_managers=>0,
:number_of_african_female_junior_managers=>0,
:number_of_coloured_female_junior_managers=>0,
:number_of_indian_female_junior_managers=>0,
:number_of_white_female_junior_managers=>0
}
but it's re-populated with data after a SQL Query.
I would like to make it so that the key must contain both the race and the gender in order for it to return something. Otherwise it must return 0. Is this right or is the regex syntax off?
It's returning 0 for all, which it shouldn't.
So the example would be
%td.total_cell= #ee_demographics_presenter.extract_gender_race_totals("male","african")
This would return 4, there are 4 African, male managers.
Try something like this.
def extract_gender_race_totals(gender, race)
#data.select{|k, v| k.to_s.match(/#{race}_#{gender}/)}.values.reduce(:+)
end
extract_gender_race_totals("male", "african")
# => 4
gmalete's answer gives an elegant solution, but here is just an explanation of why your regexp isn't quite right. If you corrected the regexp I think your approach would work, it just isn't as idiomatic Ruby.
/(#{gender})(#{race})/ won't match number_of_african_male_senior_managers for 2 reasons:
1) the race comes before the gender in the hash key and 2) there is an underscore in the hash key that needs to be in the regexp. e.g.
/(#{race})_(#{gender})/
would work, but the parentheses aren't needed so this can be simplified to
/#{race}_#{gender}/
Rather than having specific methods to query pieces of your keys (i.e. "gender_race"), you could make a general method to query any attribute in any order:
def extract_totals(*keywords)
keywords.inject(#data) { |memo, keyword| memo.select { |k, v| k.to_s =~ /_#{keyword}(?:_|\b)/ } }.values.reduce(:+)
end
Usage:
extract_totals("senior")
extract_totals("male", "african")
extract_totals("managers") # maybe you'll have _employees later...
# etc.
Not exactly what you asked for, but maybe it will help.

Resources