defmodule QuackDB.TimeWithTimeZone do
@moduledoc """
DuckDB `TIME WITH TIME ZONE` value.
DuckDB stores `TIME WITH TIME ZONE` as microseconds since midnight packed with
a timezone offset in seconds. This struct exposes the decoded `Time` and UTC
offset while preserving conversion to and from DuckDB's packed integer.
"""
import Bitwise
defstruct [:time, :utc_offset]
@offset_basis 57_599
@offset_mask 0xFF_FFFF
@type t :: %__MODULE__{time: Time.t(), utc_offset: integer()}
@doc "Builds a `TIME WITH TIME ZONE` value from time and UTC offset seconds."
@spec new(Time.t(), integer()) :: t()
def new(%Time{} = time, utc_offset) when is_integer(utc_offset) do
%__MODULE__{time: time, utc_offset: utc_offset}
end
@doc "Decodes DuckDB's packed `TIME WITH TIME ZONE` integer."
@spec from_bits(integer()) :: t()
def from_bits(bits) when is_integer(bits) do
micros = bits >>> 24
encoded_offset = bits &&& @offset_mask
new(Time.add(~T[00:00:00], micros, :microsecond), @offset_basis - encoded_offset)
end
@doc "Encodes the value into DuckDB's packed `TIME WITH TIME ZONE` integer."
@spec to_bits(t()) :: integer()
def to_bits(%__MODULE__{time: %Time{} = time, utc_offset: utc_offset}) do
micros = Time.diff(time, ~T[00:00:00], :microsecond)
encoded_offset = @offset_basis - utc_offset
micros <<< 24 ||| encoded_offset
end
@doc "Formats the value as an ISO-like time with numeric UTC offset."
@spec to_iso8601(t()) :: String.t()
def to_iso8601(%__MODULE__{} = value) do
value.time
|> Time.to_iso8601()
|> Kernel.<>(offset_to_string(value.utc_offset))
end
defp offset_to_string(offset) do
sign = if offset < 0, do: "-", else: "+"
offset = abs(offset)
hours = div(offset, 3600)
minutes = div(rem(offset, 3600), 60)
sign <> pad2(hours) <> ":" <> pad2(minutes)
end
defp pad2(value), do: value |> Integer.to_string() |> String.pad_leading(2, "0")
end
if Code.ensure_loaded?(Inspect) do
defimpl Inspect, for: QuackDB.TimeWithTimeZone do
import Inspect.Algebra
def inspect(value, _opts) do
concat([
string("#QuackDB.TimeWithTimeZone<"),
string(QuackDB.TimeWithTimeZone.to_iso8601(value)),
string(">")
])
end
end
end