defmodule Bloccs.Web.Format do @moduledoc """ Small, dependency-free formatting helpers for the dashboard: human durations, counts, rates, and percentages. Pure functions, easy to unit-test. """ @doc """ Render an uptime from a monotonic `started_at` (ms) relative to `now` (ms, defaults to the current monotonic clock). Coarse on purpose — "3d", "4h", "12m", "5s", or "just now". """ @spec uptime(integer(), integer()) :: String.t() def uptime(started_at, now \\ System.monotonic_time(:millisecond)) when is_integer(started_at) do duration(now - started_at) end @doc "Render a millisecond duration coarsely (largest unit only)." @spec duration(integer()) :: String.t() def duration(ms) when ms < 0, do: duration(0) def duration(ms) when ms < 1_000, do: "just now" def duration(ms) when ms < 60_000, do: "#{div(ms, 1_000)}s" def duration(ms) when ms < 3_600_000, do: "#{div(ms, 60_000)}m" def duration(ms) when ms < 86_400_000, do: "#{div(ms, 3_600_000)}h" def duration(ms), do: "#{div(ms, 86_400_000)}d" @doc "A latency in ms rendered with a unit (sub-ms shown in µs)." @spec latency(number() | nil) :: String.t() def latency(nil), do: "—" def latency(ms) when ms >= 1, do: "#{round_to(ms, 1)}ms" def latency(ms) when ms > 0, do: "#{round(ms * 1000)}µs" def latency(_), do: "0ms" @doc "A per-second rate, one decimal place." @spec rate(number() | nil) :: String.t() def rate(nil), do: "—" def rate(per_sec), do: "#{round_to(per_sec, 1)}/s" @doc "A 0.0–1.0 fraction as an integer percentage." @spec percent(number() | nil) :: String.t() def percent(nil), do: "—" def percent(frac), do: "#{round(frac * 100)}%" @doc "Compact integer count (1.2k, 3.4M)." @spec count(integer() | nil) :: String.t() def count(nil), do: "—" def count(n) when n < 1_000, do: Integer.to_string(n) def count(n) when n < 1_000_000, do: "#{round_to(n / 1_000, 1)}k" def count(n), do: "#{round_to(n / 1_000_000, 1)}M" defp round_to(value, places) do factor = :math.pow(10, places) rounded = Float.round(value / 1, places) # Drop a trailing ".0" so "4.0ms" reads "4ms". if rounded == Float.round(rounded) do trunc(rounded) else Float.round(rounded * factor) / factor end end end