Using a duration field in a Rails model Using a duration field in a Rails model ruby ruby

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 using 123.seconds (replace 123 with the integer from the database). Use inspect on the resulting Duration 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 define duration=(new_duration) and duration, which internally call read_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