Skip to content
Simple time arithmetics in a modern, readable, idiomatic, no-"magic" Ruby.
Branch: master
Clone or download
Latest commit e0c3187 Jul 8, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib Changelog/version Jul 8, 2019
spec Some fixes Jul 8, 2019
.gitignore Fix ver Jul 6, 2019
.rspec Initial (rip-off of TimeMath) Oct 16, 2018
.rubocop.yml Fix DST for realz Jul 5, 2019
.travis.yml Travis?.. Jul 8, 2019
.yardopts Some moar docs Jul 3, 2019
Changelog.md Changelog/version Jul 8, 2019
Gemfile Some docs Jul 3, 2019
LICENSE.txt README/LICENSE Jul 4, 2019
README.md Rakefile and spelling Jul 6, 2019
Rakefile Rakefile and spelling Jul 6, 2019
time_calc.gemspec Some moar docs Jul 3, 2019

README.md

TimeCalc -- next generation of Time arithmetic library

Gem Version Build Status Documentation

TimeCalc tries to provide a way to do simple time arithmetic in a modern, readable, idiomatic, no-"magic" Ruby.

NB: TimeCalc is a continuation of TimeMath project. As I decided to change API significantly (completely, in fact) and drop a lot of "nice to have but nobody uses" features, it is a new project rather than "absolutely incompatible new version". See API design section to understand how and why TimeCalc is different.

Features

  • Small, clean, pure-Ruby, idiomatic, no monkey-patching, no dependencies (except backports);
  • Arithmetic akin to what Ruby numbers provide: +/-, floor/ceil/round, enumerable sequences (step/to);
  • Works with Time, Date and DateTime and allows to mix them freely (e.g. create sequences from Date to Time, calculate their diffs);
  • Tries its best to preserve timezone/offset information:
    • on Ruby 2.6+, for Time with real timezones, preserves them;
    • on Ruby < 2.6, preserves at least utc_offset of Time;
    • for DateTime preserves zone name.

Synopsis

Arithmetic with units

require 'time_calc'

TC = TimeCalc

t = Time.parse('2019-03-14 08:06:15')

TC.(t).+(3, :hours)
# => 2019-03-14 11:06:15 +0200
TC.(t).round(:week)
# => 2019-03-11 00:00:00 +0200

# TimeCalc.call(Time.now) shortcut:
TC.now.floor(:day)
# => beginning of the today

Operations supported:

  • +, -
  • ceil, round, floor

Units supported:

  • :sec (also :second, :seconds);
  • :min (:minute, :minutes);
  • :hour/:hours;
  • :day/:days;
  • :week/:weeks;
  • :month/:months;
  • :year/:years.

Timezone preservation on Ruby 2.6:

require 'tzinfo'
t = Time.new(2019, 9, 1, 14, 30, 12, TZInfo::Timezone.get('Europe/Kiev'))
# => 2019-09-01 14:30:12 +0300
#                        ^^^^^
TimeCalc.(t).+(3, :months) # jump over DST: we have +3 in summer and +2 in winter
# => 2019-12-01 14:30:12 +0200
#                        ^^^^^

(Random fun fact: it is Kyiv, not Kiev!)

Difference of two values

diff = TC.(t) - Time.parse('2019-02-30 16:30')
# => #<TimeCalc::Diff(2019-03-14 08:06:15 +0200 − 2019-03-02 16:30:00 +0200)>
diff.days # or any other supported unit
# => 11
diff.factorize
# => {:year=>0, :month=>0, :week=>1, :day=>4, :hour=>15, :min=>36, :sec=>15}

There are several options to Diff#factorize to obtain the most useful result.

Chains of operations

TC.wrap(t).+(1, :hour).round(:min).unwrap
# => 2019-03-14 09:06:00 +0200

# proc constructor synopsys:
times = ['2019-06-01 14:30', '2019-06-05 17:10', '2019-07-02 13:40'].map { |t| Time.parse(t) }
times.map(&TC.+(1, :hour).round(:min))
# => [2019-06-01 15:30:00 +0300, 2019-06-05 18:10:00 +0300, 2019-07-02 14:40:00 +0300]

Enumerable time sequences

TC.(t).step(2, :weeks)
# => #<TimeCalc::Sequence (2019-03-14 08:06:15 +0200 - ...):step(2 weeks)>
TC.(t).step(2, :weeks).first(3)
# => [2019-03-14 08:06:15 +0200, 2019-03-28 08:06:15 +0200, 2019-04-11 09:06:15 +0300]
TC.(t).to(Time.parse('2019-04-30 16:30')).step(3, :weeks).to_a
# => [2019-03-14 08:06:15 +0200, 2019-04-04 09:06:15 +0300, 2019-04-25 09:06:15 +0300]
TC.(t).for(3, :months).step(4, :weeks).to_a
# => [2019-03-14 08:06:15 +0200, 2019-04-11 09:06:15 +0300, 2019-05-09 09:06:15 +0300, 2019-06-06 09:06:15 +0300]

API design

The idea of this library (as well as the idea of the previous one) grew of the simple question "how do you say <some time> + 1 hour in good Ruby?" This question also leads (me) to notifying that other arithmetical operations (like rounding, or <value> up to <value> with step <value>) seem to be applicable to Time or Date values as well.

Prominent ActiveSupport's answer of extending simple numbers to respond to 1.year never felt totally right to me. I am not completely against-any-monkey-patches kind of guy, it just doesn't sit right, to say "number has a method to produce duration". One of the attempts to find an alternative has led me to the creation of time_math2, which gained some (modest) popularity by presenting things this way: TimeMath.year.advance(time, 1).

TBH, using the library myself only eventually, I have never been too happy with it: it never felt really natural, so I constantly forgot "what should I do to calculate '2 days ago'". This simplest use case (some time from now) in TimeMath looked too far from "how you pronounce it":

# Natural language: 2 days ago
# "Formalized": now - 2 days

# ActiveSupport:
Time.now + 2.days
# also there is 2.days.ago, but I am not a big fan of "1000 synonyms just for naturality"

# TimeMath:
TimMath.day.decrease(Time.now, 2) # Ughhh what? "Day decrease now 2"?

The thought process that led to the new library is:

  • (2, days) is just a tuple of two unrelated data elements
  • days is "internal name that makes sense inside the code", which we represent by Symbol in Ruby
  • Math operators can be called just like regular methods: .+(something), which may look unusual at first, but can be super-handy even with simple numbers, in method chaining -- I am grateful to my Verbit's colleague Roman Yarovoy to pointing at that fact (or rather its usefulness);
  • To chain some calculations with Ruby core type without extending this type, we can just "wrap" it into a monad-like object, do the calculations, and unwrap at the end (TimeMath itself, and my Hash-processing gem hm have used this approach).

So, here we go:

TimeCalc.(Time.now).-(2, :days)
# Small shortcut, as `Time.now` is the frequent start value for such calculations:
TimeCalc.now.-(2, :days)

The rest of the design (see examples above) just followed naturally. There could be different opinions on the approach, but for myself the resulting API looks straightforward, hard to forget and very regular (in fact, all the hard time calculations, including support for different types, zones, DST and stuff, are done in two core methods, and the rest was easy to define in terms of those methods, which is a sign of consistency).

¯\_(ツ)_/¯

Author & license

You can’t perform that action at this time.