Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert string values to Hash

Tags:

string

ruby

hash

I have a string that I need to convert into a hash. The string's key will always be a symbol and the value will always be an integer:

"a=1, b=2, c=3, d=4"

This string should return a hash that looks like:

{ :a => 1, :b => 2, :c => 3, :d => 4 }

I've tried several different things, but the closest I've been able to come so far is to split the string twice, first for the comma and space, second for the equal sign, and create symbols:

def str_to_hash(str)
  array = str.split(', ').map{|str| str.split('=').map{|k, v| [k.to_sym, v] }}
end

I'd expected the following output:

{:a=>1, :b=>2, :c=>3, :d=>4}

Instead I got:

[[[:a, nil], [:"1", nil]], [[:b, nil], [:"2", nil]], [[:c, nil], [:"3", nil]], [[:d, nil], [:"4", nil]]]

As you can see, it is creating 8 separate strings with 4 symbols. I can't figure out how to make Ruby recognize the numbers and set them as the values in the key/value pair. I've looked online and even asked my coworkers for help, but haven't found an answer so far. Can anybody help?

like image 726
LeonTheGreat Avatar asked Jan 18 '26 14:01

LeonTheGreat


2 Answers

Try this I think it looks a little cleaner

s= "a=1, b=2, c=3, d=4"
Hash[s.scan(/(\w)=(\d)/).map{|a,b| [a.to_sym,b.to_i]}]

Here is the inner workings

#utilize scan with capture groups to produce a multidimensional Array
s.scan(/(\w)=(\d)/)
#=> [["a", "1"], ["b", "2"], ["c", "3"], ["d", "4"]]
#pass the inner Arrays to #map an replace the first item with a sym and the second to Integer
.map{|a,b| [a.to_sym,b.to_i]}
#=> [[:a, 1], [:b, 2], [:c, 3], [:d, 4]]
#Wrap the whole thing in Hash::[] syntax to convert
Hash[s.scan(/(\w)=(\d)/).map{|a,b| [a.to_sym,b.to_i]}]
#=>  {:a=>1, :b=>2, :c=>3, :d=>4}

If you want to avoid the Hash::[] method which I have always though was ugly you can do the following

#Ruby >= 2.1 you can use Array#to_h
  s.scan(/(\w)=(\d)/).map{|a,b| [a.to_sym,b.to_i]}.to_h
  #=>  {:a=>1, :b=>2, :c=>3, :d=>4}
#Ruby < 2.1 you can use Enumerable#each_with_object
  s.scan(/(\w)=(\d)/).each_with_object({}){|(k,v),obj| obj[k.to_sym] = v.to_i}
  #=>  {:a=>1, :b=>2, :c=>3, :d=>4}

Although there are a ton of other ways to handle this issue as is evident by the many other answers here is one more just for fun.

Hash[*s.scan(/(\w)=(\d)/).flatten.each_with_index.map{|k,i| i.even? ? k.to_sym : k.to_i}]
like image 64
engineersmnky Avatar answered Jan 20 '26 14:01

engineersmnky


> s = "a=1, b=2, c=3, d=4"
=> "a=1, b=2, c=3, d=4"
> Hash[s.split(",").map(&:strip).map { |p| p.split("=") }.map { |k, v| [ k.to_sym, v.to_i ] }]
=> {:a=>1, :b=>2, :c=>3, :d=>4}

Part of the problem is that you're trying to do it in a single line and losing track of what the intermediate values are. Break it down into each component, make sure you're using what Ruby gives you, etc.

Your naming assumes you get an array back (not a hash). Hash[...], however, will create a hash based on an array of [key, value] pairs. This makes manual hash stuffing go away. Also, that method should return a hash, not set something–keep methods small, and pure.

Note I strip the first set of split values. This avoids symbols like :" a", which you get if you don't trim leading/trailing spaces. My code does not take strings like "a = 1" into account–yours should.

First, make things readable. Then, if (and only if) it makes sense, and remains legible, play code golf.

> s = "a=1, b=2, c=3, d=4"
=> "a=1, b=2, c=3, d=4"
> a1 = s.split(",")
=> ["a=1", " b=2", " c=3", " d=4"]
> a2 = a1.map(&:strip)
=> ["a=1", "b=2", "c=3", "d=4"]
> a3 = a2.map { |s| s.split("=") }
=> [["a", "1"], ["b", "2"], ["c", "3"], ["d", "4"]]
> a4 = a3.map { |k, v| [ k.to_sym, v.to_i ] }
=> [[:a, 1], [:b, 2], [:c, 3], [:d, 4]]
> Hash[a4]
=> {:a=>1, :b=>2, :c=>3, :d=>4}

Unrelated, but if you're doing a lot of ETL with Ruby, especially on plain text, using mixins can make code much cleaner, closer to a DSL. You can play horrible games, too, like:

> def splitter(sym, s)
    String.send(:define_method, sym) do
      split(s).map(&:strip)
    end
  end
> s = "a=1, b=2, c=3, d=4"
> splitter :split_comma, ","
> splitter :split_eq,    "-"
> Hash[s.split_comma.map(&:split_eq).map { |k, v| [ k.to_sym, v.to_i ]}]
=> {:a=>1, :b=>2, :c=>3, :d=>4}

It can get significantly worse than this and become a full-fledged ETL DSL. It's great if you need it, though.

like image 23
Dave Newton Avatar answered Jan 20 '26 15:01

Dave Newton



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!