How to do named capture in ruby
RubyRegexRuby Problem Overview
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
[]
Ruby Solutions
Solution 1 - Ruby
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"}
Solution 2 - Ruby
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"}
Solution 3 - Ruby
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 withnamed_captures
. - It is supported in Ruby 1.9.3!
> 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.
Solution 4 - Ruby
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"}
Solution 5 - Ruby
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"}
Solution 6 - Ruby
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"}