Mongoid Metastamp

Provides Mongoid with enhanced meta-timestamps that store additional parsed time metadata, allowing more powerful querying on specific time fields and across normalized time zones.

What It Does

(Or why would I want to use this?)

Storing simple timestamps is all well and good if your queries are simple or involve just one timezone. But sometimes you need to search for each timestamp relative to the timezone it was created in. This is common when you have more than one location. For example:

  • Find all flights that depart from all airports between 1:00pm and 2:00pm local (airport) time.
  • Return all employees that clocked in later than 8:00am local time to any of our nationwide locations.

Other times you want to be able to query very specific parts of the date or time that typically can't be accessed without parsing it:

  • Find all transactions that occurred on weekdays after 12pm in 2010.
  • Return all users that signed up the first week of every month over the last 3 years.

Typically to do these things, you'd need to add a bunch of of complex time ranges to your query. Or you might query the entire range and loop through each result, running additional tests on the parsed time.

Using mongoid-metastamp gives you a custom time field type that is normalized and parsed beforehand, and then stored in a MongoDB friendly way for easy querying.

Installation

In your Gemfile

gem 'mongoid-metastamp'
$ bundle install

Usage

For the most part, Mongoid::Metastamp::Time fields can be used just like regular Time fields:

class MyEvent
  include Mongoid::Document
  field :starts_at, type: Mongoid::Metastamp::Time
  field :ends_at,   type: Mongoid::Metastamp::Time
end
event = MyEvent.new
event.starts_at = Time.now
event.ends_at = Time.now + 1.day
event.save
# => true

event.starts_at
# => Wed, 5 Oct 2011 20:46:22 UTC +00:00
event.ends_at
# => Thu, 6 Oct 2011 20:46:31 UTC +00:00

Data Stored

However, behind the scenes the following meta fields will transparently be stored inside your Mongoid::Metastamp::Time field:

  • time (Date)
  • normalized (Date)
  • year (Int)
  • month (Int)
  • day (Int)
  • wday (Int)
  • hour (Int)
  • min (Int)
  • sec (Int)
  • zone (String)
  • offset (Int)

So given a field named timestamp:

class MyEvent
  include Mongoid::Document
  field :timestamp, type: Mongoid::Metastamp::Time
end

You can access the raw metadata fields like this:

event = MyEvent.new(timestamp: "2011-10-05 10:00:00 -0800")

event['timestamp']
# => {"time"=>2011-10-05 17:00:00 UTC, "normalized"=>2011-10-05 10:00:00 UTC, "year"=>2011, "month"=>10, "day"=>5, "wday"=>3, "hour"=>10, "min"=>0, "sec"=>0, "zone"=>"-08:00", "offset"=>-25200}

event['timestamp']['month']  # => 10
event['timestamp']['day']    # => 5
event['timestamp']['year']   # => 2011
event['timestamp']['zone']   # => "-08:00"

The time meta-field is special and stores whatever you assign the field to.

event['timestamp']['time']
# => 2011-10-05 17:00:00 UTC

It will also be the value deserialized when you access the timestamp field.

event.timestamp
# => Wed, 05 Oct 2011 17:00:00 UTC +00:00

The normalized meta-field is the time normalized to a UTC value. This is useful when you want to query ignoring local offsets.

eastern_event = MyEvent.new(timestamp: "2011-10-05 10:00:00 -0500")
pacific_event = MyEvent.new(timestamp: "2011-10-05 10:00:00 -0800")

eastern_event['timestamp']['time']        # => 2011-10-05 14:00:00 UTC
eastern_event['timestamp']['normalized']  # => 2011-10-05 10:00:00 UTC

pacific_event['timestamp']['time']        # => 2011-10-05 17:00:00 UTC
pacific_event['timestamp']['normalized']  # => 2011-10-05 10:00:00 UTC

Querying

Since the time meta-field is the default, it can be queried as just timestamp:

good_old_days = Day.where(:timestamp.lt => 20.years.ago)

or as timestamp.time:

good_old_days = Day.where("timestamp.time" => { '$lt' => 20.years.ago })

For now, the other meta-fields need to be queried using the full syntax:

hump_days = Day.where("timestamp.wday" => 5)
# => Only Wednesdays

afternoon_delights = Delight.where("timestamp.hour" => { '$gte' => 12, '$lt' => 15 })
# => Only between 12pm and 3pm

See the search specs for more examples.

Todo

  • Add custom finder methods and scopes
  • Migration task to convert existing time fields
  • Additional field types

License

Copyright (c) 2011 Peter Gumeson. See LICENSE for full license.