Skip to content

feat: add comparison function for Duration #14472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions lib/elixir/lib/calendar/duration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,55 @@ defmodule Duration do
}
end

@doc """
Compares two durations.

Returns `:eq` if the durations are equal, `:lt` if the first duration is less than the second, and `:gt` if the first
duration is greater than the second.

## Examples

iex> Duration.compare(Duration.new!(second: 10), Duration.new!(second: 20))
:lt
iex> Duration.compare(Duration.new!(second: 20), Duration.new!(second: 10))
:gt
iex> Duration.compare(Duration.new!(second: 10), Duration.new!(second: 10))
:eq
"""
@doc since: "1.19.0"
@spec compare(t, t) :: :eq | :lt | :gt
def compare(%Duration{} = d1, %Duration{} = d2) do
d1_us = to_microseconds(d1)
d2_us = to_microseconds(d2)

cond do
d1_us == d2_us -> :eq
d1_us < d2_us -> :lt
d1_us > d2_us -> :gt
end
end

@second_in_us 1_000_000 # 1 second in microseconds
@minute_in_us @second_in_us * 60 # 1 minute = 60 seconds
@hour_in_us @minute_in_us * 60 # 1 hour = 60 minutes
@day_in_us @hour_in_us * 24 # 1 day = 24 hours
@week_in_us @day_in_us * 7 # 1 week = 7 days
@month_in_us @day_in_us * 30 # 1 month = 30 days
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can compare durations without knowing the date context since months are not always 30 days long.

As an example:

duration_a = Duration.new(month: 1)
duration_b = Duration.new(day: 30)

Duration.compare(duration_a, duration_b) # => :eq
Date.shift(~D[2020-02-01], duration_a)  #=> ~D[2020-03-01]
Date.shift(~D[2020-02-01], duration_b)  #=> ~D[2020-03-02]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And for DateTime.shift it might even depend on the timezone if there's DST changes.

Copy link
Contributor Author

@yordis yordis May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maennchen yeah I was wondering if year must be kept outside the comparing; the Kernel.to_timeout/1 already raise an exception based on month and year ... It would be the same with days 😭

Copy link
Member

@maennchen maennchen May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have something like this for convenience:

def compare(relative_to) do
  fn a, b ->
    # calc based on relative_to
  end
end

But essentially all you would be doing is:

[Naive]Date[Time].compare(
  [Naive]Date[Time].shift(relative_to, a),
  [Naive]Date[Time].shift(relative_to, b)
)

I'm wondering if the better course it to just add some docs pointing users to compare shifted dates instead of comparing durations directly and explain why.

@year_in_us @day_in_us * 365 # 1 year = 365 days

def to_microseconds(duration) do
{microsecond, _precision} = duration.microsecond

duration.year * @year_in_us +
duration.month * @month_in_us +
duration.week * @week_in_us +
duration.day * @day_in_us +
duration.hour * @hour_in_us +
duration.minute * @minute_in_us +
duration.second * @second_in_us +
microsecond
end

@doc """
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.

Expand Down