Ruby: Properties of a hash key - ruby

I will just paste down a simple example i tried so that it would be clear to those who read this.
irb(main):001:0> h = { }
=> {}
irb(main):002:0> a=[1,2,3]
=> [1, 2, 3]
irb(main):003:0> a.object_id
=> 69922343540500
irb(main):004:0> h[a] = 12 #Hash with the array as a key
=> 12
irb(main):005:0> a << 4 #Modified the array
=> [1, 2, 3, 4]
irb(main):006:0> a.object_id #Object id obviously remains the same.
=> 69922343540500
irb(main):007:0> h[a] #Hash with the same object_id now returns nil.
=> nil
irb(main):008:0> h #Modified hash
=> {[1, 2, 3, 4]=>12}
irb(main):009:0> h[[1,2,3,4]] #Tried to access the value with the modified key -
=> nil
irb(main):011:0> h.each { |key,value| puts "#{key.inspect} maps #{value}" }
[1, 2, 3, 4] maps 12
=> {[1, 2, 3, 4]=>12}
Now when i iterate over the hash, its possible to identify the map between the key and the value.
Could some one please explain me this behaviour of ruby hash and what are the properties of hash keys.
1) As i mentioned above, the object_id hasn't changed - then why is the value set to nil.
2) Is there any possible way so that i can get back the value '12' from the hash 'h' because h[[1,2,3,4]] as mentioned above returns nil.

This happens because the key should not have its value changed while it is in use. If the value changes, we should rebuild the hash, based on its current value. Look at Ruby API for rehash method. You can get back the value, by rebuilding the hash again after the key is changed, like this:
irb(main):022:0> h.rehash
=> {[1, 2, 3, 4]=>12}
irb(main):023:0> h[a]
=> 12

Hash keys are checked using the #eql? method, and since [1, 2, 3] isn't .eql? to [1, 2, 3,4] your hash lookup has a different result.
Maybe you want to be using something other than an Array as your Hash key if the semantics aren't working for you?

The ruby hash api provides the answer: the key should not have its value changed while it is in use as a key.
I guess intern a hash is calculated for a and used for quick lookup (because a key shouldn't change, the hash is always the same). So when you do h[a] it doesn't find a match ([1,2,3].hash != [1,2,3,4].hash) and when you do h[[1,2,3]] the hashes match but the object doesn't match ([1,2,3] != [1,2,3,4]).
A fix is to use the object_id as key because it doesn't change, h[a.object_id] = 12 will return 12 when a changes. Ofcourse this has the downside that h[[1,2,3].object_id] won't return 12.

Stefaan Colman's answer is more thorough, but a couple of observations:
Ruby uses the Object#hash method to hash objects.
You can get 12 back out like by doing a.delete(4); h[a] at that point, [1,2,3] is also able to be used as a key again.

Related

Ruby Hash initialised with each_with_object behaving weirdly

Initializing Ruby Hash like:
keys = [0, 1, 2]
hash = Hash[keys.each_with_object([]).to_a]
is behaving weirdly when trying to insert a value into a key.
hash[0].push('a')
# will result into following hash:
=> {0=>["a"], 1=>["a"], 2=>["a"]}
I am just trying to insert into one key, but it's updating the value of all keys.
Yes, that each_with_object is super-weird in itself. That's not how it should be used. And the problem arises precisely because you mis-use it.
keys.each_with_object([]).to_a
# => [[0, []], [1, []], [2, []]]
You see, even though it looks like these arrays are separate, it's actually the same array in all three cases. That's why if you push an element into one, it appears in all others.
Here's a better way:
h = keys.each_with_object({}) {|key, h| h[key] = []}
# => {0=>[], 1=>[], 2=>[]}
Or, say
h = keys.zip(Array.new(keys.size) { [] }).to_h
Or a number of other ways.
If you don't care about hash having this exact set of keys and simply want all keys to have empty array as default value, that's possible too.
h = Hash.new { |hash, key| hash[key] = [] }
All your keys reference the same array.
A simplified version that explains the problem:
a = []
b = a
a.push('something')
puts a #=> ['something']
puts b #=> ['something']
Even though you have two variables (a and b) there is only one Array Object. So any changes to the array referenced by variable a will change the array referenced by variable b as well. Because it is the same object.
The long version of what you are trying to achieve would be:
keys = [1, 2, 3]
hash = {}
keys.each do |key|
hash[key] = []
end
And a shorter version:
[1, 2 ,3].each_with_object({}) do |key, accu|
accu[key] = []
end

Hash with array as key

I'm defining a hash with an array as a key and another array as its value. For example:
for_example = {[0,1] => [:a, :b, :c]}
Everything is as expected below.
my_hash = Hash.new([])
an_array_as_key = [4,2]
my_hash[an_array_as_key] #=> []
my_hash[an_array_as_key] << "the" #=> ["the"]
my_hash[an_array_as_key] << "universal" #=> ["the", "universal"]
my_hash[an_array_as_key] << "answer" #=> ["the", "universal", "answer"]
But if I try to access the keys:
my_hash #=> {}
my_hash.keys #=> []
my_hash.count #=> 0
my_hash.values #=> []
my_hash.fetch(an_array_as_key) # KeyError: key not found: [4, 2]
my_hash.has_key?(an_array_as_key) #=> false
Rehash doesn't help:
my_hash #=> {}
my_hash.rehash #=> {}
my_hash.keys #=> []
But the values are saved:
my_hash[an_array_as_key] #=> ["the", "universal", "answer"]
Am I missing something?
To understand this, You need to understand the difference between Hash::new and Hash::new(ob). Suppose you define a hash object using Hash::new or hash literal {}. Now whenever you will write a code hsh[any_key], there is two kind of output may be seen, if any_key don't exist, then default value nil will be returned,otherwise whatever value is associated with the key will be returned. The same explanation will be applicable if you create any Hash object using Hash.new.
Now Hash.new(ob) is same as Hash.new, with one difference is, you can set any default value you want, for non existent keys of that hash object.
my_hash = Hash.new([])
my_hash[2] # => []
my_hash[2].object_id # => 83664630
my_hash[4] # => []
my_hash[4].object_id # => 83664630
my_hash[3] << 4 # => [4]
my_hash[3] # => [4]
my_hash[3].object_id # => 83664630
my_hash[5] << 8 # => [4, 8]
my_hash[5] # => [4, 8]
my_hash[5].object_id # => 83664630
Now see in the above example my_hash has no keys like 2,3 and 4. But the object_id proved that, all key access results in to return the same array object. my_hash[2] is not adding the key to the hash my_hash, rather trying to access the value of the key 2 if that key exist, otherwise it is returning the default value of my_hash. Remember all lines like my_hash[2],my_hash[3] etc is nothing but a call to Hash#[] method.
But there is a third way to go, may be you are looking for, which is Hash::new {|hash, key| block }.With this style you can add key to the hash object if that key doesn't exist, with a default value of same class instance,but not the same instance., while you are doing actually Hash#[] method call.
my_hash = Hash.new { |hash, key| hash[key] = []}
my_hash[2] # => []
my_hash[2].object_id # => 76312700
my_hash[3] # => []
my_hash[3].object_id # => 76312060
my_hash.keys # => [2, 3]

Hash use array as key in ruby

I have a hash that uses array as its key. When I change the array, the hash can no longer get the corresponding key and value:
1.9.3p194 :016 > a = [1, 2]
=> [1, 2]
1.9.3p194 :017 > b = { a => 1 }
=> {[1, 2]=>1}
1.9.3p194 :018 > b[a]
=> 1
1.9.3p194 :019 > a.delete_at(1)
=> 2
1.9.3p194 :020 > a
=> [1]
1.9.3p194 :021 > b
=> {[1]=>1}
1.9.3p194 :022 > b[a]
=> nil
1.9.3p194 :023 > b.keys.include? a
=> true
What am I doing wrong?
Update:
OK. Use a.clone is absolutely one way to deal with this problem.
What if I want to change "a" but still use "a" to retrieve the corresponding value (since "a" is still one of the keys) ?
The #rehash method will recalculate the hash, so after the key changes do:
b.rehash
TL;DR: consider Hash#compare_by_indentity
You need to decide if you want the hash to work by array value or array identity.
By default arrays .hash and .eql? by value, which is why changing the value confuses ruby. Consider this variant of your example:
pry(main)> a = [1, 2]
pry(main)> a1 = [1]
pry(main)> a.hash
=> 4266217476190334055
pry(main)> a1.hash
=> -2618378812721208248
pry(main)> h = {a => '12', a1 => '1'}
=> {[1, 2]=>"12", [1]=>"1"}
pry(main)> h[a]
=> "12"
pry(main)> a.delete_at(1)
pry(main)> a
=> [1]
pry(main)> a == a1
=> true
pry(main)> a.hash
=> -2618378812721208248
pry(main)> h[a]
=> "1"
See what happened there?
As you discovered, it fails to match on the a key because the .hash value under which it stored it is outdated [BTW, you can't even rely on that! A mutation might result in same hash (rare) or different hash that lands in the same bucket (not so rare).]
But instead of failing by returning nil, it matched on the a1 key.
See, h[a] doesn't care at all about the identity of a vs a1 (the traitor!). It compared the current value you supply — [1] with the value of a1 being [1] and found a match.
That's why using .rehash is just band-aid. It will recompute the .hash values for all keys and move them to the correct buckets, but it's error-prone, and may also cause trouble:
pry(main)> h.rehash
=> {[1]=>"1"}
pry(main)> h
=> {[1]=>"1"}
Oh oh. The two entries collapsed into one, since they now have the same value (and which wins is hard to predict).
Solutions
One sane approach is embracing lookup by value, which requires the value to never change. .freeze your keys. Or use .clone/.dup when building the hash, and feel free to mutate the original arrays — but accept that h[a] will lookup the current value of a against the values preserved from build time.
The other, which you seem to want, is deciding you care about identity — lookup by a should find a whatever its current value, and it shouldn't matter if many keys had or now have the same value.
How?
Object hashes by identity. (Arrays don't because types that .== by value tend to also override .hash and .eql? to be by value.) So one option is: don't use arrays as keys, use some custom class (which may hold an array inside).
But what if you want it to behave directly like a hash of arrays? You could subclass Hash, or Array but it's a lot of work to make everything work consistently. Luckily, Ruby has a builtin way: h.compare_by_identity switches a hash to work by identity (with no way to undo, AFAICT). If you do this before you insert anything, you can even have distinct keys with equal values, with no confusion:
[39] pry(main)> x = [1]
=> [1]
[40] pry(main)> y = [1]
=> [1]
[41] pry(main)> h = Hash.new.compare_by_identity
=> {}
[42] pry(main)> h[x] = 'x'
=> "x"
[44] pry(main)> h[y] = 'y'
=> "y"
[45] pry(main)> h
=> {[1]=>"x", [1]=>"y"}
[46] pry(main)> x.push(7)
=> [1, 7]
[47] pry(main)> y.push(7)
=> [1, 7]
[48] pry(main)> h
=> {[1, 7]=>"x", [1, 7]=>"y"}
[49] pry(main)> h[x]
=> "x"
[50] pry(main)> h[y]
=> "y"
Beware that such hashes are counter-intuitive if you try to put there e.g. strings, because we're really used to strings hashing by value.
Hashes use their key objects' hash codes (a.hash) to group them. Hash codes often depend on the state of the object; in this case, the hash code of a changes when an element has been removed from the array. Since the key has already been inserted into the hash, a is filed under its original hash code.
This means you can't retrieve the value for a in b, even though it looks alright when you print the hash.
You should use a.clone as key
irb --> a = [1, 2]
==> [1, 2]
irb --> b = { a.clone => 1 }
==> {[1, 2]=>1}
irb --> b[a]
==> 1
irb --> a.delete_at(1)
==> 2
irb --> a
==> [1]
irb --> b
==> {[1, 2]=>1} # STILL UNCHANGED
irb --> b[a]
==> nil # Trivial, since a has changed
irb --> b.keys.include? a
==> false # Trivial, since a has changed
Using a.clone will make sure that the key is unchanged even when we change a later on.
As you have already said, the trouble is that the hash key is the exact same object you later modify, meaning that the key changes during program execution.
To avoid this, make a copy of the array to use as a hash key:
a = [1, 2]
b = { a.clone => 1 }
Now you can continue to work with a and leave your hash keys intact.

Deleting a modified object from a set in a no-op?

See the example below
require "set"
s = [[1, 2], [3, 4]].to_set # s = {[1, 2], [3, 4]}
m = s.max_by {|a| a[0]} # m = [3, 4]
m[0] = 9 # m = [9, 4], s = {[1, 2], [9, 4]}
s.delete(m) # s = {[1, 2], [9, 4]} ?????
This behaves differently from an array. (If we remove .to_set, we will get s = [[1, 2]] which is expected.) Is this a bug?
Yes, this is a bug or at least I'd call it a bug. Some would call this "an implementation detail accidentally leaking to the outside world" but that's just fancy pants city-boy talk for bug.
The problem has two main causes:
You're modifying elements of the Set without Set knowing about it.
The standard Ruby Set is implemented as a Hash.
The result is that you're modifying the internal Hash's keys without the Hash knowing about it and that confuses the poor Hash into not really knowing what keys it has anymore. The Hash class has a rehash method:
rehash → hsh
Rebuilds the hash based on the current hash values for each key. If values of key objects have changed since they were inserted, this method will reindex hsh.
a = [ "a", "b" ]
c = [ "c", "d" ]
h = { a => 100, c => 300 }
h[a] #=> 100
a[0] = "z"
h[a] #=> nil
h.rehash #=> {["z", "b"]=>100, ["c", "d"]=>300}
h[a] #=> 100
Notice the interesting behavior in the example included with the rehash documentation. Hashes keep track of things using the k.hash values for the key k. If you have an array as a key and you change the array, you can change the array's hash value as well; the result is that the Hash still has that array as a key but the Hash won't be able to find that array as a key because it will be looking in the bucket for the new hash value but the array will be in the bucket for the old hash value. But, if you rehash the Hash, it will all of a sudden be able to find all of its keys again and the senility goes away. Similar problems will occur with non-array keys: you just have to change the key in such a way that its hash value changes and the Hash containing that key will get confused and wander around lost until you rehash it.
The Set class uses a Hash internally to store its members and the members are used as the hash's keys. So, if you change a member, the Set will get confused. If Set had a rehash method then you could kludge around the problem by slapping the Set upside the head with rehash to knock some sense into it; alas, there is no such method in Set. However, you can monkey patch your own in:
class Set
def rehash
#hash.rehash
end
end
Then you can change the keys, call rehash on the Set, and your delete (and various other methods such as member?) will work properly.

What is the best way to convert an array to a hash in Ruby

In Ruby, given an array in one of the following forms...
[apple, 1, banana, 2]
[[apple, 1], [banana, 2]]
...what is the best way to convert this into a hash in the form of...
{apple => 1, banana => 2}
Simply use Hash[*array_variable.flatten]
For example:
a1 = ['apple', 1, 'banana', 2]
h1 = Hash[*a1.flatten(1)]
puts "h1: #{h1.inspect}"
a2 = [['apple', 1], ['banana', 2]]
h2 = Hash[*a2.flatten(1)]
puts "h2: #{h2.inspect}"
Using Array#flatten(1) limits the recursion so Array keys and values work as expected.
NOTE: For a concise and efficient solution, please see Marc-André Lafortune's answer below.
This answer was originally offered as an alternative to approaches using flatten, which were the most highly upvoted at the time of writing. I should have clarified that I didn't intend to present this example as a best practice or an efficient approach. Original answer follows.
Warning! Solutions using flatten will not preserve Array keys or values!
Building on #John Topley's popular answer, let's try:
a3 = [ ['apple', 1], ['banana', 2], [['orange','seedless'], 3] ]
h3 = Hash[*a3.flatten]
This throws an error:
ArgumentError: odd number of arguments for Hash
from (irb):10:in `[]'
from (irb):10
The constructor was expecting an Array of even length (e.g. ['k1','v1,'k2','v2']). What's worse is that a different Array which flattened to an even length would just silently give us a Hash with incorrect values.
If you want to use Array keys or values, you can use map:
h3 = Hash[a3.map {|key, value| [key, value]}]
puts "h3: #{h3.inspect}"
This preserves the Array key:
h3: {["orange", "seedless"]=>3, "apple"=>1, "banana"=>2}
The best way is to use Array#to_h:
[ [:apple,1],[:banana,2] ].to_h #=> {apple: 1, banana: 2}
Note that to_h also accepts a block:
[:apple, :banana].to_h { |fruit| [fruit, "I like #{fruit}s"] }
# => {apple: "I like apples", banana: "I like bananas"}
Note: to_h accepts a block in Ruby 2.6.0+; for early rubies you can use my backports gem and require 'backports/2.6.0/enumerable/to_h'
to_h without a block was introduced in Ruby 2.1.0.
Before Ruby 2.1, one could use the less legible Hash[]:
array = [ [:apple,1],[:banana,2] ]
Hash[ array ] #= > {:apple => 1, :banana => 2}
Finally, be wary of any solutions using flatten, this could create problems with values that are arrays themselves.
Update
Ruby 2.1.0 is released today. And I comes with Array#to_h (release notes and ruby-doc), which solves the issue of converting an Array to a Hash.
Ruby docs example:
[[:foo, :bar], [1, 2]].to_h # => {:foo => :bar, 1 => 2}
Edit: Saw the responses posted while I was writing, Hash[a.flatten] seems the way to go.
Must have missed that bit in the documentation when I was thinking through the response. Thought the solutions that I've written can be used as alternatives if required.
The second form is simpler:
a = [[:apple, 1], [:banana, 2]]
h = a.inject({}) { |r, i| r[i.first] = i.last; r }
a = array, h = hash, r = return-value hash (the one we accumulate in), i = item in the array
The neatest way that I can think of doing the first form is something like this:
a = [:apple, 1, :banana, 2]
h = {}
a.each_slice(2) { |i| h[i.first] = i.last }
You can also simply convert a 2D array into hash using:
1.9.3p362 :005 > a= [[1,2],[3,4]]
=> [[1, 2], [3, 4]]
1.9.3p362 :006 > h = Hash[a]
=> {1=>2, 3=>4}
Summary & TL;DR:
This answer hopes to be a comprehensive wrap-up of information from other answers.
The very short version, given the data from the question plus a couple extras:
flat_array = [ apple, 1, banana, 2 ] # count=4
nested_array = [ [apple, 1], [banana, 2] ] # count=2 of count=2 k,v arrays
incomplete_f = [ apple, 1, banana ] # count=3 - missing last value
incomplete_n = [ [apple, 1], [banana ] ] # count=2 of either k or k,v arrays
# there's one option for flat_array:
h1 = Hash[*flat_array] # => {apple=>1, banana=>2}
# two options for nested_array:
h2a = nested_array.to_h # since ruby 2.1.0 => {apple=>1, banana=>2}
h2b = Hash[nested_array] # => {apple=>1, banana=>2}
# ok if *only* the last value is missing:
h3 = Hash[incomplete_f.each_slice(2).to_a] # => {apple=>1, banana=>nil}
# always ok for k without v in nested array:
h4 = Hash[incomplete_n] # or .to_h => {apple=>1, banana=>nil}
# as one might expect:
h1 == h2a # => true
h1 == h2b # => true
h1 == h3 # => false
h3 == h4 # => true
Discussion and details follow.
Setup: variables
In order to show the data we'll be using up front, I'll create some variables to represent various possibilities for the data. They fit into the following categories:
Based on what was directly in the question, as a1 and a2:
(Note: I presume that apple and banana were meant to represent variables. As others have done, I'll be using strings from here on so that input and results can match.)
a1 = [ 'apple', 1 , 'banana', 2 ] # flat input
a2 = [ ['apple', 1], ['banana', 2] ] # key/value paired input
Multi-value keys and/or values, as a3:
In some other answers, another possibility was presented (which I expand on here) – keys and/or values may be arrays on their own:
a3 = [ [ 'apple', 1 ],
[ 'banana', 2 ],
[ ['orange','seedless'], 3 ],
[ 'pear', [4, 5] ],
]
Unbalanced array, as a4:
For good measure, I thought I'd add one for a case where we might have an incomplete input:
a4 = [ [ 'apple', 1],
[ 'banana', 2],
[ ['orange','seedless'], 3],
[ 'durian' ], # a spiky fruit pricks us: no value!
]
Now, to work:
Starting with an initially-flat array, a1:
Some have suggested using #to_h (which showed up in Ruby 2.1.0, and can be backported to earlier versions). For an initially-flat array, this doesn't work:
a1.to_h # => TypeError: wrong element type String at 0 (expected array)
Using Hash::[] combined with the splat operator does:
Hash[*a1] # => {"apple"=>1, "banana"=>2}
So that's the solution for the simple case represented by a1.
With an array of key/value pair arrays, a2:
With an array of [key,value] type arrays, there are two ways to go.
First, Hash::[] still works (as it did with *a1):
Hash[a2] # => {"apple"=>1, "banana"=>2}
And then also #to_h works now:
a2.to_h # => {"apple"=>1, "banana"=>2}
So, two easy answers for the simple nested array case.
This remains true even with sub-arrays as keys or values, as with a3:
Hash[a3] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "pear"=>[4, 5]}
a3.to_h # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "pear"=>[4, 5]}
But durians have spikes (anomalous structures give problems):
If we've gotten input data that's not balanced, we'll run into problems with #to_h:
a4.to_h # => ArgumentError: wrong array length at 3 (expected 2, was 1)
But Hash::[] still works, just setting nil as the value for durian (and any other array element in a4 that's just a 1-value array):
Hash[a4] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}
Flattening - using new variables a5 and a6
A few other answers mentioned flatten, with or without a 1 argument, so let's create some new variables:
a5 = a4.flatten
# => ["apple", 1, "banana", 2, "orange", "seedless" , 3, "durian"]
a6 = a4.flatten(1)
# => ["apple", 1, "banana", 2, ["orange", "seedless"], 3, "durian"]
I chose to use a4 as the base data because of the balance problem we had, which showed up with a4.to_h. I figure calling flatten might be one approach someone might use to try to solve that, which might look like the following.
flatten without arguments (a5):
Hash[*a5] # => {"apple"=>1, "banana"=>2, "orange"=>"seedless", 3=>"durian"}
# (This is the same as calling `Hash[*a4.flatten]`.)
At a naïve glance, this appears to work – but it got us off on the wrong foot with the seedless oranges, thus also making 3 a key and durian a value.
And this, as with a1, just doesn't work:
a5.to_h # => TypeError: wrong element type String at 0 (expected array)
So a4.flatten isn't useful to us, we'd just want to use Hash[a4]
The flatten(1) case (a6):
But what about only partially flattening? It's worth noting that calling Hash::[] using splat on the partially-flattened array (a6) is not the same as calling Hash[a4]:
Hash[*a6] # => ArgumentError: odd number of arguments for Hash
Pre-flattened array, still nested (alternate way of getting a6):
But what if this was how we'd gotten the array in the first place?
(That is, comparably to a1, it was our input data - just this time some of the data can be arrays or other objects.) We've seen that Hash[*a6] doesn't work, but what if we still wanted to get the behavior where the last element (important! see below) acted as a key for a nil value?
In such a situation, there's still a way to do this, using Enumerable#each_slice to get ourselves back to key/value pairs as elements in the outer array:
a7 = a6.each_slice(2).to_a
# => [["apple", 1], ["banana", 2], [["orange", "seedless"], 3], ["durian"]]
Note that this ends up getting us a new array that isn't "identical" to a4, but does have the same values:
a4.equal?(a7) # => false
a4 == a7 # => true
And thus we can again use Hash::[]:
Hash[a7] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}
# or Hash[a6.each_slice(2).to_a]
But there's a problem!
It's important to note that the each_slice(2) solution only gets things back to sanity if the last key was the one missing a value. If we later added an extra key/value pair:
a4_plus = a4.dup # just to have a new-but-related variable name
a4_plus.push(['lychee', 4])
# => [["apple", 1],
# ["banana", 2],
# [["orange", "seedless"], 3], # multi-value key
# ["durian"], # missing value
# ["lychee", 4]] # new well-formed item
a6_plus = a4_plus.flatten(1)
# => ["apple", 1, "banana", 2, ["orange", "seedless"], 3, "durian", "lychee", 4]
a7_plus = a6_plus.each_slice(2).to_a
# => [["apple", 1],
# ["banana", 2],
# [["orange", "seedless"], 3], # so far so good
# ["durian", "lychee"], # oops! key became value!
# [4]] # and we still have a key without a value
a4_plus == a7_plus # => false, unlike a4 == a7
And the two hashes we'd get from this are different in important ways:
ap Hash[a4_plus] # prints:
{
"apple" => 1,
"banana" => 2,
[ "orange", "seedless" ] => 3,
"durian" => nil, # correct
"lychee" => 4 # correct
}
ap Hash[a7_plus] # prints:
{
"apple" => 1,
"banana" => 2,
[ "orange", "seedless" ] => 3,
"durian" => "lychee", # incorrect
4 => nil # incorrect
}
(Note: I'm using awesome_print's ap just to make it easier to show the structure here; there's no conceptual requirement for this.)
So the each_slice solution to an unbalanced flat input only works if the unbalanced bit is at the very end.
Take-aways:
Whenever possible, set up input to these things as [key, value] pairs (a sub-array for each item in the outer array).
When you can indeed do that, either #to_h or Hash::[] will both work.
If you're unable to, Hash::[] combined with the splat (*) will work, so long as inputs are balanced.
With an unbalanced and flat array as input, the only way this will work at all reasonably is if the last value item is the only one that's missing.
Side-note: I'm posting this answer because I feel there's value to be added – some of the existing answers have incorrect information, and none (that I read) gave as complete an answer as I'm endeavoring to do here. I hope that it's helpful. I nevertheless give thanks to those who came before me, several of whom provided inspiration for portions of this answer.
Appending to the answer but using anonymous arrays and annotating:
Hash[*("a,b,c,d".split(',').zip([1,2,3,4]).flatten)]
Taking that answer apart, starting from the inside:
"a,b,c,d" is actually a string.
split on commas into an array.
zip that together with the following array.
[1,2,3,4] is an actual array.
The intermediate result is:
[[a,1],[b,2],[c,3],[d,4]]
flatten then transforms that to:
["a",1,"b",2,"c",3,"d",4]
and then:
*["a",1,"b",2,"c",3,"d",4] unrolls that into
"a",1,"b",2,"c",3,"d",4
which we can use as the arguments to the Hash[] method:
Hash[*("a,b,c,d".split(',').zip([1,2,3,4]).flatten)]
which yields:
{"a"=>1, "b"=>2, "c"=>3, "d"=>4}
if you have array that looks like this -
data = [["foo",1,2,3,4],["bar",1,2],["foobar",1,"*",3,5,:foo]]
and you want the first elements of each array to become the keys for the hash and the rest of the elements becoming value arrays, then you can do something like this -
data_hash = Hash[data.map { |key| [key.shift, key] }]
#=>{"foo"=>[1, 2, 3, 4], "bar"=>[1, 2], "foobar"=>[1, "*", 3, 5, :foo]}
Not sure if it's the best way, but this works:
a = ["apple", 1, "banana", 2]
m1 = {}
for x in (a.length / 2).times
m1[a[x*2]] = a[x*2 + 1]
end
b = [["apple", 1], ["banana", 2]]
m2 = {}
for x,y in b
m2[x] = y
end
For performance and memory allocation concerns please check my answer to Rails mapping array of hashes onto single hash where I bench-marked several solutions.
reduce / inject can be the fastest or the slowest solution depending on which method you use it which.
If the numeric values are seq indexes, then we could have simpler ways...
Here's my code submission, My Ruby is a bit rusty
input = ["cat", 1, "dog", 2, "wombat", 3]
hash = Hash.new
input.each_with_index {|item, index|
if (index%2 == 0) hash[item] = input[index+1]
}
hash #=> {"cat"=>1, "wombat"=>3, "dog"=>2}

Resources