Class: Timerizer::Duration
- Inherits:
-
Object
- Object
- Timerizer::Duration
- Includes:
- Comparable
- Defined in:
- lib/timerizer/duration.rb,
lib/timerizer/duration/rounded_time.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.
Defined Under Namespace
Classes: RoundedTime
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'] } }, min_long: { units: { seconds: ['second', 'seconds'], minutes: ['minute', 'minutes'], hours: ['hour', 'hours'], days: ['day', 'days'], months: ['month', 'months'], years: ['year', 'years'] }, count: 2 } }
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_rounded_s(format = :min_long, options = nil) ⇒ String
Convert a Duration to a human-readable string using a rounded value.
-
#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.
160 161 162 163 164 165 166 167 168 169 |
# File 'lib/timerizer/duration.rb', line 160 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.
527 528 529 530 531 532 533 534 535 536 537 |
# File 'lib/timerizer/duration.rb', line 527 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
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 |
# File 'lib/timerizer/duration.rb', line 479 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.
504 505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/timerizer/duration.rb', line 504 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.
453 454 455 |
# File 'lib/timerizer/duration.rb', line 453 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.
553 554 555 556 557 558 559 560 561 562 563 |
# File 'lib/timerizer/duration.rb', line 553 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.
441 442 443 444 445 446 447 448 |
# File 'lib/timerizer/duration.rb', line 441 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.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/timerizer/duration.rb', line 234 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.
217 218 219 |
# File 'lib/timerizer/duration.rb', line 217 def ago self.before(Time.now) end |
#before(time) ⇒ Time
Returns the time ‘self` earlier than the given time.
207 208 209 |
# File 'lib/timerizer/duration.rb', line 207 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.
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/timerizer/duration.rb', line 410 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.
265 266 267 |
# File 'lib/timerizer/duration.rb', line 265 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.
184 185 186 187 188 189 190 191 192 |
# File 'lib/timerizer/duration.rb', line 184 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.
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'lib/timerizer/duration.rb', line 370 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`.
831 |
# File 'lib/timerizer/duration.rb', line 831 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`.
841 |
# File 'lib/timerizer/duration.rb', line 841 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`.
836 |
# File 'lib/timerizer/duration.rb', line 836 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`.
826 |
# File 'lib/timerizer/duration.rb', line 826 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`.
840 |
# File 'lib/timerizer/duration.rb', line 840 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`.
830 |
# File 'lib/timerizer/duration.rb', line 830 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`.
835 |
# File 'lib/timerizer/duration.rb', line 835 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`.
825 |
# File 'lib/timerizer/duration.rb', line 825 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`.
832 |
# File 'lib/timerizer/duration.rb', line 832 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`.
842 |
# File 'lib/timerizer/duration.rb', line 842 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`.
834 |
# File 'lib/timerizer/duration.rb', line 834 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`.
824 |
# File 'lib/timerizer/duration.rb', line 824 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`.
838 |
# File 'lib/timerizer/duration.rb', line 838 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`.
828 |
# File 'lib/timerizer/duration.rb', line 828 self.define_to_unit(:months) |
#to_rounded_s(format = :min_long, options = nil) ⇒ String
Convert a Duration to a human-readable string using a rounded value.
By ‘rounded’, we mean that the resulting value is rounded up if the input includes a value of more than half of one of the least-significant unit to be returned. For example, ‘(17.hours 43.minutes 31.seconds)`, when rounded to two units (hours and minutes), would return “17 hours, 44 minutes”. By contrast, `#to_s`, with a `:count` option of 2, would return a value of “17 hours, 43 minutes”: truncating, rather than rounding.
Note that this method overloads the meaning of the ‘:count` option value as documented below. If the passed-in option value is numeric, it will be honored, and rounding will take place to that number of units. If the value is either `:all` or the default `nil`, then rounding will be done to two units, and the rounded value will be passed on to `#to_s` with the options specified (which will result in a maximum of two time units being output).
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 |
# File 'lib/timerizer/duration.rb', line 701 def to_rounded_s(format = :min_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(Hash()) places = format[:count] begin places = Integer(places) # raise if nil or `:all` supplied as value rescue TypeError places = 2 end q = RoundedTime.call(self, places) q.to_s(format, ) end |
#to_s(format = :long, options = nil) ⇒ String
Convert a duration to a human-readable string.
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 644 645 646 647 648 649 650 651 652 653 |
# File 'lib/timerizer/duration.rb', line 606 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] || ' ' 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`.
833 |
# File 'lib/timerizer/duration.rb', line 833 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`.
823 |
# File 'lib/timerizer/duration.rb', line 823 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.
293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/timerizer/duration.rb', line 293 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.
333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'lib/timerizer/duration.rb', line 333 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.
572 573 574 575 |
# File 'lib/timerizer/duration.rb', line 572 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`.
837 |
# File 'lib/timerizer/duration.rb', line 837 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`.
827 |
# File 'lib/timerizer/duration.rb', line 827 self.define_to_unit(:weeks) |