Class: Timerizer::Duration
- Inherits:
-
Object
- Object
- Timerizer::Duration
- Includes:
- Comparable
- Defined in:
- lib/timerizer/duration.rb
Overview
Represents a duration of time. For example, ‘5 days’, ‘4 years’, and ‘5 years, 4 hours, 3 minutes, 2 seconds’ are all durations conceptually.
A ‘Duration` is made up of two different primitive units: seconds and months. The philosphy behind this is this: every duration of time can be broken down into these fundamental pieces, but cannot be simplified further. For example, 1 year always equals 12 months, 1 minute always equals 60 seconds, but 1 month does not always equal 30 days. This ignores some important corner cases (such as leap seconds), but this philosophy should be “good enough” for most use-cases.
This extra divide between “seconds” and “months” may seem useless or conter-intuitive at first, but can be useful when applying durations to times. For example, ‘1.year.after(Time.new(2000, 1, 1))` is guaranteed to return `Time.new(2001, 1, 1)`, which would not be possible if all durations were represented in seconds alone.
On top of that, even though 1 month cannot be exactly represented as a certain number of days, it’s still useful to often convert between durations made of different base units, especially when converting a ‘Duration` to a human-readable format. This is the reason for the #normalize and #denormalize methods. For convenience, most methods perform normalization on the input duration, so that some results or comparisons give more intuitive values.
Constant Summary collapse
- UNITS =
A hash describing the different base units of a ‘Duration`. Key represent unit names and values represent a hash describing the scale of that unit.
{ seconds: {seconds: 1}, minutes: {seconds: 60}, hours: {seconds: 60 * 60}, days: {seconds: 24 * 60 * 60}, weeks: {seconds: 7 * 24 * 60 * 60}, months: {months: 1}, years: {months: 12}, decades: {months: 12 * 10}, centuries: {months: 12 * 100}, millennia: {months: 12 * 1000} }
- UNIT_ALIASES =
A hash describing different names for various units, which allows for, e.g., pluralized unit names, or more obscure units. ‘UNIT_ALIASES` is guaranteed to also contain all of the entries from UNITS.
UNITS.merge( second: UNITS[:seconds], minute: UNITS[:minutes], hour: UNITS[:hours], day: UNITS[:days], week: UNITS[:weeks], month: UNITS[:months], year: UNITS[:years], decade: UNITS[:decades], century: UNITS[:centuries], millennium: UNITS[:millennia] )
- NORMALIZATION_METHODS =
The built-in set of normalization methods, usable with #normalize and #denormalize. Keys are method names, and values are hashes describing how units are normalized or denormalized.
The following normalization methods are defined:
-
‘:standard`: 1 month is approximated as 30 days, and 1 year is approximated as 365 days.
-
‘:minimum`: 1 month is approximated as 28 days (the minimum in any month), and 1 year is approximated as 365 days (the minimum in any year).
-
‘:maximum`: 1 month is approximated as 31 days (the maximum in any month), and 1 year is approximated as 366 days (the maximum in any year).
-
{ standard: { months: {seconds: 30 * 24 * 60 * 60}, years: {seconds: 365 * 24 * 60 * 60} }, minimum: { months: {seconds: 28 * 24 * 60 * 60}, years: {seconds: 365 * 24 * 60 * 60} }, maximum: { months: {seconds: 31 * 24 * 60 * 60}, years: {seconds: 366 * 24 * 60 * 60} } }
- FORMATS =
The built-in formats that can be used with #to_s.
The following string formats are defined:
-
‘:long`: The default, long-form string format. Example string: `“1 year, 2 months, 3 weeks, 4 days, 5 hours”`.
-
‘:short`: A shorter format, which includes 2 significant units by default. Example string: `“1mo 2d”`
-
‘:micro`: A very terse format, which includes only one significant unit by default. Example string: `“1h”`
-
{ micro: { units: { seconds: 's', minutes: 'm', hours: 'h', days: 'd', weeks: 'w', months: 'mo', years: 'y', }, separator: '', delimiter: ' ', count: 1 }, short: { units: { seconds: 'sec', minutes: 'min', hours: 'hr', days: 'd', weeks: 'wk', months: 'mo', years: 'yr' }, separator: '', delimiter: ' ', count: 2 }, long: { units: { seconds: ['second', 'seconds'], minutes: ['minute', 'minutes'], hours: ['hour', 'hours'], days: ['day', 'days'], weeks: ['week', 'weeks'], months: ['month', 'months'], years: ['year', 'years'] } } }
Instance Method Summary collapse
-
#*(other) ⇒ Duration
Multiply a duration by a scalar.
- #+(other) ⇒ Object
-
#-(other) ⇒ Duration
Subtract two durations.
-
#-@ ⇒ Duration
Negates a duration.
-
#/(other) ⇒ Duration
Divide a duration by a scalar.
-
#<=>(other) ⇒ Integer?
Compare two duartions.
-
#after(time) ⇒ Time
Returns the time ‘self` later than the given time.
-
#ago ⇒ Time
Return the time ‘self` later than the current time.
-
#before(time) ⇒ Time
Returns the time ‘self` earlier than the given time.
-
#denormalize(method: :standard) ⇒ Duration
Return a new duration that inverts an approximation made by #normalize.
-
#from_now ⇒ Time
Return the time ‘self` earlier than the current time.
-
#get(unit) ⇒ Integer
Return the number of “base” units in a Duration.
-
#initialize(units = {}) ⇒ Duration
constructor
Initialize a new instance of Duration.
-
#normalize(method: :standard) ⇒ Duration
Return a new duration that approximates the given input duration, where every “month-based” unit of the input is converted to seconds.
-
#to_centuries ⇒ Integer
Convert the duration to the given unit.
-
#to_century ⇒ Integer
Convert the duration to the given unit.
-
#to_day ⇒ Integer
Convert the duration to the given unit.
-
#to_days ⇒ Integer
Convert the duration to the given unit.
-
#to_decade ⇒ Integer
Convert the duration to the given unit.
-
#to_decades ⇒ Integer
Convert the duration to the given unit.
-
#to_hour ⇒ Integer
Convert the duration to the given unit.
-
#to_hours ⇒ Integer
Convert the duration to the given unit.
-
#to_millennia ⇒ Integer
Convert the duration to the given unit.
-
#to_millennium ⇒ Integer
Convert the duration to the given unit.
-
#to_minute ⇒ Integer
Convert the duration to the given unit.
-
#to_minutes ⇒ Integer
Convert the duration to the given unit.
-
#to_month ⇒ Integer
Convert the duration to the given unit.
-
#to_months ⇒ Integer
Convert the duration to the given unit.
-
#to_s(format = :long, options = nil) ⇒ String
Convert a duration to a human-readable string.
-
#to_second ⇒ Integer
Convert the duration to the given unit.
-
#to_seconds ⇒ Integer
NOTE: We need to manually spell out each unit with ‘define_to_unit` to get proper documentation for each method.
-
#to_unit(unit) ⇒ Integer
Convert the duration to a given unit.
-
#to_units(*units) ⇒ Hash<Symbol, Integer>
Convert the duration to a hash of units.
-
#to_wall ⇒ WallClock
Convert a duration to a WallClock.
-
#to_week ⇒ Integer
Convert the duration to the given unit.
-
#to_weeks ⇒ Integer
Convert the duration to the given unit.
-
#to_year ⇒ Integer
Convert the duration to the given unit.
-
#to_years ⇒ Integer
Convert the duration to the given unit.
Constructor Details
#initialize(units = {}) ⇒ Duration
Initialize a new instance of Timerizer::Duration.
149 150 151 152 153 154 155 156 157 158 |
# File 'lib/timerizer/duration.rb', line 149 def initialize(units = {}) @seconds = 0 @months = 0 units.each do |unit, n| unit_info = self.class.resolve_unit(unit) @seconds += n * unit_info.fetch(:seconds, 0) @months += n * unit_info.fetch(:months, 0) end end |
Instance Method Details
#*(other) ⇒ Duration
Multiply a duration by a scalar.
516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/timerizer/duration.rb', line 516 def *(other) case other when Integer Duration.new( seconds: @seconds * other, months: @months * other ) else raise ArgumentError, "Cannot multiply Duration #{self} by #{other.inspect}" end end |
#+(duration) ⇒ Duration #+(time) ⇒ Time
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 |
# File 'lib/timerizer/duration.rb', line 468 def +(other) case other when 0 self when Duration Duration.new( seconds: @seconds + other.get(:seconds), months: @months + other.get(:months) ) when Time self.after(other) else raise ArgumentError, "Cannot add #{other.inspect} to Duration #{self}" end end |
#-(other) ⇒ Duration
Subtract two durations.
493 494 495 496 497 498 499 500 501 502 503 504 505 |
# File 'lib/timerizer/duration.rb', line 493 def -(other) case other when 0 self when Duration Duration.new( seconds: @seconds - other.get(:seconds), months: @months - other.get(:months) ) else raise ArgumentError, "Cannot subtract #{other.inspect} from Duration #{self}" end end |
#-@ ⇒ Duration
Negates a duration.
442 443 444 |
# File 'lib/timerizer/duration.rb', line 442 def -@ Duration.new(seconds: -@seconds, months: -@months) end |
#/(other) ⇒ Duration
A duration can only be divided by an integer divisor. The resulting duration will have each component divided with integer division, which will result in truncation.
Divide a duration by a scalar.
542 543 544 545 546 547 548 549 550 551 552 |
# File 'lib/timerizer/duration.rb', line 542 def /(other) case other when Integer Duration.new( seconds: @seconds / other, months: @months / other ) else raise ArgumentError, "Cannot divide Duration #{self} by #{other.inspect}" end end |
#<=>(other) ⇒ Integer?
Compare two duartions. Note that durations are compared after normalization.
430 431 432 433 434 435 436 437 |
# File 'lib/timerizer/duration.rb', line 430 def <=>(other) case other when Duration self.to_unit(:seconds) <=> other.to_unit(:seconds) else nil end end |
#after(time) ⇒ Time
Returns the time ‘self` later than the given time.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/timerizer/duration.rb', line 223 def after(time) time = time.to_time prev_day = time.mday prev_month = time.month prev_year = time.year units = self.to_units(:years, :months, :days, :seconds) date_in_month = self.class.build_date( prev_year + units[:years], prev_month + units[:months], prev_day ) date = date_in_month + units[:days] Time.new( date.year, date.month, date.day, time.hour, time.min, time.sec ) + units[:seconds] end |
#ago ⇒ Time
Return the time ‘self` later than the current time.
206 207 208 |
# File 'lib/timerizer/duration.rb', line 206 def ago self.before(Time.now) end |
#before(time) ⇒ Time
Returns the time ‘self` earlier than the given time.
196 197 198 |
# File 'lib/timerizer/duration.rb', line 196 def before(time) (-self).after(time) end |
#denormalize(method: :standard) ⇒ Duration
Return a new duration that inverts an approximation made by #normalize. Denormalization results in a Timerizer::Duration where “second-based” units are converted back to “month-based” units. Note that, due to the lossy nature #normalize, the result of calling #normalize then #denormalize may result in a Timerizer::Duration that is not equal to the input.
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/timerizer/duration.rb', line 399 def denormalize(method: :standard) normalized_units = NORMALIZATION_METHODS.fetch(method).reverse_each initial = [0.seconds, self] result = normalized_units.reduce(initial) do |result, (unit, normal)| denormalized, remainder = result seconds_per_unit = normal.fetch(:seconds) remainder_seconds = remainder.get(:seconds) num_unit = self.class.div(remainder_seconds, seconds_per_unit) num_seconds_denormalized = num_unit * seconds_per_unit denormalized += Duration.new(unit => num_unit) remainder -= num_seconds_denormalized.seconds [denormalized, remainder] end denormalized, remainder = result denormalized + remainder end |
#from_now ⇒ Time
Return the time ‘self` earlier than the current time.
254 255 256 |
# File 'lib/timerizer/duration.rb', line 254 def from_now self.after(Time.now) end |
#get(unit) ⇒ Integer
Return the number of “base” units in a Timerizer::Duration. Note that this method is a lower-level method, and will not be needed by most users. See #to_unit for a more general equivalent.
173 174 175 176 177 178 179 180 181 |
# File 'lib/timerizer/duration.rb', line 173 def get(unit) if unit == :seconds @seconds elsif unit == :months @months else raise ArgumentError end end |
#normalize(method: :standard) ⇒ Duration
Return a new duration that approximates the given input duration, where every “month-based” unit of the input is converted to seconds. Because durations are composed of two distinct units (“seconds” and “months”), two durations need to be normalized before being compared. By default, most methods on Timerizer::Duration perform normalization or denormalization, so clients will not usually need to call this method directly.
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/timerizer/duration.rb', line 359 def normalize(method: :standard) normalized_units = NORMALIZATION_METHODS.fetch(method).reverse_each initial = [0.seconds, self] result = normalized_units.reduce(initial) do |result, (unit, normal)| normalized, remainder = result seconds_per_unit = normal.fetch(:seconds) unit_part = remainder.send(:to_unit_part, unit) normalized += (unit_part * seconds_per_unit).seconds remainder -= Duration.new(unit => unit_part) [normalized, remainder] end normalized, remainder = result normalized + remainder end |
#to_centuries ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:centuries`.
753 |
# File 'lib/timerizer/duration.rb', line 753 self.define_to_unit(:centuries) |
#to_century ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:century`.
763 |
# File 'lib/timerizer/duration.rb', line 763 self.define_to_unit(:century) |
#to_day ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:day`.
758 |
# File 'lib/timerizer/duration.rb', line 758 self.define_to_unit(:day) |
#to_days ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:days`.
748 |
# File 'lib/timerizer/duration.rb', line 748 self.define_to_unit(:days) |
#to_decade ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:decade`.
762 |
# File 'lib/timerizer/duration.rb', line 762 self.define_to_unit(:decade) |
#to_decades ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:decades`.
752 |
# File 'lib/timerizer/duration.rb', line 752 self.define_to_unit(:decades) |
#to_hour ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:hour`.
757 |
# File 'lib/timerizer/duration.rb', line 757 self.define_to_unit(:hour) |
#to_hours ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:hours`.
747 |
# File 'lib/timerizer/duration.rb', line 747 self.define_to_unit(:hours) |
#to_millennia ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:millennia`.
754 |
# File 'lib/timerizer/duration.rb', line 754 self.define_to_unit(:millennia) |
#to_millennium ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:millennium`.
764 |
# File 'lib/timerizer/duration.rb', line 764 self.define_to_unit(:millennium) |
#to_minute ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:minute`.
756 |
# File 'lib/timerizer/duration.rb', line 756 self.define_to_unit(:minute) |
#to_minutes ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:minutes`.
746 |
# File 'lib/timerizer/duration.rb', line 746 self.define_to_unit(:minutes) |
#to_month ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:month`.
760 |
# File 'lib/timerizer/duration.rb', line 760 self.define_to_unit(:month) |
#to_months ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:months`.
750 |
# File 'lib/timerizer/duration.rb', line 750 self.define_to_unit(:months) |
#to_s(format = :long, options = nil) ⇒ String
Convert a duration to a human-readable string.
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 |
# File 'lib/timerizer/duration.rb', line 595 def to_s(format = :long, = nil) format = case format when Symbol FORMATS.fetch(format) when Hash FORMATS.fetch(:long).merge(format) else raise ArgumentError, "Expected #{format.inspect} to be a Symbol or Hash" end format = format.merge( || {}) count = if format[:count].nil? || format[:count] == :all UNITS.count else format[:count] end format_units = format.fetch(:units) units = self.to_units(*format_units.keys).select {|unit, n| n > 0} if units.empty? units = {seconds: 0} end separator = format[:separator] || ' ' delimiter = format[:delimiter] || ', ' units.take(count).map do |unit, n| unit_label = format_units.fetch(unit) singular, plural = case unit_label when Array unit_label else [unit_label, unit_label] end unit_name = if n == 1 singular else plural || singular end [n, unit_name].join(separator) end.join(format[:delimiter] || ', ') end |
#to_second ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:second`.
755 |
# File 'lib/timerizer/duration.rb', line 755 self.define_to_unit(:second) |
#to_seconds ⇒ Integer
NOTE: We need to manually spell out each unit with ‘define_to_unit` to get proper documentation for each method. To ensure that we don’t miss any units, there’s a test in ‘duration_spec.rb` to ensure each of these methods actually exist. Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with `:seconds`.
745 |
# File 'lib/timerizer/duration.rb', line 745 self.define_to_unit(:seconds) |
#to_unit(unit) ⇒ Integer
The duration is normalized or denormalized first, depending on the unit requested. This means that, by default, the returned unit will be an approximation if it cannot be represented exactly by the duration, such as when converting a duration of months to seconds, or vice versa.
Convert the duration to a given unit.
282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/timerizer/duration.rb', line 282 def to_unit(unit) unit_details = self.class.resolve_unit(unit) if unit_details.has_key?(:seconds) seconds = self.normalize.get(:seconds) self.class.div(seconds, unit_details.fetch(:seconds)) elsif unit_details.has_key?(:months) months = self.denormalize.get(:months) self.class.div(months, unit_details.fetch(:months)) else raise "Unit should have key :seconds or :months" end end |
#to_units(*units) ⇒ Hash<Symbol, Integer>
The duration may be normalized or denormalized first, depending on the units requested. This behavior is identical to #to_unit.
Convert the duration to a hash of units. For each given unit argument, the returned hash will map the unit to the quantity of that unit present in the duration. Each returned unit will be truncated to an integer, and the remainder will “carry” to the next unit down. The resulting hash can be passed to #initialize to get the same result, so this method can be thought of as the inverse of #initialize.
322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/timerizer/duration.rb', line 322 def to_units(*units) sorted_units = self.class.sort_units(units).reverse _, parts = sorted_units.reduce([self, {}]) do |(remainder, parts), unit| part = remainder.to_unit(unit) new_remainder = remainder - Duration.new(unit => part) [new_remainder, parts.merge(unit => part)] end parts end |
#to_wall ⇒ WallClock
Convert a duration to a WallClock.
561 562 563 564 |
# File 'lib/timerizer/duration.rb', line 561 def to_wall raise WallClock::TimeOutOfBoundsError if @months > 0 WallClock.new(second: @seconds) end |
#to_week ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:week`.
759 |
# File 'lib/timerizer/duration.rb', line 759 self.define_to_unit(:week) |
#to_weeks ⇒ Integer
Convert the duration to the given unit. This is a helper that is equivalent to calling #to_unit with ‘:weeks`.
749 |
# File 'lib/timerizer/duration.rb', line 749 self.define_to_unit(:weeks) |