RubyNoobie
RubyNoobie

Reputation: 685

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

[]

Upvotes: 57

Views: 28796

Answers (8)

Philippe Perret
Philippe Perret

Reputation: 78

You can use $~[:<capture name>] to catch the value.


str = "My name is John Doe"

str = str.sub(/^My name is (?<firstname>.+?) (?<lastname>.+?)$/) do
  "My name is #{$~[:lastname]} #{$~[:firstname]}"
end

p str
# => "My name is Doe John

Upvotes: 0

mechnicov
mechnicov

Reputation: 15288

New symbolize_names: true option is available for MatchData#named_captures in Ruby 3.3+

If keyword argument symbolize_names is given a true value, the keys in the resulting hash are Symbols

m = /(?<a>.)(?<a>.)/.match("01")  # => #<MatchData "01" a:"0" a:"1">

m.named_captures #=> {"a" => "1"}
m.named_captures(symbolize_names: true) #=> {:a => "1"}

Upvotes: 0

user16452228
user16452228

Reputation:

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"}

Upvotes: 0

Tilo
Tilo

Reputation: 33732

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"}

Upvotes: 0

Christopher Oezbek
Christopher Oezbek

Reputation: 26373

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.

Upvotes: 3

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230346

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"}

Upvotes: 101

Epigene
Epigene

Reputation: 3908

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"}

Upvotes: 0

Kimmo Lehto
Kimmo Lehto

Reputation: 6041

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"}

Upvotes: 6

Related Questions