{"id":15405675,"url":"https://github.com/zverok/time_math2","last_synced_at":"2025-04-04T20:15:07.625Z","repository":{"id":31359779,"uuid":"34922663","full_name":"zverok/time_math2","owner":"zverok","description":"Small library for operations with time steps (like \"next day\", \"floor to hour\" and so on)","archived":false,"fork":false,"pushed_at":"2019-11-02T11:17:15.000Z","size":155,"stargazers_count":276,"open_issues_count":1,"forks_count":7,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-03-28T19:11:30.748Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zverok.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-05-01T20:30:28.000Z","updated_at":"2024-12-26T16:19:31.000Z","dependencies_parsed_at":"2022-08-24T13:10:31.755Z","dependency_job_id":null,"html_url":"https://github.com/zverok/time_math2","commit_stats":null,"previous_names":["zverok/time_boots"],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Ftime_math2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Ftime_math2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Ftime_math2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Ftime_math2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zverok","download_url":"https://codeload.github.com/zverok/time_math2/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247242681,"owners_count":20907134,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-01T16:18:04.586Z","updated_at":"2025-04-04T20:15:07.605Z","avatar_url":"https://github.com/zverok.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Time Math\n\n[![Gem Version](https://badge.fury.io/rb/time_math2.svg)](http://badge.fury.io/rb/time_math2)\n[![Dependency Status](https://gemnasium.com/zverok/time_math2.svg)](https://gemnasium.com/zverok/time_math2)\n[![Code Climate](https://codeclimate.com/github/zverok/time_math2/badges/gpa.svg)](https://codeclimate.com/github/zverok/time_math2)\n[![Build Status](https://travis-ci.org/zverok/time_math2.svg?branch=master)](https://travis-ci.org/zverok/time_math2)\n[![Coverage Status](https://coveralls.io/repos/zverok/time_math2/badge.svg?branch=master)](https://coveralls.io/r/zverok/time_math2?branch=master)\n\n\u003e **[TimeCalc](https://github.com/zverok/time_calc) 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.**\n\n---\n\n**TimeMath2** ~is~ was a small, no-dependencies library attempting to make time\narithmetics easier. It provides you with simple, easy-to-remember API, without\nany monkey-patching of core Ruby classes, so it can be used alongside\nRails or without it, for any purpose.\n\n## Table Of Contents\n\n* [Features](#features)\n* [Naming](#naming)\n* [Reasons](#reasons)\n* [Installation](#installation)\n* [Usage](#usage)\n    - [Full list of simple arithmetic methods](#full-list-of-simple-arithmetic-methods)\n    - [Set of operations as a value object](#set-of-operations-as-a-value-object)\n    - [Time sequence abstraction](#time-sequence-abstraction)\n    - [Measuring time periods](#measuring-time-periods)\n    - [Resampling](#resampling)\n* [Notes on timezones](#notes-on-timezones)\n* [Compatibility notes](#compatibility-notes)\n* [Alternatives](#alternatives)\n* [Links](#links)\n* [Author](#author)\n* [License](#license)\n\n## Features\n\n* No monkey-patching of core classes (now **strict**; previously existing opt-in\n  core ext removed in 0.0.5);\n* Works with Time, Date and DateTime;\n* Accurately preserves timezone offset;\n* Simple arithmetics: floor/ceil/round to any time unit (second, hour, year\n  or whatnot), advance/decrease by any unit;\n* Chainable [operations](#set-of-operations-as-a-value-object), including\n  construction of \"set of operations\" value object (like \"10:20 at next\n  month first day\"), clean and powerful;\n* Easy generation of [time sequences](#time-sequence-abstraction)\n  (like \"each day from _this_ to _that_ date\");\n* Measuring of time distances between two timestamps in any units;\n* Powerful and flexible [resampling](#resampling) of arbitrary time value\n  arrays/hashes into regular sequences.\n\n## Naming\n\n`TimeMath` is the best name I know for the task library does, yet\nit is [already taken](https://rubygems.org/gems/time_math). So, with no\nother thoughts I came with the ugly solution.\n\n(BTW, the [previous version](https://github.com/zverok/time_math/blob/e997d7ddd52fc5bce3c77dc3c8022adfc9fe7028/README.md)\nhad some dumb \"funny\" name for gem and all helper classes, and nobody liked it.)\n\n## Reasons\n\nYou frequently need to calculate things like \"exact midnight of the next\nday\", but you don't want to monkey-patch all of your integers, tug in\n5K LOC of ActiveSupport and you like to have things clean and readable.\n\n## Installation\n\nInstall it like always:\n\n```\n$ gem install time_math2\n```\n\nor add to your Gemfile\n\n```ruby\ngem 'time_math2', require: 'time_math'\n```\n\nand `bundle install` it.\n\n## Usage\n\nFirst, you take time unit you want:\n\n```ruby\nTimeMath[:day] # =\u003e #\u003cTimeMath::Units::Day\u003e\n# or\nTimeMath.day # =\u003e #\u003cTimeMath::Units::Day\u003e\n\n# List of units supported:\nTimeMath.units\n# =\u003e [:sec, :min, :hour, :day, :week, :month, :year]\n```\n\nThen you use this unit for any math you want:\n\n```ruby\nTimeMath.day.floor(Time.now) # =\u003e 2016-05-28 00:00:00 +0300\nTimeMath.day.ceil(Time.now) # =\u003e 2016-05-29 00:00:00 +0300\nTimeMath.day.advance(Time.now, +10) # =\u003e 2016-06-07 14:06:57 +0300\n# ...and so on\n```\n\n### Full list of simple arithmetic methods\n\n* `\u003cunit\u003e.floor(tm)` -- rounds down to nearest `\u003cunit\u003e`;\n* `\u003cunit\u003e.ceil(tm)` -- rounds up to nearest `\u003cunit\u003e`;\n* `\u003cunit\u003e.round(tm)` -- rounds to nearest `\u003cunit\u003e` (up or down);\n* `\u003cunit\u003e.round?(tm)` -- checks if `tm` is already round to `\u003cunit\u003e`;\n* `\u003cunit\u003e.prev(tm)` -- like `floor`, but always decreases:\n    - `2015-06-27 13:30` would be converted to `2015-06-27 00:00` by both\n      `floor` and `prev`, but\n    - `2015-06-27 00:00` would be left intact on `floor`, but would be\n      decreased to `2015-06-26 00:00` by `prev`;\n* `\u003cunit\u003e.next(tm)` -- like `ceil`, but always increases;\n* `\u003cunit\u003e.advance(tm, amount)` -- increases tm by integer amount of `\u003cunit\u003e`s;\n* `\u003cunit\u003e.decrease(tm, amount)` -- decreases tm by integer amount of `\u003cunit\u003e`s;\n* `\u003cunit\u003e.range(tm, amount)` -- creates range of `tm ... tm + amount \u003cunits\u003e`;\n* `\u003cunit\u003e.range_back(tm, amount)` -- creates range of `tm - amount \u003cunits\u003e ... tm`.\n\n**Things to note**:\n\n* rounding methods (`floor`, `ceil` and company) support optional second\n  argument—amount of units to round to, like \"each 3 hours\": `hour.floor(tm, 3)`;\n* both rounding and advance/decrease methods allow their last argument to\n  be float/rational, so you can `hour.advance(tm, 1/2r)` and this would\n  work as you may expect. Non-integer arguments are only supported for\n  units less than week (because \"half of month\" have no exact mathematical\n  sense).\n\nSee also [Units::Base](http://www.rubydoc.info/gems/time_math2/TimeMath/Units/Base).\n\n### Set of operations as a value object\n\nFor example, you want \"10 am at next monday\". By using atomic time unit\noperations, you'll need the code like:\n\n```ruby\nTimeMath.hour.advance(TimeMath.week.ceil(Time.now), 10)\n```\n...which is not really readable, to say the least. So, `TimeMath` provides\none top-level method allowing to chain any operations you want:\n\n```ruby\nTimeMath(Time.now).ceil(:week).advance(:hour, 10).call\n```\n\nMuch more readable, huh?\n\nThe best thing about it, that you can prepare \"operations list\" value\nobject, and then use it (or pass to methods, or\nserialize to YAML and deserialize in some Sidekiq task and so on):\n\n```ruby\nop = TimeMath().ceil(:week).advance(:hour, 10)\n# =\u003e #\u003cTimeMath::Op ceil(:week).advance(:hour, 10)\u003e\nop.call(Time.now)\n# =\u003e 2016-06-27 10:00:00 +0300\n\n# It also can be called on several arguments/array of arguments:\nop.call(tm1, tm2, tm3)\nop.call(array_of_timestamps)\n# ...or even used as a block-ish object:\narray_of_timestamps.map(\u0026op)\n```\n\nSee also [TimeMath()](http://www.rubydoc.info/gems/time_math2/toplevel#TimeMath-instance_method)\nand underlying [TimeMath::Op](http://www.rubydoc.info/gems/time_math2/TimeMath/Op)\nclass docs.\n\n### Time sequence abstraction\n\nTime sequence allows you to generate an array of time values between some\npoints:\n\n```ruby\nto = Time.now\n# =\u003e 2016-05-28 17:47:30 +0300\nfrom = TimeMath.day.floor(to)\n# =\u003e 2016-05-28 00:00:00 +0300\nseq = TimeMath.hour.sequence(from...to)\n# =\u003e #\u003cTimeMath::Sequence(:hour, 2016-05-28 00:00:00 +0300...2016-05-28 17:47:30 +0300)\u003e\np(*seq)\n# 2016-05-28 00:00:00 +0300\n# 2016-05-28 01:00:00 +0300\n# 2016-05-28 02:00:00 +0300\n# 2016-05-28 03:00:00 +0300\n# 2016-05-28 04:00:00 +0300\n# 2016-05-28 05:00:00 +0300\n# 2016-05-28 06:00:00 +0300\n# 2016-05-28 07:00:00 +0300\n# ...and so on\n```\n\nNote that sequence also play well with operation chain described above,\nso you can\n\n```ruby\nseq = TimeMath.day.sequence(Time.parse('2016-05-01')...Time.parse('2016-05-04')).advance(:hour, 10).decrease(:min, 5)\n# =\u003e #\u003cTimeMath::Sequence(:day, 2016-05-01 00:00:00 +0300...2016-05-04 00:00:00 +0300).advance(:hour, 10).decrease(:min, 5)\u003e\nseq.to_a\n# =\u003e [2016-05-01 09:55:00 +0300, 2016-05-02 09:55:00 +0300, 2016-05-03 09:55:00 +0300]\n```\n\nSee also [Sequence YARD docs](http://www.rubydoc.info/gems/time_math2/TimeMath/Sequence).\n\n### Measuring time periods\n\nSimple measure: just \"how many `\u003cunit\u003e`s from date A to date B\":\n\n```ruby\nTimeMath.week.measure(Time.parse('2016-05-01'), Time.parse('2016-06-01'))\n# =\u003e 4\n```\n\nMeasure with remaineder: returns number of `\u003cunit\u003e`s between dates and\nthe date when this number would be exact:\n\n```ruby\nTimeMath.week.measure_rem(Time.parse('2016-05-01'), Time.parse('2016-06-01'))\n# =\u003e [4, 2016-05-29 00:00:00 +0300]\n```\n\n(on May 29 there would be exactly 4 weeks since May 1).\n\nMulti-unit measuring:\n\n```ruby\n# My real birthday, in fact!\nbirthday = Time.parse('1983-02-14 13:30')\n\n# My full age\nTimeMath.measure(birthday, Time.now)\n# =\u003e {:years=\u003e33, :months=\u003e3, :weeks=\u003e2, :days=\u003e0, :hours=\u003e1, :minutes=\u003e25, :seconds=\u003e52}\n\n# NB: you can use this output with String#format or String%:\nputs \"%{years}y %{months}m %{weeks}w %{days}d %{hours}h %{minutes}m %{seconds}s\" %\n  TimeMath.measure(birthday, Time.now)\n# 33y 3m 2w 0d 1h 26m 15s\n\n# Option: measure without weeks\nTimeMath.measure(birthday, Time.now, weeks: false)\n# =\u003e {:years=\u003e33, :months=\u003e3, :days=\u003e14, :hours=\u003e1, :minutes=\u003e26, :seconds=\u003e31}\n\n# My full age in days, hours, minutes\nTimeMath.measure(birthday, Time.now, upto: :day)\n# =\u003e {:days=\u003e12157, :hours=\u003e2, :minutes=\u003e26, :seconds=\u003e55}\n```\n\n### Resampling\n\n**Resampling** is useful for situations when you have some timestamped\ndata (with variable holes between values), and wantto make it regular,\ne.g. for charts drawing.\n\nThe most simple (and not very useful) resampling just turns array of\nirregular timestamps into regular one:\n\n```ruby\ndates = %w[2016-06-01 2016-06-03 2016-06-06].map(\u0026Date.method(:parse))\n# =\u003e [#\u003cDate: 2016-06-01\u003e, #\u003cDate: 2016-06-03\u003e, #\u003cDate: 2016-06-06\u003e]\nTimeMath.day.resample(dates)\n# =\u003e [#\u003cDate: 2016-06-01\u003e, #\u003cDate: 2016-06-02\u003e, #\u003cDate: 2016-06-03\u003e, #\u003cDate: 2016-06-04\u003e, #\u003cDate: 2016-06-05\u003e, #\u003cDate: 2016-06-06\u003e]\nTimeMath.week.resample(dates)\n# =\u003e [#\u003cDate: 2016-05-30\u003e, #\u003cDate: 2016-06-06\u003e]\nTimeMath.month.resample(dates)\n# =\u003e [#\u003cDate: 2016-06-01\u003e]\n```\n\nMuch more useful is _hash resampling_: when you have a hash of `{timestamp =\u003e value}`\nand...\n\n```ruby\ndata = {Date.parse('2016-06-01') =\u003e 18, Date.parse('2016-06-03') =\u003e 8, Date.parse('2016-06-06') =\u003e -4}\n# =\u003e {#\u003cDate: 2016-06-01\u003e=\u003e18, #\u003cDate: 2016-06-03\u003e=\u003e8, #\u003cDate: 2016-06-06\u003e=\u003e-4}\nTimeMath.day.resample(data)\n# =\u003e {#\u003cDate: 2016-06-01\u003e=\u003e[18], #\u003cDate: 2016-06-02\u003e=\u003e[], #\u003cDate: 2016-06-03\u003e=\u003e[8], #\u003cDate: 2016-06-04\u003e=\u003e[], #\u003cDate: 2016-06-05\u003e=\u003e[], #\u003cDate: 2016-06-06\u003e=\u003e[-4]}\nTimeMath.week.resample(data)\n# =\u003e {#\u003cDate: 2016-05-30\u003e=\u003e[18, 8], #\u003cDate: 2016-06-06\u003e=\u003e[-4]}\nTimeMath.month.resample(data)\n# =\u003e {#\u003cDate: 2016-06-01\u003e=\u003e[18, 8, -4]}\n```\n\nFor values grouping strategy, `resample` accepts symbol and block arguments:\n\n```ruby\nTimeMath.week.resample(data, :first)\n# =\u003e {#\u003cDate: 2016-05-30\u003e=\u003e18, #\u003cDate: 2016-06-06\u003e=\u003e-4}\nTimeMath.week.resample(data) { |vals| vals.inject(:+) }\n =\u003e {#\u003cDate: 2016-05-30\u003e=\u003e26, #\u003cDate: 2016-06-06\u003e=\u003e-4}\n```\n\nThe functionality currently considered experimental, please notify me\nabout your ideas and use cases via [GitHub issues](https://github.com/zverok/time_math2/issues)!\n\n## Notes on timezones\n\nTimeMath tries its best to preserve timezones of original values. Currently,\nit means:\n\n* For `Time` instances, symbolic timezone is preserved; when jumping over\n  DST border, UTC offset will change and everything remains as expected;\n* For `DateTime` Ruby not provides symbolic timezone, only numeric offset;\n  it is preserved by TimeMath (but be careful about jumping around DST,\n  offset would not change).\n\n## Compatibility notes\n\nTimeMath is known to work on MRI Ruby \u003e= 2.0 and JRuby \u003e= 9.0.0.0.\n\nOn Rubinius, some of tests fail and I haven't time to investigate it. If\nsomebody still uses Rubinius and wants TimeMath to be working properly\non it, please let me know.\n\n## Alternatives\n\nThere's pretty small and useful [AS::Duration](https://github.com/janko-m/as-duration)\nby Janko Marohnić, which is time durations, extracted from ActiveSupport,\nbut without any ActiveSupport bloat.\n\n## Links\n\n* [API Docs](http://www.rubydoc.info/gems/time_math2)\n\n## Author\n\n[Victor Shepelev](http://zverok.github.io/)\n\n## License\n\n[MIT](https://github.com/zverok/time_math2/blob/master/LICENSE.txt).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Ftime_math2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzverok%2Ftime_math2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Ftime_math2/lists"}