duration.rb
Description
Objects for handling durations of time.
Installation
Add this line to your application's Gemfile:
gem 'duration.rb'
And then execute:
$ bundle
Or install directly:
$ gem install duration.rb
Philosophy
This is the anti-ActiveSupport::Duration. There, every duration is really a
number of seconds and the unit is decoration: 1.hour == 3600. Here the unit
is the class. A Minutes holds minutes — not seconds it renders as minutes —
so 30.minutes.to_i is 30, not 1800. No unit is privileged; there is no
base. Conversion is a behaviour (to_hours), not a representation.
Because nothing is normalised at construction, exactness is yours to keep: hand
a constructor an Integer or a Rational and it stays exact all the way
through, since conversions divide by exact ratios rather than through a float.
Hand it a Float and it stays a Float — the library never manufactures
precision and never silently discards it.
Duration::Minutes.new(5).to_hours.to_f # => 0.08333333333333333
Duration::Minutes.new(5).to_hours # holds exactly 1/12, not 0.0833...
(Duration::Hours.new(1) - Duration::Minutes.new(50)).to_minutes.to_f
# => 10.0 exactly (no float drift)
Loading
require 'duration' # the duration classes only
require 'duration-numeric' # opt in to the Numeric sugar (5.minutes, etc.),
# which monkeypatches Numeric
The examples below use the Numeric sugar, so they assume
require 'duration-numeric'. Without it durations are constructed directly with
Duration::Minutes.new(5) and so on.
Either require declares where the unit classes are without reading them. A unit file is read when its class is first named, so a program dealing in seconds and minutes reads the files for seconds and minutes and leaves the rest unread.
Usage
Basic Duration Creation
# Create durations using convenience methods
500.nanoseconds # => #<Nanoseconds @nanoseconds=500>
500.microseconds # => #<Microseconds @microseconds=500>
200.milliseconds # => #<Milliseconds @milliseconds=200>
1.second # => #<Seconds @seconds=1>
30.minutes # => #<Minutes @minutes=30>
2.hours # => #<Hours @hours=2>
7.days # => #<Days @days=7>
4.weeks # => #<Weeks @weeks=4>
6.months # => #<Months @months=6>
Conversions
# The number is in the unit's own terms, not a base:
30.minutes.to_i # => 30 (not 1800)
30.minutes.to_f # => 30.0
# Convert between units. The result is exact (a Rational internally) when the
# input is exact; call to_f at the edge for a float.
90.minutes.to_hours # => #<Hours @hours=(3/2)>
90.minutes.to_hours.to_f # => 1.5
1.5.hours.to_minutes.to_i # => 90
1.day.to_seconds.to_i # => 86400
5.minutes.to_hours.to_f # => 0.08333333333333333
Arithmetic and Comparison
# Durations add and subtract, with the left operand's unit deciding the result:
5.minutes + 30.seconds # => #<Minutes @minutes=(11/2)> (5.5 minutes)
5.minutes - 30.seconds # => #<Minutes @minutes=(9/2)> (4.5 minutes)
5.minutes + 30 # => TypeError (a bare number has no unit)
# Durations are Comparable, across units and exactly:
5.minutes == 300.seconds # => true
90.seconds > 1.minute # => true
[1.hour, 30.seconds, 5.minutes].sort
# => [30.seconds, 5.minutes, 1.hour]
# Scaled by a number, a duration stays a duration and keeps its unit:
5.minutes * 3 # => #<Minutes @minutes=15>
1.hour / 4 # => #<Hours @hours=(1/4)>
1.second / 3 # => #<Seconds @seconds=(1/3)> (not 0)
# Divided by a duration, the units cancel and a plain number is left. It is a
# Rational when both quantities are exact, as Numeric#quo gives, so it compares
# and calculates as the number it is; call to_f at the edge.
1.hour / 30.minutes # => (2/1)
1.second / 3.milliseconds # => (1000/3)
# There is no unit of time squared, so this is refused rather than answered:
5.minutes * 3.minutes # => TypeError
Time Calculations
# The past
2.seconds.ago # => #<Time:> (now - 2.seconds.to_seconds.to_f)
3.minutes.ago # => #<Time:> (now - 3.minutes.to_seconds.to_f)
4.hours.ago # => #<Time:> (now - 4.hours.to_seconds.to_f)
5.days.ago # => #<Time:> (now - 5.days.to_seconds.to_f)
6.weeks.ago # => #<Time:> (now - 6.weeks.to_seconds.to_f)
# The future
2.seconds.hence # => #<Time:> (now + 2.seconds.to_seconds.to_f)
3.minutes.hence # => #<Time:> (now + 3.minutes.to_seconds.to_f)
4.hours.hence # => #<Time:> (now + 4.hours.to_seconds.to_f)
5.days.hence # => #<Time:> (now + 5.days.to_seconds.to_f)
6.weeks.hence # => #<Time:> (now + 6.weeks.to_seconds.to_f)
A Note on Months
Months is the odd one out. Every other unit is a fixed quantity with an exact
ratio to every other; a month is a calendar operation whose length depends on
the calendar. Its conversions use the Gregorian mean month (2629746 seconds),
so they are honest only as a statistical average, never as calendar-correct
arithmetic. Its ago/hence inherit this — they displace by the mean month,
so they are approximate rather than calendar-correct; reach for a
calendar-anchored type such as month.rb when that matters. Months is
therefore slated for removal.
Contributing
- Fork it (https://github.com/thoran/duration.rb/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new pull request
License
MIT