Improving String#match with instance_eval
A couple of days ago, Tim Lucas wrote a cool article called "instance_eval brings sexy back" where he demonstrated how to use instance_eval to improve the usability of the match method. The downside, however, was that Tim's technique required manually defining accessor methods each time match was used.
Myles Byrne rapidly responded with a cuter solution:
class MatchData def matchnames(*names) names.each_with_index do |name, index| self.instance_eval "def #{name}; self[#{index+1}] end" end self end end time_components = /(\d+):(\d+):(\d+)/.match("17:00:34").matchnames(:hours, :mins, :secs) time_components.hours
I quickly realized, however, that I never use match in this way. I prefer to use String#match instead (Regexp#match doesn't click to me, it seems back to front). I also wasn't too keen on the method chaining as it didn't address instances where the result of match is nil. I, therefore, have come up with an uglier, but more pragmatic solution:
class String alias _match match def match(*args) m = _match(args.first) if m && m.length > 1 args[1..-1].each_with_index do |name, index| m.instance_eval "def #{name}; self[#{index+1}] end" end end m end end m = "12:34:56".match(/(\d+):(\d+):(\d+)/, :hour, :minute, :second) puts m.hour puts m.minute puts m.second
Thanks to Tim and Myles for the inspiration on this.
December 3, 2006 at 2:20 pm
This should be "12:34:56".match(/(\d+):(\d+):(\d+)/
My cryptic version: http://pastie.caboo.se/25525
December 3, 2006 at 3:10 pm
Nice, though I do prefer Myles less magical solution (and handling matchnames in NilClass too). Is there a way to add a singleton method to the nil returned by match, rather than messing up the whole NilClass class? Because nil is a singleton object it makes life a bit more difficult.
December 3, 2006 at 4:54 pm
Let's clean that up a bit, and avoid using eval. If you're going to do meta-programming and mess with an object's metaclass (a.k.a. singleton class) it makes sense to use it explicitly.
class String
alias _match match
def match(*args)
m = _match(args.shift)
if m && m.length > 1
meta = (class
December 3, 2006 at 4:56 pm
Grr, my comment got cut off and reformatted. Try this: http://pastie.caboo.se/25533
December 3, 2006 at 5:03 pm
Rimantas: I can safely blame WordPress for this. It always seems to mangle backslashes in my code and I have to go back and manually add them! Thanks for pointing it out.
Tim: To be honest, I just don't like the tacked on method. It reads a bit longer. It's really something match should provide out of the box (IMHO), so that's why I prefer it there. That said, perhaps there is wider scope for that technique on Arrays generally (not just MatchData).. so you can assign method names to different elements.
Greg: Excellent, I like this one the best :)
December 3, 2006 at 5:10 pm
http://tfletcher.com/lib/named_captures.rb
Another way :)
December 4, 2006 at 5:00 am
Oniguruma already has named captures. :)
December 4, 2006 at 4:13 pm
Tim: Nice, and I love the quality and self-contained tests. I'll aspire to similar quality next time I post some code.
December 5, 2006 at 6:56 pm
Hey guys, any chance to have this included in some gem (like facets or similar)?
Wouldn't be great to collect all these tricks?