Class: ActiveSupport::Duration

Inherits:
Object
  • Object
show all
Defined in:
lib/active_support/duration/change.rb,
lib/active_support/duration/change/version.rb

Defined Under Namespace

Modules: Change

Class Method Summary collapse

Instance Method Summary collapse

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_firstObject



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)

Examples:

(9.hours + 10.minutes + 40.seconds).change_cascade(hours: 12)  # => 12 hours
(9.hours + 10.minutes + 40.seconds).change_cascade(minutes: 5) # => 9 hours and 5 minutes


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(options)
  options.assert_valid_keys(*PARTS_IN_SECONDS, :nsec, :usec)

  reset = false
  new_parts = {}
  new_parts[:years]   = options.fetch(:years,               parts[:years])  ; reset ||= options.key?(:years)
  new_parts[:months]  = options.fetch(:months,  reset ? 0 : parts[:months]) ; reset ||= options.key?(:months)
  new_parts[:days]    = options.fetch(:days,    reset ? 0 : parts[:days])   ; reset ||= options.key?(:days)
  new_parts[:hours]   = options.fetch(:hours,   reset ? 0 : parts[:hours])  ; reset ||= options.key?(:hours)
  new_parts[:minutes] = options.fetch(:minutes, reset ? 0 : parts[:minutes]); reset ||= options.key?(:minutes)
  new_parts[:seconds] = options.fetch(:seconds, reset ? 0 : parts[:seconds])

  if new_nsec = options[:nsec]
    raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[: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

#normalizeObject

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

Examples:

30.seconds.round(:minutes)        #=> 1 minute
89.seconds.round(:minutes)        #=> 1 minute
90.seconds.round(:minutes)        #=> 2 minutes
(1.hour + 30.seconds).round(:minutes)  #=> 1 hour and 1 minute

2.5.seconds.round                 #=> 3 seconds
2.5.seconds.round(half: :down)    #=> 2 seconds


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_partObject



184
185
186
# File 'lib/active_support/duration/change.rb', line 184

def smallest_part
  [parts.to_a.last].to_h
end

#smallest_unitObject



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