• Stars
    star
    213
  • Rank 185,410 (Top 4 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 5 years ago
  • Updated almost 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Simple time arithmetics in a modern, readable, idiomatic, no-"magic" Ruby.

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;
  • Since 0.0.4, supports ActiveSupport::TimeWithZone, too. While in ActiveSupport-enabled context TimeCalc may seem redundant (you'll probably use time - 1.day anyways), some of the functionality is easier with TimeCalc (rounding to different units) or just not present in ActiveSupport (time sequences, iterate with skippking); also may be helpful for third-party libraries which want to use TimeCalc underneath but don't want to be broken in Rails context.

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!)

Math with skipping "non-business time"

TimeCalc#iterate allows to advance or decrease time values by skipping some of them (like weekends, holidays, and non-working hours):

# add 10 working days (weekends are not counted)
TimeCalc.(Time.parse('2019-07-03 23:28:54')).iterate(10, :days) { |t| (1..5).cover?(t.wday) }
# => 2019-07-17 23:28:54 +0300

# add 12 working hours
TimeCalc.(Time.parse('2019-07-03 13:28:54')).iterate(12, :hours) { |t| (9...18).cover?(t.hour) }
# => 2019-07-04 16:28:54 +0300

# negative spans are working, too:
TimeCalc.(Time.parse('2019-07-03 13:28:54')).iterate(-12, :hours) { |t| (9...18).cover?(t.hour) }
# => 2019-07-02 10:28:54 +0300

# zero span could be used to robustly enforce value into acceptable range
# (increasing forward till block is true):
TimeCalc.(Time.parse('2019-07-03 23:28:54')).iterate(0, :hours) { |t| (9...18).cover?(t.hour) }
# => 2019-07-04 09:28:54 +0300

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

More Repositories

1

wikipedia_ql

Query language for efficient data extraction from Wikipedia
Python
357
star
2

time_math2

Small library for operations with time steps (like "next day", "floor to hour" and so on)
Ruby
278
star
3

spylls

Pure Python spell-checker, (almost) full port of Hunspell
Python
265
star
4

worldize

Simple coloured countries drawing
Ruby
258
star
5

hm

Idiomatic Ruby hash transformations
Ruby
129
star
6

geo_coord

Simple yet useful Geo Coordinates class for Ruby
Ruby
122
star
7

any_good

Is this gem any good?
Ruby
119
star
8

wheretz

Fast and precise time zone by geo coordinates lookup
Ruby
99
star
9

good-value-object

Ruby Value Object conventions
Ruby
93
star
10

magic_cloud

Simple pretty word cloud for Ruby
Ruby
85
star
11

the_schema_is

ActiveRecord schema annotations done right
Ruby
70
star
12

saharspec

RSpec sugar to DRY your specs
Ruby
67
star
13

yard-junk

Get rid of the junk in your YARD docs
Ruby
67
star
14

whatthegem

Ruby gem information, stats and usage for your terminal
Ruby
62
star
15

sho

Experimental post-framework view library
Ruby
45
star
16

xkcdize

XKCD-like picture distortion in Ruby and RMagick
Ruby
45
star
17

clio

Clio β€” better Friendfeed backup tool
JavaScript
39
star
18

ruby_as_apl

Conway's game of life in one statement of idiomatic Ruby... ported from APL
Ruby
35
star
19

delegates

delegate :methods, to: :target, extracted from ActiveSupport
Ruby
32
star
20

dokaz

Use your documentation as a specification: parse and evaluate ruby code from markdown
Ruby
32
star
21

rubyseeds

Ruby core extensions repository (not a gem!)
Ruby
28
star
22

lmsa

Let's Make Something Awesome! β€” project ideas repo for mentees
Ruby
26
star
23

drosterize

Self-replicating images with Ruby & RMagick
Ruby
26
star
24

linkhum

URL auto-linker with reasonable and humane behavior
Ruby
25
star
25

object_enumerate

Object#enumerate Ruby core proposal demo. Merged in Ruby 2.7 as Enumerator.produce
Ruby
19
star
26

procme

Fun with proc
Ruby
17
star
27

my-ruby-contributions

Moved to https://zverok.github.io/ruby.html
Ruby
17
star
28

fstrings

Python-alike fstrings (formatting strings) for Ruby
Ruby
14
star
29

grok-shan-shui

Grok {Shan, Shui}*: Advent of understanding the generative art
HTML
14
star
30

did_you

Ruby version-agnostic wrapper for did_you_mean gem
Ruby
10
star
31

idempotent_enumerable

IdempotentEnumerable is like Enumerable but preserves original collection class
Ruby
8
star
32

enumerator_generate

Enumerator#generate Ruby core proposal demo
Ruby
4
star
33

grokability

Grokability -- step after Readability
JavaScript
4
star
34

clio-web

Online version of FrF backup
Ruby
3
star
35

lastic

ElasticSearch DSL which erases all the complexity
Ruby
3
star
36

bloxl

Hi-level Excel-2007 reports DSL
Ruby
3
star
37

confucius

Simple framework-agnostic configuration for any Ruby app
Ruby
2
star
38

zverok.github.io

HTML
2
star
39

sequel_marginalia

Port of 37 signals marginalia for use with Sequel
Ruby
2
star
40

culturecodes

parsers for http://friendfeed.com/culturecodes
Ruby
2
star
41

uberdictionary

HTML/JS client to several En-Ru dictionaries
JavaScript
2
star
42

pattern-matching-prototype

Showcase of possible Ruby core language pattern matching
Ruby
2
star
43

cobb

Cobb is Yet Another Web Scraper, named after Firefly's Jayne Cobb
Ruby
1
star
44

matchish

An exercise for pattern matching in Ruby
Ruby
1
star