Accessing regex matches can often create ugly code.
For example, take the following code:
time_components = /(\d+):(\d+):(\d+)/.match("17:00:34")
time_components[1] # => "17"
time_components[2] # => "00"
time_components[3] # => "34"
That’s not too bad in this trivial example, but if you start to use the regex Match object multiple times it starts to create ugly code. “[1]” isn’t exactly the most descriptive label for that chunk of data is it?
Marcel’s funky post on projectionist showing a sexy use of just-in-time methods using instance_eval and class << has had me thinking lately, and I refactored some code yesterday to make these regex’s a bit more sexy.
time_components = /(\d+):(\d+):(\d+)/.match("17:00:34")
time_components.instance_eval do
def hours; self[1] end
def minutes; self[2] end
def seconds; self[3] end
end
You can now just refer to the components by what they actually represent:
time_components.hours # => "17"
time_components.minutes # => "00"
time_components.seconds # => "34"
time_components.class # => MatchData
How sexy is that? And look mah, it’s still a MatchData object.
You can make it even sexier and break it down to a single statement by making instance_eval return self:
time_components = /(\d+):(\d+):(\d+)/.match("17:00:34").instance_eval do
def hours; self[1] end
def minutes; self[2] end
def seconds; self[3] end
self
end
time_components.hours # => "17"
In the app I’m working on we’re calling out to the system to get some stats, and then munging that data to provide information to the view. Combining the above techniques I ended up with:
def memory
@memory ||= begin
system_sysctl.instance_eval do
def values; split.map(&:to_i) end
def total; values.inject(0) { |v, total| total + v } end
def free; values.last end
def used; total - free end
def percent_used; (used.to_f / total.to_f) * 100 end
self
end
end
end
def system_sysctl
`sysctl ...`
end
And now my view can simply refer to the following:
@system.memory.percent_used
Hot, no?
Archived comments
Comments were previously allowed on articles. Though no new comments are being accepted you can see the old comments below.
-
mmm.. instance_eval is sexy i just used it a while back for a nearly identical use.. adding a yearweek method similar to sql’s yearweak to a date object.
date.instance_eval do def yearweek self.cwyear.to_s + sprintf(“%02d”, self.cweek) end end
-
if only it was formatted!!!
-
If only ;)
-
thanks mate : D
-
Nice work tim. Doing an index access on matchdata objects has always bugged me too.
You could also give the matchdata class some love directly:
class MatchData def matchnames(*names) names.each_with_index do |name, index| self.instance_eval "def #{name}; self[#{index+1}] end" end self end endand then do:
time_components = /(\d+):(\d+):(\d+)/.match("17:00:34").matchnames(:hours, :mins, :secs) time_components.hours -
Now that, is hot.
How about adding
matchnamesto NilClass and have it return nil, so you can still keep the sexy chaining if it doesn’t match the string. -
hot hot hot.
(Tim + Myles) += 1
-
I’ve posted more about this, along with an implementation for String#match (my preferred poison) at http://www.rubyinside.com/improving-stringmatch-with-instance_eval-315.html
Thanks guys!
-
syntax error, unexpected ’;’ def values; split.map(&;:to_i) end
An error came out while running your code above, what exactly does “&;:to_i’ mean then?
-
Whoops. Johan: problem with RedCloth! Sorry…