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 +4 -4
- data/CHANGELOG.md +16 -3
- data/README.md +135 -40
- data/lib/time_math.rb +50 -11
- data/lib/time_math/backports.rb +8 -0
- data/lib/time_math/op.rb +209 -0
- data/lib/time_math/resamplers.rb +76 -0
- data/lib/time_math/sequence.rb +159 -49
- data/lib/time_math/units/base.rb +184 -70
- data/lib/time_math/units/day.rb +2 -2
- data/lib/time_math/units/month.rb +4 -4
- data/lib/time_math/units/simple.rb +1 -1
- data/lib/time_math/units/week.rb +9 -6
- data/lib/time_math/units/year.rb +2 -2
- data/lib/time_math/util.rb +10 -0
- data/lib/time_math/version.rb +1 -1
- metadata +6 -4
- data/lib/time_math/core_ext.rb +0 -52
- data/lib/time_math/span.rb +0 -53
@@ -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
|
data/lib/time_math/sequence.rb
CHANGED
@@ -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
|
12
|
-
# # => #<TimeMath::Sequence(2016-05-01 13:30: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`.
|
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
|
41
|
-
# # => #<TimeMath::Sequence(2016-05-01 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
|
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
|
72
|
+
# TimeMath.day.sequence(from...to)
|
65
73
|
# ```
|
66
74
|
#
|
67
75
|
# @param unit [Symbol] one of {TimeMath.units};
|
68
|
-
# @param
|
69
|
-
#
|
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,
|
83
|
+
def initialize(unit, range, options = {})
|
77
84
|
@unit = Units.get(unit)
|
78
|
-
@from, @to =
|
85
|
+
@from, @to, @exclude_end = process_range(range)
|
79
86
|
@options = options.dup
|
80
87
|
|
81
88
|
expand! if options[:expand]
|
82
|
-
@
|
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
|
-
|
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
|
-
#
|
93
|
-
|
94
|
-
|
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.
|
133
|
+
dup.expand!
|
112
134
|
end
|
113
135
|
|
114
|
-
#
|
136
|
+
# @method floor!(unit, span = 1)
|
137
|
+
# Adds {Units::Base#floor} to list of operations to apply to sequence items.
|
115
138
|
#
|
116
|
-
#
|
117
|
-
|
118
|
-
|
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
|
-
# @
|
125
|
-
|
126
|
-
|
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 =
|
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
|
-
|
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
|
172
|
-
|
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
|
data/lib/time_math/units/base.rb
CHANGED
@@ -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
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
# @
|
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
|
-
# @
|
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
|
-
# @
|
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
|
-
# @
|
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 [
|
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(
|
220
|
-
TimeMath::Sequence.new(name,
|
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
|
-
|
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
|