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.
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) endAn error came out while running your code above, what exactly does "&;:to_i’ mean then?
-
Whoops. Johan: problem with RedCloth! Sorry…