time_math2 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 76771d4aa5435a2100af858a4bd4851ef81d35c9
4
- data.tar.gz: 23abf05b1c0b3dd19a6552619cda98e360b73840
3
+ metadata.gz: 8fe3eec624d634487ddc4b74d08a3d39ec3f34d5
4
+ data.tar.gz: 87c691b0990e5e4a03f7bad4602e8bf100d1d526
5
5
  SHA512:
6
- metadata.gz: a314061838072dac1c4bec199a7a1128f74732a3a0cc041996674e1b6d91a35b53cdd7f7800a9b187108afd73d03027e4e6776c9c5b53f3393df06b2f13d0231
7
- data.tar.gz: 192e2f2f194d52dc4d39868ccae10d83bab53350b63177725c3677c2e607574463420d62eab0c6151741747a1a0fe44de408cc103713319d3de607907ef8bf9e
6
+ metadata.gz: 8a7e17f5d2f9768c734649768b95c8ad63f168ca3c8686f2d52440caa952f8c7a20bc1af223cb5238eacfdec7e5924e814c0d8385887e83a5623da0024d15a55
7
+ data.tar.gz: a39344389ba42191d780257d97e4c53e6e2e84a5512e7deac17ba731a543e5decab9e01e833b266dc6f93efbbdec1d11135deff8cb78110071535ef382c713a7
@@ -1,5 +1,18 @@
1
- # TimeBoots changelog
1
+ # TimeMath Changelog
2
2
 
3
- # 0.0.2 (2016-05-27)
3
+ # 0.0.5 (2016-06-25)
4
4
 
5
- * Add support for `DateTime`.
5
+ * Add optional second argument to rounding functions (`floor`, `ceil` and
6
+ so on), for "floor to 3-hour mark";
7
+ * Allow this argument, as well as in `advance`/`decrease`, to be non-integer;
8
+ so, you can do `hour.advance(tm, 1/2r)` now;
9
+ * Drop any `core_ext`s completely, even despite it was optional;
10
+ * Add `Op` chainable operations concept (and drop `Span`, which
11
+ is inferior to it);
12
+ * Redesign `Sequence` creation, allow include/exclude end;
13
+ * Add (experimental) resampling feature.
14
+
15
+ # 0.0.4 (2016-05-28)
16
+
17
+ * First "real" release with current name, `Time` and `DateTime` support,
18
+ proper documentation and stuff.
data/README.md CHANGED
@@ -11,23 +11,45 @@ arithmetics easier. It provides you with simple, easy-to-remember API, without
11
11
  any monkey-patching of core Ruby classes, so it can be used alongside
12
12
  Rails or without it, for any purpose.
13
13
 
14
+ ## Table Of Contents
15
+
16
+ * [Features](#features)
17
+ * [Naming](#naming)
18
+ * [Reasons](#reasons)
19
+ * [Installation](#installation)
20
+ * [Usage](#usage)
21
+ - [Full list of simple arithmetic methods](#full-list-of-simple-arithmetic-methods)
22
+ - [Set of operations as a value object](#set-of-operations-as-a-value-object)
23
+ - [Time sequence abstraction](#time-sequence-abstraction)
24
+ - [Measuring time periods](#measuring-time-periods)
25
+ - [Resampling](#resampling)
26
+ * [Notes on timezones](#notes-on-timezones)
27
+ * [Compatibility notes](#compatibility-notes)
28
+ * [Alternatives](#alternatives)
29
+ * [Links](#links)
30
+ * [Author](#author)
31
+ * [License](#license)
32
+
14
33
  ## Features
15
34
 
16
- * No monkey-patching of core classes (opt-in patching of Time and DateTime
17
- provided, though);
18
- * Works with Time and DateTime;
35
+ * No monkey-patching of core classes (now **strict**; previously existing opt-in
36
+ core ext removed in 0.0.5);
37
+ * Works with Time, Date and DateTime;
19
38
  * Accurately preserves timezone info;
20
39
  * Simple arithmetics: floor/ceil/round to any time unit (second, hour, year
21
40
  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.
41
+ * Chainable [operations](#set-of-operations-as-a-value-object), including
42
+ construction of "set of operations" value object (like "10:20 at next
43
+ month first day"), clean and powerful;
44
+ * Easy generation of [time sequences](#time-sequence-abstraction)
45
+ (like "each day from _this_ to _that_ date");
46
+ * Measuring of time distances between two timestamps in any units;
47
+ * Powerful and flexible [resampling](#resampling) of arbitrary time value
48
+ arrays/hashes into regular sequences.
27
49
 
28
50
  ## Naming
29
51
 
30
- `TimeMath` is the better name I know for the task library does, but
52
+ `TimeMath` is the best name I know for the task library does, yet
31
53
  it is [already taken](https://rubygems.org/gems/time_math). So, with no
32
54
  other thoughts I came with the ugly solution.
33
55
 
@@ -96,21 +118,55 @@ TimeMath.day.advance(Time.now, +10) # => 2016-06-07 14:06:57 +0300
96
118
  * `<unit>.range(tm, amount)` -- creates range of `tm ... tm + amount <units>`;
97
119
  * `<unit>.range_back(tm, amount)` -- creates range of `tm - amount <units> ... tm`.
98
120
 
121
+ **Things to note**:
122
+
123
+ * rounding methods (`floor`, `ceil` and company) support optional second
124
+ argument—amount of units to round to, like "each 3 hours": `hour.floor(tm, 3)`;
125
+ * both rounding and advance/decrease methods allow their last argument to
126
+ be float/rational, so you can `hour.advance(tm, 1/2r)` and this would
127
+ work as you may expect. Non-integer arguments are only supported for
128
+ units less than week (because "half of month" have no exact mathematical
129
+ sense).
130
+
99
131
  See also [Units::Base](http://www.rubydoc.info/gems/time_math2/TimeMath/Units/Base).
100
132
 
101
- ### Time span abstraction
133
+ ### Set of operations as a value object
102
134
 
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:
135
+ For example, you want "10 am at next monday". By using atomic time unit
136
+ operations, you'll need the code like:
105
137
 
106
138
  ```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
139
+ TimeMath.hour.advance(TimeMath.week.ceil(Time.now), 10)
111
140
  ```
141
+ ...which is not really readable, to say the least. So, `TimeMath` provides
142
+ one top-level method allowing to chain any operations you want:
143
+
144
+ ```ruby
145
+ TimeMath(Time.now).ceil(:week).advance(:hour, 10).call
146
+ ```
147
+
148
+ Much more readable, huh?
112
149
 
113
- See also [Span YARD docs](http://www.rubydoc.info/gems/time_math2/TimeMath/Span).
150
+ The best thing about it, that you can prepare "operations list" value
151
+ object, and then use it (or pass to methods, or
152
+ serialize to YAML and deserialize in some Sidekiq task and so on):
153
+
154
+ ```ruby
155
+ op = TimeMath().ceil(:week).advance(:hour, 10)
156
+ # => #<TimeMath::Op ceil(:week).advance(:hour, 10)>
157
+ op.call(Time.now)
158
+ # => 2016-06-27 10:00:00 +0300
159
+
160
+ # It also can be called on several arguments/array of arguments:
161
+ op.call(tm1, tm2, tm3)
162
+ op.call(array_of_timestamps)
163
+ # ...or even used as a block-ish object:
164
+ array_of_timestamps.map(&op)
165
+ ```
166
+
167
+ See also [TimeMath()](http://www.rubydoc.info/gems/time_math2/toplevel#TimeMath-instance_method)
168
+ and underlying [TimeMath::Op](http://www.rubydoc.info/gems/time_math2/TimeMath/Op)
169
+ class docs.
114
170
 
115
171
  ### Time sequence abstraction
116
172
 
@@ -122,8 +178,8 @@ to = Time.now
122
178
  # => 2016-05-28 17:47:30 +0300
123
179
  from = TimeMath.day.floor(to)
124
180
  # => 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)>
181
+ seq = TimeMath.hour.sequence(from...to)
182
+ # => #<TimeMath::Sequence(:hour, 2016-05-28 00:00:00 +0300...2016-05-28 17:47:30 +0300)>
127
183
  p(*seq)
128
184
  # 2016-05-28 00:00:00 +0300
129
185
  # 2016-05-28 01:00:00 +0300
@@ -136,11 +192,21 @@ p(*seq)
136
192
  # ...and so on
137
193
  ```
138
194
 
195
+ Note that sequence also play well with operation chain described above,
196
+ so you can
197
+
198
+ ```ruby
199
+ seq = TimeMath.day.sequence(Time.parse('2016-05-01')...Time.parse('2016-05-04')).advance(:hour, 10).decrease(:min, 5)
200
+ # => #<TimeMath::Sequence(:day, 2016-05-01 00:00:00 +0300...2016-05-04 00:00:00 +0300).advance(:hour, 10).decrease(:min, 5)>
201
+ seq.to_a
202
+ # => [2016-05-01 09:55:00 +0300, 2016-05-02 09:55:00 +0300, 2016-05-03 09:55:00 +0300]
203
+ ```
204
+
139
205
  See also [Sequence YARD docs](http://www.rubydoc.info/gems/time_math2/TimeMath/Sequence).
140
206
 
141
207
  ### Measuring time periods
142
208
 
143
- Simple measure: just "how many `<unit>`s from date A to date B:
209
+ Simple measure: just "how many `<unit>`s from date A to date B":
144
210
 
145
211
  ```ruby
146
212
  TimeMath.week.measure(Time.parse('2016-05-01'), Time.parse('2016-06-01'))
@@ -181,23 +247,53 @@ TimeMath.measure(birthday, Time.now, upto: :day)
181
247
  # => {:days=>12157, :hours=>2, :minutes=>26, :seconds=>55}
182
248
  ```
183
249
 
184
- ### Optional `Time` and `DateTime` patches
250
+ ### Resampling
251
+
252
+ **Resampling** is useful for situations when you have some timestamped
253
+ data (with variable holes between values), and wantto make it regular,
254
+ e.g. for charts drawing.
255
+
256
+ The most simple (and not very useful) resampling just turns array of
257
+ irregular timestamps into regular one:
258
+
259
+ ```ruby
260
+ dates = %w[2016-06-01 2016-06-03 2016-06-06].map(&Date.method(:parse))
261
+ # => [#<Date: 2016-06-01>, #<Date: 2016-06-03>, #<Date: 2016-06-06>]
262
+ TimeMath.day.resample(dates)
263
+ # => [#<Date: 2016-06-01>, #<Date: 2016-06-02>, #<Date: 2016-06-03>, #<Date: 2016-06-04>, #<Date: 2016-06-05>, #<Date: 2016-06-06>]
264
+ TimeMath.week.resample(dates)
265
+ # => [#<Date: 2016-05-30>, #<Date: 2016-06-06>]
266
+ TimeMath.month.resample(dates)
267
+ # => [#<Date: 2016-06-01>]
268
+ ```
185
269
 
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:
270
+ Much more useful is _hash resampling_: when you have a hash of `{timestamp => value}`
271
+ and...
189
272
 
190
273
  ```ruby
191
- require 'time_math/core_ext'
274
+ data = {Date.parse('2016-06-01') => 18, Date.parse('2016-06-03') => 8, Date.parse('2016-06-06') => -4}
275
+ # => {#<Date: 2016-06-01>=>18, #<Date: 2016-06-03>=>8, #<Date: 2016-06-06>=>-4}
276
+ TimeMath.day.resample(data)
277
+ # => {#<Date: 2016-06-01>=>[18], #<Date: 2016-06-02>=>[], #<Date: 2016-06-03>=>[8], #<Date: 2016-06-04>=>[], #<Date: 2016-06-05>=>[], #<Date: 2016-06-06>=>[-4]}
278
+ TimeMath.week.resample(data)
279
+ # => {#<Date: 2016-05-30>=>[18, 8], #<Date: 2016-06-06>=>[-4]}
280
+ TimeMath.month.resample(data)
281
+ # => {#<Date: 2016-06-01>=>[18, 8, -4]}
282
+ ```
283
+
284
+ For values grouping strategy, `resample` accepts symbol and block arguments:
192
285
 
193
- Time.now.decrease_by(:day, 10).floor_to(:month)
194
- Time.now.sequence_to(:month, Time.now.advance_by(:year, 5))
286
+ ```ruby
287
+ TimeMath.week.resample(data, :first)
288
+ # => {#<Date: 2016-05-30>=>18, #<Date: 2016-06-06>=>-4}
289
+ TimeMath.week.resample(data) { |vals| vals.inject(:+) }
290
+ => {#<Date: 2016-05-30>=>26, #<Date: 2016-06-06>=>-4}
195
291
  ```
196
292
 
197
- See [CoreExt](http://www.rubydoc.info/gems/time_math2/TimeMath/CoreExt)
198
- documentation for full lists of methods added.
293
+ The functionality currently considered experimental, please notify me
294
+ about your ideas and use cases via [GitHub issues](https://github.com/zverok/time_math2/issues)!
199
295
 
200
- ### Notes on timezones
296
+ ## Notes on timezones
201
297
 
202
298
  TimeMath tries its best to preserve timezones of original values. Currently,
203
299
  it means:
@@ -208,19 +304,18 @@ it means:
208
304
  it is preserved by TimeMath (but be careful about jumping around DST,
209
305
  offset would not change).
210
306
 
211
- ## Got it, what else?
307
+ ## Compatibility notes
212
308
 
213
- TimeMath also play well when included into other classes or modules:
309
+ TimeMath is known to work on MRI Ruby >= 1.9.
214
310
 
215
- ```ruby
216
- class MyModel
217
- include TimeMath
311
+ On JRuby it works, too, though there could be _slightly_ unexpected results,
312
+ when JRuby fails to create time by timezone name (see [bug](https://github.com/jruby/jruby/issues/3978)).
313
+ TimeMath in this case fallbacks to the same solution that used for `DateTime`,
314
+ and at least preserves utc offset.
218
315
 
219
- def next_day
220
- day.advance # Here!
221
- end
222
- end
223
- ```
316
+ On Rubinius, some of tests fail and I haven't time to investigate it. If
317
+ somebody still uses Rubinius and wants TimeMath to be working properly
318
+ on it, please let me know.
224
319
 
225
320
  ## Alternatives
226
321
 
@@ -1,10 +1,5 @@
1
1
  require 'time'
2
2
 
3
- require_relative './time_math/units'
4
- require_relative './time_math/sequence'
5
- require_relative './time_math/measure'
6
- require_relative './time_math/span'
7
-
8
3
  # TimeMath is a small library for easy time units arithmetics (like "floor
9
4
  # the timestamp to the nearest hour", "advance the time value by 3 days"
10
5
  # and so on).
@@ -21,10 +16,18 @@ require_relative './time_math/span'
21
16
  # time unit, which incapsulates most of the functionality. Refer to
22
17
  # {Units::Base} to see what you can get of it.
23
18
  #
19
+ # See also `TimeMath()` method in global namespace, it is lot of fun!
20
+ #
24
21
  module TimeMath
25
- # rubocop:disable Style/ModuleFunction
26
- extend self
27
- # rubocop:enable Style/ModuleFunction
22
+ require_relative './time_math/backports'
23
+ require_relative './time_math/units'
24
+ require_relative './time_math/op'
25
+ require_relative './time_math/sequence'
26
+ require_relative './time_math/measure'
27
+ require_relative './time_math/resamplers'
28
+ require_relative './time_math/util'
29
+
30
+ module_function
28
31
 
29
32
  # List all unit names known.
30
33
  #
@@ -71,7 +74,7 @@ module TimeMath
71
74
  # @return [Units::Base]
72
75
  #
73
76
  Units.names.each do |unit|
74
- define_method(unit) { Units.get(unit) }
77
+ define_singleton_method(unit) { Units.get(unit) }
75
78
  end
76
79
 
77
80
  # Measures distance between two time values in all units at once.
@@ -85,8 +88,8 @@ module TimeMath
85
88
  # # => {:years=>33, :months=>3, :weeks=>2, :days=>0, :hours=>1, :minutes=>25, :seconds=>52}
86
89
  # ```
87
90
  #
88
- # @param from [Time,DateTime]
89
- # @param to [Time,DateTime]
91
+ # @param from [Time,Date,DateTime]
92
+ # @param to [Time,Date,DateTime]
90
93
  # @param options [Hash] options
91
94
  # @option options [Boolean] :weeks pass `false` to exclude weeks from calculation;
92
95
  # @option options [Symbol] :upto pass max unit to use (e.g. if you'll
@@ -97,3 +100,39 @@ module TimeMath
97
100
  Measure.measure(from, to, options)
98
101
  end
99
102
  end
103
+
104
+ # This method helps to create time arithmetics sequence as a value object.
105
+ # Some examples:
106
+ #
107
+ # ```ruby
108
+ # # 10 am at first weekday? Easy!
109
+ # TimeMath(Time.now).floor(:week).advance(:hour, 10).call
110
+ # # => 2016-06-20 10:00:00 +0300
111
+ #
112
+ # # For several time values? Nothing easier!
113
+ # TimeMath(Time.local(2016,1,1), Time.local(2016,2,1), Time.local(2016,3,1)).floor(:week).advance(:hour, 10).call
114
+ # # => [2015-12-28 10:00:00 +0200, 2016-02-01 10:00:00 +0200, 2016-02-29 10:00:00 +0200]
115
+ #
116
+ # # Or, the most fun, you can create complicated operation and call it
117
+ # # later:
118
+ # op = TimeMath().floor(:week).advance(:hour, 10)
119
+ # # => #<TimeMath::Op floor(:week).advance(:hour, 10)>
120
+ # op.call(Time.now)
121
+ # # => 2016-06-20 10:00:00 +0300
122
+ #
123
+ # # or even as a lambda:
124
+ # times = [Time.local(2016,1,1), Time.local(2016,2,1), Time.local(2016,3,1)]
125
+ # times.map(&op)
126
+ # # => [2015-12-28 10:00:00 +0200, 2016-02-01 10:00:00 +0200, 2016-02-29 10:00:00 +0200]
127
+ # ```
128
+ #
129
+ # See also {TimeMath::Op} for list of operations available, but basically
130
+ # they are all same you can call on {TimeMath::Unit}, just pass unit symbol
131
+ # as a first argument.
132
+ #
133
+ # @param arguments time-y value, or list of them, or nothing
134
+ #
135
+ # @return [TimeMath::Op]
136
+ def TimeMath(*arguments) # rubocop:disable Style/MethodName
137
+ TimeMath::Op.new(*arguments)
138
+ end
@@ -0,0 +1,8 @@
1
+ # @private
2
+ class Array
3
+ if RUBY_VERSION < '2.1'
4
+ def to_h
5
+ Hash[*flatten(1)]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,209 @@
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 = [: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
+ '(' + [*@arguments].map(&:inspect).join(', ') + ').'
201
+ end
202
+
203
+ def perform(tm)
204
+ operations.inject(tm) { |memo, (op, unit, args)|
205
+ TimeMath::Units.get(unit).send(op, memo, *args)
206
+ }
207
+ end
208
+ end
209
+ end