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.
@@ -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