Monkey Patching Core Functionality == BAD, BAD, BAD

Yesterday, I finally got around to upgrading HAML in our Rails app to the newest stable version and the first thing that happened was that 20 completely unrelated specs broke.

Why, you may ask?

A monkey got into our code, that's why.

You see, Ruby allows you to redefine any method of any class on the fly (monkey patching) and it turns out that the old version of HAML had the following code in it:

  unless String.methods.include? 'old_comp'
  class String # :nodoc
    alias_method :old_comp, :<=>

    def <=>(other)
      if other.is_a? NilClass
        -1
      else
        old_comp(other)
      end
    end
  end

  class NilClass # :nodoc:
    include Comparable

    def <=>(other)
      other.nil? ? 0 : 1
    end
  end
end

This, in turn, snuck into our codebase in all sorts of little unexpected places. In one instance a test was comparing sorted Arrays of nils and returning true. Not good.

Luckily, in all of our cases this ended up being more of an irritant than anything else, but I can easily imagine any number of ways in which relying on the assumed behavior of these methods could have broken our app in any number of subtle and terrible ways.

So I'm only going to say this once:

Don't modify core Ruby functionality in your plugins or Rubygems.

You will break your users' codebase.

If you do modify core functionality you deserve to be slapped around by those around you.

Startup School, DHH, and the Missing Marketing Piece

If you weren't at Startup School this weekend or haven't watched DHH's speech yet, you should go check it out. It was entertaining and a good counter-point to much of the ridiculous talk that you hear out here in the Valley.

As I was watching it though, I had the same thought that I always seem to have when I hear someone from 37 Signals talk, and it came to me right when I saw the slide that said:

  1. Great Application
  2. Price
  3. Profit!

If only it were that easy. The thing that these guys always leave out seems to be step 1.5:

Market the hell out of your product, and get a bunch of people to use it.

That step is really, really hard.

I bet that if you asked DHH if he thought that 37 Signals would be just as successful if he hadn't invented Rails, and without the flood of free publicity that that got them, and he answered truthfully, the answer would be "no".

There are probably all sorts of great applications out there that would help me out on a daily basis, but I have little to no time to try out most of them. I've tried out Basecamp though, simply because while learning Rails you hear again and again about how Rails was extracted from it.

Would I (or you) know anything about 37 Signals if it wasn't for Rails? Probably not.

Those guys do a great job of marketing themselves and getting things out in front of people, but just because you're having fun marketing your stuff, doesn't mean that marketing isn't work that you have to do.

If you think that you don't have to market your app, no matter how great it is, you're living in a world similar to the one that DHH had on one his final slides where he said:

500 * $40 = $125,000

That's right, an imaginary world.

Finding the index of an item using a block

Ruby 1.9 has it but if you're not that bleeding edge, you can have it now:

class Array
  def index_with_block(*args)
    return index_without_block(*args) unless block_given?
    each_with_index do |entry, index|
      return index if yield(entry)
    end
    nil
  end
  alias_method :index_without_block, :index
  alias_method :index, :index_with_block

  def rindex_with_block(*args)
    return rindex_without_block(*args) unless block_given?
    
    index = size
    reverse_each do |entry|
      index -= 1
      return index if yield(entry)
    end
    nil
  end
  alias_method :rindex_without_block, :rindex
  alias_method :rindex, :rindex_with_block
end

If you're using Rails you can substitute the two calls each to alias_method with a single call to alias_method_chain.

Drag & Drop Prioritizable Lists

Yes, it's true, Scriptaculous already provides a Sortable that makes it almost trivial to enable drag'n'drop sorting of your HTML lists. Whenever an item is moved an onUpdate() event is called (if provided) allowing you to inspect the new order and presumably perform an AJAX request to record the change. In principle, this sounds great but I've never really liked it for a couple of reasons.

For a start, if you have any appreciable number of items updating each in the database just to re-order one seems somewhat unnecessary. Not withstanding the fact that we need to send all those ids to the server in the first place.

Secondly, if you're doing any kind of filtering, it's difficult at best to take the newly constructed ordering and apply that at the back-end; what happens to all the items that may be lurking in between that aren't presently displayed?

Enter Prioritizable (itself built on top of Sortable).

You use it in much the same way as Sortable with the major difference being that the onUpdate() event is called with three arguments: the item that was moved, the sibling relative to which it was moved, and the relative position ("higher" or "lower"). And, if like me, you're feeling a bit RESTful, it's pretty easy to turn these arguments into a nice semantic URL and parameters as shown:

Prioritizable.create($("chores"), {
  onUpdate: function(item, position, sibling) {
    id = item.substring(6);                           // "chore_17" => "17"
    sibling_id = to.substring(6);                     // "chore_2" => "2"
    url = "/chores/" + sibling_id + "/" + position;   // "/chores/2/higher"
    
    new Ajax.Request(url, {
      method: "post",
      parameters: { id: id }
    });
  }
});

When the onUpdate() event is called we POST the id of the item to be moved to a path constructed from the id of the sibling and the relative position. Assuming the the user moves chore_17 just above chore_2 we would POST "id=17" to /chores/2/higher.

In practice, I combine this client-side behaviour with some server-side code that provides move_higher_than() and move_lower_than() methods that efficiently handle all the necessary database updates.

All the pieces mentioned will eventually be available alongside Cogent's other Rails plugins but until then, here's enough of the Javascript side of things to get you going.

var Prioritizable = {
  create: function(element) {
    options = Object.extend(arguments[1] || {}, {
      onChange: Prioritizable.onChange,
      onUpdate: Prioritizable.onUpdate  
    });

    Sortable.create(element, options);
  },

  destroy: function(element) {
    Sortable.destroy(element);
  },

  onChange: function(item) {
    Sortable.options(item)._item = item;
  },

  onUpdate: function(element) {
    options = Sortable.options(element);
    item = options._item;
    options._item = null;

    sibling = item.previous();
    if (other) {
      position = "higher";
    } else {
      sibling = item.next();
      position = "lower"
    }
    
    options.onUpdate(item, position, sibling);
  }
};

Enjoy!

Getting Chronic to Parse Non-U.S. Dates

In an attempt to push yet more behaviour from my Rails controllers into model classes, I was extracting some code into a yet-to-be-published plugin that allows date and time columns to be set using more human-readable values. For examples:

>> task.completed_at = "now"
>> task.completed_at
=> Wed Apr 09 14:39:12 +1000 2008

Although my actual requirement was to support "now" and "today" I figured it would be rather cool if I could support anything that Chronic does. (If you haven't used Chronic before, it's a natural language date/time parser written in pure Ruby that understands a vast array of expressions including ranges.)

Naturally (no pun intended), Chronic also supports explicit dates such as "1/2/08". As part of my testing however, I discovered that the date parsing is decidedly US-centric presuming, of course, that dates are specified as "month-day-year". So for anyone living in say, Australia, it can be pretty frustrating to have Chronic.parse("1/2/08") return "Wed Jan 02 12:00:00 +1100 2008" rather than the expected "Fri Feb 01 12:00:00 +1100 2008".

The good news is, there is a solution. The bad news is, the solution is far from elegant. But first some context.

Chronic is actually written very nicely and the code is fairly easy to follow. In essence it works as follows: the input string is tokenized; each token is inspected to see if it's a keyword such as a month name, day name, or a number, etc.; and finally tries to matches the sequence of tokens against a pattern such as "a day number followed by a month name and then a year." The problem arises because the pattern for matching "month-day-year" comes before the one for "day-month-year" meaning that unless the first number is greater than 12, Chronic will always consider it to be a month.

The less than elegant solution is almost trivial and involves switching the order in which the patterns are matched. Doing so returns the desired result:

>> Chronic.parse("1/2/08")
=> "Fri Feb 01 12:00:00 +1100 2008"

Which is all very well and good but now we have the reverse problem. What would be better is if we had a more general solution, one that allows us to specify the desired precedence when parsing:

>> Chronic.parse("1/2/08")                                    # Default to U.S. date formats
=> Wed Jan 02 12:00:00 +1100 2008

>> Chronic.parse("1/2/08", :explicit_date_format => :non_us)  # Prefer Non-U.S. formats
=> Fri Feb 01 12:00:00 +1100 2008

>> Chronic.parse("1/2/08", :explicit_date_format => :us)      # Prefer U.S. formats
=> Wed Jan 02 12:00:00 +1100 2008

Among other things, this allows us to have users in Australia enter dates with one format and users in the U.S. another.

For anyone interested, I've pasted a diff for each approach below.

A less than elegant solution:

--- a/chronic-0.2.3/lib/chronic/handlers.rb
+++ b/chronic-0.2.3/lib/chronic/handlers.rb
@@ -13,8 +13,8 @@ module Chronic
                  Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
                  Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
                  Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
                  Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
                  Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
                  Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],

A more general solution:

--- a/chronic-0.2.3/lib/chronic/chronic.rb
+++ b/chronic-0.2.3/lib/chronic/chronic.rb
@@ -43,7 +43,8 @@ module Chronic
       default_options = {:context => :future,
                          :now => Time.now,
                          :guess => true,
-                         :ambiguous_time_range => 6}
+                         :ambiguous_time_range => 6,
+                         :explicit_date_format => :us}
       options = default_options.merge specified_options
             
       # ensure the specified options are valid
@@ -51,6 +52,7 @@ module Chronic
         default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
       end
       [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
+      [:us, :non_us].include?(options[:explicit_date_format]) || raise(InvalidArgumentException, "Invalid value ':#{options[:explicit_date_format]}' for :explicit_date_format specified. Valid values are :us and :non_us.")
       
       # store now for later =)
       @now = options[:now]
--- a/chronic-0.2.3/lib/chronic/handlers.rb
+++ b/chronic-0.2.3/lib/chronic/handlers.rb
@@ -3,41 +3,50 @@ module Chronic
 	class << self
 	  
 	  def definitions #:nodoc:
-	    @definitions ||= 
-      {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
+	    if @definitions.nil?
+        us_date = [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
+                   Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
+                   Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
+                   Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
+                   Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
+                   Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
+                   Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
+                   Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
+                   Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+                   Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
+                   Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)]
+
+        non_us_date = us_date.dup
+        non_us_date[7] = us_date[8]
+        non_us_date[8] = us_date[7]
+
+	      @definitions = 
+        {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
         
-       :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
-                 Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
-                 Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
-                 Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
-                 Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
-                 Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
+         :date => {:us => us_date, :non_us => non_us_date},
                  
-       # tonight at 7pm
-       :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
-                   Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
-                   Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
+         # tonight at 7pm
+         :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                     Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                     Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
                    
-       # 3 weeks from now, in 2 months
-       :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
-                  Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
-                  Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
+         # 3 weeks from now, in 2 months
+         :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
+                    Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
+                    Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
                   
-       # 3rd week in march
-       :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
-                   Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
-      }
+         # 3rd week in march
+         :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
+                     Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
+        }
+      end
+      @definitions
     end
     
     def tokens_to_span(tokens, options) #:nodoc:                   
       # maybe it's a specific date
       
-      self.definitions[:date].each do |handler|
+      self.definitions[:date][options[:explicit_date_format]].each do |handler|
         if handler.match(tokens, self.definitions)
           puts "-date" if Chronic.debug
           good_tokens = tokens.select { |o| !o.get_tag Separator }

Rails is Moving to Git: Helpful Links For New Git Users

With the news today that Rails is moving from Subversion to Git, here are some helpful links for those of you that have never used Git before:

Feel free to add more in the comments and I'll update the list above as they come in.

Free Up Disk Space and Make Your Machine Go Faster With A Time Machine Restore

The hard drive on Macbook Pro died again the yesterday, so I had it in to my friendly neighborhood Apple Store to replace it once again.

Luckily, I was fully backed up through Time Machine this time, and when I got my machine back last night (note to self: always go to the flagship Apple store for repairs, they get your machine back to you much faster than other Apple stores) and started it up, one of the new options during setup was "Restore from Time Machine Backup". I chose that option, plugged in my disk, and when I woke up this morning my machine was back in exactly the same state that it was the day before.

That's not all though, it appears as one of the unexpected side effects of doing this is to clear out all of the temporary and log files that have built up over the last year or so. I went from 18 gigs of free space to over 30 gigs of free space just by going through the restore process.

So now I'm thinking that I might do just do a full reinstall of OS X and restore from Time Machine a couple of times a year to repeat this process. So far I haven't seen any downsides. Everything that I use in a normal day seems to be working perfectly (and actually, some things are going faster, I'm assuming because whatever trail of temp files they create over time has been cleaned up). If your machine is running slow or is low on space, doing a reinstall and a restore might be worth a try.

Culturally Sensitive JavaScript

JavaScript is a fantastic little language and with the likes of Prototype, Scriptaculous, and my newest favourite, lowpro, you can build some quite frankly, remarkable web applications.

One area where most web browsers fall down however is in their error-reporting, or lack thereof. A fact that has caused me to waste seemingly countless hours trying to find the source of some problem or other only to realise that a typo that had been staring me in the face the entire time was to blame!

Now, like just about any programming library I use these days, most JavaScript libraries use American english. initialize, capitalize, you know what I'm talking about.

For the most part the use of 'z' instead of 's' isn't too much of a problem but just recently I consistently tried to use lowpro's addBehaviour method, only there isn't one. It's called addBehavior (sans 'u').

So today after about 20 minutes cursing and swearing at the spelling Steve asked "is there anyway you could create an alias?" Being JavaScript the answer is of course "abso-bloody-lutely!":

Event.addBehaviour = Event.addBehavior

You can alias just about anything this way.

No more will my code silently fail due to differences in spelling :)

Running the Rspec spec task as the default task in Rails

Whenever I start a new Rails project, one of the first things I do is install Rspec and delete the Rails default test directory. Even with that directory missing, however, Rails continues to waste my time by trying to run its various testing tasks.

To make it stop, just add the following:

Rake::Task[:default].prerequisites.clear
task :default => :spec

to the bottom of the Rakefile in the root application directory.

Obviously, if you want to have a default task other than spec, just change the right side of the second line to whatever task you want it to run.

Selenium RC, Ruby, and Leopard == Pain

At CDD we use Selenium RC and spec_selenium to run our acceptance tests. Selenium is a slow way to test in general, but lately for us it's become excruciatingly painful.

For some some unknown reason under Leopard, our tests would seem to randomly slow to a crawl. Tests that usually take 4 seconds to run would suddenly be taking 170+ seconds.

Even worse, this would persist across reboots, and then suddenly go away while we were trying to diagnose the problem.

It was at the point this morning where we were using DTrace to try to figure out what was going on.

Luckily, it doesn't seem to have anything to do with our code, and it looks like I've solved it (or at least worked around it) for now.

Selenium RC communicates over network sockets. It appears that Ruby network communication performance under Leopard is, in a word, terrible.

This problem can be most easily seen when doing a 'gem update' and then waiting forever while the metadata updates.

There seems to be a problem with DNS resolution somewhere in the chain that pops up intermittently.

For now, our workaround is:

  1. Turn off IPv6
  2. Use OpenDNS for lookups.

as suggested by the Ruby Forum thread linked above. That seems to fix things on all of our machines here.

I would file a bug report for this, but I'm not sure if this is a Ruby thing or an OS X thing. If anyone can shed some further light on this issue it would be appreciated.