<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" xml:lang="en-US">
  <title>madhatted.com - Code</title>
  <id>tag:madhatted.com,2009:mephisto/</id>
  <generator uri="http://mephistoblog.com" version="0.8.0">Mephisto Drax</generator>
  
  <link href="http://madhatted.com/" rel="alternate" type="text/html" />
  <updated>2009-08-18T22:01:44Z</updated>
  <link rel="self" href="http://feeds.feedburner.com/madhatted" type="application/atom+xml" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2009-08-18:2795</id>
    <published>2009-08-18T21:53:00Z</published>
    <updated>2009-08-18T22:01:44Z</updated>
    <category term="performance" />
    <category term="Rails" />
    <category term="rails" />
    <category term="seed-fu" />
    <category term="web dev" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/U8Ru80bgM78/improving-seed-fu" rel="alternate" type="text/html" />
    <title>Improving Seed Fu</title>
<summary type="html">&lt;p&gt;Seed Fu is one of the most common bootstrapping solutions for Rails.  Bootstrapping is a technique for storing initial data with your application’s code.  Seed Fu let’s you have dedicated fixture files for development and production: Use less data for development mode, or create default test users.&lt;/p&gt;

&lt;p&gt;Bootstrapping a new developer database makes a great example.  After building their database, a new developer may need to create a user:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;$ script/runner User.create(:email =&amp;gt; 'me@mydomain.com', :password =&amp;gt; 'apass', :password_confirmation =&amp;gt; 'apass')&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Imagine you need to set up several users with settings and relationships, and this quickly becomes difficult to document.  Seed Fu has you build fixture files in &lt;code&gt;db/fixtures/&lt;/code&gt; or &lt;code&gt;db/fixtures/#{RAILS_ENV}&lt;/code&gt; that look like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="co"&gt;User&lt;/span&gt;.seed(&lt;span class="sy"&gt;:email&lt;/span&gt;) &lt;span class="r"&gt;do&lt;/span&gt; |s|&lt;tt&gt;
&lt;/tt&gt;  s.email    = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;me@mydomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  s.password = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;apass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  s.password_confirmation = s.password&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And makes it easy for a new developer (or production deployment) to bootstrap:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;$ rake db:seed&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The argument to &lt;code&gt;seed(:email)&lt;/code&gt; defines the columns checked before writing the row.  If there is already a row with the email address &lt;code&gt;me@mydomain.com&lt;/code&gt;, Seed Fu will update that row instead of inserting a new row.  This let’s you run seed to update a database that already has content.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;bad news&lt;/em&gt; is Seed Fu was &lt;strong&gt;terribly slow on large datafiles&lt;/strong&gt; and &lt;strong&gt;consumed RAM without freeing it&lt;/strong&gt;, which means it &lt;strong&gt;never completed seeds of many large fixtures&lt;/strong&gt;.  The &lt;em&gt;good news&lt;/em&gt; is I’ve got yer fix right here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://github.com/mixonic/seed-fu/tree"&gt;http://github.com/mixonic/seed-fu/tree&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s waiting on the maintainer proper to merge upstream (though I haven’t heard back yet).  Let’s see what changed.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;Seed Fu is one of the most common bootstrapping solutions for Rails.  Bootstrapping is a technique for storing initial data with your application’s code.  Seed Fu let’s you have dedicated fixture files for development and production: Use less data for development mode, or create default test users.&lt;/p&gt;

&lt;p&gt;Bootstrapping a new developer database makes a great example.  After building their database, a new developer may need to create a user:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;$ script/runner User.create(:email =&amp;gt; 'me@mydomain.com', :password =&amp;gt; 'apass', :password_confirmation =&amp;gt; 'apass')&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Imagine you need to set up several users with settings and relationships, and this quickly becomes difficult to document.  Seed Fu has you build fixture files in &lt;code&gt;db/fixtures/&lt;/code&gt; or &lt;code&gt;db/fixtures/#{RAILS_ENV}&lt;/code&gt; that look like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="co"&gt;User&lt;/span&gt;.seed(&lt;span class="sy"&gt;:email&lt;/span&gt;) &lt;span class="r"&gt;do&lt;/span&gt; |s|&lt;tt&gt;
&lt;/tt&gt;  s.email    = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;me@mydomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  s.password = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;apass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  s.password_confirmation = s.password&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And makes it easy for a new developer (or production deployment) to bootstrap:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;$ rake db:seed&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The argument to &lt;code&gt;seed(:email)&lt;/code&gt; defines the columns checked before writing the row.  If there is already a row with the email address &lt;code&gt;me@mydomain.com&lt;/code&gt;, Seed Fu will update that row instead of inserting a new row.  This let’s you run seed to update a database that already has content.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;bad news&lt;/em&gt; is Seed Fu was &lt;strong&gt;terribly slow on large datafiles&lt;/strong&gt; and &lt;strong&gt;consumed RAM without freeing it&lt;/strong&gt;, which means it &lt;strong&gt;never completed seeds of many large fixtures&lt;/strong&gt;.  The &lt;em&gt;good news&lt;/em&gt; is I’ve got yer fix right here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://github.com/mixonic/seed-fu/tree"&gt;http://github.com/mixonic/seed-fu/tree&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s waiting on the maintainer proper to merge upstream (though I haven’t heard back yet).  Let’s see what changed.&lt;/p&gt;
&lt;h1&gt;Go Faster&lt;/h1&gt;

&lt;p&gt;On my 2Ghz Core 2 Duo, 7200 rpm hard-drive, 2G ram laptop:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;real  114m1.482s&lt;tt&gt;
&lt;/tt&gt;user  75m53.490s&lt;tt&gt;
&lt;/tt&gt;sys  6m35.414s&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And on a production server:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;real  49m51.865s&lt;tt&gt;
&lt;/tt&gt;user  27m4.381s&lt;tt&gt;
&lt;/tt&gt;sys  1m6.247s&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;For importing &lt;strong&gt;1223431&lt;/strong&gt; rows into a truncated database…&lt;strong&gt;269&lt;/strong&gt; seeds a second on my laptop, &lt;strong&gt;753&lt;/strong&gt; seeds a second on production.  Seed Fu is still checking for existing records and using ActiveRecord to add seeds.  So what changed?&lt;/p&gt;

&lt;p&gt;The biggest change is &lt;strong&gt;dropping ActiveRecord validations&lt;/strong&gt;.  Validations are slow monsters.  The next logical step would be to stop using ActiveRecord all together, or at least toy with disabling callbacks, but that feels one step too far.  Disabling validations means &lt;strong&gt;keeping valid data in your seeds becomes your responsibility&lt;/strong&gt;.  It’s a trade-off, but worth it.&lt;/p&gt;

&lt;p&gt;Two smaller and 100% backwards compatible speed-ups are in &lt;a href="http://github.com/mixonic/seed-fu/commit/15c7b904e43b3aa448962321230cc180ad878c10"&gt;this commit&lt;/a&gt;.  The first walks the short constraints array instead of the longer data array when finding limiting conditions:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;     def condition_hash&lt;tt&gt;
&lt;/tt&gt;-      @data.reject{|a,v| !@constraints.include?(a)}&lt;tt&gt;
&lt;/tt&gt;+      @constraints.inject({}) {|a,c| a[c] = @data[c]; a }&lt;tt&gt;
&lt;/tt&gt;     end&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And the second avoids hitting method missing after the first call:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;-    def set_attribute(name, value)&lt;tt&gt;
&lt;/tt&gt;-      @data[name.to_sym] = value&lt;tt&gt;
&lt;/tt&gt;-    end&lt;tt&gt;
&lt;/tt&gt;-&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;     def method_missing(method_name, *args) #:nodoc:&lt;tt&gt;
&lt;/tt&gt;-      if (match = method_name.to_s.match(/(.*)=$/)) &amp;amp;&amp;amp; args.size == 1&lt;tt&gt;
&lt;/tt&gt;-        set_attribute(match[1], args.first)&lt;tt&gt;
&lt;/tt&gt;+      if args.size == 1 and (match = method_name.to_s.match(/(.*)=$/))&lt;tt&gt;
&lt;/tt&gt;+        self.class.class_eval &amp;quot;def #{method_name} arg; @data[:#{match[1]}] = arg; end&amp;quot;&lt;tt&gt;
&lt;/tt&gt;+        send(method_name, args[0])&lt;tt&gt;
&lt;/tt&gt;       else&lt;tt&gt;
&lt;/tt&gt;         super&lt;tt&gt;
&lt;/tt&gt;       end&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Method_missing is great for spreading some nice looking sugar around, but it was being hit several times for &lt;em&gt;each seed&lt;/em&gt;!  By creating a method and calling it directly later, we shave off more time.&lt;/p&gt;

&lt;h1&gt;Use Less IO, Memory&lt;/h1&gt;

&lt;p&gt;The seed file I used with a 1.2 million rows was 165M.  &lt;strong&gt;Gzipped it is 16M.&lt;/strong&gt;  That means Less IO for our slow disks, and fewer obnoxious files in source control.  Seed Fu now reads .rb.gz just like .rb files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seed Fu’s major failing point was that is grew to eat all RAM&lt;/strong&gt; when dealing with gigabytes or even megabytes of fixtures.  At one point, forking for each fixture looked like the only solution.  It was clumsy and not very elegant.&lt;/p&gt;

&lt;p&gt;Instead the better solution was to &lt;strong&gt;break up execution of the large seed files&lt;/strong&gt;.  Seed Fu reads a .rb.gz or .rb file into memory as a string.  If it hits:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="c"&gt;# BREAK EVAL&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;It evaluates everything it has just collected and starts again from after the comment.  The memory usage on the 1.2 million row import was about 60M of RAM (not unheard of for a Rails process), but it stayed there the whole import.&lt;/p&gt;

&lt;h1&gt;Add a Generator For Large Fixtures&lt;/h1&gt;

&lt;p&gt;165M fixture files are not being written by hand.  Chances are, if you run into issues with SeedFu and speed, you have data coming from a 3rd party.  To keep the bootstrapping for your app as easy as &lt;code&gt;rake db:seed&lt;/code&gt;, you need to create Seed Fu fixtures from XML, CSVs, Web Services, any kind of source.&lt;/p&gt;

&lt;p&gt;Say hello to &lt;code&gt;SeedFu::Writer&lt;/code&gt;!  Use the writer to generate large fixtures that take advantage of &lt;code&gt;# BREAL EVAL&lt;/code&gt; and the more concise &lt;code&gt;seed_many&lt;/code&gt; syntax.  Take a look:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;seed_writer = &lt;span class="co"&gt;SeedFu&lt;/span&gt;::&lt;span class="co"&gt;Writer&lt;/span&gt;::&lt;span class="co"&gt;SeedMany&lt;/span&gt;.new(&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="sy"&gt;:seed_file&lt;/span&gt;  =&amp;gt; &lt;span class="co"&gt;SEED_FILE&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="sy"&gt;:seed_model&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;City&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="sy"&gt;:seed_by&lt;/span&gt;    =&amp;gt; [ &lt;span class="sy"&gt;:city&lt;/span&gt;, &lt;span class="sy"&gt;:state&lt;/span&gt; ]&lt;tt&gt;
&lt;/tt&gt;)&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="co"&gt;FasterCSV&lt;/span&gt;.foreach( &lt;span class="co"&gt;CITY_CSV&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="sy"&gt;:return_headers&lt;/span&gt; =&amp;gt; &lt;span class="pc"&gt;false&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="sy"&gt;:headers&lt;/span&gt; =&amp;gt; &lt;span class="sy"&gt;:first_row&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;) &lt;span class="r"&gt;do&lt;/span&gt; |row|&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# Do some logic on row...&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# Write the seed&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;#&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  seed_writer.add_seed({&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="sy"&gt;:zip&lt;/span&gt; =&amp;gt; row[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;zipcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;],&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="sy"&gt;:state&lt;/span&gt; =&amp;gt; row[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;],&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="sy"&gt;:city&lt;/span&gt; =&amp;gt; row[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;],&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="sy"&gt;:latitude&lt;/span&gt; =&amp;gt; row[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;latitude&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;],&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="sy"&gt;:longitude&lt;/span&gt; =&amp;gt; row[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;longitude&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]&lt;tt&gt;
&lt;/tt&gt;  })&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;seed_writer.finish&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;See more detail at the bottom of &lt;a href="http://github.com/mixonic/seed-fu/tree/master"&gt;my Seed Fu fork’s github page&lt;/a&gt;.  &lt;code&gt;SeedFu::Writer::SeedMany&lt;/code&gt; takes several arguments upon initialization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:seed_file&lt;/code&gt; - Where to write output (probably &lt;code&gt;db/fixtures/my_fixture.rb&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:seed_model&lt;/code&gt; - Which model to seed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:seed_by&lt;/code&gt; - An array of which columns to constrain the seed by.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:quiet&lt;/code&gt; - Setting this to &lt;code&gt;true&lt;/code&gt; will quiet standard out.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:chunk_size&lt;/code&gt; - How many seeds to write before breaking evaluation. Default is 100.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And &lt;code&gt;SeedFu::Writer::Seed&lt;/code&gt; takes an additional argument to &lt;code&gt;add_seed&lt;/code&gt; to set the &lt;code&gt;:seed_by&lt;/code&gt; columns for that particular seed.&lt;/p&gt;

&lt;h1&gt;Seed Fu is Better, Now It’s Your Turn&lt;/h1&gt;

&lt;p&gt;This is a nice step for Seed Fu that begins to make it a real solution for 100s of megabytes of data. The writer gets us closer to a repeatable cycle for importing 3rd party data (just store your conversion scripts with the app code).&lt;/p&gt;

&lt;p&gt;Do you have an alternative plan for large data-sets in Rails?  What would you like to see Seed Fu do next?&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/U8Ru80bgM78" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2009/8/18/improving-seed-fu</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2009-08-12:2796</id>
    <published>2009-08-12T12:14:00Z</published>
    <updated>2009-08-12T12:27:52Z</updated>
    <category term="Rails" />
    <category term="rails" />
    <category term="speed" />
    <category term="web" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/heSb1AY0i2U/faster-sites-done-faster" rel="alternate" type="text/html" />
    <title>Faster Sites Done Faster</title>
<summary type="html">&lt;p&gt;&lt;a href="http://code.google.com/speed/"&gt;Google wants your site to be faster&lt;/a&gt;.  They want it &lt;em&gt;so much&lt;/em&gt;, they made videos!  &lt;strong&gt;You don’t want to watch videos though, you want to make your site faster, faster.&lt;/strong&gt;  Take an hour of time and make your website &lt;em&gt;4-5x faster&lt;/em&gt; using these 5 high-impact techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set HTTP Cache Headers&lt;/li&gt;
&lt;li&gt;Gzip Web Server Output&lt;/li&gt;
&lt;li&gt;Use Multiple, Cookie-less Domains for Assets&lt;/li&gt;
&lt;li&gt;Bundle Javascript &amp;amp; CSS&lt;/li&gt;
&lt;li&gt;Crush Image Assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m going to use &lt;a href="http://rubyonrails.org/"&gt;Rails&lt;/a&gt; for these examples, but you should have something comparable in any web framework.  Rails got a bad rap on speed for a long time, but where the server-side code failed, the architecture won.  HTTP and Rails are already in bed together, here’s how to join in.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;&lt;a href="http://code.google.com/speed/"&gt;Google wants your site to be faster&lt;/a&gt;.  They want it &lt;em&gt;so much&lt;/em&gt;, they made videos!  &lt;strong&gt;You don’t want to watch videos though, you want to make your site faster, faster.&lt;/strong&gt;  Take an hour of time and make your website &lt;em&gt;4-5x faster&lt;/em&gt; using these 5 high-impact techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set HTTP Cache Headers&lt;/li&gt;
&lt;li&gt;Gzip Web Server Output&lt;/li&gt;
&lt;li&gt;Use Multiple, Cookie-less Domains for Assets&lt;/li&gt;
&lt;li&gt;Bundle Javascript &amp;amp; CSS&lt;/li&gt;
&lt;li&gt;Crush Image Assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m going to use &lt;a href="http://rubyonrails.org/"&gt;Rails&lt;/a&gt; for these examples, but you should have something comparable in any web framework.  Rails got a bad rap on speed for a long time, but where the server-side code failed, the architecture won.  HTTP and Rails are already in bed together, here’s how to join in.&lt;/p&gt;
&lt;h1&gt;Set HTTP Cache Headers&lt;/h1&gt;

&lt;p&gt;HTTP is your friend.  Rails already handles adding timestamps to your assets URLs (that’s the &lt;code&gt;?19438273834&lt;/code&gt; after the image URI.  That’s why you always use image_tag), but you need to set up the HTTP headers yourself.  In Apache 2 this looks like:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;VirtualHost *:80&amp;gt;&lt;tt&gt;
&lt;/tt&gt;  # Your config...&lt;tt&gt;
&lt;/tt&gt;  ExpiresActive On&lt;tt&gt;
&lt;/tt&gt;  &amp;lt;FilesMatch &amp;quot;\.(ico|gif|jpe?g|png|js|css)$&amp;quot;&amp;gt;&lt;tt&gt;
&lt;/tt&gt;          ExpiresDefault &amp;quot;access plus 1 year&amp;quot;&lt;tt&gt;
&lt;/tt&gt;          Header unset ETag&lt;tt&gt;
&lt;/tt&gt;          FileETag None&lt;tt&gt;
&lt;/tt&gt;          Header unset Last-Modified&lt;tt&gt;
&lt;/tt&gt;  &amp;lt;/FilesMatch&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&amp;lt;/VirtualHost&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;In Nginx:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;server {&lt;tt&gt;
&lt;/tt&gt;  # Your config...&lt;tt&gt;
&lt;/tt&gt;  location ~* (css|js|png|jpe?g|gif|ico)$ {&lt;tt&gt;
&lt;/tt&gt;    expires max;&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;}&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;A warning to the wise- this works fairly seamlessly under Rails, but if you are on another framework be &lt;em&gt;sure&lt;/em&gt; you have some kind of rolling argument to assets for new deploys.  If not, the &lt;code&gt;expires max&lt;/code&gt; will mean browsers will not try to pull down the new content you just deployed.&lt;/p&gt;

&lt;h1&gt;Gzip Web Server Output&lt;/h1&gt;

&lt;p&gt;Web pages, Javascript, and CSS are all text, and compress very nicely under gzip.  This is a no-brainer to turn on, and it will have an immediate impact on your site. In Apache 2:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;VirtualHost *:80&amp;gt;&lt;tt&gt;
&lt;/tt&gt;  # Your config...&lt;tt&gt;
&lt;/tt&gt;  AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/javascript text/css application/x-javascript&lt;tt&gt;
&lt;/tt&gt;  BrowserMatch ^Mozilla/4 gzip-only-text/html&lt;tt&gt;
&lt;/tt&gt;  BrowserMatch ^Mozilla/4\.0[678] no-gzip&lt;tt&gt;
&lt;/tt&gt;  BrowserMatch \\bMSIE !no-gzip !gzip-only-text/html&lt;tt&gt;
&lt;/tt&gt;&amp;lt;/VirtualHost&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;In Nginx:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;server {&lt;tt&gt;
&lt;/tt&gt;  # Your config...&lt;tt&gt;
&lt;/tt&gt;  gzip             on;&lt;tt&gt;
&lt;/tt&gt;  gzip_min_length  1000;&lt;tt&gt;
&lt;/tt&gt;  gzip_proxied     expired no-cache no-store private auth;&lt;tt&gt;
&lt;/tt&gt;  gzip_types       text/plain application/xml text/css application/javascript;&lt;tt&gt;
&lt;/tt&gt;  gzip_disable     msie6;&lt;tt&gt;
&lt;/tt&gt;}&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Both of these configurations bypass IE6.  That’s a darn shame, if only we could make IE6 load pages faster somehow…liiike….&lt;/p&gt;

&lt;h1&gt;Use Multiple, Cookie-less Domains for Assets&lt;/h1&gt;

&lt;p&gt;This technique requires you to update your DNS entries, but it is &lt;em&gt;well&lt;/em&gt; worth the effort.  There are 2 Rails-side parts to this.  One is to always use &lt;code&gt;image_tag&lt;/code&gt;, &lt;code&gt;javascript_include_tag&lt;/code&gt;, etc.  Don’t write your own tags.  Then update your &lt;code&gt;asset_host&lt;/code&gt; configuration:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="c"&gt;# In config/environments/production.rb&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="co"&gt;ActionController&lt;/span&gt;::&lt;span class="co"&gt;Base&lt;/span&gt;.asset_host = &lt;span class="co"&gt;Proc&lt;/span&gt;.new { |source, request|&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;if&lt;/span&gt; request &lt;span class="r"&gt;and&lt;/span&gt; request.ssl?&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;https://www&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="c"&gt;# Just use one domain during SSL.  This avoids mixed content errors.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;else&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;http://www&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;source.hash % &lt;span class="i"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt; + &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;.coolapp.com&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;}&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Now configure A records for each asset domain:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;www0.coolsite.com 12.34.56.78&lt;tt&gt;
&lt;/tt&gt;www1.coolsite.com 12.34.56.78&lt;tt&gt;
&lt;/tt&gt;www2.coolsite.com 12.34.56.78&lt;tt&gt;
&lt;/tt&gt;www3.coolsite.com 12.34.56.78&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Lastly, configure your server.  The basic configuration (again, Apache 2):&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;VirtualHost *:80&amp;gt;&lt;tt&gt;
&lt;/tt&gt;  ServerName www.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www0.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www1.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www2.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www3.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  &lt;tt&gt;
&lt;/tt&gt;  # rewrite rules, etc&lt;tt&gt;
&lt;/tt&gt;&amp;lt;/VirtualHost *:80&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Of course, that leaves the whole app available on any of those domains.  We can use a require condition before routing to the app so only the assets are available:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;VirtualHost *:80&amp;gt;&lt;tt&gt;
&lt;/tt&gt;  ServerName www.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www0.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www1.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www2.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  ServerAlias www3.coolsite.com&lt;tt&gt;
&lt;/tt&gt;  &lt;tt&gt;
&lt;/tt&gt;  # other config...&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  # Redirect all non-static requests to cluster&lt;tt&gt;
&lt;/tt&gt;  RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f&lt;tt&gt;
&lt;/tt&gt;  RewriteCond %{HTTP_HOST} ^www.coolsite.com$ # Only if they are asking for www&lt;tt&gt;
&lt;/tt&gt;  RewriteRule ^/(.*)$ balancer://coolsite_cluster%{REQUEST_URI} [P,QSA,L]&lt;tt&gt;
&lt;/tt&gt;&amp;lt;/VirtualHost *:80&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Set up multiple asset hosts now!&lt;/strong&gt; If you have not done it, this is the most important technique on this page.  It will make your website &lt;strong&gt;3-4 times faster in most browsers&lt;/strong&gt;.  Yes, even Firefox will feel faster. &lt;strong&gt;Go, do it!&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;Bundle Javascript &amp;amp; CSS&lt;/h1&gt;

&lt;p&gt;Fewer downloads means less wait time for the browser.  Bundle your Javascript and CSS into a single file for production.  Super easy in Rails:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;&amp;lt;%=&lt;/span&gt; javascript_include_tag &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;mootools.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;lightbox.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;application.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="sy"&gt;:cache&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;cache-application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="dl"&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;&amp;lt;%=&lt;/span&gt; stylesheet_link_tag &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;lightbox.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;application.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="sy"&gt;:cache&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;cache-application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="dl"&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;h1&gt;Crush Image Assets&lt;/h1&gt;

&lt;p&gt;Ok, this is the &lt;strong&gt;hardest&lt;/strong&gt; thing on this list, and my example is the most Rails specific yet. You will need to install two applications: &lt;a href="http://en.wikipedia.org/wiki/Pngcrush"&gt;pngcrush&lt;/a&gt; and &lt;a href="http://en.wikipedia.org/wiki/Libjpeg"&gt;jpegtran&lt;/a&gt;.  Both were easily installable in Gentoo, so check Ports or Apt or what-have-you.  Now add this file to your project:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="c"&gt;# lib/tasks/assets.rake&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;namespace &lt;span class="sy"&gt;:assets&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  desc &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;Crush png images&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  task &lt;span class="sy"&gt;:crush_pngs&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="co"&gt;Dir&lt;/span&gt;[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;public/**/*.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;].each &lt;span class="r"&gt;do&lt;/span&gt; |file|&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sh"&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;span class="k"&gt;pngcrush -rem alla -reduce -brute &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;.crushing&amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sh"&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;span class="k"&gt;mv &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;.crushing&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;&amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  desc &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;Crush jp(e)g images&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  task &lt;span class="sy"&gt;:crush_jpgs&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    ( &lt;span class="co"&gt;Dir&lt;/span&gt;[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;public/**/*.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;] + &lt;span class="co"&gt;Dir&lt;/span&gt;[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;public/**/*.jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;] ).each &lt;span class="r"&gt;do&lt;/span&gt; |file|&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sh"&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;span class="k"&gt;jpegtran -copy none -optimize -perfect -outfile &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;.crushing&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;&amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sh"&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;span class="k"&gt;mv &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;.crushing&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;file&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;&amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;`&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  desc &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;Crush images&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  task &lt;span class="sy"&gt;:crush_images&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="s"&gt;&lt;span class="dl"&gt;%w(&lt;/span&gt;&lt;span class="k"&gt; assets:crush_pngs assets:crush_jpgs &lt;/span&gt;&lt;span class="dl"&gt;)&lt;/span&gt;&lt;/span&gt;.each &lt;span class="r"&gt;do&lt;/span&gt; |task|&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="co"&gt;Rake&lt;/span&gt;::&lt;span class="co"&gt;Task&lt;/span&gt;[task].invoke&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Now you can&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;rake assets:crush_images&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Review the changes, commit and push it live.  These utilities are loss-less;  They only strip fatty metadata not needed by browsers.  Run it every time you make a large number of image changes.&lt;/p&gt;

&lt;h1&gt;Be Fast &amp;amp; Prove It&lt;/h1&gt;

&lt;p&gt;Before you start using these techniques, check out &lt;a href="http://getfirebug.com/"&gt;Firebug&lt;/a&gt;, &lt;a href="http://developer.yahoo.com/yslow/"&gt;Yslow&lt;/a&gt;, and &lt;a href="http://code.google.com/speed/page-speed/"&gt;Google Page Speed&lt;/a&gt;.  Run the excellent IE focused &lt;a href="http://www.webpagetest.org/test"&gt;WebPagetest&lt;/a&gt;.  Gather some hard numbers before you make a change, then do some follow-up tests.  Be amazed.  Be fast.  &lt;strong&gt;Get back to building your great site.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do you have a dead simple, quick and high-impact web technique?  Share it with us!&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/heSb1AY0i2U" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2009/8/12/faster-sites-done-faster</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-07-10:2733</id>
    <published>2008-07-10T23:53:00Z</published>
    <updated>2008-07-10T23:55:58Z</updated>
    <link href="http://feedproxy.google.com/~r/madhatted/~3/jF7Px2fSt44/rspec-real-world-testing" rel="alternate" type="text/html" />
    <title>Rspec &amp; Real World Testing</title>
<summary type="html">&lt;p&gt;&lt;a href="http://rspec.info/"&gt;Rspec&lt;/a&gt; is a tasty testing suite for Rails.  It’s stubbing can be enhanced by using &lt;a href="http://mocha.rubyforge.org/"&gt;Mocha&lt;/a&gt;, a stubbing framework.  There is a &lt;a href="http://rspec.info/documentation/rails/"&gt;spattering&lt;/a&gt; &lt;a href="http://rspec.info/rdoc/"&gt;of&lt;/a&gt; &lt;a href="http://rspec.info/rdoc-rails/"&gt;documentation&lt;/a&gt; to get you started, but a few controller level items were challenging to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Included modules&lt;/li&gt;
&lt;li&gt;Filtered parameters&lt;/li&gt;
&lt;li&gt;Before filters&lt;/li&gt;
&lt;li&gt;Response codes&lt;/li&gt;
&lt;li&gt;Facebook redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I had to pick up a few model testing tricks too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Association testing&lt;/li&gt;
&lt;li&gt;ActionMailer testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s look at some good approaches for each of these.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;&lt;a href="http://rspec.info/"&gt;Rspec&lt;/a&gt; is a tasty testing suite for Rails.  It’s stubbing can be enhanced by using &lt;a href="http://mocha.rubyforge.org/"&gt;Mocha&lt;/a&gt;, a stubbing framework.  There is a &lt;a href="http://rspec.info/documentation/rails/"&gt;spattering&lt;/a&gt; &lt;a href="http://rspec.info/rdoc/"&gt;of&lt;/a&gt; &lt;a href="http://rspec.info/rdoc-rails/"&gt;documentation&lt;/a&gt; to get you started, but a few controller level items were challenging to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Included modules&lt;/li&gt;
&lt;li&gt;Filtered parameters&lt;/li&gt;
&lt;li&gt;Before filters&lt;/li&gt;
&lt;li&gt;Response codes&lt;/li&gt;
&lt;li&gt;Facebook redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I had to pick up a few model testing tricks too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Association testing&lt;/li&gt;
&lt;li&gt;ActionMailer testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s look at some good approaches for each of these.&lt;/p&gt;
&lt;h1&gt;Testing Tricks for Rails Controllers&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Included modules&lt;/strong&gt;&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should include AuthenticatedSystem&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    controller.class.included_modules.should include(&lt;span class="co"&gt;AuthenticatedSystem&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Filtered parameters&lt;/strong&gt; - This basically runs the filtering code over some parameters, then we test the output.  Not the most ideal way, but the best I’ve come up with.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should filter credit_cards&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    controller.send(&lt;span class="sy"&gt;:filter_parameters&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;credit_card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;nogood&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)\&lt;tt&gt;
&lt;/tt&gt;      [&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;credit_card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;].should == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;[FILTERED]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Before filters&lt;/strong&gt;&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should have a before_filter for login_required&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    controller.class.before_filters.should include( &lt;span class="sy"&gt;:login_required&lt;/span&gt; )&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Response codes&lt;/strong&gt; - This is not hard, but I always seem to forget.  Test the code, not the status message:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should return 200 success&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    response.code.should == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Facebook redirects&lt;/strong&gt; - Diving into &lt;a href="http://facebooker.rubyforge.org/"&gt;Facebooker&lt;/a&gt; and the &lt;a href="http://developers.facebook.com/"&gt;Facebook platform&lt;/a&gt; has been an…uh…engaging experience.  One thing that took me a while to realize: Facebooker alters redirect_to to use facebook’s own redirection tag.  Don’t test them like normal redirects.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;redirects on facebook based signup&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    controller.stubs(&lt;span class="sy"&gt;:request_is_for_a_facebook_canvas?&lt;/span&gt;).returns(&lt;span class="pc"&gt;true&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;    create_user&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# Test against the body.  Maybe this could even use has_tag.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    response.body.should =~ &lt;span class="rx"&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="k"&gt;&amp;lt;fb:redirect url=&amp;quot;&lt;/span&gt;&lt;span class="ch"&gt;\/&lt;/span&gt;&lt;span class="k"&gt;home&amp;quot; &lt;/span&gt;&lt;span class="ch"&gt;\/&lt;/span&gt;&lt;span class="k"&gt;&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;h1&gt;Testing Tricks for Rails Models&lt;/h1&gt;

&lt;p&gt;Model testing is really very straight ahead once you start feeling comfortable.  Some strange points for me were around model associations and ActionMailer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model associations&lt;/strong&gt; - The technique I use is a mashup of &lt;a href="http://www.blognow.com.au/q/67540/Reflect_on_association_one_liner_to_check_association_details.html"&gt;association reflection to_hash&lt;/a&gt; and a &lt;a href="http://snippets.dzone.com/posts/show/4706"&gt;deep_merge&lt;/a&gt; method for ruby.  deep_merge in general is pretty convenient when testing.  Add this to spec/spec_helper.rb:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;45&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;53&lt;tt&gt;
&lt;/tt&gt;54&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;55&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;56&lt;tt&gt;
&lt;/tt&gt;57&lt;tt&gt;
&lt;/tt&gt;58&lt;tt&gt;
&lt;/tt&gt;59&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;61&lt;tt&gt;
&lt;/tt&gt;62&lt;tt&gt;
&lt;/tt&gt;63&lt;tt&gt;
&lt;/tt&gt;64&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;65&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;66&lt;tt&gt;
&lt;/tt&gt;67&lt;tt&gt;
&lt;/tt&gt;68&lt;tt&gt;
&lt;/tt&gt;69&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;71&lt;tt&gt;
&lt;/tt&gt;72&lt;tt&gt;
&lt;/tt&gt;73&lt;tt&gt;
&lt;/tt&gt;74&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;75&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;76&lt;tt&gt;
&lt;/tt&gt;77&lt;tt&gt;
&lt;/tt&gt;78&lt;tt&gt;
&lt;/tt&gt;79&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;80&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;81&lt;tt&gt;
&lt;/tt&gt;82&lt;tt&gt;
&lt;/tt&gt;83&lt;tt&gt;
&lt;/tt&gt;84&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;85&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;86&lt;tt&gt;
&lt;/tt&gt;87&lt;tt&gt;
&lt;/tt&gt;88&lt;tt&gt;
&lt;/tt&gt;89&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;90&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;91&lt;tt&gt;
&lt;/tt&gt;92&lt;tt&gt;
&lt;/tt&gt;93&lt;tt&gt;
&lt;/tt&gt;94&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;95&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;96&lt;tt&gt;
&lt;/tt&gt;97&lt;tt&gt;
&lt;/tt&gt;98&lt;tt&gt;
&lt;/tt&gt;99&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="c"&gt;# Associations to hash&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;module&lt;/span&gt; &lt;span class="cl"&gt;ActiveRecord&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;module&lt;/span&gt; &lt;span class="cl"&gt;Reflection&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;AssociationReflection&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;to_hash&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;        {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="sy"&gt;:macro&lt;/span&gt; =&amp;gt; &lt;span class="iv"&gt;@macro&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="sy"&gt;:options&lt;/span&gt; =&amp;gt; &lt;span class="iv"&gt;@options&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="sy"&gt;:class_name&lt;/span&gt; =&amp;gt; &lt;span class="iv"&gt;@class_name&lt;/span&gt; || &lt;span class="iv"&gt;@name&lt;/span&gt;.to_s.singularize.camelize&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;# Hash#deep_merge&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;# From: http://pastie.textmate.org/pastes/30372, Elliott Hird&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;# Source: http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;# This file contains extensions to Ruby and other useful snippits of code.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;# Time to extend Hash with some recursive merging magic.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;Hash&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# Merges self with another hash, recursively.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# &lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# This code was lovingly stolen from some random gem:&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# &lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# Thanks to whoever made it.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;deep_merge&lt;/span&gt;(hash)&lt;tt&gt;
&lt;/tt&gt;    target = dup&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    hash.keys.each &lt;span class="r"&gt;do&lt;/span&gt; |key|&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; hash[key].is_a? &lt;span class="co"&gt;Hash&lt;/span&gt; &lt;span class="r"&gt;and&lt;/span&gt; &lt;span class="pc"&gt;self&lt;/span&gt;[key].is_a? &lt;span class="co"&gt;Hash&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;        target[key] = target[key].deep_merge(hash[key])&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;next&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;      target[key] = hash[key]&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    target&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# From: http://www.gemtacular.com/gemdocs/cerberus-0.2.2/doc/classes/Hash.html&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# File lib/cerberus/utils.rb, line 42&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;deep_merge!&lt;/span&gt;(second)&lt;tt&gt;
&lt;/tt&gt;    second.each_pair &lt;span class="r"&gt;do&lt;/span&gt; |k,v|&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="pc"&gt;self&lt;/span&gt;[k].is_a?(&lt;span class="co"&gt;Hash&lt;/span&gt;) &lt;span class="r"&gt;and&lt;/span&gt; second[k].is_a?(&lt;span class="co"&gt;Hash&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;self&lt;/span&gt;[k].deep_merge!(second[k])&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;else&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;self&lt;/span&gt;[k] = second[k]&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;#-----------------&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="c"&gt;# cf. http://subtech.g.hatena.ne.jp/cho45/20061122&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;deep_merge2&lt;/span&gt;(other)&lt;tt&gt;
&lt;/tt&gt;      deep_proc = &lt;span class="co"&gt;Proc&lt;/span&gt;.new { |k, s, o|&lt;tt&gt;
&lt;/tt&gt;         &lt;span class="r"&gt;if&lt;/span&gt; s.kind_of?(&lt;span class="co"&gt;Hash&lt;/span&gt;) &amp;amp;&amp;amp; o.kind_of?(&lt;span class="co"&gt;Hash&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;next&lt;/span&gt; s.merge(o, &amp;amp;deep_proc)&lt;tt&gt;
&lt;/tt&gt;         &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;         &lt;span class="r"&gt;next&lt;/span&gt; o&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      merge(other, &amp;amp;deep_proc)&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;deep_merge3&lt;/span&gt;(second)&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;# From: http://www.ruby-forum.com/topic/142809&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;# Author: Stefan Rusterholz&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;      merger = proc { |key,v1,v2| &lt;span class="co"&gt;Hash&lt;/span&gt; === v1 &amp;amp;&amp;amp; &lt;span class="co"&gt;Hash&lt;/span&gt; === v2 ? v1.merge(v2, &amp;amp;merger) : v2 }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;self&lt;/span&gt;.merge(second, &amp;amp;merger)&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;keep_merge&lt;/span&gt;(hash)&lt;tt&gt;
&lt;/tt&gt;      target = dup&lt;tt&gt;
&lt;/tt&gt;      hash.keys.each &lt;span class="r"&gt;do&lt;/span&gt; |key|&lt;tt&gt;
&lt;/tt&gt;         &lt;span class="r"&gt;if&lt;/span&gt; hash[key].is_a? &lt;span class="co"&gt;Hash&lt;/span&gt; &lt;span class="r"&gt;and&lt;/span&gt; &lt;span class="pc"&gt;self&lt;/span&gt;[key].is_a? &lt;span class="co"&gt;Hash&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;            target[key] = target[key].keep_merge(hash[key])&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;next&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;         &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;         &lt;span class="c"&gt;#target[key] = hash[key]&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;         target.update(hash) { |key, *values| values.flatten.uniq }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      target&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That creates both to_hash and deep_merge.  The tests look like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should belong to an owner, which is a User&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assoc = &lt;span class="co"&gt;Home&lt;/span&gt;.reflect_on_association(&lt;span class="sy"&gt;:owner&lt;/span&gt;).to_hash&lt;tt&gt;
&lt;/tt&gt;    assoc.deep_merge({&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sy"&gt;:macro&lt;/span&gt; =&amp;gt; &lt;span class="sy"&gt;:belongs_to&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sy"&gt;:options&lt;/span&gt; =&amp;gt; { &lt;span class="sy"&gt;:class_name&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; }&lt;tt&gt;
&lt;/tt&gt;    }).should == assoc&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should have many tree_types, through trees&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assoc = &lt;span class="co"&gt;Home&lt;/span&gt;.reflect_on_association(&lt;span class="sy"&gt;:tree_types&lt;/span&gt;).to_hash&lt;tt&gt;
&lt;/tt&gt;    assoc.deep_merge({&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sy"&gt;:macro&lt;/span&gt; =&amp;gt; &lt;span class="sy"&gt;:has_many&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="sy"&gt;:options&lt;/span&gt; =&amp;gt; { &lt;span class="sy"&gt;:through&lt;/span&gt; =&amp;gt; &lt;span class="sy"&gt;:trees&lt;/span&gt; }&lt;tt&gt;
&lt;/tt&gt;    }).should == assoc&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This allows for very exact tests on model associations, and useful error messages when they fail.  It’s a little too verbose, and there may be something on github that has already moved down this line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ActionMailer&lt;/strong&gt; - Definitely test your models separate from your notifier. For instance, test an email is sent upon User creation:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should send an email on user creation&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="co"&gt;UserNotifier&lt;/span&gt;.expects(&lt;span class="sy"&gt;:deliver_new_user_notification&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="co"&gt;User&lt;/span&gt;.create( &lt;span class="iv"&gt;@valid_user_params&lt;/span&gt; )&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That’s all that’s needed.  None of that flushing the unsent email cache you might have seen around the web googling for this.  Now test the UserNotifier:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;shared_examples_for &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;mysite.com email&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should have a prefix on the subject&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@email&lt;/span&gt;.subject.should =~ &lt;span class="rx"&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="k"&gt;[My Site] &lt;/span&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should be from noreply&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@email&lt;/span&gt;.from.should == [&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;noreply@mysite.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should be multi-part&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@email&lt;/span&gt;.parts.length.should be(&lt;span class="i"&gt;2&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;describe &lt;span class="co"&gt;UserNotifier&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  describe &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;when sending a new user e-mail&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    before(&lt;span class="sy"&gt;:each&lt;/span&gt;) &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="iv"&gt;@user&lt;/span&gt; = &lt;span class="co"&gt;User&lt;/span&gt;.create( &lt;span class="iv"&gt;@valid_user_params&lt;/span&gt; )&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="iv"&gt;@email&lt;/span&gt; = &lt;span class="co"&gt;UserNotifier&lt;/span&gt;.create_forgot_password(&lt;span class="iv"&gt;@user&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    it_should_behave_like &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;mysite.com email&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should be sent to the user's email address&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="iv"&gt;@email&lt;/span&gt;.to.should == [&lt;span class="iv"&gt;@user&lt;/span&gt;.email]&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    it &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;should contain the activation_code&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="iv"&gt;@email&lt;/span&gt;.body.should =~ &lt;span class="rx"&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;&lt;span class="iv"&gt;@user&lt;/span&gt;.activation_code&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Oh slick.  Of course you should flavour to taste, but these tricks are what got me rolling.  I hope to post some Facebooker testing tricks as soon as I have some important things like notifications figured nicely out.  What did you have to figure out?&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/jF7Px2fSt44" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/7/10/rspec-real-world-testing</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-06-25:2726</id>
    <published>2008-06-25T12:45:00Z</published>
    <updated>2008-06-25T12:46:56Z</updated>
    <link href="http://feedproxy.google.com/~r/madhatted/~3/RCgIwONKqrw/mootools-and-rails-csrf-protection" rel="alternate" type="text/html" />
    <title>MooTools and Rails CSRF Protection</title>
<content type="html">
            &lt;p&gt;“Cross site request forgery” is also known as CSRF, XSRF or just request forgery (more &lt;a href="http://en.wikipedia.org/wiki/Cross-site_request_forgery"&gt;at wikipedia&lt;/a&gt; and &lt;a href="http://isc.sans.org/diary.html?storyid=1750"&gt;sans.org&lt;/a&gt;).  It’s a method of attack toward web applications- Rails 2.0 introduced &lt;a href="http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application"&gt;a defence&lt;/a&gt; and Rails 2.1 &lt;a href="http://ryandaigle.com/articles/2007/9/24/what-s-new-in-edge-rails-better-cross-site-request-forging-prevention"&gt;enabled that defence by default&lt;/a&gt;.  Call form_for…&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;% form_for &lt;span class="iv"&gt;@friend&lt;/span&gt;, &lt;span class="sy"&gt;:action&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;do&lt;/span&gt; |f| &lt;span class="s"&gt;&lt;span class="dl"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;and Rails spits out more than just the form, it also generates a secret authenticity_token:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="ta"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="an"&gt;action&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;/friends&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;method&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="an"&gt;style&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;margin:0;padding:0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="an"&gt;name&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;authenticity_token&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;type&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;value&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;e8c827c47577e013cc4c06a99cab63da95b71915&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="ta"&gt;/&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;AJAX submission of this particular form will include the authenticity token.  &lt;em&gt;So here’s the rub:&lt;/em&gt; what if the form is generated by something else?&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new Element(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, { &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;/friends&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Or what if &lt;strong&gt;&lt;em&gt;there is no form?!&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new Request.Json( ... ).post();&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;In these cases Rails would raise an ActionController::InvalidAuthenticityToken exception.  To avoid an exception the CSRF check can be altogether disabled for a controller:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;skip_before_filter &lt;span class="sy"&gt;:verify_authenticity_token&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Of course this is a compromise between convenience and security.  For those who want security and AJAXy goodness (well, with MooTools at least), &lt;em&gt;there is a better way&lt;/em&gt;.&lt;/p&gt;

&lt;h1&gt;Using AuthenticityToken with MooTools&lt;/h1&gt;

&lt;p&gt;MooTools has a wonderful object oriented codebase.  First Rails needs to pass the authenticity_token into Javascript, then we can use the inheritance from MooTools to pass the authenticity_token with every request.&lt;/p&gt;

&lt;p&gt;The authenticity_token can be passed in the header of the Rails application layout.  It’s just one line:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;%= javascript_tag &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;const AUTH_TOKEN = #{form_authenticity_token.inspect};&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="r"&gt;if&lt;/span&gt; protect_against_forgery? %&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And now pass that token with every POST request (GET requests don’t check CSRF):&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="c"&gt;// Include authenticity_token in Request.JSON fires&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// move the send function&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;Request.prototype._send = Request.prototype.&lt;span class="fu"&gt;send&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;Request.implement({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  auth_token: &lt;span class="r"&gt;function&lt;/span&gt;(){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; AUTH_TOKEN;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;// This is more verbose than is ideal, but I don't see a better place&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;// to hook this functionality in&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="fu"&gt;send&lt;/span&gt;: &lt;span class="r"&gt;function&lt;/span&gt;(options){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; &lt;span class="r"&gt;type&lt;/span&gt; = $&lt;span class="r"&gt;type&lt;/span&gt;(options);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="r"&gt;type&lt;/span&gt; == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; || &lt;span class="r"&gt;type&lt;/span&gt; == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;) options = {data: options};&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; old = &lt;span class="pc"&gt;this&lt;/span&gt;.options;&lt;tt&gt;
&lt;/tt&gt;    options = $extend({data: old.data, url: old.url, method: old.method}, options);&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;switch&lt;/span&gt; ($&lt;span class="r"&gt;type&lt;/span&gt;(options.data)){&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;case&lt;/span&gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: options.data = $(options.data).toQueryString(); &lt;span class="r"&gt;break&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;case&lt;/span&gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: &lt;span class="r"&gt;case&lt;/span&gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: options.data = Hash.toQueryString(options.data);&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;// If this isn't a get request add the authenticity_token&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (options.method != &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; || options.method != &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;      options.data = options.data+&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;&amp;amp;authenticity_token=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;+&lt;span class="pc"&gt;this&lt;/span&gt;.auth_token();&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;// Call the original send&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;._send(options)&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Look at that, now every AJAX call from MooTools will attach our CSRF.  All the ease of normal javascript, all the security of the native Rails defences.&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/RCgIwONKqrw" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/6/25/mootools-and-rails-csrf-protection</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-06-20:2721</id>
    <published>2008-06-20T15:27:00Z</published>
    <updated>2008-07-24T20:01:58Z</updated>
    <link href="http://feedproxy.google.com/~r/madhatted/~3/weGDL4_s0Ek/the-joy-of-tables-on-cows" rel="alternate" type="text/html" />
    <title>The Joy of Tables On Cows</title>
<summary type="html">&lt;p&gt;On June 12th &lt;a href="http://blog.mootools.net/2008/6/12/mootools-1-2-it-s-official"&gt;MooTools 1.2 was released&lt;/a&gt;, to great rejoicing.  It’s a release that really sets MooTools apart with better Fx, more browser compatibility effort, and a jaw dropping &lt;a href="http://blog.mootools.net/2008/1/22/Element_Storage"&gt;element storage&lt;/a&gt; feature.  That means it’s time to update the table sort script I’ve blogged here before (&lt;a href="http://madhatted.com/2008/1/11/the-joy-of-a-minimal-complete-javascript-table-sort"&gt;The Joy of a Minimal, Complete Javascript Table Sort&lt;/a&gt; and &lt;a href="http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort"&gt;The Joy of an Optimized, Complete Javascript Table Sort&lt;/a&gt;).  This release does more than just port, and also adds a few features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sorts more out of the box:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;strings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;numbers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;decimal currency&lt;/strong&gt; (12.34, 4.50)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dates&lt;/strong&gt; (YYYY-MM-DD, YYYY-M-D)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;relative dates&lt;/strong&gt; (1 day ago, 38 years ago)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;disk memory&lt;/strong&gt; (1.75 MB, 34 KB, 8 TB)&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Passes the matcher into the conversion_function for re-use.&lt;/li&gt;
&lt;li&gt;Classes set for forward and reverse sorting th tags.&lt;/li&gt;
&lt;li&gt;A “don’t sort” class.&lt;/li&gt;
&lt;li&gt;It’s &lt;a href="http://github.com/mixonic/tables_on_cows"&gt;on github&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;Integration with the &lt;strong&gt;brand new pagination library&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oh yeah, and that.  There’s now a pagination library supporting all the same things like expanding rows.  It’ll do multiple pagination link areas and drop offset and cut-off numbers into the DOM.  Let’s take a look.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;On June 12th &lt;a href="http://blog.mootools.net/2008/6/12/mootools-1-2-it-s-official"&gt;MooTools 1.2 was released&lt;/a&gt;, to great rejoicing.  It’s a release that really sets MooTools apart with better Fx, more browser compatibility effort, and a jaw dropping &lt;a href="http://blog.mootools.net/2008/1/22/Element_Storage"&gt;element storage&lt;/a&gt; feature.  That means it’s time to update the table sort script I’ve blogged here before (&lt;a href="http://madhatted.com/2008/1/11/the-joy-of-a-minimal-complete-javascript-table-sort"&gt;The Joy of a Minimal, Complete Javascript Table Sort&lt;/a&gt; and &lt;a href="http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort"&gt;The Joy of an Optimized, Complete Javascript Table Sort&lt;/a&gt;).  This release does more than just port, and also adds a few features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sorts more out of the box:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;strings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;numbers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;decimal currency&lt;/strong&gt; (12.34, 4.50)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dates&lt;/strong&gt; (YYYY-MM-DD, YYYY-M-D)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;relative dates&lt;/strong&gt; (1 day ago, 38 years ago)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;disk memory&lt;/strong&gt; (1.75 MB, 34 KB, 8 TB)&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Passes the matcher into the conversion_function for re-use.&lt;/li&gt;
&lt;li&gt;Classes set for forward and reverse sorting th tags.&lt;/li&gt;
&lt;li&gt;A “don’t sort” class.&lt;/li&gt;
&lt;li&gt;It’s &lt;a href="http://github.com/mixonic/tables_on_cows"&gt;on github&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;Integration with the &lt;strong&gt;brand new pagination library&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oh yeah, and that.  There’s now a pagination library supporting all the same things like expanding rows.  It’ll do multiple pagination link areas and drop offset and cut-off numbers into the DOM.  Let’s take a look.&lt;/p&gt;
&lt;h1&gt;The Old: Reviewing SortingTable&lt;/h1&gt;

&lt;p&gt;SortingTable doesn’t need any wonky DOM rewrite, that’s one of it’s best perks.  Just use a &lt;em&gt;thead&lt;/em&gt; and &lt;em&gt;tbody&lt;/em&gt;, and make proper use of th.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="ta"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="an"&gt;cellpadding&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;cellspacing&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;a header&lt;span class="ta"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;a value&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;another value&lt;span class="ta"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;To sort this table you could use the most simplistic of initializations:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;These are the default settings that are assumed:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, {&lt;tt&gt;
&lt;/tt&gt;  zebra: &lt;span class="pc"&gt;true&lt;/span&gt;,                        &lt;span class="c"&gt;// Stripe the table, also on initialize&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  paginator: &lt;span class="pc"&gt;false&lt;/span&gt;,                   &lt;span class="c"&gt;// Pass a paginator object&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  dont_sort_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;nosort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;,          &lt;span class="c"&gt;// Class name on th's that don't sort&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  forward_sort_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;forward_sort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="c"&gt;// Class applied to forward sort th's&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  reverse_sort_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;reverse_sort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;  &lt;span class="c"&gt;// Class applied to reverse sort th's&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;So if you wanted to not sort a given column, just add &lt;em&gt;class=”nosort”&lt;/em&gt; and it’ll be ignored.  You could also change that to any other class.  The same goes for the classes &lt;em&gt;forward_sort&lt;/em&gt; and &lt;em&gt;reverse_sort&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The passing of conversion_matcher into the conversion_function makes it more DRY to use regex on a td for sort.  Take a look at the date sorter:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;      &lt;span class="c"&gt;// YYYY-MM-DD, YYYY-m-d&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /(\d{4})-(\d{1,2})-(\d{1,2})/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).get(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;          cell = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_matcher.&lt;span class="fu"&gt;exec&lt;/span&gt;( cell );&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; new &lt;span class="pt"&gt;Date&lt;/span&gt;(&lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[1]), &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[2], 10) - 1, &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[3], 10));&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;We get to re-use that regex right in the conversion_function.&lt;/p&gt;

&lt;p&gt;That’s it for incremental changes to SortingTable.  PaginatingTable is the thing to introduce.&lt;/p&gt;

&lt;h1&gt;The New: Introducing PaginatingTable&lt;/h1&gt;

&lt;p&gt;To paginate a table you’ll need to add one DOM element:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="ta"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;sort_table_pagination&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And then paginating the simple table provided above would look like:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new PaginatingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table_pagination&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That’s the simplest way to go.  A more complex pagination might involve two paginators as well as displaying offsets and cutoffs.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="ta"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;sort_table_pagination&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;Now showing items &lt;span class="ta"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;offset&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt; - &lt;span class="ta"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;cutoff&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="an"&gt;cellpadding&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;cellspacing&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;a header&lt;span class="ta"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;a value&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;another value&lt;span class="ta"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;sort_table_bottom_pagination&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;To start this up:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new PaginatingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, [&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table_pagination&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table_bottom_pagination&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;], {&lt;tt&gt;
&lt;/tt&gt;  per_page: 3,         &lt;span class="c"&gt;// Only 3 items per page, please&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  current_page: 2,     &lt;span class="c"&gt;// Start on page 2&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  offset_el: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;offset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="c"&gt;// Use this id for offset numbers&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  cutoff_el: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;cutoff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;  &lt;span class="c"&gt;// And this id for cutoffs&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Swank.  This will paginate, but you don’t get any sorting or zebra striping.  That’s still tied to SortingTable, and you’ll need to use them together.&lt;/p&gt;

&lt;p&gt;PaginatingTable also provides &lt;strong&gt;update_pages&lt;/strong&gt; on a given instance- so if you add rows to a table you can have the pagination update itself.&lt;/p&gt;

&lt;h1&gt;Will It Blend? SortingTable and PaginatingTable Together&lt;/h1&gt;

&lt;p&gt;SortingTable will accept a PaginatingTable object and use it to reset to the first page when you sort.  Just pass it in:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, {&lt;tt&gt;
&lt;/tt&gt;  paginator: new PaginatingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table_pagination&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; )&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;If you have expanding rows, you’ll need to tell both SortingTable and PaginatingTable about it:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, {&lt;tt&gt;
&lt;/tt&gt;  details: &lt;span class="pc"&gt;true&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  paginator: new PaginatingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_table_pagination&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, { details: &lt;span class="pc"&gt;true&lt;/span&gt; } )&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;You can check out the &lt;a href="http://madhatted.com/assets/2008/7/24/examples.html"&gt;example&lt;/a&gt; to see this in action, and get the &lt;a href="http://github.com/mixonic/tables_on_cows"&gt;javascript from github&lt;/a&gt;.  A big thanks to all the people that wrote conversion functions and made tweaks to this script, and kept it crawling from an example into a more capable library.  Enjoy!&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/weGDL4_s0Ek" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-06-18:2719</id>
    <published>2008-06-18T14:27:00Z</published>
    <updated>2008-06-18T14:30:00Z</updated>
    <link href="http://feedproxy.google.com/~r/madhatted/~3/VWMUbjIRxf4/somone-at-mozilla-is-getting-too-cute" rel="alternate" type="text/html" />
    <title>Someone At Mozilla Is Getting Too Cute</title>
<content type="html">
            &lt;p&gt;Ok, so I load up Firefox 3 and go to change some settings, you know, try reverting all the tweaks I’ve made to Firefox 2.  I enter the incantation “about:config” into the URL bar and get this:&lt;/p&gt;

&lt;p&gt;&lt;img src="http://madhatted.com/assets/2008/6/18/what_warranty.jpg" alt="What Warranty?]" /&gt;&lt;/p&gt;

&lt;p&gt;What?  &lt;strong&gt;&lt;em&gt;Void my warranty?&lt;/strong&gt;&lt;/em&gt;  I get a warranty with this open source software? That’s a new one to me.&lt;/p&gt;

&lt;p&gt;&lt;img src="http://madhatted.com/assets/2008/6/18/no_warranty.jpg" alt="No Warranty" /&gt;&lt;/p&gt;

&lt;p&gt;Oh wait, yeah, &lt;strong&gt;I don’t&lt;/strong&gt;.  Please Mozilla, stick with the warnings about understanding the settings and don’t use inappropriate legalese.  The whole “void your warranty” phrase is the fear-mongering motto of anti reverse engineering corporates everywhere.  Firefox is open source, it should embrace everything the opposite, encourage me to explore and experiement.&lt;/p&gt;

&lt;p&gt;To top it off, making me click on &lt;strong&gt;&lt;em&gt;I’ll be careful, I promise!&lt;/strong&gt;&lt;/em&gt; is just a little too cute and condescending.  I’m feeling a lack of UI polish so far in Firefox 3.  I’ll cover a few other things after I organize my thoughts, but this was just too good to wait on.&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/VWMUbjIRxf4" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/6/18/somone-at-mozilla-is-getting-too-cute</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-04-25:2711</id>
    <published>2008-04-25T19:21:00Z</published>
    <updated>2008-04-25T19:23:34Z</updated>
    <link href="http://feedproxy.google.com/~r/madhatted/~3/R6-WXIVwvnQ/missing-mephisto-upgrade-guide-0-7-3-to-0-8" rel="alternate" type="text/html" />
    <title>Missing Mephisto Upgrade Guide: 0.7.3 to 0.8</title>
<summary type="html">&lt;p&gt;My Mephisto install was 0.7.3- 0.8 doesn’t have any huge feature changes but there are some bugfixes and small enhancements, as well as a performance boost (&lt;a href="http://izumi.plan99.net/blog/index.php/2008/03/18/performance-comparison-rails-126-vs-202/"&gt;speed&lt;/a&gt;, &lt;a href="http://izumi.plan99.net/blog/index.php/2008/03/19/memory-usage-comparison-rails-126-vs-202/"&gt;memory use&lt;/a&gt;) from using rails 2.0.  My install was from a tarball, and I don’t want to &lt;a href="http://mephistoblog.com/download"&gt;move my production server to a git checkout&lt;/a&gt; (yet?).  Like other Mephisto users I’ve got theme customizations and maybe even some things in the public directory.&lt;/p&gt;

&lt;p&gt;I need to upgrade, not reinstall.  The Mephisto folks didn’t write a real upgrade guide, but this is Rails and it’s not too hard to figure out.  Almost any rails app could be updated this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Back Everything Up&lt;/li&gt;
&lt;li&gt;Create &amp;amp; Apply A Patch&lt;/li&gt;
&lt;li&gt;Stop Your Server &amp;amp; Clean Up&lt;/li&gt;
&lt;li&gt;Patch Code &amp;amp; Migrate The Database&lt;/li&gt;
&lt;li&gt;Crack Open A Beer&lt;/li&gt;
&lt;/ul&gt;</summary><content type="html">
            &lt;p&gt;My Mephisto install was 0.7.3- 0.8 doesn’t have any huge feature changes but there are some bugfixes and small enhancements, as well as a performance boost (&lt;a href="http://izumi.plan99.net/blog/index.php/2008/03/18/performance-comparison-rails-126-vs-202/"&gt;speed&lt;/a&gt;, &lt;a href="http://izumi.plan99.net/blog/index.php/2008/03/19/memory-usage-comparison-rails-126-vs-202/"&gt;memory use&lt;/a&gt;) from using rails 2.0.  My install was from a tarball, and I don’t want to &lt;a href="http://mephistoblog.com/download"&gt;move my production server to a git checkout&lt;/a&gt; (yet?).  Like other Mephisto users I’ve got theme customizations and maybe even some things in the public directory.&lt;/p&gt;

&lt;p&gt;I need to upgrade, not reinstall.  The Mephisto folks didn’t write a real upgrade guide, but this is Rails and it’s not too hard to figure out.  Almost any rails app could be updated this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Back Everything Up&lt;/li&gt;
&lt;li&gt;Create &amp;amp; Apply A Patch&lt;/li&gt;
&lt;li&gt;Stop Your Server &amp;amp; Clean Up&lt;/li&gt;
&lt;li&gt;Patch Code &amp;amp; Migrate The Database&lt;/li&gt;
&lt;li&gt;Crack Open A Beer&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Back Everything Up&lt;/h1&gt;

&lt;p&gt;Back up the current code and database.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;cp -a my_blog my_blog.bck&lt;tt&gt;
&lt;/tt&gt;mysqldump -uroot --opt mephisto_production &amp;gt; pre0.8.sql&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;There’s your safety net.&lt;/p&gt;

&lt;h1&gt;Create A Patch&lt;/h1&gt;

&lt;p&gt;The trick here is to create a patch between the &lt;em&gt;version of Mephisto you’re currently running&lt;/em&gt; and &lt;em&gt;0.8&lt;/em&gt;.  For me that was 0.7.3, so that’s what I’ll use below.  Fetch the 0.7.3 and the 0.8 source:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;wget http://s3.amazonaws.com/mephisto-blog/mephisto-0.7.3.tar.gz&lt;tt&gt;
&lt;/tt&gt;wget http://github.com/technoweenie/mephisto/tarball/master.tar.gz&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And this is weird right here.  Justin has linked the 0.8 release from the &lt;a href="http://mephistoblog.com/download"&gt;Mephisto download page&lt;/a&gt; as the git master tarball.  &lt;strong&gt;This means you don’t know what rev of git you will actually get when you click on it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So there isn’t even really an 0.8 release, there’s just a link to a dynamic current master.  Bad form.&lt;/p&gt;

&lt;p&gt;I ended up with 9072b487bf45c5e41e33c66b32d94aea84732d1b, you might get something else.  You could get the rev I used by fetching it directly:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;wget http://github.com/tarballs/technoweenie-mephisto-9072b487bf45c5e41e33c66b32d94aea84732d1b.tar.gz&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Now untar the two packages.  You get a nice “mephisto-0.7.3” directory for 0.7.3, and something messy like “technoweenie-mephisto-9072b487bf45c5e41e33c66b32d94aea84732d1b” for 0.8.  Make a diff between the two:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;diff -Nur mephisto-0.7.3 technoweenie-mephisto-9072b487bf45c5e41e33c66b32d94aea84732d1b &amp;gt; ~/mephisto-0.7.3_to_0.8.patch&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Or you can &lt;a href="http://madhatted.com/assets/2008/4/25/mephisto-0.7.3_to_0.8.patch"&gt;download the 0.7.3 to 0.8 patch from me&lt;/a&gt;, it weighs in at a hefty 4.9M.&lt;/p&gt;

&lt;h1&gt;Stop Your Server &amp;amp; Clean Up&lt;/h1&gt;

&lt;p&gt;Everyone has a custom Rails setup, so just stop your server the proper way.  After that give it a good:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;rake tmp:clear&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;To flush out the caches and such.&lt;/p&gt;

&lt;h1&gt;Patch Code &amp;amp; Migrate The Database&lt;/h1&gt;

&lt;p&gt;Now that the server is turned off and cache is cleared, try patching.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;cd /var/ww/my_blog&lt;tt&gt;
&lt;/tt&gt;patch -p1 &amp;lt; ~/mephisto-0.7.3_to_0.8.patch&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Mostly you’ll just see “patching file ….” flying by, but you may get a changed file question like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;patching file public/.htaccess&lt;tt&gt;
&lt;/tt&gt;Reversed (or previously applied) patch detected!  Assume -R? [n]&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;You can likely answer “n” to both the Assume and the Apply question:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;patching file public/.htaccess&lt;tt&gt;
&lt;/tt&gt;Reversed (or previously applied) patch detected!  Assume -R? [n] n&lt;tt&gt;
&lt;/tt&gt;Apply anyway? [n] n&lt;tt&gt;
&lt;/tt&gt;Skipping patch.&lt;tt&gt;
&lt;/tt&gt;1 out of 1 hunk ignored -- saving rejects to file public/.htaccess.rej&lt;tt&gt;
&lt;/tt&gt;patching file public/install.html&lt;tt&gt;
&lt;/tt&gt;...&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;I also had some custom filters added to environment.rb, and got this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;patching file config/environment.rb&lt;tt&gt;
&lt;/tt&gt;Hunk #3 FAILED at 44.&lt;tt&gt;
&lt;/tt&gt;1 out of 3 hunks FAILED -- saving rejects to file config/environment.rb.rej&lt;tt&gt;
&lt;/tt&gt;patching file config/environments/development.rb&lt;tt&gt;
&lt;/tt&gt;...&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;A failed hunk means patching that file didn’t work, usually because you have local modifications.  In case the solution is easy: You want to use the version of config/environment.rb that came with 0.8, and put your customizations into config/initializers/custom.rb.  Copy the good config/environment.rb from the 0.8 tarball you extracted earlier right over the local file.  You can still find your customizations in config/environment.rb.orig and copy them from there to the new file for environment customizations, config/initializers/custom.rb.&lt;/p&gt;

&lt;h2&gt;Install Gems (Well, I Had To)&lt;/h2&gt;

&lt;p&gt;Before I could migrate I had to install tzinfo.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;sudo gem install tzinfo&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Maybe you will too.&lt;/p&gt;

&lt;h2&gt;Migrate The Database&lt;/h2&gt;

&lt;p&gt;Compared to applying the patch this is easy:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;cd /var/www/my_blog&lt;tt&gt;
&lt;/tt&gt;RAILS_ENV=production rake db:migrate&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;My database updated with no problems at all.&lt;/p&gt;

&lt;h1&gt;Crack Open A Beer&lt;/h1&gt;

&lt;p&gt;Start up your mongrel, thin, eventd_mongrel or other server and crack open that frosty brew.  You’ve just upgraded yourself to the &lt;em&gt;best release&lt;/em&gt; of the self-proclaimed &lt;em&gt;best blogging system ever&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So what kind of beer are you enjoying today?&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/R6-WXIVwvnQ" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/4/25/missing-mephisto-upgrade-guide-0-7-3-to-0-8</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-04-24:2709</id>
    <published>2008-04-24T17:09:00Z</published>
    <updated>2008-04-24T17:09:39Z</updated>
    <link href="http://feedproxy.google.com/~r/madhatted/~3/xjeowOsPIJ8/keep-google-happy-seo-for-online-stores" rel="alternate" type="text/html" />
    <title>Keep Google Happy: SEO For Online Stores</title>
<summary type="html">&lt;p&gt;As a side project I’ve helped some friends launch &lt;a href="http://www.liquidware.com"&gt;Liquidware&lt;/a&gt;, a simple storefront for their &lt;a href="http://www.arduino.cc"&gt;Arduino&lt;/a&gt; modules and other open/hobby hardware.  They created a decent amount of buzz about their launch by saving up a bunch of content and pushing it out the same weekend.  Blog posts, video, images, all that good stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://youtube.com/user/AVRman82"&gt;YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.flickr.com/photos/22071518@N04/"&gt;Flickr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://antipastohw.blogspot.com/"&gt;Blogspot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was good noise, and it got them enough traffic to sell out of a few stocked items (woohoo!).  But after the launch bubble a steady stream of sales needs to come from Google.  I’ve been experimenting with the SEO on Liquidware’s site and learned a few smart tricks for your next storefront.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;As a side project I’ve helped some friends launch &lt;a href="http://www.liquidware.com"&gt;Liquidware&lt;/a&gt;, a simple storefront for their &lt;a href="http://www.arduino.cc"&gt;Arduino&lt;/a&gt; modules and other open/hobby hardware.  They created a decent amount of buzz about their launch by saving up a bunch of content and pushing it out the same weekend.  Blog posts, video, images, all that good stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://youtube.com/user/AVRman82"&gt;YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.flickr.com/photos/22071518@N04/"&gt;Flickr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://antipastohw.blogspot.com/"&gt;Blogspot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was good noise, and it got them enough traffic to sell out of a few stocked items (woohoo!).  But after the launch bubble a steady stream of sales needs to come from Google.  I’ve been experimenting with the SEO on Liquidware’s site and learned a few smart tricks for your next storefront.&lt;/p&gt;
&lt;h1&gt;Plan Your HTML For SEO&lt;/h1&gt;

&lt;p&gt;Most of this comes for free if you’re a good standards-adherent web designer.  Use alt tags for images, title tags for href, try and keep your content earlier on the page than navigation.  Google looks for keywords in a couple of places, you should use all of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title tag&lt;/li&gt;
&lt;li&gt;Keywords meta-tag&lt;/li&gt;
&lt;li&gt;Description meta-tag&lt;/li&gt;
&lt;li&gt;Headers, especially h1&lt;/li&gt;
&lt;li&gt;Link tags&lt;/li&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;li&gt;Content blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice thing about using an easily customizable cart (like &lt;a href="http://code.google.com/p/substruct/"&gt;Substruct&lt;/a&gt;, which we used) if that much of this can be automated.  Product pages at liquidware.com use the first few lines of the product description as the description meta-tag content.  They add their category and name to the keywords content, title tag and h1 header.  For that matter, the name of every product is also in the URL.&lt;/p&gt;

&lt;h1&gt;Test &amp;amp; Tune Your Keyword Content&lt;/h1&gt;

&lt;p&gt;Once you put up your site at a demo or production URL run it through &lt;a href="https://adwords.google.com/select/KeywordToolExternal"&gt;Google’s keyword tool&lt;/a&gt;.  Liquidware gets these results:&lt;/p&gt;

&lt;p&gt;&lt;img src="http://madhatted.com/assets/2008/4/24/liquidware_keywords.jpg" alt="Liquidware Keywords" /&gt;&lt;/p&gt;

&lt;p&gt;This will give you an idea of how Google will see your site later on.  You can use the keyword ideas to find alternate wordings that may index better.  &lt;em&gt;Even better&lt;/em&gt;, drop your competition’s site into the keyword tool and see if you should bother competing on their keywords or not.&lt;/p&gt;

&lt;h1&gt;Use Analytics &amp;amp; Webmaster Tools&lt;/h1&gt;

&lt;p&gt;Really &lt;em&gt;use&lt;/em&gt; these tools.  The &lt;a href="https://www.google.com/webmasters/tools/siteoverview"&gt;Google webmaster tool&lt;/a&gt; will tell you when the big G indexes your site, who links to you, what their keywords are, and what your site’s keywords are.  It’ll even tell you where you fall in search query results, and what position your site was in when it was clicked.  It’s got &lt;a href="http://www.searchenginejournal.com/google-webmaster-tools-a-comprehensive-guide/5712/"&gt;some other great features&lt;/a&gt; too.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://www.google.com/analytics/index.html"&gt;Google analytics&lt;/a&gt; is a no brainer, it’s got great info on your site, as well as being able to show you how people arrive at a given page.  Even better, making a purchase can be identified as a goal under analytics, showing you how many users start the checkout process and how many complete it, as well as where they came from.  This guy has a nice video that sums up goals quite well: &lt;a href="http://www.conversationmarketing.com/2007/03/google_analytics_tutorial_4_wo.htm"&gt;Google Analytics: Working With Goals&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;Use Google Base&lt;/h1&gt;

&lt;p&gt;This is the step that really helps push your products further out front.  &lt;a href="http://www.google.com/base"&gt;Google Base&lt;/a&gt; is a way to inject items into google’s search index, among them individual products.  When you search for “arduino” on Google this sneaks right into the results list:&lt;/p&gt;

&lt;p&gt;&lt;img src="http://madhatted.com/assets/2008/4/24/arduino_product_results.jpg" alt="Arduino Product Search Results" /&gt;&lt;/p&gt;

&lt;p&gt;Liquidware’s products should be there!  Google base let’s us add them:&lt;/p&gt;

&lt;p&gt;&lt;img src="http://madhatted.com/assets/2008/4/24/google_product_hack_pack.jpg" alt="Google Product Search For Hack Pack" /&gt;&lt;/p&gt;

&lt;p&gt;There are a couple was to do this, and they’re all quite confusingly documented.  They break down to this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload a tsv or xml file&lt;/li&gt;
&lt;li&gt;Have google fetch the former from a webserver on a regular cycle&lt;/li&gt;
&lt;li&gt;Use an API and your own cart software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ve combined the first two: added a &lt;a href="http://www.liquidware.com/products_rss.xml"&gt;Liquidware products RSS feed&lt;/a&gt; for Google to fetch nightly, and uploaded it the first time to seed the process.  This is a great solution, allowing the Liquidware guys to update products and prices on their store and see them change on Google nightly.  Liquidware does have product variations, which Google base does not, so each variation is spun off as another product.&lt;/p&gt;

&lt;h1&gt;Stay Active&lt;/h1&gt;

&lt;p&gt;Incoming links have the largest effect on ranking.  Pushing updates to youtube will get attention and a quick bursts of links, but the SEO and Google Base techniques above will keep up every-day traffic.&lt;/p&gt;

&lt;p&gt;Oh, I’ve been talking about releasing an updated version of the &lt;a href="http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort"&gt;Mootools table sort script&lt;/a&gt; I posted on, and that’s still coming.  Moving from Nashville to New York was slightly distracting, but there’s a full set of conversion_functions and other features to introduce.&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/xjeowOsPIJ8" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/4/24/keep-google-happy-seo-for-online-stores</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-02-21:575</id>
    <published>2008-02-21T22:52:00Z</published>
    <updated>2008-02-22T15:49:07Z</updated>
    <category term="morph javascript tag cloud mootools" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/u-OSPC8OvoA/dancing-tags" rel="alternate" type="text/html" />
    <title>Dancing Tags</title>
<summary type="html">&lt;p&gt;So just two days ago Obama swept Hawaii for his 10th Democratic primary win in a row.  Barack in particular has been beat up for lacking real content in his message, though I personally think his actions have spoken loudly (he &lt;a href="http://www.suntimes.com/news/politics/obama/701490,CST-NWS-obamaprof18.article"&gt;taught constitutional law&lt;/a&gt;, &lt;a href="http://obama.senate.gov/podcast/060608-network_neutral/"&gt;supported Net-neutrality&lt;/a&gt;, and helped push the &lt;a href="http://www.raisingkaine.com/showDiary.do?diaryId=12761"&gt;ethics reform bill&lt;/a&gt;).  Using tag clouds for visualizing messages has been &lt;a href="http://www.pollster.com/blogs/tag_clouds_for_the_democratic.php"&gt;done before&lt;/a&gt;, with pretty interesting results&lt;/p&gt;

&lt;p&gt;That example was nice for a snapshot of each candidate, but I’m looking to dig into the data for a single candidate a little more.  I want to &lt;em&gt;compare&lt;/em&gt; tag clouds for Obama to see how his message has changed over time.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;So just two days ago Obama swept Hawaii for his 10th Democratic primary win in a row.  Barack in particular has been beat up for lacking real content in his message, though I personally think his actions have spoken loudly (he &lt;a href="http://www.suntimes.com/news/politics/obama/701490,CST-NWS-obamaprof18.article"&gt;taught constitutional law&lt;/a&gt;, &lt;a href="http://obama.senate.gov/podcast/060608-network_neutral/"&gt;supported Net-neutrality&lt;/a&gt;, and helped push the &lt;a href="http://www.raisingkaine.com/showDiary.do?diaryId=12761"&gt;ethics reform bill&lt;/a&gt;).  Using tag clouds for visualizing messages has been &lt;a href="http://www.pollster.com/blogs/tag_clouds_for_the_democratic.php"&gt;done before&lt;/a&gt;, with pretty interesting results&lt;/p&gt;

&lt;p&gt;That example was nice for a snapshot of each candidate, but I’m looking to dig into the data for a single candidate a little more.  I want to &lt;em&gt;compare&lt;/em&gt; tag clouds for Obama to see how his message has changed over time.&lt;/p&gt;
&lt;h1&gt;Word Frequency Analysis&lt;/h1&gt;

&lt;p&gt;First we need some speeches.  I’ve chosen these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="http://madhatted.com/assets/2008/2/21/convention.txt"&gt;DNC Speech in 2004&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://madhatted.com/assets/2008/2/21/iowa.txt"&gt;Winning Iowa&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://madhatted.com/assets/2008/2/21/new_hamp.txt"&gt;Losing New Hampshire&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://madhatted.com/assets/2008/2/21/super_tuesday.txt"&gt;Winning super-tuesday&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://madhatted.com/assets/2008/2/21/wisconsin.txt"&gt;Winning Wisconsin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://madhatted.com/assets/2008/2/22/austin_obama.txt"&gt;Austin Debate&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cleaned up of APPLAUSE and MR. BARACK prompts, we can get to ripping some word frequencies.  There are surely easier ways to do this, but I’ve written a bash script to handle it.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;15&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;#!/bin/bash&lt;tt&gt;
&lt;/tt&gt;#&lt;tt&gt;
&lt;/tt&gt;# parse.sh prints out a json/javascript style word frequency list&lt;tt&gt;
&lt;/tt&gt;#&lt;tt&gt;
&lt;/tt&gt;echo 'frequencies: {'&lt;tt&gt;
&lt;/tt&gt;EXCLUDES=`cat exclude.txt |sed -e s/\\\\\\(.*\\\\\\)/\\\\\\|\\\1/ |tr -d '[:space:]'`&lt;tt&gt;
&lt;/tt&gt;tr ' ' '&lt;tt&gt;
&lt;/tt&gt;'&amp;lt;$1 |\&lt;tt&gt;
&lt;/tt&gt;sed -e 's/[^a-zA-Z0-9]//g'|\&lt;tt&gt;
&lt;/tt&gt;tr '[:upper:]' '[:lower:]'|\&lt;tt&gt;
&lt;/tt&gt;sort |\&lt;tt&gt;
&lt;/tt&gt;grep -Eiv &amp;quot;^(\ $EXCLUDES)$&amp;quot; |\&lt;tt&gt;
&lt;/tt&gt;uniq -c |\&lt;tt&gt;
&lt;/tt&gt;grep -iv ^\\\ *[0-9]*\\\ *$ |\&lt;tt&gt;
&lt;/tt&gt;grep -iv ^\\\ *[12345]\\\ .*$ |\&lt;tt&gt;
&lt;/tt&gt;sed -e 's/\( *\)\([0-9]*\)\ \([^ ]*\)/  &amp;quot;\3&amp;quot;: \2,/'&lt;tt&gt;
&lt;/tt&gt;echo '}'&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;You can download is as &lt;a href="http://madhatted.com/assets/2008/2/21/parse.sh"&gt;parse_1.sh&lt;/a&gt; &lt;em&gt;note: Wrap the keys in double quotes to keep IE and Safari happy&lt;/em&gt;.  This code assumes a file called exclude.txt that contains common words.  I’m using the 100 most common English words, you can get that list as &lt;a href="http://madhatted.com/assets/2008/2/21/exclude_1.txt"&gt;exclude.txt&lt;/a&gt;.  The parse script will also drop words with a frequency below 6.  Output is formatted to drop right into a javascript file.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;frequencies: {&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;country&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 5,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;hope&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 11,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;just&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 6,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;led&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 8,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;me&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 9,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 8,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;never&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 10,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 6,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;our&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 9,&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;us&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: 7,&lt;tt&gt;
&lt;/tt&gt;}&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;There we go, some word frequencies.  Now to draw a basic tag cloud.&lt;/p&gt;

&lt;h1&gt;Tag Cloud Markup&lt;/h1&gt;

&lt;p&gt;Some basic cloud markup:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="ta"&gt;&amp;lt;style&lt;/span&gt; &lt;span class="an"&gt;type&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;text/css&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;ol.cloud { width: 300px; }&lt;tt&gt;
&lt;/tt&gt;ol.cloud li { display:inline; padding: 2px 5px; }&lt;tt&gt;
&lt;/tt&gt;ol.cloud li.hidden { padding: 2px 4px; }&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;example_1_cloud&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;class&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;cloud&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="an"&gt;style&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;font-size:14px;&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;A tag&lt;span class="ta"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="an"&gt;style&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;font-size:22px;&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;A big important tag&lt;span class="ta"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;The important part of the CSS is the inline display of list elements.  That’s what lets them wrap onto new lines, along with having newlines after the list element tags.&lt;/p&gt;

&lt;h1&gt;Drawing A Cloud In Javascript&lt;/h1&gt;

&lt;p&gt;This code uses frequency lists like the one generated earlier and styles them like the markup used above.  It accepts a request like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;15&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;my cloud = new TagCloud( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;example_cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, {&lt;tt&gt;
&lt;/tt&gt;  clouds: {&lt;tt&gt;
&lt;/tt&gt;    convention: {&lt;tt&gt;
&lt;/tt&gt;      frequencies: {&lt;tt&gt;
&lt;/tt&gt;        blue: 4,&lt;tt&gt;
&lt;/tt&gt;        country: 9,&lt;tt&gt;
&lt;/tt&gt;        even: 5,&lt;tt&gt;
&lt;/tt&gt;        expect: 3&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    },&lt;tt&gt;
&lt;/tt&gt;    iowa: {&lt;tt&gt;
&lt;/tt&gt;      frequencies: {&lt;tt&gt;
&lt;/tt&gt;        blue: 4,&lt;tt&gt;
&lt;/tt&gt;        country: 2,&lt;tt&gt;
&lt;/tt&gt;        even: 5,&lt;tt&gt;
&lt;/tt&gt;        let: 1&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Now draw one&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;cloud.draw(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;convention&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Lines right up with the frequency lists we generated on the command line.  It could also accept some arguments for customized clouds:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;my cloud = new TagCloud( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;example_cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, {&lt;tt&gt;
&lt;/tt&gt;  tag_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;tag&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class="c"&gt;// By default a class of &amp;quot;tag&amp;quot; is set on all tags, change it here&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  hidden_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class="c"&gt;// By default a class of &amp;quot;hidden&amp;quot; is applied to hidden tags&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  tag_sizes: [ &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;8px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;16px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;30px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ], &lt;span class="c"&gt;// Set as many size increments as you like&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  clouds: {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;// The clouds&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;});&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;So you can set three size increments or 15, whatever amount of detail you want.&lt;/p&gt;

&lt;p&gt;TagCloud looks like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;15&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;25&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;35&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;45&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;53&lt;tt&gt;
&lt;/tt&gt;54&lt;tt&gt;
&lt;/tt&gt;55&lt;tt&gt;
&lt;/tt&gt;56&lt;tt&gt;
&lt;/tt&gt;57&lt;tt&gt;
&lt;/tt&gt;58&lt;tt&gt;
&lt;/tt&gt;59&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;61&lt;tt&gt;
&lt;/tt&gt;62&lt;tt&gt;
&lt;/tt&gt;63&lt;tt&gt;
&lt;/tt&gt;64&lt;tt&gt;
&lt;/tt&gt;65&lt;tt&gt;
&lt;/tt&gt;66&lt;tt&gt;
&lt;/tt&gt;67&lt;tt&gt;
&lt;/tt&gt;68&lt;tt&gt;
&lt;/tt&gt;69&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;71&lt;tt&gt;
&lt;/tt&gt;72&lt;tt&gt;
&lt;/tt&gt;73&lt;tt&gt;
&lt;/tt&gt;74&lt;tt&gt;
&lt;/tt&gt;75&lt;tt&gt;
&lt;/tt&gt;76&lt;tt&gt;
&lt;/tt&gt;77&lt;tt&gt;
&lt;/tt&gt;78&lt;tt&gt;
&lt;/tt&gt;79&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;80&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;81&lt;tt&gt;
&lt;/tt&gt;82&lt;tt&gt;
&lt;/tt&gt;83&lt;tt&gt;
&lt;/tt&gt;84&lt;tt&gt;
&lt;/tt&gt;85&lt;tt&gt;
&lt;/tt&gt;86&lt;tt&gt;
&lt;/tt&gt;87&lt;tt&gt;
&lt;/tt&gt;88&lt;tt&gt;
&lt;/tt&gt;89&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;90&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;91&lt;tt&gt;
&lt;/tt&gt;92&lt;tt&gt;
&lt;/tt&gt;93&lt;tt&gt;
&lt;/tt&gt;94&lt;tt&gt;
&lt;/tt&gt;95&lt;tt&gt;
&lt;/tt&gt;96&lt;tt&gt;
&lt;/tt&gt;97&lt;tt&gt;
&lt;/tt&gt;98&lt;tt&gt;
&lt;/tt&gt;99&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;101&lt;tt&gt;
&lt;/tt&gt;102&lt;tt&gt;
&lt;/tt&gt;103&lt;tt&gt;
&lt;/tt&gt;104&lt;tt&gt;
&lt;/tt&gt;105&lt;tt&gt;
&lt;/tt&gt;106&lt;tt&gt;
&lt;/tt&gt;107&lt;tt&gt;
&lt;/tt&gt;108&lt;tt&gt;
&lt;/tt&gt;109&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// TagCloud requires mootools v1.11 with these modules:&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Class.Extras, Array, Number, Element.Event, Element.Selectors,&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Window.DomReady, Fx.Style, Fx.Styles, Fx.Elements, Fx.Transitions,&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Hash&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; TagCloud = new Class({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( cloud, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.options = $merge({&lt;tt&gt;
&lt;/tt&gt;      clouds: {},&lt;tt&gt;
&lt;/tt&gt;      tag_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;      hidden_class: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;      tag_sizes: [ &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;8px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;12px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;18px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;20px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;22px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;24px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;26px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;28px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ]&lt;tt&gt;
&lt;/tt&gt;    }, options);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.cloud = $(cloud);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.depth = &lt;span class="pc"&gt;this&lt;/span&gt;.options.tag_sizes.length;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tags = $A();&lt;tt&gt;
&lt;/tt&gt;    $each(&lt;span class="pc"&gt;this&lt;/span&gt;.options.clouds, &lt;span class="r"&gt;function&lt;/span&gt;(v, k){&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.reset_bounds();&lt;tt&gt;
&lt;/tt&gt;      $each(v.frequencies, &lt;span class="r"&gt;function&lt;/span&gt;(v2, k2){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.expand_bounds(v2);&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; )); &lt;tt&gt;
&lt;/tt&gt;      $each(v.frequencies, &lt;span class="r"&gt;function&lt;/span&gt;(v2, k2){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.update_tag( k, k2, v2 );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; )); &lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; )); &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.sort_tags();&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  update_tag: &lt;span class="r"&gt;function&lt;/span&gt;(cloud, tag_content, frequency){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; found = &lt;span class="pc"&gt;this&lt;/span&gt;.tags.some(&lt;span class="r"&gt;function&lt;/span&gt;(tag, i) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (tag.content == tag_content) {&lt;tt&gt;
&lt;/tt&gt;        tag.cloud_weights.set(cloud, &lt;span class="pc"&gt;this&lt;/span&gt;.get_weight(frequency));&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (!found){&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; cloud_weights = new Hash();&lt;tt&gt;
&lt;/tt&gt;      cloud_weights.set(cloud, &lt;span class="pc"&gt;this&lt;/span&gt;.get_weight(frequency));&lt;tt&gt;
&lt;/tt&gt;      tag = { content: tag_content, cloud_weights: cloud_weights };&lt;tt&gt;
&lt;/tt&gt;      tag.&lt;span class="fu"&gt;toString&lt;/span&gt; = &lt;span class="r"&gt;function&lt;/span&gt;(){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;this&lt;/span&gt;.content }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.tags.&lt;span class="fu"&gt;push&lt;/span&gt;(tag);&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  get_weight: &lt;span class="r"&gt;function&lt;/span&gt;( frequency ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; class_i = &lt;span class="pt"&gt;Math&lt;/span&gt;.&lt;span class="fu"&gt;floor&lt;/span&gt;(&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="fu"&gt;parseFloat&lt;/span&gt;(&lt;tt&gt;
&lt;/tt&gt;        ((frequency-&lt;span class="pc"&gt;this&lt;/span&gt;.lower) / (&lt;span class="pc"&gt;this&lt;/span&gt;.upper-&lt;span class="pc"&gt;this&lt;/span&gt;.lower)),&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="pc"&gt;this&lt;/span&gt;.depth&lt;tt&gt;
&lt;/tt&gt;      ) * &lt;span class="pc"&gt;this&lt;/span&gt;.depth&lt;tt&gt;
&lt;/tt&gt;    );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (class_i == &lt;span class="pc"&gt;this&lt;/span&gt;.depth) class_i = class_i - 1;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; class_i;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  reset_bounds: &lt;span class="r"&gt;function&lt;/span&gt;(){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.lower = 99999999999;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.upper = 0;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  expand_bounds: &lt;span class="r"&gt;function&lt;/span&gt;(v){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (v &amp;gt; &lt;span class="pc"&gt;this&lt;/span&gt;.upper) &lt;span class="pc"&gt;this&lt;/span&gt;.upper = v;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (v &amp;lt; &lt;span class="pc"&gt;this&lt;/span&gt;.lower) &lt;span class="pc"&gt;this&lt;/span&gt;.lower = v;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_tags: &lt;span class="r"&gt;function&lt;/span&gt;( cloud_name ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tags.&lt;span class="fu"&gt;sort&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  draw: &lt;span class="r"&gt;function&lt;/span&gt;( cloud_name ) {&lt;tt&gt;
&lt;/tt&gt;    $each(&lt;span class="pc"&gt;this&lt;/span&gt;.tags, &lt;span class="r"&gt;function&lt;/span&gt;( tag, i ){&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (!tag.element) {&lt;tt&gt;
&lt;/tt&gt;        tag.element = new Element( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;li&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;rel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: &lt;span class="pc"&gt;this&lt;/span&gt;.options.tag_class&lt;tt&gt;
&lt;/tt&gt;        });&lt;tt&gt;
&lt;/tt&gt;        tag.element.setHTML(tag.content).injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.cloud );&lt;tt&gt;
&lt;/tt&gt;        tag.fx = new Fx.Styles( tag.element );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.cloud.appendText(&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;);  &lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;        &lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;+tag.cloud_weights.get(cloud_name) != &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NaN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &amp;amp;&amp;amp;&lt;tt&gt;
&lt;/tt&gt;           &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;+tag.cloud_weights.get(cloud_name) != &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;) {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.options.hidden_class)&lt;tt&gt;
&lt;/tt&gt;          tag.element.removeClass(&lt;span class="pc"&gt;this&lt;/span&gt;.options.hidden_class);&lt;tt&gt;
&lt;/tt&gt;        tag.fx.start({&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;opacity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: 1,&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;font-size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: &lt;span class="pc"&gt;this&lt;/span&gt;.options.tag_sizes[tag.cloud_weights.get(cloud_name)]&lt;tt&gt;
&lt;/tt&gt;        });&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;return&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; ( tag.element.getStyle(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;opacity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;) != 0 ) {&lt;tt&gt;
&lt;/tt&gt;        tag.fx.start({&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;opacity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: 0,&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;font-size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;: 0&lt;tt&gt;
&lt;/tt&gt;        });&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.options.hidden_class)&lt;tt&gt;
&lt;/tt&gt;          tag.element.addClass(&lt;span class="pc"&gt;this&lt;/span&gt;.options.hidden_class);&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;return&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Give it a try!&lt;/p&gt;

&lt;p&gt;As always, you can download &lt;a href="http://madhatted.com/assets/2008/2/21/morphing_cloud_1.js"&gt;morphing_cloud.js&lt;/a&gt; or play with an &lt;a href="http://madhatted.com/assets/2008/2/22/campaign_speeches_in_tag_clouds.html"&gt;html example of democratic campaign speeches&lt;/a&gt;.  Take a look at how the use of “hope” has changed over time, and how more details have emerged in recent speeches.  I’m looking forward expanding this to look at some other candidates.&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/u-OSPC8OvoA" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/2/21/dancing-tags</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-01-16:19</id>
    <published>2008-01-16T17:26:00Z</published>
    <updated>2008-06-20T18:07:35Z</updated>
    <category term="Javascript" />
    <category term="javascript" />
    <category term="optimize" />
    <category term="performance" />
    <category term="sort" />
    <category term="table" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/7UID524xNec/the-joy-of-an-optimized-complete-javascript-table-sort" rel="alternate" type="text/html" />
    <title>The Joy of an Optimized, Complete Javascript Table Sort</title>
<summary type="html">&lt;p&gt;&lt;em&gt;When you’re done poking through this, check out the final version of this script at &lt;a href="http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows"&gt;The Joy of Cows On Tables&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few days ago we walked through writing our own mootools-based table sort in &lt;a href="http://madhatted.com/2008/1/11/the-joy-of-a-minimal-complete-javascript-table-sort"&gt;the Joy of a Minimal, Complete Javascript Table Sort&lt;/a&gt;.  A poster going by “hello there” raised a good point about performance:&lt;/p&gt;

&lt;blockquote&gt;
    &lt;p&gt;“would be nice to see the example with several hundred rows - performance in sorting is a huge issue, and looking at the zillions of libraries out there… many of them konk out completely, taking 5 seconds to sort a table with 1,000 rows.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Right.  Javascript should be used to enhance a user’s experience.  5 seconds of wait times for a table sort is completely asinine.   Let’s look at some quick ways to optimize our code, and uncover a slick way to double the speed of our sorting.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;&lt;em&gt;When you’re done poking through this, check out the final version of this script at &lt;a href="http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows"&gt;The Joy of Cows On Tables&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few days ago we walked through writing our own mootools-based table sort in &lt;a href="http://madhatted.com/2008/1/11/the-joy-of-a-minimal-complete-javascript-table-sort"&gt;the Joy of a Minimal, Complete Javascript Table Sort&lt;/a&gt;.  A poster going by “hello there” raised a good point about performance:&lt;/p&gt;

&lt;blockquote&gt;
    &lt;p&gt;“would be nice to see the example with several hundred rows - performance in sorting is a huge issue, and looking at the zillions of libraries out there… many of them konk out completely, taking 5 seconds to sort a table with 1,000 rows.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Right.  Javascript should be used to enhance a user’s experience.  5 seconds of wait times for a table sort is completely asinine.   Let’s look at some quick ways to optimize our code, and uncover a slick way to double the speed of our sorting.&lt;/p&gt;
&lt;h1&gt;The Easy Stuff&lt;/h1&gt;

&lt;p&gt;I’m going to use The &lt;a href="http://www.getfirebug.com/"&gt;Firebug&lt;/a&gt; Firefox plugin for my analysis.  These changes should have pretty a universal effect though.  Let’s look at some sorting times on 1000 rows:&lt;/p&gt;

&lt;p&gt;&lt;img src="http://madhatted.com/assets/2008/1/15/table_sort_performance.jpg" alt="Table Sort Performance" /&gt;&lt;/p&gt;

&lt;p&gt;Ok, so about a second and a half on my 2.0 Ghz C2D and 2G RAM laptop.  Not as bad as the 5 seconds we were worried about, but not really great.  Some easy targets in optimization stand out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;removeClass&lt;/strong&gt; - I’d bet dollars to donuts this can be tweaked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.length()&lt;/strong&gt; is checked every loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;.removeClass()&lt;/strong&gt; is a function in mootools.  It looks like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;  removeClass: &lt;span class="r"&gt;function&lt;/span&gt;(className){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.className = &lt;span class="pc"&gt;this&lt;/span&gt;.className.&lt;span class="fu"&gt;replace&lt;/span&gt;(new &lt;span class="pt"&gt;RegExp&lt;/span&gt;(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;(^|\\s)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; + className + &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;(?:\\s|$)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;), &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;).clean();&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;this&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;We run removeClass at least 1000 times after a sort on our table.  The className our code &lt;em&gt;always&lt;/em&gt; passes to removeClass is “alt”.  We can avoid the initialization of 1000 RegExp objects if we save &lt;em&gt;one&lt;/em&gt; initialized RegExp somewhere.  This is an easy change that’ll save us as much as 300ms of time:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; SortingTable = new Class({&lt;tt&gt;
&lt;/tt&gt; &lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;// And here it is&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  removeAltClassRe: new &lt;span class="pt"&gt;RegExp&lt;/span&gt;(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;(^|\\s)alt(?:\\s|$)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;),&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.options = $merge({&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Now that same RegExp needs to be used where we before called removeClass.  For example, in stripe_table():&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;      counter++;&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;// tr.removeClass( 'alt' );&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;// Now use our already existing RegExp&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    tr.className = tr.className.&lt;span class="fu"&gt;replace&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.removeAltClassRe, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;).clean();&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( !(( counter % 2 ) == 0) ) {&lt;tt&gt;
&lt;/tt&gt;      tr.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );   &lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;One down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.length()&lt;/strong&gt; was our other easy tweak.  Instead of looping with:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;while&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.rows.length &amp;gt; 0) {&lt;tt&gt;
&lt;/tt&gt;     &lt;span class="r"&gt;var&lt;/span&gt; row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;We can consolidate those lines down to:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;   &lt;span class="r"&gt;while&lt;/span&gt; ( row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;() ) {&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Neat.&lt;/p&gt;

&lt;p&gt;With those two small tweaks, things have been speed up slightly.  Performance on the 1000 row table hovers around 2.1 seconds at best.  We can do better, but things are going to get weird.&lt;/p&gt;

&lt;h1&gt;The Good Stuff&lt;/h1&gt;

&lt;p&gt;Russel over at &lt;a href="http://lindsay.id.au/code/2006/faster-javascript-sorting/"&gt;lindsay.ie.au&lt;/a&gt; found something neat out.  The native sort() method is far faster if you don’t pass it a function to sort with.  Internally, sort() calls toString() on every array element it sorts, so if we overload toString, we can take advantage of that huge speed boost:&lt;/p&gt;

&lt;p&gt;Remember this from the middle of the sort_by_header function?&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows.each(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;      row.compare_value = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function( row );&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.compare_rows.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;If we overload toString for the elements on this.rows, we won’t need to pass a function into sort.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows.each(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;      row.compare_value = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function( row );&lt;tt&gt;
&lt;/tt&gt;      row.&lt;span class="fu"&gt;toString&lt;/span&gt; = &lt;span class="r"&gt;function&lt;/span&gt;(){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;this&lt;/span&gt;.compare_value }&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Now sort should be super fast.&lt;/p&gt;

&lt;p&gt;And it is very fast, it takes about 1.2 seconds to sort 1000 rows in Firefox.  The difference on Internet Explorer under VMware isn’t as large, but it is noticeable.  The big fault is that we’re now tied to how sort() sorts.  Alphabetically.&lt;/p&gt;

&lt;p&gt;That means we can’t sort numbers properly.  We’ll end up with&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;mixonic@pandora ~/Projects/table $ js&lt;tt&gt;
&lt;/tt&gt;js&amp;gt; [ 0, 1, 2, 11 ].sort();&lt;tt&gt;
&lt;/tt&gt;0,1,11,2&lt;tt&gt;
&lt;/tt&gt;js&amp;gt; // So instead, lets pad numbers into strings&lt;tt&gt;
&lt;/tt&gt;js&amp;gt; [ '000', '001', '002', '011' ].sort();&lt;tt&gt;
&lt;/tt&gt;000,001,002,011&lt;tt&gt;
&lt;/tt&gt;js&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;As “minroi_aoi” mentioned in the last post, sorting with numbers was funky.  The solution was to pass real integers out of the conversion function instead of strings.  getText() always returns a string.  parseInt() is the javascript function to convert them to integers:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;     &lt;span class="c"&gt;// Numbers&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /^\d+$/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell);&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;As we saw above though, we need a padded string now, not an integer.  Our number function will have to look like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// Numbers&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /^\d+$/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;0000000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;.&lt;span class="fu"&gt;substr&lt;/span&gt;(0,10-cell.length).&lt;span class="fu"&gt;concat&lt;/span&gt;(cell);&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And if you want to sort integers longer than 10 digits, you’d need to expand the pad string and the offset.  There is a tradeoff here: storing the strings for sort takes up more memory than just the number would.  In this script, that memory is only taken up while sorting, after that the memory is freed.&lt;/p&gt;

&lt;p&gt;All of that left us with this new script:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;45&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;53&lt;tt&gt;
&lt;/tt&gt;54&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;55&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;56&lt;tt&gt;
&lt;/tt&gt;57&lt;tt&gt;
&lt;/tt&gt;58&lt;tt&gt;
&lt;/tt&gt;59&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;61&lt;tt&gt;
&lt;/tt&gt;62&lt;tt&gt;
&lt;/tt&gt;63&lt;tt&gt;
&lt;/tt&gt;64&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;65&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;66&lt;tt&gt;
&lt;/tt&gt;67&lt;tt&gt;
&lt;/tt&gt;68&lt;tt&gt;
&lt;/tt&gt;69&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;71&lt;tt&gt;
&lt;/tt&gt;72&lt;tt&gt;
&lt;/tt&gt;73&lt;tt&gt;
&lt;/tt&gt;74&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;75&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;76&lt;tt&gt;
&lt;/tt&gt;77&lt;tt&gt;
&lt;/tt&gt;78&lt;tt&gt;
&lt;/tt&gt;79&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;80&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;81&lt;tt&gt;
&lt;/tt&gt;82&lt;tt&gt;
&lt;/tt&gt;83&lt;tt&gt;
&lt;/tt&gt;84&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;85&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;86&lt;tt&gt;
&lt;/tt&gt;87&lt;tt&gt;
&lt;/tt&gt;88&lt;tt&gt;
&lt;/tt&gt;89&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;90&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;91&lt;tt&gt;
&lt;/tt&gt;92&lt;tt&gt;
&lt;/tt&gt;93&lt;tt&gt;
&lt;/tt&gt;94&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;95&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;96&lt;tt&gt;
&lt;/tt&gt;97&lt;tt&gt;
&lt;/tt&gt;98&lt;tt&gt;
&lt;/tt&gt;99&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;101&lt;tt&gt;
&lt;/tt&gt;102&lt;tt&gt;
&lt;/tt&gt;103&lt;tt&gt;
&lt;/tt&gt;104&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;105&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;106&lt;tt&gt;
&lt;/tt&gt;107&lt;tt&gt;
&lt;/tt&gt;108&lt;tt&gt;
&lt;/tt&gt;109&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;110&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;111&lt;tt&gt;
&lt;/tt&gt;112&lt;tt&gt;
&lt;/tt&gt;113&lt;tt&gt;
&lt;/tt&gt;114&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;115&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;116&lt;tt&gt;
&lt;/tt&gt;117&lt;tt&gt;
&lt;/tt&gt;118&lt;tt&gt;
&lt;/tt&gt;119&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;120&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;121&lt;tt&gt;
&lt;/tt&gt;122&lt;tt&gt;
&lt;/tt&gt;123&lt;tt&gt;
&lt;/tt&gt;124&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;125&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;126&lt;tt&gt;
&lt;/tt&gt;127&lt;tt&gt;
&lt;/tt&gt;128&lt;tt&gt;
&lt;/tt&gt;129&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;130&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;131&lt;tt&gt;
&lt;/tt&gt;132&lt;tt&gt;
&lt;/tt&gt;133&lt;tt&gt;
&lt;/tt&gt;134&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;135&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;136&lt;tt&gt;
&lt;/tt&gt;137&lt;tt&gt;
&lt;/tt&gt;138&lt;tt&gt;
&lt;/tt&gt;139&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;140&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;141&lt;tt&gt;
&lt;/tt&gt;142&lt;tt&gt;
&lt;/tt&gt;143&lt;tt&gt;
&lt;/tt&gt;144&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;145&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;146&lt;tt&gt;
&lt;/tt&gt;147&lt;tt&gt;
&lt;/tt&gt;148&lt;tt&gt;
&lt;/tt&gt;149&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// new SortingTable( 'my_table', {&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//   zebra: true,     // Stripe the table, also on initialize&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//   details: false   // Has details every other row&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// });&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// The above were the defaults.  The regexes in load_conversions test a cell&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// begin sorted for a match, then use that conversion for all elements on that&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// column.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Requires mootools Class, Array, Function, Element, Element.Selectors,&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Element.Event, and you should probably get Window.DomReady if you're smart.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; SortingTable = new Class({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  removeAltClassRe: new &lt;span class="pt"&gt;RegExp&lt;/span&gt;(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;(^|\\s)alt(?:\\s|$)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;),&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.options = $merge({&lt;tt&gt;
&lt;/tt&gt;      zebra: &lt;span class="pc"&gt;true&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;      details: &lt;span class="pc"&gt;false&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    }, options);&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.table = $(table);&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tbody = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.options.zebra) {&lt;tt&gt;
&lt;/tt&gt;      SortingTable.stripe_table( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElementsByTagName( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.headers = new Hash;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; thead = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;thead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    $each(thead.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0].getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;), &lt;span class="r"&gt;function&lt;/span&gt;( header, index ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; header = $(header);&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header.getText(), { column: index } );&lt;tt&gt;
&lt;/tt&gt;      header.addEvent( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;(evt){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;var&lt;/span&gt; evt = new Event(evt);&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.sort_by_header( evt.target.getText() );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.load_conversions();&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_by_header: &lt;span class="r"&gt;function&lt;/span&gt;( header_text ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = new &lt;span class="pt"&gt;Array&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; trs = $A(&lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElementsByTagName( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; ( row = trs.&lt;span class="fu"&gt;shift&lt;/span&gt;() ) {&lt;tt&gt;
&lt;/tt&gt;      row = { row: row.remove() };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.options.details ) {&lt;tt&gt;
&lt;/tt&gt;        row.detail = trs.&lt;span class="fu"&gt;shift&lt;/span&gt;().remove();&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;unshift&lt;/span&gt;( row );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; header = &lt;span class="pc"&gt;this&lt;/span&gt;.headers.get( header_text );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column &amp;gt;= 0 &amp;amp;&amp;amp; &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column == header.column ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// They were pulled off in reverse&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column = header.column;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (header.conversion_function) {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = header.conversion_function;&lt;tt&gt;
&lt;/tt&gt;      } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.rows.some(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; to_match = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (to_match == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="pc"&gt;this&lt;/span&gt;.conversions.some(&lt;span class="r"&gt;function&lt;/span&gt;(conversion){&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;if&lt;/span&gt; (conversion.matcher.&lt;span class="fu"&gt;test&lt;/span&gt;( to_match )){&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = conversion.conversion_function;&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;            }&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;          }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;        header.conversion_function = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function.bind( &lt;span class="pc"&gt;this&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header_text, header );&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.each(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;        row.compare_value = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function( row );&lt;tt&gt;
&lt;/tt&gt;        row.&lt;span class="fu"&gt;toString&lt;/span&gt; = &lt;span class="r"&gt;function&lt;/span&gt;(){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;this&lt;/span&gt;.compare_value }&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; index = 0;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; ( row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;() ) {&lt;tt&gt;
&lt;/tt&gt;      row.row.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody );&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (row.detail){ row.detail.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody ) };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.options.zebra ) {&lt;tt&gt;
&lt;/tt&gt;        row.row.className = row.row.className.&lt;span class="fu"&gt;replace&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.removeAltClassRe, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;).clean();&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; (row.detail){&lt;tt&gt;
&lt;/tt&gt;          row.detail.className = row.detail.className.&lt;span class="fu"&gt;replace&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.removeAltClassRe, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;).clean();&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; ( ( index % 2 ) == 0 ) {&lt;tt&gt;
&lt;/tt&gt;          row.row.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (row.detail){ row.detail.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ); }&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      index++;&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  load_conversions: &lt;span class="r"&gt;function&lt;/span&gt;() {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.conversions = $A([&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// YYYY-MM-DD, YYYY-m-d&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /\d{4}-\d{1,2}-\d{1,2}/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; re = /(\d{4})-(\d{1,2})-(\d{1,2})/;&lt;tt&gt;
&lt;/tt&gt;          cell = re.&lt;span class="fu"&gt;exec&lt;/span&gt;( cell );&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; new &lt;span class="pt"&gt;Date&lt;/span&gt;(&lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[1]), &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[2], 10) - 1, &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[3], 10));&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// Numbers&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /^\d+$/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;00000000000000000000000000000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;.&lt;span class="fu"&gt;substr&lt;/span&gt;(0,32-cell.length).&lt;span class="fu"&gt;concat&lt;/span&gt;(cell);&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// Fallback &lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /.*/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    ]);&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;SortingTable.stripe_table = &lt;span class="r"&gt;function&lt;/span&gt; ( tr_elements  ) {&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; counter = 0;&lt;tt&gt;
&lt;/tt&gt;  $$( tr_elements ).each( &lt;span class="r"&gt;function&lt;/span&gt;( tr ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( tr.style.display != &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &amp;amp;&amp;amp; !tr.hasClass(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;collapsed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;) ) {&lt;tt&gt;
&lt;/tt&gt;      counter++;&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    tr.className = tr.className.&lt;span class="fu"&gt;replace&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.removeAltClassRe, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;$1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;).clean();&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( !(( counter % 2 ) == 0) ) {&lt;tt&gt;
&lt;/tt&gt;      tr.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );   &lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;}&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Now a bit over twice as fast as before.&lt;/p&gt;

&lt;p&gt;Again, you can pull this script down as &lt;a href="http://madhatted.com/assets/2008/1/16/minimal_sort_v2.js"&gt;javascript&lt;/a&gt; or see an &lt;a href="http://madhatted.com/assets/2008/1/16/minimal_sort_v2_1.html"&gt;example&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don’t forget to find the final version of this script in &lt;a href="http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows"&gt;The Joy of Cows On Tables&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/7UID524xNec" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2008-01-11:11</id>
    <published>2008-01-11T17:40:00Z</published>
    <updated>2008-06-20T18:03:47Z</updated>
    <category term="Javascript" />
    <category term="javascript" />
    <category term="sort" />
    <category term="table" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/3HCToTFftgA/the-joy-of-a-minimal-complete-javascript-table-sort" rel="alternate" type="text/html" />
    <title>The Joy of a Minimal, Complete Javascript Table Sort</title>
<summary type="html">&lt;p&gt;&lt;em&gt;When you’re done poking through this, take a peek at how you can double the speed of this script in &lt;a href="http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort"&gt;The Joy of an Optimized, Complete Javascript Table Sort&lt;/a&gt;, and then check out the final version of it at &lt;a href="http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows"&gt;The Joy of Cows On Tables&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ah table sorting.  There are few problems that have been solved as many times as you have been.  Unfortunately, some of the nicest solutions, such as &lt;a href="http://joomlicious.com/mootable/"&gt;mootable&lt;/a&gt;, are also pretty overbearing.  Check out this feature list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Total re-styling of your table.&lt;/li&gt;
&lt;li&gt;Editable table cells.&lt;/li&gt;
&lt;li&gt;Loading table contents from JSON.&lt;/li&gt;
&lt;li&gt;Loading table contents from JSON over XHR.&lt;/li&gt;
&lt;li&gt;Server-side sorting using the above.&lt;/li&gt;
&lt;li&gt;Client-side sorting.&lt;/li&gt;
&lt;li&gt;Re-ordering of columns, column options.&lt;/li&gt;
&lt;li&gt;Nice fade effects.&lt;/li&gt;
&lt;li&gt;Event hooks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whoa, too much.  Over at &lt;a href="http://icainformatics.com/"&gt;ICA&lt;/a&gt; we needed something way more lightweight.  I was pretty much looking for this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client-side sort of various formats (like mm/dd/yy).&lt;/li&gt;
&lt;li&gt;Zebra or striped tables.&lt;/li&gt;
&lt;li&gt;Support sorting with hidden rows on the table.&lt;/li&gt;
&lt;li&gt;Be fairly fast.&lt;/li&gt;
&lt;li&gt;Use mootools (which we already use).&lt;/li&gt;
&lt;li&gt;Use a table already on the DOM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s take a look at how to make a javascript table sort that follows best practices, is relatively minimal, and fast.  Nothing here is completely new stuff, but hopefully walking through it will help &lt;em&gt;you&lt;/em&gt; write a better table sort next time you need just a table sort, and not all the overhead of a library.  I’ll be using &lt;a href="http://mootools.net/"&gt;mootools&lt;/a&gt; sort of aggressively, but the core ideas and practices here are portable to any environment.&lt;/p&gt;

&lt;p&gt;Here we go!&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;&lt;em&gt;When you’re done poking through this, take a peek at how you can double the speed of this script in &lt;a href="http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort"&gt;The Joy of an Optimized, Complete Javascript Table Sort&lt;/a&gt;, and then check out the final version of it at &lt;a href="http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows"&gt;The Joy of Cows On Tables&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ah table sorting.  There are few problems that have been solved as many times as you have been.  Unfortunately, some of the nicest solutions, such as &lt;a href="http://joomlicious.com/mootable/"&gt;mootable&lt;/a&gt;, are also pretty overbearing.  Check out this feature list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Total re-styling of your table.&lt;/li&gt;
&lt;li&gt;Editable table cells.&lt;/li&gt;
&lt;li&gt;Loading table contents from JSON.&lt;/li&gt;
&lt;li&gt;Loading table contents from JSON over XHR.&lt;/li&gt;
&lt;li&gt;Server-side sorting using the above.&lt;/li&gt;
&lt;li&gt;Client-side sorting.&lt;/li&gt;
&lt;li&gt;Re-ordering of columns, column options.&lt;/li&gt;
&lt;li&gt;Nice fade effects.&lt;/li&gt;
&lt;li&gt;Event hooks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whoa, too much.  Over at &lt;a href="http://icainformatics.com/"&gt;ICA&lt;/a&gt; we needed something way more lightweight.  I was pretty much looking for this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client-side sort of various formats (like mm/dd/yy).&lt;/li&gt;
&lt;li&gt;Zebra or striped tables.&lt;/li&gt;
&lt;li&gt;Support sorting with hidden rows on the table.&lt;/li&gt;
&lt;li&gt;Be fairly fast.&lt;/li&gt;
&lt;li&gt;Use mootools (which we already use).&lt;/li&gt;
&lt;li&gt;Use a table already on the DOM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s take a look at how to make a javascript table sort that follows best practices, is relatively minimal, and fast.  Nothing here is completely new stuff, but hopefully walking through it will help &lt;em&gt;you&lt;/em&gt; write a better table sort next time you need just a table sort, and not all the overhead of a library.  I’ll be using &lt;a href="http://mootools.net/"&gt;mootools&lt;/a&gt; sort of aggressively, but the core ideas and practices here are portable to any environment.&lt;/p&gt;

&lt;p&gt;Here we go!&lt;/p&gt;
&lt;h1&gt;HTML Assumptions and Javascript Style&lt;/h1&gt;

&lt;p&gt;Our code is going to make a few assumptions.  Assumptions are always a tradeoff.  They can be a detriment if you don’t know what the assumptions are, but are a great enhancement to your consistency and coding speed if you know what they are.  We’re going to assume the table we want to sort’s HTML looks something like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="an"&gt;cellpadding&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;cellspacing&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class="an"&gt;id&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class="ta"&gt;&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;a header&lt;span class="ta"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      ...&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;a value&lt;tt&gt;
&lt;/tt&gt;      ...&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="ta"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;another value&lt;span class="ta"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      ...&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="ta"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="ta"&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="ta"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Take note of a few things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We gave the table an id, “sort_this”&lt;/li&gt;
&lt;li&gt;We used thead and tbody sections&lt;/li&gt;
&lt;li&gt;We used “th” tags for the headers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of that is really just good HTML, the semantic use of th and td, for example, is just good HTML table markup.&lt;/p&gt;

&lt;p&gt;Javascript can be kludged onto a page in a thousand different ways, we’re going to stick with three tenets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Be unobtrusive (keep javascript out of our HTML).&lt;/li&gt;
&lt;li&gt;Use objects and instances to keep our code reusable (and able to work with multiple tables).&lt;/li&gt;
&lt;li&gt;Use options as a hash for readability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s look at a basic mootools javascript object:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; SortingTable;&lt;tt&gt;
&lt;/tt&gt;SortingTable = new Class({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ){&lt;tt&gt;
&lt;/tt&gt;    ...&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_by_header: &lt;span class="r"&gt;function&lt;/span&gt;( text ){&lt;tt&gt;
&lt;/tt&gt;    ...&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;SortingTable.stripe_table = &lt;span class="r"&gt;function&lt;/span&gt;( tr_elements ){&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;}&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;You can see how we added instance and class functions.  “initialize” is an instance function run when we instantiate our object, “sort_by_headers” is an instance function we call on an object.&lt;/p&gt;

&lt;p&gt;“stripe_table” is added in a different manner, that’s because “stripe_table” is a class function, we want to be able to use it without instantiating “SortingTable” at all.  When we use SortingTable, it should look like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Or maybe if we have options:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, { zebra: &lt;span class="pc"&gt;false&lt;/span&gt; } );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;We could create an instance and call sort_by_methods:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; sorting_table = new SortingTable( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;sort_this&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, { zebra: &lt;span class="pc"&gt;false&lt;/span&gt; } );&lt;tt&gt;
&lt;/tt&gt;sorting_table.sort_by_header( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Or call a class method without an instance:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;SortingTable.stripe_table( $$( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;#sort_this tbody tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Alright, we have a basic set of common practices we can base our code off.  Let’s dive into a very basic table sort script.&lt;/p&gt;

&lt;h1&gt;Basic Javascript Table Sorting&lt;/h1&gt;

&lt;p&gt;Now this is only a starting point, the table sort below is so basic it isn’t very user friendly.  It is a good place to start understanding how we sort things in general, and deal with the tables on the DOM.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;45&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; SortingTable;&lt;tt&gt;
&lt;/tt&gt;SortingTable = new Class({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.table = $(table);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tbody = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.headers = new Hash;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; thead = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;thead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    $each(thead.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0].getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;), &lt;span class="r"&gt;function&lt;/span&gt;( header, index ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; header = $(header);&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header.getText(), { column: index } );&lt;tt&gt;
&lt;/tt&gt;      header.addEvent( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;(evt){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;var&lt;/span&gt; evt = new Event(evt);&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.sort_by_header( evt.target.getText() );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_by_header: &lt;span class="r"&gt;function&lt;/span&gt;( header_text ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = new &lt;span class="pt"&gt;Array&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; trs = &lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElements( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; ( trs.length &amp;gt; 0 ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = { row: trs.&lt;span class="fu"&gt;shift&lt;/span&gt;().remove() };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;unshift&lt;/span&gt;( row );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; header = &lt;span class="pc"&gt;this&lt;/span&gt;.headers.get( header_text );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column &amp;gt;= 0 &amp;amp;&amp;amp; &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column == header.column ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// They were pulled off in reverse&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column = header.column;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.compare_rows.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.rows.length &amp;gt; 0) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;      row.row.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  compare_rows: &lt;span class="r"&gt;function&lt;/span&gt;( r1, r2 ) {&lt;tt&gt;
&lt;/tt&gt;    r1.compare_value = $(r1.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;    r2.compare_value = $(r2.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;gt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt;  1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;lt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt; -1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; 0;&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Oh the bitter pill of javascript.  Let’s boil it down, there really isn’t too much going on here.  In the initialize (which remember, is run as soon as we instantiate):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We find some nodes on the DOM so we don’t need to find them again later: this.table, this.tbody and this.thead.&lt;/li&gt;
&lt;li&gt;In $each, we walk all of the “th” tags in thead and do two things:
&lt;ol&gt;
&lt;li&gt;add the innerText as a key in this.headers, with a value that includes the index, or which column we’re dealing with.  This is a map for later.&lt;/li&gt;
&lt;li&gt;add a “mousedown” event handler to the “th” tag, firing sort_by_header with the th’s innerText&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stay with me.  Look at the initialize again:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.table = $(table);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tbody = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.headers = new Hash;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; thead = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;thead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    $each(thead.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0].getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;), &lt;span class="r"&gt;function&lt;/span&gt;( header, index ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; header = $(header);&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header.getText(), { column: index } );&lt;tt&gt;
&lt;/tt&gt;      header.addEvent( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;(evt){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;var&lt;/span&gt; evt = new Event( evt );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.sort_by_header( evt.target.getText() );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;See the lines that close out the each and addEvent functions?  They’re using .bind( this ) to attach the internal “this” of the function back to our instantiated object.  It’s a nice trick that let’s us have a “mousedown” event that is attached to our object.&lt;/p&gt;

&lt;p&gt;Deep breath here, let’s step through the sort_by_header section.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_by_header: &lt;span class="r"&gt;function&lt;/span&gt;( header_text ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = new &lt;span class="pt"&gt;Array&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; trs = &lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElements( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; ( trs.length &amp;gt; 0 ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = { row: trs.&lt;span class="fu"&gt;shift&lt;/span&gt;().remove() };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;unshift&lt;/span&gt;( row );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;So this creates an internal array of rows, then proceeds to walk all the tr’s on tbody.  For each one, it is doing two things: Shifting one tr off the array of trs, and using .remove() to drop it off the DOM.  Each row is stuffed in an object and added to the top of the this.rows array.&lt;/p&gt;

&lt;p&gt;It’s important that it’s added to the beginning of the rows array, because that means if all we needed to do was reverse the rows, we can just replay this array one by one and attach it’s rows to the DOM again.  Reverse without calling .reverse(), nice.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; header = &lt;span class="pc"&gt;this&lt;/span&gt;.headers.get( header_text );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column &amp;gt;= 0 &amp;amp;&amp;amp; &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column == header.column ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// They were pulled off in reverse&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column = header.column;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.compare_rows.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Here we pull the header object out of our headers hash using the passed text (which is the innerText from the attached event in initialize), and then compare it to our last sort_column. It they are the same, we can move on and just re-insert the rows.  If they’re different, we need to call this.rows.sort().&lt;/p&gt;

&lt;p&gt;.sort() is a javascript method for sorting an array, it’s native to the language.  By default, .sort() will sort rows in alphabetical order.  To sort in any other way, you can pass it a function, and it’ll use answers of 1, -1 and 0 to figure out the row order.  In this example, we’re telling sort to use this.compare_rows, and also reminding it that compare_rows should be run on our current object.&lt;/p&gt;

&lt;p&gt;Learn more about sort &lt;a href="http://www.w3schools.com/jsref/jsref_sort.asp"&gt;at w3schools&lt;/a&gt;.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.rows.length &amp;gt; 0) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;      row.row.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This section is the meat- we take out sorted or reversed array, shift it’s contents off the top one by one, and add then to this.tbody.  Shifting them off gets them out of memory as we move along so we don’t leave arrays sitting in RAM.&lt;/p&gt;

&lt;p&gt;That’s the brunt of table sorting in Javascript right there.  sort_by_header ripped rows off the table, reversed or sorted them, and then reinserted them onto the DOM.&lt;/p&gt;

&lt;p&gt;Our actual table sort logic is the following:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;  compare_rows: &lt;span class="r"&gt;function&lt;/span&gt;( r1, r2 ) {&lt;tt&gt;
&lt;/tt&gt;    r1.compare_value = $(r1.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;    r2.compare_value = $(r2.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;gt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt;  1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;lt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt; -1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; 0;&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;compare_rows get’s two arguments from sort, two rows objects to compare.  The text of the td cells is fetched and compared.  Pretty straight ahead here, the only trickery is finding out what column to use by reaching into this.sort_column.  I like having the whole row in there to compare, it opens to door to having secondary sorting by another column (like iTunes’ “Album By Artist” sorting).&lt;/p&gt;

&lt;p&gt;Huzzah!  we can nearly rejoice.  I’d go back and look over the code we just walked through, if you can understand what’s going on up there, this next block of hackery should make perfect sense.&lt;/p&gt;

&lt;h1&gt;Why Our Simple Sort Sucks&lt;/h1&gt;

&lt;p&gt;There are some problems with the simple script above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It pulls the a given td cell off the DOM and does conversion on it multiple times, that makes it slow.&lt;/li&gt;
&lt;li&gt;It isn’t flexible enough to sort mm/dd/yy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those are two pretty damning faults, so let’s clean them up before adding any new features.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;45&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;53&lt;tt&gt;
&lt;/tt&gt;54&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;55&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;56&lt;tt&gt;
&lt;/tt&gt;57&lt;tt&gt;
&lt;/tt&gt;58&lt;tt&gt;
&lt;/tt&gt;59&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;61&lt;tt&gt;
&lt;/tt&gt;62&lt;tt&gt;
&lt;/tt&gt;63&lt;tt&gt;
&lt;/tt&gt;64&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;65&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;66&lt;tt&gt;
&lt;/tt&gt;67&lt;tt&gt;
&lt;/tt&gt;68&lt;tt&gt;
&lt;/tt&gt;69&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;71&lt;tt&gt;
&lt;/tt&gt;72&lt;tt&gt;
&lt;/tt&gt;73&lt;tt&gt;
&lt;/tt&gt;74&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;75&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;76&lt;tt&gt;
&lt;/tt&gt;77&lt;tt&gt;
&lt;/tt&gt;78&lt;tt&gt;
&lt;/tt&gt;79&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;80&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;81&lt;tt&gt;
&lt;/tt&gt;82&lt;tt&gt;
&lt;/tt&gt;83&lt;tt&gt;
&lt;/tt&gt;84&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;85&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;86&lt;tt&gt;
&lt;/tt&gt;87&lt;tt&gt;
&lt;/tt&gt;88&lt;tt&gt;
&lt;/tt&gt;89&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;90&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;91&lt;tt&gt;
&lt;/tt&gt;92&lt;tt&gt;
&lt;/tt&gt;93&lt;tt&gt;
&lt;/tt&gt;94&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;95&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; SortingTable;&lt;tt&gt;
&lt;/tt&gt;SortingTable = new Class({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.table = $(table);    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tbody = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.headers = new Hash;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; thead = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;thead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    $each(thead.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0].getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;), &lt;span class="r"&gt;function&lt;/span&gt;( header, index ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; header = $(header);&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header.getText(), { column: index } );&lt;tt&gt;
&lt;/tt&gt;      $(header).addEvent( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;(evt){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;var&lt;/span&gt; evt = new Event(evt);&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.sort_by_header( new evt.target.getText() );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.load_conversions();&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_by_header: &lt;span class="r"&gt;function&lt;/span&gt;( header_text ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = new &lt;span class="pt"&gt;Array&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; trs = &lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElements( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; ( trs.length &amp;gt; 0 ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = { row: trs.&lt;span class="fu"&gt;shift&lt;/span&gt;().remove() };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;unshift&lt;/span&gt;( row );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; header = &lt;span class="pc"&gt;this&lt;/span&gt;.headers.get( header_text );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column &amp;gt;= 0 &amp;amp;&amp;amp; &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column == header.column ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// They were pulled off in reverse&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column = header.column;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (header.conversion_function) {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = header.conversion_function;&lt;tt&gt;
&lt;/tt&gt;      } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.rows.some(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; to_match = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (to_match == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="pc"&gt;this&lt;/span&gt;.conversions.some(&lt;span class="r"&gt;function&lt;/span&gt;(conversion){&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;if&lt;/span&gt; (conversion.matcher.&lt;span class="fu"&gt;test&lt;/span&gt;( to_match )){&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = conversion.conversion_function;&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;            }&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;          }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;        header.conversion_function = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function.bind( &lt;span class="pc"&gt;this&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header_text, header );&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.each(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;        row.compare_value = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function( row );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.compare_rows.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.rows.length &amp;gt; 0) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;      row.row.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  compare_rows: &lt;span class="r"&gt;function&lt;/span&gt;( r1, r2 ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;gt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt;  1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;lt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt; -1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; 0;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;  &lt;tt&gt;
&lt;/tt&gt;  load_conversions: &lt;span class="r"&gt;function&lt;/span&gt;() {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.conversions = $A([&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// YYYY-MM-DD, YYYY-m-d&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /\d{4}-\d{1,2}-\d{1,2}/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; re = /(\d{4})-(\d{1,2})-(\d{1,2})/;&lt;tt&gt;
&lt;/tt&gt;          cell = re.&lt;span class="fu"&gt;exec&lt;/span&gt;( cell );&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; new &lt;span class="pt"&gt;Date&lt;/span&gt;(&lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[1]), &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[2], 10) - 1, &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[3], 10));&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// Fallback &lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /.*/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    ]);&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;this.load_conversions(); is the big new thing in initialize.  It’s that function at the end of the class that has and array of hashes each with a “matcher” and “conversion_function”.&lt;/p&gt;

&lt;p&gt;Really, the main difference is in the sorting section of sort_by_header, in the meat and bones:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column = header.column;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (header.conversion_function) {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = header.conversion_function;&lt;tt&gt;
&lt;/tt&gt;      } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.rows.some(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; to_match = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (to_match == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="pc"&gt;this&lt;/span&gt;.conversions.some(&lt;span class="r"&gt;function&lt;/span&gt;(conversion){&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;if&lt;/span&gt; (conversion.matcher.&lt;span class="fu"&gt;test&lt;/span&gt;( to_match )){&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = conversion.conversion_function;&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;            }&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;          }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;        header.conversion_function = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function.bind( &lt;span class="pc"&gt;this&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header_text, header );&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.each(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;        row.compare_value = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function( row );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.compare_rows.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Ok, don’t get thrown.  Javascript’s weird features mean loops are pretty messy.  Notice the first time the rows are walked we use “.some(“.  Some is a mootools array function that acts like each until the function returns true, then it breaks the loop.  This is what’s going on here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;See if we have a conversion_function.  If we don’t…
&lt;ol&gt;
&lt;li&gt;Walk through td’s in the column.&lt;/li&gt;
&lt;li&gt;If it’s innerText is blank, go to the next element.&lt;/li&gt;
&lt;li&gt;Walk through the available conversions.&lt;/li&gt;
&lt;li&gt;If a conversion matches the matcher, assign it to this.conversion_function and break the loop&lt;/li&gt;
&lt;li&gt;Save the conversion_function on the header object so we don’t need to find it later.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;Walk all our row objects and run the conversion_function on each, save it onto the row.&lt;/li&gt;
&lt;li&gt;Run sort with compare_rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;compare_rows, you can see, now expects to sort with the compare_value:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;  compare_rows: &lt;span class="r"&gt;function&lt;/span&gt;( r1, r2 ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;gt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt;  1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;lt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt; -1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; 0;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;By adding new conversions to load_conversions, you can support sorting of all kinds of different formats and sub-columns.  And you’ll only be running that conversion once on a cell (until you sort another column and come back, this doesn’t do aggressive caching of the whole table in memory).&lt;/p&gt;

&lt;h1&gt;The Sweet Smell Of Success&lt;/h1&gt;

&lt;p&gt;All that’s needed now is a sprinkling of zebra or striped tables, and stuffing some extra baggage onto the row objects, and we’ll support those last two features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zebra or striped tables.&lt;/li&gt;
&lt;li&gt;Hidden/expandable rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It looks something like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;45&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;53&lt;tt&gt;
&lt;/tt&gt;54&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;55&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;56&lt;tt&gt;
&lt;/tt&gt;57&lt;tt&gt;
&lt;/tt&gt;58&lt;tt&gt;
&lt;/tt&gt;59&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;61&lt;tt&gt;
&lt;/tt&gt;62&lt;tt&gt;
&lt;/tt&gt;63&lt;tt&gt;
&lt;/tt&gt;64&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;65&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;66&lt;tt&gt;
&lt;/tt&gt;67&lt;tt&gt;
&lt;/tt&gt;68&lt;tt&gt;
&lt;/tt&gt;69&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;71&lt;tt&gt;
&lt;/tt&gt;72&lt;tt&gt;
&lt;/tt&gt;73&lt;tt&gt;
&lt;/tt&gt;74&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;75&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;76&lt;tt&gt;
&lt;/tt&gt;77&lt;tt&gt;
&lt;/tt&gt;78&lt;tt&gt;
&lt;/tt&gt;79&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;80&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;81&lt;tt&gt;
&lt;/tt&gt;82&lt;tt&gt;
&lt;/tt&gt;83&lt;tt&gt;
&lt;/tt&gt;84&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;85&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;86&lt;tt&gt;
&lt;/tt&gt;87&lt;tt&gt;
&lt;/tt&gt;88&lt;tt&gt;
&lt;/tt&gt;89&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;90&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;91&lt;tt&gt;
&lt;/tt&gt;92&lt;tt&gt;
&lt;/tt&gt;93&lt;tt&gt;
&lt;/tt&gt;94&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;95&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;96&lt;tt&gt;
&lt;/tt&gt;97&lt;tt&gt;
&lt;/tt&gt;98&lt;tt&gt;
&lt;/tt&gt;99&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;101&lt;tt&gt;
&lt;/tt&gt;102&lt;tt&gt;
&lt;/tt&gt;103&lt;tt&gt;
&lt;/tt&gt;104&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;105&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;106&lt;tt&gt;
&lt;/tt&gt;107&lt;tt&gt;
&lt;/tt&gt;108&lt;tt&gt;
&lt;/tt&gt;109&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;110&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;111&lt;tt&gt;
&lt;/tt&gt;112&lt;tt&gt;
&lt;/tt&gt;113&lt;tt&gt;
&lt;/tt&gt;114&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;115&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;116&lt;tt&gt;
&lt;/tt&gt;117&lt;tt&gt;
&lt;/tt&gt;118&lt;tt&gt;
&lt;/tt&gt;119&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;120&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;121&lt;tt&gt;
&lt;/tt&gt;122&lt;tt&gt;
&lt;/tt&gt;123&lt;tt&gt;
&lt;/tt&gt;124&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;125&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;126&lt;tt&gt;
&lt;/tt&gt;127&lt;tt&gt;
&lt;/tt&gt;128&lt;tt&gt;
&lt;/tt&gt;129&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;130&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;131&lt;tt&gt;
&lt;/tt&gt;132&lt;tt&gt;
&lt;/tt&gt;133&lt;tt&gt;
&lt;/tt&gt;134&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;135&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;136&lt;tt&gt;
&lt;/tt&gt;137&lt;tt&gt;
&lt;/tt&gt;138&lt;tt&gt;
&lt;/tt&gt;139&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;140&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;141&lt;tt&gt;
&lt;/tt&gt;142&lt;tt&gt;
&lt;/tt&gt;143&lt;tt&gt;
&lt;/tt&gt;144&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;145&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// new Star.Table( 'my_table', {&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//   zebra: true,     // Stripe the table, also on initialize&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//   details: false,  // Has details every other row&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// });&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// The above were the defaults.  The regexes in load_conversions test a cell&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// begin sorted for a match, then use that conversion for all elements on that&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// column.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Requires mootools Class, Array, Function, Element, Element.Selectors,&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Element.Event, and you should probably get Window.DomReady if you're smart.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;var&lt;/span&gt; SortingTable;&lt;tt&gt;
&lt;/tt&gt;SortingTable = new Class({&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  initialize: &lt;span class="r"&gt;function&lt;/span&gt;( table, options ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.options = $merge({&lt;tt&gt;
&lt;/tt&gt;      zebra: &lt;span class="pc"&gt;true&lt;/span&gt;,&lt;tt&gt;
&lt;/tt&gt;      details: &lt;span class="pc"&gt;false&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    }, options);&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.table = $(table);&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.tbody = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.options.zebra) {&lt;tt&gt;
&lt;/tt&gt;      SortingTable.stripe_table( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElements( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.headers = new Hash;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; thead = $(&lt;span class="pc"&gt;this&lt;/span&gt;.table.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;thead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0]);&lt;tt&gt;
&lt;/tt&gt;    $each(thead.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[0].getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;), &lt;span class="r"&gt;function&lt;/span&gt;( header, index ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; header = $(header);&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header.getText(), { column: index } );&lt;tt&gt;
&lt;/tt&gt;      header.addEvent( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;(evt){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;var&lt;/span&gt; evt = new Event(evt);&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.sort_by_header( evt.target.getText() );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;    }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.load_conversions();&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  sort_by_header: &lt;span class="r"&gt;function&lt;/span&gt;( header_text ){&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = new &lt;span class="pt"&gt;Array&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; trs = &lt;span class="pc"&gt;this&lt;/span&gt;.tbody.getElements( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; ( trs.length &amp;gt; 0 ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = { row: trs.&lt;span class="fu"&gt;shift&lt;/span&gt;().remove() };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.options.details ) {&lt;tt&gt;
&lt;/tt&gt;        row.detail = trs.&lt;span class="fu"&gt;shift&lt;/span&gt;().remove();&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;unshift&lt;/span&gt;( row );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; header = &lt;span class="pc"&gt;this&lt;/span&gt;.headers.get( header_text );&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column &amp;gt;= 0 &amp;amp;&amp;amp; &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column == header.column ) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// They were pulled off in reverse&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.sort_column = header.column;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (header.conversion_function) {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = header.conversion_function;&lt;tt&gt;
&lt;/tt&gt;      } &lt;span class="r"&gt;else&lt;/span&gt; {&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.rows.some(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; to_match = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (to_match == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="pc"&gt;this&lt;/span&gt;.conversions.some(&lt;span class="r"&gt;function&lt;/span&gt;(conversion){&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;if&lt;/span&gt; (conversion.matcher.&lt;span class="fu"&gt;test&lt;/span&gt;( to_match )){&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function = conversion.conversion_function;&lt;tt&gt;
&lt;/tt&gt;              &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;            }&lt;tt&gt;
&lt;/tt&gt;            &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;          }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function){ &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;; }&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;        }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;        header.conversion_function = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function.bind( &lt;span class="pc"&gt;this&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="pc"&gt;this&lt;/span&gt;.headers.set( header_text, header );&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.each(&lt;span class="r"&gt;function&lt;/span&gt;(row){&lt;tt&gt;
&lt;/tt&gt;        row.compare_value = &lt;span class="pc"&gt;this&lt;/span&gt;.conversion_function( row );&lt;tt&gt;
&lt;/tt&gt;      }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;sort&lt;/span&gt;( &lt;span class="pc"&gt;this&lt;/span&gt;.compare_rows.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ) );&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; index = 0;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; (&lt;span class="pc"&gt;this&lt;/span&gt;.rows.length &amp;gt; 0) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;var&lt;/span&gt; row = &lt;span class="pc"&gt;this&lt;/span&gt;.rows.&lt;span class="fu"&gt;shift&lt;/span&gt;();&lt;tt&gt;
&lt;/tt&gt;      row.row.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody );&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (row.detail){ row.detail.injectInside( &lt;span class="pc"&gt;this&lt;/span&gt;.tbody ) };&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; ( &lt;span class="pc"&gt;this&lt;/span&gt;.options.zebra ) {&lt;tt&gt;
&lt;/tt&gt;        row.row.removeClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; (row.detail){ row.detail.removeClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ); }&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; ( ( index % 2 ) == 0 ) {&lt;tt&gt;
&lt;/tt&gt;          row.row.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;if&lt;/span&gt; (row.detail){ row.detail.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ); }&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;      index++;&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.rows = &lt;span class="pc"&gt;false&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  compare_rows: &lt;span class="r"&gt;function&lt;/span&gt;( r1, r2 ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;gt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt;  1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( r1.compare_value &amp;lt; r2.compare_value ) { &lt;span class="r"&gt;return&lt;/span&gt; -1 }&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; 0;&lt;tt&gt;
&lt;/tt&gt;  },&lt;tt&gt;
&lt;/tt&gt;  &lt;tt&gt;
&lt;/tt&gt;  load_conversions: &lt;span class="r"&gt;function&lt;/span&gt;() {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="pc"&gt;this&lt;/span&gt;.conversions = $A([&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// YYYY-MM-DD, YYYY-m-d&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /\d{4}-\d{1,2}-\d{1,2}/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; cell = $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;var&lt;/span&gt; re = /(\d{4})-(\d{1,2})-(\d{1,2})/;&lt;tt&gt;
&lt;/tt&gt;          cell = re.&lt;span class="fu"&gt;exec&lt;/span&gt;( cell );&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; new &lt;span class="pt"&gt;Date&lt;/span&gt;(&lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[1]), &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[2], 10) - 1, &lt;span class="fu"&gt;parseInt&lt;/span&gt;(cell[3], 10));&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      },&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="c"&gt;// Fallback &lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      { matcher: /.*/,&lt;tt&gt;
&lt;/tt&gt;        conversion_function: &lt;span class="r"&gt;function&lt;/span&gt;( row ) {&lt;tt&gt;
&lt;/tt&gt;          &lt;span class="r"&gt;return&lt;/span&gt; $(row.row.getElementsByTagName(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;td&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;)[&lt;span class="pc"&gt;this&lt;/span&gt;.sort_column]).getText();&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    ]);&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;SortingTable.stripe_table = &lt;span class="r"&gt;function&lt;/span&gt; ( tr_elements  ) {&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; counter = 0;&lt;tt&gt;
&lt;/tt&gt;  $$( tr_elements ).each( &lt;span class="r"&gt;function&lt;/span&gt;( tr ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( tr.style.display != &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; &amp;amp;&amp;amp; !tr.hasClass(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;collapsed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;) ) {&lt;tt&gt;
&lt;/tt&gt;      counter++;&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;    tr.removeClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );   &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; ( !(( counter % 2 ) == 0) ) {&lt;tt&gt;
&lt;/tt&gt;      tr.addClass( &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; );   &lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  }.bind( &lt;span class="pc"&gt;this&lt;/span&gt; ));&lt;tt&gt;
&lt;/tt&gt;}&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Nice.&lt;/p&gt;

&lt;p&gt;That was a &lt;em&gt;lot&lt;/em&gt; of ground to cover, as in, way more than I had any intention of covering :-).  I’ve made a lot of assumptions (remember those?) about your javascript fu in this post, but if you have any questions just ask!&lt;/p&gt;

&lt;p&gt;You can pull this code down as &lt;a href="http://madhatted.com/assets/2008/1/11/minimal_sort.js"&gt;javascript&lt;/a&gt; or take a look at some running &lt;a href="http://madhatted.com/assets/2008/1/11/minimal_sort_1.html"&gt;examples&lt;/a&gt;, including how to use hidden rows.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also look at optimization steps and an updated script at &lt;a href="http://madhatted.com/2008/1/16/the-joy-of-an-optimized-complete-javascript-table-sort"&gt;The Joy of an Optimized, Complete Javascript Table Sort&lt;/a&gt;, and then check out the final script in &lt;a href="http://madhatted.com/2008/6/20/the-joy-of-tables-on-cows"&gt;The Joy of Cows On Tables&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/3HCToTFftgA" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2008/1/11/the-joy-of-a-minimal-complete-javascript-table-sort</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2007-06-04:10</id>
    <published>2007-06-04T16:53:00Z</published>
    <updated>2007-06-04T16:53:51Z</updated>
    <category term="code" />
    <category term="Ferret" />
    <category term="FindYourDoc" />
    <category term="parse" />
    <category term="Rails" />
    <category term="regex" />
    <category term="Ruby" />
    <category term="search" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/9c5Ph1m8MvY/making-more-of-ferret-queries" rel="alternate" type="text/html" />
    <title>Making More of Ferret Queries</title>
<summary type="html">&lt;p&gt;Oh &lt;a href="http://ferret.davebalmain.com/trac/"&gt;Ferret&lt;/a&gt;, how lovely your speed, how confusing your documentation.  Maybe we’ll go over that in another post, but for now, let’s see how we can make Ferret a bit kinder to normal users by better understanding their queries.&lt;/p&gt;

&lt;p&gt;Over at &lt;a href="http://www.findyourdoc.com/"&gt;FindYourDoc&lt;/a&gt; we’re searching not only a large number of records, but a large variety of fields.  When we get a query such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  Doctor in Nashville TN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You and I know the user has given us a lot to go on, but Ferret doesn’t.  When humans look at that query we pull out a human understanding.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doctor - a type of care provider&lt;/li&gt;
&lt;li&gt;in - a throwaway word&lt;/li&gt;
&lt;li&gt;Nashville - a city&lt;/li&gt;
&lt;li&gt;TN - a state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if I were a programmer (imagine that), I would form the same query using Ferret’s query syntax:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  type:Doctor city:Nashville state:TN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Well great, but our visitors are not programmers, they’re grandmothers and dog trainers, patients and college students.  Let’s look at that query again, and see how we could break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doctor &amp;amp; TN - These are phrase that look for a match in specific sets.&lt;/li&gt;
&lt;li&gt;Nashville - matches data in a very large set.&lt;/li&gt;
&lt;li&gt;in - throwaway word.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let’s see how the raw query might be moved closer to my programmers query.&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;Oh &lt;a href="http://ferret.davebalmain.com/trac/"&gt;Ferret&lt;/a&gt;, how lovely your speed, how confusing your documentation.  Maybe we’ll go over that in another post, but for now, let’s see how we can make Ferret a bit kinder to normal users by better understanding their queries.&lt;/p&gt;

&lt;p&gt;Over at &lt;a href="http://www.findyourdoc.com/"&gt;FindYourDoc&lt;/a&gt; we’re searching not only a large number of records, but a large variety of fields.  When we get a query such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  Doctor in Nashville TN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You and I know the user has given us a lot to go on, but Ferret doesn’t.  When humans look at that query we pull out a human understanding.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doctor - a type of care provider&lt;/li&gt;
&lt;li&gt;in - a throwaway word&lt;/li&gt;
&lt;li&gt;Nashville - a city&lt;/li&gt;
&lt;li&gt;TN - a state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if I were a programmer (imagine that), I would form the same query using Ferret’s query syntax:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  type:Doctor city:Nashville state:TN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Well great, but our visitors are not programmers, they’re grandmothers and dog trainers, patients and college students.  Let’s look at that query again, and see how we could break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doctor &amp;amp; TN - These are phrase that look for a match in specific sets.&lt;/li&gt;
&lt;li&gt;Nashville - matches data in a very large set.&lt;/li&gt;
&lt;li&gt;in - throwaway word.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let’s see how the raw query might be moved closer to my programmers query.&lt;/p&gt;
&lt;h1&gt;Regex to the Rescue&lt;/h1&gt;

&lt;p&gt;Wow, that was a whole intro paragraph with no code!  Let’s take a basic transformation that we could do to move toward our programmer’s query, that of changing TN to state:TN.  Code time!&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="s"&gt;&lt;span class="dl"&gt;%w(&lt;/span&gt;&lt;span class="k"&gt; AL AZ AR CA CO CT DE FL GA HI ID&lt;tt&gt;
&lt;/tt&gt;    IL IA KS KY LA ME MD MA MI MN&lt;tt&gt;
&lt;/tt&gt;    MS MD MT NE NV NH NJ NM NY&lt;tt&gt;
&lt;/tt&gt;    NC ND OH OK OR PA RI SC SD TN&lt;tt&gt;
&lt;/tt&gt;    TX UT VT VA WA WV WI WY &lt;/span&gt;&lt;span class="dl"&gt;)&lt;/span&gt;&lt;/span&gt;.each &lt;span class="r"&gt;do&lt;/span&gt; |state|&lt;tt&gt;
&lt;/tt&gt;  query.gsub!( &lt;span class="rx"&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="k"&gt;(?:&lt;/span&gt;&lt;span class="ch"&gt;\A&lt;/span&gt;&lt;span class="k"&gt;|&lt;/span&gt;&lt;span class="ch"&gt;\s&lt;/span&gt;&lt;span class="k"&gt;)(&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;state&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;)(?=&lt;/span&gt;&lt;span class="ch"&gt;\s&lt;/span&gt;&lt;span class="k"&gt;|&lt;/span&gt;&lt;span class="ch"&gt;\z&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="mod"&gt;i&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt; state:&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;state&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; )&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;And what a block of code it is.  Naturally, we wouldn’t normally have a big block of states there, it would be in self.states or something similar.   So what’s going on?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  query.gsub!( /(?:\A|\s)(#{state})(?=\s|\z)/i, " state:#{state}" )&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For each state we run this gsub line.  The regex in it has three parts:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  (?:\A|\s)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This section is a “grouping”.  We know it’s a grouping because it’s in ().  The magic of this particular grouping is the use of ?:, which tells the regex engine this is not a grouping to be saved for reference later on.  It will require this group to be matched, but when we replace TN with state:TN we won’t want to replace what this grouping matched.  That’s why we have ?:.&lt;/p&gt;

&lt;p&gt;Inside our non-referenced group we have a short snippet “\A|\s”.  Well, that’s simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;\A - The beginning of the string&lt;/li&gt;
&lt;li&gt;\s - A space or other white-space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipe symbol, |, is an “or” in regex.  So we have a non-referenced group that matches either the beginning of the string, or a space.  The next segment is easy:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  (#{state})&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Super easy.  We’re matching a state.  Note that the state is in (), which means this is our match to be replaced later on.  Our last section is quite similar to the first:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  (?=\s|\z)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Look at how we’ve used ?= inside the ().  Adding ?= first thing in our parentheses turns it into a look-ahead assertion.  We’re looking forward in the string to see if we can find something, but we’re not storing it for later.  The \s|\z is looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;\s - a space or other white-space&lt;/li&gt;
&lt;li&gt;\z - the end of the string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So remembering |, we want to find a space or the end of the string after our match.
Take a peek at the whole thing again:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="s"&gt;&lt;span class="dl"&gt;%w(&lt;/span&gt;&lt;span class="k"&gt; AL AZ AR CA CO CT DE FL GA HI ID&lt;tt&gt;
&lt;/tt&gt;    IL IA KS KY LA ME MD MA MI MN&lt;tt&gt;
&lt;/tt&gt;    MS MD MT NE NV NH NJ NM NY&lt;tt&gt;
&lt;/tt&gt;    NC ND OH OK OR PA RI SC SD TN&lt;tt&gt;
&lt;/tt&gt;    TX UT VT VA WA WV WI WY &lt;/span&gt;&lt;span class="dl"&gt;)&lt;/span&gt;&lt;/span&gt;.each &lt;span class="r"&gt;do&lt;/span&gt; |state|&lt;tt&gt;
&lt;/tt&gt;  query.gsub!( &lt;span class="rx"&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="k"&gt;(?:&lt;/span&gt;&lt;span class="ch"&gt;\A&lt;/span&gt;&lt;span class="k"&gt;|&lt;/span&gt;&lt;span class="ch"&gt;\s&lt;/span&gt;&lt;span class="k"&gt;)(&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;state&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="k"&gt;)(?=&lt;/span&gt;&lt;span class="ch"&gt;\s&lt;/span&gt;&lt;span class="k"&gt;|&lt;/span&gt;&lt;span class="ch"&gt;\z&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="dl"&gt;/&lt;/span&gt;&lt;span class="mod"&gt;i&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt; state:&lt;/span&gt;&lt;span class="il"&gt;&lt;span class="dl"&gt;#{&lt;/span&gt;state&lt;span class="dl"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; )&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Notice the regex also uses an i at the end.  That will make our regex case insensitive so we can match TN and tn.  Also notice that we don’t look for IN.  Well, sorry Indiana, but we don’t want queries like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  Cardiology in New York&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to become:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  Cardiology state:IN New York&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It just wouldn’t work.&lt;/p&gt;

&lt;h1&gt;Wash, Rinse, Repeat&lt;/h1&gt;

&lt;p&gt;Well, neato, what other kinds of data could we apply this same technique to?  Two types that I can come up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discrete values in a set (matching a state)&lt;/li&gt;
&lt;li&gt;Structured values (like a zipcode)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a peek at an example of the latter:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  query.gsub!( /(?:\A|\s)([0-9]{5})(?=\s|\z)/i, ' zipcode:\1' )&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We’re looking for 5 numbers, then tacking zipcode: onto the front of them.  We’ve taken our human understanding of a structure and explained it to ferret.&lt;/p&gt;

&lt;p&gt;Ferret has a concept of weighting certain fields, and that can help tweak your results to better match your queries, but tricks like this can help a lot.  Searching for:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  Doctor in Nashville TN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Without FindYourDoc’s query tweaking, my top result is scored at ~0.46.  With it turned on, the top result is ~7.97.  That’s a sign Ferret is doing much better at understanding what I was asking for.  We can’t manage to trap city names, since there’s just too many, but we can trap the provider type and state to get this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  type:Doctor in Nashville state:TN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Other fields besides provider type and state are captured by our query tweaker as well.  Those tweaks give every visitor a personal programmer to help re-phrase what they say, and that makes our results far better for grandmothers and dog trainers.  Try it our on your own Ferret site, it doesn’t disappoint.&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/9c5Ph1m8MvY" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2007/6/4/making-more-of-ferret-queries</feedburner:origLink></entry>
  <entry xml:base="http://madhatted.com/">
    <author>
      <name>mixonic</name>
    </author>
    <id>tag:madhatted.com,2007-05-31:6</id>
    <published>2007-05-31T20:48:00Z</published>
    <updated>2007-06-01T13:58:29Z</updated>
    <category term="Caching" />
    <category term="Cookies" />
    <category term="Geocoding" />
    <category term="GeoKit" />
    <category term="Javascript" />
    <category term="Rails" />
    <category term="Ruby" />
    <link href="http://feedproxy.google.com/~r/madhatted/~3/sEdMMKWvUMU/yes-geocode-but-save-your-caches" rel="alternate" type="text/html" />
    <title>Yes, geocode, but save your caches</title>
<summary type="html">&lt;p&gt;Geocoding is like a spicy pepper, it provides an impressive kick, but should normally be sprinkled in moderation.  Now that rails has &lt;a href="http://geokit.rubyforge.org/"&gt;GeoKit&lt;/a&gt;, it’s even super-easy to do.&lt;/p&gt;

&lt;p&gt;Once you fight your way through the API-only documentation.&lt;/p&gt;

&lt;p&gt;But lucky you, we’re going to walk through a basic GeoKit set up modeled after what we used over at &lt;a href="http://www.findyourdoc.com/"&gt;FindYourDoc&lt;/a&gt; (we launch next week-ish):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install GeoKit&lt;/li&gt;
&lt;li&gt;Find out where a visitor is from and stuff it in a cookie&lt;/li&gt;
&lt;li&gt;Test our geocoding (‘cause &lt;em&gt;duh&lt;/em&gt;, you’re testing, right?)&lt;/li&gt;
&lt;li&gt;Use javascript to put it somewhere&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the end of this you’ll be customizing pages for visitors based on their location, but without killing off your action_caches.  Won’t that be a ball?&lt;/p&gt;</summary><content type="html">
            &lt;p&gt;Geocoding is like a spicy pepper, it provides an impressive kick, but should normally be sprinkled in moderation.  Now that rails has &lt;a href="http://geokit.rubyforge.org/"&gt;GeoKit&lt;/a&gt;, it’s even super-easy to do.&lt;/p&gt;

&lt;p&gt;Once you fight your way through the API-only documentation.&lt;/p&gt;

&lt;p&gt;But lucky you, we’re going to walk through a basic GeoKit set up modeled after what we used over at &lt;a href="http://www.findyourdoc.com/"&gt;FindYourDoc&lt;/a&gt; (we launch next week-ish):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install GeoKit&lt;/li&gt;
&lt;li&gt;Find out where a visitor is from and stuff it in a cookie&lt;/li&gt;
&lt;li&gt;Test our geocoding (‘cause &lt;em&gt;duh&lt;/em&gt;, you’re testing, right?)&lt;/li&gt;
&lt;li&gt;Use javascript to put it somewhere&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the end of this you’ll be customizing pages for visitors based on their location, but without killing off your action_caches.  Won’t that be a ball?&lt;/p&gt;
&lt;h1&gt;Installing GeoKit&lt;/h1&gt;

&lt;p&gt;This one’s easy, like any rails plugin if you run:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;ruby script/plugin install -x svn://rubyforge.org/var/svn/geokit/trunk&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;You’ll install it as a SVN external (and then you can use &lt;a href="http://piston.rubyforge.org/usage.html"&gt;piston&lt;/a&gt; to manage it!), or you can run:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;and just pull it down locally.  Next we’ll need some configuration lovin’.  You’ll want to go get a &lt;a href="http://www.google.com/apis/maps/signup.html"&gt;Google Maps API key&lt;/a&gt; for http://localhost:3000/ for while you develop, and chances are you’ll want another key for production.  GeoKit’s installer appends a bunch of lines to your config/environment.rb, find the one that looks like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="co"&gt;GeoKit&lt;/span&gt;::&lt;span class="co"&gt;Geocoders&lt;/span&gt;::&lt;span class="co"&gt;GOOGLE&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;REPLACE_WITH_YOUR_GOOGLE_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;and drop the key Google gave you in there.  If you got a key for your production install, you could always:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="co"&gt;RAILS_ENV&lt;/span&gt; == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="co"&gt;GeoKit&lt;/span&gt;::&lt;span class="co"&gt;Geocoders&lt;/span&gt;::&lt;span class="co"&gt;GOOGLE&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;MyProductionKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;else&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="c"&gt;# This will work for most people doing normal dev on localhost&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="co"&gt;GeoKit&lt;/span&gt;::&lt;span class="co"&gt;Geocoders&lt;/span&gt;::&lt;span class="co"&gt;GOOGLE&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;MyDevKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;You’ll also want to tell GeoKit to only use Google:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="co"&gt;GeoKit&lt;/span&gt;::&lt;span class="co"&gt;Geocoders&lt;/span&gt;::&lt;span class="co"&gt;PROVIDER_ORDER&lt;/span&gt;=[&lt;span class="sy"&gt;:google&lt;/span&gt;]&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;So there you are, now to do some geocoding.&lt;/p&gt;

&lt;h1&gt;Geocode a visitor’s ip, stash it in a cookie&lt;/h1&gt;

&lt;p&gt;“A cookie?!” I hear you cry, “Why lord why?!”  GeoKit comes with a nifty little helper that will stuff a geocoding object into your session automatically, but FindYourDoc needs a bit more. We wanted to know we could scale safely, so we’ve made aggressive use of caching, specifically with the &lt;a href="http://agilewebdevelopment.com/plugins/action_cache"&gt;action_cache plugin&lt;/a&gt;.  Using sessions means we render our geocoded values in our view, so we can’t cache that page.&lt;/p&gt;

&lt;p&gt;Ahh, but if we put them in a cookie, we can use a before_filter for that, and still cache our actual action.  Then we can grab the cookies in Javascript and render them on our page load.  Cache is good to go, and we have different geocoded values appear for each visitor.&lt;/p&gt;

&lt;p&gt;Our application controller ends up looking something like this:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;15&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;25&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;ApplicationController&lt;/span&gt; &amp;lt; &lt;span class="co"&gt;ActionController&lt;/span&gt;::&lt;span class="co"&gt;Base&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  before_filter &lt;span class="sy"&gt;:put_geocoding_into_cookies&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;private&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;put_geocoding_into_cookies&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; set_geokit_cookies?&lt;tt&gt;
&lt;/tt&gt;      set_geokit_cookies( request.remote_ip )&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;set_geokit_cookies?&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;unless&lt;/span&gt; cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;geocoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;unless&lt;/span&gt; cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;geocoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;] == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;true&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt; &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;false&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;set_geokit_cookies&lt;/span&gt;( ip )&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@location&lt;/span&gt; = &lt;span class="co"&gt;GeoKit&lt;/span&gt;::&lt;span class="co"&gt;Geocoders&lt;/span&gt;::&lt;span class="co"&gt;IpGeocoder&lt;/span&gt;.geocode( ip )&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="iv"&gt;@location&lt;/span&gt;.success &amp;amp;&amp;amp;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="iv"&gt;@location&lt;/span&gt;.respond_to?( &lt;span class="sy"&gt;:country_code&lt;/span&gt; ) &amp;amp;&amp;amp;&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="iv"&gt;@location&lt;/span&gt;.country_code == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;zipcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]  = &lt;span class="iv"&gt;@location&lt;/span&gt;.zipcode &lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="iv"&gt;@location&lt;/span&gt;.respond_to? &lt;span class="sy"&gt;:zipcode&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]    = &lt;span class="iv"&gt;@location&lt;/span&gt;.state &lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="iv"&gt;@location&lt;/span&gt;.respond_to? &lt;span class="sy"&gt;:state&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]     = &lt;span class="iv"&gt;@location&lt;/span&gt;.city &lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="iv"&gt;@location&lt;/span&gt;.respond_to? &lt;span class="sy"&gt;:city&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;geocoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;] = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;YES&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;else&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;      cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;geocoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;] = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Note how it’s split into private methods we could write tests against.  If only testing private instance methods on the application controller was that easy (if you have ideas on this, let me know).  To run over the highlights of that code:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;before_filter &lt;span class="sy"&gt;:put_geocoding_into_cookies&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Tells Rails to run our method put_geocoding_into_cookies when any page has attempted access.  set_geokit_cookies? is looking to find out if we have tried to geocode before, and also if we’ve already set a flag of ‘NO’ to say we’ve tried and failed already.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="iv"&gt;@location&lt;/span&gt; = &lt;span class="co"&gt;GeoKit&lt;/span&gt;::&lt;span class="co"&gt;Geocoders&lt;/span&gt;::&lt;span class="co"&gt;IpGeocoder&lt;/span&gt;.geocode( ip )&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;That’s our magic geocoding line.  Note that the ip was passed in from request.remote_ip in put_geocoding_into_cookies.  The if statement tests for a valid result, and a result in the US.  If we pass the statement, we set our cookies for state, zipcode, and city.  Note that we use strings for our keys: &lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;]    = &lt;span class="iv"&gt;@location&lt;/span&gt;.state &lt;span class="r"&gt;if&lt;/span&gt; &lt;span class="iv"&gt;@location&lt;/span&gt;.respond_to? &lt;span class="sy"&gt;:state&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Which is a quirk in Rails, you’d expect a HashWithIndifferentAccess there.  The respond_to? test is a work-around for GeoKit’s decision to not create attributes that is has no value for.&lt;/p&gt;

&lt;p&gt;And that’s that.  Your addresses should be geocoded in tossed into cookies.  Try using &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/1843"&gt;Firebug&lt;/a&gt; and you’ll see them in your server headers.  Of course, you’re probably geocoding 127.0.0.1, which won’t really give you anything useful.  So how do we know any of this if working?&lt;/p&gt;

&lt;h1&gt;Testing IP Geocoding&lt;/h1&gt;

&lt;p&gt;We test it.  There are two little hurdles to doing that, the first is pretty handily taken care of by the &lt;a href="http://blog.brightredglow.com/articles/2006/08/27/assert_cookie-for-ooey-gooey-fun"&gt;assert_cookie plugin&lt;/a&gt;.   Install that, and you’ll be able to:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;assert_cookie &lt;span class="sy"&gt;:geocoded&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;and we’ll need that.  With assert_cookie we can test what cookies get set for what IPs, but we still have another hurdle:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;set_geokit_cookies( request.remote_ip )&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;We need to fake an IP.  Enter the AnywhereController:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;AnywhereController&lt;/span&gt; &amp;lt; &lt;span class="co"&gt;ActionController&lt;/span&gt;::&lt;span class="co"&gt;TestRequest&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;remote_ip&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@my_remote_ip&lt;/span&gt; || &lt;span class="iv"&gt;@remote_ip&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;go_to_sunnyvale&lt;/span&gt; &lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@my_remote_ip&lt;/span&gt; = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;64.233.187.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;go_to_nowhere&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@my_remote_ip&lt;/span&gt; = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;With ActionController::TestRequest subclassed, can can overwrite the remote_ip method to return any IP we want, and therefor test some IPs that will actually geocode.  A full geocoding test might look like:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;15&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;25&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;31&lt;tt&gt;
&lt;/tt&gt;32&lt;tt&gt;
&lt;/tt&gt;33&lt;tt&gt;
&lt;/tt&gt;34&lt;tt&gt;
&lt;/tt&gt;35&lt;tt&gt;
&lt;/tt&gt;36&lt;tt&gt;
&lt;/tt&gt;37&lt;tt&gt;
&lt;/tt&gt;38&lt;tt&gt;
&lt;/tt&gt;39&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;40&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;41&lt;tt&gt;
&lt;/tt&gt;42&lt;tt&gt;
&lt;/tt&gt;43&lt;tt&gt;
&lt;/tt&gt;44&lt;tt&gt;
&lt;/tt&gt;45&lt;tt&gt;
&lt;/tt&gt;46&lt;tt&gt;
&lt;/tt&gt;47&lt;tt&gt;
&lt;/tt&gt;48&lt;tt&gt;
&lt;/tt&gt;49&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;51&lt;tt&gt;
&lt;/tt&gt;52&lt;tt&gt;
&lt;/tt&gt;53&lt;tt&gt;
&lt;/tt&gt;54&lt;tt&gt;
&lt;/tt&gt;55&lt;tt&gt;
&lt;/tt&gt;56&lt;tt&gt;
&lt;/tt&gt;57&lt;tt&gt;
&lt;/tt&gt;58&lt;tt&gt;
&lt;/tt&gt;59&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;61&lt;tt&gt;
&lt;/tt&gt;62&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;require &lt;span class="co"&gt;File&lt;/span&gt;.dirname(&lt;span class="pc"&gt;__FILE__&lt;/span&gt;) + &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;/../test_helper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;require &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;welcome_controller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;# Re-raise errors caught by the controller.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;WelcomeController&lt;/span&gt;; &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;rescue_action&lt;/span&gt;(e) raise e &lt;span class="r"&gt;end&lt;/span&gt;; &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;AnywhereController&lt;/span&gt; &amp;lt; &lt;span class="co"&gt;ActionController&lt;/span&gt;::&lt;span class="co"&gt;TestRequest&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;remote_ip&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@my_remote_ip&lt;/span&gt; || &lt;span class="iv"&gt;@remote_ip&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;go_to_sunnyvale&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# Google geocoding google? should work, right? ;-)&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@my_remote_ip&lt;/span&gt; = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;64.233.187.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;go_to_nowhere&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@my_remote_ip&lt;/span&gt; = &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;class&lt;/span&gt; &lt;span class="cl"&gt;GeocodingTest&lt;/span&gt; &amp;lt; &lt;span class="co"&gt;Test&lt;/span&gt;::&lt;span class="co"&gt;Unit&lt;/span&gt;::&lt;span class="co"&gt;TestCase&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;setup&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# Here we use our new AnywhereController&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@controller&lt;/span&gt; = &lt;span class="co"&gt;WelcomeController&lt;/span&gt;.new&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@request&lt;/span&gt;    = &lt;span class="co"&gt;AnywhereController&lt;/span&gt;.new&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@response&lt;/span&gt;   = &lt;span class="co"&gt;ActionController&lt;/span&gt;::&lt;span class="co"&gt;TestResponse&lt;/span&gt;.new&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;test_should_set_cookies_for_sunnyvale&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# Try out google's IP&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@request&lt;/span&gt;.go_to_sunnyvale&lt;tt&gt;
&lt;/tt&gt;    get &lt;span class="sy"&gt;:index&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assert_cookie &lt;span class="sy"&gt;:city&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;Sunnyvale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assert_cookie &lt;span class="sy"&gt;:state&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;CA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assert_cookie &lt;span class="sy"&gt;:geocoded&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;YES&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;test_should_not_set_cookies&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# And nowhere-land&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@request&lt;/span&gt;.go_to_nowhere&lt;tt&gt;
&lt;/tt&gt;    get &lt;span class="sy"&gt;:index&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assert_cookie &lt;span class="sy"&gt;:geocoded&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;def&lt;/span&gt; &lt;span class="fu"&gt;test_should_not_keep_trying&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# Changing the IP halfway through should have no affect, since&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="c"&gt;# we don't geocode if we've already tried, right?&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@request&lt;/span&gt;.go_to_nowhere&lt;tt&gt;
&lt;/tt&gt;    assert_equal &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="iv"&gt;@request&lt;/span&gt;.remote_ip&lt;tt&gt;
&lt;/tt&gt;    get &lt;span class="sy"&gt;:index&lt;/span&gt;  &lt;tt&gt;
&lt;/tt&gt;    assert_cookie &lt;span class="sy"&gt;:geocoded&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@request&lt;/span&gt;.go_to_sunnyvale&lt;tt&gt;
&lt;/tt&gt;    assert_equal &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;64.233.187.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="iv"&gt;@request&lt;/span&gt;.remote_ip&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="iv"&gt;@request&lt;/span&gt;.cookies[&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;geocoded&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;] = &lt;span class="co"&gt;CGI&lt;/span&gt;::&lt;span class="co"&gt;Cookie&lt;/span&gt;.new(&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;geocoded&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;)&lt;tt&gt;
&lt;/tt&gt;    get &lt;span class="sy"&gt;:index&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;    assert_cookie &lt;span class="sy"&gt;:geocoded&lt;/span&gt;, &lt;span class="sy"&gt;:value&lt;/span&gt; =&amp;gt; &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;end&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="r"&gt;end&lt;/span&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Whammo, as someone once said: “This code may not run, I have not tried it, I have only tested it”.  Our production server should geocode perfectly well if this test suite passes.&lt;/p&gt;

&lt;h1&gt;Finally, To Javascript&lt;/h1&gt;

&lt;p&gt;All right, we’ve run the gauntlet.  We have GeoKit installed, we’ve stuffed some data into our cookies, and we’ve tested the whole ordeal.  All that without disturbing our super-fast action-cached page just past this.  But our visitors still can’t see a thing.&lt;/p&gt;

&lt;p&gt;Well, Javascript has access to cookies, so let’s use that to push the info into a form field.  It’ll be cool, visitors will all get the same rendered page, but the headers will send different cookies.  The Javascript, which will itself be cached, can alter the rendered and drawn page so each visitor views something custom.&lt;/p&gt;

&lt;p&gt;Accessing cookies is a bit of a pain, but there are a few options, including a prototype module.  We’re going to stick with a few lines lifted from quirksmode.org and their &lt;a href="http://www.quirksmode.org/js/cookies.html"&gt;write up on cookies&lt;/a&gt;.  I’ll ignore the particulars, here’s the function we’ll use:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&lt;span class="r"&gt;function&lt;/span&gt; readCookie(name) {&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; nameEQ = name + &lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; ca = &lt;span class="pt"&gt;document&lt;/span&gt;.cookie.&lt;span class="fu"&gt;split&lt;/span&gt;(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;for&lt;/span&gt;(&lt;span class="r"&gt;var&lt;/span&gt; i=0;i &amp;lt; ca.length;i++) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; c = ca[i];&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;while&lt;/span&gt; (c.&lt;span class="fu"&gt;charAt&lt;/span&gt;(0)==&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;) c = c.&lt;span class="fu"&gt;substring&lt;/span&gt;(1,c.length);&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;if&lt;/span&gt; (c.&lt;span class="fu"&gt;indexOf&lt;/span&gt;(nameEQ) == 0) &lt;span class="r"&gt;return&lt;/span&gt; c.&lt;span class="fu"&gt;substring&lt;/span&gt;(nameEQ.length,c.length);&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;return&lt;/span&gt; &lt;span class="pc"&gt;null&lt;/span&gt;;&lt;tt&gt;
&lt;/tt&gt;}&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Toss that into your public/stylesheets/application.js and make sure you’ve included it in your application’s layout.  Now we’ll use a few functions from prototype and readCookie to load our data into some fields:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;12&lt;tt&gt;
&lt;/tt&gt;13&lt;tt&gt;
&lt;/tt&gt;14&lt;tt&gt;
&lt;/tt&gt;15&lt;tt&gt;
&lt;/tt&gt;16&lt;tt&gt;
&lt;/tt&gt;17&lt;tt&gt;
&lt;/tt&gt;18&lt;tt&gt;
&lt;/tt&gt;19&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;21&lt;tt&gt;
&lt;/tt&gt;22&lt;tt&gt;
&lt;/tt&gt;23&lt;tt&gt;
&lt;/tt&gt;24&lt;tt&gt;
&lt;/tt&gt;25&lt;tt&gt;
&lt;/tt&gt;26&lt;tt&gt;
&lt;/tt&gt;27&lt;tt&gt;
&lt;/tt&gt;28&lt;tt&gt;
&lt;/tt&gt;29&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;30&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;&amp;lt;script &lt;span class="r"&gt;type&lt;/span&gt;=&lt;span class="s"&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;text/javascript&lt;/span&gt;&lt;span class="dl"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&amp;gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//&amp;lt; ! [ C D A T A [&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;// Sorry, the above spaces are needed for a bug in html-scanner locally.&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;Event.observe( &lt;span class="pt"&gt;window&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;() {&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; zipcode_el = $(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;search[zipcode]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;if&lt;/span&gt; ( zipcode_el &amp;amp;&amp;amp; zipcode_el.value == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; zipcode = readCookie(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;zipcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (zipcode) { zipcode_el.value = zipcode }&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; city_el = $(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;search[city]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;if&lt;/span&gt; ( city_el &amp;amp;&amp;amp; city_el.value == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; city = readCookie(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (city) { city_el.value = city }&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;var&lt;/span&gt; state_el = $(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;search[state]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;if&lt;/span&gt; ( state_el &amp;amp;&amp;amp; state_el.options[state_el.selectedIndex].value == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; state = readCookie(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (state) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;for&lt;/span&gt; (&lt;span class="r"&gt;var&lt;/span&gt; i=0;i &amp;lt; state_el.length;i++){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; ( state_el.options[i].value == state ) {&lt;tt&gt;
&lt;/tt&gt;          state_el.selectedIndex = i;&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  }&lt;tt&gt;
&lt;/tt&gt;});&lt;tt&gt;
&lt;/tt&gt;&lt;span class="c"&gt;//]]&amp;gt;&lt;/span&gt;&lt;tt&gt;
&lt;/tt&gt;&amp;lt;/script&amp;gt;&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;We can even toss that in our form’s partial, and as long as everything else attached to onload uses:&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;Event.observe( &lt;span class="pt"&gt;window&lt;/span&gt;, &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="r"&gt;function&lt;/span&gt;() { } );&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;Then the onloads wont over-write each other.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  &lt;span class="r"&gt;var&lt;/span&gt; zipcode_el = $(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;search[zipcode]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;if&lt;/span&gt; ( zipcode_el &amp;amp;&amp;amp; zipcode_el.value == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; zipcode = readCookie(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;zipcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (zipcode) { zipcode_el.value = zipcode }&lt;tt&gt;
&lt;/tt&gt;  }&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This block first finds a field with the id of search[zipcode], aborts if it already has a value in it (say, from your server’s render), then writes the cookie contents into the element if the cookie exists.&lt;/p&gt;

&lt;table class="CodeRay"&gt;&lt;tr&gt;
  &lt;td title="click to toggle" class="line_numbers"&gt;&lt;pre&gt;1&lt;tt&gt;
&lt;/tt&gt;2&lt;tt&gt;
&lt;/tt&gt;3&lt;tt&gt;
&lt;/tt&gt;4&lt;tt&gt;
&lt;/tt&gt;5&lt;tt&gt;
&lt;/tt&gt;6&lt;tt&gt;
&lt;/tt&gt;7&lt;tt&gt;
&lt;/tt&gt;8&lt;tt&gt;
&lt;/tt&gt;9&lt;tt&gt;
&lt;/tt&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;tt&gt;
&lt;/tt&gt;11&lt;tt&gt;
&lt;/tt&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;td class="code"&gt;&lt;pre&gt;  &lt;span class="r"&gt;var&lt;/span&gt; state_el = $(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;search[state]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;  &lt;span class="r"&gt;if&lt;/span&gt; ( state_el &amp;amp;&amp;amp; state_el.options[state_el.selectedIndex].value == &lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt; ) {&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;var&lt;/span&gt; state = readCookie(&lt;span class="s"&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;/span&gt;);&lt;tt&gt;
&lt;/tt&gt;    &lt;span class="r"&gt;if&lt;/span&gt; (state) {&lt;tt&gt;
&lt;/tt&gt;      &lt;span class="r"&gt;for&lt;/span&gt; (&lt;span class="r"&gt;var&lt;/span&gt; i=0;i &amp;lt; state_el.length;i++){&lt;tt&gt;
&lt;/tt&gt;        &lt;span class="r"&gt;if&lt;/span&gt; ( state_el.options[i].value == state ) {&lt;tt&gt;
&lt;/tt&gt;          state_el.selectedIndex = i;&lt;tt&gt;
&lt;/tt&gt;        }&lt;tt&gt;
&lt;/tt&gt;      }&lt;tt&gt;
&lt;/tt&gt;    }&lt;tt&gt;
&lt;/tt&gt;  }&lt;/pre&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/table&gt;

&lt;p&gt;This block handles a select box in a similar manner, setting the state only if it hasn’t already been set.&lt;/p&gt;

&lt;h1&gt;&lt;em&gt;Whew&lt;/em&gt;&lt;/h1&gt;

&lt;p&gt;Well, that’s a wrap.  We’ve cached, we’ve cried, we’ve read cookies in Javascript and tested IPs our browsers didn’t even know existed.  Best yet, you can customizes pages by location now without fragging your caches, so scale with joy!&lt;/p&gt;

&lt;p&gt;This is my first write up on rails at this blog, thanks for stopping by.  My background is a pretty varied one, hence the name madhatted, but they’ll be a big focus on Rails and Javascript here, so stick around.  I’ll be adding links and such over the next few weeks, pardon my mess!&lt;/p&gt;
          &lt;img src="http://feeds.feedburner.com/~r/madhatted/~4/sEdMMKWvUMU" height="1" width="1"/&gt;</content>  <feedburner:origLink>http://madhatted.com/2007/5/31/yes-geocode-but-save-your-caches</feedburner:origLink></entry>
</feed>
