I would like to convert a couple (start_at, end_at) into an iso 8601 duration, with only 1 periodicity set.
For example:
In case of ambiguity, I always want the bigger period to win.
For example:
I know it's really specific, but maybe someone has an idea on how to proceed? This seems like a problem people has already solved. The only resources I've found on the internet were converting a duration (in seconds) into iso 8601, but it can't work for some of the special cases described before (for example, the 4th one).
I would start by calculating the difference in (whole) days between the two dates:
d = (to_date - from_date).to_i
If everything else fails, we'd resort to this as Pd
D.
Weeks are always 7 days, regardless of months or leap years. We can determine a week-difference via divmod
:
w, r = d.divmod(7)
If the remainder r
is zero, the two dates are exactly w
weeks apart, or Pw
W.
Due to leap years and months having different number of days, I'd take a different approach for the remaining two cases. I would calculate the approximate number of whole months between the dates via: (30.436875 is the average number of days in a month when including leap years)
m = (d / 30.436875).round
And then check if this value happens to be an exact match:
from_date.next_month(m) == to_date
If so, we have Pm
M.
For years, I'd take a similar approach:
y = m / 12
and check via:
from_date.next_year(y) == to_date
Which gives Py
Y if true.
Everything in one method: (pure Ruby, no Rails)
def duration(from_date, to_date)
d = (to_date - from_date).to_i # number of days
m = (d / 30.436875).round # number of months
y = m / 12 # number of years
w, r = d.divmod(7) # number of weeks
return "P#{y}Y" if from_date.next_year(y) == to_date
return "P#{m}M" if from_date.next_month(m) == to_date
return "P#{w}W" if r == 0
"P#{d}D"
end
Examples:
duration(Date.parse('2023/03/01'), Date.parse('2023/03/10')) #=> "P9D"
duration(Date.parse('2023/03/31'), Date.parse('2023/04/28')) #=> "P4W"
duration(Date.parse('2023/03/31'), Date.parse('2023/04/29')) #=> "P29D"
duration(Date.parse('2023/03/31'), Date.parse('2023/04/30')) #=> "P1M"
duration(Date.parse('2024/02/29'), Date.parse('2025/02/28')) #=> "P1Y"
duration(Date.parse('2024/02/29'), Date.parse('2024/03/29')) #=> "P1M"
duration(Date.parse('2023/01/31'), Date.parse('2023/02/28')) #=> "P1M"