Phase 1: Foundation Types (100% complete)
- query_helpers: SQL LIKE sanitization with pipe operators
- numeric: Integer parsing with pattern matching guards
- result: Pure Elixir Result monad (map, and_then, unwrap_or)
Phase 2: Ecto Domain Types (100% complete)
- ip_address: IPv4/IPv6 validation using :inet directly
- mac_address: Multi-format MAC parsing (colon/hyphen/dot/compact)
- snmp_oid: OID parsing/manipulation with recursive pattern matching
All 198 tests passing across converted modules.
API changed from Gleam-style {:some/:none to idiomatic {:ok/:error.
Refactored parse_numeric_oid to use with statement, reducing nesting depth.
Reviewed-on: graham/towerops-web#196
225 lines
5.6 KiB
Elixir
225 lines
5.6 KiB
Elixir
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
|