├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── time_math.rb └── time_math │ ├── measure.rb │ ├── op.rb │ ├── resamplers.rb │ ├── sequence.rb │ ├── units.rb │ ├── units │ ├── base.rb │ ├── day.rb │ ├── month.rb │ ├── simple.rb │ ├── week.rb │ └── year.rb │ ├── util.rb │ └── version.rb ├── spec ├── fixtures │ ├── advance.yml │ ├── ceil.yml │ ├── ceil_3.yml │ ├── decrease.yml │ ├── floor.yml │ ├── floor_3.yml │ ├── floor_half.yml │ ├── measure.yml │ ├── resample.yml │ ├── round.yml │ ├── sequence_pairs.yml │ └── sequence_to_a.yml ├── spec_helper.rb └── time_math │ ├── measure_spec.rb │ ├── op_spec.rb │ ├── resamplers_spec.rb │ ├── sequence_spec.rb │ ├── time_math_spec.rb │ └── units │ └── base_spec.rb └── time_math2.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | vendor 3 | Gemfile.lock 4 | TODO.md 5 | rubocop 6 | coverage 7 | *.gem 8 | .coveralls.yml 9 | .yardoc 10 | doc 11 | tmp 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require=./spec/spec_helper.rb 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | require: rubocop-rspec 3 | 4 | AllCops: 5 | Include: 6 | - 'lib/**/*' 7 | Exclude: 8 | - 'vendor/**/*' 9 | - Gemfile 10 | - Rakefile 11 | - '*.gemspec' 12 | DisplayCopNames: true 13 | 14 | Metrics/LineLength: 15 | Enabled: false 16 | # IgnoreComments: true -- somehow did not recognized?.. 17 | 18 | Metrics/MethodLength: 19 | Max: 12 20 | Exclude: 21 | - 'spec/**/*' 22 | 23 | Metrics/ClassLength: 24 | Max: 120 25 | 26 | # I'm big fan of and/or for flow control 27 | Style/AndOr: 28 | Enabled: false 29 | 30 | # Somehow he didn't recognize that module TimeMath is documented... 31 | Style/Documentation: 32 | Enabled: false 33 | 34 | # Everything else is just my personal style 35 | Style/SingleLineBlockParams: 36 | Enabled: false 37 | 38 | Style/FormatString: 39 | EnforcedStyle: percent 40 | 41 | Style/ParallelAssignment: 42 | Enabled: false 43 | 44 | Style/BlockDelimiters: 45 | Enabled: false 46 | 47 | RSpec/LeadingSubject: 48 | Enabled: false 49 | 50 | Layout/SpaceInsideHashLiteralBraces: 51 | EnforcedStyle: no_space 52 | 53 | Naming/UncommunicativeMethodParamName: 54 | AllowedNames: 55 | - tm 56 | - to 57 | - sz 58 | 59 | RSpec/ExampleLength: 60 | Enabled: false 61 | 62 | RSpec/NestedGroups: 63 | Max: 10 64 | 65 | RSpec/EmptyExampleGroup: 66 | Enabled: false 67 | 68 | RSpec/MultipleDescribes: 69 | Enabled: false 70 | 71 | RSpec/MultipleExpectations: 72 | Max: 5 73 | 74 | Lint/BooleanSymbol: 75 | Enabled: false 76 | 77 | # RSpec 78 | Metrics/PerceivedComplexity: 79 | Exclude: 80 | - 'spec/**/*' 81 | 82 | Metrics/CyclomaticComplexity: 83 | Exclude: 84 | - 'spec/**/*' 85 | 86 | Metrics/BlockLength: 87 | Exclude: 88 | - 'spec/**/*' 89 | 90 | Security/YAMLLoad: 91 | Exclude: 92 | - 'spec/**/*' 93 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2017-06-28 17:58:01 +0300 using RuboCop version 0.49.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 13 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: bundler 2 | language: ruby 3 | rvm: 4 | - "2.3.0" 5 | - "2.4.0" 6 | - "2.5.0" 7 | - "2.6.0" 8 | # Honestly, I don't know what I am doing, JRuby breaks on Travis different way each time... 9 | - jruby-9.2.5.0 10 | install: 11 | - bundle install --retry=3 12 | script: 13 | - bundle exec rspec 14 | - bundle exec rubocop 15 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup=markdown 2 | --no-private 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TimeMath Changelog 2 | 3 | # 0.1.1 (2019-03-15) 4 | 5 | * Ruby 2.6 compatibility, thanks @flash-gordon. 6 | 7 | # 0.1.0 (2017-07-30) 8 | 9 | * Update sequences logic to be, well... More logical (Thanks @kenn again for meaningful discussion!); 10 | * Various code cleanups. 11 | 12 | # 0.0.8 (2017-06-02) 13 | 14 | * Fix `Units::Base#measure` to correctly measure negative distances (e.g. from > to, thanks @kenn for 15 | pointing it); 16 | * Cleanup the same method to work correctly with sub-second precisions and different Time-y types. 17 | * Drop Ruby 2.0 support, finally. 18 | 19 | # 0.0.7 (2017-05-31) 20 | 21 | * Fix month advancing/decreasing. Thanks @dikond for pointing to problem! 22 | 23 | # 0.0.6 (2016-12-14) 24 | 25 | * Fix approach to timezone info preservation (previously, it was clear bug, emerging from 26 | false believing of how `Time.mktime` works). Thanks, @wojtha, for pointing to the problem. 27 | * Add `#each` and `Enumerable` to `Sequence` (no idea why it wasn't done from the very 28 | beginning). Again: thanks, @wojtha! 29 | 30 | # 0.0.5 (2016-06-25) 31 | 32 | * Add support for `Date`; 33 | * Add optional second argument to rounding functions (`floor`, `ceil` and 34 | so on), for "floor to 3-hour mark"; 35 | * Allow this argument, as well as in `advance`/`decrease`, to be non-integer; 36 | so, you can do `hour.advance(tm, 1/2r)` now; 37 | * Drop any `core_ext`s completely, even despite it was optional; 38 | * Add `Op` chainable operations concept (and drop `Span`, which 39 | is inferior to it); 40 | * Redesign `Sequence` creation, allow include/exclude end; 41 | * Add (experimental) resampling feature. 42 | 43 | # 0.0.4 (2016-05-28) 44 | 45 | * First "real" release with current name, `Time` and `DateTime` support, 46 | proper documentation and stuff. 47 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'coveralls', require: false 7 | end 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-15 Victor 'Zverok' Shepelev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Time Math 2 | 3 | [![Gem Version](https://badge.fury.io/rb/time_math2.svg)](http://badge.fury.io/rb/time_math2) 4 | [![Dependency Status](https://gemnasium.com/zverok/time_math2.svg)](https://gemnasium.com/zverok/time_math2) 5 | [![Code Climate](https://codeclimate.com/github/zverok/time_math2/badges/gpa.svg)](https://codeclimate.com/github/zverok/time_math2) 6 | [![Build Status](https://travis-ci.org/zverok/time_math2.svg?branch=master)](https://travis-ci.org/zverok/time_math2) 7 | [![Coverage Status](https://coveralls.io/repos/zverok/time_math2/badge.svg?branch=master)](https://coveralls.io/r/zverok/time_math2?branch=master) 8 | 9 | > **[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.** 10 | 11 | --- 12 | 13 | **TimeMath2** ~is~ was a small, no-dependencies library attempting to make time 14 | arithmetics easier. It provides you with simple, easy-to-remember API, without 15 | any monkey-patching of core Ruby classes, so it can be used alongside 16 | Rails or without it, for any purpose. 17 | 18 | ## Table Of Contents 19 | 20 | * [Features](#features) 21 | * [Naming](#naming) 22 | * [Reasons](#reasons) 23 | * [Installation](#installation) 24 | * [Usage](#usage) 25 | - [Full list of simple arithmetic methods](#full-list-of-simple-arithmetic-methods) 26 | - [Set of operations as a value object](#set-of-operations-as-a-value-object) 27 | - [Time sequence abstraction](#time-sequence-abstraction) 28 | - [Measuring time periods](#measuring-time-periods) 29 | - [Resampling](#resampling) 30 | * [Notes on timezones](#notes-on-timezones) 31 | * [Compatibility notes](#compatibility-notes) 32 | * [Alternatives](#alternatives) 33 | * [Links](#links) 34 | * [Author](#author) 35 | * [License](#license) 36 | 37 | ## Features 38 | 39 | * No monkey-patching of core classes (now **strict**; previously existing opt-in 40 | core ext removed in 0.0.5); 41 | * Works with Time, Date and DateTime; 42 | * Accurately preserves timezone offset; 43 | * Simple arithmetics: floor/ceil/round to any time unit (second, hour, year 44 | or whatnot), advance/decrease by any unit; 45 | * Chainable [operations](#set-of-operations-as-a-value-object), including 46 | construction of "set of operations" value object (like "10:20 at next 47 | month first day"), clean and powerful; 48 | * Easy generation of [time sequences](#time-sequence-abstraction) 49 | (like "each day from _this_ to _that_ date"); 50 | * Measuring of time distances between two timestamps in any units; 51 | * Powerful and flexible [resampling](#resampling) of arbitrary time value 52 | arrays/hashes into regular sequences. 53 | 54 | ## Naming 55 | 56 | `TimeMath` is the best name I know for the task library does, yet 57 | it is [already taken](https://rubygems.org/gems/time_math). So, with no 58 | other thoughts I came with the ugly solution. 59 | 60 | (BTW, the [previous version](https://github.com/zverok/time_math/blob/e997d7ddd52fc5bce3c77dc3c8022adfc9fe7028/README.md) 61 | had some dumb "funny" name for gem and all helper classes, and nobody liked it.) 62 | 63 | ## Reasons 64 | 65 | You frequently need to calculate things like "exact midnight of the next 66 | day", but you don't want to monkey-patch all of your integers, tug in 67 | 5K LOC of ActiveSupport and you like to have things clean and readable. 68 | 69 | ## Installation 70 | 71 | Install it like always: 72 | 73 | ``` 74 | $ gem install time_math2 75 | ``` 76 | 77 | or add to your Gemfile 78 | 79 | ```ruby 80 | gem 'time_math2', require: 'time_math' 81 | ``` 82 | 83 | and `bundle install` it. 84 | 85 | ## Usage 86 | 87 | First, you take time unit you want: 88 | 89 | ```ruby 90 | TimeMath[:day] # => # 91 | # or 92 | TimeMath.day # => # 93 | 94 | # List of units supported: 95 | TimeMath.units 96 | # => [:sec, :min, :hour, :day, :week, :month, :year] 97 | ``` 98 | 99 | Then you use this unit for any math you want: 100 | 101 | ```ruby 102 | TimeMath.day.floor(Time.now) # => 2016-05-28 00:00:00 +0300 103 | TimeMath.day.ceil(Time.now) # => 2016-05-29 00:00:00 +0300 104 | TimeMath.day.advance(Time.now, +10) # => 2016-06-07 14:06:57 +0300 105 | # ...and so on 106 | ``` 107 | 108 | ### Full list of simple arithmetic methods 109 | 110 | * `.floor(tm)` -- rounds down to nearest ``; 111 | * `.ceil(tm)` -- rounds up to nearest ``; 112 | * `.round(tm)` -- rounds to nearest `` (up or down); 113 | * `.round?(tm)` -- checks if `tm` is already round to ``; 114 | * `.prev(tm)` -- like `floor`, but always decreases: 115 | - `2015-06-27 13:30` would be converted to `2015-06-27 00:00` by both 116 | `floor` and `prev`, but 117 | - `2015-06-27 00:00` would be left intact on `floor`, but would be 118 | decreased to `2015-06-26 00:00` by `prev`; 119 | * `.next(tm)` -- like `ceil`, but always increases; 120 | * `.advance(tm, amount)` -- increases tm by integer amount of ``s; 121 | * `.decrease(tm, amount)` -- decreases tm by integer amount of ``s; 122 | * `.range(tm, amount)` -- creates range of `tm ... tm + amount `; 123 | * `.range_back(tm, amount)` -- creates range of `tm - amount ... tm`. 124 | 125 | **Things to note**: 126 | 127 | * rounding methods (`floor`, `ceil` and company) support optional second 128 | argument—amount of units to round to, like "each 3 hours": `hour.floor(tm, 3)`; 129 | * both rounding and advance/decrease methods allow their last argument to 130 | be float/rational, so you can `hour.advance(tm, 1/2r)` and this would 131 | work as you may expect. Non-integer arguments are only supported for 132 | units less than week (because "half of month" have no exact mathematical 133 | sense). 134 | 135 | See also [Units::Base](http://www.rubydoc.info/gems/time_math2/TimeMath/Units/Base). 136 | 137 | ### Set of operations as a value object 138 | 139 | For example, you want "10 am at next monday". By using atomic time unit 140 | operations, you'll need the code like: 141 | 142 | ```ruby 143 | TimeMath.hour.advance(TimeMath.week.ceil(Time.now), 10) 144 | ``` 145 | ...which is not really readable, to say the least. So, `TimeMath` provides 146 | one top-level method allowing to chain any operations you want: 147 | 148 | ```ruby 149 | TimeMath(Time.now).ceil(:week).advance(:hour, 10).call 150 | ``` 151 | 152 | Much more readable, huh? 153 | 154 | The best thing about it, that you can prepare "operations list" value 155 | object, and then use it (or pass to methods, or 156 | serialize to YAML and deserialize in some Sidekiq task and so on): 157 | 158 | ```ruby 159 | op = TimeMath().ceil(:week).advance(:hour, 10) 160 | # => # 161 | op.call(Time.now) 162 | # => 2016-06-27 10:00:00 +0300 163 | 164 | # It also can be called on several arguments/array of arguments: 165 | op.call(tm1, tm2, tm3) 166 | op.call(array_of_timestamps) 167 | # ...or even used as a block-ish object: 168 | array_of_timestamps.map(&op) 169 | ``` 170 | 171 | See also [TimeMath()](http://www.rubydoc.info/gems/time_math2/toplevel#TimeMath-instance_method) 172 | and underlying [TimeMath::Op](http://www.rubydoc.info/gems/time_math2/TimeMath/Op) 173 | class docs. 174 | 175 | ### Time sequence abstraction 176 | 177 | Time sequence allows you to generate an array of time values between some 178 | points: 179 | 180 | ```ruby 181 | to = Time.now 182 | # => 2016-05-28 17:47:30 +0300 183 | from = TimeMath.day.floor(to) 184 | # => 2016-05-28 00:00:00 +0300 185 | seq = TimeMath.hour.sequence(from...to) 186 | # => # 187 | p(*seq) 188 | # 2016-05-28 00:00:00 +0300 189 | # 2016-05-28 01:00:00 +0300 190 | # 2016-05-28 02:00:00 +0300 191 | # 2016-05-28 03:00:00 +0300 192 | # 2016-05-28 04:00:00 +0300 193 | # 2016-05-28 05:00:00 +0300 194 | # 2016-05-28 06:00:00 +0300 195 | # 2016-05-28 07:00:00 +0300 196 | # ...and so on 197 | ``` 198 | 199 | Note that sequence also play well with operation chain described above, 200 | so you can 201 | 202 | ```ruby 203 | seq = TimeMath.day.sequence(Time.parse('2016-05-01')...Time.parse('2016-05-04')).advance(:hour, 10).decrease(:min, 5) 204 | # => # 205 | seq.to_a 206 | # => [2016-05-01 09:55:00 +0300, 2016-05-02 09:55:00 +0300, 2016-05-03 09:55:00 +0300] 207 | ``` 208 | 209 | See also [Sequence YARD docs](http://www.rubydoc.info/gems/time_math2/TimeMath/Sequence). 210 | 211 | ### Measuring time periods 212 | 213 | Simple measure: just "how many ``s from date A to date B": 214 | 215 | ```ruby 216 | TimeMath.week.measure(Time.parse('2016-05-01'), Time.parse('2016-06-01')) 217 | # => 4 218 | ``` 219 | 220 | Measure with remaineder: returns number of ``s between dates and 221 | the date when this number would be exact: 222 | 223 | ```ruby 224 | TimeMath.week.measure_rem(Time.parse('2016-05-01'), Time.parse('2016-06-01')) 225 | # => [4, 2016-05-29 00:00:00 +0300] 226 | ``` 227 | 228 | (on May 29 there would be exactly 4 weeks since May 1). 229 | 230 | Multi-unit measuring: 231 | 232 | ```ruby 233 | # My real birthday, in fact! 234 | birthday = Time.parse('1983-02-14 13:30') 235 | 236 | # My full age 237 | TimeMath.measure(birthday, Time.now) 238 | # => {:years=>33, :months=>3, :weeks=>2, :days=>0, :hours=>1, :minutes=>25, :seconds=>52} 239 | 240 | # NB: you can use this output with String#format or String%: 241 | puts "%{years}y %{months}m %{weeks}w %{days}d %{hours}h %{minutes}m %{seconds}s" % 242 | TimeMath.measure(birthday, Time.now) 243 | # 33y 3m 2w 0d 1h 26m 15s 244 | 245 | # Option: measure without weeks 246 | TimeMath.measure(birthday, Time.now, weeks: false) 247 | # => {:years=>33, :months=>3, :days=>14, :hours=>1, :minutes=>26, :seconds=>31} 248 | 249 | # My full age in days, hours, minutes 250 | TimeMath.measure(birthday, Time.now, upto: :day) 251 | # => {:days=>12157, :hours=>2, :minutes=>26, :seconds=>55} 252 | ``` 253 | 254 | ### Resampling 255 | 256 | **Resampling** is useful for situations when you have some timestamped 257 | data (with variable holes between values), and wantto make it regular, 258 | e.g. for charts drawing. 259 | 260 | The most simple (and not very useful) resampling just turns array of 261 | irregular timestamps into regular one: 262 | 263 | ```ruby 264 | dates = %w[2016-06-01 2016-06-03 2016-06-06].map(&Date.method(:parse)) 265 | # => [#, #, #] 266 | TimeMath.day.resample(dates) 267 | # => [#, #, #, #, #, #] 268 | TimeMath.week.resample(dates) 269 | # => [#, #] 270 | TimeMath.month.resample(dates) 271 | # => [#] 272 | ``` 273 | 274 | Much more useful is _hash resampling_: when you have a hash of `{timestamp => value}` 275 | and... 276 | 277 | ```ruby 278 | data = {Date.parse('2016-06-01') => 18, Date.parse('2016-06-03') => 8, Date.parse('2016-06-06') => -4} 279 | # => {#=>18, #=>8, #=>-4} 280 | TimeMath.day.resample(data) 281 | # => {#=>[18], #=>[], #=>[8], #=>[], #=>[], #=>[-4]} 282 | TimeMath.week.resample(data) 283 | # => {#=>[18, 8], #=>[-4]} 284 | TimeMath.month.resample(data) 285 | # => {#=>[18, 8, -4]} 286 | ``` 287 | 288 | For values grouping strategy, `resample` accepts symbol and block arguments: 289 | 290 | ```ruby 291 | TimeMath.week.resample(data, :first) 292 | # => {#=>18, #=>-4} 293 | TimeMath.week.resample(data) { |vals| vals.inject(:+) } 294 | => {#=>26, #=>-4} 295 | ``` 296 | 297 | The functionality currently considered experimental, please notify me 298 | about your ideas and use cases via [GitHub issues](https://github.com/zverok/time_math2/issues)! 299 | 300 | ## Notes on timezones 301 | 302 | TimeMath tries its best to preserve timezones of original values. Currently, 303 | it means: 304 | 305 | * For `Time` instances, symbolic timezone is preserved; when jumping over 306 | DST border, UTC offset will change and everything remains as expected; 307 | * For `DateTime` Ruby not provides symbolic timezone, only numeric offset; 308 | it is preserved by TimeMath (but be careful about jumping around DST, 309 | offset would not change). 310 | 311 | ## Compatibility notes 312 | 313 | TimeMath is known to work on MRI Ruby >= 2.0 and JRuby >= 9.0.0.0. 314 | 315 | On Rubinius, some of tests fail and I haven't time to investigate it. If 316 | somebody still uses Rubinius and wants TimeMath to be working properly 317 | on it, please let me know. 318 | 319 | ## Alternatives 320 | 321 | There's pretty small and useful [AS::Duration](https://github.com/janko-m/as-duration) 322 | by Janko Marohnić, which is time durations, extracted from ActiveSupport, 323 | but without any ActiveSupport bloat. 324 | 325 | ## Links 326 | 327 | * [API Docs](http://www.rubydoc.info/gems/time_math2) 328 | 329 | ## Author 330 | 331 | [Victor Shepelev](http://zverok.github.io/) 332 | 333 | ## License 334 | 335 | [MIT](https://github.com/zverok/time_math2/blob/master/LICENSE.txt). 336 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rubygems/tasks' 3 | Gem::Tasks.new 4 | 5 | namespace :doc do 6 | desc "Prints TOC for README.md (doesn't inserts it automatically!)" 7 | task :toc do 8 | # NB: really dumb. Yet better than any Solution I can find :( 9 | 10 | File.readlines('README.md'). 11 | map(&:chomp).grep(/\#{2,}\s*/). 12 | each do |ln| 13 | level, text = ln.scan(/^(\#{2,})\s*(.+)$/).flatten 14 | next if text == 'Table Of Contents' 15 | 16 | link = text.downcase. 17 | gsub(/[\/]/, ''). 18 | gsub(/[.?, ]/, '-').gsub(/-{2,}/, '-'). 19 | gsub(/^-|-$/, '') 20 | 21 | puts '%s* [%s](#%s)' % 22 | [ 23 | ' ' * (level.count('#') - 2), 24 | text, 25 | link 26 | ] 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/time_math.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | # TimeMath is a small library for easy time units arithmetics (like "floor 4 | # the timestamp to the nearest hour", "advance the time value by 3 days" 5 | # and so on). 6 | # 7 | # It has clean and easy-to-remember API, just like this: 8 | # 9 | # ```ruby 10 | # TimeMath.day.floor(Time.now) 11 | # # or 12 | # TimeMath[:day].floor(Time.now) 13 | # ``` 14 | # 15 | # `TimeMath[unit]` and `TimeMath.` give you an instance of 16 | # time unit, which incapsulates most of the functionality. Refer to 17 | # {Units::Base} to see what you can get of it. 18 | # 19 | # See also `TimeMath()` method in global namespace, it is lot of fun! 20 | # 21 | module TimeMath 22 | require_relative './time_math/units' 23 | require_relative './time_math/op' 24 | require_relative './time_math/sequence' 25 | require_relative './time_math/measure' 26 | require_relative './time_math/resamplers' 27 | require_relative './time_math/util' 28 | 29 | module_function 30 | 31 | # List all unit names known. 32 | # 33 | # @return [Array] 34 | def units 35 | Units.names 36 | end 37 | 38 | # Main method to do something with TimeMath. Returns an object 39 | # representing some time measurement unit. See {Units::Base} documentation 40 | # to know what you can do with it. 41 | # 42 | # @return [Units::Base] 43 | def [](unit) 44 | Units.get(unit) 45 | end 46 | 47 | # @!method sec 48 | # Shortcut to get second unit. 49 | # @return [Units::Base] 50 | # 51 | # @!method min 52 | # Shortcut to get minute unit. 53 | # @return [Units::Base] 54 | # 55 | # @!method hour 56 | # Shortcut to get hour unit. 57 | # @return [Units::Base] 58 | # 59 | # @!method day 60 | # Shortcut to get day unit. 61 | # @return [Units::Base] 62 | # 63 | # @!method week 64 | # Shortcut to get week unit. 65 | # @return [Units::Base] 66 | # 67 | # @!method month 68 | # Shortcut to get month unit. 69 | # @return [Units::Base] 70 | # 71 | # @!method year 72 | # Shortcut to get year unit. 73 | # @return [Units::Base] 74 | # 75 | Units.names.each do |unit| 76 | define_singleton_method(unit) { Units.get(unit) } 77 | end 78 | 79 | # Measures distance between two time values in all units at once. 80 | # 81 | # Just like this: 82 | # 83 | # ```ruby 84 | # birthday = Time.parse('1983-02-14 13:30') 85 | # 86 | # TimeMath.measure(birthday, Time.now) 87 | # # => {:years=>33, :months=>3, :weeks=>2, :days=>0, :hours=>1, :minutes=>25, :seconds=>52} 88 | # ``` 89 | # 90 | # @param from [Time,Date,DateTime] 91 | # @param to [Time,Date,DateTime] 92 | # @param options [Hash] options 93 | # @option options [Boolean] :weeks pass `false` to exclude weeks from calculation; 94 | # @option options [Symbol] :upto pass max unit to use (e.g. if you'll 95 | # pass `:day`, period would be measured in days, hours, minutes and seconds). 96 | # 97 | # @return [Hash] 98 | def measure(from, to, options = {}) 99 | Measure.measure(from, to, options) 100 | end 101 | end 102 | 103 | # This method helps to create time arithmetics sequence as a value object. 104 | # Some examples: 105 | # 106 | # ```ruby 107 | # # 10 am at first weekday? Easy! 108 | # TimeMath(Time.now).floor(:week).advance(:hour, 10).call 109 | # # => 2016-06-20 10:00:00 +0300 110 | # 111 | # # For several time values? Nothing easier! 112 | # TimeMath(Time.local(2016,1,1), Time.local(2016,2,1), Time.local(2016,3,1)).floor(:week).advance(:hour, 10).call 113 | # # => [2015-12-28 10:00:00 +0200, 2016-02-01 10:00:00 +0200, 2016-02-29 10:00:00 +0200] 114 | # 115 | # # Or, the most fun, you can create complicated operation and call it 116 | # # later: 117 | # op = TimeMath().floor(:week).advance(:hour, 10) 118 | # # => # 119 | # op.call(Time.now) 120 | # # => 2016-06-20 10:00:00 +0300 121 | # 122 | # # or even as a lambda: 123 | # times = [Time.local(2016,1,1), Time.local(2016,2,1), Time.local(2016,3,1)] 124 | # times.map(&op) 125 | # # => [2015-12-28 10:00:00 +0200, 2016-02-01 10:00:00 +0200, 2016-02-29 10:00:00 +0200] 126 | # ``` 127 | # 128 | # See also {TimeMath::Op} for list of operations available, but basically 129 | # they are all same you can call on {TimeMath::Units::Base}, just pass unit symbol 130 | # as a first argument. 131 | # 132 | # @param arguments time-y value, or list of them, or nothing 133 | # 134 | # @return [TimeMath::Op] 135 | def TimeMath(*arguments) # rubocop:disable Naming/MethodName 136 | TimeMath::Op.new(*arguments) 137 | end 138 | -------------------------------------------------------------------------------- /lib/time_math/measure.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | # @private 3 | module Measure 4 | PLURALS = { 5 | year: :years, 6 | month: :months, 7 | week: :weeks, 8 | day: :days, 9 | hour: :hours, 10 | min: :minutes, 11 | sec: :seconds 12 | }.freeze 13 | 14 | def self.measure(from, to, options = {}) 15 | select_units(options).reverse.inject({}) do |res, unit| 16 | span, from = Units.get(unit).measure_rem(from, to) 17 | res.merge(PLURALS[unit] => span) 18 | end 19 | end 20 | 21 | def self.select_units(options) 22 | units = Units.names 23 | units.delete(:week) if options[:weeks] == false 24 | 25 | if (idx = units.index(options[:upto])) 26 | units = units.first(idx + 1) 27 | end 28 | 29 | units 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/time_math/op.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | # `Op` is value object, incapsulating several operations performed on 3 | # time unit. The names of operations are the same the single unit can 4 | # perform, first parameter is always a unit. 5 | # 6 | # Ops can be created by `TimeMath::Op.new` or with pretty shortcut 7 | # `TimeMath()`. 8 | # 9 | # Available usages: 10 | # 11 | # ```ruby 12 | # # 1. chain operations: 13 | # # without Op: 10:25 at first day of next week: 14 | # TimeMath.min.advance(TimeMath.hour.advance(TimeMath.week.ceil(tm), 10), 25) 15 | # # FOOOOOO 16 | # # ...but with Op: 17 | # TimeMath(tm).ceil(:week).advance(:hour, 10).advance(:min, 25).call 18 | # 19 | # # 2. chain operations on multiple objects: 20 | # TimeMath(tm1, tm2, tm3).ceil(:week).advance(:hour, 10).advance(:min, 25).call 21 | # # or 22 | # TimeMath([array_of_times]).ceil(:week).advance(:hour, 10).advance(:min, 25).call 23 | # 24 | # # 3. preparing operation to be used on any objects: 25 | # op = TimeMath().ceil(:week).advance(:hour, 10).advance(:min, 25) 26 | # op.call(tm) 27 | # op.call(tm1, tm2, tm3) 28 | # op.call(array_of_times) 29 | # # or even block-ish behavior: 30 | # [tm1, tm2, tm3].map(&op) 31 | # ``` 32 | # 33 | # Note that Op also plays well with {Sequence} (see its docs for more). 34 | class Op 35 | # @private 36 | OPERATIONS = %i[floor ceil round next prev advance decrease].freeze 37 | 38 | attr_reader :operations, :arguments 39 | 40 | # Creates Op. Could (and recommended be also by its alias -- just 41 | # `TimeMath(*arguments)`. 42 | # 43 | # @param arguments one, or several, or an array of time-y values 44 | # (Time, Date, DateTime). 45 | def initialize(*arguments) 46 | @arguments = arguments 47 | @operations = [] 48 | end 49 | 50 | # @private 51 | def initialize_copy(other) 52 | @arguments = other.arguments.dup 53 | @operations = other.operations.dup 54 | end 55 | 56 | # @method floor!(unit, span = 1) 57 | # Adds {Units::Base#floor} to list of operations. 58 | # 59 | # @param unit [Symbol] One of {TimeMath.units} 60 | # @param span [Numeric] how many units to floor to. 61 | # @return [self] 62 | # 63 | # @method floor(unit, span = 1) 64 | # Non-destructive version of {#floor!}. 65 | # @param unit [Symbol] One of {TimeMath.units} 66 | # @param span [Numeric] how many units to floor to. 67 | # @return [Op] 68 | # 69 | # @method ceil!(unit, span = 1) 70 | # Adds {Units::Base#ceil} to list of operations. 71 | # @param unit [Symbol] One of {TimeMath.units} 72 | # @param span [Numeric] how many units to ceil to. 73 | # @return [self] 74 | # 75 | # @method ceil(unit, span = 1) 76 | # Non-destructive version of {#ceil!}. 77 | # @param unit [Symbol] One of {TimeMath.units} 78 | # @param span [Numeric] how many units to ceil to. 79 | # @return [Op] 80 | # 81 | # @method round!(unit, span = 1) 82 | # Adds {Units::Base#round} to list of operations. 83 | # @param unit [Symbol] One of {TimeMath.units} 84 | # @param span [Numeric] how many units to round to. 85 | # @return [self] 86 | # 87 | # @method round(unit, span = 1) 88 | # Non-destructive version of {#round!}. 89 | # @param unit [Symbol] One of {TimeMath.units} 90 | # @param span [Numeric] how many units to round to. 91 | # @return [Op] 92 | # 93 | # @method next!(unit, span = 1) 94 | # Adds {Units::Base#next} to list of operations. 95 | # @param unit [Symbol] One of {TimeMath.units} 96 | # @param span [Numeric] how many units to ceil to. 97 | # @return [self] 98 | # 99 | # @method next(unit, span = 1) 100 | # Non-destructive version of {#next!}. 101 | # @param unit [Symbol] One of {TimeMath.units} 102 | # @param span [Numeric] how many units to ceil to. 103 | # @return [Op] 104 | # 105 | # @method prev!(unit, span = 1) 106 | # Adds {Units::Base#prev} to list of operations. 107 | # @param unit [Symbol] One of {TimeMath.units} 108 | # @param span [Numeric] how many units to floor to. 109 | # @return [self] 110 | # 111 | # @method prev(unit, span = 1) 112 | # Non-destructive version of {#prev!}. 113 | # @param unit [Symbol] One of {TimeMath.units} 114 | # @param span [Numeric] how many units to floor to. 115 | # @return [Op] 116 | # 117 | # @method advance!(unit, amount = 1) 118 | # Adds {Units::Base#advance} to list of operations. 119 | # @param unit [Symbol] One of {TimeMath.units} 120 | # @param amount [Numeric] how many units to advance. 121 | # @return [self] 122 | # 123 | # @method advance(unit, amount = 1) 124 | # Non-destructive version of {#advance!}. 125 | # @param unit [Symbol] One of {TimeMath.units} 126 | # @param amount [Numeric] how many units to advance. 127 | # @return [Op] 128 | # 129 | # @method decrease!(unit, amount = 1) 130 | # Adds {Units::Base#decrease} to list of operations. 131 | # @param unit [Symbol] One of {TimeMath.units} 132 | # @param amount [Numeric] how many units to decrease. 133 | # @return [self] 134 | # 135 | # @method decrease(unit, amount = 1) 136 | # Non-destructive version of {#decrease!}. 137 | # @param unit [Symbol] One of {TimeMath.units} 138 | # @param amount [Numeric] how many units to decrease. 139 | # @return [Op] 140 | # 141 | 142 | OPERATIONS.each do |op| 143 | define_method "#{op}!" do |unit, *args| 144 | Units.names.include?(unit) or raise(ArgumentError, "Unknown unit #{unit}") 145 | @operations << [op, unit, args] 146 | self 147 | end 148 | 149 | define_method op do |unit, *args| 150 | dup.send("#{op}!", unit, *args) 151 | end 152 | end 153 | 154 | def inspect 155 | "#<#{self.class}#{inspect_args}" + inspect_operations + '>' 156 | end 157 | 158 | # @private 159 | def inspect_operations 160 | operations.map { |op, unit, args| 161 | "#{op}(#{[unit, *args].map(&:inspect).join(', ')})" 162 | }.join('.') 163 | end 164 | 165 | def ==(other) 166 | self.class == other.class && operations == other.operations && 167 | arguments == other.arguments 168 | end 169 | 170 | # Performs op. If an Op was created with arguments, just performs all 171 | # operations on them and returns the result. If it was created without 172 | # arguments, performs all operations on arguments provided to `call`. 173 | # 174 | # @param tms one, or several, or an array of time-y values; should not 175 | # be passed if Op was created with arguments. 176 | # @return [Time,Date,DateTime,Array] one, or an array of processed arguments 177 | def call(*tms) 178 | unless @arguments.empty? 179 | tms.empty? or raise(ArgumentError, 'Op arguments is already set, use call()') 180 | tms = @arguments 181 | end 182 | res = [*tms].flatten.map(&method(:perform)) 183 | tms.count == 1 && Util.timey?(tms.first) ? res.first : res 184 | end 185 | 186 | # Allows to use Op as a block: 187 | # 188 | # ```ruby 189 | # timestamps.map(&TimeMath().ceil(:week).advance(:day, 1)) 190 | # ``` 191 | # @return [Proc] 192 | def to_proc 193 | method(:call).to_proc 194 | end 195 | 196 | private 197 | 198 | def inspect_args 199 | return ' ' if @arguments.empty? 200 | 201 | '(' + [*@arguments].map(&:inspect).join(', ') + ').' 202 | end 203 | 204 | def perform(tm) 205 | operations.inject(tm) { |memo, (op, unit, args)| 206 | TimeMath::Units.get(unit).send(op, memo, *args) 207 | } 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/time_math/resamplers.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | # @private 3 | class Resampler 4 | class << self 5 | def call(unit, array_or_hash, symbol = nil, &block) 6 | resampler = 7 | ArrayResampler.try(unit, array_or_hash) || 8 | HashResampler.try(unit, array_or_hash) or 9 | raise ArgumentError, "Expected array of timestamps or hash with timestamp keys, #{array_or_hash} got" 10 | 11 | resampler.call(symbol, &block) 12 | end 13 | end 14 | 15 | def initialize(unit) 16 | @unit = Units.get(unit) 17 | end 18 | 19 | def call 20 | raise NotImplementedError 21 | end 22 | 23 | private 24 | 25 | def sequence 26 | @sequence ||= @unit.sequence(timestamps.min..timestamps.max) 27 | end 28 | end 29 | 30 | # @private 31 | class ArrayResampler < Resampler 32 | def self.try(unit, array) 33 | return nil unless array.is_a?(Array) && array.all?(&Util.method(:timey?)) 34 | 35 | new(unit, array) 36 | end 37 | 38 | def initialize(unit, array) 39 | super(unit) 40 | @array = array 41 | end 42 | 43 | def call(*) 44 | sequence.to_a 45 | end 46 | 47 | private 48 | 49 | def timestamps 50 | @array 51 | end 52 | end 53 | 54 | # @private 55 | class HashResampler < Resampler 56 | def self.try(unit, hash) 57 | return unless hash.is_a?(Hash) && hash.keys.all?(&Util.method(:timey?)) 58 | 59 | new(unit, hash) 60 | end 61 | 62 | def initialize(unit, hash) 63 | super(unit) 64 | @hash = hash 65 | end 66 | 67 | def call(symbol = nil, &block) 68 | block = symbol.to_proc if symbol && !block 69 | 70 | sequence.ranges.map do |r| 71 | values = @hash.select { |k, _| r.cover?(k) }.map(&:last) 72 | values = block.call(values) if block 73 | [r.begin, values] 74 | end.to_h 75 | end 76 | 77 | private 78 | 79 | def timestamps 80 | @hash.keys 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/time_math/sequence.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | # Sequence represents a sequential units of time between two points. 3 | # It has several options and convenience methods for creating arrays of 4 | # data. 5 | # 6 | # Basic usage example: 7 | # 8 | # ```ruby 9 | # from = Time.parse('2016-05-01 13:30') 10 | # to = Time.parse('2016-05-04 18:20') 11 | # seq = TimeMath.day.sequence(from...to) 12 | # # => # 13 | # ``` 14 | # 15 | # Now, you can use it: 16 | # 17 | # ```ruby 18 | # seq.to_a 19 | # # => [2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300] 20 | # ``` 21 | # -- it's an "each day start between from and to". 22 | # 23 | # Depending of including/excluding of range, you will, or will not receive period that includes `to`: 24 | # 25 | # ```ruby 26 | # TimeMath.day.sequence(from..to).to_a 27 | # # => [2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300] 28 | # ``` 29 | # 30 | # Besides each period beginning, you can also request pairs of begin/end 31 | # of a period, either as an array of arrays, or array of ranges: 32 | # 33 | # ```ruby 34 | # seq.pairs 35 | # # => [[2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300], [2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300], [2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300]] 36 | # seq.ranges 37 | # # => [2016-05-01 00:00:00 +0300...2016-05-02 00:00:00 +0300, 2016-05-02 00:00:00 +0300...2016-05-03 00:00:00 +0300, 2016-05-03 00:00:00 +0300...2016-05-04 00:00:00 +0300] 38 | # ``` 39 | # 40 | # It is pretty convenient for filtering data from databases or APIs: TimeMath creates list of 41 | # filtering ranges in a blink. 42 | # 43 | # Sequence also supports any item-updating operations in the same fashion 44 | # {Op} does: 45 | # 46 | # ```ruby 47 | # seq = TimeMath.day.sequence(from...to).advance(:hour, 5).decrease(:min, 20) 48 | # # => # 49 | # seq.to_a 50 | # # => [2016-05-01 04:40:00 +0300, 2016-05-02 04:40:00 +0300, 2016-05-03 04:40:00 +0300] 51 | # ``` 52 | # 53 | class Sequence 54 | # Creates a sequence. Typically, it is easier to do it with {Units::Base#sequence}, 55 | # like this: 56 | # 57 | # ```ruby 58 | # TimeMath.day.sequence(from...to) 59 | # ``` 60 | # 61 | # @param unit [Symbol] one of {TimeMath.units}; 62 | # @param range [Range] range of time-y values (Time, Date, DateTime); 63 | # note that range with inclusive and exclusive and will produce 64 | # different sequences. 65 | # 66 | def initialize(unit, range) 67 | @unit = Units.get(unit) 68 | @from, @to = process_range(range) 69 | @op = Op.new 70 | end 71 | 72 | # @private 73 | def initialize_copy(other) 74 | @unit = other.unit 75 | @from, @to = other.from, other.to 76 | @op = other.op.dup 77 | end 78 | 79 | attr_reader :from, :to, :unit, :op 80 | 81 | # Compares two sequences, considering their start, end, unit and 82 | # operations. 83 | # 84 | # @param other [Sequence] 85 | # @return [Boolean] 86 | def ==(other) # rubocop:disable Metrics/AbcSize 87 | self.class == other.class && unit == other.unit && 88 | from == other.from && to == other.to && 89 | op == other.op 90 | end 91 | 92 | # @method floor!(unit, span = 1) 93 | # Adds {Units::Base#floor} to list of operations to apply to sequence items. 94 | # 95 | # @param unit [Symbol] One of {TimeMath.units} 96 | # @param span [Numeric] how many units to floor to. 97 | # @return [self] 98 | # 99 | # @method floor(unit, span = 1) 100 | # Non-destructive version of {#floor!}. 101 | # @param unit [Symbol] One of {TimeMath.units} 102 | # @param span [Numeric] how many units to floor to. 103 | # @return [Sequence] 104 | # 105 | # @method ceil!(unit, span = 1) 106 | # Adds {Units::Base#ceil} to list of operations to apply to sequence items. 107 | # @param unit [Symbol] One of {TimeMath.units} 108 | # @param span [Numeric] how many units to ceil to. 109 | # @return [self] 110 | # 111 | # @method ceil(unit, span = 1) 112 | # Non-destructive version of {#ceil!}. 113 | # @param unit [Symbol] One of {TimeMath.units} 114 | # @param span [Numeric] how many units to ceil to. 115 | # @return [Sequence] 116 | # 117 | # @method round!(unit, span = 1) 118 | # Adds {Units::Base#round} to list of operations to apply to sequence items. 119 | # @param unit [Symbol] One of {TimeMath.units} 120 | # @param span [Numeric] how many units to round to. 121 | # @return [self] 122 | # 123 | # @method round(unit, span = 1) 124 | # Non-destructive version of {#round!}. 125 | # @param unit [Symbol] One of {TimeMath.units} 126 | # @param span [Numeric] how many units to round to. 127 | # @return [Sequence] 128 | # 129 | # @method next!(unit, span = 1) 130 | # Adds {Units::Base#next} to list of operations to apply to sequence items. 131 | # @param unit [Symbol] One of {TimeMath.units} 132 | # @param span [Numeric] how many units to ceil to. 133 | # @return [self] 134 | # 135 | # @method next(unit, span = 1) 136 | # Non-destructive version of {#next!}. 137 | # @param unit [Symbol] One of {TimeMath.units} 138 | # @param span [Numeric] how many units to ceil to. 139 | # @return [Sequence] 140 | # 141 | # @method prev!(unit, span = 1) 142 | # Adds {Units::Base#prev} to list of operations to apply to sequence items. 143 | # @param unit [Symbol] One of {TimeMath.units} 144 | # @param span [Numeric] how many units to floor to. 145 | # @return [self] 146 | # 147 | # @method prev(unit, span = 1) 148 | # Non-destructive version of {#prev!}. 149 | # @param unit [Symbol] One of {TimeMath.units} 150 | # @param span [Numeric] how many units to floor to. 151 | # @return [Sequence] 152 | # 153 | # @method advance!(unit, amount = 1) 154 | # Adds {Units::Base#advance} to list of operations to apply to sequence items. 155 | # @param unit [Symbol] One of {TimeMath.units} 156 | # @param amount [Numeric] how many units to advance. 157 | # @return [self] 158 | # 159 | # @method advance(unit, amount = 1) 160 | # Non-destructive version of {#advance!}. 161 | # @param unit [Symbol] One of {TimeMath.units} 162 | # @param amount [Numeric] how many units to advance. 163 | # @return [Sequence] 164 | # 165 | # @method decrease!(unit, amount = 1) 166 | # Adds {Units::Base#decrease} to list of operations to apply to sequence items. 167 | # @param unit [Symbol] One of {TimeMath.units} 168 | # @param amount [Numeric] how many units to decrease. 169 | # @return [self] 170 | # 171 | # @method decrease(unit, amount = 1) 172 | # Non-destructive version of {#decrease!}. 173 | # @param unit [Symbol] One of {TimeMath.units} 174 | # @param amount [Numeric] how many units to decrease. 175 | # @return [Sequence] 176 | # 177 | 178 | Op::OPERATIONS.each do |operation| 179 | define_method "#{operation}!" do |*arg| 180 | @op.send("#{operation}!", *arg) 181 | self 182 | end 183 | 184 | define_method operation do |*arg| 185 | dup.send("#{operation}!", *arg) 186 | end 187 | end 188 | 189 | # Enumerates time unit between `from` and `to`. They will have same granularity as from 190 | # (e.g. if `unit` is day and from is 2016-05-01 13:30, each of return values will be next 191 | # day at 13:30), unless sequence is not set to floor values. 192 | # 193 | # @return [Enumerator] 194 | def each 195 | return to_enum(:each) unless block_given? 196 | 197 | iter = from 198 | while iter <= to 199 | yield(op.call(iter)) 200 | 201 | iter = unit.advance(iter) 202 | end 203 | end 204 | 205 | include Enumerable 206 | 207 | # Creates an array of pairs (time unit start, time unit end) between 208 | # from and to. 209 | # 210 | # @return [Array] 211 | def pairs 212 | seq = to_a 213 | seq.zip([*seq[1..-1], unit.advance(to)]) 214 | end 215 | 216 | # Creates an array of Ranges (time unit start...time unit end) between 217 | # from and to. 218 | # 219 | # @return [Array] 220 | def ranges 221 | pairs.map { |b, e| (b...e) } 222 | end 223 | 224 | def inspect 225 | ops = op.inspect_operations 226 | ops = '.' + ops unless ops.empty? 227 | "#<#{self.class} #{unit.name} (#{from} - #{to})#{ops}>" 228 | end 229 | 230 | private 231 | 232 | def valid_time_range?(range) 233 | range.is_a?(Range) && Util.timey?(range.begin) && Util.timey?(range.end) 234 | end 235 | 236 | def process_range(range) 237 | valid_time_range?(range) or 238 | raise ArgumentError, "Range of time-y values expected, #{range} got" 239 | 240 | range_end = unit.floor(range.end) 241 | [unit.floor(range.begin), range.exclude_end? ? unit.decrease(range_end) : range_end] 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /lib/time_math/units.rb: -------------------------------------------------------------------------------- 1 | require_relative 'units/base' 2 | require_relative 'units/simple' 3 | require_relative 'units/day' 4 | require_relative 'units/week' 5 | require_relative 'units/month' 6 | require_relative 'units/year' 7 | 8 | module TimeMath 9 | # See {Units::Base} for detailed description of all units functionality. 10 | module Units 11 | # @private 12 | UNITS = { 13 | sec: Units::Sec.new, min: Units::Min.new, hour: Units::Hour.new, 14 | day: Units::Day.new, week: Units::Week.new, month: Units::Month.new, 15 | year: Units::Year.new 16 | }.freeze 17 | 18 | # @private 19 | def self.names 20 | UNITS.keys 21 | end 22 | 23 | # @private 24 | def self.get(name) 25 | UNITS[name] or 26 | raise ArgumentError, "Unsupported unit: #{name}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/time_math/units/base.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | module Units 3 | # It is a main class representing most of TimeMath functionality. 4 | # It (or rather its descendants) represents "unit of time" and 5 | # connected calculations logic. Typical usage: 6 | # 7 | # ```ruby 8 | # TimeMath.day.advance(tm, 5) # advances tm by 5 days 9 | # ``` 10 | # 11 | # See also {TimeMath::Op} for performing multiple operations in 12 | # concise & DRY manner, like this: 13 | # 14 | # ```ruby 15 | # TimeMath().advance(:day, 5).floor(:hour).advance(:min, 20).call(tm) 16 | # ``` 17 | # 18 | class Base 19 | # Creates unit of time. Typically you don't need it, as it is 20 | # easier to do `TimeMath.day` or `TimeMath[:day]` to obtain it. 21 | # 22 | # @param name [Symbol] one of {TimeMath.units}. 23 | def initialize(name) 24 | @name = name 25 | end 26 | 27 | attr_reader :name 28 | 29 | # Rounds `tm` down to nearest unit (this means, `TimeMath.day.floor(tm)` 30 | # will return beginning of `tm`-s day, and so on). 31 | # 32 | # An optional second argument allows you to floor to arbitrary 33 | # number of units, like to "each 3-hour" mark: 34 | # 35 | # ```ruby 36 | # TimeMath.hour.floor(Time.parse('14:00'), 3) 37 | # # => 2016-06-23 12:00:00 +0300 38 | # 39 | # # works well with float/rational spans 40 | # TimeMath.hour.floor(Time.parse('14:15'), 1/2r) 41 | # # => 2016-06-23 14:00:00 +0300 42 | # TimeMath.hour.floor(Time.parse('14:45'), 1/2r) 43 | # # => 2016-06-23 14:30:00 +0300 44 | # ``` 45 | # 46 | # @param tm [Time,Date,DateTime] time value to floor. 47 | # @param span [Numeric] how many units to floor to. For units 48 | # less than week supports float/rational values. 49 | # @return [Time,Date,DateTime] floored time value; class and timezone offset of origin 50 | # would be preserved. 51 | def floor(tm, span = 1) 52 | int_floor = advance(floor_1(tm), (tm.send(name) / span.to_f).floor * span - tm.send(name)) 53 | float_fix(tm, int_floor, span % 1) 54 | end 55 | 56 | # Rounds `tm` up to nearest unit (this means, `TimeMath.day.ceil(tm)` 57 | # will return beginning of day next after `tm`, and so on). 58 | # An optional second argument allows to ceil to arbitrary 59 | # amount of units (see {#floor} for more detailed explanation). 60 | # 61 | # @param tm [Time,Date,DateTime] time value to ceil. 62 | # @param span [Numeric] how many units to ceil to. For units 63 | # less than week supports float/rational values. 64 | # @return [Time,Date,DateTime] ceiled time value; class and timezone offset 65 | # of origin would be preserved. 66 | def ceil(tm, span = 1) 67 | f = floor(tm, span) 68 | 69 | f == tm ? f : advance(f, span) 70 | end 71 | 72 | # Rounds `tm` up or down to nearest unit (this means, `TimeMath.day.round(tm)` 73 | # will return beginning of `tm` day if `tm` is before noon, and 74 | # day next after `tm` if it is after, and so on). 75 | # An optional second argument allows to round to arbitrary 76 | # amount of units (see {#floor} for more detailed explanation). 77 | # 78 | # @param tm [Time,Date,DateTime] time value to round. 79 | # @param span [Numeric] how many units to round to. For units 80 | # less than week supports float/rational values. 81 | # @return [Time,Date,DateTime] rounded time value; class and timezone offset 82 | # of origin would be preserved. 83 | def round(tm, span = 1) 84 | f, c = floor(tm, span), ceil(tm, span) 85 | 86 | (tm - f).abs < (tm - c).abs ? f : c 87 | end 88 | 89 | # Like {#floor}, but always return value lower than `tm` (e.g. if 90 | # `tm` is exactly midnight, then `TimeMath.day.prev(tm)` will return 91 | # _previous midnight_). 92 | # An optional second argument allows to floor to arbitrary 93 | # amount of units (see {#floor} for more detailed explanation). 94 | # 95 | # @param tm [Time,Date,DateTime] time value to calculate prev on. 96 | # @param span [Numeric] how many units to floor to. For units 97 | # less than week supports float/rational values. 98 | # @return [Time,Date,DateTime] prev time value; class and timezone offset 99 | # of origin would be preserved. 100 | def prev(tm, span = 1) 101 | f = floor(tm, span) 102 | f == tm ? decrease(f, span) : f 103 | end 104 | 105 | # Like {#ceil}, but always return value greater than `tm` (e.g. if 106 | # `tm` is exactly midnight, then `TimeMath.day.next(tm)` will return 107 | # _next midnight_). 108 | # An optional second argument allows to ceil to arbitrary 109 | # amount of units (see {#floor} for more detailed explanation). 110 | # 111 | # @param tm [Time,Date,DateTime] time value to calculate next on. 112 | # @param span [Numeric] how many units to ceil to. For units 113 | # less than week supports float/rational values. 114 | # @return [Time,Date,DateTime] next time value; class and timezone offset 115 | # of origin would be preserved. 116 | def next(tm, span = 1) 117 | c = ceil(tm, span) 118 | c == tm ? advance(c, span) : c 119 | end 120 | 121 | # Checks if `tm` is exactly rounded to unit. 122 | # 123 | # @param tm [Time,Date,DateTime] time value to check. 124 | # @param span [Numeric] how many units to check round at. For units 125 | # less than week supports float/rational values. 126 | # @return [Boolean] whether `tm` is exactly round to unit. 127 | def round?(tm, span = 1) 128 | floor(tm, span) == tm 129 | end 130 | 131 | # Advances `tm` by given amount of unit. 132 | # 133 | # @param tm [Time,Date,DateTime] time value to advance; 134 | # @param amount [Numeric] how many units forward to go. For units 135 | # less than week supports float/rational values. 136 | # 137 | # @return [Time,Date,DateTime] advanced time value; class and timezone offset 138 | # of origin would be preserved. 139 | def advance(tm, amount = 1) 140 | return decrease(tm, -amount) if amount < 0 141 | 142 | _advance(tm, amount) 143 | end 144 | 145 | # Decreases `tm` by given amount of unit. 146 | # 147 | # @param tm [Time,Date,DateTime] time value to decrease; 148 | # @param amount [Integer] how many units forward to go. For units 149 | # less than week supports float/rational values. 150 | # 151 | # @return [Time,Date,DateTime] decrease time value; class and timezone offset 152 | # of origin would be preserved. 153 | def decrease(tm, amount = 1) 154 | return advance(tm, -amount) if amount < 0 155 | 156 | _decrease(tm, amount) 157 | end 158 | 159 | # Creates range from `tm` to `tm` increased by amount of units. 160 | # 161 | # ```ruby 162 | # tm = Time.parse('2016-05-28 16:30') 163 | # TimeMath.day.range(tm, 5) 164 | # # => 2016-05-28 16:30:00 +0300...2016-06-02 16:30:00 +0300 165 | # ``` 166 | # 167 | # @param tm [Time,Date,DateTime] time value to create range from; 168 | # @param amount [Integer] how many units should be between range 169 | # start and end. 170 | # 171 | # @return [Range] 172 | def range(tm, amount = 1) 173 | (tm...advance(tm, amount)) 174 | end 175 | 176 | # Creates range from `tm` decreased by amount of units to `tm`. 177 | # 178 | # ```ruby 179 | # tm = Time.parse('2016-05-28 16:30') 180 | # TimeMath.day.range_back(tm, 5) 181 | # # => 2016-05-23 16:30:00 +0300...2016-05-28 16:30:00 +0300 182 | # ``` 183 | # 184 | # @param tm [Time,Date,DateTime] time value to create range from; 185 | # @param amount [Integer] how many units should be between range 186 | # start and end. 187 | # 188 | # @return [Range] 189 | def range_back(tm, amount = 1) 190 | (decrease(tm, amount)...tm) 191 | end 192 | 193 | # Measures distance between `from` and `to` in units of this class. 194 | # 195 | # @param from [Time,Date,DateTime] start of period; 196 | # @param to [Time,Date,DateTime] end of period. 197 | # 198 | # @return [Integer] how many full units are inside the period. 199 | # :nocov: 200 | def measure(from, to) 201 | from, to = from.to_time, to.to_time unless from.class == to.class 202 | from <= to ? _measure(from, to) : -_measure(to, from) 203 | end 204 | # :nocov: 205 | 206 | # Like {#measure} but also returns "remainder": the time where 207 | # it would be **exactly** returned amount of units between `from` 208 | # and `to`: 209 | # 210 | # ```ruby 211 | # TimeMath.day.measure(Time.parse('2016-05-01 16:20'), Time.parse('2016-05-28 15:00')) 212 | # # => 26 213 | # TimeMath.day.measure_rem(Time.parse('2016-05-01 16:20'), Time.parse('2016-05-28 15:00')) 214 | # # => [26, 2016-05-27 16:20:00 +0300] 215 | # ``` 216 | # 217 | # @param from [Time,Date,DateTime] start of period; 218 | # @param to [Time,Date,DateTime] end of period. 219 | # 220 | # @return [Array] how many full units 221 | # are inside the period; exact value of `from` + full units. 222 | def measure_rem(from, to) 223 | m = measure(from, to) 224 | [m, advance(from, m)] 225 | end 226 | 227 | # Creates {Sequence} instance for producing all time units between 228 | # from and too. See {Sequence} class documentation for detailed functionality description. 229 | # 230 | # @param range [Range] start and end of sequence. 231 | # 232 | # @return [Sequence] 233 | def sequence(range) 234 | TimeMath::Sequence.new(name, range) 235 | end 236 | 237 | # Converts input timestamps list to regular list of timestamps 238 | # over current unit. 239 | # 240 | # Like this: 241 | # 242 | # ```ruby 243 | # times = [Time.parse('2016-05-01'), Time.parse('2016-05-03'), Time.parse('2016-05-08')] 244 | # TimeMath.day.resample(times) 245 | # # => => [2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300, 2016-05-05 00:00:00 +0300, 2016-05-06 00:00:00 +0300, 2016-05-07 00:00:00 +0300, 2016-05-08 00:00:00 +0300] 246 | # ``` 247 | # 248 | # The best way about resampling it also works for hashes with time 249 | # keys. Like this: 250 | # 251 | # ```ruby 252 | # h = {Date.parse('Wed, 01 Jun 2016')=>1, Date.parse('Tue, 07 Jun 2016')=>3, Date.parse('Thu, 09 Jun 2016')=>1} 253 | # # => {#=>1, #=>3, #=>1} 254 | # 255 | # pp TimeMath.day.resample(h) 256 | # # {#=>[1], 257 | # # #=>[], 258 | # # #=>[], 259 | # # #=>[], 260 | # # #=>[], 261 | # # #=>[], 262 | # # #=>[3], 263 | # # #=>[], 264 | # # #=>[1]} 265 | # 266 | # # The default resample just groups all related values in arrays 267 | # # You can pass block or symbol, to have the values you need: 268 | # pp TimeMath.day.resample(h,&:first) 269 | # # {#=>1, 270 | # # #=>nil, 271 | # # #=>nil, 272 | # # #=>nil, 273 | # # #=>nil, 274 | # # #=>nil, 275 | # # #=>3, 276 | # # #=>nil, 277 | # # #=>1} 278 | # ``` 279 | # 280 | # @param array_or_hash array of time-y values (Time/Date/DateTime) 281 | # or hash with time-y keys. 282 | # @param symbol in case of first param being a hash -- method to 283 | # call on key arrays while grouping. 284 | # @param block in case of first param being a hash -- block to 285 | # call on key arrays while grouping. 286 | # 287 | # @return array or hash spread regular by unit; if first param was 288 | # hash, keys corresponding to each period are grouped into arrays; 289 | # this array could be further processed with block/symbol provided. 290 | def resample(array_or_hash, symbol = nil, &block) 291 | Resampler.call(name, array_or_hash, symbol, &block) 292 | end 293 | 294 | def inspect 295 | "#<#{self.class}>" 296 | end 297 | 298 | private 299 | 300 | def index 301 | Util::NATURAL_UNITS.index(name) or 302 | raise NotImplementedError, "Can not be used for #{name}" 303 | end 304 | 305 | def floor_1(tm) 306 | components = Util.tm_to_array(tm).first(index + 1) 307 | Util.array_to_tm(tm, *components) 308 | end 309 | 310 | def float_fix(tm, floored, float_span_part) 311 | if float_span_part.zero? 312 | floored 313 | else 314 | float_floored = advance(floored, float_span_part) 315 | float_floored > tm ? floored : float_floored 316 | end 317 | end 318 | end 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /lib/time_math/units/day.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | module Units 3 | # @private 4 | class Day < Simple 5 | def initialize 6 | super(:day) 7 | end 8 | 9 | protected 10 | 11 | def _advance(tm, steps) 12 | fix_dst(super(tm, steps), tm) 13 | end 14 | 15 | def _decrease(tm, steps) 16 | fix_dst(super(tm, steps), tm) 17 | end 18 | 19 | # :nocov: - somehow Travis env thinks other things about DST 20 | def fix_dst(res, src) 21 | return res unless res.is_a?(Time) 22 | 23 | if res.dst? && !src.dst? 24 | TimeMath.hour.decrease(res) 25 | elsif !res.dst? && src.dst? 26 | TimeMath.hour.advance(res) 27 | else 28 | res 29 | end 30 | end 31 | # :nocov: 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/time_math/units/month.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | module Units 3 | # @private 4 | class Month < Base 5 | def initialize 6 | super(:month) 7 | end 8 | 9 | protected 10 | 11 | def _measure(from, to) 12 | ydiff = to.year - from.year 13 | mdiff = to.month - from.month 14 | 15 | to.day >= from.day ? (ydiff * 12 + mdiff) : (ydiff * 12 + mdiff - 1) 16 | end 17 | 18 | def _advance(tm, steps) 19 | target = tm.month + steps.to_i 20 | m = (target - 1) % 12 + 1 21 | dy = (target - 1) / 12 22 | Util.merge(tm, year: tm.year + dy, month: m) 23 | end 24 | 25 | def _decrease(tm, steps) 26 | _advance(tm, -steps) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/time_math/units/simple.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | module Units 3 | # @private 4 | class Simple < Base 5 | def to_seconds(sz = 1) 6 | sz * MULTIPLIERS[index..-1].inject(:*) 7 | end 8 | 9 | protected 10 | 11 | def _measure(from, to) 12 | to.to_time.-(from.to_time)./(to_seconds).to_i 13 | end 14 | 15 | def _advance(tm, steps) 16 | _shift(tm, to_seconds(steps)) 17 | end 18 | 19 | def _decrease(tm, steps) 20 | _shift(tm, -to_seconds(steps)) 21 | end 22 | 23 | def _shift(tm, seconds) 24 | case tm 25 | when Time 26 | tm + seconds 27 | when Date 28 | tm + Rational(seconds, 86_400) 29 | else 30 | raise ArgumentError, "Expected Time or DateTime, got #{tm.class}" 31 | end 32 | end 33 | 34 | MULTIPLIERS = [12, 30, 24, 60, 60, 1].freeze 35 | end 36 | 37 | # @private 38 | class Sec < Simple 39 | def initialize 40 | super(:sec) 41 | end 42 | end 43 | 44 | # @private 45 | class Min < Simple 46 | def initialize 47 | super(:min) 48 | end 49 | end 50 | 51 | # @private 52 | class Hour < Simple 53 | def initialize 54 | super(:hour) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/time_math/units/week.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | module Units 3 | # @private 4 | class Week < Simple 5 | def initialize 6 | super(:week) 7 | end 8 | 9 | def floor(tm, span = 1) 10 | span == 1 or 11 | raise NotImplementedError, 'For now, week only can floor to one' 12 | 13 | f = TimeMath.day.floor(tm) 14 | extra_days = tm.wday.zero? ? 6 : tm.wday - 1 15 | TimeMath.day.decrease(f, extra_days) 16 | end 17 | 18 | def to_seconds(sz = 1) 19 | TimeMath.day.to_seconds(sz * 7) 20 | end 21 | 22 | protected 23 | 24 | def _advance(tm, steps) 25 | TimeMath.day.advance(tm, steps * 7) 26 | end 27 | 28 | def _decrease(tm, steps) 29 | TimeMath.day.decrease(tm, steps * 7) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/time_math/units/year.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | module Units 3 | # @private 4 | class Year < Base 5 | def initialize 6 | super(:year) 7 | end 8 | 9 | protected 10 | 11 | def _measure(from, to) 12 | if Util.merge(from, year: to.year) <= to 13 | to.year - from.year 14 | else 15 | to.year - from.year - 1 16 | end 17 | end 18 | 19 | def _advance(tm, steps) 20 | Util.merge(tm, year: tm.year + steps.to_i) 21 | end 22 | 23 | def _decrease(tm, steps) 24 | Util.merge(tm, year: tm.year - steps.to_i) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/time_math/util.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | # @private 3 | module Util 4 | COMMON_UNITS = %i[year month day hour min sec].freeze 5 | NATURAL_UNITS = [*COMMON_UNITS, :subsec].freeze 6 | EMPTY_VALUES = [nil, 1, 1, 0, 0, 0].freeze 7 | 8 | module_function 9 | 10 | def timey?(val) 11 | [Time, DateTime, Date].include?(val.class) 12 | end 13 | 14 | def merge(tm, **attrs) 15 | hash_to_tm(tm, tm_to_hash(tm).merge(attrs)) 16 | end 17 | 18 | def array_to_tm(origin, *components) 19 | components = EMPTY_VALUES.zip(components).map { |d, c| c || d } 20 | fix_month_day(components) 21 | 22 | case origin 23 | when Time 24 | Time.new(*components, origin.utc_offset) 25 | when DateTime 26 | DateTime.new(*components, origin.zone) 27 | when Date 28 | Date.new(*components.first(3)) 29 | else 30 | raise ArgumentError, "Expected Time, Date or DateTime, got #{origin.class}" 31 | end 32 | end 33 | 34 | def tm_to_array(tm) 35 | case tm 36 | when Time, DateTime 37 | [tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec] 38 | when Date 39 | [tm.year, tm.month, tm.day] 40 | else 41 | raise ArgumentError, "Expected Time, Date or DateTime, got #{tm.class}" 42 | end 43 | end 44 | 45 | def tm_to_hash(tm) 46 | NATURAL_UNITS.map { |s| [s, extract_component(tm, s)] }.to_h 47 | end 48 | 49 | def hash_to_tm(origin, hash) 50 | components = NATURAL_UNITS[0..-2].map { |s| hash[s] || 0 } 51 | components[-1] += (hash[:subsec] || hash[:sec_fraction] || 0) 52 | array_to_tm(origin, *components) 53 | end 54 | 55 | DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze 56 | 57 | def fix_month_day(components) 58 | return if components[2].nil? || components[1].nil? 59 | 60 | days_in_month = 61 | if components[1] == 2 && components[0] && Date.gregorian_leap?(components[0]) 62 | 29 63 | else 64 | DAYS_IN_MONTH[components[1]] 65 | end 66 | components[2] = [components[2], days_in_month].min 67 | end 68 | 69 | def extract_component(tm, component) 70 | case component 71 | when :subsec, :sec_fraction 72 | subsec(tm) 73 | when *COMMON_UNITS 74 | tm.send(component) 75 | end 76 | end 77 | 78 | def subsec(tm) 79 | case tm 80 | when Time 81 | tm.subsec 82 | when Date 83 | 0 84 | when DateTime 85 | tm.send(:sec_fraction) 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/time_math/version.rb: -------------------------------------------------------------------------------- 1 | module TimeMath 2 | # @private 3 | VERSION = '0.1.1'.freeze 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/advance.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-03-27 11:40:20 +03' 2 | :targets: 3 | :sec: '2015-03-27 11:40:21 +03' 4 | :min: '2015-03-27 11:41:20 +03' 5 | :hour: '2015-03-27 12:40:20 +03' 6 | :day: '2015-03-28 11:40:20 +03' 7 | :week: '2015-04-03 11:40:20 +03' 8 | :month: '2015-04-27 11:40:20 +03' 9 | :year: '2016-03-27 11:40:20 +03' 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/ceil.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-03-28 11:40:20 +03' 2 | :targets: 3 | :sec: '2015-03-28 11:40:20 +03' 4 | :min: '2015-03-28 11:41:00 +03' 5 | :hour: '2015-03-28 12:00:00 +03' 6 | :day: '2015-03-29 00:00:00 +03' 7 | # :week:'2015-03-23 00:00:00 +03' 8 | :month: '2015-04-01 00:00:00 +03' 9 | :year: '2016-01-01 00:00:00 +03' 10 | -------------------------------------------------------------------------------- /spec/fixtures/ceil_3.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-04-28 11:40:20 +03' 2 | :targets: 3 | :sec: '2015-04-28 11:40:21 +03' 4 | :min: '2015-04-28 11:42:00 +03' 5 | :hour: '2015-04-28 12:00:00 +03' 6 | :day: '2015-04-30 00:00:00 +03' 7 | # :week:'2015-04-23 00:00:00 +03' 8 | :month: '2015-06-01 00:00:00 +03' 9 | :year: '2016-01-01 00:00:00 +03' 10 | -------------------------------------------------------------------------------- /spec/fixtures/decrease.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-03-27 11:40:20' 2 | :targets: 3 | :sec: '2015-03-27 11:40:19' 4 | :min: '2015-03-27 11:39:20' 5 | :hour: '2015-03-27 10:40:20' 6 | :day: '2015-03-26 11:40:20' 7 | :week: '2015-03-20 11:40:20' 8 | :month: '2015-02-27 11:40:20' 9 | :year: '2014-03-27 11:40:20' 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/floor.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-03-28 11:40:20.123 +03' 2 | :targets: 3 | :sec: '2015-03-28 11:40:20 +03' 4 | :min: '2015-03-28 11:40:00 +03' 5 | :hour: '2015-03-28 11:00:00 +03' 6 | :day: '2015-03-28 00:00:00 +03' 7 | :week: '2015-03-23 00:00:00 +03' 8 | :month: '2015-03-01 00:00:00 +03' 9 | :year: '2015-01-01 00:00:00 +03' 10 | -------------------------------------------------------------------------------- /spec/fixtures/floor_3.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-04-28 11:40:20 +03' 2 | :targets: 3 | :sec: '2015-04-28 11:40:18 +03' 4 | :min: '2015-04-28 11:39:00 +03' 5 | :hour: '2015-04-28 09:00:00 +03' 6 | :day: '2015-04-27 00:00:00 +03' 7 | # :week:'2015-04-23 00:00:00 +03' 8 | :month: '2015-03-01 00:00:00 +03' 9 | :year: '2013-01-01 00:00:00 +03' 10 | -------------------------------------------------------------------------------- /spec/fixtures/floor_half.yml: -------------------------------------------------------------------------------- 1 | :source: '2015-04-28 18:40:45' 2 | :targets: 3 | :min: '2015-04-28 18:40:30' 4 | :hour: '2015-04-28 18:30:00' 5 | :day: '2015-04-28 12:00:00' 6 | -------------------------------------------------------------------------------- /spec/fixtures/measure.yml: -------------------------------------------------------------------------------- 1 | # sec examples 2 | - :unit: :sec 3 | :from: '2015-03-10 11:40:35 +03' 4 | :to: '2015-03-10 11:43:03 +03' 5 | :val: 148 6 | 7 | # min examples 8 | - :unit: :min 9 | :from: '2015-03-10 11:40:35 +03' 10 | :to: '2015-03-10 11:43:03 +03' 11 | :val: 2 12 | 13 | # hour examples 14 | - :unit: :hour 15 | :from: '2015-03-10 11:40 +03' 16 | :to: '2015-03-10 11:50 +03' 17 | :val: 0 18 | - :unit: :hour 19 | :from: '2015-03-10 11:40 +03' 20 | :to: '2015-03-10 15:50 +03' 21 | :val: 4 22 | - :unit: :hour 23 | :from: '2015-03-10 11:40 +03' 24 | :to: '2015-03-10 15:20 +03' 25 | :val: 3 26 | 27 | # day examples 28 | - :unit: :day 29 | :from: '2015-03-10 11:40 +03' 30 | :to: '2015-03-20 15:50 +03' 31 | :val: 10 32 | 33 | # week examples 34 | # Mystically broken in some timezones... TODO 35 | # - :unit: :week 36 | # :from: '2015-03-10 11:40 +03' 37 | # :to: '2015-04-14 8:10 +03' 38 | # :val: 4 39 | 40 | # month examples 41 | - :unit: :month 42 | :from: '2015-03-10 11:40 +03' 43 | :to: '2015-03-20 15:50 +03' 44 | :val: 0 45 | - :unit: :month 46 | :from: '2015-03-10 11:40 +03' 47 | :to: '2015-04-20 15:50 +03' 48 | :val: 1 49 | # february 50 | - :unit: :month 51 | :from: '2015-02-28 11:40 +03' 52 | :to: '2015-03-28 15:50 +03' 53 | :val: 1 54 | # several years 55 | - :unit: :month 56 | :from: '2013-02-28 11:40 +03' 57 | :to: '2015-03-28 15:50 +03' 58 | :val: 25 59 | 60 | # year example 61 | - :unit: :year 62 | :from: '2015-03-10 11:40 +03' 63 | :to: '2015-03-20 15:50 +03' 64 | :val: 0 65 | - :unit: :year 66 | :from: '2012-03-10 11:40 +03' 67 | :to: '2015-03-20 15:50 +03' 68 | :val: 3 69 | - :unit: :year 70 | :from: '2012-03-10 11:40 +03' 71 | :to: '2015-02-20 15:50 +03' 72 | :val: 2 73 | - :unit: :year 74 | :from: '2012-03-10 11:40 +03' 75 | :to: '2015-03-09 15:50 +03' 76 | :val: 2 77 | - :unit: :year 78 | :from: '2012-03-10 11:40 +03' 79 | :to: '2015-03-10 10:50 +03' 80 | :val: 2 81 | -------------------------------------------------------------------------------- /spec/fixtures/resample.yml: -------------------------------------------------------------------------------- 1 | - :unit: :day 2 | :source: 3 | - '2016-05-01 12:30' 4 | - '2016-05-02 14:20' 5 | - '2016-05-04 15:00' 6 | - '2016-05-04 16:00' 7 | - '2016-05-04 17:00' 8 | - '2016-05-06 00:00' 9 | - '2016-05-09 10:00' 10 | :target: 11 | - '2016-05-01' 12 | - '2016-05-02' 13 | - '2016-05-03' 14 | - '2016-05-04' 15 | - '2016-05-05' 16 | - '2016-05-06' 17 | - '2016-05-07' 18 | - '2016-05-08' 19 | - '2016-05-09' 20 | 21 | - :unit: :week 22 | :source: 23 | - '2016-05-01 12:30' 24 | - '2016-05-02 14:20' 25 | - '2016-05-04 15:00' 26 | - '2016-05-04 16:00' 27 | - '2016-05-04 17:00' 28 | - '2016-05-06 00:00' 29 | - '2016-05-09 10:00' 30 | :target: 31 | - '2016-04-25' 32 | - '2016-05-02' 33 | - '2016-05-09' 34 | -------------------------------------------------------------------------------- /spec/fixtures/round.yml: -------------------------------------------------------------------------------- 1 | :sec: 2 | :true: '2015-03-28 11:40:20' 3 | :false: '2015-03-28 11:40:20.123' 4 | :min: 5 | :true: '2015-03-28 11:40:00' 6 | :false: '2015-03-28 11:40:20' 7 | :hour: 8 | :true: '2015-03-28 11:00:00' 9 | :false: '2015-03-28 11:40:00' 10 | :day: 11 | :true: '2015-03-28 00:00:00' 12 | :false: '2015-03-28 11:00:00' 13 | :week: 14 | :true: '2015-03-23 00:00:00' 15 | :false: '2015-03-24 00:00:00' 16 | :month: 17 | :true: '2015-03-01 00:00:00' 18 | :false: '2015-03-28 00:00:00' 19 | :year: 20 | :true: '2015-01-01 00:00:00' 21 | :false: '2015-03-01 00:00:00' 22 | 23 | -------------------------------------------------------------------------------- /spec/fixtures/sequence_pairs.yml: -------------------------------------------------------------------------------- 1 | :from: '2014-03-10 11:00 +03' 2 | :to: '2014-11-05 12:30 +03' 3 | :step: :month 4 | :sequence: 5 | - ['2014-03-01 00:00 +03', '2014-04-01 00:00 +03'] 6 | - ['2014-04-01 00:00 +03', '2014-05-01 00:00 +03'] 7 | - ['2014-05-01 00:00 +03', '2014-06-01 00:00 +03'] 8 | - ['2014-06-01 00:00 +03', '2014-07-01 00:00 +03'] 9 | - ['2014-07-01 00:00 +03', '2014-08-01 00:00 +03'] 10 | - ['2014-08-01 00:00 +03', '2014-09-01 00:00 +03'] 11 | - ['2014-09-01 00:00 +03', '2014-10-01 00:00 +03'] 12 | - ['2014-10-01 00:00 +03', '2014-11-01 00:00 +03'] 13 | -------------------------------------------------------------------------------- /spec/fixtures/sequence_to_a.yml: -------------------------------------------------------------------------------- 1 | :from: '2014-03-10 11:00 +03' 2 | :to: '2014-11-05 12:30 +03' 3 | :step: :month 4 | :sequence: 5 | - '2014-03-01 00:00 +03' 6 | - '2014-04-01 00:00 +03' 7 | - '2014-05-01 00:00 +03' 8 | - '2014-06-01 00:00 +03' 9 | - '2014-07-01 00:00 +03' 10 | - '2014-08-01 00:00 +03' 11 | - '2014-09-01 00:00 +03' 12 | - '2014-10-01 00:00 +03' 13 | :sequence_include_end: 14 | - '2014-03-01 00:00 +03' 15 | - '2014-04-01 00:00 +03' 16 | - '2014-05-01 00:00 +03' 17 | - '2014-06-01 00:00 +03' 18 | - '2014-07-01 00:00 +03' 19 | - '2014-08-01 00:00 +03' 20 | - '2014-09-01 00:00 +03' 21 | - '2014-10-01 00:00 +03' 22 | - '2014-11-01 00:00 +03' 23 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | require 'rspec/its' 4 | require 'yaml' 5 | 6 | Coveralls.wear! 7 | 8 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( 9 | [SimpleCov::Formatter::HTMLFormatter, 10 | Coveralls::SimpleCov::Formatter] 11 | ) 12 | 13 | SimpleCov.start do 14 | add_filter 'spec' 15 | # minimum_coverage_by_file 95 -- coveralls & JRuby doesn't play really well together 16 | end 17 | 18 | $LOAD_PATH.unshift 'lib' 19 | require 'time_math' 20 | 21 | def load_fixture(name, time_class = nil) # rubocop:disable Metrics/AbcSize 22 | res = YAML.load(File.read("spec/fixtures/#{name}.yml")) 23 | return res if time_class != Date 24 | 25 | if res.is_a?(Hash) && res.keys.include?(:sec) 26 | res = limit_units(res, time_class) 27 | elsif res.is_a?(Hash) && res.keys.include?(:targets) 28 | res[:targets] = limit_units(res[:targets], time_class) 29 | elsif res.is_a?(Array) && res.first.is_a?(Hash) && res.first.key?(:unit) 30 | res = limit_units(res, time_class) 31 | end 32 | res 33 | end 34 | 35 | NON_DATE_STEPS = %i[hour min sec].freeze 36 | 37 | def limit_units(values, time_class) 38 | return values unless time_class == Date 39 | case values 40 | when Array 41 | if values.all? { |v| v.is_a?(Symbol) } 42 | values - NON_DATE_STEPS 43 | elsif values.all? { |v| v.is_a?(Hash) } 44 | values.reject { |v| NON_DATE_STEPS.include?(v[:unit]) } 45 | else 46 | values 47 | end 48 | when Hash 49 | values.reject { |k, _| NON_DATE_STEPS.include?(k) } 50 | else 51 | raise ArgumentError, "Can't limit steps for #{values}" 52 | end 53 | end 54 | 55 | # Ruby prior to 2.2 couldn't parse offsets in time 56 | def Time.parse(str) 57 | dt = DateTime.parse(str) 58 | Time.new(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec + dt.sec_fraction, 3600 * 24 * dt.offset) 59 | end 60 | -------------------------------------------------------------------------------- /spec/time_math/measure_spec.rb: -------------------------------------------------------------------------------- 1 | describe TimeMath::Measure do 2 | [Time, DateTime].each do |t| 3 | describe "with #{t}" do 4 | let(:from) { t.parse('2013-03-01 14:40:53') } 5 | let(:to) { t.parse('2015-02-25 10:18:47') } 6 | let(:options) { {} } 7 | 8 | subject { described_class.measure(from, to, options) } 9 | 10 | context 'when long period' do 11 | it { is_expected.to eq(years: 1, months: 11, weeks: 3, days: 2, hours: 19, minutes: 37, seconds: 54) } 12 | 13 | context ':upto limit' do 14 | let(:options) { {upto: :day} } 15 | 16 | it { is_expected.to eq(days: 725, hours: 19, minutes: 37, seconds: 54) } 17 | end 18 | 19 | context 'weeks: false' do 20 | let(:options) { {weeks: false} } 21 | 22 | it { is_expected.to eq(years: 1, months: 11, days: 23, hours: 19, minutes: 37, seconds: 54) } 23 | end 24 | end 25 | 26 | context 'zero difference' do 27 | subject { described_class.measure(from, from, options) } 28 | 29 | it { is_expected.to eq(years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0) } 30 | end 31 | 32 | context 'backwards' do 33 | subject { described_class.measure(to, from, options) } 34 | 35 | it { is_expected.to eq(years: -1, months: -11, weeks: -3, days: -2, hours: -19, minutes: -37, seconds: -54) } 36 | end 37 | 38 | context 'when short period', pending: true do 39 | context 'show_empty: false' do 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/time_math/op_spec.rb: -------------------------------------------------------------------------------- 1 | describe TimeMath::Op do 2 | describe 'default' do 3 | subject(:op) { 4 | described_class.new.floor(:month).advance(:day, 3).decrease(:min, 20) 5 | } 6 | 7 | context 'validation' do 8 | it 'fails on unknown units' do 9 | expect { op.decrease(:ages, 2) }.to raise_error(ArgumentError, /ages/) 10 | end 11 | end 12 | 13 | context '#operations' do 14 | its(:operations) { 15 | is_expected.to eq([ 16 | [:floor, :month, []], 17 | [:advance, :day, [3]], 18 | [:decrease, :min, [20]] 19 | ]) 20 | } 21 | 22 | context 'bang' do 23 | subject!(:ceiled) { op.ceil!(:hour) } 24 | its(:operations) { is_expected.to include([:ceil, :hour, []]) } 25 | it { expect(op.operations).to include([:ceil, :hour, []]) } 26 | end 27 | 28 | context 'non-bang' do 29 | subject!(:ceiled) { op.ceil(:hour) } 30 | its(:operations) { is_expected.to include([:ceil, :hour, []]) } 31 | it { expect(op.operations).not_to include([:ceil, :hour, []]) } 32 | end 33 | end 34 | 35 | context '#==' do 36 | let(:tm) { Time.parse('2016-05-14 13:40') } 37 | 38 | it 'is equal when is equal' do 39 | expect(described_class.new.floor(:month).advance(:day, 3)) 40 | .to eq described_class.new.floor(:month).advance(:day, 3) 41 | 42 | expect(described_class.new(tm).floor(:month).advance(:day, 3)) 43 | .to eq described_class.new(tm).floor(:month).advance(:day, 3) 44 | end 45 | 46 | it 'is not equal on different ops or params' do 47 | expect(described_class.new.floor(:month).advance(:day, 3)) 48 | .not_to eq described_class.new.ceil(:month).advance(:day, 3) 49 | 50 | expect(described_class.new.floor(:month).advance(:day, 3)) 51 | .not_to eq described_class.new.floor(:month).advance(:day, 2) 52 | end 53 | 54 | it 'considers op order' do 55 | expect(described_class.new.floor(:month).advance(:day, 3)) 56 | .not_to eq described_class.new.advance(:day, 3).floor(:month) 57 | end 58 | 59 | it 'considers arguments' do 60 | expect(described_class.new.floor(:month).advance(:day, 3)) 61 | .not_to eq described_class.new(tm).floor(:month).advance(:day, 3) 62 | end 63 | end 64 | 65 | context '#call' do 66 | context 'single argument' do 67 | subject { op.call(Time.parse('2016-05-14 13:40')) } 68 | 69 | it { is_expected.to eq Time.parse('2016-05-03 23:40') } 70 | end 71 | 72 | context 'multiple arguments' do 73 | subject { op.call(Time.parse('2016-05-14 13:40'), Time.parse('2016-07-02 12:10')) } 74 | 75 | it { is_expected.to eq [Time.parse('2016-05-03 23:40'), Time.parse('2016-07-03 23:40')] } 76 | end 77 | 78 | context 'array of arguments' do 79 | subject { op.call([Time.parse('2016-05-14 13:40'), Time.parse('2016-07-02 12:10')]) } 80 | 81 | it { is_expected.to eq [Time.parse('2016-05-03 23:40'), Time.parse('2016-07-03 23:40')] } 82 | end 83 | 84 | context 'no-op' do 85 | subject { described_class.new.call(Time.parse('2016-05-14 13:40')) } 86 | 87 | it { is_expected.to eq Time.parse('2016-05-14 13:40') } 88 | end 89 | 90 | context 'pre-set arguments' do 91 | subject(:op) { 92 | described_class.new(Time.parse('2016-05-14 13:40')) 93 | .floor(:month).advance(:day, 3).decrease(:min, 20) 94 | } 95 | 96 | context 'without args' do 97 | subject { op.call } 98 | 99 | it { is_expected.to eq Time.parse('2016-05-03 23:40') } 100 | end 101 | 102 | it 'fails without args' do 103 | expect { op.call(Time.parse('2016-05-14 13:40')) } 104 | .to raise_error(ArgumentError, /already/) 105 | end 106 | end 107 | end 108 | 109 | context '#to_proc' do 110 | let(:tm1) { Time.parse('2016-05-14 13:40') } 111 | let(:tm2) { Time.parse('2016-06-14 13:40') } 112 | 113 | subject { [tm1, tm2].map(&op) } 114 | 115 | it { is_expected.to eq [op.call(tm1), op.call(tm2)] } 116 | end 117 | 118 | context '#inspect' do 119 | its(:inspect) { is_expected.to eq '#' } 120 | 121 | context 'with preset arg' do 122 | let(:tm1) { Time.parse('2016-05-14 13:40') } 123 | let(:tm2) { Time.parse('2016-06-14 13:40') } 124 | 125 | context 'one arg' do 126 | subject(:op) { 127 | described_class.new(tm1) 128 | .floor(:month).advance(:day, 3).decrease(:min, 20) 129 | } 130 | 131 | its(:inspect) { is_expected.to eq "#" } 132 | end 133 | 134 | context 'multiple arg' do 135 | subject(:op) { 136 | described_class.new(tm1, tm2) 137 | .floor(:month).advance(:day, 3).decrease(:min, 20) 138 | } 139 | 140 | its(:inspect) { is_expected.to eq "#" } 141 | end 142 | 143 | context 'array of args' do 144 | let(:tm) { Time.parse('2016-05-14 13:40') } 145 | 146 | subject(:op) { 147 | described_class.new([tm1, tm2]) 148 | .floor(:month).advance(:day, 3).decrease(:min, 20) 149 | } 150 | 151 | its(:inspect) { is_expected.to eq "#" } 152 | end 153 | end 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /spec/time_math/resamplers_spec.rb: -------------------------------------------------------------------------------- 1 | describe TimeMath::ArrayResampler do 2 | fixture = load_fixture(:resample) 3 | fixture.each do |f| 4 | context "with #{f[:unit]}" do 5 | let(:source) { f[:source].map(&Time.method(:parse)) } 6 | let(:target) { f[:target].map(&Time.method(:parse)) } 7 | 8 | subject { described_class.new(f[:unit], source).call } 9 | 10 | it { is_expected.to eq target } 11 | end 12 | end 13 | end 14 | 15 | describe TimeMath::HashResampler do 16 | let(:hash) { 17 | { 18 | Date.parse('Wed, 01 Jun 2016') => 1, 19 | Date.parse('Tue, 07 Jun 2016') => 3, 20 | Date.parse('Thu, 09 Jun 2016') => 1 21 | } 22 | } 23 | 24 | let(:resampler) { described_class.new(unit, hash) } 25 | 26 | context 'without block' do 27 | subject { resampler.call } 28 | 29 | context 'one in group' do 30 | let(:unit) { :day } 31 | 32 | it { 33 | is_expected.to eq( 34 | Date.parse('Wed, 01 Jun 2016') => [1], 35 | Date.parse('Wed, 02 Jun 2016') => [], 36 | Date.parse('Wed, 03 Jun 2016') => [], 37 | Date.parse('Wed, 04 Jun 2016') => [], 38 | Date.parse('Wed, 05 Jun 2016') => [], 39 | Date.parse('Wed, 06 Jun 2016') => [], 40 | Date.parse('Tue, 07 Jun 2016') => [3], 41 | Date.parse('Wed, 08 Jun 2016') => [], 42 | Date.parse('Thu, 09 Jun 2016') => [1] 43 | ) 44 | } 45 | end 46 | 47 | context 'several in group' do 48 | let(:unit) { :week } 49 | 50 | it { 51 | is_expected.to eq( 52 | Date.parse('Wed, 30 May 2016') => [1], 53 | Date.parse('Wed, 06 Jun 2016') => [3, 1] 54 | ) 55 | } 56 | end 57 | end 58 | 59 | context 'with block' do 60 | subject { resampler.call { |v| v.inject(:+) } } 61 | 62 | context 'one in group' do 63 | let(:unit) { :day } 64 | 65 | it { 66 | is_expected.to eq( 67 | Date.parse('Wed, 01 Jun 2016') => 1, 68 | Date.parse('Wed, 02 Jun 2016') => nil, 69 | Date.parse('Wed, 03 Jun 2016') => nil, 70 | Date.parse('Wed, 04 Jun 2016') => nil, 71 | Date.parse('Wed, 05 Jun 2016') => nil, 72 | Date.parse('Wed, 06 Jun 2016') => nil, 73 | Date.parse('Tue, 07 Jun 2016') => 3, 74 | Date.parse('Wed, 08 Jun 2016') => nil, 75 | Date.parse('Thu, 09 Jun 2016') => 1 76 | ) 77 | } 78 | end 79 | 80 | context 'several in group' do 81 | let(:unit) { :week } 82 | 83 | it { 84 | is_expected.to eq( 85 | Date.parse('Wed, 30 May 2016') => 1, 86 | Date.parse('Wed, 06 Jun 2016') => 4 87 | ) 88 | } 89 | end 90 | end 91 | 92 | context 'with symbol' do 93 | subject { resampler.call(:first) } 94 | 95 | context 'one in group' do 96 | let(:unit) { :day } 97 | 98 | it { 99 | is_expected.to eq( 100 | Date.parse('Wed, 01 Jun 2016') => 1, 101 | Date.parse('Wed, 02 Jun 2016') => nil, 102 | Date.parse('Wed, 03 Jun 2016') => nil, 103 | Date.parse('Wed, 04 Jun 2016') => nil, 104 | Date.parse('Wed, 05 Jun 2016') => nil, 105 | Date.parse('Wed, 06 Jun 2016') => nil, 106 | Date.parse('Tue, 07 Jun 2016') => 3, 107 | Date.parse('Wed, 08 Jun 2016') => nil, 108 | Date.parse('Thu, 09 Jun 2016') => 1 109 | ) 110 | } 111 | end 112 | 113 | context 'several in group' do 114 | let(:unit) { :week } 115 | 116 | it { 117 | is_expected.to eq( 118 | Date.parse('Wed, 30 May 2016') => 1, 119 | Date.parse('Wed, 06 Jun 2016') => 3 120 | ) 121 | } 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/time_math/sequence_spec.rb: -------------------------------------------------------------------------------- 1 | describe TimeMath::Sequence do 2 | [Time, Date, DateTime].each do |t| 3 | describe "with #{t}" do 4 | let(:from) { t.parse('2014-03-10') } 5 | let(:floor_from) { TimeMath.month.floor(from) } 6 | let(:to) { t.parse('2014-11-10') } 7 | let(:floor_to) { TimeMath.month.decrease(TimeMath.month.floor(to)) } 8 | let(:ceil_to) { TimeMath.month.floor(to) } 9 | 10 | subject(:sequence) { described_class.new(:month, from...to) } 11 | 12 | describe 'creation' do 13 | context 'exclude end' do 14 | its(:from) { is_expected.to eq floor_from } 15 | its(:to) { is_expected.to eq floor_to } 16 | end 17 | 18 | context 'include end' do 19 | subject(:sequence) { described_class.new(:month, from..to) } 20 | 21 | its(:from) { is_expected.to eq floor_from } 22 | its(:to) { is_expected.to eq ceil_to } 23 | end 24 | end 25 | 26 | describe '#inspect' do 27 | its(:inspect) { is_expected.to eq "#" } 28 | end 29 | 30 | describe '#==' do 31 | it 'works' do 32 | expect(sequence).to eq described_class.new(:month, from...to) 33 | expect(sequence).not_to eq described_class.new(:day, from...to) 34 | expect(sequence).not_to eq described_class.new(:month, from...TimeMath.month.advance(to)) 35 | expect(sequence.advance(:min)).not_to eq sequence 36 | end 37 | end 38 | 39 | describe 'operations' do 40 | subject(:seq) { sequence.advance(:hour, 2).decrease(:sec, 3).floor(:min) } 41 | 42 | it { is_expected.to be_a described_class } 43 | its(:methods) { is_expected.to include(*TimeMath::Op::OPERATIONS) } 44 | its(:op) { is_expected.to eq TimeMath::Op.new.advance(:hour, 2).decrease(:sec, 3).floor(:min) } 45 | its(:inspect) { is_expected.to eq "#" } 46 | 47 | context 'bang' do 48 | subject!(:ceiled) { seq.ceil!(:hour) } 49 | its(:op) { is_expected.to eq TimeMath().advance(:hour, 2).decrease(:sec, 3).floor(:min).ceil(:hour) } 50 | it { expect(seq.op).to eq TimeMath().advance(:hour, 2).decrease(:sec, 3).floor(:min).ceil(:hour) } 51 | end 52 | 53 | context 'non-bang' do 54 | subject!(:ceiled) { seq.ceil(:hour) } 55 | its(:op) { is_expected.to eq TimeMath().advance(:hour, 2).decrease(:sec, 3).floor(:min).ceil(:hour) } 56 | it { expect(seq.op).to eq TimeMath().advance(:hour, 2).decrease(:sec, 3).floor(:min) } 57 | end 58 | end 59 | 60 | describe '#to_a' do 61 | let(:fixture) { load_fixture(:sequence_to_a) } 62 | 63 | let(:from) { t.parse(fixture[:from]) } 64 | let(:to) { t.parse(fixture[:to]) } 65 | 66 | let(:sequence) { described_class.new(fixture[:step], from...to) } 67 | 68 | let(:expected) { fixture[:sequence].map(&t.method(:parse)) } 69 | 70 | subject { sequence.to_a } 71 | 72 | it { is_expected.to eq expected } 73 | 74 | context 'when include end' do 75 | let(:sequence) { described_class.new(fixture[:step], from..to) } 76 | let(:expected) { fixture[:sequence_include_end].map(&t.method(:parse)) } 77 | 78 | it { is_expected.to eq expected } 79 | end 80 | 81 | context 'with operations' do 82 | let(:sequence) { 83 | described_class 84 | .new(fixture[:step], from...to) 85 | .advance(:hour, 2).decrease(:sec, 3).floor(:min) 86 | } 87 | let(:op) { TimeMath().advance(:hour, 2).decrease(:sec, 3).floor(:min) } 88 | 89 | let(:expected) { fixture[:sequence].map(&t.method(:parse)).map(&op) } 90 | 91 | it { is_expected.to eq expected } 92 | end 93 | end 94 | 95 | describe '#pairs' do 96 | let(:fixture) { load_fixture(:sequence_pairs) } 97 | let(:from) { t.parse(fixture[:from]) } 98 | let(:to) { t.parse(fixture[:to]) } 99 | 100 | let(:lace) { described_class.new(fixture[:step], from, to, options) } 101 | 102 | let(:expected) { fixture[:sequence].map { |b, e| [t.parse(b), t.parse(e)] } } 103 | 104 | subject { sequence.pairs } 105 | 106 | it { is_expected.to eq expected } 107 | 108 | describe '#ranges' do 109 | subject { sequence.ranges } 110 | 111 | let(:expected) { 112 | fixture[:sequence].map { |b, e| (t.parse(b)...t.parse(e)) } 113 | } 114 | 115 | it { is_expected.to eq expected } 116 | end 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/time_math/time_math_spec.rb: -------------------------------------------------------------------------------- 1 | describe TimeMath do 2 | subject { described_class } 3 | 4 | describe 'basics' do 5 | its(:units) { is_expected.to eq %i[sec min hour day week month year] } 6 | end 7 | 8 | describe '#' do 9 | described_class.units.each do |u| 10 | its(u) { is_expected.to eq(TimeMath::Units.get(u)) } 11 | end 12 | end 13 | 14 | describe '#[]' do 15 | described_class.units.each do |u| 16 | its([u]) { is_expected.to eq(TimeMath::Units.get(u)) } 17 | end 18 | 19 | it { expect { described_class[:age] }.to raise_error ArgumentError, /age/ } 20 | end 21 | 22 | describe '#()' do 23 | let(:tm) { Time.parse('2013-03-01 14:40:53') } 24 | 25 | it { expect(TimeMath()).to eq TimeMath::Op.new } 26 | it { expect(TimeMath(tm)).to eq TimeMath::Op.new(tm) } 27 | end 28 | 29 | describe '#measure' do 30 | let(:from) { Time.parse('2013-03-01 14:40:53') } 31 | let(:to) { Time.parse('2015-02-25 10:18:47') } 32 | 33 | it 'delegates' do 34 | expect(described_class.measure(from, to)).to eq TimeMath::Measure.measure(from, to) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/time_math/units/base_spec.rb: -------------------------------------------------------------------------------- 1 | describe TimeMath::Units::Base do 2 | def u(name) 3 | TimeMath::Units.get(name) 4 | end 5 | 6 | [Time, DateTime, Date].each do |t| 7 | describe "math with #{t}" do 8 | describe '#floor' do 9 | context 'default' do 10 | fixture = load_fixture(:floor, t) 11 | 12 | let(:source) { t.parse(fixture[:source]) } 13 | 14 | fixture[:targets].each do |step, val| 15 | it "rounds down to #{step}" do 16 | expect(u(step).floor(source)).to eq t.parse(val) 17 | end 18 | end 19 | end 20 | 21 | context '#floor(3)' do 22 | fixture = load_fixture(:floor_3, t) 23 | 24 | let(:source) { t.parse(fixture[:source]) } 25 | 26 | fixture[:targets].each do |step, val| 27 | it "rounds down to #{step}" do 28 | expect(u(step).floor(source, 3)).to eq t.parse(val) 29 | end 30 | end 31 | end 32 | 33 | context '#floor(1/2)' do 34 | fixture = load_fixture(:floor_half, t) 35 | 36 | let(:source) { t.parse(fixture[:source]) } 37 | 38 | fixture[:targets].each do |step, val| 39 | it "rounds down to #{step}" do 40 | expect(u(step).floor(source, Rational(1, 2))).to eq t.parse(val) 41 | end 42 | end 43 | 44 | specify do 45 | unless t == Date 46 | expect(TimeMath.day.floor(t.parse('2016-06-22 11:14'), Rational(1, 2))) 47 | .to eq t.parse('2016-06-22 00:00') 48 | expect(TimeMath.day.floor(t.parse('2016-06-22 12:00'), Rational(1, 2))) 49 | .to eq t.parse('2016-06-22 12:00') 50 | end 51 | end 52 | end 53 | end 54 | 55 | describe '#ceil' do 56 | context 'default' do 57 | fixture = load_fixture(:ceil, t) 58 | 59 | let(:source) { t.parse(fixture[:source]) } 60 | 61 | fixture[:targets].each do |step, val| 62 | it "rounds up to #{step}" do 63 | next if step == :day && t == Date 64 | expect(u(step).ceil(source)).to eq t.parse(val) 65 | end 66 | end 67 | end 68 | 69 | context '#ceil(3)' do 70 | fixture = load_fixture(:ceil_3, t) 71 | 72 | let(:source) { t.parse(fixture[:source]) } 73 | 74 | fixture[:targets].each do |step, val| 75 | it "rounds up to #{step}" do 76 | expect(u(step).ceil(source, 3)).to eq t.parse(val) 77 | end 78 | end 79 | end 80 | 81 | context '#ceil(1/2)' do 82 | specify do 83 | expect(TimeMath.day.ceil(t.parse('2016-06-22 11:14'), Rational(1, 2))) 84 | .to eq t.parse('2016-06-22 12:00') 85 | end 86 | end 87 | end 88 | 89 | describe '#round' do 90 | context 'default' do 91 | let(:ceiled) { t.parse('2015-03-01 12:32') } 92 | let(:floored) { t.parse('2015-03-01 12:22') } 93 | let(:edge) { t.parse('2015-03-01 12:30') } 94 | 95 | let(:unit) { u(:hour) } 96 | 97 | it 'smartly rounds to ceil or floor' do 98 | expect(unit.round(ceiled)).to eq unit.ceil(ceiled) 99 | expect(unit.round(floored)).to eq unit.floor(floored) 100 | expect(unit.round(edge)).to eq unit.ceil(edge) 101 | end 102 | end 103 | 104 | context '#round(3)' do 105 | let(:ceiled) { t.parse('2015-03-01 14:32') } 106 | let(:floored) { t.parse('2015-03-01 12:22') } 107 | let(:edge) { t.parse('2015-03-01 13:30') } 108 | 109 | let(:unit) { u(:hour) } 110 | 111 | it 'smartly rounds to ceil or floor' do 112 | expect(unit.round(ceiled, 3)).to eq unit.ceil(ceiled, 3) 113 | expect(unit.round(floored, 3)).to eq unit.floor(floored, 3) 114 | expect(unit.round(edge, 3)).to eq unit.ceil(edge, 3) 115 | end 116 | end 117 | end 118 | 119 | describe '#prev' do 120 | let(:floored) { t.parse('2015-03-05') } 121 | let(:decreased) { t.parse('2015-03-01') } 122 | 123 | let(:unit) { u(:month) } 124 | 125 | it 'smartly calculates previous round' do 126 | expect(unit.prev(floored)).to eq unit.floor(floored) 127 | expect(unit.prev(decreased)).to eq unit.decrease(unit.floor(floored)) 128 | end 129 | end 130 | 131 | describe '#next' do 132 | let(:ceiled) { t.parse('2015-03-05') } 133 | let(:advanced) { t.parse('2015-03-01') } 134 | 135 | let(:unit) { u(:month) } 136 | 137 | it 'smartly calculates next round' do 138 | expect(unit.next(ceiled)).to eq unit.ceil(ceiled) 139 | expect(unit.next(advanced)).to eq unit.advance(unit.floor(advanced)) 140 | end 141 | end 142 | 143 | describe '#advance' do 144 | context 'one step' do 145 | fixture = load_fixture(:advance, t) 146 | let(:source) { t.parse(fixture[:source]) } 147 | 148 | fixture[:targets].each do |step, val| 149 | it "advances one #{step} exactly" do 150 | expect(u(step).advance(source)).to eq t.parse(val) 151 | end 152 | end 153 | end 154 | 155 | context 'several steps' do 156 | let(:tm) { t.parse('2015-03-27 11:40:20') } 157 | 158 | limit_units(%i[sec min hour day month year], t).each do |step| 159 | [3, 100, 1000].each do |amount| 160 | context "when advanced #{amount} #{step}s" do 161 | let(:unit) { u(step) } 162 | let(:correct) { amount.times.inject(tm) { |tt| unit.advance(tt) } } 163 | 164 | subject { unit.advance(tm, amount) } 165 | 166 | it { is_expected.to eq correct } 167 | end 168 | end 169 | end 170 | end 171 | 172 | context 'negative advance' do 173 | let(:tm) { t.parse('2015-03-27 11:40:20') } 174 | 175 | limit_units(%i[sec min hour day month year], t).each do |step| 176 | context "when step=#{step}" do 177 | let(:unit) { u(step) } 178 | 179 | it 'treats negative advance as decrease' do 180 | expect(unit.advance(tm, -13)).to eq(unit.decrease(tm, 13)) 181 | end 182 | end 183 | end 184 | end 185 | 186 | context 'zero advance' do 187 | let(:tm) { t.parse('2015-03-27 11:40:20') } 188 | 189 | %i[sec min hour day month year].each do |step| 190 | context "when step=#{step}" do 191 | let(:unit) { u(step) } 192 | 193 | it 'does nothing on zero advance' do 194 | expect(unit.advance(tm, 0)).to eq tm 195 | end 196 | end 197 | end 198 | end 199 | 200 | context 'non-integer advance' do 201 | let(:tm) { t.parse('2015-03-27 11:40:20') } 202 | let(:r) { Rational(1, 2) } 203 | 204 | if t != Date 205 | it { expect(u(:min).advance(tm, r)).to eq t.parse('2015-03-27 11:40:50') } 206 | it { expect(u(:hour).advance(tm, r)).to eq t.parse('2015-03-27 12:10:20') } 207 | it { expect(u(:day).advance(tm, r)).to eq t.parse('2015-03-27 23:40:20') } 208 | it { expect(u(:week).advance(tm, r)).to eq t.parse('2015-03-30 23:40:20') } 209 | else 210 | xit { expect(u(:week).advance(tm, r)).to eq t.parse('2015-03-30') } 211 | end 212 | 213 | it 'behaves when non-integer advance have no clear sense' do 214 | expect(u(:month).advance(tm, r)).to eq tm 215 | expect(u(:year).advance(tm, r)).to eq tm 216 | 217 | expect(u(:month).advance(tm, 1 + r)).to eq u(:month).advance(tm, 1) 218 | expect(u(:year).advance(tm, 1 + r)).to eq u(:year).advance(tm, 1) 219 | end 220 | end 221 | end 222 | 223 | describe '#decrease' do 224 | context 'one step' do 225 | fixture = load_fixture(:decrease, t) 226 | let(:source) { t.parse(fixture[:source]) } 227 | 228 | fixture[:targets].each do |step, val| 229 | it "decreases one #{step} exactly" do 230 | expect(u(step).decrease(source)).to eq t.parse(val) 231 | end 232 | end 233 | end 234 | 235 | context 'several steps' do 236 | let(:tm) { t.parse('2015-03-27 11:40:20') } 237 | 238 | limit_units(%i[sec min hour day month year], t).each do |step| 239 | [3, 100, 1000].each do |amount| 240 | context "when decreased #{amount} #{step}s" do 241 | let(:unit) { u(step) } 242 | let(:correct) { amount.times.inject(tm) { |tt| unit.decrease(tt) } } 243 | 244 | subject { unit.decrease(tm, amount) } 245 | 246 | it { is_expected.to eq correct } 247 | end 248 | end 249 | end 250 | end 251 | 252 | context 'negative decrease' do 253 | let(:tm) { t.parse('2015-03-27 11:40:20') } 254 | 255 | limit_units(%i[sec min hour day month year], t).each do |step| 256 | context "when step=#{step}" do 257 | let(:unit) { u(step) } 258 | 259 | it 'treats negative decrease as advance' do 260 | expect(unit.decrease(tm, -13)).to eq(unit.advance(tm, 13)) 261 | end 262 | end 263 | end 264 | end 265 | 266 | context 'zero decrease' do 267 | let(:tm) { t.parse('2015-03-27 11:40:20') } 268 | 269 | %i[sec min hour day month year].each do |step| 270 | context "when step=#{step}" do 271 | let(:unit) { u(step) } 272 | 273 | it 'does nothing on zero decrease' do 274 | expect(unit.decrease(tm, 0)).to eq tm 275 | end 276 | end 277 | end 278 | end 279 | end 280 | 281 | describe '#round?' do 282 | let(:fixture) { load_fixture(:round, t) } 283 | 284 | it 'determines if tm is round to step' do 285 | fixture.each do |step, vals| 286 | expect(u(step)).to be_round(t.parse(vals[:true])) 287 | 288 | next if step == :day && t == Date # always round, you know :) 289 | 290 | expect(u(step)).not_to be_round(t.parse(vals[:false])) 291 | end 292 | end 293 | end 294 | 295 | describe '#range' do 296 | let(:from) { Time.now } 297 | 298 | limit_units(TimeMath::Units.names, t).each do |step| 299 | context "with step=#{step}" do 300 | let(:unit) { u(step) } 301 | 302 | context 'when single step' do 303 | subject { unit.range(from) } 304 | 305 | it { is_expected.to eq(from...unit.advance(from)) } 306 | end 307 | 308 | context 'when several steps' do 309 | subject { unit.range(from, 5) } 310 | 311 | it { is_expected.to eq(from...unit.advance(from, 5)) } 312 | end 313 | end 314 | end 315 | end 316 | 317 | describe '#range_back' do 318 | let(:from) { Time.now } 319 | 320 | limit_units(TimeMath::Units.names, t).each do |step| 321 | context "with step=#{step}" do 322 | let(:unit) { u(step) } 323 | 324 | context 'when single step' do 325 | subject { unit.range_back(from) } 326 | 327 | it { is_expected.to eq(unit.decrease(from)...from) } 328 | end 329 | 330 | context 'when several steps' do 331 | subject { unit.range_back(from, 5) } 332 | 333 | it { is_expected.to eq(unit.decrease(from, 5)...from) } 334 | end 335 | end 336 | end 337 | end 338 | 339 | describe '#measure' do 340 | fixture = load_fixture(:measure, t) 341 | 342 | fixture.each do |data| 343 | next if t == Date && data[:unit] == :year # problematic edge case, works but different from others 344 | context data[:unit] do 345 | let(:unit) { u(data[:unit]) } 346 | let(:from) { t.parse(data[:from]) } 347 | let(:to) { t.parse(data[:to]) } 348 | 349 | subject { unit.measure(from, to) } 350 | 351 | it { is_expected.to eq data[:val] } 352 | end 353 | end 354 | end 355 | 356 | describe '#measure_rem' do 357 | fixture = load_fixture(:measure, t) 358 | 359 | fixture.each do |data| 360 | next if t == Date && data[:unit] == :year # problematic edge case, works but different from others 361 | context data[:unit] do 362 | let(:unit) { u(data[:unit]) } 363 | let(:from) { t.parse(data[:from]) } 364 | let(:to) { t.parse(data[:to]) } 365 | 366 | it 'measures integer steps between from and to and return reminder' do 367 | measure, rem = unit.measure_rem(from, to) 368 | 369 | expected_rem = unit.advance(from, measure) 370 | 371 | expect(measure).to eq data[:val] 372 | expect(rem).to eq expected_rem 373 | end 374 | 375 | it 'is exactly the same for same data' do 376 | measure, rem = unit.measure_rem(to, to) 377 | expect(measure).to eq 0 378 | expect(rem).to eq to 379 | end 380 | 381 | it 'is correct for backwards measurement' do 382 | measure, rem = unit.measure_rem(to, from) 383 | expected_rem = unit.advance(to, measure) 384 | 385 | expect(measure).to eq(-data[:val]) 386 | expect(rem).to eq expected_rem 387 | end 388 | end 389 | end 390 | end 391 | 392 | describe '#sequence' do 393 | let(:from) { t.parse('2016-05-01 13:30') } 394 | let(:to) { t.parse('2016-05-15 15:45') } 395 | 396 | limit_units(TimeMath.units, t).each do |unit| 397 | context "with #{unit}" do 398 | subject { u(unit).sequence(from...to) } 399 | 400 | it { is_expected.to eq TimeMath::Sequence.new(unit, from...to) } 401 | end 402 | end 403 | end 404 | end 405 | end 406 | 407 | describe '#resample' do 408 | let(:unit) { TimeMath.day } 409 | 410 | context 'array of time' do 411 | let(:data) { [Time.parse('2016-06-01'), Time.parse('2016-06-03'), Time.parse('2016-06-05')] } 412 | 413 | subject { unit.resample(data) } 414 | 415 | it { 416 | is_expected.to eq([ 417 | Time.parse('2016-06-01'), 418 | Time.parse('2016-06-02'), 419 | Time.parse('2016-06-03'), 420 | Time.parse('2016-06-04'), 421 | Time.parse('2016-06-05') 422 | ]) 423 | } 424 | end 425 | 426 | context 'hash with time keys' do 427 | let(:data) { 428 | { 429 | Time.parse('2016-06-01') => 1, Time.parse('2016-06-03') => 2, Time.parse('2016-06-05') => 3 430 | } } 431 | 432 | context 'no block' do 433 | subject { unit.resample(data) } 434 | 435 | it { 436 | is_expected.to eq( 437 | Time.parse('2016-06-01') => [1], 438 | Time.parse('2016-06-02') => [], 439 | Time.parse('2016-06-03') => [2], 440 | Time.parse('2016-06-04') => [], 441 | Time.parse('2016-06-05') => [3] 442 | ) 443 | } 444 | end 445 | 446 | context 'block' do 447 | subject { unit.resample(data, &:first) } 448 | 449 | it { 450 | is_expected.to eq( 451 | Time.parse('2016-06-01') => 1, 452 | Time.parse('2016-06-02') => nil, 453 | Time.parse('2016-06-03') => 2, 454 | Time.parse('2016-06-04') => nil, 455 | Time.parse('2016-06-05') => 3 456 | ) 457 | } 458 | end 459 | 460 | context 'symbol' do 461 | subject { unit.resample(data, :first) } 462 | 463 | it { 464 | is_expected.to eq( 465 | Time.parse('2016-06-01') => 1, 466 | Time.parse('2016-06-02') => nil, 467 | Time.parse('2016-06-03') => 2, 468 | Time.parse('2016-06-04') => nil, 469 | Time.parse('2016-06-05') => 3 470 | ) 471 | } 472 | end 473 | end 474 | 475 | context 'wrong arguments' do 476 | let(:data) { [1, 2, 3] } 477 | 478 | it { expect { unit.resample(data) }.to raise_error(ArgumentError) } 479 | end 480 | end 481 | 482 | # TODO: edge cases: 483 | # * monthes decr/incr, including leap years 484 | describe 'edge cases' do 485 | specify 'advance over month end' do 486 | expect(TimeMath.month.advance(DateTime.new(2017, 5, 29), 9)).to eq DateTime.new(2018, 2, 28) 487 | expect(TimeMath.month.advance(Time.new(2017, 5, 29), 10)).to eq Time.new(2018, 3, 29) 488 | end 489 | end 490 | 491 | describe 'type compatibility' do 492 | let(:t) { Time.parse('2017-05-01 17:30') } 493 | let(:d) { Date.parse('2017-06-15') } 494 | let(:dt) { DateTime.parse('2017-03-10 15:40') } 495 | 496 | it { expect(TimeMath.month.measure(t, d)).to eq 1 } 497 | it { expect(TimeMath.month.measure(t, dt)).to eq(-1) } 498 | it { expect(TimeMath.month.measure(d, dt)).to eq(-3) } 499 | end 500 | 501 | describe 'Preserve time offset' do 502 | let(:tm) { Time.parse('2016-06-01 14:30 +08') } 503 | let(:day) { TimeMath.day } 504 | 505 | it 'preserves time offset always' do 506 | expect(day.floor(tm)).to eq(Time.parse('2016-06-01 00:00 +08')).and(have_attributes(gmt_offset: 8 * 3600)) 507 | expect(day.ceil(tm)).to eq(Time.parse('2016-06-02 00:00 +08')).and(have_attributes(gmt_offset: 8 * 3600)) 508 | expect(day.advance(tm)).to eq(Time.parse('2016-06-02 14:30 +08')).and(have_attributes(gmt_offset: 8 * 3600)) 509 | expect(day.decrease(tm)).to eq(Time.parse('2016-05-31 14:30 +08')).and(have_attributes(gmt_offset: 8 * 3600)) 510 | end 511 | end 512 | 513 | context 'graceful fail' do 514 | let(:unit) { u(:day) } 515 | 516 | it { expect { unit.advance('2016-05-01') }.to raise_error ArgumentError, /got String/ } 517 | end 518 | end 519 | -------------------------------------------------------------------------------- /time_math2.gemspec: -------------------------------------------------------------------------------- 1 | require './lib/time_math/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'time_math2' 5 | s.version = TimeMath::VERSION 6 | s.authors = ['Victor Shepelev'] 7 | s.email = 'zverok.offline@gmail.com' 8 | s.homepage = 'https://github.com/zverok/time_math2' 9 | 10 | s.summary = 'Easy time math' 11 | s.description = <<-EOF 12 | TimeMath is small, no-dependencies library attemting to make work with 13 | time units easier. It provides you with simple, easy remembered API, without 14 | any monkey patching of core Ruby classes, so it can be used alongside 15 | Rails or without it, for any purpose. 16 | EOF 17 | s.licenses = ['MIT'] 18 | 19 | s.required_ruby_version = '>= 2.1.0' 20 | 21 | s.files = `git ls-files`.split($RS).reject do |file| 22 | file =~ /^(?: 23 | spec\/.* 24 | |Gemfile 25 | |Rakefile 26 | |\.rspec 27 | |\.gitignore 28 | |\.rubocop.yml 29 | |\.travis.yml 30 | )$/x 31 | end 32 | s.require_paths = ["lib"] 33 | 34 | s.add_development_dependency 'rubocop', '~> 0.65.0' 35 | s.add_development_dependency 'rspec', '>= 3' 36 | s.add_development_dependency 'rubocop-rspec', '>= 1.17.1' 37 | s.add_development_dependency 'rspec-its', '~> 1' 38 | s.add_development_dependency 'simplecov', '~> 0.9' 39 | s.add_development_dependency 'rake' 40 | s.add_development_dependency 'rubygems-tasks' 41 | s.add_development_dependency 'yard' 42 | end 43 | --------------------------------------------------------------------------------