Packages
electric
1.6.9
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/pg_interop/interval.ex
defmodule PgInterop.Interval do
@moduledoc """
PostgreSQL interval representation.
`Timex.Duration` does not match PG interval representation because it doesn't store month
separately, leading to discrepancies like `date '2024-01-31' + interval '1 month'` being
`datetime '2024-02-29 00:00:00` in Postgres, but `Timex.add(~N[2024-01-31 00:00:00],
Timex.Duration.parse!("P1M"))` being `~N[2024-03-01 00:00:00]`. This implementation
sticks to PG interpretation of the events.
"""
alias __MODULE__
defstruct months: 0, days: 0, microseconds: 0
@type t :: %__MODULE__{months: integer(), days: integer(), microseconds: integer()}
@doc """
Format the interval in ISO8601 format.
## Examples
iex> parse!("5 minutes 3d 4 hours 6") |> format()
"P3DT4H5M6S"
"""
def format(%Interval{} = interval), do: Interval.Iso8601Formatter.format(interval)
@doc """
Parse a PostgreSQL interval string, in any of the supported PostgreSQL input formats.
For supported formats, see `parse!/1`
"""
def parse(string) do
with {:error, _} <- Interval.PostgresAndSQLParser.parse(string),
{:error, _} <- Interval.ISO8601Parser.parse(string),
{:error, _} <- Interval.ISO8601AlternativeParser.parse(string) do
:error
end
end
@doc """
Parse a PostgreSQL interval string, in any of the supported PostgreSQL input formats.
Raises an error when parsing fails, unlike `parse/1`
## Examples
SQL standard format
iex> parse!("1-2")
Interval.parse!("P1Y2M")
iex> parse!("3 4:05:06")
Interval.parse!("P3DT4H5M6S")
iex> parse!("5 minutes 3d 4 hours 6")
Interval.parse!("P3DT4H5M6S")
ISO8601 format
iex> parse!("P1Y2M3DT4H5M6S")
Interval.parse!("P1Y2M3DT4H5M6S")
ISO8601 "alternative" format
iex> parse!("P0001-02-03T04:05:06")
Interval.parse!("P1Y2M3DT4H5M6S")
iex> parse!("what")
** (RuntimeError) Not a valid PostgreSQL interval
"""
def parse!(string) do
case parse(string) do
{:ok, value} -> value
:error -> raise RuntimeError, message: "Not a valid PostgreSQL interval"
end
end
@doc """
Zero-length interval, useful in reductions
"""
def zero(), do: %Interval{}
@doc """
Add two intervals together
"""
def add(
%Interval{months: m1, days: d1, microseconds: s1},
%Interval{months: m2, days: d2, microseconds: s2}
),
do: %Interval{months: m1 + m2, days: d1 + d2, microseconds: s1 + s2}
@doc """
Subtracts the second interval from the first one
## Examples
iex> subtract(parse!("P2D"), parse!("P1D"))
Interval.parse!("P1D")
"""
def subtract(
%Interval{months: m1, days: d1, microseconds: s1},
%Interval{months: m2, days: d2, microseconds: s2}
),
do: %Interval{months: m1 - m2, days: d1 - d2, microseconds: s1 - s2}
@doc """
Add the interval to a given `Date` or `NaiveDateTime`.
## Examples
iex> add_to_time(~T[10:00:00], parse!("PT1H1M"))
~T[11:01:00.000000]
"""
def add_to_time(%Time{} = time, %Interval{microseconds: us}),
do: Time.add(time, us, :microsecond)
@doc """
Add the interval to a given `Date` or `NaiveDateTime`.
## Examples
iex> add_to_date(~D[2024-01-01], parse!("P1D"))
~N[2024-01-02 00:00:00]
iex> add_to_date(~N[2024-01-01 12:00:00], parse!("P1DT10M"))
~N[2024-01-02 12:10:00.000000]
"""
def add_to_date(%Date{} = date, %Interval{} = interval),
do: date |> NaiveDateTime.new!(~T[00:00:00]) |> add_to_date(interval)
def add_to_date(%NaiveDateTime{} = date, %Interval{} = interval) do
%NaiveDateTime{} =
NaiveDateTime.shift(date,
month: interval.months,
day: interval.days,
microsecond: {interval.microseconds, 6}
)
end
@doc """
Subtracts an interval from a given date or date-time to get a new date-time.
Accepts `DateTime`, `NaiveDateTime`, and `Date`. Returns `DateTime` in the first case
and `NaiveDateTime` in second and third. If a plain `Date` is passed, midnight is assumed.
## Examples
iex> subtract_from_date(~D[2024-01-10], parse!("P2DT12H"))
~N[2024-01-07 12:00:00.000000]
iex> subtract_from_date(~N[2024-01-10 13:00:00], parse!("P2DT12H"))
~N[2024-01-08 01:00:00.000000]
iex> subtract_from_date(~U[2024-01-10 13:00:00Z], parse!("P2DT12H"))
~U[2024-01-08 01:00:00.000000Z]
"""
def subtract_from_date(%Date{} = date, %Interval{} = interval),
do: date |> NaiveDateTime.new!(~T[00:00:00]) |> subtract_from_date(interval)
def subtract_from_date(%m{} = date, %Interval{} = interval)
when m in [DateTime, NaiveDateTime] do
%^m{} =
m.shift(date,
month: -interval.months,
day: -interval.days,
microsecond: {-interval.microseconds, 6}
)
end
@doc """
Create an interval from specified amount of days.
## Examples
iex> from_days(10)
Interval.parse!("P10D")
iex> from_days(10.5)
Interval.parse!("P10DT12H")
"""
def from_days(days) when is_integer(days), do: %Interval{days: days}
def from_days(days) when is_float(days) do
full_days = floor(days)
%{from_hours((days - full_days) * 24) | days: full_days}
end
@doc """
Create an interval from specified amount of hours.
## Examples
iex> from_hours(10)
Interval.parse!("PT10H")
"""
def from_hours(hours) when is_number(hours),
do: %Interval{microseconds: round(hours * 3_600_000_000)}
@doc """
Create an interval from specified amount of microseconds.
## Examples
iex> from_microseconds(1_000_000)
Interval.parse!("PT1S")
"""
def from_microseconds(microseconds) when is_integer(microseconds),
do: %Interval{microseconds: microseconds}
@doc """
Create an interval from specified amount of milliseconds.
## Examples
iex> from_milliseconds(1_000)
Interval.parse!("PT1S")
"""
def from_milliseconds(milliseconds) when is_number(milliseconds),
do: %Interval{microseconds: round(milliseconds * 1000)}
@doc """
Create an interval from specified amount of seconds.
## Examples
iex> from_seconds(60)
Interval.parse!("PT1M")
"""
def from_seconds(seconds) when is_number(seconds),
do: %Interval{microseconds: round(seconds * 1_000_000)}
@doc """
Create an interval from specified amount of minutes.
## Examples
iex> from_minutes(60.5)
Interval.parse!("PT1H30S")
"""
def from_minutes(minutes) when is_number(minutes),
do: %Interval{microseconds: round(minutes * 60_000_000)}
@doc """
Create an interval from specified amount of weeks.
## Examples
iex> from_weeks(4.2)
Interval.parse!("P29DT9H36M")
"""
def from_weeks(weeks) when is_number(weeks), do: from_days(7 * weeks)
@doc """
Create an interval from specified amount of months. Fractional
months are counted as parts of 30 days.
## Examples
iex> from_months(14.5)
Interval.parse!("P1Y2M15D")
"""
def from_months(months) do
full_months = floor(months)
%{from_days((months - full_months) * 30) | months: full_months}
end
@doc """
Create an interval from a time instance.
## Examples
iex> from_time(~T[12:30:40.1])
Interval.parse!("PT12H30M40.1S")
iex> from_time(~T[12:30:40.000001])
Interval.parse!("PT12H30M40.000001S")
"""
def from_time(%Time{hour: h, minute: m, second: s, microsecond: {ms, _}}) do
from_microseconds(ms + s * 1_000_000 + m * 60_000_000 + h * 3_600_000_000)
end
@doc """
Scale an interval by a factor.
## Examples
iex> scale(parse!("P2M4DT6H"), 1.5)
Interval.parse!("P3M6DT9H")
"""
def scale(%Interval{months: m, days: d, microseconds: ms}, by) when is_number(by) do
from_microseconds(floor(ms * by))
|> add(from_days(d * by))
|> add(from_months(m * by))
end
@day_in_us 3_600_000_000 * 24
@doc """
Build an interval as a difference between DateTimes. Interval is positive when
first datetime is greater than the second one.
## Examples
iex> datetime_diff(~N[2024-01-02 00:10:00], ~N[2024-01-01 00:00:00])
Interval.parse!("P1DT10M")
iex> datetime_diff(~N[2024-01-02 00:00:00], ~N[2024-01-01 00:10:00])
Interval.parse!("PT23H50M")
iex> datetime_diff(~N[2024-01-02 00:00:00], ~N[2024-01-03 00:00:00])
Interval.parse!("P-1D")
iex> datetime_diff(DateTime.from_naive!(~N[2024-01-02 00:00:00], "Europe/Istanbul"), ~U[2024-01-02 00:00:00Z])
Interval.parse!("PT-3H")
"""
def datetime_diff(%NaiveDateTime{} = d1, %NaiveDateTime{} = d2) do
%Interval{
days: NaiveDateTime.diff(d1, d2, :day),
microseconds: NaiveDateTime.diff(d1, d2, :microsecond) |> rem(@day_in_us)
}
end
def datetime_diff(%DateTime{} = d1, %DateTime{} = d2) do
%Interval{
days: DateTime.diff(d1, d2, :day),
microseconds: DateTime.diff(d1, d2, :microsecond) |> rem(@day_in_us)
}
end
@doc """
Move complete 24 hour periods from the microsecond portion of the interval to the day portion.
"""
def justify_hours(%Interval{months: m, days: d, microseconds: us}) do
%Interval{months: m, days: d + div(us, @day_in_us), microseconds: rem(us, @day_in_us)}
end
@doc """
Move complete 30 day periods from the day portion of the interval to the month portion.
"""
def justify_days(%Interval{months: m, days: d, microseconds: us}) do
%Interval{months: m + div(d, 30), days: rem(d, 30), microseconds: us}
end
@doc """
Move complete 24 hour periods from the microsecond portion of the interval to the day portion
and complete 30 day periods from the day portion of the interval to the month portion.
## Examples
iex> interval = %Interval{months: 0, days: 29, microseconds: #{@day_in_us} + 60_000_000}
Interval.parse!("P29DT24H1M")
iex> justify_interval(interval)
Interval.parse!("P1MT1M")
"""
def justify_interval(%Interval{} = i), do: i |> justify_hours() |> justify_days()
end
defimpl Inspect, for: PgInterop.Interval do
def inspect(interval, _opts) do
~s|Interval.parse!("#{PgInterop.Interval.Iso8601Formatter.to_iodata(interval)}")|
end
end