lib/active_support/duration.rb in activesupport-6.1.7.10 vs lib/active_support/duration.rb in activesupport-7.0.0.alpha1

- old
+ new

@@ -9,11 +9,11 @@ # Provides accurate date and time measurements using Date#advance and # Time#advance, respectively. It mainly supports the methods on Numeric. # # 1.month.ago # equivalent to Time.now.advance(months: -1) class Duration - class Scalar < Numeric #:nodoc: + class Scalar < Numeric # :nodoc: attr_reader :value delegate :to_i, :to_f, :to_s, to: :value def initialize(value) @value = value @@ -37,39 +37,39 @@ end end def +(other) if Duration === other - seconds = value + other.parts.fetch(:seconds, 0) - new_parts = other.parts.merge(seconds: seconds) + seconds = value + other._parts.fetch(:seconds, 0) + new_parts = other._parts.merge(seconds: seconds) new_value = value + other.value - Duration.new(new_value, new_parts) + Duration.new(new_value, new_parts, other.variable?) else calculate(:+, other) end end def -(other) if Duration === other - seconds = value - other.parts.fetch(:seconds, 0) - new_parts = other.parts.transform_values(&:-@) + seconds = value - other._parts.fetch(:seconds, 0) + new_parts = other._parts.transform_values(&:-@) new_parts = new_parts.merge(seconds: seconds) new_value = value - other.value - Duration.new(new_value, new_parts) + Duration.new(new_value, new_parts, other.variable?) else calculate(:-, other) end end def *(other) if Duration === other - new_parts = other.parts.transform_values { |other_value| value * other_value } + new_parts = other._parts.transform_values { |other_value| value * other_value } new_value = value * other.value - Duration.new(new_value, new_parts) + Duration.new(new_value, new_parts, other.variable?) else calculate(:*, other) end end @@ -87,10 +87,14 @@ else calculate(:%, other) end end + def variable? # :nodoc: + false + end + private def calculate(op, other) if Scalar === other Scalar.new(value.public_send(op, other.value)) elsif Numeric === other @@ -121,12 +125,13 @@ months: SECONDS_PER_MONTH, years: SECONDS_PER_YEAR }.freeze PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze + VARIABLE_PARTS = [:years, :months, :weeks, :days].freeze - attr_accessor :value, :parts + attr_reader :value autoload :ISO8601Parser, "active_support/duration/iso8601_parser" autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer" class << self @@ -138,42 +143,42 @@ def parse(iso8601duration) parts = ISO8601Parser.new(iso8601duration).parse! new(calculate_total_seconds(parts), parts) end - def ===(other) #:nodoc: + def ===(other) # :nodoc: other.is_a?(Duration) rescue ::NoMethodError false end - def seconds(value) #:nodoc: - new(value, seconds: value) + def seconds(value) # :nodoc: + new(value, { seconds: value }, false) end - def minutes(value) #:nodoc: - new(value * SECONDS_PER_MINUTE, minutes: value) + def minutes(value) # :nodoc: + new(value * SECONDS_PER_MINUTE, { minutes: value }, false) end - def hours(value) #:nodoc: - new(value * SECONDS_PER_HOUR, hours: value) + def hours(value) # :nodoc: + new(value * SECONDS_PER_HOUR, { hours: value }, false) end - def days(value) #:nodoc: - new(value * SECONDS_PER_DAY, days: value) + def days(value) # :nodoc: + new(value * SECONDS_PER_DAY, { days: value }, true) end - def weeks(value) #:nodoc: - new(value * SECONDS_PER_WEEK, weeks: value) + def weeks(value) # :nodoc: + new(value * SECONDS_PER_WEEK, { weeks: value }, true) end - def months(value) #:nodoc: - new(value * SECONDS_PER_MONTH, months: value) + def months(value) # :nodoc: + new(value * SECONDS_PER_MONTH, { months: value }, true) end - def years(value) #:nodoc: - new(value * SECONDS_PER_YEAR, years: value) + def years(value) # :nodoc: + new(value * SECONDS_PER_YEAR, { years: value }, true) end # Creates a new Duration from a seconds value that is converted # to the individual parts: # @@ -184,40 +189,55 @@ unless value.is_a?(::Numeric) raise TypeError, "can't build an #{self.name} from a #{value.class.name}" end parts = {} - remainder_sign = value <=> 0 - remainder = value.round(9).abs + remainder = value.round(9) + variable = false PARTS.each do |part| unless part == :seconds part_in_seconds = PARTS_IN_SECONDS[part] - parts[part] = remainder.div(part_in_seconds) * remainder_sign + parts[part] = remainder.div(part_in_seconds) remainder %= part_in_seconds + + unless parts[part].zero? + variable ||= VARIABLE_PARTS.include?(part) + end end end unless value == 0 - parts[:seconds] = remainder * remainder_sign + parts[:seconds] = remainder - new(value, parts) + new(value, parts, variable) end private def calculate_total_seconds(parts) parts.inject(0) do |total, (part, value)| total + value * PARTS_IN_SECONDS[part] end end end - def initialize(value, parts) #:nodoc: + def initialize(value, parts, variable = nil) # :nodoc: @value, @parts = value, parts @parts.reject! { |k, v| v.zero? } unless value == 0 + @parts.freeze + @variable = variable + + if @variable.nil? + @variable = @parts.any? { |part, _| VARIABLE_PARTS.include?(part) } + end end - def coerce(other) #:nodoc: + # Returns a copy of the parts hash that defines the duration + def parts + @parts.dup + end + + def coerce(other) # :nodoc: case other when Scalar [other, self] when Duration [Scalar.new(other.value), self] @@ -238,17 +258,17 @@ # Adds another Duration or a Numeric to this Duration. Numeric values # are treated as seconds. def +(other) if Duration === other - parts = @parts.merge(other.parts) do |_key, value, other_value| + parts = @parts.merge(other._parts) do |_key, value, other_value| value + other_value end - Duration.new(value + other.value, parts) + Duration.new(value + other.value, parts, @variable || other.variable?) else seconds = @parts.fetch(:seconds, 0) + other - Duration.new(value + other, @parts.merge(seconds: seconds)) + Duration.new(value + other, @parts.merge(seconds: seconds), @variable) end end # Subtracts another Duration or a Numeric from this Duration. Numeric # values are treated as seconds. @@ -257,26 +277,26 @@ end # Multiplies this Duration by a Numeric and returns a new Duration. def *(other) if Scalar === other || Duration === other - Duration.new(value * other.value, parts.transform_values { |number| number * other.value }) + Duration.new(value * other.value, @parts.transform_values { |number| number * other.value }, @variable || other.variable?) elsif Numeric === other - Duration.new(value * other, parts.transform_values { |number| number * other }) + Duration.new(value * other, @parts.transform_values { |number| number * other }, @variable) else raise_type_error(other) end end # Divides this Duration by a Numeric and returns a new Duration. def /(other) if Scalar === other - Duration.new(value / other.value, parts.transform_values { |number| number / other.value }) + Duration.new(value / other.value, @parts.transform_values { |number| number / other.value }, @variable) elsif Duration === other value / other.value elsif Numeric === other - Duration.new(value / other, parts.transform_values { |number| number / other }) + Duration.new(value / other, @parts.transform_values { |number| number / other }, @variable) else raise_type_error(other) end end @@ -290,19 +310,19 @@ else raise_type_error(other) end end - def -@ #:nodoc: - Duration.new(-value, parts.transform_values(&:-@)) + def -@ # :nodoc: + Duration.new(-value, @parts.transform_values(&:-@), @variable) end - def +@ #:nodoc: + def +@ # :nodoc: self end - def is_a?(klass) #:nodoc: + def is_a?(klass) # :nodoc: Duration == klass || value.is_a?(klass) end alias :kind_of? :is_a? def instance_of?(klass) # :nodoc: @@ -418,46 +438,54 @@ sum(-1, time) end alias :until :ago alias :before :ago - def inspect #:nodoc: - return "#{value} seconds" if parts.empty? + def inspect # :nodoc: + return "#{value} seconds" if @parts.empty? - parts. + @parts. sort_by { |unit, _ | PARTS.index(unit) }. map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }. - to_sentence(locale: ::I18n.default_locale) + to_sentence(locale: false) end - def as_json(options = nil) #:nodoc: + def as_json(options = nil) # :nodoc: to_i end - def init_with(coder) #:nodoc: + def init_with(coder) # :nodoc: initialize(coder["value"], coder["parts"]) end - def encode_with(coder) #:nodoc: + def encode_with(coder) # :nodoc: coder.map = { "value" => @value, "parts" => @parts } end # Build ISO 8601 Duration string for this duration. # The +precision+ parameter can be used to limit seconds' precision of duration. def iso8601(precision: nil) ISO8601Serializer.new(self, precision: precision).serialize end + def variable? # :nodoc: + @variable + end + + def _parts # :nodoc: + @parts + end + private def sum(sign, time = ::Time.current) unless time.acts_like?(:time) || time.acts_like?(:date) raise ::ArgumentError, "expected a time or date, got #{time.inspect}" end - if parts.empty? + if @parts.empty? time.since(sign * value) else - parts.inject(time) do |t, (type, number)| + @parts.inject(time) do |t, (type, number)| if type == :seconds t.since(sign * number) elsif type == :minutes t.since(sign * number * 60) elsif type == :hours