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.
@@ -0,0 +1,76 @@
1
+ module TimeMath
2
+ # @private
3
+ class Resampler
4
+ class << self
5
+ def call(name, array_or_hash, symbol = nil, &block)
6
+ if array_or_hash.is_a?(Array) && array_or_hash.all?(&Util.method(:timey?))
7
+ ArrayResampler.new(name, array_or_hash).call
8
+ elsif array_or_hash.is_a?(Hash) && array_or_hash.keys.all?(&Util.method(:timey?))
9
+ HashResampler.new(name, array_or_hash).call(symbol, &block)
10
+ else
11
+ raise ArgumentError, "Array of timestamps or hash with timestamp keys, #{array_or_hash} got"
12
+ end
13
+ end
14
+ end
15
+
16
+ def initialize(unit)
17
+ @unit = Units.get(unit)
18
+ end
19
+
20
+ private
21
+
22
+ def sequence
23
+ @sequence ||= @unit.sequence(from...to, expand: true)
24
+ end
25
+
26
+ def from
27
+ timestamps.min
28
+ end
29
+
30
+ def to
31
+ @unit.next(timestamps.max)
32
+ end
33
+ end
34
+
35
+ # @private
36
+ class ArrayResampler < Resampler
37
+ def initialize(unit, array)
38
+ super(unit)
39
+ @array = array
40
+ end
41
+
42
+ def call
43
+ sequence.to_a
44
+ end
45
+
46
+ private
47
+
48
+ def timestamps
49
+ @array
50
+ end
51
+ end
52
+
53
+ # @private
54
+ class HashResampler < Resampler
55
+ def initialize(unit, hash)
56
+ super(unit)
57
+ @hash = hash
58
+ end
59
+
60
+ def call(symbol = nil, &block)
61
+ block = symbol.to_proc if symbol && !block
62
+
63
+ sequence.ranges.map do |r|
64
+ values = @hash.select { |k, _| r.cover?(k) }.map(&:last)
65
+ values = block.call(values) if block # rubocop:disable Performance/RedundantBlockCall
66
+ [r.begin, values]
67
+ end.to_h
68
+ end
69
+
70
+ private
71
+
72
+ def timestamps
73
+ @hash.keys
74
+ end
75
+ end
76
+ end
@@ -8,27 +8,18 @@ module TimeMath
8
8
  # ```ruby
9
9
  # from = Time.parse('2016-05-01 13:30')
10
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)>
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
13
  # ```
14
14
  #
15
15
  # Now, you can use it:
16
+ #
16
17
  # ```ruby
17
18
  # seq.to_a
18
19
  # # => [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
  # ```
20
21
  # -- 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.
22
+ # the period start is the same as in `from`.
32
23
  #
33
24
  # You can expand from and to to nearest round unit by {#expand} method
34
25
  # or `:expand` option:
@@ -37,16 +28,23 @@ module TimeMath
37
28
  # seq.expand.to_a
38
29
  # # => [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
30
  # # 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)>
31
+ # seq = TimeMath.day.sequence(from...to, expand: true)
32
+ # # => #<TimeMath::Sequence(2016-05-01 00:00:00 +0300...2016-05-05 00:00:00 +0300)>
33
+ # seq.to_a
34
+ # # => [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]
35
+ # # ^ note that `to` is excluded.
36
+ # # You can include it by creating sequence from including-end range:
37
+ # seq = TimeMath.day.sequence(from..to, expand: true)
38
+ # # => #<TimeMath::Sequence(:day, 2016-05-01 00:00:00 +0300..2016-05-05 00:00:00 +0300)>
42
39
  # seq.to_a
40
+ # # => [2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300, 2016-05-05 00:00:00 +0300]
43
41
  # ```
44
42
  #
45
43
  # Besides each period beginning, you can also request pairs of begin/end
46
44
  # of a period, either as an array of arrays, or array of ranges:
47
45
  #
48
46
  # ```ruby
49
- # seq = TimeMath.day.sequence(from, to)
47
+ # seq = TimeMath.day.sequence(from...to)
50
48
  # seq.pairs
51
49
  # # => [[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
50
  # seq.ranges
@@ -56,47 +54,71 @@ module TimeMath
56
54
  # It is pretty convenient for filtering data from databases or APIs,
57
55
  # TimeMath creates list of filtering ranges in a blink.
58
56
  #
57
+ # Sequence also supports any item-updating operations in the same fashion
58
+ # {Op} does:
59
+ #
60
+ # ```ruby
61
+ # seq = TimeMath.day.sequence(from...to, expand: true).advance(:hour, 5).decrease(:min, 20)
62
+ # # => #<TimeMath::Sequence(:day, 2016-05-01 00:00:00 +0300...2016-05-05 00:00:00 +0300).advance(:hour, 5).decrease(:min, 20)>
63
+ # seq.to_a
64
+ # # => [2016-05-01 04:40:00 +0300, 2016-05-02 04:40:00 +0300, 2016-05-03 04:40:00 +0300, 2016-05-04 04:40:00 +0300]
65
+ # ```
66
+ #
59
67
  class Sequence
60
68
  # Creates a sequence. Typically, it is easier to to it with {Units::Base#sequence},
61
69
  # like this:
62
70
  #
63
71
  # ```ruby
64
- # TimeMath.day.sequence(from, to)
72
+ # TimeMath.day.sequence(from...to)
65
73
  # ```
66
74
  #
67
75
  # @param unit [Symbol] one of {TimeMath.units};
68
- # @param from [Time,DateTime] start of sequence;
69
- # @param to [Time,DateTime] upper limit of sequence;
76
+ # @param range [Range] range of time-y values (Time, Date, DateTime);
77
+ # note that range with inclusive and exclusive and will produce
78
+ # different sequences.
70
79
  # @param options [Hash]
71
80
  # @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.
81
+ # (`from` is floored and `to` is ceiled);
75
82
  #
76
- def initialize(unit, from, to, options = {})
83
+ def initialize(unit, range, options = {})
77
84
  @unit = Units.get(unit)
78
- @from, @to = from, to
85
+ @from, @to, @exclude_end = process_range(range)
79
86
  @options = options.dup
80
87
 
81
88
  expand! if options[:expand]
82
- @floor = options[:floor]
89
+ @op = Op.new
90
+ end
91
+
92
+ # @private
93
+ def initialize_copy(other)
94
+ @unit = other.unit
95
+ @from, @to, @exclude_end = other.from, other.to, other.exclude_end?
96
+ @op = other.op.dup
83
97
  end
84
98
 
85
- attr_reader :from, :to, :unit
99
+ attr_reader :from, :to, :unit, :op
86
100
 
87
- def ==(other)
101
+ # Compares two sequences, considering their start, end, unit and
102
+ # operations.
103
+ #
104
+ # @param other [Sequence]
105
+ # @return [Boolean]
106
+ def ==(other) # rubocop:disable Metrics/AbcSize
88
107
  self.class == other.class && unit == other.unit &&
89
- from == other.from && to == other.to
108
+ from == other.from && to == other.to &&
109
+ exclude_end? == other.exclude_end? &&
110
+ op == other.op
90
111
  end
91
112
 
92
- # If `:floor` option is set for sequence.
93
- def floor?
94
- @floor
113
+ # Whether sequence was created from exclude-end range (and, therefore,
114
+ # will exclude `to` when converted to array).
115
+ def exclude_end?
116
+ @exclude_end
95
117
  end
96
118
 
97
119
  # Expand sequence ends to nearest round unit.
98
120
  #
99
- # @return self
121
+ # @return [self]
100
122
  def expand!
101
123
  @from = unit.floor(from)
102
124
  @to = unit.ceil(to)
@@ -108,22 +130,104 @@ module TimeMath
108
130
  #
109
131
  # @return [Sequence]
110
132
  def expand
111
- dup.tap(&:expand!)
133
+ dup.expand!
112
134
  end
113
135
 
114
- # Sets sequence to floor all the intermediate values.
136
+ # @method floor!(unit, span = 1)
137
+ # Adds {Units::Base#floor} to list of operations to apply to sequence items.
115
138
  #
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.
139
+ # @param unit [Symbol] One of {TimeMath.units}
140
+ # @param span [Numeric] how many units to floor to.
141
+ # @return [self]
123
142
  #
124
- # @return [Sequence]
125
- def floor
126
- dup.tap(&:floor!)
143
+ # @method floor(unit, span = 1)
144
+ # Non-destructive version of {#floor!}.
145
+ # @param unit [Symbol] One of {TimeMath.units}
146
+ # @param span [Numeric] how many units to floor to.
147
+ # @return [Sequence]
148
+ #
149
+ # @method ceil!(unit, span = 1)
150
+ # Adds {Units::Base#ceil} to list of operations to apply to sequence items.
151
+ # @param unit [Symbol] One of {TimeMath.units}
152
+ # @param span [Numeric] how many units to ceil to.
153
+ # @return [self]
154
+ #
155
+ # @method ceil(unit, span = 1)
156
+ # Non-destructive version of {#ceil!}.
157
+ # @param unit [Symbol] One of {TimeMath.units}
158
+ # @param span [Numeric] how many units to ceil to.
159
+ # @return [Sequence]
160
+ #
161
+ # @method round!(unit, span = 1)
162
+ # Adds {Units::Base#round} to list of operations to apply to sequence items.
163
+ # @param unit [Symbol] One of {TimeMath.units}
164
+ # @param span [Numeric] how many units to round to.
165
+ # @return [self]
166
+ #
167
+ # @method round(unit, span = 1)
168
+ # Non-destructive version of {#round!}.
169
+ # @param unit [Symbol] One of {TimeMath.units}
170
+ # @param span [Numeric] how many units to round to.
171
+ # @return [Sequence]
172
+ #
173
+ # @method next!(unit, span = 1)
174
+ # Adds {Units::Base#next} to list of operations to apply to sequence items.
175
+ # @param unit [Symbol] One of {TimeMath.units}
176
+ # @param span [Numeric] how many units to ceil to.
177
+ # @return [self]
178
+ #
179
+ # @method next(unit, span = 1)
180
+ # Non-destructive version of {#next!}.
181
+ # @param unit [Symbol] One of {TimeMath.units}
182
+ # @param span [Numeric] how many units to ceil to.
183
+ # @return [Sequence]
184
+ #
185
+ # @method prev!(unit, span = 1)
186
+ # Adds {Units::Base#prev} to list of operations to apply to sequence items.
187
+ # @param unit [Symbol] One of {TimeMath.units}
188
+ # @param span [Numeric] how many units to floor to.
189
+ # @return [self]
190
+ #
191
+ # @method prev(unit, span = 1)
192
+ # Non-destructive version of {#prev!}.
193
+ # @param unit [Symbol] One of {TimeMath.units}
194
+ # @param span [Numeric] how many units to floor to.
195
+ # @return [Sequence]
196
+ #
197
+ # @method advance!(unit, amount = 1)
198
+ # Adds {Units::Base#advance} to list of operations to apply to sequence items.
199
+ # @param unit [Symbol] One of {TimeMath.units}
200
+ # @param amount [Numeric] how many units to advance.
201
+ # @return [self]
202
+ #
203
+ # @method advance(unit, amount = 1)
204
+ # Non-destructive version of {#advance!}.
205
+ # @param unit [Symbol] One of {TimeMath.units}
206
+ # @param amount [Numeric] how many units to advance.
207
+ # @return [Sequence]
208
+ #
209
+ # @method decrease!(unit, amount = 1)
210
+ # Adds {Units::Base#decrease} to list of operations to apply to sequence items.
211
+ # @param unit [Symbol] One of {TimeMath.units}
212
+ # @param amount [Numeric] how many units to decrease.
213
+ # @return [self]
214
+ #
215
+ # @method decrease(unit, amount = 1)
216
+ # Non-destructive version of {#decrease!}.
217
+ # @param unit [Symbol] One of {TimeMath.units}
218
+ # @param amount [Numeric] how many units to decrease.
219
+ # @return [Sequence]
220
+ #
221
+
222
+ Op::OPERATIONS.each do |operation|
223
+ define_method "#{operation}!" do |*arg|
224
+ @op.send("#{operation}!", *arg)
225
+ self
226
+ end
227
+
228
+ define_method operation do |*arg|
229
+ dup.send("#{operation}!", *arg)
230
+ end
127
231
  end
128
232
 
129
233
  # Creates an array of time unit starts between from and to. They will
@@ -139,10 +243,11 @@ module TimeMath
139
243
  while iter < to
140
244
  seq << iter
141
245
 
142
- iter = cond_floor(unit.advance(iter))
246
+ iter = unit.advance(iter)
143
247
  end
248
+ seq << to unless exclude_end?
144
249
 
145
- seq
250
+ op.call(seq)
146
251
  end
147
252
 
148
253
  # Creates an array of pairs (time unit start, time unit end) between
@@ -163,13 +268,18 @@ module TimeMath
163
268
  end
164
269
 
165
270
  def inspect
166
- "#<#{self.class}(#{from} - #{to})>"
271
+ ops = op.inspect_operations
272
+ ops = '.' + ops unless ops.empty?
273
+ "#<#{self.class}(#{unit.name.inspect}, #{from}#{exclude_end? ? '...' : '..'}#{to})#{ops}>"
167
274
  end
168
275
 
169
276
  private
170
277
 
171
- def cond_floor(tm)
172
- @floor ? unit.floor(tm) : tm
278
+ def process_range(range)
279
+ range.is_a?(Range) && Util.timey?(range.begin) && Util.timey?(range.end) or
280
+ raise ArgumentError, "Range of time-y values expected, #{range} got"
281
+
282
+ [range.begin, range.end, range.exclude_end?]
173
283
  end
174
284
  end
175
285
  end
@@ -8,6 +8,13 @@ module TimeMath
8
8
  # TimeMath.day.advance(tm, 5) # advances tm by 5 days
9
9
  # ```
10
10
  #
11
+ # See also {TimeMath::Op} for performing multiple operations in
12
+ # concise & DRY manner, like this:
13
+ #
14
+ # ```ruby
15
+ # TimeMath().advance(:day, 5).floor(:hour).advance(:min, 20).call(tm)
16
+ # ```
17
+ #
11
18
  class Base
12
19
  # Creates unit of time. Typically you don't need it, as it is
13
20
  # easier to do `TimeMath.day` or `TimeMath[:day]` to obtain it.
@@ -22,41 +29,59 @@ module TimeMath
22
29
  # Rounds `tm` down to nearest unit (this means, `TimeMath.day.floor(tm)`
23
30
  # will return beginning of `tm`-s day, and so on).
24
31
  #
25
- # @param tm [Time,DateTime] time value to floor.
26
- # @return [Time,DateTime] floored time value; class and timezone info
27
- # of origin would be preserved.
28
- def floor(tm)
29
- components = [tm.year,
30
- tm.month,
31
- tm.day,
32
- tm.hour,
33
- tm.min,
34
- tm.sec].first(index + 1)
35
-
36
- new_from_components(tm, *components)
32
+ # An optional second argument allows you to floor to arbitrary
33
+ # number of units, like to "each 3-hour" mark:
34
+ #
35
+ # ```ruby
36
+ # TimeMath.hour.floor(Time.parse('14:00'), 3)
37
+ # # => 2016-06-23 12:00:00 +0300
38
+ #
39
+ # # works well with float/rational spans
40
+ # TimeMath.hour.floor(Time.parse('14:15'), 1/2r)
41
+ # # => 2016-06-23 14:00:00 +0300
42
+ # TimeMath.hour.floor(Time.parse('14:45'), 1/2r)
43
+ # # => 2016-06-23 14:30:00 +0300
44
+ # ```
45
+ #
46
+ # @param tm [Time,Date,DateTime] time value to floor.
47
+ # @param span [Numeric] how many units to floor to. For units
48
+ # less than week supports float/rational values.
49
+ # @return [Time,Date,DateTime] floored time value; class and timezone
50
+ # info of origin would be preserved.
51
+ def floor(tm, span = 1)
52
+ int_floor = advance(floor_1(tm), (tm.send(name) / span.to_f).floor * span - tm.send(name))
53
+ float_fix(tm, int_floor, span % 1)
37
54
  end
38
55
 
39
56
  # Rounds `tm` up to nearest unit (this means, `TimeMath.day.ceil(tm)`
40
57
  # will return beginning of day next after `tm`, and so on).
58
+ # An optional second argument allows to ceil to arbitrary
59
+ # amount of units (see {#floor} for more detailed explanation).
41
60
  #
42
- # @param tm [Time,DateTime] time value to ceil.
43
- # @return [Time,DateTime] ceiled time value; class and timezone info
61
+ # @param tm [Time,Date,DateTime] time value to ceil.
62
+ # @param span [Numeric] how many units to ceil to. For units
63
+ # less than week supports float/rational values.
64
+ # @return [Time,Date,DateTime] ceiled time value; class and timezone info
44
65
  # of origin would be preserved.
45
- def ceil(tm)
46
- f = floor(tm)
66
+ def ceil(tm, span = 1)
67
+ f = floor(tm, span)
47
68
 
48
- f == tm ? f : advance(f)
69
+ f == tm ? f : advance(f, span)
49
70
  end
50
71
 
51
72
  # Rounds `tm` up or down to nearest unit (this means, `TimeMath.day.round(tm)`
52
73
  # will return beginning of `tm` day if `tm` is before noon, and
53
74
  # day next after `tm` if it is after, and so on).
75
+ # An optional second argument allows to round to arbitrary
76
+ # amount of units (see {#floor} for more detailed explanation).
54
77
  #
55
- # @param tm [Time,DateTime] time value to round.
56
- # @return [Time,DateTime] rounded time value; class and timezone info
78
+ # @param tm [Time,Date,DateTime] time value to round.
79
+ # @param span [Numeric] how many units to round to. For units
80
+ # less than week supports float/rational values.
81
+ # @return [Time,Date,DateTime] rounded time value; class and timezone info
57
82
  # of origin would be preserved.
58
- def round(tm)
59
- f, c = floor(tm), ceil(tm)
83
+ def round(tm, span = 1)
84
+ f, c = floor(tm, span), ceil(tm, span)
60
85
 
61
86
  (tm - f).abs < (tm - c).abs ? f : c
62
87
  end
@@ -64,41 +89,52 @@ module TimeMath
64
89
  # Like {#floor}, but always return value lower than `tm` (e.g. if
65
90
  # `tm` is exactly midnight, then `TimeMath.day.prev(tm)` will return
66
91
  # _previous midnight_).
92
+ # An optional second argument allows to floor to arbitrary
93
+ # amount of units (see {#floor} for more detailed explanation).
67
94
  #
68
- # @param tm [Time,DateTime] time value to calculate prev on.
69
- # @return [Time,DateTime] prev time value; class and timezone info
95
+ # @param tm [Time,Date,DateTime] time value to calculate prev on.
96
+ # @param span [Numeric] how many units to floor to. For units
97
+ # less than week supports float/rational values.
98
+ # @return [Time,Date,DateTime] prev time value; class and timezone info
70
99
  # of origin would be preserved.
71
- def prev(tm)
72
- f = floor(tm)
73
- f == tm ? decrease(f) : f
100
+ def prev(tm, span = 1)
101
+ f = floor(tm, span)
102
+ f == tm ? decrease(f, span) : f
74
103
  end
75
104
 
76
105
  # Like {#ceil}, but always return value greater than `tm` (e.g. if
77
106
  # `tm` is exactly midnight, then `TimeMath.day.next(tm)` will return
78
107
  # _next midnight_).
108
+ # An optional second argument allows to ceil to arbitrary
109
+ # amount of units (see {#floor} for more detailed explanation).
79
110
  #
80
- # @param tm [Time,DateTime] time value to calculate next on.
81
- # @return [Time,DateTime] next time value; class and timezone info
111
+ # @param tm [Time,Date,DateTime] time value to calculate next on.
112
+ # @param span [Numeric] how many units to ceil to. For units
113
+ # less than week supports float/rational values.
114
+ # @return [Time,Date,DateTime] next time value; class and timezone info
82
115
  # of origin would be preserved.
83
- def next(tm)
84
- c = ceil(tm)
85
- c == tm ? advance(c) : c
116
+ def next(tm, span = 1)
117
+ c = ceil(tm, span)
118
+ c == tm ? advance(c, span) : c
86
119
  end
87
120
 
88
121
  # Checks if `tm` is exactly rounded to unit.
89
122
  #
90
- # @param tm [Time,DateTime] time value to check.
123
+ # @param tm [Time,Date,DateTime] time value to check.
124
+ # @param span [Numeric] how many units to check round at. For units
125
+ # less than week supports float/rational values.
91
126
  # @return [Boolean] whether `tm` is exactly round to unit.
92
- def round?(tm)
93
- floor(tm) == tm
127
+ def round?(tm, span = 1)
128
+ floor(tm, span) == tm
94
129
  end
95
130
 
96
131
  # Advances `tm` by given amount of unit.
97
132
  #
98
- # @param tm [Time,DateTime] time value to advance;
99
- # @param amount [Integer] how many units forward to go.
133
+ # @param tm [Time,Date,DateTime] time value to advance;
134
+ # @param amount [Numeric] how many units forward to go. For units
135
+ # less than week supports float/rational values.
100
136
  #
101
- # @return [Time,DateTime] advanced time value; class and timezone info
137
+ # @return [Time,Date,DateTime] advanced time value; class and timezone info
102
138
  # of origin would be preserved.
103
139
  def advance(tm, amount = 1)
104
140
  return decrease(tm, -amount) if amount < 0
@@ -107,10 +143,11 @@ module TimeMath
107
143
 
108
144
  # Decreases `tm` by given amount of unit.
109
145
  #
110
- # @param tm [Time,DateTime] time value to decrease;
111
- # @param amount [Integer] how many units forward to go.
146
+ # @param tm [Time,Date,DateTime] time value to decrease;
147
+ # @param amount [Integer] how many units forward to go. For units
148
+ # less than week supports float/rational values.
112
149
  #
113
- # @return [Time,DateTime] decrease time value; class and timezone info
150
+ # @return [Time,Date,DateTime] decrease time value; class and timezone info
114
151
  # of origin would be preserved.
115
152
  def decrease(tm, amount = 1)
116
153
  return advance(tm, -amount) if amount < 0
@@ -125,7 +162,7 @@ module TimeMath
125
162
  # # => 2016-05-28 16:30:00 +0300...2016-06-02 16:30:00 +0300
126
163
  # ```
127
164
  #
128
- # @param tm [Time,DateTime] time value to create range from;
165
+ # @param tm [Time,Date,DateTime] time value to create range from;
129
166
  # @param amount [Integer] how many units should be between range
130
167
  # start and end.
131
168
  #
@@ -142,7 +179,7 @@ module TimeMath
142
179
  # # => 2016-05-23 16:30:00 +0300...2016-05-28 16:30:00 +0300
143
180
  # ```
144
181
  #
145
- # @param tm [Time,DateTime] time value to create range from;
182
+ # @param tm [Time,Date,DateTime] time value to create range from;
146
183
  # @param amount [Integer] how many units should be between range
147
184
  # start and end.
148
185
  #
@@ -153,8 +190,8 @@ module TimeMath
153
190
 
154
191
  # Measures distance between `from` and `to` in units of this class.
155
192
  #
156
- # @param from [Time,DateTime] start of period;
157
- # @param to [Time,DateTime] end of period.
193
+ # @param from [Time,Date,DateTime] start of period;
194
+ # @param to [Time,Date,DateTime] end of period.
158
195
  #
159
196
  # @return [Integer] how many full units are inside the period.
160
197
  # :nocov:
@@ -175,8 +212,8 @@ module TimeMath
175
212
  # # => [26, 2016-05-27 16:20:00 +0300]
176
213
  # ```
177
214
  #
178
- # @param from [Time,DateTime] start of period;
179
- # @param to [Time,DateTime] end of period.
215
+ # @param from [Time,Date,DateTime] start of period;
216
+ # @param to [Time,Date,DateTime] end of period.
180
217
  #
181
218
  # @return [Array<Integer, Time or DateTime>] how many full units
182
219
  # are inside the period; exact value of `from` + full units.
@@ -185,30 +222,12 @@ module TimeMath
185
222
  [m, advance(from, m)]
186
223
  end
187
224
 
188
- # Creates {Span} instance representing amount of units.
189
- #
190
- # Use it like this:
191
- #
192
- # ```ruby
193
- # span = TimeMath.day.span(5) # => #<TimeMath::Span(day): +5>
194
- # # now you can save this variable or path it to the methods...
195
- # # and then:
196
- # span.before(Time.parse('2016-05-01')) # => 2016-04-26 00:00:00 +0300
197
- # span.after(Time.parse('2016-05-01')) # => 2016-05-06 00:00:00 +0300
198
- # ```
199
- #
200
- # @param amount [Integer]
201
- # @return [Span]
202
- def span(amount = 1)
203
- TimeMath::Span.new(name, amount)
204
- end
205
-
206
225
  # Creates {Sequence} instance for producing all time units between
207
226
  # from and too. See {Sequence} class documentation for available
208
227
  # options and functionality.
209
228
  #
210
- # @param from [Time,DateTime] start of sequence;
211
- # @param to [Time,DateTime] upper limit of sequence;
229
+ # @param from [Time,Date,DateTime] start of sequence;
230
+ # @param to [Time,Date,DateTime] upper limit of sequence;
212
231
  # @param options [Hash]
213
232
  # @option options [Boolean] :expand round sequence ends on creation
214
233
  # (from is floored and to is ceiled);
@@ -216,8 +235,65 @@ module TimeMath
216
235
  # the intermediate values.
217
236
  #
218
237
  # @return [Sequence]
219
- def sequence(from, to, options = {})
220
- TimeMath::Sequence.new(name, from, to, options)
238
+ def sequence(range, options = {})
239
+ TimeMath::Sequence.new(name, range, options)
240
+ end
241
+
242
+ # Converts input timestamps list to regular list of timestamps
243
+ # over current unit.
244
+ #
245
+ # Like this:
246
+ #
247
+ # ```ruby
248
+ # times = [Time.parse('2016-05-01'), Time.parse('2016-05-03'), Time.parse('2016-05-08')]
249
+ # TimeMath.day.resample(times)
250
+ # # => => [2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300, 2016-05-05 00:00:00 +0300, 2016-05-06 00:00:00 +0300, 2016-05-07 00:00:00 +0300, 2016-05-08 00:00:00 +0300]
251
+ # ```
252
+ #
253
+ # The best way about resampling it also works for hashes with time
254
+ # keys. Like this:
255
+ #
256
+ # ```ruby
257
+ # h = {Date.parse('Wed, 01 Jun 2016')=>1, Date.parse('Tue, 07 Jun 2016')=>3, Date.parse('Thu, 09 Jun 2016')=>1}
258
+ # # => {#<Date: 2016-06-01>=>1, #<Date: 2016-06-07>=>3, #<Date: 2016-06-09>=>1}
259
+ #
260
+ # pp TimeMath.day.resample(h)
261
+ # # {#<Date: 2016-06-01>=>[1],
262
+ # # #<Date: 2016-06-02>=>[],
263
+ # # #<Date: 2016-06-03>=>[],
264
+ # # #<Date: 2016-06-04>=>[],
265
+ # # #<Date: 2016-06-05>=>[],
266
+ # # #<Date: 2016-06-06>=>[],
267
+ # # #<Date: 2016-06-07>=>[3],
268
+ # # #<Date: 2016-06-08>=>[],
269
+ # # #<Date: 2016-06-09>=>[1]}
270
+ #
271
+ # # The default resample just groups all related values in arrays
272
+ # # You can pass block or symbol, to have the values you need:
273
+ # pp TimeMath.day.resample(h,&:first)
274
+ # # {#<Date: 2016-06-01>=>1,
275
+ # # #<Date: 2016-06-02>=>nil,
276
+ # # #<Date: 2016-06-03>=>nil,
277
+ # # #<Date: 2016-06-04>=>nil,
278
+ # # #<Date: 2016-06-05>=>nil,
279
+ # # #<Date: 2016-06-06>=>nil,
280
+ # # #<Date: 2016-06-07>=>3,
281
+ # # #<Date: 2016-06-08>=>nil,
282
+ # # #<Date: 2016-06-09>=>1}
283
+ # ```
284
+ #
285
+ # @param array_or_hash array of time-y values (Time/Date/DateTime)
286
+ # or hash with time-y keys.
287
+ # @param symbol in case of first param being a hash -- method to
288
+ # call on key arrays while grouping.
289
+ # @param block in case of first param being a hash -- block to
290
+ # call on key arrays while grouping.
291
+ #
292
+ # @return array or hash spread regular by unit; if first param was
293
+ # hash, keys corresponding to each period are grouped into arrays;
294
+ # this array could be further processed with block/symbol provided.
295
+ def resample(array_or_hash, symbol = nil, &block)
296
+ Resampler.call(name, array_or_hash, symbol, &block)
221
297
  end
222
298
 
223
299
  def inspect
@@ -252,13 +328,51 @@ module TimeMath
252
328
  components = EMPTY_VALUES.zip(components).map { |d, c| c || d }
253
329
  case origin
254
330
  when Time
255
- Time.mktime(*components.reverse, nil, nil, nil, origin.zone)
331
+ res = Time.mktime(*components.reverse, nil, nil, nil, origin.zone)
332
+ fix_no_zone(res, origin, *components)
256
333
  when DateTime
257
334
  DateTime.new(*components, origin.zone)
335
+ when Date
336
+ Date.new(*components.first(3))
337
+ else
338
+ raise ArgumentError, "Expected Time, Date or DateTime, got #{origin.class}"
258
339
  end
259
340
  end
260
341
 
261
- include TimeMath # now we can use something like #day inside other units
342
+ def to_components(tm)
343
+ case tm
344
+ when Time, DateTime
345
+ [tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec]
346
+ when Date
347
+ [tm.year, tm.month, tm.day]
348
+ else
349
+ raise ArgumentError, "Expected Time, Date or DateTime, got #{tm.class}"
350
+ end
351
+ end
352
+
353
+ def floor_1(tm)
354
+ components = to_components(tm).first(index + 1)
355
+ new_from_components(tm, *components)
356
+ end
357
+
358
+ def float_fix(tm, floored, float_span_part)
359
+ if float_span_part.zero?
360
+ floored
361
+ else
362
+ float_floored = advance(floored, float_span_part)
363
+ float_floored > tm ? floored : float_floored
364
+ end
365
+ end
366
+
367
+ def fix_no_zone(tm, origin, *components)
368
+ if origin.zone != 'UTC' && tm.zone == 'UTC'
369
+ # Fixes things like this one: https://github.com/jruby/jruby/issues/3978
370
+ # ...by falling back to use of UTC offset instead of timezone abbr
371
+ Time.new(*components, origin.utc_offset)
372
+ else
373
+ tm
374
+ end
375
+ end
262
376
  end
263
377
  end
264
378
  end