time_math2 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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