1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activesupport/lib/active_support/duration/iso8601_serializer.rb
Vipul A M fdfac8760f
Although libraries support both formats of sign before and after DIGITS(ex: https://github.com/moment/luxon/pull/683, https://github.com/moment/moment/issues/2408), many do not.
For example PG refers to https://www.ietf.org/rfc/rfc3339.txt when converting(Ref: https://www.postgresql.org/docs/current/datatype-datetime.html)

According to the ref there is no explicit mention of allowing sign before the parts, which reads as below:

 Durations:

    dur-second        = 1*DIGIT "S"
    dur-minute        = 1*DIGIT "M" [dur-second]
    dur-hour          = 1*DIGIT "H" [dur-minute]
    dur-time          = "T" (dur-hour / dur-minute / dur-second)
    dur-day           = 1*DIGIT "D"
    dur-week          = 1*DIGIT "W"
    dur-month         = 1*DIGIT "M" [dur-day]
    dur-year          = 1*DIGIT "Y" [dur-month]
    dur-date          = (dur-day / dur-month / dur-year) [dur-time]

    duration          = "P" (dur-date / dur-time / dur-week)

We should not attempt to move sign forward in this case.
2020-11-02 20:41:48 +00:00

59 lines
2 KiB
Ruby

# frozen_string_literal: true
require "active_support/core_ext/object/blank"
module ActiveSupport
class Duration
# Serializes duration to string according to ISO 8601 Duration format.
class ISO8601Serializer # :nodoc:
DATE_COMPONENTS = %i(years months days)
def initialize(duration, precision: nil)
@duration = duration
@precision = precision
end
# Builds and returns output string.
def serialize
parts = normalize
return "PT0S" if parts.empty?
output = +"P"
output << "#{parts[:years]}Y" if parts.key?(:years)
output << "#{parts[:months]}M" if parts.key?(:months)
output << "#{parts[:days]}D" if parts.key?(:days)
output << "#{parts[:weeks]}W" if parts.key?(:weeks)
time = +""
time << "#{parts[:hours]}H" if parts.key?(:hours)
time << "#{parts[:minutes]}M" if parts.key?(:minutes)
if parts.key?(:seconds)
time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
end
output << "T#{time}" unless time.empty?
output
end
private
# Return pair of duration's parts and whole duration sign.
# Parts are summarized (as they can become repetitive due to addition, etc).
# Zero parts are removed as not significant.
# If all parts are negative it will negate all of them and return minus as a sign.
def normalize
parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
p[k] += v unless v.zero?
end
# Convert weeks to days and remove weeks if mixed with date parts
if week_mixed_with_date?(parts)
parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
end
parts
end
def week_mixed_with_date?(parts)
parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
end
end
end
end