use alloc::format;
use alloc::string::String;
use core::fmt::Write;
use crate::error::Result;
use crate::value::Value;
pub(crate) fn needs_space(prev: &Value, next: &Value) -> bool {
!matches!(prev, Value::String(_)) && !matches!(next, Value::String(_))
}
pub(crate) const PRINTF_MAX_LEN: usize = u16::MAX as usize;
fn clamp_fmt_len(n: usize) -> usize {
if n > PRINTF_MAX_LEN {
PRINTF_MAX_LEN
} else {
n
}
}
struct FmtSpec {
left_align: bool,
plus: bool,
space: bool,
hash: bool,
zero: bool,
width: Option<usize>,
precision: Option<usize>,
}
impl FmtSpec {
fn parse(chars: &mut core::iter::Peekable<core::str::Chars<'_>>) -> Self {
let mut spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
loop {
match chars.peek() {
Some('-') => {
spec.left_align = true;
chars.next();
}
Some('+') => {
spec.plus = true;
chars.next();
}
Some(' ') => {
spec.space = true;
chars.next();
}
Some('#') => {
spec.hash = true;
chars.next();
}
Some('0') => {
spec.zero = true;
chars.next();
}
_ => break,
}
}
let mut w = String::new();
while let Some(c) = chars.next_if(|c| c.is_ascii_digit()) {
w.push(c);
}
if !w.is_empty() {
spec.width = w.parse().ok().map(clamp_fmt_len);
}
if chars.peek() == Some(&'.') {
chars.next();
let mut p = String::new();
while let Some(c) = chars.next_if(|c| c.is_ascii_digit()) {
p.push(c);
}
spec.precision = Some(if p.is_empty() {
0
} else {
clamp_fmt_len(p.parse().unwrap_or(0))
});
}
spec
}
fn pad_in_place(&self, out: &mut String, start: usize, is_numeric: bool) {
let Some(width) = self.width else {
return;
};
let written = out[start..].chars().count();
if written >= width {
return;
}
let pad_count = width - written;
if self.left_align {
for _ in 0..pad_count {
out.push(' ');
}
} else if self.zero && is_numeric {
let sign_off = match out.as_bytes().get(start) {
Some(&b'-') | Some(&b'+') => 1,
_ => 0,
};
insert_repeated(out, start + sign_off, '0', pad_count);
} else {
insert_repeated(out, start, ' ', pad_count);
}
}
fn write_signed(&self, out: &mut String, n: i64) {
if n >= 0 {
if self.plus {
out.push('+');
} else if self.space {
out.push(' ');
}
}
let _ = write!(out, "{}", n);
}
}
fn insert_repeated(out: &mut String, pos: usize, ch: char, count: usize) {
let mut buf = [0u8; 4];
let s: &str = ch.encode_utf8(&mut buf);
out.insert_str(pos, &s.repeat(count));
}
pub(crate) fn sprintf(fmt_str: &str, args: &[Value]) -> Result<String> {
let mut out = String::with_capacity(fmt_str.len() + 16 * args.len());
sprintf_into(&mut out, fmt_str, args)?;
Ok(out)
}
fn sprintf_into(out: &mut String, fmt_str: &str, args: &[Value]) -> Result<()> {
let mut chars = fmt_str.chars().peekable();
let mut arg_idx = 0;
while let Some(ch) = chars.next() {
if ch != '%' {
out.push(ch);
continue;
}
let spec = FmtSpec::parse(&mut chars);
let verb = match chars.next() {
Some(v) => v,
None => {
out.push('%');
break;
}
};
if verb == '%' {
out.push('%');
continue;
}
let arg = if arg_idx < args.len() {
arg_idx += 1;
&args[arg_idx - 1]
} else {
let _ = write!(out, "%!{}(MISSING)", verb);
continue;
};
let start = out.len();
match verb {
's' => {
match spec.precision {
Some(prec) => write_display_truncated(out, arg, prec),
None => {
let _ = write!(out, "{}", arg);
}
}
spec.pad_in_place(out, start, false);
}
'd' => match arg.as_int() {
Some(n) => {
spec.write_signed(out, n);
spec.pad_in_place(out, start, true);
}
None => write_bad_verb(out, verb, arg),
},
'f' => match arg.as_float() {
Some(f) => {
let prec = spec.precision.unwrap_or(6);
if f >= 0.0 && !f.is_nan() {
if spec.plus {
out.push('+');
} else if spec.space {
out.push(' ');
}
}
let _ = write!(out, "{:.prec$}", f);
spec.pad_in_place(out, start, true);
}
None => write_bad_verb(out, verb, arg),
},
'e' | 'E' => match arg.as_float() {
Some(f) => {
let prec = spec.precision.unwrap_or(6);
let raw = if verb == 'e' {
format!("{:.prec$e}", f)
} else {
format!("{:.prec$E}", f)
};
write_normalized_sci(out, &raw, verb == 'E', false);
apply_float_sign_in_place(out, start, f, &spec);
spec.pad_in_place(out, start, true);
}
None => write_bad_verb(out, verb, arg),
},
'g' | 'G' => match arg.as_float() {
Some(f) => {
if f.is_nan() || f.is_infinite() {
let _ = write!(out, "{}", f);
} else if let Some(prec) = spec.precision {
write_g_with_precision(out, f, prec.max(1), verb == 'G');
} else {
write_g_default(out, f, verb == 'G');
};
apply_float_sign_in_place(out, start, f, &spec);
spec.pad_in_place(out, start, true);
}
None => write_bad_verb(out, verb, arg),
},
'v' => {
let _ = write!(out, "{}", arg);
spec.pad_in_place(out, start, false);
}
'q' => match arg {
Value::String(s) => {
if !(spec.hash && try_backquote_into(out, s)) {
go_quote_into(out, s);
}
spec.pad_in_place(out, start, false);
}
Value::Int(n) => {
if let Some(c) = u32::try_from(*n).ok().and_then(char::from_u32) {
let _ = write!(out, "'{}'", c.escape_default());
spec.pad_in_place(out, start, false);
} else {
write_bad_verb(out, verb, arg);
}
}
_ => write_bad_verb(out, verb, arg),
},
't' => match arg {
Value::Bool(b) => {
out.push_str(if *b { "true" } else { "false" });
spec.pad_in_place(out, start, false);
}
_ => write_bad_verb(out, verb, arg),
},
'x' | 'X' | 'o' | 'b' => match arg {
Value::String(s) if verb == 'x' || verb == 'X' => {
format_string_hex_into(out, s, verb == 'X', &spec);
spec.pad_in_place(out, start, false);
}
_ => match arg.as_int() {
Some(n) => {
format_int_base_into(out, n, verb, &spec);
spec.pad_in_place(out, start, true);
}
None => write_bad_verb(out, verb, arg),
},
},
'c' => match arg.as_int() {
Some(n) => match u32::try_from(n).ok().and_then(char::from_u32) {
Some(c) => out.push(c),
None => out.push('\u{FFFD}'),
},
None => write_bad_verb(out, verb, arg),
},
_ => write_bad_verb(out, verb, arg),
}
}
Ok(())
}
fn write_display_truncated(out: &mut String, arg: &Value, max_chars: usize) {
struct Truncate<'a> {
out: &'a mut String,
remaining: usize,
}
impl core::fmt::Write for Truncate<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for ch in s.chars() {
if self.remaining == 0 {
return Ok(());
}
self.out.push(ch);
self.remaining -= 1;
}
Ok(())
}
}
let mut w = Truncate {
out,
remaining: max_chars,
};
let _ = write!(w, "{}", arg);
}
fn apply_float_sign_in_place(out: &mut String, start: usize, f: f64, spec: &FmtSpec) {
if f < 0.0 || f.is_nan() {
return;
}
if matches!(out.as_bytes().get(start), Some(&b'-')) {
return;
}
if spec.plus {
out.insert(start, '+');
} else if spec.space {
out.insert(start, ' ');
}
}
fn write_bad_verb(out: &mut String, verb: char, arg: &Value) {
let _ = write!(out, "%!{}({}=", verb, arg.type_name());
match arg {
Value::String(s) => out.push_str(s),
other => {
let _ = write!(out, "{}", other);
}
}
out.push(')');
}
fn format_string_hex_into(out: &mut String, s: &str, upper: bool, spec: &FmtSpec) {
let bytes = s.as_bytes();
let take = spec
.precision
.map(|p| p.min(bytes.len()))
.unwrap_or(bytes.len());
out.reserve(take * 2);
for b in &bytes[..take] {
let _ = if upper {
write!(out, "{:02X}", b)
} else {
write!(out, "{:02x}", b)
};
}
}
fn go_quote_into(out: &mut String, s: &str) {
out.reserve(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\x07' => out.push_str("\\a"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
'\x0B' => out.push_str("\\v"),
c if (c as u32) < 0x20 || c == '\x7F' => {
let _ = write!(out, "\\x{:02x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
fn try_backquote_into(out: &mut String, s: &str) -> bool {
if s.contains('`') {
return false;
}
for ch in s.chars() {
if ch != '\t' && (ch < ' ' || ch == '\x7F') {
return false;
}
}
out.reserve(s.len() + 2);
out.push('`');
out.push_str(s);
out.push('`');
true
}
fn write_normalized_sci(out: &mut String, raw: &str, upper: bool, strip_zeros: bool) {
let e_pos = raw.bytes().position(|b| b == b'e' || b == b'E');
let Some(e_pos) = e_pos else {
out.push_str(if strip_zeros {
trim_trailing_zeros_view(raw)
} else {
raw
});
return;
};
let mut mantissa = &raw[..e_pos];
if strip_zeros && mantissa.contains('.') {
mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
}
out.push_str(mantissa);
out.push(if upper { 'E' } else { 'e' });
let exp_str = &raw[e_pos + 1..];
let (sign, digits) = if let Some(d) = exp_str.strip_prefix('-') {
('-', d)
} else if let Some(d) = exp_str.strip_prefix('+') {
('+', d)
} else {
('+', exp_str)
};
out.push(sign);
if digits.len() < 2 {
out.push('0');
}
out.push_str(digits);
}
fn trim_trailing_zeros_view(s: &str) -> &str {
if !s.contains('.') {
return s;
}
s.trim_end_matches('0').trim_end_matches('.')
}
fn decimal_exp(f: f64) -> i32 {
let s = format!("{:e}", f.abs());
s.find('e')
.and_then(|i| s[i + 1..].parse::<i32>().ok())
.unwrap_or(0)
}
fn write_g_default(out: &mut String, f: f64, upper: bool) {
if f == 0.0 {
out.push_str(if f.is_sign_negative() { "-0" } else { "0" });
return;
}
let exp = decimal_exp(f);
if !(-4..6).contains(&exp) {
let raw = format!("{:e}", f);
write_normalized_sci(out, &raw, upper, false);
} else {
let _ = write!(out, "{}", f);
}
}
fn write_g_with_precision(out: &mut String, f: f64, prec: usize, upper: bool) {
if f == 0.0 {
out.push_str(if f.is_sign_negative() { "-0" } else { "0" });
return;
}
let exp = decimal_exp(f);
if exp < -4 || exp >= prec as i32 {
let e_prec = prec.saturating_sub(1);
let raw = format!("{:.prec$e}", f, prec = e_prec);
write_normalized_sci(out, &raw, upper, true);
} else {
let f_prec = if prec as i32 > exp + 1 {
(prec as i32 - exp - 1) as usize
} else {
0
};
let raw = format!("{:.prec$}", f, prec = f_prec);
out.push_str(trim_trailing_zeros_view(&raw));
}
}
fn format_int_base_into(out: &mut String, n: i64, base: char, spec: &FmtSpec) {
let abs = n.unsigned_abs();
if n < 0 {
out.push('-');
}
if spec.hash {
match base {
'x' => out.push_str("0x"),
'X' => out.push_str("0X"),
'o' => out.push('0'),
'b' => out.push_str("0b"),
#[allow(
clippy::unreachable,
reason = "private helper; callers only pass 'x', 'X', 'o', 'b'"
)]
_ => unreachable!(),
}
}
let _ = match base {
'x' => write!(out, "{:x}", abs),
'X' => write!(out, "{:X}", abs),
'o' => write!(out, "{:o}", abs),
'b' => write!(out, "{:b}", abs),
#[allow(
clippy::unreachable,
reason = "private helper; callers only pass 'x', 'X', 'o', 'b'"
)]
_ => unreachable!(),
};
}
pub fn html_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\0' => out.push('\u{FFFD}'), '&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}
pub fn js_escape(s: &str) -> String {
let mut out = String::new();
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\'' => out.push_str("\\'"),
'<' => out.push_str("\\u003C"),
'>' => out.push_str("\\u003E"),
'&' => out.push_str("\\u0026"),
'=' => out.push_str("\\u003D"),
_ if (ch as u32) < 0x20 => {
write!(out, "\\u{:04X}", ch as u32).ok();
}
_ => out.push(ch),
}
}
out
}
pub fn url_encode(s: &str) -> String {
let mut out = String::new();
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
write!(out, "%{:02X}", byte).ok();
}
}
}
out
}
pub(crate) fn parse_hex_float(s: &str) -> Option<f64> {
let negative = s.starts_with('-');
let s = s.trim_start_matches('+').trim_start_matches('-');
let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?;
let (mantissa_str, exp_str) = if let Some(p) = s.find(['p', 'P']) {
(&s[..p], &s[p + 1..])
} else {
(s, "0")
};
let mantissa = if let Some(dot) = mantissa_str.find('.') {
let int_part = &mantissa_str[..dot];
let frac_part = &mantissa_str[dot + 1..];
let int_val = if int_part.is_empty() {
0u64
} else {
u64::from_str_radix(int_part, 16).ok()?
};
let frac_val = if frac_part.is_empty() {
0u64
} else {
u64::from_str_radix(frac_part, 16).ok()?
};
let frac_bits = frac_part.len() as i32 * 4;
int_val as f64 + frac_val as f64 / (2f64).powi(frac_bits)
} else {
u64::from_str_radix(mantissa_str, 16).ok()? as f64
};
let exp: i32 = exp_str.parse().ok()?;
let result = mantissa * (2.0_f64).powi(exp);
Some(if negative { -result } else { result })
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
fn sf(fmt: &str, args: &[Value]) -> String {
sprintf(fmt, args).unwrap()
}
impl FmtSpec {
fn pad(&self, s: &str, is_numeric: bool) -> String {
let mut out = String::from(s);
self.pad_in_place(&mut out, 0, is_numeric);
out
}
fn format_signed(&self, n: i64) -> String {
let mut out = String::new();
self.write_signed(&mut out, n);
out
}
}
fn go_quote(s: &str) -> String {
let mut out = String::new();
go_quote_into(&mut out, s);
out
}
fn go_backquote(s: &str) -> Option<String> {
let mut out = String::new();
if try_backquote_into(&mut out, s) {
Some(out)
} else {
None
}
}
fn format_int_base(n: i64, base: &str, spec: &FmtSpec) -> String {
let mut out = String::new();
let ch = base.chars().next().unwrap_or('x');
format_int_base_into(&mut out, n, ch, spec);
out
}
fn go_normalize_sci(s: &str) -> String {
let mut out = String::new();
let upper = s.bytes().any(|b| b == b'E');
write_normalized_sci(&mut out, s, upper, false);
out
}
fn strip_trailing_zeros(s: &str) -> String {
trim_trailing_zeros_view(s).to_string()
}
fn strip_trailing_zeros_sci(s: &str) -> String {
if let Some(e_pos) = s.bytes().position(|b| b == b'e' || b == b'E') {
let mantissa = trim_trailing_zeros_view(&s[..e_pos]);
let mut out = String::with_capacity(s.len());
out.push_str(mantissa);
out.push_str(&s[e_pos..]);
out
} else {
trim_trailing_zeros_view(s).to_string()
}
}
fn format_g_default(f: f64, upper: bool) -> String {
let mut out = String::new();
write_g_default(&mut out, f, upper);
out
}
fn format_g_with_precision(f: f64, prec: usize, upper: bool) -> String {
let mut out = String::new();
write_g_with_precision(&mut out, f, prec, upper);
out
}
#[test]
fn needs_space_two_ints() {
assert!(needs_space(&Value::Int(1), &Value::Int(2)));
}
#[test]
fn needs_space_two_strings() {
let a = Value::String("a".into());
let b = Value::String("b".into());
assert!(!needs_space(&a, &b));
}
#[test]
fn needs_space_string_int() {
let s = Value::String("x".into());
assert!(!needs_space(&s, &Value::Int(1)));
assert!(!needs_space(&Value::Int(1), &s));
}
#[test]
fn needs_space_bool_bool() {
assert!(needs_space(&Value::Bool(true), &Value::Bool(false)));
}
#[test]
fn quote_simple() {
assert_eq!(go_quote("hello"), r#""hello""#);
}
#[test]
fn quote_empty() {
assert_eq!(go_quote(""), r#""""#);
}
#[test]
fn quote_special_chars() {
assert_eq!(go_quote("a\"b"), r#""a\"b""#);
assert_eq!(go_quote("a\\b"), r#""a\\b""#);
assert_eq!(go_quote("a\nb"), r#""a\nb""#);
assert_eq!(go_quote("a\tb"), r#""a\tb""#);
assert_eq!(go_quote("a\rb"), r#""a\rb""#);
}
#[test]
fn quote_bell_backspace_formfeed_vtab() {
assert_eq!(go_quote("\x07"), r#""\a""#);
assert_eq!(go_quote("\x08"), r#""\b""#);
assert_eq!(go_quote("\x0C"), r#""\f""#);
assert_eq!(go_quote("\x0B"), r#""\v""#);
}
#[test]
fn quote_control_chars() {
assert_eq!(go_quote("\x01"), r#""\x01""#);
assert_eq!(go_quote("\x1f"), r#""\x1f""#);
assert_eq!(go_quote("\x7f"), r#""\x7f""#);
}
#[test]
fn quote_unicode_passthrough() {
assert_eq!(go_quote("caf\u{00e9}"), "\"caf\u{00e9}\"");
}
#[test]
fn backquote_simple() {
assert_eq!(go_backquote("hello"), Some("`hello`".into()));
}
#[test]
fn backquote_with_tab() {
assert_eq!(go_backquote("a\tb"), Some("`a\tb`".into()));
}
#[test]
fn backquote_rejects_backtick() {
assert_eq!(go_backquote("hel`lo"), None);
}
#[test]
fn backquote_rejects_control_char() {
assert_eq!(go_backquote("a\x01b"), None);
assert_eq!(go_backquote("a\nb"), None);
assert_eq!(go_backquote("\x7f"), None);
}
#[test]
fn backquote_empty() {
assert_eq!(go_backquote(""), Some("``".into()));
}
#[test]
fn normalize_positive_single_digit_exp() {
assert_eq!(go_normalize_sci("1.5e0"), "1.5e+00");
assert_eq!(go_normalize_sci("1.5e2"), "1.5e+02");
assert_eq!(go_normalize_sci("1.5e9"), "1.5e+09");
}
#[test]
fn normalize_negative_single_digit_exp() {
assert_eq!(go_normalize_sci("1.5e-3"), "1.5e-03");
assert_eq!(go_normalize_sci("1.5e-9"), "1.5e-09");
}
#[test]
fn normalize_already_two_digit_exp() {
assert_eq!(go_normalize_sci("1.5e+10"), "1.5e+10");
assert_eq!(go_normalize_sci("1.5e-10"), "1.5e-10");
}
#[test]
fn normalize_large_exp() {
assert_eq!(go_normalize_sci("5e-324"), "5e-324");
assert_eq!(go_normalize_sci("1e308"), "1e+308");
}
#[test]
fn normalize_uppercase() {
assert_eq!(go_normalize_sci("1.5E2"), "1.5E+02");
assert_eq!(go_normalize_sci("1.5E-3"), "1.5E-03");
}
#[test]
fn normalize_no_exponent() {
assert_eq!(go_normalize_sci("3.14"), "3.14");
}
#[test]
fn normalize_explicit_plus() {
assert_eq!(go_normalize_sci("1.5e+2"), "1.5e+02");
}
#[test]
fn strip_zeros_basic() {
assert_eq!(strip_trailing_zeros("1.50000"), "1.5");
assert_eq!(strip_trailing_zeros("1.0"), "1");
assert_eq!(strip_trailing_zeros("1.20"), "1.2");
}
#[test]
fn strip_zeros_no_dot() {
assert_eq!(strip_trailing_zeros("42"), "42");
}
#[test]
fn strip_zeros_no_trailing() {
assert_eq!(strip_trailing_zeros("1.23"), "1.23");
}
#[test]
fn strip_zeros_all_fractional_zeros() {
assert_eq!(strip_trailing_zeros("5.000"), "5");
}
#[test]
fn strip_zeros_sci_basic() {
assert_eq!(strip_trailing_zeros_sci("1.50000e+02"), "1.5e+02");
assert_eq!(strip_trailing_zeros_sci("1.00000e+02"), "1e+02");
}
#[test]
fn strip_zeros_sci_no_trailing() {
assert_eq!(strip_trailing_zeros_sci("1.23e+02"), "1.23e+02");
}
#[test]
fn strip_zeros_sci_uppercase() {
assert_eq!(strip_trailing_zeros_sci("1.50000E+02"), "1.5E+02");
}
#[test]
fn g_default_zero() {
assert_eq!(format_g_default(0.0, false), "0");
}
#[test]
fn g_default_negative_zero() {
assert_eq!(format_g_default(-0.0, false), "-0");
}
#[test]
fn g_default_small() {
assert_eq!(format_g_default(3.5, false), "3.5");
assert_eq!(format_g_default(0.5, false), "0.5");
assert_eq!(format_g_default(100000.0, false), "100000");
}
#[test]
fn g_default_switches_to_sci() {
assert_eq!(format_g_default(1e6, false), "1e+06");
assert_eq!(format_g_default(1e7, false), "1e+07");
}
#[test]
fn g_default_very_small_switches_to_sci() {
assert_eq!(format_g_default(0.000035, false), "3.5e-05");
}
#[test]
fn g_default_upper() {
assert_eq!(format_g_default(1e7, true), "1E+07");
}
#[test]
fn g_prec_basic() {
assert_eq!(format_g_with_precision(3.5, 4, false), "3.5");
assert_eq!(format_g_with_precision(1.5, 6, false), "1.5");
}
#[test]
fn g_prec_switches_to_sci() {
assert_eq!(format_g_with_precision(123456.0, 4, false), "1.235e+05");
}
#[test]
fn g_prec_rounding() {
assert_eq!(format_g_with_precision(10.0, 2, false), "10");
assert_eq!(format_g_with_precision(100.0, 2, false), "1e+02");
}
#[test]
fn g_prec_small_number() {
assert_eq!(format_g_with_precision(0.0123, 2, false), "0.012");
}
#[test]
fn g_prec_zero() {
assert_eq!(format_g_with_precision(0.0, 4, false), "0");
}
#[test]
fn g_prec_upper() {
assert_eq!(format_g_with_precision(1e7, 4, true), "1E+07");
}
#[test]
fn int_base_hex() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(format_int_base(255, "x", &spec), "ff");
assert_eq!(format_int_base(255, "X", &spec), "FF");
assert_eq!(format_int_base(-255, "x", &spec), "-ff");
}
#[test]
fn int_base_octal() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(format_int_base(8, "o", &spec), "10");
}
#[test]
fn int_base_binary() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(format_int_base(10, "b", &spec), "1010");
}
#[test]
fn int_base_hash_prefix() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: true,
zero: false,
width: None,
precision: None,
};
assert_eq!(format_int_base(255, "x", &spec), "0xff");
assert_eq!(format_int_base(255, "X", &spec), "0XFF");
assert_eq!(format_int_base(8, "o", &spec), "010");
assert_eq!(format_int_base(10, "b", &spec), "0b1010");
assert_eq!(format_int_base(-255, "x", &spec), "-0xff");
}
#[test]
fn html_basic() {
assert_eq!(html_escape("<b>hi</b>"), "<b>hi</b>");
}
#[test]
fn html_ampersand() {
assert_eq!(html_escape("a&b"), "a&b");
}
#[test]
fn html_quotes() {
assert_eq!(html_escape(r#"a"b'c"#), "a"b'c");
}
#[test]
fn html_nul() {
assert_eq!(html_escape("a\0b"), "a\u{FFFD}b");
}
#[test]
fn html_passthrough() {
assert_eq!(html_escape("hello world"), "hello world");
}
#[test]
fn html_empty() {
assert_eq!(html_escape(""), "");
}
#[test]
fn js_basic() {
assert_eq!(js_escape("It'd be nice."), "It\\'d be nice.");
}
#[test]
fn js_backslash_and_quotes() {
assert_eq!(js_escape(r#"a\b"c"#), r#"a\\b\"c"#);
}
#[test]
fn js_newline_tab() {
assert_eq!(js_escape("a\nb\tc"), "a\\u000Ab\\u0009c");
}
#[test]
fn js_angle_brackets_ampersand_equals() {
assert_eq!(js_escape("<b>&="), "\\u003Cb\\u003E\\u0026\\u003D");
}
#[test]
fn js_control_char() {
assert_eq!(js_escape("\x01"), "\\u0001"); }
#[test]
fn js_carriage_return() {
assert_eq!(js_escape("\r"), "\\u000D");
}
#[test]
fn js_passthrough() {
assert_eq!(js_escape("hello"), "hello");
}
#[test]
fn js_empty() {
assert_eq!(js_escape(""), "");
}
#[test]
fn url_basic() {
assert_eq!(url_encode("hello world"), "hello%20world");
}
#[test]
fn url_unreserved_passthrough() {
assert_eq!(url_encode("az-AZ-09-_.~"), "az-AZ-09-_.~");
}
#[test]
fn url_special_chars() {
assert_eq!(url_encode("a&b=c"), "a%26b%3Dc");
}
#[test]
fn url_slash() {
assert_eq!(
url_encode("http://www.example.org/"),
"http%3A%2F%2Fwww.example.org%2F"
);
}
#[test]
fn url_unicode() {
assert_eq!(url_encode("café"), "caf%C3%A9");
}
#[test]
fn url_empty() {
assert_eq!(url_encode(""), "");
}
#[test]
fn hex_float_basic() {
assert_eq!(parse_hex_float("0x1.ep+2"), Some(7.5));
}
#[test]
fn hex_float_no_frac() {
assert_eq!(parse_hex_float("0x1p+4"), Some(16.0));
}
#[test]
fn hex_float_negative() {
assert_eq!(parse_hex_float("-0x1p-2"), Some(-0.25));
}
#[test]
fn hex_float_positive_sign() {
assert_eq!(parse_hex_float("+0x1.ep+2"), Some(7.5));
}
#[test]
fn hex_float_uppercase() {
assert_eq!(parse_hex_float("0X1.EP+2"), Some(7.5));
}
#[test]
fn hex_float_no_exponent() {
assert_eq!(parse_hex_float("0xA"), Some(10.0));
}
#[test]
fn hex_float_zero() {
assert_eq!(parse_hex_float("0x0p0"), Some(0.0));
}
#[test]
fn hex_float_invalid() {
assert_eq!(parse_hex_float("not_a_float"), None);
assert_eq!(parse_hex_float(""), None);
}
#[test]
fn sprintf_s_basic() {
assert_eq!(sf("%s", &[Value::String("hello".into())]), "hello");
}
#[test]
fn sprintf_s_non_string() {
assert_eq!(sf("%s", &[Value::Int(42)]), "42");
assert_eq!(sf("%s", &[Value::Bool(true)]), "true");
assert_eq!(sf("%s", &[Value::Nil]), "<nil>");
}
#[test]
fn sprintf_s_width() {
assert_eq!(sf("%10s", &[Value::String("hi".into())]), " hi");
assert_eq!(sf("%-10s", &[Value::String("hi".into())]), "hi ");
}
#[test]
fn sprintf_s_precision() {
assert_eq!(sf("%.3s", &[Value::String("hello".into())]), "hel");
assert_eq!(sf("%.10s", &[Value::String("hi".into())]), "hi");
}
#[test]
fn sprintf_s_precision_multibyte() {
assert_eq!(sf("%.3s", &[Value::String("café".into())]), "caf");
assert_eq!(sf("%.4s", &[Value::String("café".into())]), "café");
}
#[test]
fn sprintf_s_left_align_with_truncation() {
assert_eq!(sf("%-6.3s", &[Value::String("hello".into())]), "hel ");
}
#[test]
fn sprintf_s_zero_precision() {
assert_eq!(sf("%.0s", &[Value::String("hello".into())]), "");
}
#[test]
fn sprintf_d_basic() {
assert_eq!(sf("%d", &[Value::Int(42)]), "42");
assert_eq!(sf("%d", &[Value::Int(-42)]), "-42");
assert_eq!(sf("%d", &[Value::Int(0)]), "0");
}
#[test]
fn sprintf_d_plus_flag() {
assert_eq!(sf("%+d", &[Value::Int(42)]), "+42");
assert_eq!(sf("%+d", &[Value::Int(-42)]), "-42");
}
#[test]
fn sprintf_d_space_flag() {
assert_eq!(sf("% d", &[Value::Int(42)]), " 42");
assert_eq!(sf("% d", &[Value::Int(-42)]), "-42");
}
#[test]
fn sprintf_d_width() {
assert_eq!(sf("%10d", &[Value::Int(42)]), " 42");
assert_eq!(sf("%-10d", &[Value::Int(42)]), "42 ");
}
#[test]
fn sprintf_d_zero_pad() {
assert_eq!(sf("%06d", &[Value::Int(42)]), "000042");
assert_eq!(sf("%06d", &[Value::Int(-42)]), "-00042");
}
#[test]
fn sprintf_d_zero_pad_with_plus() {
assert_eq!(sf("%+06d", &[Value::Int(42)]), "+00042");
}
#[test]
fn sprintf_d_zero_pad_plus_negative() {
assert_eq!(sf("%+06d", &[Value::Int(-42)]), "-00042");
}
#[test]
fn sprintf_d_left_align_overrides_zero() {
assert_eq!(sf("%-06d", &[Value::Int(42)]), "42 ");
}
#[test]
fn sprintf_d_zero_pad_no_overpad() {
assert_eq!(sf("%02d", &[Value::Int(12345)]), "12345");
}
#[test]
fn sprintf_f_default_precision() {
assert_eq!(sf("%f", &[Value::Float(1.5)]), "1.500000");
assert_eq!(sf("%f", &[Value::Float(0.0)]), "0.000000");
}
#[test]
fn sprintf_f_explicit_precision() {
assert_eq!(sf("%.2f", &[Value::Float(1.5)]), "1.50");
assert_eq!(sf("%.0f", &[Value::Float(1.5)]), "2"); }
#[test]
fn sprintf_f_plus_flag() {
assert_eq!(sf("%+f", &[Value::Float(1.5)]), "+1.500000");
assert_eq!(sf("%+f", &[Value::Float(-1.5)]), "-1.500000");
}
#[test]
fn sprintf_f_space_flag() {
assert_eq!(sf("% f", &[Value::Float(1.5)]), " 1.500000");
assert_eq!(sf("% f", &[Value::Float(-1.5)]), "-1.500000");
}
#[test]
fn sprintf_e_default() {
assert_eq!(sf("%e", &[Value::Float(1.5)]), "1.500000e+00");
assert_eq!(sf("%e", &[Value::Float(100.0)]), "1.000000e+02");
assert_eq!(sf("%e", &[Value::Float(0.001)]), "1.000000e-03");
}
#[test]
fn sprintf_e_precision() {
assert_eq!(sf("%.2e", &[Value::Float(1.5)]), "1.50e+00");
assert_eq!(sf("%.0e", &[Value::Float(1.5)]), "2e+00");
}
#[test]
fn sprintf_e_uppercase() {
assert_eq!(sf("%E", &[Value::Float(1.5)]), "1.500000E+00");
}
#[test]
fn sprintf_e_plus_flag() {
assert_eq!(sf("%+e", &[Value::Float(1.5)]), "+1.500000e+00");
assert_eq!(sf("%+e", &[Value::Float(-1.5)]), "-1.500000e+00");
}
#[test]
fn sprintf_e_zero() {
assert_eq!(sf("%e", &[Value::Float(0.0)]), "0.000000e+00");
}
#[test]
fn sprintf_e_space_flag() {
assert_eq!(sf("% e", &[Value::Float(1.5)]), " 1.500000e+00");
assert_eq!(sf("% e", &[Value::Float(-1.5)]), "-1.500000e+00");
}
#[test]
fn sprintf_e_width() {
assert_eq!(sf("%16e", &[Value::Float(1.5)]), " 1.500000e+00");
}
#[test]
fn sprintf_e_plus_width_negative() {
assert_eq!(sf("%+16e", &[Value::Float(-1.5)]), " -1.500000e+00");
}
#[test]
fn sprintf_g_default() {
assert_eq!(sf("%g", &[Value::Float(3.5)]), "3.5");
assert_eq!(sf("%g", &[Value::Float(0.0)]), "0");
assert_eq!(sf("%g", &[Value::Float(1.0)]), "1");
}
#[test]
fn sprintf_g_large_switches_to_sci() {
assert_eq!(sf("%g", &[Value::Float(1e6)]), "1e+06");
assert_eq!(sf("%g", &[Value::Float(1e7)]), "1e+07");
}
#[test]
fn sprintf_g_small_switches_to_sci() {
assert_eq!(sf("%g", &[Value::Float(0.000035)]), "3.5e-05");
}
#[test]
fn sprintf_g_boundary() {
assert_eq!(sf("%g", &[Value::Float(100000.0)]), "100000");
assert_eq!(sf("%g", &[Value::Float(1000000.0)]), "1e+06");
}
#[test]
fn sprintf_g_precision() {
assert_eq!(sf("%.4g", &[Value::Float(123456.0)]), "1.235e+05");
assert_eq!(sf("%.2g", &[Value::Float(10.0)]), "10");
assert_eq!(sf("%.2g", &[Value::Float(100.0)]), "1e+02");
}
#[test]
fn sprintf_g_uppercase() {
assert_eq!(sf("%G", &[Value::Float(1e7)]), "1E+07");
}
#[test]
fn sprintf_g_plus_flag() {
assert_eq!(sf("%+g", &[Value::Float(3.5)]), "+3.5");
assert_eq!(sf("%+g", &[Value::Float(-3.5)]), "-3.5");
}
#[test]
fn sprintf_g_negative_zero() {
assert_eq!(sf("%g", &[Value::Float(-0.0)]), "-0");
}
#[test]
fn sprintf_g_nan_no_sign_synthesis() {
assert_eq!(sf("%g", &[Value::Float(f64::NAN)]), "NaN");
assert_eq!(sf("%+g", &[Value::Float(f64::NAN)]), "NaN");
assert_eq!(sf("% g", &[Value::Float(f64::NAN)]), "NaN");
}
#[test]
fn sprintf_g_inf_with_sign_flags() {
assert_eq!(sf("%g", &[Value::Float(f64::INFINITY)]), "inf");
assert_eq!(sf("%g", &[Value::Float(f64::NEG_INFINITY)]), "-inf");
assert_eq!(sf("%+g", &[Value::Float(f64::INFINITY)]), "+inf");
}
#[test]
fn sprintf_g_width_and_sci() {
assert_eq!(sf("%10g", &[Value::Float(1e7)]), " 1e+07");
}
#[test]
fn sprintf_f_nan() {
assert_eq!(sf("%f", &[Value::Float(f64::NAN)]), "NaN");
assert_eq!(sf("%+f", &[Value::Float(f64::NAN)]), "NaN");
}
#[test]
fn sprintf_f_inf() {
assert_eq!(sf("%f", &[Value::Float(f64::INFINITY)]), "inf");
assert_eq!(sf("%f", &[Value::Float(f64::NEG_INFINITY)]), "-inf");
}
#[test]
fn sprintf_v() {
assert_eq!(sf("%v", &[Value::Int(42)]), "42");
assert_eq!(sf("%v", &[Value::String("hi".into())]), "hi");
assert_eq!(sf("%v", &[Value::Bool(true)]), "true");
assert_eq!(sf("%v", &[Value::Nil]), "<nil>");
}
#[test]
fn sprintf_v_width() {
assert_eq!(sf("%6v", &[Value::Int(42)]), " 42");
assert_eq!(sf("%-6v", &[Value::String("hi".into())]), "hi ");
}
#[test]
fn sprintf_q_basic() {
assert_eq!(sf("%q", &[Value::String("hello".into())]), r#""hello""#);
}
#[test]
fn sprintf_q_with_special() {
assert_eq!(sf("%q", &[Value::String("a\nb".into())]), r#""a\nb""#);
}
#[test]
fn sprintf_hash_q_backtick() {
assert_eq!(sf("%#q", &[Value::String("hello".into())]), "`hello`");
}
#[test]
fn sprintf_hash_q_fallback_on_backtick() {
assert_eq!(sf("%#q", &[Value::String("hel`lo".into())]), r#""hel`lo""#);
}
#[test]
fn sprintf_hash_q_fallback_on_control() {
assert_eq!(sf("%#q", &[Value::String("a\nb".into())]), r#""a\nb""#);
}
#[test]
fn sprintf_q_width() {
assert_eq!(sf("%8q", &[Value::String("hi".into())]), r#" "hi""#);
assert_eq!(sf("%-8q", &[Value::String("hi".into())]), r#""hi" "#);
}
#[test]
fn sprintf_hash_q_width() {
assert_eq!(sf("%#8q", &[Value::String("hi".into())]), " `hi`");
}
#[test]
fn sprintf_t() {
assert_eq!(sf("%t", &[Value::Bool(true)]), "true");
assert_eq!(sf("%t", &[Value::Bool(false)]), "false");
}
#[test]
fn sprintf_t_non_bool_emits_bad_verb() {
assert_eq!(sf("%t", &[Value::Int(42)]), "%!t(int=42)");
}
#[test]
fn sprintf_t_width() {
assert_eq!(sf("%6t", &[Value::Bool(true)]), " true");
assert_eq!(sf("%-6t", &[Value::Bool(true)]), "true ");
}
#[test]
fn sprintf_x_basic() {
assert_eq!(sf("%x", &[Value::Int(255)]), "ff");
assert_eq!(sf("%X", &[Value::Int(255)]), "FF");
}
#[test]
fn sprintf_x_negative() {
assert_eq!(sf("%x", &[Value::Int(-255)]), "-ff");
}
#[test]
fn sprintf_x_hash() {
assert_eq!(sf("%#x", &[Value::Int(255)]), "0xff");
assert_eq!(sf("%#X", &[Value::Int(255)]), "0XFF");
}
#[test]
fn sprintf_x_zero_pad() {
assert_eq!(sf("%04x", &[Value::Int(127)]), "007f");
}
#[test]
fn sprintf_x_left_align() {
assert_eq!(sf("%-6x", &[Value::Int(255)]), "ff ");
}
#[test]
fn sprintf_x_hash_zero_pad_negative() {
assert_eq!(sf("%#08x", &[Value::Int(-255)]), "-0000xff");
}
#[test]
fn sprintf_x_hash_zero_pad_positive() {
assert_eq!(sf("%#08x", &[Value::Int(255)]), "00000xff");
}
#[test]
fn sprintf_o() {
assert_eq!(sf("%o", &[Value::Int(8)]), "10");
assert_eq!(sf("%#o", &[Value::Int(8)]), "010");
}
#[test]
fn sprintf_b() {
assert_eq!(sf("%b", &[Value::Int(10)]), "1010");
assert_eq!(sf("%#b", &[Value::Int(10)]), "0b1010");
}
#[test]
fn sprintf_c() {
assert_eq!(sf("%c", &[Value::Int(65)]), "A");
assert_eq!(sf("%c", &[Value::Int(0x1F600)]), "\u{1F600}"); }
#[test]
fn sprintf_percent_literal() {
assert_eq!(sf("100%%", &[]), "100%");
}
#[test]
fn sprintf_multiple_verbs() {
assert_eq!(
sf(
"%d %s %d %s",
&[
Value::Int(1),
Value::String("one".into()),
Value::Int(2),
Value::String("two".into())
]
),
"1 one 2 two"
);
}
#[test]
fn sprintf_missing_arg() {
assert_eq!(sf("%d %d", &[Value::Int(1)]), "1 %!d(MISSING)");
}
#[test]
fn sprintf_no_args_no_verbs() {
assert_eq!(sf("hello world", &[]), "hello world");
}
#[test]
fn sprintf_trailing_percent() {
assert_eq!(sf("test%", &[]), "test%");
}
#[test]
fn sprintf_huge_width_is_clamped() {
let out = sf("%999999999999d", &[Value::Int(1)]);
assert!(out.len() <= PRINTF_MAX_LEN + 16, "got len {}", out.len());
}
#[test]
fn sprintf_huge_precision_is_clamped() {
let out = sf("%.999999999999f", &[Value::Float(1.0)]);
assert!(out.len() <= PRINTF_MAX_LEN + 16, "got len {}", out.len());
}
#[test]
fn sprintf_unknown_verb_emits_bad_verb() {
assert_eq!(sf("%z", &[Value::Int(1)]), "%!z(int=1)");
}
#[test]
fn sprintf_d_non_int_emits_bad_verb() {
assert_eq!(sf("%d", &[Value::String("abc".into())]), "%!d(string=abc)");
}
#[test]
fn sprintf_f_non_numeric_emits_bad_verb() {
assert_eq!(sf("%f", &[Value::String("abc".into())]), "%!f(string=abc)");
}
#[test]
fn sprintf_x_string_full() {
assert_eq!(sf("%x", &[Value::String("abc".into())]), "616263");
}
#[test]
fn sprintf_x_string_precision_limits_input_bytes() {
assert_eq!(sf("%.2x", &[Value::String("abc".into())]), "6162");
assert_eq!(sf("%.3x", &[Value::String("abc".into())]), "616263");
assert_eq!(sf("%.0x", &[Value::String("abc".into())]), "");
}
#[test]
fn sprintf_x_string_uppercase_verb() {
assert_eq!(sf("%X", &[Value::String("abc".into())]), "616263");
}
#[test]
fn sprintf_x_string_precision_over_length_is_capped() {
assert_eq!(sf("%.99x", &[Value::String("ab".into())]), "6162");
}
#[test]
fn pad_no_width() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(spec.pad("hello", false), "hello");
}
#[test]
fn pad_right_align() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: Some(10),
precision: None,
};
assert_eq!(spec.pad("hi", false), " hi");
}
#[test]
fn pad_left_align() {
let spec = FmtSpec {
left_align: true,
plus: false,
space: false,
hash: false,
zero: false,
width: Some(10),
precision: None,
};
assert_eq!(spec.pad("hi", false), "hi ");
}
#[test]
fn pad_zero_unsigned() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: true,
width: Some(6),
precision: None,
};
assert_eq!(spec.pad("42", true), "000042");
}
#[test]
fn pad_zero_negative() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: true,
width: Some(6),
precision: None,
};
assert_eq!(spec.pad("-42", true), "-00042");
}
#[test]
fn pad_zero_positive_sign() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: true,
width: Some(6),
precision: None,
};
assert_eq!(spec.pad("+42", true), "+00042");
}
#[test]
fn pad_wider_than_needed() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: Some(2),
precision: None,
};
assert_eq!(spec.pad("hello", false), "hello");
}
#[test]
fn format_signed_plain() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(spec.format_signed(42), "42");
assert_eq!(spec.format_signed(-42), "-42");
assert_eq!(spec.format_signed(0), "0");
}
#[test]
fn format_signed_plus() {
let spec = FmtSpec {
left_align: false,
plus: true,
space: false,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(spec.format_signed(42), "+42");
assert_eq!(spec.format_signed(-42), "-42");
assert_eq!(spec.format_signed(0), "+0");
}
#[test]
fn format_signed_space() {
let spec = FmtSpec {
left_align: false,
plus: false,
space: true,
hash: false,
zero: false,
width: None,
precision: None,
};
assert_eq!(spec.format_signed(42), " 42");
assert_eq!(spec.format_signed(-42), "-42");
assert_eq!(spec.format_signed(0), " 0");
}
}