1

I have a hash which contains values such as....

    EXAMPLE = {
      "101" => "SO01",
      "102" => "SO02",
      "103" => "SO03",
      "105" => %w(S005 SO04)
    }

I want to search the hash for say SO04 and get back "105", or "SO03" and get back "103". I'm sure there is a good way to do this but it's slipping my mind currently. I had been simply using EXAMPLE.key() but that's not going to cut it now that I realize there are arrays in there.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
thatdude99
  • 27
  • 5

2 Answers2

2

Try this:

>> EXAMPLE = {"101" => "SO01",
              "102" => "SO02",
              "103" => "SO03",
              "105" => %w(SO05 SO04)}
>> EXAMPLE.find { |_,v| Array(v).include?('SO02') }.try(:first)
=> "102"
>> EXAMPLE.find { |_,v| Array(v).include?('SO05') }.try(:first)
=> "105"
>> EXAMPLE.find { |_,v| Array(v).include?('SO06') }.try(:first)
=> nil

.first is needed on the end because #find returns an array of [key, value].

If you are using Ruby 2.3 or later, you can use the safe navigation operator in place of try:

>> EXAMPLE.find { |_,v| Array(v).include?('SO05') }&.first
=> "105"
>> EXAMPLE.find { |_,v| Array(v).include?('SO06') }&.first
=> nil
moveson
  • 5,103
  • 1
  • 15
  • 32
  • awesome. thanks a bunch. I knew I was forgetting something simple. I'll accept once the 5 minute wait is up. – thatdude99 Dec 22 '16 at 19:26
  • Out of curiosity why did you choose to place the `hash_values` in an array first then `flatten` before using the `include?` method? It seems like you could just as well write `v.include?('SO05')` since both arrays and strings have the `include?` method – Adetoyese Kola-Balogun Dec 22 '16 at 20:42
  • String#include? would return an item if the search term is a substring of any value. For example: `EXAMPLE.find { |_, v| v.include?('SO') }.first` would return `'101'`. The solution I suggested is more consistent with the #key method, which the OP was using before the array complication was introduced. – moveson Dec 22 '16 at 21:37
  • I see the logic behind your approach now. Thanks for clearing that up. Another approach could involve just using `Array(v).include?(string)` as that way when `v` is an array it just returns that array so there won't be any array of arrays that needs to be flattened – Adetoyese Kola-Balogun Dec 22 '16 at 22:06
  • Yes, that is a slightly better solution. I'll edit and make the change. – moveson Dec 22 '16 at 22:22
  • Getting error usign `try`: > `NoMethodError: undefined method `try' for ["102", "SO02"]:Array` – leompeters Jul 11 '17 at 16:02
  • Use the safe navigation operator (`&`) instead. See edited answer. – moveson Jul 12 '17 at 17:04
1
EXAMPLE = {
  "101" => "SO01",
  "102" => "SO02",
  "103" => "SO03",
  "105" => ["S005", "SO04"]
}

def value_to_key(h, value)
  h.keys.find { |k| Array(h[k]).include?(value) }
end

value_to_key(EXAMPLE, "SO02") #=> "102" 
value_to_key(EXAMPLE, "SO04") #=> "105" 
value_to_key(EXAMPLE, "cat")  #=> nil 

Note:

Array("SO03")           #=> ["SO03"] 
Array(["S005", "SO04"]) #=> ["S005", "SO04"] 

An equivalent, but more memory-demanding, method is shown below. The reason for the difference in memory requirements is explained in the comments.

def value_to_key(h, value)
  h.keys.find { |k| [*h[k]].include?(value) }
end

[*"SO03"]           #=> ["SO03"] 
[*["S005", "SO04"]] #=> ["S005", "SO04"] 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Why not `Array(h[k])`? It wouldn't create a new array from an existing one. – Eric Duminil Dec 22 '16 at 22:27
  • @Eric, please explain your second sentence. Readers: Eric is suggesting `Array(h[k])` in place of `[*h[k]]`, as `Array("SO03") #=> ["SO03"]` and `Array(["S005", "SO04"]) #=> ["S005", "SO04"] `. – Cary Swoveland Dec 22 '16 at 22:36
  • It's my understanding that `Array(a)` returns `a` directly. `[*a]` first flattens a, and packs it again in a new array. – Eric Duminil Dec 22 '16 at 22:38
  • 1
    @Eric, so it does: `a = [1,2]; a.object_id #=> 70110799250340; Array(a).object_id #=> 70110799250340`. I'll do the edit. Thanks. – Cary Swoveland Dec 22 '16 at 22:41