Ruby string to hash values - ruby

I just started to learn Ruby!
I have the following string:
"Mark Smith, 29"
and I want to convert it to hash, so it looks like this:
{:name=>"Mark", :surname=>"Smith", :age=>29}
I've written the following code, to cut the input:
a1 = string.scan(/\w+|\d+/)
Now I have an array of strings. Is there an elegant way to convert this to hash? I know I can make three iterations like this:
pers = Hash.new
pers[:name] = a1[0]
pers[:surname] = a1[1]
pers[:age] = a1[2]
But maybe there is a way to do it using .each method or something like this? Or maybe it is possible to define a class Person, with predefined keys (:name, :surname, :age), and then just "throw" my string to an instance of this class?

Yes, you can do like,
%i(name surname age)
.zip(string.scan(/\w+|\d+/))
.to_h
# => {:name=>"Mark", :surname=>"Smith", :age=>"29"}
Or, you can take the benefit of Struct, like:
Person = Struct.new(:name, :surname, :age )
person = Person.new( *string.scan(/\w+|\d+/) )
person.age # => "29"
person.name # => "Mark"

For simplicity, precision and clarity I'd do it like so:
"Mark Smith, 29" =~ /(\w+)\s+(\w+),\s+(\d+)/
#=> 0
{ :name=> $1, :surname=> $2, :age=> $3.to_i }
#=> {:name=>"Mark", :surname=>"Smith", :age=>29}
Unlike /\w+|\d+/, this regex requires "Mark" and "Smith" to be strings and "29" to be digits.

Related

How to do named capture in ruby

I want to name the capture of string that I get from scan. How to do it?
"555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten #=> ["555", "333", "7777"]
Is it possible to turn it into like this
{:area => "555", :city => "333", :local => "7777" }
or
[["555","area"], [...]]
I tried
"555-333-7777".scan(/((?<area>)\d{3})-(\d{3})-(\d{4})/).flatten
but it returns
[]
You should use match with named captures, not scan
m = "555-333-7777".match(/(?<area>\d{3})-(?<city>\d{3})-(?<number>\d{4})/)
m # => #<MatchData "555-333-7777" area:"555" city:"333" number:"7777">
m[:area] # => "555"
m[:city] # => "333"
If you want an actual hash, you can use something like this:
m.names.zip(m.captures).to_h # => {"area"=>"555", "city"=>"333", "number"=>"7777"}
Or this (ruby 2.4 or later)
m.named_captures # => {"area"=>"555", "city"=>"333", "number"=>"7777"}
Something like this?
"555-333-7777" =~ /^(?<area>\d+)\-(?<city>\d+)\-(?<local>\d+)$/
Hash[$~.names.collect{|x| [x.to_sym, $~[x]]}]
=> {:area=>"555", :city=>"333", :local=>"7777"}
Bonus version:
Hash[[:area, :city, :local].zip("555-333-7777".split("-"))]
=> {:area=>"555", :city=>"333", :local=>"7777"}
In case you don't really need the hash, but just local variables:
if /(?<area>\d{3})-(?<city>\d{3})-(?<number>\d{4})/ =~ "555-333-7777"
puts area
puts city
puts number
end
How does it work?
You need to use =~ regex operator.
The regex (sadly) needs to be on the left. It doesn't work if you use string =~ regex.
Otherwise it is the same syntax ?<var> as with named_captures.
It is supported in Ruby 1.9.3!
Official documentation:
When named capture groups are used with a literal regexp on the
left-hand side of an expression and the =~ operator, the captured text
is also assigned to local variables with corresponding names.
A way to turn capture group names and their values into a hash is to use a regex with named captures using (?<capture_name> and then access the %~ global "last match" variable.
regex_with_named_capture_groups = %r'(?<area>\d{3})-(?<city>\d{3})-(?<local>\d{4})'
"555-333-7777"[regex_with_named_capture_groups]
match_hash = $~.names.inject({}){|mem, capture| mem[capture] = $~[capture]; mem}
# => {"area"=>"555", "city"=>"333", "local"=>"7777"}
# If ActiveSupport is available
match_hash.symbolize_keys!
# => {area: "555", city: "333", local: "7777"}
This alternative also works:
regex = /^(?<area>\d+)\-(?<city>\d+)\-(?<local>\d+)$/
m = "555-333-7777".match regex
m.named_captures
=> {"area"=>"555", "city"=>"333", "local"=>"7777"}
There are a LOT of ways to create named captures, many of which have been mentioned already. For the record though, we could have even used the originally posted code along with Multiple Assignment like so:
a, b, c = "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten
hash = {area: a, city: b, local: c}
#=> {:area=>"555", :city=>"333", :local=>"7777"}
OR
hash = {}
hash[:area], hash[:city], hash[:local] = "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten
hash
#=> {:area=>"555", :city=>"333", :local=>"7777"}
OR along with zip and optionally to_h:
[:area, :city, :local].zip "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten
#=> [[:area, "555"], [:city, "333"], [:local, "7777"]]
([:area, :city, :local].zip "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten).to_h
#=> {:area=>"555", :city=>"333", :local=>"7777"}

Ruby what's the lazy way to get input in loop

Say I want to get 10 inputs in loop and store it in an array. The input will be either string or line or json string.
I'm aware of Ruby's upto and gets.chomp but I'm looking for a simple and lazy technique like:
n=10
arr = []
loop(n) { arr.push getline } #Just an example to share my thought. Will not work
Don't know if this is "simple and lazy" enough:
irb> 3.times.collect { gets.chomp }
foo
bar
baz
# => ["foo", "bar", baz"]
Array.new.
Array.new(3){gets.chomp}
(1..3).map {gets.strip!}
This works nice, and is clean for noise before and after the entries.
Valid in 1.9 and 2.0.
>> (1..3).map {gets.strip!}
Hello
1
2
=> ["Hello", "1", "2"]

simple hash merge by array of keys and values in ruby (with perl example)

In Perl to perform a hash update based on arrays of keys and values I can do something like:
#hash{'key1','key2','key3'} = ('val1','val2','val3');
In Ruby I could do something similar in a more complicated way:
hash.merge!(Hash[ *[['key1','key2','key3'],['val1','val2','val3']].transpose ])
OK but I doubt the effectivity of such procedure.
Now I would like to do a more complex assignment in a single line.
Perl example:
(#hash{'key1','key2','key3'}, $key4) = &some_function();
I have no idea if such a thing is possible in some simple Ruby way. Any hints?
For the Perl impaired, #hash{'key1','key2','key3'} = ('a', 'b', 'c') is a hash slice and is a shorthand for something like this:
$hash{'key1'} = 'a';
$hash{'key2'} = 'b';
$hash{'key3'} = 'c';
In Ruby 1.9 Hash.[] can take as its argument an array of two-valued arrays (in addition to the old behavior of a flat list of alternative key/value arguments). So it's relatively simple to do:
mash.merge!( Hash[ keys.zip(values) ] )
I do not know perl, so I'm not sure what your final "more complex assignment" is trying to do. Can you explain in words—or with the sample input and output—what you are trying to achieve?
Edit: based on the discussion in #fl00r's answer, you can do this:
def f(n)
# return n arguments
(1..n).to_a
end
h = {}
keys = [:a,:b,:c]
*vals, last = f(4)
h.merge!( Hash[ keys.zip(vals) ] )
p vals, last, h
#=> [1, 2, 3]
#=> 4
#=> {:a=>1, :b=>2, :c=>3}
The code *a, b = some_array will assign the last element to b and create a as an array of the other values. This syntax requires Ruby 1.9. If you require 1.8 compatibility, you can do:
vals = f(4)
last = vals.pop
h.merge!( Hash[ *keys.zip(vals).flatten ] )
You could redefine []= to support this:
class Hash
def []=(*args)
*keys, vals = args # if this doesn't work in your version of ruby, use "keys, vals = args[0...-1], args.last"
merge! Hash[keys.zip(vals.respond_to?(:each) ? vals : [vals])]
end
end
Now use
myhash[:key1, :key2, :key3] = :val1, :val2, :val3
# or
myhash[:key1, :key2, :key3] = some_method_returning_three_values
# or even
*myhash[:key1, :key2, :key3], local_var = some_method_returning_four_values
you can do this
def some_method
# some code that return this:
[{:key1 => 1, :key2 => 2, :key3 => 3}, 145]
end
hash, key = some_method
puts hash
#=> {:key1 => 1, :key2 => 2, :key3 => 3}
puts key
#=> 145
UPD
In Ruby you can do "parallel assignment", but you can't use hashes like you do in Perl (hash{:a, :b, :c)). But you can try this:
hash[:key1], hash[:key2], hash[:key3], key4 = some_method
where some_method returns an Array with 4 elements.

Test if variable matches any of several strings w/o long if-elsif chain, or case-when

I assume there is a nice one-line way to say in ruby
if mystr == "abc" or "def " or "ghi" or "xyz"
but cannot find how to do that in the online references I normally consult...
Thanks!
Perhaps you didn't know that you can put multiple conditions on a single case:
case mystr
when "abc", "def", "ghi", "xyz"
..
end
But for this specific string-based test, I would use regex:
if mystr =~ /\A(?:abc|def|ghi|xyz)\z/
If you don't want to construct a regex, and you don't want a case statement, you can create an array of objects and use Array#include? test to see if the object is in the array:
if [a,b,c,d].include?( o )
or, by monkey-patching Object, you can even turn it around:
class Object
def in?( *values )
values.include?( self )
end
end
if o.in?( a, b, c, d )
You can use Array#include? like this:
if ["abc", "def ", "ghi", "xyz"].include?(mystr)
>> mystr="abc"
=> "abc"
>> mystr[/\A(abc|def|ghi|xyz)\z/]
=> "abc"
>> mystr="abcd"
=> "abcd"
>> mystr[/\A(abc|def|ghi|xyz)\z/]
=> nil

How can I assign multiple values to a hash key?

For the sake of convenience I am trying to assign multiple values to a hash key in Ruby. Here's the code so far
myhash = { :name => ["Tom" , "Dick" , "Harry"] }
Looping through the hash gives a concatenated string of the 3 values
Output:
name : TomDickHarry
Required Output:
:name => "Tom" , :name => "Dick" , :name => "Harry"
What code must I write to get the required output?
myhash.each_pair {|k,v| v.each {|n| puts "#{k} => #{n}"}}
#name => Tom
#name => Dick
#name => Harry
The output format is not exactly what you need, but I think you get the idea.
The answers from Rohith and pierr are fine in this case. However, if this is something you're going to make extensive use of it's worth knowing that the data structure which behaves like a Hash but allows multiple values for a key is usually referred to as a multimap. There are a couple of implementations of this for Ruby including this one.
You've created a hash with the symbol name as the key and an array with three elements as the value, so you'll need to iterate through myhash[:name] to get the individual array elements.
re: the issue of iterating over selective keys. Try using reject with the condition inverted instead of using select.
e.g. given:
{:name=>["Tom", "Dick", "Harry"], :keep=>[4, 5, 6], :discard=>[1, 2, 3]}
where we want :name and :keep but not :discard
with select:
myhash.select { |k, v| [:name, :keep].include?(k) }
=> [[:name, ["Tom", "Dick", "Harry"]], [:keep, [4, 5, 6]]]
The result is a list of pairs.
but with reject:
myhash.reject { |k, v| ![:name, :keep].include?(k) }
=> {:name=>["Tom", "Dick", "Harry"], :keep=>[4, 5, 6]}
The result is a Hash with only the entries you want.
This can then be combined with pierr's answer:
hash_to_use = myhash.reject { |k, v| ![:name, :keep].include?(k) }
hash_to_use.each_pair {|k,v| v.each {|n| puts "#{k} => #{n}"}}

Resources