defmodule SnmpKit.Formatting do @moduledoc """ Pure formatting functions for SnmpKit. Provides number formatting, byte size formatting, rate formatting, response time formatting, string truncation, and SNMP TimeTicks uptime. """ @doc """ Format a non-negative integer with comma thousand-separators. ## Examples iex> SnmpKit.Formatting.format_number(1234567) "1,234,567" iex> SnmpKit.Formatting.format_number(-1234) "-1,234" """ @spec format_number(integer()) :: String.t() def format_number(number) when number < 0 do "-" <> format_positive_number(-number) end def format_number(number) do format_positive_number(number) end @doc """ Format bytes as a human-readable size string. Uses binary units (1024-based): B, KB, MB, GB. ## Examples iex> SnmpKit.Formatting.format_bytes(1024) "1.0 KB" iex> SnmpKit.Formatting.format_bytes(1_048_576) "1.0 MB" iex> SnmpKit.Formatting.format_bytes(500) "500 B" """ @spec format_bytes(integer()) :: String.t() def format_bytes(bytes) do cond do bytes >= 1_073_741_824 -> format_float(bytes / 1_073_741_824.0, 1) <> " GB" bytes >= 1_048_576 -> format_float(bytes / 1_048_576.0, 1) <> " MB" bytes >= 1024 -> format_float(bytes / 1024.0, 1) <> " KB" true -> "#{bytes} B" end end @doc """ Format a rate with SI-prefix units (K, M, G). ## Examples iex> SnmpKit.Formatting.format_rate(1_500_000, "bps") "1.5 Mbps" iex> SnmpKit.Formatting.format_rate(1_000, "bps") "1.0 Kbps" """ @spec format_rate(integer(), String.t()) :: String.t() def format_rate(value, unit) do cond do value >= 1_000_000_000 -> format_float(value / 1_000_000_000.0, 1) <> " G" <> unit value >= 1_000_000 -> format_float(value / 1_000_000.0, 1) <> " M" <> unit value >= 1_000 -> format_float(value / 1_000.0, 1) <> " K" <> unit true -> "#{value} #{unit}" end end @doc """ Format microseconds as a human-readable response time. ## Examples iex> SnmpKit.Formatting.format_response_time(1500) "1.50ms" iex> SnmpKit.Formatting.format_response_time(2_500_000) "2.50s" iex> SnmpKit.Formatting.format_response_time(500) "500μs" """ @spec format_response_time(integer()) :: String.t() def format_response_time(microseconds) do cond do microseconds >= 1_000_000 -> format_float(microseconds / 1_000_000.0, 2) <> "s" microseconds >= 1_000 -> format_float(microseconds / 1_000.0, 2) <> "ms" true -> "#{microseconds}μs" end end @doc """ Truncate a string to a maximum length, appending "..." if truncated. For max_length <= 3, returns a plain slice with no ellipsis. ## Examples iex> SnmpKit.Formatting.truncate_string("Hello, World!", 10) "Hello, ..." iex> SnmpKit.Formatting.truncate_string("Hi", 10) "Hi" iex> SnmpKit.Formatting.truncate_string("Hello", 2) "He" """ @spec truncate_string(String.t(), non_neg_integer()) :: String.t() def truncate_string(s, max_length) when max_length > 3 do if String.length(s) <= max_length do s else String.slice(s, 0, max_length - 3) <> "..." end end def truncate_string(s, max_length) do String.slice(s, 0, max(max_length, 0)) end @doc """ Format SNMP TimeTicks (centiseconds) as a human-readable uptime string. ## Examples iex> SnmpKit.Formatting.format_timeticks_uptime(9000) "1 minute 30 seconds" iex> SnmpKit.Formatting.format_timeticks_uptime(42) "42 centiseconds" iex> SnmpKit.Formatting.format_timeticks_uptime(0) "0 centiseconds" """ @spec format_timeticks_uptime(non_neg_integer()) :: String.t() def format_timeticks_uptime(centiseconds) do total_seconds = div(centiseconds, 100) remaining_cs = rem(centiseconds, 100) case {total_seconds, remaining_cs} do {0, 0} -> "0 centiseconds" {0, cs} -> "#{cs} centiseconds" {_, _} -> build_time_parts(total_seconds, remaining_cs) end end # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- defp format_float(value, decimals) when is_number(value) and is_integer(decimals) do :erlang.float_to_binary(value * 1.0, decimals: decimals) end defp format_positive_number(number) do number |> Integer.to_string() |> String.graphemes() |> Enum.reverse() |> Enum.chunk_every(3) |> Enum.map(fn chunk -> chunk |> Enum.reverse() |> Enum.join("") end) |> Enum.reverse() |> Enum.join(",") end defp build_time_parts(total_seconds, centiseconds) do days = div(total_seconds, 86_400) remaining = rem(total_seconds, 86_400) hours = div(remaining, 3600) remaining = rem(remaining, 3600) minutes = div(remaining, 60) seconds = rem(remaining, 60) [] |> append_if_positive(days, "day") |> append_if_positive(hours, "hour") |> append_if_positive(minutes, "minute") |> append_if_positive(seconds, "second") |> append_if_positive(centiseconds, "centisecond") |> case do [] -> "0 centiseconds" parts -> Enum.join(parts, " ") end end defp append_if_positive(parts, value, label) when value > 0 do parts ++ ["#{value} #{label}#{plural(value)}"] end defp append_if_positive(parts, _value, _label), do: parts defp plural(1), do: "" defp plural(_), do: "s" end