time_math2 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +316 -0
- data/lib/time_math/core_ext.rb +52 -0
- data/lib/time_math/measure.rb +33 -0
- data/lib/time_math/sequence.rb +175 -0
- data/lib/time_math/span.rb +53 -0
- data/lib/time_math/units/base.rb +264 -0
- data/lib/time_math/units/day.rb +34 -0
- data/lib/time_math/units/month.rb +48 -0
- data/lib/time_math/units/simple.rb +58 -0
- data/lib/time_math/units/week.rb +30 -0
- data/lib/time_math/units/year.rb +28 -0
- data/lib/time_math/units.rb +29 -0
- data/lib/time_math/version.rb +4 -0
- data/lib/time_math.rb +99 -0
- data/time_math2.gemspec +39 -0
- metadata +163 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 006dfa63a22116ad083fd98e2aca4390734bce26
|
4
|
+
data.tar.gz: 028d5e1ca0a36f80840b95aadb675f60497385f5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 261cbba1b37663d18b456edfb0f0ea7a18dbd7c78c72d4c77d6d20feb664a90f4ca2baaffcaf7493a04181cf37c40a4c3480e2493dd767d4dece3dcdf0271e37
|
7
|
+
data.tar.gz: 2d9beccf3cd5e3a3b4f7a35366ec20576dc770c7570768cc6784b8cfb0bcc139df713ac88ac764ba2eb2eb44b6c4aa070d6c3b1e20626bb5c58540a38eff991b
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,316 @@
|
|
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
|
+
**TimeMath2** is a small, no-dependencies library attempting to make time
|
10
|
+
arithmetics easier. It provides you with simple, easy-to-remember API, without
|
11
|
+
any monkey-patching of core Ruby classes, so it can be used alongside
|
12
|
+
Rails or without it, for any purpose.
|
13
|
+
|
14
|
+
## Features
|
15
|
+
|
16
|
+
* No monkey-patching of core classes (opt-in patching of Time and DateTime
|
17
|
+
provided, though);
|
18
|
+
* Works with Time and DateTime;
|
19
|
+
* Accurately preserves timezone info;
|
20
|
+
* Simple arithmetics: floor/ceil/round to any time unit (second, hour, year
|
21
|
+
or whatnot), advance/decrease by any unit;
|
22
|
+
* Simple time span abstraction (like "5 years" object you can store and
|
23
|
+
pass to other methods);
|
24
|
+
* Easy generation of time sequences (like "each day from _this_ to _that_
|
25
|
+
date");
|
26
|
+
* Measuring of time distances between two timestamps in any units.
|
27
|
+
|
28
|
+
## Naming
|
29
|
+
|
30
|
+
`TimeMath` is the better name I know for the task library does, but
|
31
|
+
it is [already taken](https://rubygems.org/gems/time_math). So, with no
|
32
|
+
other thoughts I came with the ugly solution.
|
33
|
+
|
34
|
+
(BTW, the [previous version](https://github.com/zverok/time_math/blob/e997d7ddd52fc5bce3c77dc3c8022adfc9fe7028/README.md)
|
35
|
+
had some dumb "funny" name for gem and all helper classes, and nobody liked it.)
|
36
|
+
|
37
|
+
## Reasons
|
38
|
+
|
39
|
+
You frequently need to calculate things like "exact midnight of the next
|
40
|
+
day", but you don't want to monkey-patch all of your integers, tug in
|
41
|
+
5K LOC of ActiveSupport and you like to have things clean and readable.
|
42
|
+
|
43
|
+
## Installation
|
44
|
+
|
45
|
+
Install it like always:
|
46
|
+
|
47
|
+
```
|
48
|
+
$ gem install time_math2
|
49
|
+
```
|
50
|
+
|
51
|
+
or add to your Gemfile
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
gem 'time_math2'
|
55
|
+
```
|
56
|
+
|
57
|
+
and `bundle install` it.
|
58
|
+
|
59
|
+
## Usage
|
60
|
+
|
61
|
+
First, you take time unit you want:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
TimeMath[:day] # => #<TimeMath::Units::Day>
|
65
|
+
# or
|
66
|
+
TimeMath.day # => #<TimeMath::Units::Day>
|
67
|
+
|
68
|
+
# List of units supported:
|
69
|
+
TimeMath.units
|
70
|
+
# => [:sec, :min, :hour, :day, :week, :month, :year]
|
71
|
+
```
|
72
|
+
|
73
|
+
Then you use this unit for any math you want:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
TimeMath.day.floor(Time.now) # => 2016-05-28 00:00:00 +0300
|
77
|
+
TimeMath.day.ceil(Time.now) # => 2016-05-29 00:00:00 +0300
|
78
|
+
TimeMath.day.advance(Time.now, +10) # => 2016-06-07 14:06:57 +0300
|
79
|
+
# ...and so on
|
80
|
+
```
|
81
|
+
|
82
|
+
### Full list of simple arithmetic methods
|
83
|
+
|
84
|
+
* `<unit>.floor(tm)` -- rounds down to nearest `<unit>`;
|
85
|
+
* `<unit>.ceil(tm)` -- rounds up to nearest `<unit>`;
|
86
|
+
* `<unit>.round(tm)` -- rounds to nearest `<unit>` (up or down);
|
87
|
+
* `<unit>.round?(tm)` -- checks if `tm` is already round to `<unit>`;
|
88
|
+
* `<unit>.prev(tm)` -- like `floor`, but always decreases:
|
89
|
+
- `2015-06-27 13:30` would be converted to `2015-06-27 00:00` by both
|
90
|
+
`floor` and `prev`, but
|
91
|
+
- `2015-06-27 00:00` would be left intact on `floor`, but would be
|
92
|
+
decreased to `2015-06-26 00:00` by `prev`;
|
93
|
+
* `<unit>.next(tm)` -- like `ceil`, but always increases;
|
94
|
+
* `<unit>.advance(tm, amount)` -- increases tm by integer amount of `<unit>`s;
|
95
|
+
* `<unit>.decrease(tm, amount)` -- decreases tm by integer amount of `<unit>`s;
|
96
|
+
* `<unit>.range(tm, amount)` -- creates range of `tm ... tm + amount <units>`;
|
97
|
+
* `<unit>.range_back(tm, amount)` -- creates range of `tm - amount <units> ... tm`.
|
98
|
+
|
99
|
+
See also [Units::Base](http://www.rubydoc.info/gems/time_math2/TimeMath/Units/Base).
|
100
|
+
|
101
|
+
### Time span abstraction
|
102
|
+
|
103
|
+
`TimeMath::Span` is a simple abstraction of "N units of time", which you
|
104
|
+
can store in variable and then apply to some time value:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
span = TimeMath.day.span(5)
|
108
|
+
# => #<TimeMath::Span(day): +5>
|
109
|
+
span.before(Time.now)
|
110
|
+
# => 2016-05-23 17:46:13 +0300
|
111
|
+
```
|
112
|
+
|
113
|
+
See also [Span YARD docs](http://www.rubydoc.info/gems/time_math2/TimeMath/Span).
|
114
|
+
|
115
|
+
### Time sequence abstraction
|
116
|
+
|
117
|
+
Time sequence allows you to generate an array of time values between some
|
118
|
+
points:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
to = Time.now
|
122
|
+
# => 2016-05-28 17:47:30 +0300
|
123
|
+
from = TimeMath.day.floor(to)
|
124
|
+
# => 2016-05-28 00:00:00 +0300
|
125
|
+
seq = TimeMath.hour.sequence(from, to)
|
126
|
+
# => #<TimeMath::Sequence(2016-05-28 00:00:00 +0300 - 2016-05-28 17:47:30 +0300)>
|
127
|
+
p(*seq)
|
128
|
+
# 2016-05-28 00:00:00 +0300
|
129
|
+
# 2016-05-28 01:00:00 +0300
|
130
|
+
# 2016-05-28 02:00:00 +0300
|
131
|
+
# 2016-05-28 03:00:00 +0300
|
132
|
+
# 2016-05-28 04:00:00 +0300
|
133
|
+
# 2016-05-28 05:00:00 +0300
|
134
|
+
# 2016-05-28 06:00:00 +0300
|
135
|
+
# 2016-05-28 07:00:00 +0300
|
136
|
+
# ...and so on
|
137
|
+
```
|
138
|
+
|
139
|
+
See also [Sequence YARD docs](http://www.rubydoc.info/gems/time_math2/TimeMath/Sequence).
|
140
|
+
|
141
|
+
### Measuring time periods
|
142
|
+
|
143
|
+
Simple measure: just "how many `<unit>`s from date A to date B:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
TimeMath.week.measure(Time.parse('2016-05-01'), Time.parse('2016-06-01'))
|
147
|
+
# => 4
|
148
|
+
```
|
149
|
+
|
150
|
+
Measure with remaineder: returns number of `<unit>`s between dates and
|
151
|
+
the date when this number would be exact:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
TimeMath.week.measure_rem(Time.parse('2016-05-01'), Time.parse('2016-06-01'))
|
155
|
+
# => [4, 2016-05-29 00:00:00 +0300]
|
156
|
+
```
|
157
|
+
|
158
|
+
(on May 29 there would be exactly 4 weeks since May 1).
|
159
|
+
|
160
|
+
Multi-unit measuring:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
# My real birthday, in fact!
|
164
|
+
birthday = Time.parse('1983-02-14 13:30')
|
165
|
+
|
166
|
+
# My full age
|
167
|
+
TimeMath.measure(birthday, Time.now)
|
168
|
+
# => {:years=>33, :months=>3, :weeks=>2, :days=>0, :hours=>1, :minutes=>25, :seconds=>52}
|
169
|
+
|
170
|
+
# NB: you can use this output with String#format or String%:
|
171
|
+
puts "%{years}y %{months}m %{weeks}w %{days}d %{hours}h %{minutes}m %{seconds}s" %
|
172
|
+
TimeMath.measure(birthday, Time.now)
|
173
|
+
# 33y 3m 2w 0d 1h 26m 15s
|
174
|
+
|
175
|
+
# Option: measure without weeks
|
176
|
+
TimeMath.measure(birthday, Time.now, weeks: false)
|
177
|
+
# => {:years=>33, :months=>3, :days=>14, :hours=>1, :minutes=>26, :seconds=>31}
|
178
|
+
|
179
|
+
# My full age in days, hours, minutes
|
180
|
+
TimeMath.measure(birthday, Time.now, upto: :day)
|
181
|
+
# => {:days=>12157, :hours=>2, :minutes=>26, :seconds=>55}
|
182
|
+
```
|
183
|
+
|
184
|
+
### Optional `Time` and `DateTime` patches
|
185
|
+
|
186
|
+
This core classes extension is optional and should be required explicitly.
|
187
|
+
TimeMath doesn't change behavior of any existing methods (like `#+`),
|
188
|
+
it just adds a couple of new ones:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
require 'time_math/core_ext'
|
192
|
+
|
193
|
+
Time.now.decrease_by(:day, 10).floor_to(:month)
|
194
|
+
Time.now.sequence_to(:month, Time.now.advance_by(:year, 5))
|
195
|
+
```
|
196
|
+
|
197
|
+
See [CoreExt](http://www.rubydoc.info/gems/time_math2/TimeMath/CoreExt)
|
198
|
+
documentation for full lists of methods added.
|
199
|
+
|
200
|
+
### Notes on timezones
|
201
|
+
|
202
|
+
TimeMath tries its best to preserve timezones of original values. Currently,
|
203
|
+
it means:
|
204
|
+
|
205
|
+
* For `Time` instances, symbolic timezone is preserved; when jumping over
|
206
|
+
DST border, UTC offset will change and everything remains as expected;
|
207
|
+
* For `DateTime` Ruby not provides symbolic timezone, only numeric offset;
|
208
|
+
it is preserved by TimeMath (but be careful about jumping around DST,
|
209
|
+
offset would not change).
|
210
|
+
|
211
|
+
|
212
|
+
## Time series generation: "laces"
|
213
|
+
|
214
|
+
I'm a real fan of funny names in gems. Here we have time **boots** for working
|
215
|
+
with time **steps**. So, something continuous will be called **lace**.
|
216
|
+
|
217
|
+
I hope, next examples are pretty self-explanatory.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
from = Time.parse('2015-03-05 10:08')
|
221
|
+
to = Time.parse('2015-03-09 11:07')
|
222
|
+
|
223
|
+
lace = TimeBoots.month.lace(from, to)
|
224
|
+
# => #<TimeBoots::Lace(2015-03-05 10:08:00 +0200 - 2015-03-09 11:07:00 +0200)>
|
225
|
+
|
226
|
+
# or
|
227
|
+
TimeBoots.lace(:month, from, to)
|
228
|
+
# => #<TimeBoots::Lace(2015-03-05 10:08:00 +0200 - 2015-03-09 11:07:00 +0200)>
|
229
|
+
|
230
|
+
lace.pull
|
231
|
+
# => [2015-03-05 10:08:00 +0200,
|
232
|
+
# 2015-03-06 10:08:00 +0200,
|
233
|
+
# 2015-03-07 10:08:00 +0200,
|
234
|
+
# 2015-03-08 10:08:00 +0200,
|
235
|
+
# 2015-03-09 10:08:00 +0200]
|
236
|
+
```
|
237
|
+
|
238
|
+
So, we have just a series of times, each day, from `from` until `to`.
|
239
|
+
Note, there is a same time of the day (10:08), as it was in `from`.
|
240
|
+
|
241
|
+
The `pull` method has an optional argument, when it is `true`, the
|
242
|
+
method returns `floor`-ed times (e.g. midnights for daily lace):
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
lace.pull(true)
|
246
|
+
# => [2015-03-05 10:08:00 +0200,
|
247
|
+
# 2015-03-06 00:00:00 +0200,
|
248
|
+
# 2015-03-07 00:00:00 +0200,
|
249
|
+
# 2015-03-08 00:00:00 +0200,
|
250
|
+
# 2015-03-09 00:00:00 +0200]
|
251
|
+
```
|
252
|
+
|
253
|
+
Note the first value still at 10:08: we don't want to go before `from`.
|
254
|
+
Lace also can "expand" your period for you (`floor` the beginning and
|
255
|
+
`ceil` the end):
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
lace.expand
|
259
|
+
# => #<TimeBoots::Lace(2015-03-05 00:00:00 +0200-2015-03-10 00:00:00 +0200)>
|
260
|
+
|
261
|
+
# or
|
262
|
+
lace.expand!
|
263
|
+
|
264
|
+
# or start with expanded:
|
265
|
+
TimeBoots.month.lace(from, to, expanded: true)
|
266
|
+
```
|
267
|
+
|
268
|
+
Another useful lace's functionality is generating periods.
|
269
|
+
It can be useful for filtering daily data from database, for example:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
lace.pull_ranges
|
273
|
+
# => [2015-03-05 10:08:00 +0200...2015-03-06 10:08:00 +0200,
|
274
|
+
# 2015-03-06 10:08:00 +0200...2015-03-07 10:08:00 +0200,
|
275
|
+
# 2015-03-07 10:08:00 +0200...2015-03-08 10:08:00 +0200,
|
276
|
+
# 2015-03-08 10:08:00 +0200...2015-03-09 10:08:00 +0200,
|
277
|
+
# 2015-03-09 10:08:00 +0200...2015-03-09 11:07:00 +0200]
|
278
|
+
|
279
|
+
# Now, you can do something like:
|
280
|
+
|
281
|
+
lace.pull_ranges.map{|range| dataset.where(timestamp: range).count}
|
282
|
+
# ...and have your daily stats with one line of code
|
283
|
+
|
284
|
+
```
|
285
|
+
|
286
|
+
## Got it, what else?
|
287
|
+
|
288
|
+
TimeMath also play well when included into other classes or modules:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
class MyModel
|
292
|
+
include TimeMath
|
293
|
+
|
294
|
+
def next_day
|
295
|
+
day.advance # Here!
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
## Alternatives
|
301
|
+
|
302
|
+
There's pretty small and useful [AS::Duration](https://github.com/janko-m/as-duration)
|
303
|
+
by Janko Marohnić, which is time durations, extracted from ActiveSupport,
|
304
|
+
but without any ActiveSupport bloat.
|
305
|
+
|
306
|
+
## Links
|
307
|
+
|
308
|
+
* [API Docs](http://www.rubydoc.info/gems/time_math2)
|
309
|
+
|
310
|
+
## Author
|
311
|
+
|
312
|
+
[Victor Shepelev](http://zverok.github.io/)
|
313
|
+
|
314
|
+
## License
|
315
|
+
|
316
|
+
[MIT](https://github.com/zverok/time_math2/blob/master/LICENSE.txt).
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module TimeMath
|
2
|
+
# This module is included into `Time` and `DateTime`. It is optional
|
3
|
+
# and only included by explicit `require "time_math/core_ext"`.
|
4
|
+
module CoreExt
|
5
|
+
# @!method floor_to(unit)
|
6
|
+
# Shortcut to {Units::Base#floor}
|
7
|
+
# @!method ceil_to(unit)
|
8
|
+
# Shortcut to {Units::Base#ceil}
|
9
|
+
# @!method round_to(unit)
|
10
|
+
# Shortcut to {Units::Base#round}
|
11
|
+
# @!method next_to(unit)
|
12
|
+
# Shortcut to {Units::Base#next}
|
13
|
+
# @!method prev_to(unit)
|
14
|
+
# Shortcut to {Units::Base#prev}
|
15
|
+
[:floor, :ceil, :round, :next, :prev].each do |sym|
|
16
|
+
define_method("#{sym}_to") { |unit| TimeMath[unit].send(sym, self) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Shortcut to {Units::Base#round?}
|
20
|
+
def round_to?(unit)
|
21
|
+
TimeMath[unit].round?(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Shortcut to {Units::Base#advance}
|
25
|
+
def advance_by(unit, amount = 1)
|
26
|
+
TimeMath[unit].advance(self, amount)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Shortcut to {Units::Base#decrease}
|
30
|
+
def decrease_by(unit, amount = 1)
|
31
|
+
TimeMath[unit].decrease(self, amount)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Shortcut to {Units::Base#range}
|
35
|
+
def range_to(unit, amount = 1)
|
36
|
+
TimeMath[unit].range(self, amount)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Shortcut to {Units::Base#range_back}
|
40
|
+
def range_from(unit, amount = 1)
|
41
|
+
TimeMath[unit].range_back(self, amount)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Shortcut to {Units::Base#sequence}. See its docs for possible options.
|
45
|
+
def sequence_to(unit, other, options = {})
|
46
|
+
TimeMath[unit].sequence(self, other, options)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Time.send :include, CoreExt
|
51
|
+
DateTime.send :include, CoreExt if ::Kernel.const_defined?(:DateTime)
|
52
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module TimeMath
|
3
|
+
# @private
|
4
|
+
module Measure
|
5
|
+
PLURALS = {
|
6
|
+
year: :years,
|
7
|
+
month: :months,
|
8
|
+
week: :weeks,
|
9
|
+
day: :days,
|
10
|
+
hour: :hours,
|
11
|
+
min: :minutes,
|
12
|
+
sec: :seconds
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def self.measure(from, to, options = {})
|
16
|
+
select_units(options).reverse.inject({}) do |res, unit|
|
17
|
+
span, from = Units.get(unit).measure_rem(from, to)
|
18
|
+
res.merge(PLURALS[unit] => span)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.select_units(options)
|
23
|
+
units = Units.names
|
24
|
+
units.delete(:week) if options[:weeks] == false
|
25
|
+
|
26
|
+
if (idx = units.index(options[:upto]))
|
27
|
+
units = units.first(idx + 1)
|
28
|
+
end
|
29
|
+
|
30
|
+
units
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,175 @@
|
|
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
|
+
# # => #<TimeMath::Sequence(2016-05-01 13:30:00 +0300 - 2016-05-04 18:20:00 +0300)>
|
13
|
+
# ```
|
14
|
+
#
|
15
|
+
# Now, you can use it:
|
16
|
+
# ```ruby
|
17
|
+
# seq.to_a
|
18
|
+
# # => [2016-05-01 13:30:00 +0300, 2016-05-02 13:30:00 +0300, 2016-05-03 13:30:00 +0300, 2016-05-04 13:30:00 +0300]
|
19
|
+
# ```
|
20
|
+
# -- it's an "each day start between from and to". As you can see,
|
21
|
+
# the period start is the same as in `from`. You can request to floor
|
22
|
+
# them to beginning of day with {#floor} method or `:floor` option:
|
23
|
+
#
|
24
|
+
# ```ruby
|
25
|
+
# seq.floor.to_a
|
26
|
+
# # => [2016-05-01 13:30:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300]
|
27
|
+
# # or:
|
28
|
+
# seq = TimeMath.day.sequence(from, to, floor: true)
|
29
|
+
# seq.to_a
|
30
|
+
# ```
|
31
|
+
# -- it floors all day starts except of `from`, which is preserved.
|
32
|
+
#
|
33
|
+
# You can expand from and to to nearest round unit by {#expand} method
|
34
|
+
# or `:expand` option:
|
35
|
+
#
|
36
|
+
# ```ruby
|
37
|
+
# seq.expand.to_a
|
38
|
+
# # => [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]
|
39
|
+
# # or:
|
40
|
+
# seq = TimeMath.day.sequence(from, to, expand: true)
|
41
|
+
# # => #<TimeMath::Sequence(2016-05-01 00:00:00 +0300 - 2016-05-05 00:00:00 +0300)>
|
42
|
+
# seq.to_a
|
43
|
+
# ```
|
44
|
+
#
|
45
|
+
# Besides each period beginning, you can also request pairs of begin/end
|
46
|
+
# of a period, either as an array of arrays, or array of ranges:
|
47
|
+
#
|
48
|
+
# ```ruby
|
49
|
+
# seq = TimeMath.day.sequence(from, to)
|
50
|
+
# seq.pairs
|
51
|
+
# # => [[2016-05-01 13:30:00 +0300, 2016-05-02 13:30:00 +0300], [2016-05-02 13:30:00 +0300, 2016-05-03 13:30:00 +0300], [2016-05-03 13:30:00 +0300, 2016-05-04 13:30:00 +0300], [2016-05-04 13:30:00 +0300, 2016-05-04 18:20:00 +0300]]
|
52
|
+
# seq.ranges
|
53
|
+
# # => [2016-05-01 13:30:00 +0300...2016-05-02 13:30:00 +0300, 2016-05-02 13:30:00 +0300...2016-05-03 13:30:00 +0300, 2016-05-03 13:30:00 +0300...2016-05-04 13:30:00 +0300, 2016-05-04 13:30:00 +0300...2016-05-04 18:20:00 +0300]
|
54
|
+
# ```
|
55
|
+
#
|
56
|
+
# It is pretty convenient for filtering data from databases or APIs,
|
57
|
+
# TimeMath creates list of filtering ranges in a blink.
|
58
|
+
#
|
59
|
+
class Sequence
|
60
|
+
# Creates a sequence. Typically, it is easier to to it with {Units::Base#sequence},
|
61
|
+
# like this:
|
62
|
+
#
|
63
|
+
# ```ruby
|
64
|
+
# TimeMath.day.sequence(from, to)
|
65
|
+
# ```
|
66
|
+
#
|
67
|
+
# @param unit [Symbol] one of {TimeMath.units};
|
68
|
+
# @param from [Time,DateTime] start of sequence;
|
69
|
+
# @param to [Time,DateTime] upper limit of sequence;
|
70
|
+
# @param options [Hash]
|
71
|
+
# @option options [Boolean] :expand round sequence ends on creation
|
72
|
+
# (from is floored and to is ceiled);
|
73
|
+
# @option options [Boolean] :floor sequence will be rounding'ing all
|
74
|
+
# the intermediate values.
|
75
|
+
#
|
76
|
+
def initialize(unit, from, to, options = {})
|
77
|
+
@unit = Units.get(unit)
|
78
|
+
@from, @to = from, to
|
79
|
+
@options = options.dup
|
80
|
+
|
81
|
+
expand! if options[:expand]
|
82
|
+
@floor = options[:floor]
|
83
|
+
end
|
84
|
+
|
85
|
+
attr_reader :from, :to, :unit
|
86
|
+
|
87
|
+
def ==(other)
|
88
|
+
self.class == other.class && unit == other.unit &&
|
89
|
+
from == other.from && to == other.to
|
90
|
+
end
|
91
|
+
|
92
|
+
# If `:floor` option is set for sequence.
|
93
|
+
def floor?
|
94
|
+
@floor
|
95
|
+
end
|
96
|
+
|
97
|
+
# Expand sequence ends to nearest round unit.
|
98
|
+
#
|
99
|
+
# @return self
|
100
|
+
def expand!
|
101
|
+
@from = unit.floor(from)
|
102
|
+
@to = unit.ceil(to)
|
103
|
+
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
# Creates new sequence with ends rounded to nearest unit.
|
108
|
+
#
|
109
|
+
# @return [Sequence]
|
110
|
+
def expand
|
111
|
+
dup.tap(&:expand!)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Sets sequence to floor all the intermediate values.
|
115
|
+
#
|
116
|
+
# @return self
|
117
|
+
def floor!
|
118
|
+
@floor = true
|
119
|
+
end
|
120
|
+
|
121
|
+
# Creates new sequence with setting to floor all the intermediate
|
122
|
+
# values.
|
123
|
+
#
|
124
|
+
# @return [Sequence]
|
125
|
+
def floor
|
126
|
+
dup.tap(&:floor!)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Creates an array of time unit starts between from and to. They will
|
130
|
+
# have same granularity as from (e.g. if unit is day and from is
|
131
|
+
# 2016-05-01 13:30, each of return values will be next day at 13:30),
|
132
|
+
# unless sequence is not set to floor values.
|
133
|
+
#
|
134
|
+
# @return [Array<Time or DateTime>]
|
135
|
+
def to_a
|
136
|
+
seq = []
|
137
|
+
|
138
|
+
iter = from
|
139
|
+
while iter < to
|
140
|
+
seq << iter
|
141
|
+
|
142
|
+
iter = cond_floor(unit.advance(iter))
|
143
|
+
end
|
144
|
+
|
145
|
+
seq
|
146
|
+
end
|
147
|
+
|
148
|
+
# Creates an array of pairs (time unit start, time unit end) between
|
149
|
+
# from and to.
|
150
|
+
#
|
151
|
+
# @return [Array<Array>]
|
152
|
+
def pairs
|
153
|
+
seq = to_a
|
154
|
+
seq.zip(seq[1..-1] + [to])
|
155
|
+
end
|
156
|
+
|
157
|
+
# Creates an array of Ranges (time unit start...time unit end) between
|
158
|
+
# from and to.
|
159
|
+
#
|
160
|
+
# @return [Array<Range>]
|
161
|
+
def ranges
|
162
|
+
pairs.map { |b, e| (b...e) }
|
163
|
+
end
|
164
|
+
|
165
|
+
def inspect
|
166
|
+
"#<#{self.class}(#{from} - #{to})>"
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def cond_floor(tm)
|
172
|
+
@floor ? unit.floor(tm) : tm
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TimeMath
|
2
|
+
# Represents time span (amount of units), like "4 years".
|
3
|
+
# Allows to advance or decrease Time or DateTime.
|
4
|
+
#
|
5
|
+
# Use it like this:
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# TimeMath.year.span(4).before(Time.now)
|
9
|
+
# ```
|
10
|
+
#
|
11
|
+
class Span
|
12
|
+
attr_reader :unit, :amount
|
13
|
+
|
14
|
+
# Creates Span instance.
|
15
|
+
# Typically, it is easire to use {Units::Base#span} than create
|
16
|
+
# spans directly.
|
17
|
+
#
|
18
|
+
# @param unit [Symbol] one of {Units.names}, unit of span;
|
19
|
+
# @param amount [Integer] amount of units in span.
|
20
|
+
def initialize(unit, amount)
|
21
|
+
@unit, @amount = unit, amount
|
22
|
+
@unit_impl = Units.get(unit)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Decreases `tm` by `amount` of `unit`.
|
26
|
+
#
|
27
|
+
# @param tm [Time,DateTime] time value to decrease;
|
28
|
+
# @return [Time,DateTime] decreased time.
|
29
|
+
def before(tm = Time.now)
|
30
|
+
@unit_impl.decrease(tm, amount)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Increases `tm` by `amount` of `unit`.
|
34
|
+
#
|
35
|
+
# @param tm [Time,DateTime] time value to increase;
|
36
|
+
# @return [Time,DateTime] increased time.
|
37
|
+
def after(tm = Time.now)
|
38
|
+
@unit_impl.advance(tm, amount)
|
39
|
+
end
|
40
|
+
|
41
|
+
alias ago before
|
42
|
+
alias from after
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
self.class == other.class &&
|
46
|
+
unit == other.unit && amount == other.amount
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
'#<%s(%s): %+i>' % [self.class, unit, amount]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|