//! Padding and fixed-point formatting, as methods on `Int`, `String` and
//! `Float`.
//!
//! `pad` writes an integer padded with zeros, `pad_left` and `pad_right`
//! align a string with spaces, and `fixed` writes a float with a set number
//! of decimal places. The padding methods never truncate: a value already as
//! wide as asked comes back as it is.
//!
//! Padding integers and strings to a width, and writing a float to a fixed
//! number of places:
//!
//! ```
//! assert_eq(7.pad(3), "007")
//! assert_eq((-5).pad(3), "-05")
//! assert_eq(1234.pad(2), "1234") // never truncated
//! assert_eq("ab".pad_left(4), " ab")
//! assert_eq("ab".pad_right(4), "ab ")
//! assert_eq((100.0 / 3.0).fixed(2), "33.33")
//! assert_eq((2.5).fixed(0), "2") // a tie goes to the even digit
//!
//! // A row of a table, in columns of fixed width.
//! let row = "${"apples".pad_right(8)}|${3.pad(4)}|${(0.5).fixed(2).pad_left(6)}"
//! assert_eq(row, "apples |0003| 0.50")
//! ```
// Nova standard library -- formatting.
//
// Methods rather than top-level `pub fn`s, and that is not a style choice.
// Nova has no imports and no qualified paths: every std module's public names
// are glob-imported into every other module, and a collision resolves
// *silently in the user's favour*, so a top-level `pub fn pad` would take
// `pad` from every Nova program ever written and a program with its own `pad`
// would lose access to this one with no diagnostic. `std/strings` refused the
// same trade for `join`; `std/log` refused it for five level names.
impl Int {
// Zero-pad to `width`, with the sign counted toward it: `(7).pad(2)` is
// "07" and `(-5).pad(3)` is "-05", matching Rust's `{:03}`. A value
// already at or beyond `width` is returned unpadded, never truncated --
// a formatter that drops digits turns a display concern into a
// correctness one.
//
// The early return is also the negative-width clamp: `len` is at least 1
// for every Int, so a negative `width` takes that branch and
// `String::repeat` -- which *panics* on a negative count -- is never
// reached with one. The upper end of `width` is not clamped at all --
// an absurdly large one (e.g. `i64::MAX`) reaches `String::repeat` and
// aborts cleanly with `array length is too large` (measured), guarded
// by the array-length check at `crates/nova-mir/src/lower.rs:1080-1101`
// rather than by anything in this function. A documented outcome, not
// a silent one.
/// Returns the integer as text, padded on the left with zeros to `width`
/// characters.
///
/// A negative number's minus sign counts toward `width` and stays in
/// front: `(-5).pad(3)` is `"-05"`. A number already `width` characters or
/// longer comes back without padding, never truncated, as does any number
/// when `width` is zero or negative.
pub fn pad(self, width: Int) -> String {
let s = "${self}"
let len = s.len()
if len >= width { return s }
let zeros = "0".repeat(width - len)
if self < 0 {
let digits = s.slice(1, len)
"-${zeros}${digits}"
} else {
"${zeros}${s}"
}
}
}
impl String {
// Right-align within `width` by padding with spaces on the left. At or
// beyond `width`, returned unchanged. The early return clamps a negative
// `width` before `repeat` can see it.
//
// `width` is counted in codepoints, as `String::len` counts them, not
// in display columns: a wide or combining character will not align by
// column. Measured: `"日本".pad_right(4)` is `"日本 "` (two spaces),
// four codepoints wide but six columns in a monospace terminal.
/// Returns the string right-aligned in `width` characters, with spaces
/// added on the left.
///
/// `width` counts characters, as `len` does, not columns on a screen, so a
/// wide character such as `日` counts as one. A string already `width`
/// characters or longer comes back as it is.
pub fn pad_left(self, width: Int) -> String {
let len = self.len()
if len >= width { return self }
let fill = " ".repeat(width - len)
"${fill}${self}"
}
// Left-align within `width` by padding with spaces on the right. Same
// codepoint-counted `width` as `pad_left` above.
/// Returns the string left-aligned in `width` characters, with spaces
/// added on the right.
///
/// `width` counts characters, as for `pad_left`. A string already `width`
/// characters or longer comes back as it is.
pub fn pad_right(self, width: Int) -> String {
let len = self.len()
if len >= width { return self }
let fill = " ".repeat(width - len)
"${self}${fill}"
}
}
impl Float {
// Fixed-place decimal: `(100.0 / 3.0).fixed(2)` is "33.33".
//
// The rendering is done by the runtime, deliberately breaking this
// project's habit of keeping policy in Nova. Decimal rendering of a
// binary float is a solved problem with sharp edges -- shortest
// round-trip, ties at the cut -- and Nova cannot begin on it, having no
// `Float` to `Int` conversion at all. This is the case where the Rust
// side is the correct implementation rather than the convenient one.
//
// `places` is clamped to 0..=17 by the intrinsic.
/// Returns the number as text with `places` digits after the decimal
/// point: `(100.0 / 3.0).fixed(2)` is `"33.33"`.
///
/// It rounds the float's exact binary value to the nearest such text, a
/// tie going to the even digit, so `(2.5).fixed(0)` is `"2"` and
/// `(1.005).fixed(2)` is `"1.00"`, 1.005 being stored as a little less.
/// A negative number that rounds to zero keeps its sign, as `-0.00`.
/// `places` is clamped to `0..=17`, and 0 gives no decimal point. It never
/// uses an exponent, and `NaN` and the infinities give `NaN`, `inf` and
/// `-inf`.
pub fn fixed(self, places: Int) -> String { float_fixed(self, places) }
}