0

I have an array ( array = ["home", "style", "honor", "home" ] and what I want to do is create a hash from this array like this :

hash = { "home" => 1, "style" => 2, "honor" => 3} #Array has two "home" element but I should give one value ("home" => 1) to both.

But I'm wondering is there any easy way to do this? Such as if i = 1 and "home" => 1 and then i will be 2, following "style" => 2. Should I use loops, enmerables ? Or is there better way?

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
elixir
  • 137
  • 8

3 Answers3

4

The first thing we need to do is remove the duplicate elements from the Array. For that, we can use the Enumerable#uniq method:

array.uniq
#=> ['home', 'style', 'honor']

Next, we need to pair up every element with its index. For that, we can use the Enumerable#each_with_index method, which gives us an Enumerator that yields two-element arrays of the original element and its index:

array.uniq.each_with_index
#=> #<Enumerator: ['home', 'style', 'honor']:each_with_index>

We can take a peek at what happens when we use the Enumerator by converting it to an Array:

array.uniq.each_with_index.to_a
#=> [['home', 0], ['style', 1], ['honor', 2]]

The indices are off-by-one, but we could fix that using Enumerable#map:

array.uniq.each_with_index.map {|el, i| [el, i.succ] }
#=> [['home', 1], ['style', 2], ['honor', 3]]

Alternatively, we could fix it later, after we create the Hash by using the Hash#transform_values method.

Another possibility is to convert the Array to an Enumerator first by using Array#each and then use the Enumerator#with_index method which takes an optional offset argument:

array.uniq.each.with_index(1)
#=> #<Enumerator: #<Enumerator: ['home', 'style', 'honor']:each>:with_index(1)>

array.uniq.each.with_index(1).to_a
#=> [['home', 1], ['style', 2], ['honor', 3]]

In order to create the Hash, all we need to do, is use the Enumerable#to_h method to convert the two-element "pairs" we already have into key-value pairs. Enumerable#to_h also takes an optional block, which is another possible way for us to transform the indices.

array.uniq.each.with_index(1).to_h
array.uniq.each_with_index.to_h {|el, i| [el, i.succ]}
array.uniq.each_with_index.to_h.transform_values(&:succ)
#=> { 'home' => 1, 'style' => 2, 'honor' => 3 }
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
3

The desired hash can be obtained in a single pass, thereby avoiding the creation of temporary arrays.

array = ["home", "style", "honor", "home", "style", "hello"] 
e = 1.step
array.each_with_object({}) { |x,h| h[x] = e.next unless h.key?(x) }
  #=> {"home"=>1, "style"=>2, "honor"=>3, "hello"=>4}

See the form of Numeric#step that returns an enumerator. Key lookups (h.key?(x)) are very fast, the time required being almost independent of the size of the hash.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Seems like `h[x] ||= e.next` would be equally as efficient since `h` does not have a `default_value` and `e.next` will never be `nil`. Both `Hash#key?` and `Hash#[]` utilize the same function (`hash_stlike_lookup`) or we could go with a default `array.each_with_object(Hash.new {|h,k| h[k] = e.next}) {|x,h| h[x] }` – engineersmnky Dec 28 '20 at 17:20
  • @engineersmnky, all good points. I especially like your `{ |x,h| h[x] }` because it is so very mysterious. For readability, I slightly prefer `h.key?(x)`. – Cary Swoveland Dec 28 '20 at 19:50
  • if it is mystery you are looking for then `{_2[_1]}` (ruby >= 2.7.0 [Numbered block parameters](https://rubyreferences.github.io/rubychanges/2.7.html#numbered-block-parameters)) or better yet `array.each.with_index(1).with_object({}) {_2.store *_1 unless _2.key?_1[0] }` – engineersmnky Dec 28 '20 at 20:03
  • @engineersmnky, it appears you are are enjoying your Christmas eggnog. Note sure about better yet; isn't `_2` incrementing with every `_1`? – Cary Swoveland Dec 28 '20 at 20:15
  • You are correct. It would require a `uniq` although it would be `_1[1]` that is incrementing with each `_1[0]` (`_2` would be the injected `Hash`) – engineersmnky Dec 28 '20 at 20:16
1

Here's a one liner approach:

array
  .uniq
  .each_with_object({})
  .each_with_index { |(item, hash), index| 
      hash[item] = index + 1 
    }

Basically, I'm removing duplicates first, then iterating over the array (which is now essentially a set) with a hash and an index at hand, and just assign the next index to each item. I added 1 to each result just to match your desired outcome.

Eliav Lavi
  • 66
  • 6