Displaying Dates and Times Using JavaScript

Some considerations when displaying dates and times on a website include showing delta times, customized timezones and caching. Often it's nice to show a delta time like "10 minutes ago" or "5 days ago" to give readers a frame of reference instead of an absolute date. When the date is far enough in the past and an absolute date becomes desired, customizing the date to the user's timezone is useful. And if your site grows large enough that caching becomes useful, finding a way to display customized deltas and timezone information in a cacheable static page becomes an ideal solution.

JavaScript is an ideal solution for all three issues. With JavaScript you can place an absolute date in the web page and have the JS dynamically update it when the page is loaded. This can be used to calculate delta times and accommodate timezones as well. The result is that the page can embed the same date every time and thus becomes more cache-friendly.

The Typo blog engine (which runs this blog) comes with a useful MIT-licensed JavaScript in it's typo.js script. Just copy three of the JS date/time functions, wrap your dates with spans (using the appropriate class name and absolute date in the span title) and then call show_dates_as_local_time() when your page is finished loading. The two other functions you'll need are get_local_time_for_date(time) and distance_of_time_in_words(minutes). This is what I did for Planet Catalyst's Plagger theme a while back.

Although it's pretty easy to accommodate timezones, the Typo script doesn't do that. I've done this for some projects and might post some code in the future but it's not hard.

Customization and cacheability, two great advantages for using JavaScript to handle dates and times.

Displaying Dates and Times via JavaScript

Some considerations when displaying dates and times on a website include showing delta times, customized timezones and caching. Often it's nice to show a delta time like "10 minutes ago" or "5 days ago" to give readers a frame of reference instead of an absolute date. When the date is far enough in the past and an absolute date becomes desired, customizing the date to the user's timezone is useful. And if your site grows large enough that caching becomes useful, finding a way to display customized deltas and timezone information in a cacheable static page becomes an ideal solution.

JavaScript is an ideal solution for all three issues. With JavaScript you can place an absolute date in the web page and have the JS dynamically update it when the page is loaded. This can be used to calculate delta times and accommodate timezones as well. The result is that the page can embed the same date every time and thus becomes more cache-friendly.

The Typo blog engine comes with a useful MIT-licensed JavaScript in it's typo.js script. Just copy three of the JS date/time functions, wrap your dates with spans (using the appropriate class name and absolute date in the span title) and then call show_dates_as_local_time() when your page is finished loading. The two other functions you'll need are get_local_time_for_date(time) and distance_of_time_in_words(minutes). This is what I did for Planet Catalyst's Plagger theme a while back.

Although it's pretty easy to accommodate timezones, the Typo script doesn't do that. I've done this for some projects and might post some code in the future but it's not very hard.

Customization and cacheability, two great advantages for using JavaScript to handle dates and times.

Technical Entitlement

At RailsConf Europe this year, DHH went in quite strong on the idea that simply using an opensource framework, like, say, Rails didn’t entitle you to much of anything. The precise phrase used was, if memory serves “We don’t owe you shit.”

Which is why I’m finding a certain irony in this, from the official Rails weblog:

Versions [of SQLite] after 3.3.7 incompatibly changed the way in which default values are stored, making it so that current versions of Rails get into quoting issues and problems with columns with NULL defaults . . . hopefully this will be recognized as an unacceptable change for a point release and will be reverted.

There appears to be a fair amount of confusion going on in the responses to Jamis’s bug report on the SQLite trac. It seems a Rails user has already reported the bug in the changed behaviour of 3.3.8, but the fix for it seems to be differently incompatible with Rails’ expectations…

This one could run for a while I think.

Rails Workshop in Salt Lake City, March 15

Register

Updated with more info about content and times.

Initial release of acts_as_resource

Right. I’ve bundled acts_as_resource up and stuck it on the typosphere SVN server. You can grab it from http://svn.typosphere.org/typo/plugins/acts_as_resource if you’re interested.

It’s currently in what I’d call an all convention, no configuration state – if your resources don’t look pretty similar to the kind of things you get from the resource scaffolding, you’ll probably have some pain, but I expect to rectify that with coming releases. One thing I want/need to do for instance is to allow for ‘relative’ ids in your resource url. For instance, if you’re looking at /albums/10/tracks/982, it’s not the most readable of permalinks… next trick is to allow you to have urls like /albums/because-its-there/tracks/1, ie: the first track on the album ‘Because it’s There’. I’m sort of expecting that you’d do that by doing:

class Album
  has_many :tracks
  acts_as_resource :uri_field => :name_dasherized
end

class Track
  belongs_to :album
  acts_as_list
  acts_as_resource :uri_field => :position, :parent => :album
end

However, my first priority is to add some tests (or, more likely,Rspec specifications) so I’ve got some confidence that I’m not breaking things as I go.

Anyhow, go grab the plugin, have a play, let me know what you think.

'acts_as_resource' progress

I’m very nearly ready to release acts_as_resource, I just have to pull up and tidy code that’s currently in my working directory’s ApplicationController and we’re laughing. However, I thought you’d like to see what my nested controller looks like.

Tips for data smugglers

While I was working on the acts_as_resource plugin trying to fix things up so that the resource finding side of things works neatly, I realised that I needed some way to get at the ordered list of parameter keys that were matched by the routing system.

One way to do it would have been to parse the path again, but that smacked a little too much of repetition, after all, the routing system knows this stuff already, but how to get at it?

My first 'acts_as' plugin

So, you’ve upgraded to Rails 1.2.1 and you’re working on a tool to maintain a database of all the tunes you have in your various songbooks and (eventually) your record collection. You start with:

$ ./script/generate rspec_resource MusicBook title:string author_id:integer \
  abstract:text
$ ./script/generate rspec_resource Tune title:string composer_id:integer \
  abc:text book_id:integer

You decide to come back to composers and authors later, so you set up your models1:

MusicBook.has_many :tunes
Tune.belongs_to :music_book

And your routes:

map.resource :music_books do |book|
  book.resource :tunes
end

Problems start here

Being a cautious sort, before you start adding behaviour, you fire up a development server and go and check things with the browser. The /music_books/ stuff works fine, but once you start looking at /music_books/1/tunes things start to get weird; all of a sudden your links aren’t making sense.

memcached Basics for Rails: Part II

In Part I I showed how easy it is to install memcached and use it for simple queries or manually stored objects.

Today I was experimenting with running against multiple memcached servers. I was preparing myself for a gruelling process of learning the memcache server configuration format, headaches while trying to get the two servers to talk to each other, etc. Then I read this:

...the API hashes your key to a unique server. If a host goes down, the API re-maps that dead host’s requests onto the servers that are available.

So the servers run without any knowledge of each other. All you have to do is start up a few instances of memcached, then tell your Rails app about them. The Ruby API is responsible for finding out about the servers and distributing the keys between them. It even recovers if one dies.

So easy!

Configuration

I tried this out using a laptop and a desktop machine. The only quirk was that I initially had a firewall on my desktop machine so the memcached port wasn’t open.

# In production.rb
require 'cached_model'

memcache_options = {
  :c_threshold => 10_000,
  :compression => true,
  :debug => false,
  :namespace => 'rails_production',
  :readonly => false,
  :urlencode => false
}

CACHE = MemCache.new memcache_options

# These are the IP addresses and ports of the memcached servers
CACHE.servers = ['192.168.0.3:11211', '192.168.0.2:11211']

Sessions

UPDATE: Argh…my current implementation of this isn’t any faster than ActiveRecord sessions. Back to the drawing board…

UPDATE 2: See the updated plugin for a Memcache session class that uses memcache-client instead of ruby-memcache.

Memcached can sometimes be faster than sessions stored in the database (see RailsExpress). The problem is that you lose all your sessions if you restart the memcached servers.

I have heard legends of an epic session library used at the Robot Coop. Sessions are stored primarily in the database but are also kept in memcached for speedy lookup. I found a nice SqlBypass session template in the Rails source and modified it to work similarly (packaged as a plugin). I’ll be benchmarking it later this week, but it works in the situations I’ve tried it in.

./script/plugin install http://topfunky.net/svn/plugins/db_memcache_store/
# In the Initializer section of environment.rb.
# Explicitly load the library since plugins haven't been loaded at this point.
require "#{RAILS_ROOT}/vendor/plugins/db_memcache_store/lib/db_memcache_store" 
config.action_controller.session_store = CGI::Session::DBMemcacheStore

Startup a few memcached servers.

#           Verbose, Port 11211, 128 MB RAM, IP to listen on
$ memcached -vv      -p 11211    -m 128      -l 192.168.0.2

You’ll see sessions being stored and retrieved. Kill one or more servers and it will keep working.

# Memcached server output
<3 server listening
<6 new client connection
<6 get rails_development:session:bd8b2ba714c059002af6a19283072997
>6 sending key rails_development:session:bd8b2ba714c059002af6a19283072997
>6 END
<6 set rails_development:session:bd8b2ba714c059002af6a19283072997 0 0 216
>6 STORED
<6 get rails_development:session:bd8b2ba714c059002af6a19283072997
>6 sending key rails_development:session:bd8b2ba714c059002af6a19283072997
>6 END

TODO

Actually, it’s back to the drawing board for this one…I found other details about the system used at the Robot Coop and other uses of memcached elsewhere.

Continuing Sudoku

Your mission, should you choose to accept it, is to explain what the following code does:

class Amb
  def initialize
    @error = Exception.new("Ran out of possibilities")
    @failure_continuation = lambda {|v| @error}
  end

  def assert(assertion)
    if !assertion
      self.fail
    end
  end

  def deny(assertion)
    assert !assertion
  end

  def fail
    @failure_continuation.call(nil)
  end

  def maybe
    one_of [true, false]
  end

  def one_of(collection = [])
    k_prev = @failure_continuation
    callcc do |k_entry|
      collection.each do |item|
        callcc do |k_next|
          @failure_continuation = lambda do |v|
            @failure_continuation = k_prev
            k_next.call(v)
          end
          k_entry.call(item)
        end
      end
      result = k_prev.call(nil)
      if result == @error
        raise @error.message
      end
    end
  end

  def all_values(&a_block)
    k_prev = @failure_continuation
    results = []
    callcc do |k_retry|
      @failure_continuation = lambda {|v| k_retry.call(false)}
      results << a_block.call
      k_retry.call(true)
    end && fail
    @failure_continuation = k_prev
    results
  end
end

Easy? Now explain how it does it.

Five things you probably don't know about me...

I’ve been watching this five things meme coursing round the blogs I read for a while now, and chromatic just tagged me to share five little-known personal facts.

  1. I like to collect gameroom decorations, like neon signs, antique beer signs, old juke boxes, pool cues, and pool tables, .
  2. I build cedar wood dog houses.
  3. Unless you were at that OSCON that time, you probably don’t know that I can make a trampoline out of modelling balloons. If anyone has worked out how to make a buckyball from balloons, I would be grateful for the pointer.
  4. I’m a compulsive gambler: it’s been years since I had a bet and more years still since I attended a Gamblers’ Anonymous meeting. I basically threw away thousands of dollars and any chance I had of a decent degree. I can’t say I’d recommend it.
  5. I am a tea snob. Not for me the Tetley’s teabag in a mug being doused with boiling water (but if must be bag tea, then Tetley’s is the one), oh no. In the afternoon it must be a pot made with loose leaf Earl Grey China Moon from Imperial Tea and Coffee of Lincoln in the morning, a pot of Superior English Breakfast from the same place. Tell ‘em I sent you.

Tradition demands that I now tag 5 others to do the same thing, so I call on: Scott Laird, Jesse Vincent, Dave Cross, Andy Lester and Mary Branscombe to spill their beans.

Word of the day: Musicking

A friend of mine, David Morton, just pointed me at a transcript of a lecture given by one Christopher Small. In it Small nails something I’ve been trying to articulate for ages. I don’t know whether to applaud madly or seethe with silent resentment that someone said it so much better, and in 1995 at that.

On balance, I’m applauding. Read it, it’s worth it for Opera story alone.

PeepCode: Capistrano Concepts and a Free Rails Ubuntu Screencast

Capistrano Concepts is the newest PeepCode (released earlier this week). People are already saying that “it put a lot of things into perspective and really helped [me] grasp the concepts. I think I’ve learned Rails faster from your screencasts than all the books I have combined.” (Jon Baer, Developer)

Also available as a free bonus is a 10 minute screencast showing you how to install a full Rails stack on Ubuntu using the deprec gem. It’s a great way to build a staging server or a sandbox for experimentation. Download it at the new site.

The new site also has some new features that people have been asking for:

  • The option to sign up and keep a record of the screencasts you purchase from now on
  • Subscribe for a discount
  • All screencasts are now available in a format compatible with the video iPod
  • Now running on Mongrel and speedy new RailsMachine hardware
  • Other features coming soon…

A Hodel 3000 Compliant Logger for the Rest of Us

The Rails Analyzer Tools are a very useful way to keep tabs on the performance of your site. They were written by Eric Hodel and have been open-sourced to the community by the Robot Co-Op.

The problem is that you need to install and run SysLogLogger to make it work. If you have more than one Rails app on a box, or if you don’t have root access to the box, or if you are on a shared host, you are out of luck.

No longer!

Install the Gems on Your Server

gem install production_log_analyzer
gem install rails_analyzer_tools

Install the Hodel3000CompliantLogger in your Rails app

Download the logger replacement and put it in your lib directory. (I’ll make this into a proper plugin soon.)

In the Initializer section of environment.rb, add

require 'hodel_3000_compliant_logger'
config.logger = Hodel3000CompliantLogger.new(config.log_path)

Run your app

For some reason, the standard script/server forces logging to work the old way. If I run mongrel_rails start, it works fine. I’m also running a production app with mongrel and it works as expected.

If you tail -f log/development.log you should see something like this:

Jan 03 10:08:09 topfunky rails[4535]: Rendered shared/_menu (0.14430)
Jan 03 10:08:09 topfunky rails[4535]: Rendered shared/_flashes (0.00882)
Jan 03 10:08:09 topfunky rails[4535]: Completed in 1.70117 (0 reqs/sec) | Rendering: 1.61409 (94%) | DB: 0.02340 (1%) | 200 OK [http://localhost/products/capistrano-concepts]

Analyze

Deploy your app, or just try this out locally from the command-line.

$ pl_analyze log/development.log

Request Times Summary:          Count   Avg     Std Dev Min     Max
ALL REQUESTS:                   33      0.226   0.304   0.005   1.695

OrdersController#show:          6       0.071   0.112   0.005   0.316
ProductsController#show:        3       0.306   0.061   0.231   0.381
ProductsController#home:        2       0.318   0.068   0.249   0.386
OrdersController#index:         2       0.328   0.206   0.123   0.534
PagesController#show:           2       0.432   0.047   0.385   0.479
ProductsController#index:       1       0.279   0.000   0.279   0.279

Slowest Request Times:
        OrdersController#index took 0.534s
        PagesController#show took 0.479s
        ProductsController#home took 0.386s
        PagesController#show took 0.385s
        ProductsController#show took 0.381s

# DB times and Render times follow
rails_stat is also nice for seeing a live report of the performance of your app.
$ rails_stat log/development.log

~ 0.9 req/sec, 14.2 queries/sec, 19.9 lines/sec
~ 0.3 req/sec, 14.1 queries/sec, 17.0 lines/sec

Once more, with feeling!

Run the report in a cron task and send the results to yourself via email (see the -e flag to pl_analyze), or automate the reporting with Capistrano.

desc "Analyze Rails Log instantaneously" 
task :pl_analyze, :roles => :app do
  run "pl_analyze #{shared_path}/log/#{rails_env}.log" do |ch, st, data|
    print data
  end
end

desc "Run rails_stat" 
task :rails_stat, :roles => :app do
  stream "rails_stat #{shared_path}/log/#{rails_env}.log" 
end

For more details on Capistrano, buy the new PeepCode Capistrano Concepts screencast.

And again, from the top

For extra credit, use my Mint Pepper plugin (download) and my Mint Rails Plugin to put nightly data into the database (also requires the setup of logrotate, to be discussed later).

Note

Remember, this is not about maximum performance. These reports show you the actual performance of your app as it is being browsed, not the theoretical maximum performance.

Shameless Advert

Dynamic Graphics with Rails 1.2

The upcoming release of Rails 1.2 has some nice features for creating dynamic graphics in your application.

Here we have a simple shopping cart icon (purchased and modified from the Iconfactory).

I want to show the number of items in the cart. I could manually create a series of graphics with each number, but that seems inelegant. Anytime the icon needed tweaking, I would have to regenerate all the icons.

Rails 1.2 has the ability to send different types of content from the same action. Basically, I just want a graphical representation of the shopping cart. I’ll use the Cart#show action to render a graphic if it is accessed with a “png” extension. Otherwise, it will show the shopping cart items, total price, and checkout button in HTML.

Add a Content-Type

In config/environment.rb:

# For the drawing
require "RMagick" 

Mime::Type.register "image/png", :png

The Action

Purchase a TTF font and copy it to your Rails app so it can be deployed to the server. I put it in artwork/fonts.

class CartsController < ApplicationController

  def show
    @order = Order.find(params[:id])
    respond_to do |format|
      format.html do
        # Render the show.rhtml template
      end

      format.png do
        # Show cart icon with number of items in it
        icon = Magick::Image.read("#{RAILS_ROOT}/public/images/cart.png").first

        drawable = Magick::Draw.new
        drawable.pointsize = 18.0
        drawable.font = ("#{RAILS_ROOT}/artwork/fonts/VeraMono.ttf")
        drawable.fill = 'black'
        drawable.gravity = Magick::CenterGravity

        # Tweak the font to draw slightly up and left from the center
        drawable.annotate(icon, 0, 0, -3, -6, @order.quantity.to_s)

        send_data icon.to_blob, :filename => "#{@order.id}.png", 
                                :disposition => 'inline', 
                                :type => "image/png" 
      end

    end
  end

Reference the dynamic icon

Add some logic to your view (or a helper) to draw the icon with the number if the cart has items in it. You can use a regular image tag, but reference the controller instead:

# Generates an image tag to "/carts/1.png" 
image_tag formatted_cart_path(@order, :png)

The result

Caveat

If you can, use caching to speed up the delivery of images that have already been rendered. Rails doesn’t automatically apply the correct extension for non-standard content-types CORRECTION: This has been fixed and now works smoothly with rev 5736 (and maybe earlier revisions).

Resources

PeepCode Screencasts Places a $1,000 Bet on Rubinius

This is an awesome time to be a Rubyist. Ruby is already a great language, but many brilliant programmers are working on projects that will make it even better.

After hearing a RubyConf presentation from Seattle.rb member Evan Phoenix I had wanted to find a way to help. It’s a project to write a fast, maintainable, Ruby 1.8-compatible interpreter. Last night I learned that Evan is between jobs and won’t start his next job until January. This seemed like a fantastic opportunity to support the Rubinius Project financially and give him a reason to spend the month of December working on it.

PeepCode Screencasts has been more successful than I expected and I want to give back to the community. So I’m putting US$1,000 toward the development of Rubinius in the month of December. Yes, there have been other funding drives recently, but this is a real project with real code that is being actively worked on.

If you want to support the Rubinius Project now or in the future, you can send money via PayPal to evan (at) fallingsnow (dot) net.

Questions

  • Why?: The current Ruby interpreter powers many high traffic sites and enterprise projects, but a faster, more maintainable interpreter would be a huge benefit to current and future Ruby projects.
  • Why not YARV?: Matz has expressed support for other Ruby interpreters and has said that YARV has a difficult task since the specs of Ruby 1.9/2.0 are not finalized yet (mentioned in this interview). Rubinius is targeting the current Ruby 1.8 series and is using the existing interpreter to bootstrap the project.
  • When will it be done?: There is no timeline. It may take 6 months, 6 years, or never. The idea is to put resources behind this project so progress can be made.

Other information about Rubinius

What I've been up to recently

Cast an eye over my new venture. It’s pretty much a place holder site at the time being, but I’m beavering away at the bits that need beavering away at.

Blessed are the toolmakers

My dad drives a vintage Fraser Nash. I say drives, but that’s only half the battle, a large part of his Nash time is spent fettling it. It’s an old car; bits wear out, break or drop off. And because it’s an old car, you can’t just nip round to Halfords and pic up a replacement; nor can you head down to the breaker’s yard and cannibalize something else. So he has a lathe and a milling machine and a bewildering collection of tools. When he needs a part, he will disappear into the machine shop and, after sufficient swearing and/or bleeding, he will emerge with a newly made part. For dad, it’s all part of the fun of running a vintage car. If he weren’t able to do the work, the Nash would have had to remain a pleasant pipedream.

I don’t know my way around a machine shop, except in the vaguest and most theoretical way. The tools I’ve grown up knowing to use are programming languages, editors, fine manuals and the mental tools a grounding in mathematics brings.

So, when I’m putting a new photography business together, and I realise that a couple of the supporting software tools that I had vaguely assumed ‘should exist’ don’t actually exist, I know that it doesn’t matter. I may not know Cocoa programming yet, but I know programming, so I’m confident that, like dad in his machine shop, I’ll be able to knock something up that does the job.

On reflection, I realised that this is probably a good thing. If I can set up and run the business with a combination of off the shelf software, then it’s trivial for potential competitors to reverse engineer the business and do the same (let’s assume here that the business is a success) and I’m left competing on margin in a service industry. No fun at all.

Being able to make my own tools gives me a competitive edge.

Why aren’t there more tool makers?

Campin' in Oz, PeepCode Test-First Development

Sydney, Australia is a great place. I taught a basic and an advanced Rails workshop there and only got back to Seattle last week. It was a little rough leaving 25°C and coming back to 25°F, but I’ll survive!

The Ruby community there is awesome. Myles Byrne gave a talk on a presenter app he wrote in Camping. A few months ago I had an idea for an app that could be used to present and could also be viewed locally by people listening. This makes that possible and could even make an interactive presentation possible where one of the slides is just a frame to Try Ruby or a screen of code that people could copy to their own machine.

I also offered an idea for a Ruby/CSS plugin in the vein of Builder and Markaby (originally developed by Scott Barron). Keith Rowell immediately took the idea on and is working on a plugin (the name may change).

PeepCode 4: Test-First Development

The Ruby community is awesome and has fully embraced my idea for a screencast series. I’m glad to be able to spend time on it.

This week, it’s Test-First Development. Already, people have said

I really love the Test First method. I’m doing it now and it is MUCH better. I feel far more confident about my code than I did before.

Thank for you for the peepcode video – it made my day to see a new post on the peepcode blog in my RSS reader, especially on test-first development.

I was also experimenting yesterday with running Debian on Parallels, which allows me to try out some EXTREME!! Capistrano programming without fear of turning my VPS into a brick. I’ve already got a series of tasks that helps me do a full build of Apache2.2 and remotely control apt-get from Capistrano. Look for a gem and screencast in a few weeks…

Getting the Rspec religion

I’ve been eyeing the rspec and rspec on rails packages and thinking I should give them a go.

To my eye at least, something like:

context 'Given a published article' do
  fixtures :contents

  setup { @article = contents(:published_article) }

  specify 'changing content invalidates the cache' do
    @article.body = 'new body'
    @article.invalidates_cache?.should_be true
  end
end

context 'Given an unpublished article' do
  fixtures :contents

  setup { @article = contents(:unpublished_article) }

  specify 'changing content keeps the cache' do
    @article.body = 'new body'
    @article.invalidates_cache?.should_be false
  end
end

reads far more fluently than the equivalent Test::Unit based tests:

class CacheSupportTest < Test::Unit::TestCase
  fixtures :contents

  def test_changing_published_article_invalidates_the_cache
    art = contents(:published_article)
    art.body = 'new body'
    assert art.invalidates_cache?
  end

  def test_changing_unpublished_article_keeps_the_cache
    art = contents(:unpublished_article)
    art.body = 'new body'
    assert ! art.invalidates_cache?
  end
end

So, I installed everything and started to work on a new class in Typo using rspec. Rather annoyingly, this seemed to break the current test suite, so instead of working on my new model class, I set to porting the existing suite.

And, on about my third test suite, I found what I think is a bug in the suite. I’m not sure it’s a bug, because, the way the test is written (by me, I admit it), masks the intent quite dramatically. I’m also finding that the freedom to name specifications and contexts in English rather than method_names_that_go_on_for_ever is forcing me to come up with much more useful descriptions of what I’m testing. I find myself working on making the spec runner output read reasonably well as English, and doing that casts light on what is and isn’t being tested.

I’ve known for a while that Typo’s test suite is, um, spotty, but the porting process is really helping me get familiar with what’s being tested. I’m half tempted to start adding extra specs as I go, and if I could work out how to keep the existing tests working while I did it, I would, but my priority for now is to get to the point where I can check the specs and be confident that the new specs are no worse than the old tests.

Because I’m much more confident that I know what the specs are doing, I’m also confident that it won’t be hard to revisit them to help specify typo’s behaviour better. I’ll just have to give myself the discipline of beginning each coding session with half an hour of fleshing out the specifications before I get back to adding behaviour.