towerops/lib/towerops_web/helpers/time_helpers.ex

222 lines
6.3 KiB
Elixir

defmodule ToweropsWeb.TimeHelpers do
@moduledoc """
Helper functions for formatting time and date values with timezone support.
"""
# Common timezone offsets in seconds
# Note: These are standard time offsets. DST handling would require a full timezone database.
@timezone_offsets %{
"UTC" => 0,
"America/New_York" => -18_000,
# EST (UTC-5)
"America/Chicago" => -21_600,
# CST (UTC-6)
"America/Denver" => -25_200,
# MST (UTC-7)
"America/Los_Angeles" => -28_800,
# PST (UTC-8)
"America/Phoenix" => -25_200,
# MST (no DST)
"Europe/London" => 0,
# GMT (UTC+0)
"Europe/Paris" => 3600,
# CET (UTC+1)
"Europe/Berlin" => 3600,
# CET (UTC+1)
"Asia/Tokyo" => 32_400,
# JST (UTC+9)
"Asia/Shanghai" => 28_800,
# CST (UTC+8)
"Asia/Dubai" => 14_400,
# GST (UTC+4)
"Australia/Sydney" => 36_000
# AEST (UTC+10)
}
@timezone_abbrs %{
"UTC" => "UTC",
"America/New_York" => "EST",
"America/Chicago" => "CST",
"America/Denver" => "MST",
"America/Los_Angeles" => "PST",
"America/Phoenix" => "MST",
"Europe/London" => "GMT",
"Europe/Paris" => "CET",
"Europe/Berlin" => "CET",
"Asia/Tokyo" => "JST",
"Asia/Shanghai" => "CST",
"Asia/Dubai" => "GST",
"Australia/Sydney" => "AEST"
}
@doc """
Formats a DateTime into a human-readable "time ago" string.
Supports short time periods (seconds, minutes, hours, days) and
long time periods (months, years) for things like build timestamps.
## Examples
iex> datetime = DateTime.add(DateTime.utc_now(), -65, :second)
iex> ToweropsWeb.TimeHelpers.format_time_ago(datetime)
"1m ago"
iex> datetime = DateTime.add(DateTime.utc_now(), -7200, :second)
iex> ToweropsWeb.TimeHelpers.format_time_ago(datetime)
"2h ago"
"""
@spec format_time_ago(DateTime.t() | nil) :: String.t()
def format_time_ago(nil), do: "Never"
def format_time_ago(datetime) do
now = DateTime.utc_now()
diff = DateTime.diff(now, datetime, :second)
cond do
diff < 60 ->
"#{diff}s ago"
diff < 3600 ->
minutes = div(diff, 60)
"#{minutes}m ago"
diff < 86_400 ->
hours = div(diff, 3600)
"#{hours}h ago"
diff < 2_592_000 ->
days = div(diff, 86_400)
"#{days}d ago"
diff < 31_536_000 ->
months = div(diff, 2_592_000)
"#{months}mo ago"
true ->
years = div(diff, 31_536_000)
"#{years}y ago"
end
end
@doc """
Formats a DateTime into a full date and time string in UTC.
"""
@spec format_datetime(DateTime.t() | nil) :: String.t()
def format_datetime(datetime), do: format_datetime(datetime, "UTC")
@doc """
Formats a DateTime into a full date and time string, converted to the given timezone.
## Examples
iex> datetime = ~U[2026-01-15 14:34:00Z]
iex> ToweropsWeb.TimeHelpers.format_datetime(datetime, "America/New_York")
"Jan 15, 2026 at 09:34 AM EST"
iex> datetime = ~U[2026-01-15 14:34:00Z]
iex> ToweropsWeb.TimeHelpers.format_datetime(datetime, "UTC")
"Jan 15, 2026 at 02:34 PM UTC"
"""
@spec format_datetime(DateTime.t() | nil, String.t() | nil) :: String.t()
def format_datetime(nil, _timezone), do: "Never"
def format_datetime(datetime, nil), do: format_datetime(datetime, "UTC")
def format_datetime(datetime, timezone) when is_binary(timezone) do
case shift_timezone(datetime, timezone) do
{:ok, shifted_dt, tz_abbr} ->
Calendar.strftime(shifted_dt, "%b %d, %Y at %I:%M %p #{tz_abbr}")
{:error, _reason} ->
# Fallback to UTC if timezone is invalid
Calendar.strftime(datetime, "%b %d, %Y at %I:%M %p UTC")
end
end
@doc """
Formats a DateTime into a short date string in UTC.
"""
@spec format_date(DateTime.t() | nil) :: String.t()
def format_date(datetime), do: format_date(datetime, "UTC")
@doc """
Formats a DateTime into a short date string, converted to the given timezone.
## Examples
iex> datetime = ~U[2026-01-15 14:34:00Z]
iex> ToweropsWeb.TimeHelpers.format_date(datetime, "UTC")
"Jan 15, 2026"
iex> datetime = ~U[2026-01-15 02:00:00Z]
iex> ToweropsWeb.TimeHelpers.format_date(datetime, "America/Los_Angeles")
"Jan 14, 2026"
"""
@spec format_date(DateTime.t() | nil, String.t() | nil) :: String.t()
def format_date(nil, _timezone), do: "Never"
def format_date(datetime, nil), do: format_date(datetime, "UTC")
def format_date(datetime, timezone) when is_binary(timezone) do
case shift_timezone(datetime, timezone) do
{:ok, shifted_dt, _tz_abbr} ->
Calendar.strftime(shifted_dt, "%b %d, %Y")
{:error, _reason} ->
# Fallback to UTC if timezone is invalid
Calendar.strftime(datetime, "%b %d, %Y")
end
end
@doc """
Formats a DateTime into ISO 8601 format in UTC.
"""
@spec format_iso8601(DateTime.t() | nil) :: String.t()
def format_iso8601(datetime), do: format_iso8601(datetime, "UTC")
@doc """
Formats a DateTime into ISO 8601 format, converted to the given timezone.
## Examples
iex> datetime = ~U[2026-01-15 14:34:00Z]
iex> ToweropsWeb.TimeHelpers.format_iso8601(datetime, "UTC")
"2026-01-15 14:34:00 UTC"
iex> datetime = ~U[2026-01-15 14:34:00Z]
iex> ToweropsWeb.TimeHelpers.format_iso8601(datetime, "America/New_York")
"2026-01-15 09:34:00 EST"
"""
@spec format_iso8601(DateTime.t() | nil, String.t() | nil) :: String.t()
def format_iso8601(nil, _timezone), do: "Never"
def format_iso8601(datetime, nil), do: format_iso8601(datetime, "UTC")
def format_iso8601(datetime, timezone) when is_binary(timezone) do
case shift_timezone(datetime, timezone) do
{:ok, shifted_dt, tz_abbr} ->
Calendar.strftime(shifted_dt, "%Y-%m-%d %H:%M:%S #{tz_abbr}")
{:error, _reason} ->
# Fallback to UTC if timezone is invalid
Calendar.strftime(datetime, "%Y-%m-%d %H:%M:%S UTC")
end
end
# Private helper to shift a DateTime by a timezone offset
defp shift_timezone(datetime, timezone) do
case Map.get(@timezone_offsets, timezone) do
nil ->
{:error, :unknown_timezone}
offset_seconds ->
shifted_dt = DateTime.add(datetime, offset_seconds, :second)
tz_abbr = Map.get(@timezone_abbrs, timezone, timezone)
{:ok, shifted_dt, tz_abbr}
end
end
end