Using a duration field in a Rails model
- Store as integers in your database (number of seconds, probably).
- Your entry form will depend on the exact use case. Dropdowns are painful; better to use small text fields for duration in hours + minutes + seconds.
- Simply run a
SUM
query over the duration column to produce a grand total. If you use integers, this is easy and fast.
Additionally:
- Use a helper to display the duration in your views. You can easily convert a duration as integer of seconds to
ActiveSupport::Duration
by using123.seconds
(replace123
with the integer from the database). Useinspect
on the resultingDuration
for nice formatting. (It is not perfect. You may want to write something yourself.) - In your model, you'll probably want attribute readers and writers that return/take
ActiveSupport::Duration
objects, rather than integers. Simply defineduration=(new_duration)
andduration
, which internally callread_attribute
/write_attribute
with integer arguments.
In Rails 5, you can use ActiveRecord::Attributes to store ActiveSupport::Durations as ISO8601 strings. The advantage of using ActiveSupport::Duration over integers is that you can use them for date/time calculations right out of the box. You can do things like Time.now + 1.month
and it's always correct.
Here's how:
Add config/initializers/duration_type.rb
class DurationType < ActiveRecord::Type::String def cast(value) return value if value.blank? || value.is_a?(ActiveSupport::Duration) ActiveSupport::Duration.parse(value) end def serialize(duration) duration ? duration.iso8601 : nil endendActiveRecord::Type.register(:duration, DurationType)
Migration
create_table :somethings do |t| t.string :durationend
Model
class Something < ApplicationRecord attribute :duration, :durationend
Usage
something = Something.newsomething.duration = 1.year # 1 yearsomething.duration = nilsomething.duration = "P2M3D" # 2 months, 3 days (ISO8601 string)Time.now + something.duration # calculation is always correct
I tried using ActiveSupport::Duration but had trouble getting the output to be clear.
You may like ruby-duration, an immutable type that represents some amount of time with accuracy in seconds. It has lots of tests and a Mongoid model field type.
I wanted to also easily parse human duration strings so I went with Chronic Duration. Here's an example of adding it to a model that has a time_spent in seconds field.
class Completion < ActiveRecord::Base belongs_to :task belongs_to :user def time_spent_text ChronicDuration.output time_spent end def time_spent_text= text self.time_spent = ChronicDuration.parse text logger.debug "time_spent: '#{self.time_spent_text}' for text '#{text}'" endend