timerizer 0.2.1 → 0.3.0
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/lib/timerizer.rb +70 -637
- data/lib/timerizer/core.rb +3 -0
- data/lib/timerizer/duration.rb +766 -0
- data/lib/timerizer/version.rb +3 -0
- data/lib/timerizer/wall_clock.rb +224 -0
- metadata +65 -5
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
module Timerizer
|
|
2
|
+
# Represents a duration of time. For example, '5 days', '4 years', and
|
|
3
|
+
# '5 years, 4 hours, 3 minutes, 2 seconds' are all durations conceptually.
|
|
4
|
+
#
|
|
5
|
+
# A `Duration` is made up of two different primitive units: seconds and
|
|
6
|
+
# months. The philosphy behind this is this: every duration of time
|
|
7
|
+
# can be broken down into these fundamental pieces, but cannot be simplified
|
|
8
|
+
# further. For example, 1 year always equals 12 months, 1 minute always
|
|
9
|
+
# equals 60 seconds, but 1 month does not always equal 30 days. This
|
|
10
|
+
# ignores some important corner cases (such as leap seconds), but this
|
|
11
|
+
# philosophy should be "good enough" for most use-cases.
|
|
12
|
+
#
|
|
13
|
+
# This extra divide between "seconds" and "months" may seem useless or
|
|
14
|
+
# conter-intuitive at first, but can be useful when applying durations to
|
|
15
|
+
# times. For example, `1.year.after(Time.new(2000, 1, 1))` is guaranteed
|
|
16
|
+
# to return `Time.new(2001, 1, 1)`, which would not be possible if all
|
|
17
|
+
# durations were represented in seconds alone.
|
|
18
|
+
#
|
|
19
|
+
# On top of that, even though 1 month cannot be _exactly_ represented as a
|
|
20
|
+
# certain number of days, it's still useful to often convert between durations
|
|
21
|
+
# made of different base units, especially when converting a `Duration` to a
|
|
22
|
+
# human-readable format. This is the reason for the {#normalize} and
|
|
23
|
+
# {#denormalize} methods. For convenience, most methods perform normalization
|
|
24
|
+
# on the input duration, so that some results or comparisons give more
|
|
25
|
+
# intuitive values.
|
|
26
|
+
class Duration
|
|
27
|
+
include Comparable
|
|
28
|
+
|
|
29
|
+
# A hash describing the different base units of a `Duration`. Key represent
|
|
30
|
+
# unit names and values represent a hash describing the scale of that unit.
|
|
31
|
+
UNITS = {
|
|
32
|
+
seconds: {seconds: 1},
|
|
33
|
+
minutes: {seconds: 60},
|
|
34
|
+
hours: {seconds: 60 * 60},
|
|
35
|
+
days: {seconds: 24 * 60 * 60},
|
|
36
|
+
weeks: {seconds: 7 * 24 * 60 * 60},
|
|
37
|
+
months: {months: 1},
|
|
38
|
+
years: {months: 12},
|
|
39
|
+
decades: {months: 12 * 10},
|
|
40
|
+
centuries: {months: 12 * 100},
|
|
41
|
+
millennia: {months: 12 * 1000}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# A hash describing different names for various units, which allows for,
|
|
45
|
+
# e.g., pluralized unit names, or more obscure units. `UNIT_ALIASES` is
|
|
46
|
+
# guaranteed to also contain all of the entries from {UNITS}.
|
|
47
|
+
UNIT_ALIASES = UNITS.merge(
|
|
48
|
+
second: UNITS[:seconds],
|
|
49
|
+
minute: UNITS[:minutes],
|
|
50
|
+
hour: UNITS[:hours],
|
|
51
|
+
day: UNITS[:days],
|
|
52
|
+
week: UNITS[:weeks],
|
|
53
|
+
month: UNITS[:months],
|
|
54
|
+
year: UNITS[:years],
|
|
55
|
+
decade: UNITS[:decades],
|
|
56
|
+
century: UNITS[:centuries],
|
|
57
|
+
millennium: UNITS[:millennia]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# The built-in set of normalization methods, usable with {#normalize} and
|
|
61
|
+
# {#denormalize}. Keys are method names, and values are hashes describing
|
|
62
|
+
# how units are normalized or denormalized.
|
|
63
|
+
#
|
|
64
|
+
# The following normalization methods are defined:
|
|
65
|
+
#
|
|
66
|
+
# - `:standard`: 1 month is approximated as 30 days, and 1 year is
|
|
67
|
+
# approximated as 365 days.
|
|
68
|
+
# - `:minimum`: 1 month is approximated as 28 days (the minimum in any
|
|
69
|
+
# month), and 1 year is approximated as 365 days (the minimum in any
|
|
70
|
+
# year).
|
|
71
|
+
# - `:maximum`: 1 month is approximated as 31 days (the maximum in any
|
|
72
|
+
# month), and 1 year is approximated as 366 days (the maximum in any
|
|
73
|
+
# year).
|
|
74
|
+
NORMALIZATION_METHODS = {
|
|
75
|
+
standard: {
|
|
76
|
+
months: {seconds: 30 * 24 * 60 * 60},
|
|
77
|
+
years: {seconds: 365 * 24 * 60 * 60}
|
|
78
|
+
},
|
|
79
|
+
minimum: {
|
|
80
|
+
months: {seconds: 28 * 24 * 60 * 60},
|
|
81
|
+
years: {seconds: 365 * 24 * 60 * 60}
|
|
82
|
+
},
|
|
83
|
+
maximum: {
|
|
84
|
+
months: {seconds: 31 * 24 * 60 * 60},
|
|
85
|
+
years: {seconds: 366 * 24 * 60 * 60}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# The built-in formats that can be used with {#to_s}.
|
|
90
|
+
#
|
|
91
|
+
# The following string formats are defined:
|
|
92
|
+
#
|
|
93
|
+
# - `:long`: The default, long-form string format. Example string:
|
|
94
|
+
# `"1 year, 2 months, 3 weeks, 4 days, 5 hours"`.
|
|
95
|
+
# - `:short`: A shorter format, which includes 2 significant units by
|
|
96
|
+
# default. Example string: `"1mo 2d"`
|
|
97
|
+
# - `:micro`: A very terse format, which includes only one significant unit
|
|
98
|
+
# by default. Example string: `"1h"`
|
|
99
|
+
FORMATS = {
|
|
100
|
+
micro: {
|
|
101
|
+
units: {
|
|
102
|
+
seconds: 's',
|
|
103
|
+
minutes: 'm',
|
|
104
|
+
hours: 'h',
|
|
105
|
+
days: 'd',
|
|
106
|
+
weeks: 'w',
|
|
107
|
+
months: 'mo',
|
|
108
|
+
years: 'y',
|
|
109
|
+
},
|
|
110
|
+
separator: '',
|
|
111
|
+
delimiter: ' ',
|
|
112
|
+
count: 1
|
|
113
|
+
},
|
|
114
|
+
short: {
|
|
115
|
+
units: {
|
|
116
|
+
seconds: 'sec',
|
|
117
|
+
minutes: 'min',
|
|
118
|
+
hours: 'hr',
|
|
119
|
+
days: 'd',
|
|
120
|
+
weeks: 'wk',
|
|
121
|
+
months: 'mo',
|
|
122
|
+
years: 'yr'
|
|
123
|
+
},
|
|
124
|
+
separator: '',
|
|
125
|
+
delimiter: ' ',
|
|
126
|
+
count: 2
|
|
127
|
+
},
|
|
128
|
+
long: {
|
|
129
|
+
units: {
|
|
130
|
+
seconds: ['second', 'seconds'],
|
|
131
|
+
minutes: ['minute', 'minutes'],
|
|
132
|
+
hours: ['hour', 'hours'],
|
|
133
|
+
days: ['day', 'days'],
|
|
134
|
+
weeks: ['week', 'weeks'],
|
|
135
|
+
months: ['month', 'months'],
|
|
136
|
+
years: ['year', 'years']
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Initialize a new instance of {Duration}.
|
|
142
|
+
#
|
|
143
|
+
# @param [Hash<Symbol, Integer>] units A hash that maps from unit names
|
|
144
|
+
# to the quantity of that unit. See the keys of {UNIT_ALIASES} for
|
|
145
|
+
# a list of valid unit names.
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# Timerizer::Duration.new(years: 4, months: 2, hours: 12, minutes: 60)
|
|
149
|
+
def initialize(units = {})
|
|
150
|
+
@seconds = 0
|
|
151
|
+
@months = 0
|
|
152
|
+
|
|
153
|
+
units.each do |unit, n|
|
|
154
|
+
unit_info = self.class.resolve_unit(unit)
|
|
155
|
+
@seconds += n * unit_info.fetch(:seconds, 0)
|
|
156
|
+
@months += n * unit_info.fetch(:months, 0)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Return the number of "base" units in a {Duration}. Note that this method
|
|
161
|
+
# is a lower-level method, and will not be needed by most users. See
|
|
162
|
+
# {#to_unit} for a more general equivalent.
|
|
163
|
+
#
|
|
164
|
+
# @param [Symbol] unit The base unit to return, either
|
|
165
|
+
# `:seconds` or `:months`.
|
|
166
|
+
#
|
|
167
|
+
# @return [Integer] The requested unit count. Note that this method does
|
|
168
|
+
# not perform normalization first, so results may not be intuitive.
|
|
169
|
+
#
|
|
170
|
+
# @raise [ArgumentError] The unit requested was not `:seconds` or `:months`.
|
|
171
|
+
#
|
|
172
|
+
# @see #to_unit
|
|
173
|
+
def get(unit)
|
|
174
|
+
if unit == :seconds
|
|
175
|
+
@seconds
|
|
176
|
+
elsif unit == :months
|
|
177
|
+
@months
|
|
178
|
+
else
|
|
179
|
+
raise ArgumentError
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns the time `self` earlier than the given time.
|
|
184
|
+
#
|
|
185
|
+
# @param [Time] time The initial time.
|
|
186
|
+
# @return [Time] The time before this {Duration} has elapsed past the
|
|
187
|
+
# given time.
|
|
188
|
+
#
|
|
189
|
+
# @example 5 minutes before January 1st, 2000 at noon
|
|
190
|
+
# 5.minutes.before(Time.new(2000, 1, 1, 12, 00, 00))
|
|
191
|
+
# # => 2000-01-01 11:55:00 -0800
|
|
192
|
+
#
|
|
193
|
+
# @see #ago
|
|
194
|
+
# @see #after
|
|
195
|
+
# @see #from_now
|
|
196
|
+
def before(time)
|
|
197
|
+
(-self).after(time)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Return the time `self` later than the current time.
|
|
201
|
+
#
|
|
202
|
+
# @return [Time] The time after this {Duration} has elapsed past the
|
|
203
|
+
# current system time.
|
|
204
|
+
#
|
|
205
|
+
# @see #before
|
|
206
|
+
def ago
|
|
207
|
+
self.before(Time.now)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Returns the time `self` later than the given time.
|
|
211
|
+
#
|
|
212
|
+
# @param [Time] time The initial time.
|
|
213
|
+
# @return [Time] The time after this {Duration} has elapsed past the
|
|
214
|
+
# given time.
|
|
215
|
+
#
|
|
216
|
+
# @example 5 minutes after January 1st, 2000 at noon
|
|
217
|
+
# 5.minutes.after(Time.new(2000, 1, 1, 12, 00, 00))
|
|
218
|
+
# # => 2000-01-01 12:05:00 -0800
|
|
219
|
+
#
|
|
220
|
+
# @see #ago
|
|
221
|
+
# @see #before
|
|
222
|
+
# @see #from_now
|
|
223
|
+
def after(time)
|
|
224
|
+
time = time.to_time
|
|
225
|
+
|
|
226
|
+
prev_day = time.mday
|
|
227
|
+
prev_month = time.month
|
|
228
|
+
prev_year = time.year
|
|
229
|
+
|
|
230
|
+
units = self.to_units(:years, :months, :days, :seconds)
|
|
231
|
+
|
|
232
|
+
date_in_month = self.class.build_date(
|
|
233
|
+
prev_year + units[:years],
|
|
234
|
+
prev_month + units[:months],
|
|
235
|
+
prev_day
|
|
236
|
+
)
|
|
237
|
+
date = date_in_month + units[:days]
|
|
238
|
+
|
|
239
|
+
Time.new(
|
|
240
|
+
date.year,
|
|
241
|
+
date.month,
|
|
242
|
+
date.day,
|
|
243
|
+
time.hour,
|
|
244
|
+
time.min,
|
|
245
|
+
time.sec
|
|
246
|
+
) + units[:seconds]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Return the time `self` earlier than the current time.
|
|
250
|
+
#
|
|
251
|
+
# @return [Time] The time current system time before this {Duration}.
|
|
252
|
+
#
|
|
253
|
+
# @see #before
|
|
254
|
+
def from_now
|
|
255
|
+
self.after(Time.now)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Convert the duration to a given unit.
|
|
259
|
+
#
|
|
260
|
+
# @param [Symbol] unit The unit to convert to. See {UNIT_ALIASES} for a list
|
|
261
|
+
# of valid unit names.
|
|
262
|
+
#
|
|
263
|
+
# @return [Integer] The quantity of the given unit present in `self`. Note
|
|
264
|
+
# that, if `self` cannot be represented exactly by `unit`, then the result
|
|
265
|
+
# will be truncated (rounded toward 0 instead of rounding down, unlike
|
|
266
|
+
# normal Ruby integer division).
|
|
267
|
+
#
|
|
268
|
+
# @raise ArgumentError if the given unit could not be resolved.
|
|
269
|
+
#
|
|
270
|
+
# @example
|
|
271
|
+
# 1.hour.to_unit(:minutes)
|
|
272
|
+
# # => 60
|
|
273
|
+
# 121.seconds.to_unit(:minutes)
|
|
274
|
+
# # => 2
|
|
275
|
+
#
|
|
276
|
+
# @note The duration is normalized or denormalized first, depending on the
|
|
277
|
+
# unit requested. This means that, by default, the returned unit will
|
|
278
|
+
# be an approximation if it cannot be represented exactly by the duration,
|
|
279
|
+
# such as when converting a duration of months to seconds, or vice versa.
|
|
280
|
+
#
|
|
281
|
+
# @see #to_units
|
|
282
|
+
def to_unit(unit)
|
|
283
|
+
unit_details = self.class.resolve_unit(unit)
|
|
284
|
+
|
|
285
|
+
if unit_details.has_key?(:seconds)
|
|
286
|
+
seconds = self.normalize.get(:seconds)
|
|
287
|
+
self.class.div(seconds, unit_details.fetch(:seconds))
|
|
288
|
+
elsif unit_details.has_key?(:months)
|
|
289
|
+
months = self.denormalize.get(:months)
|
|
290
|
+
self.class.div(months, unit_details.fetch(:months))
|
|
291
|
+
else
|
|
292
|
+
raise "Unit should have key :seconds or :months"
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Convert the duration to a hash of units. For each given unit argument,
|
|
297
|
+
# the returned hash will map the unit to the quantity of that unit present
|
|
298
|
+
# in the duration. Each returned unit will be truncated to an integer, and
|
|
299
|
+
# the remainder will "carry" to the next unit down. The resulting hash can
|
|
300
|
+
# be passed to {Duration#initialize} to get the same result, so this method
|
|
301
|
+
# can be thought of as the inverse of {Duration#initialize}.
|
|
302
|
+
#
|
|
303
|
+
# @param [Array<Symbol>] units The units to convert to. Each unit
|
|
304
|
+
# will correspond with a key in the returned hash.
|
|
305
|
+
#
|
|
306
|
+
# @return [Hash<Symbol, Integer>] A hash mapping each unit to the quantity
|
|
307
|
+
# of that unit. Note that whether the returned unit is plural, or uses
|
|
308
|
+
# an alias, depends on what unit was passed in as an argument.
|
|
309
|
+
#
|
|
310
|
+
# @note The duration may be normalized or denormalized first, depending
|
|
311
|
+
# on the units requested. This behavior is identical to {#to_unit}.
|
|
312
|
+
#
|
|
313
|
+
# @example
|
|
314
|
+
# 121.seconds.to_units(:minutes)
|
|
315
|
+
# # => {minutes: 2}
|
|
316
|
+
# 121.seconds.to_units(:minutes, :seconds)
|
|
317
|
+
# # => {minutes: 2, seconds: 1}
|
|
318
|
+
# 1.year.to_units(:days)
|
|
319
|
+
# # => {days: 365}
|
|
320
|
+
# (91.days 12.hours).to_units(:months, :hours)
|
|
321
|
+
# # => {months: 3, hours: 36}
|
|
322
|
+
def to_units(*units)
|
|
323
|
+
sorted_units = self.class.sort_units(units).reverse
|
|
324
|
+
|
|
325
|
+
_, parts = sorted_units.reduce([self, {}]) do |(remainder, parts), unit|
|
|
326
|
+
part = remainder.to_unit(unit)
|
|
327
|
+
new_remainder = remainder - Duration.new(unit => part)
|
|
328
|
+
|
|
329
|
+
[new_remainder, parts.merge(unit => part)]
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
parts
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Return a new duration that approximates the given input duration, where
|
|
336
|
+
# every "month-based" unit of the input is converted to seconds. Because
|
|
337
|
+
# durations are composed of two distinct units ("seconds" and "months"),
|
|
338
|
+
# two durations need to be normalized before being compared. By default,
|
|
339
|
+
# most methods on {Duration} perform normalization or denormalization, so
|
|
340
|
+
# clients will not usually need to call this method directly.
|
|
341
|
+
#
|
|
342
|
+
# @param [Symbol] method The normalization method to be used. For a list
|
|
343
|
+
# of normalization methods, see {NORMALIZATION_METHODS}.
|
|
344
|
+
#
|
|
345
|
+
# @return [Duration] The duration after being normalized.
|
|
346
|
+
#
|
|
347
|
+
# @example
|
|
348
|
+
# 1.month.normalize == 30.days
|
|
349
|
+
# 1.month.normalize(method: :standard) == 30.days
|
|
350
|
+
# 1.month.normalize(method: :maximum) == 31.days
|
|
351
|
+
# 1.month.normalize(method: :minimum) == 28.days
|
|
352
|
+
#
|
|
353
|
+
# 1.year.normalize == 365.days
|
|
354
|
+
# 1.year.normalize(method: :standard) == 365.days
|
|
355
|
+
# 1.year.normalize(method: :minimum) == 365.days
|
|
356
|
+
# 1.year.normalize(method: :maximum) == 366.days
|
|
357
|
+
#
|
|
358
|
+
# @see #denormalize
|
|
359
|
+
def normalize(method: :standard)
|
|
360
|
+
normalized_units = NORMALIZATION_METHODS.fetch(method).reverse_each
|
|
361
|
+
|
|
362
|
+
initial = [0.seconds, self]
|
|
363
|
+
result = normalized_units.reduce(initial) do |result, (unit, normal)|
|
|
364
|
+
normalized, remainder = result
|
|
365
|
+
|
|
366
|
+
seconds_per_unit = normal.fetch(:seconds)
|
|
367
|
+
unit_part = remainder.send(:to_unit_part, unit)
|
|
368
|
+
|
|
369
|
+
normalized += (unit_part * seconds_per_unit).seconds
|
|
370
|
+
remainder -= Duration.new(unit => unit_part)
|
|
371
|
+
[normalized, remainder]
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
normalized, remainder = result
|
|
375
|
+
normalized + remainder
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Return a new duration that inverts an approximation made by {#normalize}.
|
|
379
|
+
# Denormalization results in a {Duration} where "second-based" units are
|
|
380
|
+
# converted back to "month-based" units. Note that, due to the lossy nature
|
|
381
|
+
# {#normalize}, the result of calling {#normalize} then {#denormalize} may
|
|
382
|
+
# result in a {Duration} that is _not_ equal to the input.
|
|
383
|
+
#
|
|
384
|
+
# @param [Symbol] method The normalization method to invert. For a list of
|
|
385
|
+
# normalization methods, see {NORMALIZATION_METHODS}.
|
|
386
|
+
#
|
|
387
|
+
# @return [Duration] The duration after being denormalized.
|
|
388
|
+
#
|
|
389
|
+
# @example
|
|
390
|
+
# 30.days.denormalize == 1.month
|
|
391
|
+
# 30.days.denormalize(method: :standard) == 1.month
|
|
392
|
+
# 28.days.denormalize(method: :minimum) == 1.month
|
|
393
|
+
# 31.days.denormalize(method: :maximum) == 1.month
|
|
394
|
+
#
|
|
395
|
+
# 365.days.denormalize == 1.year
|
|
396
|
+
# 365.days.denormalize(method: :standard) == 1.year
|
|
397
|
+
# 365.days.denormalize(method: :minimum) == 1.year
|
|
398
|
+
# 366.days.denormalize(method: :maximum) == 1.year
|
|
399
|
+
def denormalize(method: :standard)
|
|
400
|
+
normalized_units = NORMALIZATION_METHODS.fetch(method).reverse_each
|
|
401
|
+
|
|
402
|
+
initial = [0.seconds, self]
|
|
403
|
+
result = normalized_units.reduce(initial) do |result, (unit, normal)|
|
|
404
|
+
denormalized, remainder = result
|
|
405
|
+
|
|
406
|
+
seconds_per_unit = normal.fetch(:seconds)
|
|
407
|
+
remainder_seconds = remainder.get(:seconds)
|
|
408
|
+
|
|
409
|
+
num_unit = self.class.div(remainder_seconds, seconds_per_unit)
|
|
410
|
+
num_seconds_denormalized = num_unit * seconds_per_unit
|
|
411
|
+
|
|
412
|
+
denormalized += Duration.new(unit => num_unit)
|
|
413
|
+
remainder -= num_seconds_denormalized.seconds
|
|
414
|
+
|
|
415
|
+
[denormalized, remainder]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
denormalized, remainder = result
|
|
419
|
+
denormalized + remainder
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Compare two duartions. Note that durations are compared after
|
|
423
|
+
# normalization.
|
|
424
|
+
#
|
|
425
|
+
# @param [Duration] other The duration to compare.
|
|
426
|
+
#
|
|
427
|
+
# @return [Integer, nil] 0 if the durations are equal, -1 if the left-hand
|
|
428
|
+
# side is greater, +1 if the right-hand side is greater. Returns `nil` if
|
|
429
|
+
# the duration cannot be compared ot `other`.
|
|
430
|
+
def <=>(other)
|
|
431
|
+
case other
|
|
432
|
+
when Duration
|
|
433
|
+
self.to_unit(:seconds) <=> other.to_unit(:seconds)
|
|
434
|
+
else
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Negates a duration.
|
|
440
|
+
#
|
|
441
|
+
# @return [Duration] A new duration where each component was negated.
|
|
442
|
+
def -@
|
|
443
|
+
Duration.new(seconds: -@seconds, months: -@months)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# @overload +(duration)
|
|
447
|
+
# Add together two durations.
|
|
448
|
+
#
|
|
449
|
+
# @param [Duration] duration The duration to add.
|
|
450
|
+
#
|
|
451
|
+
# @return [Duration] The resulting duration with each component added
|
|
452
|
+
# to the input duration.
|
|
453
|
+
#
|
|
454
|
+
# @example
|
|
455
|
+
# 1.day + 1.hour == 25.hours
|
|
456
|
+
#
|
|
457
|
+
# @overload +(time)
|
|
458
|
+
# Add a time to a duration, returning a new time.
|
|
459
|
+
#
|
|
460
|
+
# @param [Time] time The time to add this duration to.
|
|
461
|
+
#
|
|
462
|
+
# @return [Time] The time after the duration has elapsed.
|
|
463
|
+
#
|
|
464
|
+
# @example
|
|
465
|
+
# 1.day + Time.new(2000, 1, 1) == Time.new(2000, 1, 2)
|
|
466
|
+
#
|
|
467
|
+
# @see #after
|
|
468
|
+
def +(other)
|
|
469
|
+
case other
|
|
470
|
+
when 0
|
|
471
|
+
self
|
|
472
|
+
when Duration
|
|
473
|
+
Duration.new(
|
|
474
|
+
seconds: @seconds + other.get(:seconds),
|
|
475
|
+
months: @months + other.get(:months)
|
|
476
|
+
)
|
|
477
|
+
when Time
|
|
478
|
+
self.after(other)
|
|
479
|
+
else
|
|
480
|
+
raise ArgumentError, "Cannot add #{other.inspect} to Duration #{self}"
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Subtract two durations.
|
|
485
|
+
#
|
|
486
|
+
# @param [Duration] other The duration to subtract.
|
|
487
|
+
#
|
|
488
|
+
# @return [Duration] The resulting duration with each component subtracted
|
|
489
|
+
# from the input duration.
|
|
490
|
+
#
|
|
491
|
+
# @example
|
|
492
|
+
# 1.day - 1.hour == 23.hours
|
|
493
|
+
def -(other)
|
|
494
|
+
case other
|
|
495
|
+
when 0
|
|
496
|
+
self
|
|
497
|
+
when Duration
|
|
498
|
+
Duration.new(
|
|
499
|
+
seconds: @seconds - other.get(:seconds),
|
|
500
|
+
months: @months - other.get(:months)
|
|
501
|
+
)
|
|
502
|
+
else
|
|
503
|
+
raise ArgumentError, "Cannot subtract #{other.inspect} from Duration #{self}"
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Multiply a duration by a scalar.
|
|
508
|
+
#
|
|
509
|
+
# @param [Integer] other The scalar to multiply by.
|
|
510
|
+
#
|
|
511
|
+
# @return [Duration] The resulting duration with each component multiplied
|
|
512
|
+
# by the scalar.
|
|
513
|
+
#
|
|
514
|
+
# @example
|
|
515
|
+
# 1.day * 7 == 1.week
|
|
516
|
+
def *(other)
|
|
517
|
+
case other
|
|
518
|
+
when Integer
|
|
519
|
+
Duration.new(
|
|
520
|
+
seconds: @seconds * other,
|
|
521
|
+
months: @months * other
|
|
522
|
+
)
|
|
523
|
+
else
|
|
524
|
+
raise ArgumentError, "Cannot multiply Duration #{self} by #{other.inspect}"
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Divide a duration by a scalar.
|
|
529
|
+
#
|
|
530
|
+
# @param [Integer] other The scalar to divide by.
|
|
531
|
+
#
|
|
532
|
+
# @return [Duration] The resulting duration with each component divided by
|
|
533
|
+
# the scalar.
|
|
534
|
+
#
|
|
535
|
+
# @note A duration can only be divided by an integer divisor. The resulting
|
|
536
|
+
# duration will have each component divided with integer division, which
|
|
537
|
+
# will result in truncation.
|
|
538
|
+
#
|
|
539
|
+
# @example
|
|
540
|
+
# 1.week / 7 == 1.day
|
|
541
|
+
# 1.second / 2 == 0.seconds # This is a result of truncation
|
|
542
|
+
def /(other)
|
|
543
|
+
case other
|
|
544
|
+
when Integer
|
|
545
|
+
Duration.new(
|
|
546
|
+
seconds: @seconds / other,
|
|
547
|
+
months: @months / other
|
|
548
|
+
)
|
|
549
|
+
else
|
|
550
|
+
raise ArgumentError, "Cannot divide Duration #{self} by #{other.inspect}"
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Convert a duration to a {WallClock}.
|
|
555
|
+
#
|
|
556
|
+
# @return [WallClock] `self` as a {WallClock}
|
|
557
|
+
#
|
|
558
|
+
# @example
|
|
559
|
+
# (17.hours 30.minutes).to_wall
|
|
560
|
+
# # => 5:30:00 PM
|
|
561
|
+
def to_wall
|
|
562
|
+
raise WallClock::TimeOutOfBoundsError if @months > 0
|
|
563
|
+
WallClock.new(second: @seconds)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# Convert a duration to a human-readable string.
|
|
567
|
+
#
|
|
568
|
+
# @param [Symbol, Hash] format The format type to format the duration with.
|
|
569
|
+
# `format` can either be a key from the {FORMATS} hash or a hash with
|
|
570
|
+
# the same shape as `options`.
|
|
571
|
+
# @param [Hash, nil] options Additional options to use to override default
|
|
572
|
+
# format options.
|
|
573
|
+
#
|
|
574
|
+
# @option options [Hash<Symbol, String>] :units The full list of unit names
|
|
575
|
+
# to use. Keys are unit names (see {UNIT_ALIASES} for a full list) and
|
|
576
|
+
# values are strings to use when converting that unit to a string. Values
|
|
577
|
+
# can also be an array, where the first item of the array will be used
|
|
578
|
+
# for singular unit names and the second item will be used for plural
|
|
579
|
+
# unit names. Note that this option will completely override the input
|
|
580
|
+
# formats' list of names, so all units that should be used must be
|
|
581
|
+
# specified!
|
|
582
|
+
# @option options [String] :separator The separator to use between a unit
|
|
583
|
+
# quantity and the unit's name. For example, the string `"1 second"` uses
|
|
584
|
+
# a separator of `" "`.
|
|
585
|
+
# @option options [String] :delimiter The delimiter to use between separate
|
|
586
|
+
# units. For example, the string `"1 minute, 1 second"` uses a separator
|
|
587
|
+
# of `", "`
|
|
588
|
+
# @option options [Integer, nil, :all] :count The number of significant
|
|
589
|
+
# units to use in the string, or `nil` / `:all` to use all units.
|
|
590
|
+
# For example, if the given duration is `1.day 1.week 1.month`, and
|
|
591
|
+
# `options[:count]` is 2, then the resulting string will only include
|
|
592
|
+
# the month and the week components of the string.
|
|
593
|
+
#
|
|
594
|
+
# @return [String] The duration formatted as a string.
|
|
595
|
+
def to_s(format = :long, options = nil)
|
|
596
|
+
format =
|
|
597
|
+
case format
|
|
598
|
+
when Symbol
|
|
599
|
+
FORMATS.fetch(format)
|
|
600
|
+
when Hash
|
|
601
|
+
FORMATS.fetch(:long).merge(format)
|
|
602
|
+
else
|
|
603
|
+
raise ArgumentError, "Expected #{format.inspect} to be a Symbol or Hash"
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
format = format.merge(options || {})
|
|
607
|
+
|
|
608
|
+
count =
|
|
609
|
+
if format[:count].nil? || format[:count] == :all
|
|
610
|
+
UNITS.count
|
|
611
|
+
else
|
|
612
|
+
format[:count]
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
format_units = format.fetch(:units)
|
|
616
|
+
units = self.to_units(*format_units.keys).select {|unit, n| n > 0}
|
|
617
|
+
if units.empty?
|
|
618
|
+
units = {seconds: 0}
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
separator = format[:separator] || ' '
|
|
622
|
+
delimiter = format[:delimiter] || ', '
|
|
623
|
+
units.take(count).map do |unit, n|
|
|
624
|
+
unit_label = format_units.fetch(unit)
|
|
625
|
+
|
|
626
|
+
singular, plural =
|
|
627
|
+
case unit_label
|
|
628
|
+
when Array
|
|
629
|
+
unit_label
|
|
630
|
+
else
|
|
631
|
+
[unit_label, unit_label]
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
unit_name =
|
|
635
|
+
if n == 1
|
|
636
|
+
singular
|
|
637
|
+
else
|
|
638
|
+
plural || singular
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
[n, unit_name].join(separator)
|
|
642
|
+
end.join(format[:delimiter] || ', ')
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
private
|
|
646
|
+
|
|
647
|
+
# This method is like {#to_unit}, except it does not perform normalization
|
|
648
|
+
# first. Put another way, this method is essentially the same as {#to_unit}
|
|
649
|
+
# except it does not normalize the value first. It is similar to {#get}
|
|
650
|
+
# except that it can be used with non-primitive units as well.
|
|
651
|
+
#
|
|
652
|
+
# @example
|
|
653
|
+
# (1.year 1.month 365.days).to_unit_part(:month)
|
|
654
|
+
# # => 13
|
|
655
|
+
# # Returns 13 because that is the number of months contained exactly
|
|
656
|
+
# # within the sepcified duration. Since "days" cannot be translated
|
|
657
|
+
# # to an exact number of months, they *are not* factored into the result
|
|
658
|
+
# # at all.
|
|
659
|
+
#
|
|
660
|
+
# (25.months).to_unit_part(:year)
|
|
661
|
+
# # => 2
|
|
662
|
+
# # Returns 2 becasue that is the number of months contained exactly
|
|
663
|
+
# # within the specified duration. Since "years" is essentially an alias
|
|
664
|
+
# # for "12 months", months *are* factored into the result.
|
|
665
|
+
def to_unit_part(unit)
|
|
666
|
+
unit_details = self.class.resolve_unit(unit)
|
|
667
|
+
|
|
668
|
+
if unit_details.has_key?(:seconds)
|
|
669
|
+
seconds = self.get(:seconds)
|
|
670
|
+
self.class.div(seconds, unit_details.fetch(:seconds))
|
|
671
|
+
elsif unit_details.has_key?(:months)
|
|
672
|
+
months = self.get(:months)
|
|
673
|
+
self.class.div(months, unit_details.fetch(:months))
|
|
674
|
+
else
|
|
675
|
+
raise "Unit should have key :seconds or :months"
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def self.resolve_unit(unit)
|
|
680
|
+
UNIT_ALIASES[unit] or raise ArgumentError, "Unknown unit: #{unit.inspect}"
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def self.sort_units(units)
|
|
684
|
+
units.sort_by do |unit|
|
|
685
|
+
unit_info = self.resolve_unit(unit)
|
|
686
|
+
[unit_info.fetch(:months, 0), unit_info.fetch(:seconds, 0)]
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def self.mod_div(x, divisor)
|
|
691
|
+
modulo = x % divisor
|
|
692
|
+
[modulo, (x - modulo).to_i / divisor]
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Like the normal Ruby division operator, except it rounds towards 0 when
|
|
696
|
+
# dividing `Integer`s (instead of rounding down).
|
|
697
|
+
def self.div(x, divisor)
|
|
698
|
+
(x.to_f / divisor).to_i
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def self.month_carry(month)
|
|
702
|
+
month_offset, year_carry = self.mod_div(month - 1, 12)
|
|
703
|
+
[month_offset + 1, year_carry]
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# Create a date from a given year, month, and date. If the month is not in
|
|
707
|
+
# the range `1..12`, then the month will "wrap around", adjusting the given
|
|
708
|
+
# year accordingly (so a year of 2017 and a month of 0 corresponds with
|
|
709
|
+
# 12/2016, a year of 2017 and a month of 13 correpsonds with 1/2018, and so
|
|
710
|
+
# on). If the given day is out of range of the given month, then the
|
|
711
|
+
# date will be nudged back to the last day of the month.
|
|
712
|
+
def self.build_date(year, month, day)
|
|
713
|
+
new_month, year_carry = self.month_carry(month)
|
|
714
|
+
new_year = year + year_carry
|
|
715
|
+
|
|
716
|
+
if Date.valid_date?(new_year, new_month, day)
|
|
717
|
+
Date.new(new_year, new_month, day)
|
|
718
|
+
else
|
|
719
|
+
Date.new(new_year, new_month, -1)
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# @!macro [attach] define_to_unit
|
|
724
|
+
# @method to_$1
|
|
725
|
+
#
|
|
726
|
+
# Convert the duration to the given unit. This is a helper that
|
|
727
|
+
# is equivalent to calling {#to_unit} with `:$1`.
|
|
728
|
+
#
|
|
729
|
+
# @return [Integer] the quantity of the unit in the duration.
|
|
730
|
+
#
|
|
731
|
+
# @see #to_unit
|
|
732
|
+
def self.define_to_unit(unit)
|
|
733
|
+
define_method("to_#{unit}") do
|
|
734
|
+
self.to_unit(unit)
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
public
|
|
739
|
+
|
|
740
|
+
# NOTE: We need to manually spell out each unit with `define_to_unit` to
|
|
741
|
+
# get proper documentation for each method. To ensure that we don't miss
|
|
742
|
+
# any units, there's a test in `duration_spec.rb` to ensure each of these
|
|
743
|
+
# methods actually exist.
|
|
744
|
+
|
|
745
|
+
self.define_to_unit(:seconds)
|
|
746
|
+
self.define_to_unit(:minutes)
|
|
747
|
+
self.define_to_unit(:hours)
|
|
748
|
+
self.define_to_unit(:days)
|
|
749
|
+
self.define_to_unit(:weeks)
|
|
750
|
+
self.define_to_unit(:months)
|
|
751
|
+
self.define_to_unit(:years)
|
|
752
|
+
self.define_to_unit(:decades)
|
|
753
|
+
self.define_to_unit(:centuries)
|
|
754
|
+
self.define_to_unit(:millennia)
|
|
755
|
+
self.define_to_unit(:second)
|
|
756
|
+
self.define_to_unit(:minute)
|
|
757
|
+
self.define_to_unit(:hour)
|
|
758
|
+
self.define_to_unit(:day)
|
|
759
|
+
self.define_to_unit(:week)
|
|
760
|
+
self.define_to_unit(:month)
|
|
761
|
+
self.define_to_unit(:year)
|
|
762
|
+
self.define_to_unit(:decade)
|
|
763
|
+
self.define_to_unit(:century)
|
|
764
|
+
self.define_to_unit(:millennium)
|
|
765
|
+
end
|
|
766
|
+
end
|