Packages
moar
1.57.0
5.0.0
4.3.0
4.2.0
4.1.0
4.0.0
3.2.0
3.1.0
3.0.0
2.7.0
2.6.0
2.5.0
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.64.0
1.63.1
1.63.0
1.62.0
1.61.2
1.61.1
1.61.0
1.60.0
1.59.2
1.59.1
1.59.0
1.58.0
1.57.0
1.56.2
1.56.1
1.56.0
1.55.0
1.54.0
1.53.0
1.52.1
1.52.0
1.51.0
1.50.0
1.49.0
1.48.0
1.47.0
1.46.0
1.45.1
1.45.0
1.44.0
1.43.0
1.42.0
1.41.0
1.40.0
1.39.0
1.38.0
1.37.0
1.36.0
1.35.0
1.34.0
1.33.0
1.32.0
1.31.0
1.30.0
1.29.0
1.28.0
1.27.0
1.26.0
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.3
1.19.2
1.19.1
1.19.0
1.18.1
1.18.0
1.17.0
1.16.0
1.15.0
1.14.0
1.13.1
1.13.0
1.12.0
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.0
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.1.0
A dependency-free utility library containing 100+ useful functions.
Current section
Files
Jump to
Current section
Files
lib/datetime.ex
defmodule Moar.DateTime do
# @related [test](/test/datetime_test.exs)
@moduledoc """
DateTime-related functions. See also `Moar.NaiveDateTime`.
"""
@doc """
Adds `duration` to `date_time`.
See also `subtract/1` and `Moar.NaiveDateTime.add/2`.
> #### Note {: .info}
>
> This function is naive and intentionally doesn't account for real-world calendars and all of their complexity,
> such as leap years, leap days, daylight saving time, past and future calendar oddities, etc.
>
> As ["Falsehoods programmers believe about time"](https://gist.github.com/timvisee/fcda9bbdff88d45cc9061606b4b923ca)
> says, "If you think you understand everything about time, you're probably doing it wrong."
>
> See [`Cldr.Calendar.plus/2`](https://hexdocs.pm/ex_cldr_calendars/Cldr.Calendar.html#plus/2) for one example
> of a function that is far more likely to be correct.
```elixir
iex> start = ~U[2022-01-01T00:00:00.000Z]
iex> Moar.DateTime.add(start, {3, :minute})
~U[2022-01-01T00:03:00.000Z]
```
"""
@spec add(DateTime.t(), Moar.Duration.t()) :: DateTime.t()
def add(date_time, duration),
do: DateTime.add(date_time, Moar.Duration.convert(duration, :millisecond), :millisecond)
@doc """
Returns true if `date_time` is inclusively inside `range` which is a tuple containing a start datetime and an end
datetime.
```elixir
iex> Moar.DateTime.between?(~U[2022-01-01T02:00:00Z], {~U[2022-01-01T01:00:00Z], ~U[2022-01-01T03:00:00Z]})
true
iex> Moar.DateTime.between?(~U[2022-01-01T04:00:00Z], {~U[2022-01-01T01:00:00Z], ~U[2022-01-01T03:00:00Z]})
false
```
"""
@spec between?(DateTime.t(), {DateTime.t(), DateTime.t()}) :: boolean()
def between?(date_time, {range_start, range_end} = _range),
do: DateTime.compare(date_time, range_start) in [:eq, :gt] && DateTime.compare(date_time, range_end) in [:eq, :lt]
@doc """
Like `DateTime.from_iso8601/1` but raises if the string cannot be parsed.
```elixir
iex> Moar.DateTime.from_iso8601!("2022-01-01T00:00:00Z")
~U[2022-01-01T00:00:00Z]
iex> Moar.DateTime.from_iso8601!("2022-01-01T00:00:00+0800")
** (ArgumentError) Expected \"2022-01-01T00:00:00+0800\" to have a UTC offset of 0, but was: 28800
iex> Moar.DateTime.from_iso8601!("Next Thursday after lunch")
** (ArgumentError) Invalid ISO8601 format: "Next Thursday after lunch"
```
"""
@spec from_iso8601!(date_time_string :: String.t()) :: DateTime.t()
def from_iso8601!(date_time_string) when is_binary(date_time_string) do
case DateTime.from_iso8601(date_time_string) do
{:ok, date_time, 0} ->
date_time
{:ok, _date_time, other_offset} ->
raise ArgumentError, ~s|Expected "#{date_time_string}" to have a UTC offset of 0, but was: #{other_offset}|
{:error, :invalid_format} ->
raise ArgumentError, ~s|Invalid ISO8601 format: "#{date_time_string}"|
end
end
@doc """
Returns true if `date_time` is now or in the past and no older than `duration` ago (which defaults to 1 minute).
Returns false if `date_time` is in the future.
```elixir
iex> Moar.DateTime.recent?(Moar.DateTime.utc_now(minus: {30, :second}))
true
iex> Moar.DateTime.recent?(Moar.DateTime.utc_now(minus: {5, :minute}))
false
iex> Moar.DateTime.recent?(Moar.DateTime.utc_now(minus: {5, :minute}), {1, :hour})
true
iex> Moar.DateTime.recent?(Moar.DateTime.utc_now(plus: {5, :second}))
false
```
"""
@spec recent?(DateTime.t(), Moar.Duration.t() | nil) :: boolean()
def recent?(date_time, duration \\ {1, :minute}) do
case DateTime.diff(date_time, DateTime.utc_now(), :microsecond) do
diff when diff > 0 -> false
diff -> diff >= -1 * Moar.Duration.convert(duration, :microsecond)
end
end
@doc """
Subtracts `duration` from `date_time`.
See also `add/1` and `Moar.NaiveDateTime.subtract/2`.
> #### Note {: .info}
>
> This function is naive and intentionally doesn't account for real-world calendars and all of their complexity,
> such as leap years, leap days, daylight saving time, past and future calendar oddities, etc.
>
> As ["Falsehoods programmers believe about time"](https://gist.github.com/timvisee/fcda9bbdff88d45cc9061606b4b923ca)
> says, "If you think you understand everything about time, you're probably doing it wrong."
>
> See [`Cldr.Calendar.minus/4`](https://hexdocs.pm/ex_cldr_calendars/Cldr.Calendar.html#minus/4) for one example
> of a function that is far more likely to be correct.
```elixir
iex> start = ~U[2022-01-01T00:03:00.000Z]
iex> Moar.DateTime.subtract(start, {3, :minute})
~U[2022-01-01T00:00:00.000Z]
```
"""
@spec subtract(DateTime.t(), Moar.Duration.t()) :: DateTime.t()
def subtract(date_time, {time, unit} = _duration),
do: DateTime.add(date_time, Moar.Duration.convert({-1 * time, unit}, :millisecond), :millisecond)
@doc """
Like `DateTime.to_iso8601/1` but rounds to the nearest second first.
```elixir
iex> Moar.DateTime.to_iso8601_rounded(~U[2022-01-01T01:02:03.456789Z])
"2022-01-01T01:02:03Z"
```
"""
@spec to_iso8601_rounded(date_time :: DateTime.t()) :: String.t()
def to_iso8601_rounded(date), do: date |> DateTime.truncate(:second) |> DateTime.to_iso8601()
@doc """
Returns the current UTC time plus or minus the given duration.
```elixir
iex> Moar.DateTime.utc_now(plus: {10500, :millisecond})
...> |> Moar.Duration.format([:approx, :from_now])
"10 seconds from now"
iex> Moar.DateTime.utc_now(minus: {10, :second})
...> |> Moar.Duration.format([:approx, :ago])
"10 seconds ago"
```
"""
@spec utc_now([plus: Moar.Duration.t()] | [minus: Moar.Duration.t()]) :: DateTime.t()
def utc_now(plus: duration), do: DateTime.utc_now() |> add(duration)
def utc_now(minus: duration), do: DateTime.utc_now() |> subtract(duration)
@doc """
Returns true if `date_time` is no older than `duration` ago, and no later than `duration` from now.
```elixir
iex> Moar.DateTime.within?(Moar.DateTime.utc_now(minus: {30, :second}), {1, :minute})
true
iex> Moar.DateTime.within?(Moar.DateTime.utc_now(plus: {30, :second}), {1, :minute})
true
iex> Moar.DateTime.within?(Moar.DateTime.utc_now(minus: {5, :minute}), {1, :minute})
false
iex> Moar.DateTime.within?(Moar.DateTime.utc_now(plus: {5, :minute}), {1, :minute})
false
```
"""
@spec within?(DateTime.t(), Moar.Duration.t() | nil) :: boolean()
def within?(date_time, duration) do
now = DateTime.utc_now()
between?(date_time, {subtract(now, duration), add(now, duration)})
end
end