Packages
ex_cldr_calendars
1.3.0
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.26.4
1.26.3
1.26.2
1.26.1
1.26.0
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
retired
1.23.1
1.23.0
1.22.1
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.18.0
1.17.3
1.17.2
1.17.1
1.17.0
1.17.0-rc.3
1.17.0-rc.2
1.17.0-rc.1
1.17.0-rc.0
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.1
1.14.0
1.13.0
1.13.0-rc.1
1.13.0-rc.0
1.12.1
1.12.0
1.11.0
1.11.0-rc.0
1.10.1
1.10.0
1.9.0
1.8.1
1.8.0
1.8.0-rc.0
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.0
retired
1.1.0
retired
1.0.0
retired
0.9.0
retired
0.8.0
retired
0.7.0
retired
0.6.0
retired
0.5.0
retired
0.4.1
0.4.0
retired
0.3.0
retired
0.2.0
retired
0.1.0
retired
Localized month- and week-based calendars and calendar functions based upon CLDR data.
Retired package: Deprecated - Deprecated
Current section
Files
Jump to
Current section
Files
lib/cldr/calendar/duration.ex
defmodule Cldr.Calendar.Duration do
@moduledoc """
Functions to create and format a difference between
two dates, times or datetimes.
The difference between two dates (or times or datetimes) is
usually defined in terms of days or seconds.
A duration is calculated as the difference in time in calendar
units: years, months, days, hours, minutes, seconds and microseconds.
This is useful to support formatting a string for users in
easy-to-understand terms. For example `11 months, 3 days and 4 minutes`
is a lot easier to understand than `28771440` seconds.
The package [ex_cldr_units](https://hex.pm/packages/ex_cldr_units) can
be optionally configured to provide localized formatting of durations.
If configured, the following providers should be configured in the
appropriate CLDR backend module. For example:
```elixir
defmodule MyApp.Cldr do
use Cldr,
locales: ["en", "ja"],
providers: [Cldr.Calendar, Cldr.Number, Cldr.Unit, Cldr.List]
end
```
"""
@struct_list [year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0]
@keys Keyword.keys(@struct_list)
defstruct @struct_list
@typedoc "Measure a duration in calendar units"
@type t :: %__MODULE__{
year: non_neg_integer(),
month: non_neg_integer(),
day: non_neg_integer(),
hour: non_neg_integer(),
minute: non_neg_integer(),
second: non_neg_integer(),
microsecond: non_neg_integer(),
}
@typedoc "A date, time or datetime"
@type date_or_datetime :: Calendar.date() | Calendar.time() | Calendar.datetime()
@microseconds_in_second 1_000_000
@microseconds_in_day 86_400_000_000
defimpl String.Chars do
def to_string(duration) do
Cldr.Calendar.Duration.to_string!(duration)
end
end
if Code.ensure_loaded?(Cldr.Unit) do
@doc """
Returns a string formatted representation of
a duration.
Note that time units that are zero are ommitted
from the output.
Formatting is
## Arguments
* `duration` is a duration of type `t()` returned
by `Cldr.Calendar.Duration.new/2`
* `options` is a Keyword list of options
## Options
* `:except` is a list of time units to be omitted from
the formatted output. It may be useful to use
`except: [:microsecond]` for example. The default is
`[]`.
* `locale` is any valid locale name returned by `Cldr.known_locale_names/1`
or a `Cldr.LanguageTag` struct returned by `Cldr.Locale.new!/2`
The default is `Cldr.get_locale/0`
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend/0`
* `:list_options` is a list of options passed to `Cldr.List.to_string/3` to
control the final list output.
Any other options are passed to `Cldr.Number.to_string/3` and
`Cldr.Unit.to_string/3` during the formatting process.
## Example
iex> {:ok, duration} = Cldr.Calendar.Duration.new(~D[2019-01-01], ~D[2019-12-31])
iex> Cldr.Calendar.Duration.to_string(duration)
{:ok, "11 months and 30 days"}
"""
def to_string(%__MODULE__{} = duration, options \\ []) do
{except, options} = Keyword.pop(options, :except, [])
for key <- @keys, value = Map.get(duration, key), value != 0 && key not in except do
Cldr.Unit.new(key, value)
end
|> Cldr.Unit.to_string(options)
end
else
@doc """
Returns a string formatted representation of
a duration.
Note that time units that are zero are ommitted
from the output.
## Localized formatting
If localized formatting of a duration is desired,
add `{:ex_cldr_units, "~> 2.0"}` to your `mix.exs`
and ensure you have configured your providers in
your backend configuration to include: `providers:
[Cldr.Calendar, Cldr.Number, Cldr.Unit, Cldr.List]`
## Arguments
* `duration` is a duration of type `t()` returned
by `Cldr.Calendar.Duration.new/2`
* `options` is a Keyword list of options
## Options
* `:except` is a list of time units to be omitted from
the formatted output. It may be useful to use
`except: [:microsecond]` for example. The default is
`[]`.
## Example
iex> {:ok, duration} = Cldr.Calendar.Duration.new(~D[2019-01-01], ~D[2019-12-31])
iex> Cldr.Calendar.Duration.to_string(duration)
{:ok, "11 months, 30 days"}
"""
def to_string(%__MODULE__{} = duration, options \\ []) do
except = Keyword.get(options, :except, [])
formatted =
for key <- @keys, value = Map.get(duration, key), value != 0 && key not in except do
if value > 1, do: "#{value} #{key}s", else: "#{value} #{key}"
end
|> Enum.join(", ")
{:ok, formatted}
end
end
@doc """
Formats a duration as a string or raises
an exception on error.
## Arguments
* `duration` is a duration of type `t()` returned
by `Cldr.Calendar.Duration.new/2`
* `options` is a Keyword list of options
## Options
See `Cldr.Calendar.Duration.to_string/2`
## Returns
* A formatted string or
* raises an exception
"""
@spec to_string!(t(), Keyword.t()) :: String.t() | no_return
def to_string!(duration, options \\ []) do
case to_string(duration, options) do
{:ok, string} -> string
{:error, {exception, reason}} -> raise exception, reason
end
end
@doc """
Calculates the calendar difference between two dates
returning a `Duration` struct.
The difference calculated is in terms of years, months,
days, hours, minutes, seconds and microseconds.
## Arguments
* `from` is a date, time or datetime representing the
start of the duration
* `to` is a date, time or datetime representing the
end of the duration
Note that `from` must be before or at the same time
as `to`. In addition, both `from` and `to` must
be in the same calendar.
## Returns
* A `{:ok, duration struct}` tuple or a
* `{:error, {exception, reason}}` tuple
## Example
iex> Cldr.Calendar.Duration.new(~D[2019-01-01], ~D[2019-12-31])
{:ok,
%Cldr.Calendar.Duration{
year: 0,
month: 11,
day: 30,
hour: 0,
microsecond: 0,
minute: 0,
second: 0
}}
"""
@spec new(from :: date_or_datetime(), to :: date_or_datetime()) ::
{:ok, t()} | {:error, {module(), String.t()}}
def new(%{calendar: calendar} = from, %{calendar: calendar} = to) do
time_diff = time_duration(from, to)
date_diff = date_duration(from, to)
duration =
if time_diff < 0 do
back_one_day(date_diff, calendar) |> merge(@microseconds_in_day + time_diff)
else
date_diff |> merge(time_diff)
end
{:ok, duration}
end
def new(%{calendar: _calendar1} = from, %{calendar: _calendar2} = to) do
{:error,
{Cldr.IncompatibleCalendarError,
"The two dates must be in the same calendar. Found #{inspect(from)} and #{inspect(to)}"}}
end
@doc """
Calculates the calendar difference between two dates
returning a `Duration` struct.
The difference calculated is in terms of years, months,
days, hours, minutes, seconds and microseconds.
## Arguments
* `from` is a date, time or datetime representing the
start of the duration
* `to` is a date, time or datetime representing the
end of the duration
Note that `from` must be before or at the same time
as `to`. In addition, both `from` and `to` must
be in the same calendar.
## Returns
* A `duration` struct or
* raises an exception
## Example
iex> Cldr.Calendar.Duration.new!(~D[2019-01-01], ~D[2019-12-31])
%Cldr.Calendar.Duration{
year: 0,
month: 11,
day: 30,
hour: 0,
microsecond: 0,
minute: 0,
second: 0
}
"""
@spec new!(from :: date_or_datetime(), to :: date_or_datetime()) ::
t() | no_return()
def new!(from, to) do
case new(from, to) do
{:ok, duration} -> duration
{:error, {exception, reason}} -> raise exception, reason
end
end
defp time_duration(
%{hour: _, minute: _, second: _, microsecond: _, calendar: calendar} = from,
%{hour: _, minute: _, second: _, microsecond: _, calendar: calendar} = to
) do
Time.diff(to, from, :microsecond)
end
defp time_duration(_to, _from) do
0
end
defp date_duration(
%{year: year, month: month, day: day, calendar: calendar},
%{year: year, month: month, day: day, calendar: calendar}
) do
%__MODULE__{}
end
defp date_duration(%{calendar: calendar} = from, %{calendar: calendar} = to) do
if Date.compare(from, to) in [:gt] do
raise ArgumentError, "`from` date must be before or equal to `to` date"
end
%{year: year1, month: month1, day: day1} = from
%{year: year2, month: month2, day: day2} = to
# Doesnt account for era in calendars like Japanese
year_diff = year2 - year1 - possible_adjustment(month2, month1, day2, day1)
month_diff =
if month2 > month1 do
month2 - month1 - possible_adjustment(day2, day1)
else
calendar.months_in_year(year1) - month1 + month2 - possible_adjustment(day2, day1)
end
day_diff =
if day2 > day1 do
day2 - day1
else
calendar.days_in_month(year1, month1) - day1 + day2
end
%__MODULE__{year: year_diff, month: month_diff, day: day_diff}
end
defp back_one_day(date_diff, calendar) do
back_one_day(date_diff, :day, calendar)
end
defp back_one_day(%{day: day} = date_diff, :day, calendar) do
%{date_diff | day: day - 1}
|> back_one_day(:month, calendar)
end
defp back_one_day(%{month: month, day: day} = date_diff, :month, calendar) when day < 1 do
%{date_diff | month: month - 1}
|> back_one_day(:year, calendar)
end
defp back_one_day(%{year: _, month: _, day: _} = date_diff, :month, _calendar) do
date_diff
end
defp back_one_day(%{year: year, month: month} = date_diff, :year, calendar) when month < 1 do
diff = %{date_diff | year: year - 1}
diff = if diff.month < 1, do: %{diff | month: calendar.months_in_year(year)}, else: diff
diff =
if diff.day < 1, do: %{diff | day: calendar.days_in_month(year, diff.month)}, else: diff
diff
end
defp back_one_day(%{year: _, month: _, day: _} = date_diff, :year, _calendar) do
date_diff
end
defp merge(duration, microseconds) do
{seconds, microseconds} = Cldr.Math.div_mod(microseconds, @microseconds_in_second)
{hours, minutes, seconds} = :calendar.seconds_to_time(seconds)
duration
|> Map.put(:hour, hours)
|> Map.put(:minute, minutes)
|> Map.put(:second, seconds)
|> Map.put(:microsecond, microseconds)
end
# The difference in years is adjusted if the
# month of the `to` year is less than the
# month of the `from` year
defp possible_adjustment(m2, m1, _d2, _d1) when m2 < m1, do: 1
defp possible_adjustment(m2, m2, d2, d1) when d2 < d1, do: 1
defp possible_adjustment(_m2, _m1, _d2, _d1), do: 0
defp possible_adjustment(m2, m1) when m2 < m1, do: 1
defp possible_adjustment(_m2, _m1), do: 0
end