Finding N keys with highest value in hash, keeping order - ruby

In a Ruby script,
I have a hash that has sentences as keys and relevance scores as values.
I want to retrieve an array containing the N most relevant sentences (highest scores).
I want to retain the order in which these sentences are extracted.
Given:
hash = {
'This is the first sentence.' => 5,
'This is the second sentence.' => 1,
'This is the last sentence.' => 6
}
Then:
choose_best(hash, 2)
Should return:
['This is the first sentence.', 'This is the last sentence.']
All the methods I can think of involve reordering the hash, thus losing the order of the sentences. What would be the best way to tackle this?

Try the following monster:
hash.map(&:reverse).each_with_index
.sort_by(&:first).reverse
.take(2)
.sort_by(&:last)
.map { |(_,s),_| s }
Another functional one:
hash.to_a.values_at(*hash.values.each_with_index
.sort.reverse
.map(&:last)
.sort.take(2))
.map(&:first)
Note however, that as an unordered data structure, a hash table is not really suitable for this use case (although the order is remembered in Ruby 1.9). You should use an array instead (the sorting code remains the same):
sentences = [
['This is the first sentence.', 5],
['This is the second sentence.', 1],
['This is the last sentence.', 6],
]

def extract hash, n
min = hash.values.sort[-n]
a = []
i = 0
hash.each{|k, v| (a.push(k) and i += 1) if i < n and v >= min}
a
end

hash = {
'This is the first sentence.' => 5,
'This is the second sentence.' => 1,
'This is the last sentence.' => 6
}
cutoff_val = hash.values.sort[-2] #cf. sawa
p hash.select{|k,v| v >= cutoff_val }
# =>{"This is the first sentence."=>5, "This is the last sentence."=>6}

Starting in Ruby 2.2.0, Enumerable#max_by takes an optional integer argument that makes it return an array instead of just a single element. Therefore, we can do:
hash = {
'This is the first sentence.' => 6,
'This is the second sentence.' => 1,
'This is the last sentence.' => 5
}
p hash.max_by(2, &:last).map(&:first).sort_by { |k| hash.keys.index k }
# => ["This is the first sentence.", "This is the last sentence."]
The call to sort_by at the end guarantees the sentences are in the right order, as you requested.

a = hash.sort_by { |sentence, score| score }.reverse
The array a now contains pairs of values of your top scoring sentences. You can select the first N of them.
hash = {"foo" => 7, "bar" => 2, "blah" => 3 }
a = hash.sort_by { |sentence, score| score }.reverse
=> [["foo", 7], ["blah", 3], ["bar", 2]]

Related

Understanding Ruby sort_by

I am new to ruby and I struggle understanding sort_by!. This method does something magically I really don't understand.
Here is a simple example:
pc= ["Z6","Z5","Z4"]
c = [
{
id: "Z4",
name: "zlah1"
},
{
id: "Z5",
name: "blah2"
},
{
id: "Z6",
name: "clah3"
}
]
c.sort_by! do |c|
pc.index c[:id]
end
This procedure returns:
=> [{:id=>"Z6", :name=>"clah3"}, {:id=>"Z5", :name=>"blah2"}, {:id=>"Z4", :name=>"zlah1"}]
It somehow reverses the array order. How does it do that? pc.index c[:id] just returns a number. What does this method do under the hood? The documentation is not very beginners friendly.
Suppose we are given the following:
order = ['Z6', 'Z5', 'Z4']
array = [{id: 'Z4', name: 'zlah1'},
{id: 'Z5', name: 'blah2'},
{id: 'Z6', name: 'clah3'},
{id: 'Z5', name: 'dlah4'}]
Notice that I added a 4th hash ({id: 'Z5', name: 'dlah4'}) to the array array given in the question. I did this so that two elements of array would the same value for the key :id ("Z5").
Now let's consider how Ruby might implement the following:
array.sort_by { |hash| order.index(hash[:id]) }
#=> [{:id=>"Z6", :naCme=>"clah3"},
# {:id=>"Z5", :name=>"blah2"},
# {:id=>"Z5", :name=>"dlah4"},
# {:id=>"Z4", :name=>"zlah1"}]
That could be done in four steps.
Step 1: Create a hash that maps the values of the sort criterion to the values of sort_by's receiver
sort_map = array.each_with_object(Hash.new { |h,k| h[k] = [] }) do |hash,h|
h[order.index(hash[:id])] << hash
end
#=> {2=>[{:id=>"Z4", :name=>"zlah1"}],
# 1=>[{:id=>"Z5", :name=>"blah2"}, {:id=>"Z5", :name=>"dlah4"}],
# 0=>[{:id=>"Z6", :name=>"clah3"}]}
h = Hash.new { |h,k| h[k] = [] } creates an empty hash with a default proc that operates as follows when evaluating:
h[k] << hash
If h has a key k this operation is performed as usual. If, however, h does not have a key k the proc is called, causing the operation h[k] = [] to be performed, after which h[k] << hash is executed as normal.
The values in this hash must be arrays, rather than individual elements of array, due the possibility that, as here, two elements of sort_by's receiver map to the same key. Note that this operation has nothing to do with the particular mapping of the elements of sort_by's receiver to the sort criterion.
Step 2: Sort the keys of sort_map
keys = sort_map.keys
#=> [2, 1, 0]
sorted_keys = keys.sort
#=> [0, 1, 2]
Step 3: Map sorted_keys to the values of sort_map
sort_map_values = sorted_keys.map { |k| sort_map[k] }
#=> [[{:id=>"Z6", :name=>"clah3"}],
# [{:id=>"Z5", :name=>"blah2"}, {:id=>"Z5", :name=>"dlah4"}],
# [{:id=>"Z4", :name=>"zlah1"}]]
Step 4: Flatten sort_map_values
sort_map_values.flatten
#=> [{:id=>"Z6", :name=>"clah3"},
# {:id=>"Z5", :name=>"blah2"},
# {:id=>"Z5", :name=>"dlah4"},
# {:id=>"Z4", :name=>"zlah1"}]
One of the advantages of using sort_by rather than sort (with a block) is that the sort criterion (here order.index(hash[:id])) is computed only once for each element of sort_by's receiver, whereas sort would recompute these values for each pairwise comparison in its block. The time savings can be considerable if this operation is computationally expensive.
order = ['Z6', 'Z5', 'Z4']
array = [{id: 'Z4', name: 'zlah1'},
{id: 'Z5', name: 'blah2'},
{id: 'Z6', name: 'clah3'}]
array.sort_by { |hash| order.index(hash[:id]) }
#=> [{:id=>"Z6", :name=>"clah3"}, {:id=>"Z5", :name=>"blah2"}, {:id=>"Z4", :name=>"zlah1"}]
This doesn't magically reverse the order of the array. To explain what happens we first need to understand what order.index(hash[:id]) does. This becomes better visible with the map method.
array.map { |hash| order.index(hash[:id]) }
#=> [2, 1, 0]
Like you can see, the first element with id 'Z4' will return the number 2 since 'Z4' in the order array has index 2. The same happens with all other array elements. The retuned value is used to sort the objects, sort_by will always sort asynchronous, so the order of the above array should become [0, 1, 2]. However, the actual content is not replaced, the number is only used for comparison vs other elements. Thus resulting in:
#=> [{:id=>"Z6", :name=>"clah3"}, {:id=>"Z5", :name=>"blah2"}, {:id=>"Z4", :name=>"zlah1"}]

How to get the highest and the lowest points in the hash?

#participants[id] = {nick: nick, points: 1}
=> {"1"=>{:nick=>"Test", :points=>3}, "30"=>{:nick=>"AnotherTest", :points=>5}, "20"=>{:nick=>"Newtest", :points=>3}}
I want my the lowest points (ID: 1 and 20). How do I get the lowest points go first and then ID 30 go last?
If you use Enumerable#max_by() or Enumerable#min_by() you can do following;
data = {
"1" => {nick: "U1", points: 3},
"30" => {nick: "U30", points: 5},
"20" => {nick: "U20", points: 3}
}
max_id, max_data = data.max_by {|k,v| v[:points]}
puts max_id # => 30
puts max_data # => {nick: "U30", points: 5}
Same thing works with #min_by() and if you want to get back Hash you do this:
minimal = Hash[*data.min_by {|k,v| v[:points]}]
puts minimal # => {"1"=>{:nick=>"U1", :points=>3}}
Functions min_by() and max_by() will always return one record. If you want to get all records with same points then you have to use min / max data an do another "lookup" like this:
min_id, min_data = data.min_by {|k,v| v[:points]}
all_minimal = data.select {|k,v| v[:points] == min_data[:points]}
puts all_minimal
# => {"1"=>{:nick=>"U1", :points=>3}, "20"=>{:nick=>"U20", :points=>3}}
Hashes are not a suitable data structure for this operation. They are meant when you need to get a value in O(1) complexity.
You are better off using a sorted array or a tree if you are interested in comparisons or Heap (in case you are interested in only maximum or minimum value) as #Vadim suggested
Use Enumerable#minmax_by:
h = { "1"=>{:nick=>"Test", :points=>3},
"30"=>{:nick=>"AnotherTest", :points=>5},
"20"=>{:nick=>"Newtest", :points=>3}}
h.minmax_by { |_,g| g[:points] }
#=> [[ "1", {:nick=>"Test", :points=>3}],
# ["30", {:nick=>"AnotherTest", :points=>5}]]
You can work around the fact that min_by and max_by are returning only one result by combining sort and chunk:
data.sort_by{|_,v| v[:points]}.chunk{|(_,v)| v[:points]}.first.last.map(&:first)
#=> ["1", "20"]

Am I able to use each_cons(2).any? with hash values?

If I have an array of integers and wish to check if a value is less than the previous value. I'm using:
array = [1,2,3,1,5,7]
con = array.each_cons(2).any? { |x,y| y < x }
p con
This returns true, as expected as 1 is less than 3.
How would I go about checking if a hash value is less than the previous hash value?
hash = {"0"=>"1", "1"=>"2","2"=>"3","4"=>"1","5"=>"5","6"=>"7"}
I'm still learning Ruby so help would be greatly appreciated.
If you want to find out whether all elements meet the criteria, starting with an array:
array = [1,2,3,1,5,7]
con = array.each_cons(2).all? { |x,y| x < y }
con # => false
Changing the array so the elements are all less than the next:
array = [1,2,3,4,5,7]
con = array.each_cons(2).all? { |x,y| x < y }
con # => true
A lot of the methods behave similarly for array elements and hashes, so the basic code is the same, how they're passed into the block changes. I reduced the hash to the bare minimum to demonstrate the code:
hash = {"3"=>"3","4"=>"1","5"=>"5"}
con = hash.each_cons(2).all? { |(_, x_value), (_, y_value) | x_value < y_value }
con # => false
Changing the hash to be incrementing:
hash = {"3"=>"3","4"=>"4","5"=>"5"}
con = hash.each_cons(2).all? { |(_, x_value), (_, y_value) | x_value < y_value }
con # => true
Using any? would work the same way. If you want to know whether any are >=:
hash = {"3"=>"3","4"=>"1","5"=>"5"}
con = hash.each_cons(2).any? { |(_, x_value), (_, y_value) | y_value >= x_value }
con # => true
Or:
hash = {"3"=>"3","4"=>"4","5"=>"5"}
con = hash.each_cons(2).any? { |(_, x_value), (_, y_value) | x_value >= y_value }
con # => false
I'm creating the hash by
stripped = Hash[x.scan(/(\w+): (\w+)/).map { |(first, second)| [first.to_i, second.to_i] }]
I'm then removing empty arrays by
new = stripped.delete_if { |elem| elem.flatten.empty? }
This isn't a good way to use scan. Consider these:
'1: 23'.scan(/\d+/) # => ["1", "23"]
'1: 23'.scan(/(\d+)/) # => [["1"], ["23"]]
'1: 23'.scan(/(\d+): (\d+)/) # => [["1", "23"]]
In the first, scan returns an array of values. In the second, it returns an array of arrays, where each sub-array is a single element. In the third it returns an array of arrays, where each sub-array contains both elements scanned. You are using the third form, which unnecessarily complicates everything done after that.
Don't complicate the pattern passed to scan, and, instead, rely on its ability to return multiple matching elements as it looks through the string and to return an array of those:
'1: 23'.scan(/\d+/) # => ["1", "23"]
Build on top of that:
'1: 23'.scan(/\d+/).map(&:to_i) # => [1, 23]
Hash[*'1: 23'.scan(/\d+/).map(&:to_i)] # => {1=>23}
Notice the leading * inside Hash[]. That "splat" tells Ruby to burst or explode the array into its components. Here's what happens if it's not there:
Hash['1: 23'.scan(/\d+/).map(&:to_i)] # => {} # !> this causes ArgumentError in the next release
And, finally, if you don't need the hash elements to be integers, which contradicts the hash you gave in your question, just remove .map(&:to_i) from the examples above.
First, isolate the values from the hash:
values = hash.map { |key, value| value.to_i } #=> [1, 2, 3, 1, 5, 7]
or:
values = hash.values.map(&:to_i) #=> to_i is a shortcut for:
values = hash.values.map { |value| value.to_i }
and then do the same thing you did for your array example.

How to display dynamic case statement in Ruby

How would I write a case statement that would list all elements in an array, allow the user to pick one, and do processing on that element?
I have an array:
array = [ 'a', 'b', 'c', 'd' ]
Ultimately I'd like it to behave like this:
Choices:
1) a
2) b
3) c
4) d
Choice =>
After the user picks 3, I would then do processing based off the choice of the user. I can do it in bash pretty easily.
Ruby has no built-in menu stuff like shell scripting languages do. When doing menus, I favor constructing a hash of possible options and operating on that:
def array_to_menu_hash arr
Hash[arr.each_with_index.map { |e, i| [i+1, e] }]
end
def print_menu menu_hash
puts 'Choices:'
menu_hash.each { |k,v| puts "#{k}) #{v}" }
puts
end
def get_user_menu_choice menu_hash
print 'Choice => '
number = STDIN.gets.strip.to_i
menu_hash.fetch(number, nil)
end
def show_menu menu_hash
print_menu menu_hash
get_user_menu_choice menu_hash
end
def user_menu_choice choice_array
until choice = show_menu(array_to_menu_hash(choice_array)); end
choice
end
array = %w{a b c d}
choice = user_menu_choice(array)
puts "User choice was #{choice}"
The magic happens in array_to_menu_hash:
The [] method of Hash converts an array with the form [ [1, 2], [3, 4] ] to a hash {1 => 2, 3 => 4}. To get this array, we first call each_with_index on the original menu choice array. This returns an Enumerator that emits [element, index_number] when iterated. There are two problems with this Enumerator: the first is that Hash[] needs an array, not an Enumerator. The second is that the arrays emitted by the Enumerator have the elements in the wrong order (we need [index_number, element]). Both of these problems are solved with #map. This converts the Enumerator from each_with_index into an array of arrays, and the block given to it allows us to alter the result. In this case, we are adding one to the zero-based index and reversing the order of the sub-arrays.

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