36

I'm looking for the best way to use a duration field in a Rails model. I would like the format to be HH:MM:SS (ex: 01:30:23). The database in use is sqlite locally and Postgres in production.

I would also like to work with this field so I can take a look at all of the objects in the field and come up with the total time of all objects in that model and end up with something like:

30 records totaling 45 hours, 25 minutes, and 34 seconds.

So what would work best for?

  • Field type for the migration
  • Form field for the CRUD forms (hour, minute, second drop downs?)
  • Least expensive method to generate the total duration of all records in the model
mwilliams
  • 9,946
  • 13
  • 50
  • 71

4 Answers4

46
  • 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.
molf
  • 73,644
  • 13
  • 135
  • 118
27

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
  end
end

ActiveRecord::Type.register(:duration, DurationType)

Migration

create_table :somethings do |t|
  t.string :duration
end

Model

class Something < ApplicationRecord
  attribute :duration, :duration
end

Usage

something = Something.new
something.duration = 1.year    # 1 year
something.duration = nil
something.duration = "P2M3D"   # 2 months, 3 days (ISO8601 string)
Time.now + something.duration  # calculation is always correct
ToTenMilan
  • 582
  • 1
  • 9
  • 19
Anthony Wang
  • 1,285
  • 1
  • 13
  • 14
  • I've tried this in a rails engine and when I `db:migrate`, the command doesn't report an error but the generated schema contains `# Could not dump table "mynamespace_mytable" because of following StandardError # Unknown type 'duration' for column 'duration_column'` – Harry Lime Nov 04 '22 at 08:52
  • My mistake was trying to use `:duration` as the column type – Harry Lime Nov 04 '22 at 10:34
6

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}'"
  end

end
Turadg
  • 7,471
  • 2
  • 48
  • 49
4

I've wrote a some stub to support and use PostgreSQL's interval type as ActiveRecord::Duration.

See this gist (you can use it as initializer in Rails 4.1): https://gist.github.com/Envek/7077bfc36b17233f60ad

Also I've opened pull requests to the Rails there: https://github.com/rails/rails/pull/16919

Envek
  • 4,426
  • 3
  • 34
  • 42
  • We wrote an updated gist for Rails 5.1: https://gist.github.com/vollnhals/a7d2ce1c077ae2289056afdf7bba094a – lion.vollnhals Nov 19 '17 at 10:40
  • @Envek thanks mate you saved me a lot of time. Here is the final PR: https://github.com/rails/rails/commit/0475215d4fa1a6db2a92a0065081fe19c64cc124 – BenKoshy Mar 23 '22 at 07:24