Iggy
Iggy

Reputation: 5251

Ruby - converting a string into hash with each character as key and index as value?

I am trying to transform a given string into a hash with each its character = key and index = value.

For example, if I have str = "hello", I would like it to transform into {"h"=>0, "e"=>1, "l"=>2, "l"=>3, "o"=>4}.

I created a method as such:

def map_indices(arr)
  arr.map.with_index {|el, index| [el, index]}.to_h
end
#=> map_indices('hello'.split(''))
#=> {"h"=>0, "e"=>1, "l"=>3, "o"=>4}

The problem is it skips the first l. If I reverse the order of el and index: arr.map.with_index {|el, index| [index, el]}.to_h, I get all the letters spelled out: {0=>"h", 1=>"e", 2=>"l", 3=>"l", 4=>"o"}

But when I invert it, I get the same hash that skips one of the l's.

map_indices('hello'.split('')).invert
#=> {"h"=>0, "e"=>1, "l"=>3, "o"=>4}

Why is this behaving like such? How can I get it to print {"h"=>0, "e"=>1, "l"=>2, "l"=>3, "o"=>4}?

Upvotes: 2

Views: 1921

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110655

Any of the following could be used. For

str = "hello"

all return

{"h"=>[0], "e"=>[1], "l"=>[2, 3], "o"=>[4]}

str.each_char
   .with_index
   .with_object({}) { |(c,i),h| (h[c] ||= []) << i }

See String#each_char, Enumerator#with_index and Enumerator#with_object. The block variables have been written to exploit array decomposition.


str.each_char
   .with_index
   .with_object(Hash.new { |h,k| h[k] = [] }) { |(c,i),h| h[c] << i }

See the form of Hash::new that takes a block and no argument. If a hash has been defined

h = Hash.new { |h,k| h[k] = [] }

and later

h[c] << i

is executed, h[c] is first set equal to an empty array if h does not have a key c.


str.size
   .times
   .with_object(Hash.new { |h,k| h[k] = [] }) { |i,h| h[str[i]] << i }

str.each_char
   .with_index
   .group_by(&:first)
   .transform_values { |a| a.flat_map(&:last) }

See Enumerable#group_by, Hash#transform_values (introduced in Ruby v2.5) and Enumerable#flat_map.

Note that

str.each_char
   .with_index
   .group_by(&:first)
  #=> {"h"=>[["h", 0]], "e"=>[["e", 1]], "l"=>[["l", 2], ["l", 3]],
  #    "o"=>[["o", 4]]}

Upvotes: 3

Julius Dzidzevičius
Julius Dzidzevičius

Reputation: 11000

I am new to Ruby and I am sure this can be refactored, but another alternative might be:

arr1 = "Hello".split(%r{\s*})

arr2 = []

for i in 0..arr1.size - 1
  arr2 << i
end

o = arr1.zip(arr2)

a_h = []

o.each do |i|
  a_h << Hash[*i]
end

p a_h.each_with_object({}) { |k, v| k.each { |kk,vv| (v[kk] ||= []) << vv } }

=> {"H"=>[0], "e"=>[1], "l"=>[2, 3], "o"=>[4]}

Upvotes: 0

steenslag
steenslag

Reputation: 80065

It can be done, but will confuse other Ruby programmers.A normal hash treats a key "a" as identical to another "a". Unless a little known feature .compare_by_identity is used:

h = {}.compare_by_identity
"hello".chars.each_with_index{|c,i| h[c] = i}
p h    # => {"h"=>0, "e"=>1, "l"=>2, "l"=>3, "o"=>4}

Upvotes: 8

squiguy
squiguy

Reputation: 33360

Another option you can use is zipping two enumerations together.

s = "hello"
s.chars.zip(0..s.size)

This yields: [["h", 0], ["e", 1], ["l", 2], ["l", 3], ["o", 4]]

Upvotes: 3

Related Questions