Skip to main content

lib/bloccs/web/format.ex

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