time_math2 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ --markup=markdown
2
+ --no-private
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # TimeBoots changelog
2
+
3
+ # 0.0.2 (2016-05-27)
4
+
5
+ * Add support for `DateTime`.
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