Class: ActiveSupport::Duration
- Inherits:
-
Object
- Object
- ActiveSupport::Duration
- Defined in:
- lib/active_support/duration/change.rb,
lib/active_support/duration/change/version.rb
Defined Under Namespace
Modules: Change
Class Method Summary collapse
-
.from_parts(parts, normalize: true) ⇒ Object
(also: parse_parts)
Creates a new Duration from a Hash of parts (inverse of Duration#parts).
- .next_smaller_unit(unit) ⇒ Object
- .smaller_units(unit) ⇒ Object
- .units_largest_first ⇒ Object
Instance Method Summary collapse
-
#change(**changes) ⇒ Object
Replaces parts of duration with given part values.
-
#change_cascade(options) ⇒ Object
Changes the given part(s) of the duration and resets any smaller parts.
-
#normalize ⇒ Object
Re-builds the Duration using build(value).
-
#round(precision = smallest_unit, *args, **opts) ⇒ Object
Returns duration rounded to the nearest value having a precision of ‘precision`, which is a unit such as :hours, which would mean “round to the nearest hour”.
-
#smaller_parts(unit) ⇒ Object
Returns all parts than ‘unit` as a Hash that is a subset of self.parts.
-
#smaller_parts_to_fraction_of(unit) ⇒ Object
Convert the parts that are smaller than ‘unit` to be a fraction (Rational) of that `unit`.
- #smallest_part ⇒ Object
- #smallest_unit ⇒ Object
-
#truncate(precision = smallest_unit, *args, **opts) ⇒ Object
Truncates the Duration to the specified precision.
Class Method Details
.from_parts(parts, normalize: true) ⇒ Object Also known as: parse_parts
Creates a new Duration from a Hash of parts (inverse of Duration#parts).
Surprising that upstream ActiveSupport doesn’t provide this method
normalize: true (the default) changes 30.5m into 30m, 30s, for example.
11 12 13 14 15 16 17 18 19 |
# File 'lib/active_support/duration/change.rb', line 11 def from_parts(parts, normalize: true) parts = parts.compact.reject { |k, v| v.zero? } duration = new(calculate_total_seconds(parts), parts) if normalize duration.normalize else duration end end |
.next_smaller_unit(unit) ⇒ Object
28 29 30 31 |
# File 'lib/active_support/duration/change.rb', line 28 def next_smaller_unit(unit) i = PARTS.index(unit) or raise(ArgumentError, "unknown unit #{unit}") PARTS[i + 1] end |
.smaller_units(unit) ⇒ Object
33 34 35 36 37 |
# File 'lib/active_support/duration/change.rb', line 33 def smaller_units(unit) # The index of unit; we only want parts with indexes > this index unit_i = units_largest_first.index(unit) or raise(ArgumentError, "unknown unit #{unit}") units_largest_first.select.with_index { |key, i| i > unit_i } end |
.units_largest_first ⇒ Object
23 24 25 26 |
# File 'lib/active_support/duration/change.rb', line 23 def units_largest_first # Reverse since PARTS_IN_SECONDS is ordered smallest to largest PARTS_IN_SECONDS.keys.reverse.freeze end |
Instance Method Details
#change(**changes) ⇒ Object
Replaces parts of duration with given part values. Unlike #change_cascade and Time#change, only ever changes the given parts; it does not reset any smaller-unit parts.
53 54 55 56 57 58 |
# File 'lib/active_support/duration/change.rb', line 53 def change(**changes) self.class.from_parts( parts.merge(changes), normalize: false ) end |
#change_cascade(options) ⇒ Object
Changes the given part(s) of the duration and resets any smaller parts.
Similar to Time#change But note that the keys are plural, so :years instead of :year. Should we allow key aliases? Should we raise ArgumentError if key not recognized? Yes. (Why doesn’t Time#change?) or should this be named truncate? or change_reset_smaller_parts?
Returns a new Duration where one or more of the elements have been changed according to the options
parameter. The time options (:hour
, :min
, :sec
, :usec
, :nsec
) reset cascadingly, so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour and minute is passed, then sec, usec and nsec is set to 0. The options
parameter takes a hash with any of these keys: :year
, :month
, :day
, :hour
, :min
, :sec
, :usec
, :nsec
, :offset
. Pass either :usec
or :nsec
, not both.
Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)
Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0)
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/active_support/duration/change.rb', line 84 def change_cascade() .assert_valid_keys(*PARTS_IN_SECONDS, :nsec, :usec) reset = false new_parts = {} new_parts[:years] = .fetch(:years, parts[:years]) ; reset ||= .key?(:years) new_parts[:months] = .fetch(:months, reset ? 0 : parts[:months]) ; reset ||= .key?(:months) new_parts[:days] = .fetch(:days, reset ? 0 : parts[:days]) ; reset ||= .key?(:days) new_parts[:hours] = .fetch(:hours, reset ? 0 : parts[:hours]) ; reset ||= .key?(:hours) new_parts[:minutes] = .fetch(:minutes, reset ? 0 : parts[:minutes]); reset ||= .key?(:minutes) new_parts[:seconds] = .fetch(:seconds, reset ? 0 : parts[:seconds]) if new_nsec = [:nsec] raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{.inspect}" if [:usec] new_usec = Rational(new_nsec, 1000) else new_usec = nil # new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : # Rational(nsec, 1000)) end if new_usec raise ArgumentError, "argument out of range" if new_usec >= 1000000 new_parts[:seconds] += Rational(new_usec, 1000000) end self.class.from_parts( new_parts.compact.reject { |k, v| v.zero? }, normalize: false, ) end |
#normalize ⇒ Object
Re-builds the Duration using build(value). Useful if you may have “extra” seconds, minutes, etc. that could be carried over to the next higher unit, such as if you’ve built a Duration using Duration.seconds and a number of seconds > 60.
ActiveSupport::Duration.seconds(61).normalize
> 1 minute and 1 second
47 48 49 |
# File 'lib/active_support/duration/change.rb', line 47 def normalize Duration.build(value) end |
#round(precision = smallest_unit, *args, **opts) ⇒ Object
Returns duration rounded to the nearest value having a precision of ‘precision`, which is a unit such as :hours, which would mean “round to the nearest hour”. The smaller parts (:minutes and :seconds in this example) are turned into a fraction of the requested precision (:hours), which is then added to requested precision part. Finally, `round` is called on the requested precision part (hours in this example).
If optional [ndigits] [, half: mode] arguments are supplied, they are passed along to [round](ruby-doc.org/core/Float.html#method-i-round).
TODO raise ArgumentError if precision not recognized as a unit
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/active_support/duration/change.rb', line 138 def round(precision = smallest_unit, *args, **opts) #puts "Rounding #{parts.inspect} (in particular #{parts[precision]} #{precision}) to nearest #{precision.inspect}" new_part_value = orig_part_value = (parts[precision] || 0) fraction = smaller_parts_to_fraction_of(precision) # Usually fraction is in the range 0..1, unless the smaller units are overflowed (non-normalized) new_part_value += fraction #puts "Adding #{orig_part_value} + fraction parts #{fraction.inspect} (#{fraction.to_f}) = #{new_part_value} (#{new_part_value.to_f})" new_part_value = new_part_value.round(*args, **opts) change_cascade( precision => new_part_value ) end |
#smaller_parts(unit) ⇒ Object
Returns all parts than ‘unit` as a Hash that is a subset of self.parts.
For example, if ‘unit` is :hours and self is 1h 29m 60s, then it would return the parts smaller than hour, 29m 60s, as the hash { minutes: 29, seconds: 60 }.
180 181 182 |
# File 'lib/active_support/duration/change.rb', line 180 def smaller_parts(unit) parts.slice *ActiveSupport::Duration.smaller_units(unit) end |
#smaller_parts_to_fraction_of(unit) ⇒ Object
Convert the parts that are smaller than ‘unit` to be a fraction (Rational) of that `unit`.
For example, if ‘unit` is :hours and self is 1h 29m 60s, then it would look at the parts smaller than hour, 29m 60s, which is the same as 30m, and would convert that to a fraction of hours, which would be 30m/60m = 1/2r.
161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/active_support/duration/change.rb', line 161 def smaller_parts_to_fraction_of(unit) #next_smaller_unit = self.class.next_smaller_unit(unit) #next_smaller_unit_in_s = ActiveSupport::Duration::PARTS_IN_SECONDS[next_smaller_unit] # 1 if unit == :minutes #puts %(unit_in_s=#{(unit_in_s).inspect}, next_smaller_unit_in_s=#{(next_smaller_unit_in_s).inspect}) smaller_parts = smaller_parts(unit) numerator_s = ActiveSupport::Duration.send(:calculate_total_seconds, smaller_parts) denominator_s = ActiveSupport::Duration::PARTS_IN_SECONDS[unit] # 60 if unit == :minutes fraction = Rational(numerator_s, denominator_s) #puts "#{smaller_parts.inspect} converted to fraction #{numerator_s}/#{denominator_s} = #{fraction} (#{fraction.to_f})" fraction end |
#smallest_part ⇒ Object
184 185 186 |
# File 'lib/active_support/duration/change.rb', line 184 def smallest_part [parts.to_a.last].to_h end |
#smallest_unit ⇒ Object
188 189 190 |
# File 'lib/active_support/duration/change.rb', line 188 def smallest_unit parts.to_a.last[0] end |
#truncate(precision = smallest_unit, *args, **opts) ⇒ Object
Truncates the Duration to the specified precision. All smaller parts are discarded.
Similar to ruby-doc.org/core-2.7.1/Float.html#method-i-truncate
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/active_support/duration/change.rb', line 196 def truncate(precision = smallest_unit, *args, **opts) #puts %(Truncating #{parts.inspect} to #{precision.inspect}) # TODO: only use truncate here if :seconds or if part is Float ? # or just always pass them along, although they are probably only needed for :seconds part_value = (parts[precision] || 0) new_part_value = part_value.truncate(*args) change_cascade( precision => new_part_value ) end |