// File size humanization (port of python-humanize filesize.py).
// Phase 2 target functions: naturalsize.
// See docs/specs/port-to-moonbit.md.
// Symbol tables without the trailing "B" (the `suffix` is appended separately).
// Index 0 is unused here; the sub-threshold "Byte"/"Bytes" case is handled
// specially. Decimal/binary/gnu follow python-humanize's tables.
let decimal_symbols : Array[String] = [
"", "k", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q",
]
let binary_symbols : Array[String] = [
"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi", "Ri", "Qi",
]
let gnu_symbols : Array[String] = [
"", "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q",
]
fn _fs_get_symbols(symbols : String?) -> Array[String] {
match symbols {
Some("decimal") => decimal_symbols
Some("binary") => binary_symbols
Some("gnu") => gnu_symbols
Some(_) => decimal_symbols
None => decimal_symbols
}
}
/// Humanize a byte count into a friendly size string.
///
/// * `value` is the number of bytes (a Double or Int).
/// * `binary` selects binary (1024-based) units when `true` and `symbols` is None.
/// * `gnu` selects GNU units (1024-based, space-free, e.g. `"300B"`, `"2.9K"`) —
/// mirrors python-humanize's `gnu=True` exactly, including the quirk that the
/// suffix is only shown below one unit (`ls -sh` style).
/// * `suffix` is appended after the unit symbol (default `"B"`).
/// * `format` is a Python-style format string (default `"%.1f"`); passing `"%.0f"`
/// mirrors python-humanize's `format=True` (no fractional digits).
/// * `symbols` selects the unit set: `None`, `"decimal"`, `"binary"`, or `"gnu"`.
pub fn naturalsize(
value : Double,
binary~ : Bool = false,
gnu~ : Bool = false,
suffix~ : String = "B",
format~ : String = "%.1f",
symbols~ : String? = None
) -> String {
let is_gnu = gnu || symbols is Some("gnu")
let symbols_arr = if is_gnu {
gnu_symbols
} else if binary {
if symbols is Some(_) {
_fs_get_symbols(symbols)
} else {
binary_symbols
}
} else {
_fs_get_symbols(symbols)
}
// python: base = 1024 if (gnu or binary) else 1000.
let base = if is_gnu || binary { 1024.0 } else { 1000.0 }
let mut negative = false
let mut num = value
if num < 0.0 {
negative = true
num = -num
}
// Largest exponent with `num / base^exponent >= base`, capped at the top
// symbol — mirrors python's `int(min(log(abs, base), len(suffix)))`.
let mut exponent = 0
let mut divisor = 1.0
let mut n = num
while n >= base && exponent < symbols_arr.length() - 1 {
n = n / base
divisor = divisor * base
exponent = exponent + 1
}
let sign = if negative { "-" } else { "" }
if exponent == 0 {
// Sub-threshold (incl. exactly one byte): python renders an integer and
// ignores `format` — `"%d Byte"/"%d Bytes"`, or `f"{int}B"` for gnu.
let count = num.to_int().to_string()
if is_gnu {
sign + count + suffix
} else if num == 1.0 {
sign + count + " Byte"
} else {
sign + count + " Bytes"
}
} else {
let mut val = num / divisor
// python 4.16.0: rounding can push the mantissa up to `base` (999999 is
// 999.999 kB, which formats to "1000.0 kB"); step up one unit so it
// reads "1.0 MB". (`exponent == 0` already returned above.)
if exponent < symbols_arr.length() - 1 {
if _fs_str_to_double(_fs_format(val, format)) >= base {
exponent = exponent + 1
divisor = divisor * base
val = num / divisor
}
}
let mantissa = _fs_format(val, format)
if is_gnu {
// python: no space and no `suffix` above the threshold ("2.9K").
sign + mantissa + symbols_arr[exponent]
} else {
sign + mantissa + " " + symbols_arr[exponent] + suffix
}
}
}
fn _fs_format(value : Double, format : String) -> String {
let prec = _fs_parse_precision(format)
format_fixed(value, prec)
}
fn _fs_parse_precision(format : String) -> Int {
let chars = format.to_array()
let mut i = 0
let mut dot = -1
while i < chars.length() {
if chars[i] == '.' {
dot = i
}
i = i + 1
}
if dot == -1 {
return 1
}
let mut num = ""
let mut j = dot + 1
while j < chars.length() && chars[j] >= '0' && chars[j] <= '9' {
num = num + chars[j].to_string()
j = j + 1
}
if num == "" { 1 } else { _fs_str_to_int(num) }
}
fn _fs_str_to_int(s : String) -> Int {
let a = s.to_array()
let mut r = 0
let mut i = 0
while i < a.length() {
if a[i] >= '0' && a[i] <= '9' {
r = r * 10 + (a[i].to_int() - '0'.to_int())
}
i = i + 1
}
r
}
// Parse a fixed-point string like "1000.0" / "-2.9" back into a Double, used to
// mirror python's rounding push-up check (abs(float(format % ...)) >= base).
fn _fs_str_to_double(s : String) -> Double {
let a = s.to_array()
let mut i = 0
let mut sign = 1.0
if a.length() > 0 && a[0] == '-' {
sign = -1.0
i = 1
}
let mut int_part = 0.0
while i < a.length() && a[i] != '.' {
if a[i] >= '0' && a[i] <= '9' {
int_part = int_part * 10.0 + (a[i].to_int() - '0'.to_int()).to_double()
}
i = i + 1
}
if i >= a.length() {
return sign * int_part
}
i = i + 1 // skip '.'
let mut frac = 0.0
let mut place = 0.1
while i < a.length() {
if a[i] >= '0' && a[i] <= '9' {
frac = frac + (a[i].to_int() - '0'.to_int()).to_double() * place
place = place * 0.1
}
i = i + 1
}
sign * (int_part + frac)
}
fn _fs_str_contains(s : String, sub : String) -> Bool {
let a = s.to_array()
let b = sub.to_array()
if b.length() == 0 {
return true
}
if b.length() > a.length() {
return false
}
let mut i = 0
while i + b.length() <= a.length() {
let mut k = 0
let mut ok = true
while k < b.length() {
if a[i + k] != b[k] {
ok = false
break
}
k = k + 1
}
if ok {
return true
}
i = i + 1
}
false
}
fn _fs_pow10(n : Int) -> Double {
let mut r = 1.0
let m = if n < 0 { -n } else { n }
let mut i = 0
while i < m {
r = r * 10.0
i = i + 1
}
if n < 0 { 1.0 / r } else { r }
}