Migrate three pure-function modules from Elixir to Gleam: - polling_offset: deterministic hash-based polling offset (pure Gleam, no FFI) - changelog_parser: text parsing logic in Gleam, thin Elixir wrapper for File I/O - user_agent_parser: regex-based UA parsing in Gleam with Erlang FFI for atom-keyed map conversion Add gleam_regexp dependency for regex support in Gleam modules. Update all call sites and tests to use Gleam module atoms. Reviewed-on: graham/towerops-web#103
174 lines
5.2 KiB
Gleam
174 lines
5.2 KiB
Gleam
/// Pure formatting functions shared by SnmpKit.SnmpLib.Types and Utils.
|
|
///
|
|
/// Provides number formatting, byte size formatting, rate formatting,
|
|
/// response time formatting, string truncation, and SNMP TimeTicks uptime.
|
|
///
|
|
/// Compiles to `:snmpkit@formatting`.
|
|
import gleam/int
|
|
import gleam/list
|
|
import gleam/string
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FFI declarations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Format a float to a string with N decimal places.
|
|
@external(erlang, "snmpkit_formatting_ffi", "format_float")
|
|
fn format_float(value: Float, decimals: Int) -> String
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Format a non-negative integer with comma thousand-separators.
|
|
///
|
|
/// Example: `1234567` → `"1,234,567"`
|
|
pub fn format_number(number: Int) -> String {
|
|
case number < 0 {
|
|
True -> "-" <> format_positive_number(0 - number)
|
|
False -> format_positive_number(number)
|
|
}
|
|
}
|
|
|
|
/// Format bytes as a human-readable size string.
|
|
///
|
|
/// Uses binary units (1024-based): B, KB, MB, GB.
|
|
pub fn format_bytes(bytes: Int) -> String {
|
|
case bytes >= 1_073_741_824 {
|
|
True ->
|
|
format_float(int.to_float(bytes) /. 1_073_741_824.0, 1) <> " GB"
|
|
False ->
|
|
case bytes >= 1_048_576 {
|
|
True ->
|
|
format_float(int.to_float(bytes) /. 1_048_576.0, 1) <> " MB"
|
|
False ->
|
|
case bytes >= 1024 {
|
|
True ->
|
|
format_float(int.to_float(bytes) /. 1024.0, 1) <> " KB"
|
|
False -> int.to_string(bytes) <> " B"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Format a rate with SI-prefix units (K, M, G).
|
|
///
|
|
/// Example: `format_rate(1500000, "bps")` → `"1.5 Mbps"`
|
|
pub fn format_rate(value: Int, unit: String) -> String {
|
|
case value >= 1_000_000_000 {
|
|
True ->
|
|
format_float(int.to_float(value) /. 1_000_000_000.0, 1) <> " G" <> unit
|
|
False ->
|
|
case value >= 1_000_000 {
|
|
True ->
|
|
format_float(int.to_float(value) /. 1_000_000.0, 1) <> " M" <> unit
|
|
False ->
|
|
case value >= 1_000 {
|
|
True ->
|
|
format_float(int.to_float(value) /. 1_000.0, 1) <> " K" <> unit
|
|
False -> int.to_string(value) <> " " <> unit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Format microseconds as a human-readable response time.
|
|
///
|
|
/// Example: `1500` → `"1.50ms"`, `2500000` → `"2.50s"`, `500` → `"500μs"`
|
|
pub fn format_response_time(microseconds: Int) -> String {
|
|
case microseconds >= 1_000_000 {
|
|
True ->
|
|
format_float(int.to_float(microseconds) /. 1_000_000.0, 2) <> "s"
|
|
False ->
|
|
case microseconds >= 1_000 {
|
|
True ->
|
|
format_float(int.to_float(microseconds) /. 1_000.0, 2) <> "ms"
|
|
False -> int.to_string(microseconds) <> "μs"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Truncate a string to a maximum length, appending "..." if truncated.
|
|
///
|
|
/// For max_length <= 3, returns a plain slice with no ellipsis.
|
|
pub fn truncate_string(s: String, max_length: Int) -> String {
|
|
case max_length > 3 {
|
|
True ->
|
|
case string.length(s) <= max_length {
|
|
True -> s
|
|
False -> string.slice(s, 0, max_length - 3) <> "..."
|
|
}
|
|
False -> string.slice(s, 0, int.max(max_length, 0))
|
|
}
|
|
}
|
|
|
|
/// Format SNMP TimeTicks (centiseconds) as a human-readable uptime string.
|
|
///
|
|
/// Example: `9000` → `"1 minute 30 seconds"`, `42` → `"42 centiseconds"`
|
|
pub fn format_timeticks_uptime(centiseconds: Int) -> String {
|
|
let total_seconds = centiseconds / 100
|
|
let remaining_cs = centiseconds % 100
|
|
|
|
case total_seconds, remaining_cs {
|
|
0, 0 -> "0 centiseconds"
|
|
0, cs -> int.to_string(cs) <> " centiseconds"
|
|
_, _ -> build_time_parts(total_seconds, remaining_cs)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Internal helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn format_positive_number(number: Int) -> String {
|
|
number
|
|
|> int.to_string()
|
|
|> string.to_graphemes()
|
|
|> list.reverse()
|
|
|> list.sized_chunk(3)
|
|
|> list.map(fn(chunk) { chunk |> list.reverse() |> string.join("") })
|
|
|> list.reverse()
|
|
|> string.join(",")
|
|
}
|
|
|
|
fn build_time_parts(total_seconds: Int, centiseconds: Int) -> String {
|
|
let days = total_seconds / 86_400
|
|
let remaining = total_seconds % 86_400
|
|
let hours = remaining / 3600
|
|
let remaining = remaining % 3600
|
|
let minutes = remaining / 60
|
|
let seconds = remaining % 60
|
|
|
|
let parts = []
|
|
let parts = append_if_positive(parts, days, "day")
|
|
let parts = append_if_positive(parts, hours, "hour")
|
|
let parts = append_if_positive(parts, minutes, "minute")
|
|
let parts = append_if_positive(parts, seconds, "second")
|
|
let parts = append_if_positive(parts, centiseconds, "centisecond")
|
|
|
|
case parts {
|
|
[] -> "0 centiseconds"
|
|
_ -> string.join(parts, " ")
|
|
}
|
|
}
|
|
|
|
fn append_if_positive(
|
|
parts: List(String),
|
|
value: Int,
|
|
label: String,
|
|
) -> List(String) {
|
|
case value > 0 {
|
|
True ->
|
|
list.append(parts, [
|
|
int.to_string(value) <> " " <> label <> plural(value),
|
|
])
|
|
False -> parts
|
|
}
|
|
}
|
|
|
|
fn plural(n: Int) -> String {
|
|
case n {
|
|
1 -> ""
|
|
_ -> "s"
|
|
}
|
|
}
|