• Stars
    star
    278
  • Rank 148,454 (Top 3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 9 years ago
  • Updated about 5 years ago

Reviews

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

Repository Details

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

Time Math

Gem Version Dependency Status Code Climate Build Status Coverage Status

TimeCalc is the next iteration of ideas for the time-arithmetics library, with nicer API and better support for modern Ruby (for example, Ruby 2.6 real timezones). It would be evolved and supported instead of TimeMath. This gem should be considered discontinued.


TimeMath2 is was a small, no-dependencies library attempting to make time arithmetics easier. It provides you with simple, easy-to-remember API, without any monkey-patching of core Ruby classes, so it can be used alongside Rails or without it, for any purpose.

Table Of Contents

Features

  • No monkey-patching of core classes (now strict; previously existing opt-in core ext removed in 0.0.5);
  • Works with Time, Date and DateTime;
  • Accurately preserves timezone offset;
  • Simple arithmetics: floor/ceil/round to any time unit (second, hour, year or whatnot), advance/decrease by any unit;
  • Chainable operations, including construction of "set of operations" value object (like "10:20 at next month first day"), clean and powerful;
  • Easy generation of time sequences (like "each day from this to that date");
  • Measuring of time distances between two timestamps in any units;
  • Powerful and flexible resampling of arbitrary time value arrays/hashes into regular sequences.

Naming

TimeMath is the best name I know for the task library does, yet it is already taken. So, with no other thoughts I came with the ugly solution.

(BTW, the previous version had some dumb "funny" name for gem and all helper classes, and nobody liked it.)

Reasons

You frequently need to calculate things like "exact midnight of the next day", but you don't want to monkey-patch all of your integers, tug in 5K LOC of ActiveSupport and you like to have things clean and readable.

Installation

Install it like always:

$ gem install time_math2

or add to your Gemfile

gem 'time_math2', require: 'time_math'

and bundle install it.

Usage

First, you take time unit you want:

TimeMath[:day] # => #<TimeMath::Units::Day>
# or
TimeMath.day # => #<TimeMath::Units::Day>

# List of units supported:
TimeMath.units
# => [:sec, :min, :hour, :day, :week, :month, :year]

Then you use this unit for any math you want:

TimeMath.day.floor(Time.now) # => 2016-05-28 00:00:00 +0300
TimeMath.day.ceil(Time.now) # => 2016-05-29 00:00:00 +0300
TimeMath.day.advance(Time.now, +10) # => 2016-06-07 14:06:57 +0300
# ...and so on

Full list of simple arithmetic methods

  • <unit>.floor(tm) -- rounds down to nearest <unit>;
  • <unit>.ceil(tm) -- rounds up to nearest <unit>;
  • <unit>.round(tm) -- rounds to nearest <unit> (up or down);
  • <unit>.round?(tm) -- checks if tm is already round to <unit>;
  • <unit>.prev(tm) -- like floor, but always decreases:
    • 2015-06-27 13:30 would be converted to 2015-06-27 00:00 by both floor and prev, but
    • 2015-06-27 00:00 would be left intact on floor, but would be decreased to 2015-06-26 00:00 by prev;
  • <unit>.next(tm) -- like ceil, but always increases;
  • <unit>.advance(tm, amount) -- increases tm by integer amount of <unit>s;
  • <unit>.decrease(tm, amount) -- decreases tm by integer amount of <unit>s;
  • <unit>.range(tm, amount) -- creates range of tm ... tm + amount <units>;
  • <unit>.range_back(tm, amount) -- creates range of tm - amount <units> ... tm.

Things to note:

  • rounding methods (floor, ceil and company) support optional second argumentโ€”amount of units to round to, like "each 3 hours": hour.floor(tm, 3);
  • both rounding and advance/decrease methods allow their last argument to be float/rational, so you can hour.advance(tm, 1/2r) and this would work as you may expect. Non-integer arguments are only supported for units less than week (because "half of month" have no exact mathematical sense).

See also Units::Base.

Set of operations as a value object

For example, you want "10 am at next monday". By using atomic time unit operations, you'll need the code like:

TimeMath.hour.advance(TimeMath.week.ceil(Time.now), 10)

...which is not really readable, to say the least. So, TimeMath provides one top-level method allowing to chain any operations you want:

TimeMath(Time.now).ceil(:week).advance(:hour, 10).call

Much more readable, huh?

The best thing about it, that you can prepare "operations list" value object, and then use it (or pass to methods, or serialize to YAML and deserialize in some Sidekiq task and so on):

op = TimeMath().ceil(:week).advance(:hour, 10)
# => #<TimeMath::Op ceil(:week).advance(:hour, 10)>
op.call(Time.now)
# => 2016-06-27 10:00:00 +0300

# It also can be called on several arguments/array of arguments:
op.call(tm1, tm2, tm3)
op.call(array_of_timestamps)
# ...or even used as a block-ish object:
array_of_timestamps.map(&op)

See also TimeMath() and underlying TimeMath::Op class docs.

Time sequence abstraction

Time sequence allows you to generate an array of time values between some points:

to = Time.now
# => 2016-05-28 17:47:30 +0300
from = TimeMath.day.floor(to)
# => 2016-05-28 00:00:00 +0300
seq = TimeMath.hour.sequence(from...to)
# => #<TimeMath::Sequence(:hour, 2016-05-28 00:00:00 +0300...2016-05-28 17:47:30 +0300)>
p(*seq)
# 2016-05-28 00:00:00 +0300
# 2016-05-28 01:00:00 +0300
# 2016-05-28 02:00:00 +0300
# 2016-05-28 03:00:00 +0300
# 2016-05-28 04:00:00 +0300
# 2016-05-28 05:00:00 +0300
# 2016-05-28 06:00:00 +0300
# 2016-05-28 07:00:00 +0300
# ...and so on

Note that sequence also play well with operation chain described above, so you can

seq = TimeMath.day.sequence(Time.parse('2016-05-01')...Time.parse('2016-05-04')).advance(:hour, 10).decrease(:min, 5)
# => #<TimeMath::Sequence(:day, 2016-05-01 00:00:00 +0300...2016-05-04 00:00:00 +0300).advance(:hour, 10).decrease(:min, 5)>
seq.to_a
# => [2016-05-01 09:55:00 +0300, 2016-05-02 09:55:00 +0300, 2016-05-03 09:55:00 +0300]

See also Sequence YARD docs.

Measuring time periods

Simple measure: just "how many <unit>s from date A to date B":

TimeMath.week.measure(Time.parse('2016-05-01'), Time.parse('2016-06-01'))
# => 4

Measure with remaineder: returns number of <unit>s between dates and the date when this number would be exact:

TimeMath.week.measure_rem(Time.parse('2016-05-01'), Time.parse('2016-06-01'))
# => [4, 2016-05-29 00:00:00 +0300]

(on May 29 there would be exactly 4 weeks since May 1).

Multi-unit measuring:

# My real birthday, in fact!
birthday = Time.parse('1983-02-14 13:30')

# My full age
TimeMath.measure(birthday, Time.now)
# => {:years=>33, :months=>3, :weeks=>2, :days=>0, :hours=>1, :minutes=>25, :seconds=>52}

# NB: you can use this output with String#format or String%:
puts "%{years}y %{months}m %{weeks}w %{days}d %{hours}h %{minutes}m %{seconds}s" %
  TimeMath.measure(birthday, Time.now)
# 33y 3m 2w 0d 1h 26m 15s

# Option: measure without weeks
TimeMath.measure(birthday, Time.now, weeks: false)
# => {:years=>33, :months=>3, :days=>14, :hours=>1, :minutes=>26, :seconds=>31}

# My full age in days, hours, minutes
TimeMath.measure(birthday, Time.now, upto: :day)
# => {:days=>12157, :hours=>2, :minutes=>26, :seconds=>55}

Resampling

Resampling is useful for situations when you have some timestamped data (with variable holes between values), and wantto make it regular, e.g. for charts drawing.

The most simple (and not very useful) resampling just turns array of irregular timestamps into regular one:

dates = %w[2016-06-01 2016-06-03 2016-06-06].map(&Date.method(:parse))
# => [#<Date: 2016-06-01>, #<Date: 2016-06-03>, #<Date: 2016-06-06>]
TimeMath.day.resample(dates)
# => [#<Date: 2016-06-01>, #<Date: 2016-06-02>, #<Date: 2016-06-03>, #<Date: 2016-06-04>, #<Date: 2016-06-05>, #<Date: 2016-06-06>]
TimeMath.week.resample(dates)
# => [#<Date: 2016-05-30>, #<Date: 2016-06-06>]
TimeMath.month.resample(dates)
# => [#<Date: 2016-06-01>]

Much more useful is hash resampling: when you have a hash of {timestamp => value} and...

data = {Date.parse('2016-06-01') => 18, Date.parse('2016-06-03') => 8, Date.parse('2016-06-06') => -4}
# => {#<Date: 2016-06-01>=>18, #<Date: 2016-06-03>=>8, #<Date: 2016-06-06>=>-4}
TimeMath.day.resample(data)
# => {#<Date: 2016-06-01>=>[18], #<Date: 2016-06-02>=>[], #<Date: 2016-06-03>=>[8], #<Date: 2016-06-04>=>[], #<Date: 2016-06-05>=>[], #<Date: 2016-06-06>=>[-4]}
TimeMath.week.resample(data)
# => {#<Date: 2016-05-30>=>[18, 8], #<Date: 2016-06-06>=>[-4]}
TimeMath.month.resample(data)
# => {#<Date: 2016-06-01>=>[18, 8, -4]}

For values grouping strategy, resample accepts symbol and block arguments:

TimeMath.week.resample(data, :first)
# => {#<Date: 2016-05-30>=>18, #<Date: 2016-06-06>=>-4}
TimeMath.week.resample(data) { |vals| vals.inject(:+) }
 => {#<Date: 2016-05-30>=>26, #<Date: 2016-06-06>=>-4}

The functionality currently considered experimental, please notify me about your ideas and use cases via GitHub issues!

Notes on timezones

TimeMath tries its best to preserve timezones of original values. Currently, it means:

  • For Time instances, symbolic timezone is preserved; when jumping over DST border, UTC offset will change and everything remains as expected;
  • For DateTime Ruby not provides symbolic timezone, only numeric offset; it is preserved by TimeMath (but be careful about jumping around DST, offset would not change).

Compatibility notes

TimeMath is known to work on MRI Ruby >= 2.0 and JRuby >= 9.0.0.0.

On Rubinius, some of tests fail and I haven't time to investigate it. If somebody still uses Rubinius and wants TimeMath to be working properly on it, please let me know.

Alternatives

There's pretty small and useful AS::Duration by Janko Marohniฤ‡, which is time durations, extracted from ActiveSupport, but without any ActiveSupport bloat.

Links

Author

Victor Shepelev

License

MIT.

More Repositories

1

wikipedia_ql

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

spylls

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

worldize

Simple coloured countries drawing
Ruby
258
star
4

time_calc

Simple time arithmetics in a modern, readable, idiomatic, no-"magic" Ruby.
Ruby
213
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