///|
/// Human-readable size parsing and formatting.
///
/// The log rotator accepts limits such as `10M`, `1G`, `512K` or a bare
/// byte count. Unlike the original `pm2-logrotate`, which uses `parseInt`
/// and silently truncates decimals (`1.5G` became `1G`), this parser keeps
/// the fractional part, so `1.5G` really means 1.5 GiB.

///|
/// Multiplier for a size unit suffix. Returns `None` for unknown units.
fn size_unit_factor(unit : StringView) -> Int64? {
  let u = unit.to_upper().to_owned()
  match u {
    "" | "B" => Some(1L)
    "K" | "KB" | "KIB" => Some(1024L)
    "M" | "MB" | "MIB" => Some(1024L * 1024L)
    "G" | "GB" | "GIB" => Some(1024L * 1024L * 1024L)
    "T" | "TB" | "TIB" => Some(1024L * 1024L * 1024L * 1024L)
    _ => None
  }
}

///|
/// Parse the numeric prefix of a size string. Supports a single decimal
/// point. Returns `None` when no digit is present.
fn parse_size_number(text : StringView) -> Double? {
  if text.length() == 0 {
    return None
  }
  let mut value = 0.0
  let mut seen_dot = false
  let mut scale = 0.1
  let mut seen_digit = false
  for i in 0..= '0' && c <= '9' {
      seen_digit = true
      let d = (c.to_int() - '0'.to_int()).to_double()
      if seen_dot {
        value = value + d * scale
        scale = scale * 0.1
      } else {
        value = value * 10.0 + d
      }
    } else {
      return None
    }
  }
  if !seen_digit {
    return None
  }
  Some(value)
}

///|
/// Parse a size string such as `10M` into bytes.
pub fn parse_size(text : StringView) -> Int64? {
  let t = text.trim()
  if t.length() == 0 {
    return None
  }
  // Split the numeric prefix from the unit suffix.
  let mut i = 0
  let n = t.length()
  while i < n {
    guard t.get_char(i) is Some(c) else { break }
    if (c >= '0' && c <= '9') || c == '.' {
      i = i + 1
    } else {
      break
    }
  }
  if i == 0 {
    return None
  }
  let number_part = t.view(start_offset=0, end_offset=i)
  let unit_part = t.view(start_offset=i, end_offset=n).trim()
  guard parse_size_number(number_part) is Some(value) else { return None }
  guard size_unit_factor(unit_part) is Some(factor) else { return None }
  Some((value * factor.to_double()).to_int64())
}

///|
/// Parse a size, falling back to `default` when the input is empty or invalid.
pub fn parse_size_or(text : StringView, default : Int64) -> Int64 {
  match parse_size(text) {
    Some(v) => v
    None => default
  }
}

///|
/// Left-pad `text` with `pad` until it is at least `width` characters wide.
fn pad_left(text : String, width : Int, pad : Char) -> String {
  if text.length() >= width {
    return text
  }
  let buf = StringBuilder()
  for _ in 0..<(width - text.length()) {
    buf.write_char(pad)
  }
  buf.write_string(text)
  buf.to_string()
}

///|
/// Format `value` with a fixed number of decimal places, without relying on
/// locale-aware formatting.
pub fn format_fixed(value : Double, precision : Int) -> String {
  let p = if precision < 0 { 0 } else { precision }
  let mut factor = 1.0
  for _ in 0..

String { let p = match precision { Some(p) => p None => 2 } if bytes < 1024L { return "\{bytes} B" } let units : Array[(Int64, String)] = [ (1024L * 1024L * 1024L * 1024L, "TB"), (1024L * 1024L * 1024L, "GB"), (1024L * 1024L, "MB"), (1024L, "KB"), ] for unit in units { let (factor, name) = unit if bytes >= factor { let scaled = bytes.to_double() / factor.to_double() return "\{format_fixed(scaled, p)} \{name}" } } "\{bytes} B" } ///| /// Render a byte count so that `parse_size` reads back exactly the same number. /// /// `format_size` rounds to two decimals, which is right for a status column but /// wrong for a config file: it would turn `1000` into `1003` on the next load. /// This only uses a unit when the value is an exact multiple, and otherwise /// falls back to a plain byte count. pub fn format_size_exact(bytes : Int64) -> String { let units : Array[(Int64, String)] = [ (1024L * 1024L * 1024L, "G"), (1024L * 1024L, "M"), (1024L, "K"), ] for unit in units { let (factor, name) = unit if bytes != 0L && bytes % factor == 0L { return (bytes / factor).to_string() + name } } bytes.to_string() } ///| /// Render a byte count the way PM2's `bytesToSize` does: one decimal place, /// a lowercase unit and no space — `2.0mb`, `512.0kb`, `1.2gb`, `123b`. /// /// The status tables use this so that `pm3 list` and `pm2 ls` agree on the /// number they show for the same process. PM2's own function appends a /// trailing space (`'24.1mb '`); that only matters where the value is /// concatenated into a sentence, and the tables pad the cell anyway. pub fn format_size_pm2(bytes : Int64, precision? : Int) -> String { let p = match precision { Some(p) => p None => 1 } let units : Array[(Int64, String)] = [ (1024L * 1024L * 1024L * 1024L, "tb"), (1024L * 1024L * 1024L, "gb"), (1024L * 1024L, "mb"), (1024L, "kb"), ] for unit in units { let (factor, name) = unit if bytes >= factor { return format_fixed(bytes.to_double() / factor.to_double(), p) + name } } "\{bytes}b" } ///| /// Render a CPU percentage the way PM2 does: rounded to one decimal, with a /// trailing `.0` dropped — `0%`, `85.4%`, `100%`. /// /// PM2 stores `Math.round(stat.cpu * 10) / 10` and prints it with JavaScript's /// default number formatting, which is why a whole percentage has no decimal /// point. Matching that keeps the two tools' tables comparable line by line. pub fn format_cpu_percent(cpu : Double) -> String { let rounded = (cpu * 10.0).round() / 10.0 if rounded == rounded.round() { rounded.to_int64().to_string() + "%" } else { format_fixed(rounded, 1) + "%" } }