use crate::bignum::{float_trunc_integer, mpfr_string_for_percent_s, value_to_mpfr};
use crate::runtime::Value;
use rug::float::Round;
use std::sync::atomic::{AtomicBool, Ordering};
pub static AWK_TRADITIONAL_MODE: AtomicBool = AtomicBool::new(false);
#[inline]
fn fmt_peek(fmt: &str, i: usize) -> Option<char> {
fmt.get(i..)?.chars().next()
}
pub fn awk_sprintf(fmt: &str, vals: &[Value]) -> Result<String, String> {
awk_sprintf_with_decimal(fmt, vals, '.', Some(','), None)
}
fn convfmt_preprocess_for_percent_s<'a>(
fmt: &str,
vals: &'a [Value],
convfmt: &str,
) -> std::borrow::Cow<'a, [Value]> {
if !fmt.contains("%s") && !fmt.contains("s$") {
return std::borrow::Cow::Borrowed(vals);
}
let bytes = fmt.as_bytes();
let mut percent_s_indices: Vec<usize> = Vec::new();
let mut i = 0;
let mut vi: usize = 0;
while i < bytes.len() {
if bytes[i] != b'%' {
i += 1;
continue;
}
i += 1;
if i >= bytes.len() {
break;
}
if bytes[i] == b'%' {
i += 1;
continue;
}
let mut pos: Option<usize> = None;
let mut j = i;
let mut m = 0usize;
let mut has_digits = false;
while j < bytes.len() && bytes[j].is_ascii_digit() {
m = m * 10 + (bytes[j] - b'0') as usize;
has_digits = true;
j += 1;
}
if has_digits && j < bytes.len() && bytes[j] == b'$' {
pos = Some(m);
i = j + 1;
}
while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'\'' | b'0') {
i += 1;
}
if i < bytes.len() && bytes[i] == b'*' {
i += 1;
let mut star_pos: Option<usize> = None;
let mut sm = 0usize;
let mut sj = i;
let mut s_has = false;
while sj < bytes.len() && bytes[sj].is_ascii_digit() {
sm = sm * 10 + (bytes[sj] - b'0') as usize;
s_has = true;
sj += 1;
}
if s_has && sj < bytes.len() && bytes[sj] == b'$' {
star_pos = Some(sm);
i = sj + 1;
}
match star_pos {
Some(p) => vi = vi.max(p),
None => vi += 1,
}
} else {
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
if i < bytes.len() && bytes[i] == b'*' {
i += 1;
let mut star_pos: Option<usize> = None;
let mut sm = 0usize;
let mut sj = i;
let mut s_has = false;
while sj < bytes.len() && bytes[sj].is_ascii_digit() {
sm = sm * 10 + (bytes[sj] - b'0') as usize;
s_has = true;
sj += 1;
}
if s_has && sj < bytes.len() && bytes[sj] == b'$' {
star_pos = Some(sm);
i = sj + 1;
}
match star_pos {
Some(p) => vi = vi.max(p),
None => vi += 1,
}
} else {
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
}
}
while i < bytes.len() && matches!(bytes[i], b'h' | b'l' | b'L') {
i += 1;
}
if i >= bytes.len() {
break;
}
let conv = bytes[i];
i += 1;
let arg_idx = if let Some(p) = pos {
if p == 0 {
continue;
}
p - 1
} else {
let idx = vi;
vi += 1;
idx
};
if conv == b's' {
percent_s_indices.push(arg_idx);
}
}
if percent_s_indices.is_empty() {
return std::borrow::Cow::Borrowed(vals);
}
let mut out = vals.to_vec();
for &idx in &percent_s_indices {
if let Some(v) = out.get_mut(idx) {
if let Value::Num(n) = v {
let s = format_num_via_convfmt(*n, convfmt);
*v = Value::StrLit(s);
}
}
}
std::borrow::Cow::Owned(out)
}
fn format_num_via_convfmt(n: f64, convfmt: &str) -> String {
if n.is_finite() && n.fract() == 0.0 {
if n == 0.0 {
return "0".to_string();
}
return format!("{:.0}", n);
}
if !n.is_finite() {
let sign = if n.is_sign_negative() { '-' } else { '+' };
let body = if n.is_nan() { "nan" } else { "inf" };
return format!("{sign}{body}");
}
awk_sprintf_with_decimal(convfmt, &[Value::Num(n)], '.', Some(','), None)
.unwrap_or_else(|_| format!("{n}"))
}
pub fn awk_sprintf_with_convfmt(
fmt: &str,
vals: &[Value],
decimal: char,
thousands_sep: Option<char>,
mpfr_mode: Option<(u32, Round)>,
convfmt: &str,
) -> Result<String, String> {
let vals = convfmt_preprocess_for_percent_s(fmt, vals, convfmt);
awk_sprintf_with_decimal(fmt, &vals, decimal, thousands_sep, mpfr_mode)
}
pub fn awk_sprintf_with_decimal(
fmt: &str,
vals: &[Value],
decimal: char,
thousands_sep: Option<char>,
mpfr_mode: Option<(u32, Round)>,
) -> Result<String, String> {
let mut out = String::new();
let mut vi = 0usize;
let mut i = 0usize;
while i < fmt.len() {
let c = fmt_peek(fmt, i).ok_or_else(|| "truncated format".to_string())?;
if c != '%' {
out.push(c);
i += c.len_utf8();
continue;
}
i += c.len_utf8();
if i >= fmt.len() {
out.push('%');
break;
}
let start_after_pct = i;
let mut m = 0usize;
let mut has_digits = false;
while let Some(ch) = fmt_peek(fmt, i) {
if !ch.is_ascii_digit() {
break;
}
has_digits = true;
m = m * 10 + (ch as u8 - b'0') as usize;
i += ch.len_utf8();
}
let val_pos = if has_digits && fmt_peek(fmt, i) == Some('$') {
i += '$'.len_utf8();
if m == 0 {
return Err("sprintf: positional argument was 0".into());
}
Some(m)
} else {
i = start_after_pct;
None
};
let (piece, new_i) = parse_conversion_rest(
fmt,
i,
vals,
&mut vi,
val_pos,
decimal,
thousands_sep,
mpfr_mode,
)?;
i = new_i;
out.push_str(&piece);
}
Ok(out)
}
fn take_val<'a>(vals: &'a [Value], vi: &mut usize) -> Result<&'a Value, String> {
let v = vals
.get(*vi)
.ok_or_else(|| "sprintf: not enough arguments".to_string())?;
*vi += 1;
Ok(v)
}
fn val_at(vals: &[Value], one_based: usize) -> Result<&Value, String> {
vals.get(one_based - 1)
.ok_or_else(|| "sprintf: invalid positional argument".to_string())
}
fn parse_star_value(
fmt: &str,
mut i: usize,
vals: &[Value],
vi: &mut usize,
) -> Result<(f64, usize), String> {
let start = i;
let mut n = 0usize;
let mut has_digits = false;
while let Some(ch) = fmt_peek(fmt, i) {
if !ch.is_ascii_digit() {
break;
}
has_digits = true;
n = n * 10 + (ch as u8 - b'0') as usize;
i += ch.len_utf8();
}
if has_digits && fmt_peek(fmt, i) == Some('$') {
i += '$'.len_utf8();
let v = val_at(vals, n)?;
*vi = (*vi).max(n);
return Ok((v.as_number(), i));
}
i = start;
let v = take_val(vals, vi)?;
Ok((v.as_number(), i))
}
#[allow(clippy::too_many_arguments)] fn parse_conversion_rest(
fmt: &str,
mut i: usize,
vals: &[Value],
vi: &mut usize,
val_pos: Option<usize>,
decimal: char,
thousands_sep: Option<char>,
mpfr_mode: Option<(u32, Round)>,
) -> Result<(String, usize), String> {
let mut left = false;
let mut sign = false;
let mut space = false;
let mut alt = false;
let mut pad_zero = false;
let mut group = false;
while let Some(flag) = fmt_peek(fmt, i) {
match flag {
'-' => {
left = true;
i += flag.len_utf8();
}
'+' => {
sign = true;
i += flag.len_utf8();
}
' ' => {
space = true;
i += flag.len_utf8();
}
'#' => {
alt = true;
i += flag.len_utf8();
}
'\'' => {
group = true;
i += flag.len_utf8();
}
'0' => {
pad_zero = true;
i += flag.len_utf8();
}
_ => break,
}
}
let (width, star_left, i2) = parse_width_or_star(fmt, i, vals, vi)?;
i = i2;
if star_left {
left = true;
}
let mut prec: Option<usize> = None;
if fmt_peek(fmt, i) == Some('.') {
i += '.'.len_utf8();
if fmt_peek(fmt, i) == Some('*') {
i += '*'.len_utf8();
let (p, i2) = parse_star_value(fmt, i, vals, vi)?;
i = i2;
prec = Some(if p < 0.0 { 0 } else { p as usize });
} else {
let mut p = 0usize;
let mut any = false;
while let Some(d) = fmt_peek(fmt, i) {
if !d.is_ascii_digit() {
break;
}
p = p * 10 + (d as u8 - b'0') as usize;
any = true;
i += d.len_utf8();
}
prec = if any { Some(p) } else { Some(0) };
}
}
while matches!(fmt_peek(fmt, i), Some('h' | 'l' | 'L')) {
let m = fmt_peek(fmt, i).unwrap();
i += m.len_utf8();
}
let conv = fmt_peek(fmt, i).ok_or_else(|| "truncated format".to_string())?;
i += conv.len_utf8();
if conv == '%' {
return Ok(("%".to_string(), i));
}
if !is_known_conv(conv) {
return Ok((format!("%{conv}"), i));
}
let v = if let Some(p) = val_pos {
val_at(vals, p)?
} else {
take_val(vals, vi)?
};
let piece = format_one(
conv,
v,
left,
sign,
space,
alt,
pad_zero,
group,
width,
prec,
decimal,
thousands_sep,
mpfr_mode,
)?;
Ok((piece, i))
}
fn is_known_conv(c: char) -> bool {
matches!(
c,
's' | 'd'
| 'i'
| 'u'
| 'o'
| 'x'
| 'X'
| 'a'
| 'A'
| 'f'
| 'F'
| 'e'
| 'E'
| 'g'
| 'G'
| 'c'
)
}
fn insert_thousands_sep_float(s: String, sep: char, decimal: char) -> String {
let (int_part, frac_part) = match s.find(decimal) {
Some(i) => (&s[..i], &s[i..]),
None => (s.as_str(), ""),
};
let int_grouped = insert_thousands_sep(int_part.to_string(), sep);
if frac_part.is_empty() {
int_grouped
} else {
format!("{int_grouped}{frac_part}")
}
}
fn insert_thousands_sep(s: String, sep: char) -> String {
if sep == '\0' || s.is_empty() {
return s;
}
let neg = s.starts_with('-');
let digit_part = if neg { &s[1..] } else { &s[..] };
if digit_part.is_empty() {
return s;
}
let mut out = String::new();
if neg {
out.push('-');
}
let len = digit_part.len();
for (i, c) in digit_part.chars().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
out.push(sep);
}
out.push(c);
}
out
}
fn localize_float_radix(s: String, decimal: char) -> String {
if decimal == '.' {
return s;
}
let rep = decimal.to_string();
s.replacen('.', &rep, 1)
}
fn trim_trailing_zero_fraction(s: &str) -> String {
if !s.contains('.') {
return s.to_string();
}
let mut t = s.trim_end_matches('0').to_string();
if t.ends_with('.') {
t.pop();
}
t
}
fn format_non_finite(n: f64, upper: bool) -> Option<String> {
if n.is_finite() {
return None;
}
let body = if n.is_nan() {
if upper {
"NAN"
} else {
"nan"
}
} else if upper {
"INF"
} else {
"inf"
};
let sign = if n.is_sign_negative() { '-' } else { '+' };
Some(format!("{sign}{body}"))
}
fn format_sprintf_exponent(exp: i32, upper_e: bool) -> String {
let ec = if upper_e { 'E' } else { 'e' };
let sign = if exp < 0 { '-' } else { '+' };
let mag = exp.unsigned_abs();
let w = if mag == 0 {
2usize
} else {
(mag.ilog10() as usize + 1).max(2)
};
format!("{ec}{sign}{mag:0w$}", w = w)
}
fn format_hex_float(n: f64, prec: Option<usize>, upper: bool, alt: bool) -> String {
if let Some(s) = format_non_finite(n, upper) {
return s;
}
if n == 0.0 {
let sign = if n.is_sign_negative() { "-" } else { "" };
let prefix = if upper { "0X" } else { "0x" };
let p = prec.unwrap_or(0);
let dot = if p > 0 || alt { "." } else { "" };
let frac = "0".repeat(p);
let exp_char = if upper { 'P' } else { 'p' };
return format!("{sign}{prefix}0{dot}{frac}{exp_char}+0");
}
let sign = if n < 0.0 { "-" } else { "" };
let abs_n = n.abs();
let bits: u64 = abs_n.to_bits();
let raw_exp = ((bits >> 52) & 0x7FF) as i64;
let raw_mant = bits & 0x000F_FFFF_FFFF_FFFF;
let (exp, int_digit, frac_bits) = if raw_exp == 0 {
if raw_mant == 0 {
(0i64, 0u64, 0u64)
} else {
let shift = raw_mant.leading_zeros() as i64 - 12; let normalized = raw_mant << shift;
let exp = -1022 - shift;
(exp, 1, normalized & 0x000F_FFFF_FFFF_FFFF)
}
} else {
(raw_exp - 1023, 1, raw_mant)
};
let (int_digit, frac_bits) = if let Some(p) = prec {
if p < 13 {
let keep_bits = p * 4;
let drop_bits = 52 - keep_bits;
let half = 1u64 << (drop_bits - 1);
let mask = (1u64 << drop_bits) - 1;
let dropped = frac_bits & mask;
let mut kept = frac_bits >> drop_bits;
let mut id = int_digit;
let lsb = if p > 0 { kept & 1 } else { id & 1 };
if dropped > half || (dropped == half && lsb != 0) {
kept += 1;
if kept >= (1u64 << keep_bits) {
kept = 0;
id += 1;
}
}
(id, kept << drop_bits)
} else {
(int_digit, frac_bits)
}
} else {
(int_digit, frac_bits)
};
let full_frac = format!("{frac_bits:013x}");
let frac_str = match prec {
Some(0) if !alt => String::new(),
Some(p) => {
let needed = p.min(13);
if needed <= full_frac.len() {
format!(".{}", &full_frac[..needed])
} else {
let pad = needed - full_frac.len();
format!(".{}{}", full_frac, "0".repeat(pad))
}
}
None => {
let trimmed = full_frac.trim_end_matches('0');
if trimmed.is_empty() && !alt {
String::new()
} else if trimmed.is_empty() {
".".to_string()
} else {
format!(".{trimmed}")
}
}
};
let prefix = if upper { "0X" } else { "0x" };
let exp_char = if upper { 'P' } else { 'p' };
let exp_sign = if exp >= 0 { '+' } else { '-' };
let exp_abs = exp.unsigned_abs();
let int_hex = if upper {
format!("{int_digit:X}")
} else {
format!("{int_digit:x}")
};
let frac_str = if upper {
frac_str.to_uppercase()
} else {
frac_str
};
format!("{sign}{prefix}{int_hex}{frac_str}{exp_char}{exp_sign}{exp_abs}")
}
fn normalize_sprintf_scientific_exponent(s: &str) -> String {
let Some(pos) = s.find(['e', 'E']) else {
return s.to_string();
};
let (mant, rest) = s.split_at(pos);
let upper = rest.starts_with('E');
let exp: i32 = rest[1..].parse().unwrap_or(0);
format!("{}{}", mant, format_sprintf_exponent(exp, upper))
}
fn trim_sprintf_g_scientific(s: &str) -> String {
let Some(pos) = s.find(['e', 'E']) else {
return trim_trailing_zero_fraction(s);
};
let (mant, exp_with_e) = s.split_at(pos);
let upper = exp_with_e.starts_with('E');
let exp: i32 = exp_with_e[1..].parse().unwrap_or(0);
format!(
"{}{}",
trim_trailing_zero_fraction(mant),
format_sprintf_exponent(exp, upper)
)
}
fn format_g_decimal_significant_f64(mut n: f64, p: usize) -> String {
let p = p.max(1);
if !n.is_finite() {
return format!("{n}");
}
let neg = n.is_sign_negative();
n = n.abs();
if n == 0.0 {
return if neg {
"-0".to_string()
} else {
"0".to_string()
};
}
let e = n.log10().floor() as i32;
let sig_scale = 10f64.powi(p as i32 - 1 - e);
let r = (n * sig_scale).round() / sig_scale;
if r == 0.0 {
return if neg {
"-0".to_string()
} else {
"0".to_string()
};
}
let e2 = r.log10().floor() as i32;
let frac = (p as i32 - e2 - 1).max(0) as usize;
let body = format!("{:.*}", frac, r);
if neg {
format!("-{body}")
} else {
body
}
}
fn sprintf_c_char(v: &Value) -> String {
match v {
Value::Str(s) | Value::StrLit(s) | Value::Regexp(s) => {
s.chars().next().map(|c| c.to_string()).unwrap_or_default()
}
Value::Mpfr(f) => {
let code = float_trunc_integer(f).to_u32_wrapping();
char::from_u32(code).unwrap_or('\u{fffd}').to_string()
}
_ => {
let code = v.as_number() as u32;
char::from_u32(code).unwrap_or('\u{fffd}').to_string()
}
}
}
fn parse_width_or_star(
fmt: &str,
mut i: usize,
vals: &[Value],
vi: &mut usize,
) -> Result<(Option<usize>, bool, usize), String> {
if fmt_peek(fmt, i) == Some('*') {
i += '*'.len_utf8();
let (n, i2) = parse_star_value(fmt, i, vals, vi)?;
i = i2;
if n < 0.0 {
let w = (-n) as usize;
return Ok((Some(w), true, i));
}
return Ok((Some(n as usize), false, i));
}
if fmt_peek(fmt, i).is_some_and(|c| c.is_ascii_digit()) {
let mut w = 0usize;
while let Some(d) = fmt_peek(fmt, i) {
if !d.is_ascii_digit() {
break;
}
w = w * 10 + (d as u8 - b'0') as usize;
i += d.len_utf8();
}
return Ok((Some(w), false, i));
}
Ok((None, false, i))
}
#[allow(clippy::too_many_arguments)] fn format_one(
conv: char,
v: &Value,
left: bool,
sign: bool,
space: bool,
alt: bool,
pad_zero: bool,
group: bool,
width: Option<usize>,
prec: Option<usize>,
decimal: char,
thousands_sep: Option<char>,
mpfr_mode: Option<(u32, Round)>,
) -> Result<String, String> {
let pad_char = if pad_zero && !left { '0' } else { ' ' };
let w = width.unwrap_or(0);
let sep = if group {
thousands_sep.unwrap_or('\0')
} else {
'\0'
};
match conv {
's' => {
let mut s: String = match (mpfr_mode, v) {
(Some(_), Value::Mpfr(f)) => mpfr_string_for_percent_s(f),
_ => v.as_str(),
};
if let Some(p) = prec {
s = s.chars().take(p).collect::<String>();
}
let s_pad = if AWK_TRADITIONAL_MODE.load(Ordering::Relaxed) {
pad_char
} else {
' '
};
pad_string(&s, w, left, s_pad)
}
'd' | 'i' => {
let mut s = if let Some((pr, rd)) = mpfr_mode {
let f = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
format!("{}", float_trunc_integer(&f))
} else {
let n = v.as_number();
const I64_BOUND: f64 = 9_223_372_036_854_775_808.0; if !n.is_finite() {
format_non_finite(n, false).unwrap_or_default()
} else if (-I64_BOUND..I64_BOUND).contains(&n) {
let i = n as i64;
format!("{i}")
} else {
let trunc = n.trunc();
if trunc.is_sign_negative() {
format!("-{:.0}", trunc.abs())
} else {
format!("{:.0}", trunc)
}
}
};
if matches!(prec, Some(0)) {
let mag = s.trim_start_matches('-');
if mag == "0" {
s.clear();
}
}
if let Some(p) = prec {
let neg = s.starts_with('-');
let mag = if neg { &s[1..] } else { &s[..] };
if mag.len() < p {
let padded = format!("{:0>width$}", mag, width = p);
s = if neg { format!("-{padded}") } else { padded };
}
}
let pos = !s.starts_with('-');
apply_sign(&mut s, pos, sign, space);
if group && sep != '\0' {
s = insert_thousands_sep(s, sep);
}
pad_numeric(&s, w, left, pad_char)
}
'u' => {
let mut s = if let Some((pr, rd)) = mpfr_mode {
let f = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
let int = float_trunc_integer(&f);
if int < 0 {
format!("{}", int.to_u64_wrapping())
} else {
format!("{}", int)
}
} else {
let n = v.as_number();
const I64_BOUND: f64 = 9_223_372_036_854_775_808.0; const U64_BOUND: f64 = 18_446_744_073_709_551_616.0; if !n.is_finite() {
format_non_finite(n, false).unwrap_or_default()
} else if (-I64_BOUND..I64_BOUND).contains(&n) {
let u = n as i64 as u64;
format!("{u}")
} else if (0.0..U64_BOUND).contains(&n) {
let u = n as u64;
format!("{u}")
} else if n == U64_BOUND {
format!("{}", u64::MAX)
} else if n > U64_BOUND {
crate::format::awk_sprintf_with_decimal(
"%.6g",
&[Value::Num(n)],
'.',
None,
None,
)
.unwrap_or_else(|_| format!("{}", u64::MAX))
} else {
let trunc = n.trunc();
if trunc.is_sign_negative() {
format!("-{:.0}", trunc.abs())
} else {
format!("{:.0}", trunc)
}
}
};
if matches!(prec, Some(0)) && s == "0" {
s.clear();
}
if group && sep != '\0' {
s = insert_thousands_sep(s, sep);
}
pad_numeric(&s, w, left, pad_char)
}
'o' => {
let mut s = if let Some((pr, rd)) = mpfr_mode {
let f = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
let un = float_trunc_integer(&f).to_u64_wrapping();
format!("{un:o}")
} else {
let n = v.as_number() as i64;
let un = n as u64;
format!("{un:o}")
};
if matches!(prec, Some(0)) && s == "0" {
s.clear();
}
if alt && s != "0" && !s.is_empty() {
s = format!("0{s}");
}
pad_numeric(&s, w, left, pad_char)
}
'x' | 'X' => {
let mut s = if let Some((pr, rd)) = mpfr_mode {
let f = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
let un = float_trunc_integer(&f).to_u64_wrapping();
if conv == 'x' {
format!("{un:x}")
} else {
format!("{un:X}")
}
} else {
let n = v.as_number() as i64;
let un = n as u64;
if conv == 'x' {
format!("{un:x}")
} else {
format!("{un:X}")
}
};
if matches!(prec, Some(0)) && s == "0" {
s.clear();
}
if alt && !s.is_empty() && s != "0" {
s = if conv == 'x' {
format!("0x{s}")
} else {
format!("0X{s}")
};
}
pad_numeric(&s, w, left, pad_char)
}
'a' | 'A' => {
let n = v.as_number();
let s = format_hex_float(n, prec, conv == 'A', alt);
let s = localize_float_radix(s, decimal);
pad_numeric(&s, w, left, pad_char)
}
'f' | 'F' => {
let p = prec.unwrap_or(6);
let n_f64 = match (mpfr_mode, v) {
(Some(_), Value::Mpfr(f)) => f.to_f64(),
_ => v.as_number(),
};
if let Some(spelled) = format_non_finite(n_f64, conv == 'F') {
return pad_numeric(&spelled, w, left, ' ');
}
let mut s = if let Some((pr, rd)) = mpfr_mode {
let fsrc = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
localize_float_radix(format!("{:.*}", p, fsrc), decimal)
} else {
let n = v.as_number();
localize_float_radix(format!("{:.*}", p, n), decimal)
};
if group && sep != '\0' {
s = insert_thousands_sep_float(s, sep, decimal);
}
pad_numeric(&s, w, left, pad_char)
}
'e' | 'E' => {
let p = prec.unwrap_or(6);
let n_f64 = match (mpfr_mode, v) {
(Some(_), Value::Mpfr(f)) => f.to_f64(),
_ => v.as_number(),
};
if let Some(spelled) = format_non_finite(n_f64, conv == 'E') {
return pad_numeric(&spelled, w, left, ' ');
}
let raw = if let Some((pr, rd)) = mpfr_mode {
let fsrc = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
if conv == 'e' {
format!("{:.*e}", p, fsrc)
} else {
format!("{:.*E}", p, fsrc)
}
} else {
let n = v.as_number();
if conv == 'e' {
format!("{:.*e}", p, n)
} else {
format!("{:.*E}", p, n)
}
};
let localized = localize_float_radix(raw, decimal);
let s = normalize_sprintf_scientific_exponent(&localized);
pad_numeric(&s, w, left, pad_char)
}
'g' | 'G' => {
let p = prec.unwrap_or(6).max(1);
if let Some((pr, rd)) = mpfr_mode {
let fsrc = match v {
Value::Mpfr(f) => f.clone(),
_ => value_to_mpfr(v, pr, rd),
};
let n = fsrc.to_f64();
if let Some(spelled) = format_non_finite(n, conv == 'G') {
return pad_numeric(&spelled, w, left, ' ');
}
let abs_n = n.abs();
if abs_n == 0.0 {
let raw = format!("{:.*}", p, fsrc);
let localized = localize_float_radix(raw, decimal);
let s = trim_trailing_zero_fraction(&localized);
return pad_numeric(&s, w, left, pad_char);
}
let exp = abs_n.log10().floor() as i32;
let use_e = exp < -4 || exp >= p as i32;
let raw = if use_e {
let mantissa_prec = p.saturating_sub(1);
format!("{:.*e}", mantissa_prec, fsrc)
} else {
let n0 = fsrc.to_f64();
if n0.is_finite() {
format_g_decimal_significant_f64(n0, p)
} else {
format!("{:.*}", p, fsrc)
}
};
let localized = localize_float_radix(raw, decimal);
let mut s = if use_e {
trim_sprintf_g_scientific(&localized)
} else {
trim_trailing_zero_fraction(&localized)
};
if conv == 'G' {
s = s.replace('e', "E");
}
return pad_numeric(&s, w, left, pad_char);
}
let n = v.as_number();
if let Some(spelled) = format_non_finite(n, conv == 'G') {
return pad_numeric(&spelled, w, left, ' ');
}
let abs_n = n.abs();
if abs_n == 0.0 {
let raw = format!("{:.*}", p, n);
let localized = localize_float_radix(raw, decimal);
let s = trim_trailing_zero_fraction(&localized);
return pad_numeric(&s, w, left, pad_char);
}
let raw_e = format!("{:.*e}", p.saturating_sub(1), n);
let exp_x: i32 = raw_e
.find('e')
.and_then(|i| raw_e[i + 1..].parse().ok())
.unwrap_or(0);
let use_e = exp_x < -4 || exp_x >= p as i32;
let raw = if use_e {
raw_e
} else {
format_g_decimal_significant_f64(n, p)
};
let localized = localize_float_radix(raw, decimal);
let mut s = if use_e {
trim_sprintf_g_scientific(&localized)
} else {
trim_trailing_zero_fraction(&localized)
};
if conv == 'G' {
s = s.replace('e', "E");
}
pad_numeric(&s, w, left, pad_char)
}
'c' => {
let s = sprintf_c_char(v);
let c_pad = if AWK_TRADITIONAL_MODE.load(Ordering::Relaxed) {
pad_char
} else {
' '
};
pad_string(&s, w, left, c_pad)
}
_ => Err(format!("unsupported conversion %{conv}")),
}
}
fn apply_sign(s: &mut String, pos: bool, sign: bool, space: bool) {
if pos {
if sign {
s.insert(0, '+');
} else if space {
s.insert(0, ' ');
}
}
}
fn pad_numeric(s: &str, width: usize, left: bool, pad: char) -> Result<String, String> {
if pad == '0' && !left {
if let Some(stripped) = s.strip_prefix('-') {
return Ok(format!(
"-{}",
pad_string(stripped, width.saturating_sub(1), false, '0')?
));
}
if let Some(stripped) = s.strip_prefix('+') {
return Ok(format!(
"+{}",
pad_string(stripped, width.saturating_sub(1), false, '0')?
));
}
if let Some(stripped) = s.strip_prefix(' ') {
return Ok(format!(
" {}",
pad_string(stripped, width.saturating_sub(1), false, '0')?
));
}
}
pad_string(s, width, left, pad)
}
fn pad_string(s: &str, width: usize, left: bool, pad: char) -> Result<String, String> {
let len = s.chars().count();
if width <= len {
return Ok(s.to_string());
}
let padn = width - len;
let pad_s: String = std::iter::repeat_n(pad, padn).collect();
if left {
Ok(format!("{s}{pad_s}"))
} else {
Ok(format!("{pad_s}{s}"))
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
use crate::runtime::Value;
#[test]
fn star_width() {
let s = awk_sprintf("%*d", &[Value::Num(5.0), Value::Num(3.0)]).unwrap();
assert_eq!(s, " 3");
}
#[test]
fn star_width_negative_left_justifies() {
let s = awk_sprintf("%*d", &[Value::Num(-5.0), Value::Num(3.0)]).unwrap();
assert_eq!(s, "3 ");
}
#[test]
fn star_precision() {
let s = awk_sprintf("%.*f", &[Value::Num(2.0), Value::Num(1.234567)]).unwrap();
assert_eq!(s, "1.23");
}
#[test]
fn width_and_star_precision() {
let s = awk_sprintf("%*.*f", &[Value::Num(8.0), Value::Num(2.0), Value::Num(PI)]).unwrap();
assert_eq!(s, " 3.14");
}
#[test]
fn positional_swap() {
let s = awk_sprintf("%2$d %1$d", &[Value::Num(10.0), Value::Num(20.0)]).unwrap();
assert_eq!(s, "20 10");
}
#[test]
fn positional_with_width() {
let s = awk_sprintf("%2$5d", &[Value::Num(1.0), Value::Num(2.0)]).unwrap();
assert_eq!(s, " 2");
}
#[test]
fn positional_and_sequential_mixed() {
let s = awk_sprintf(
"%d %3$d %d",
&[Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
)
.unwrap();
assert_eq!(s, "1 3 2");
}
#[test]
fn star_positional_width() {
let s = awk_sprintf("%*1$d", &[Value::Num(4.0), Value::Num(7.0)]).unwrap();
assert_eq!(s, " 7");
}
#[test]
fn star_positional_precision() {
let s = awk_sprintf("%.*1$f", &[Value::Num(3.0), Value::Num(1.234567)]).unwrap();
assert_eq!(s, "1.235");
}
#[test]
fn star_width_second_positional_arg() {
let s = awk_sprintf(
"%*2$d",
&[Value::Num(5.0), Value::Num(4.0), Value::Num(9.0)],
)
.unwrap();
assert_eq!(s, " 9");
}
#[test]
fn percent_sign_escape() {
let s = awk_sprintf("ok%% done", &[]).unwrap();
assert_eq!(s, "ok% done");
}
#[test]
fn not_enough_arguments_errors() {
let e = awk_sprintf("%d", &[]).unwrap_err();
assert!(e.contains("not enough"), "got {e:?}");
}
#[test]
fn star_precision_positional_second_arg() {
let s = awk_sprintf(
"%.*2$f",
&[Value::Num(9.0), Value::Num(2.0), Value::Num(PI)],
)
.unwrap();
assert_eq!(s, "3.14");
}
#[test]
fn hex_lower() {
let s = awk_sprintf("%x", &[Value::Num(255.0)]).unwrap();
assert_eq!(s, "ff");
}
#[test]
fn hex_upper_conversion_x_uppercase() {
let s = awk_sprintf("%X", &[Value::Num(255.0)]).unwrap();
assert_eq!(s, "FF");
}
#[test]
fn hex_alt_prefix() {
let s = awk_sprintf("%#x", &[Value::Num(255.0)]).unwrap();
assert_eq!(s, "0xff");
}
#[test]
fn string_precision_truncates() {
let s = awk_sprintf("%.3s", &[Value::Str("abcdef".into())]).unwrap();
assert_eq!(s, "abc");
}
#[test]
fn signed_positive_d() {
let s = awk_sprintf("%+d", &[Value::Num(5.0)]).unwrap();
assert_eq!(s, "+5");
}
#[test]
fn space_sign_positive_d() {
let s = awk_sprintf("% d", &[Value::Num(5.0)]).unwrap();
assert_eq!(s, " 5");
}
#[test]
fn scientific_upper() {
let s = awk_sprintf("%.1E", &[Value::Num(1000.0)]).unwrap();
assert!(s.contains('E'), "got {s:?}");
}
#[test]
fn float_default_precision_six() {
let s = awk_sprintf("%f", &[Value::Num(1.0)]).unwrap();
assert_eq!(s, "1.000000");
}
#[test]
fn positional_value_only() {
let s = awk_sprintf(
"%2$s",
&[Value::Str("skip".into()), Value::Str("use".into())],
)
.unwrap();
assert_eq!(s, "use");
}
#[test]
fn positional_mixed_order_integer_then_string() {
let s = awk_sprintf("%2$d %1$s", &[Value::Str("z".into()), Value::Num(9.0)]).unwrap();
assert_eq!(s, "9 z");
}
#[test]
fn invalid_positional_zero_errors() {
let e = awk_sprintf("%0$d", &[Value::Num(1.0)]).unwrap_err();
assert!(e.contains("0"), "{e:?}");
}
#[test]
fn lc_numeric_replaces_float_radix() {
let s = awk_sprintf_with_decimal("%f", &[Value::Num(1.5)], ',', Some(','), None).unwrap();
assert_eq!(s, "1,500000");
}
#[test]
fn lc_numeric_scientific_lowercase_e() {
let s = awk_sprintf_with_decimal("%.2e", &[Value::Num(1.0)], ',', Some(','), None).unwrap();
assert!(s.contains('e'), "got {s:?}");
assert!(s.contains(','), "got {s:?}");
}
#[test]
fn lc_numeric_scientific_uppercase_e() {
let s =
awk_sprintf_with_decimal("%.1E", &[Value::Num(1000.0)], ',', Some(','), None).unwrap();
assert!(s.contains('E'), "got {s:?}");
assert!(s.contains(','), "got {s:?}");
}
#[test]
fn lc_numeric_general_g() {
let s = awk_sprintf_with_decimal("%.4g", &[Value::Num(PI)], ',', Some(','), None).unwrap();
assert!(s.contains(','), "got {s:?}");
}
#[test]
fn percent_g_uses_significant_digits_not_fraction_digits() {
let s = awk_sprintf("%.6g", &[Value::Num(1.23456789)]).unwrap();
assert_eq!(s, "1.23457", "got {s:?}");
}
#[test]
fn printf_apostrophe_groups_integer() {
let s = awk_sprintf_with_decimal("%'d", &[Value::Num(1234567.0)], '.', Some(','), None)
.unwrap();
assert_eq!(s, "1,234,567");
}
#[test]
fn traditional_mode_zero_pads_percent_s_and_c() {
use std::sync::Mutex;
static GUARD: Mutex<()> = Mutex::new(());
let _g = GUARD.lock().unwrap();
let prev = AWK_TRADITIONAL_MODE.swap(true, Ordering::Relaxed);
let s = awk_sprintf(
"[%05s][%-05s][%05c]",
&[
Value::Str("ab".into()),
Value::Str("cd".into()),
Value::Num(65.0),
],
)
.unwrap();
AWK_TRADITIONAL_MODE.store(prev, Ordering::Relaxed);
assert_eq!(s, "[000ab][cd ][0000A]");
}
#[test]
fn default_mode_space_pads_percent_s_and_c() {
use std::sync::Mutex;
static GUARD: Mutex<()> = Mutex::new(());
let _g = GUARD.lock().unwrap();
let prev = AWK_TRADITIONAL_MODE.swap(false, Ordering::Relaxed);
let s = awk_sprintf(
"[%05s][%-05s][%05c]",
&[
Value::Str("ab".into()),
Value::Str("cd".into()),
Value::Num(65.0),
],
)
.unwrap();
AWK_TRADITIONAL_MODE.store(prev, Ordering::Relaxed);
assert_eq!(s, "[ ab][cd ][ A]");
}
#[test]
fn negative_integer_percent_d() {
let s = awk_sprintf("%d", &[Value::Num(-42.0)]).unwrap();
assert_eq!(s, "-42");
}
#[test]
fn percent_i_same_as_d_for_integers() {
let a = awk_sprintf("%i", &[Value::Num(5.0)]).unwrap();
let b = awk_sprintf("%d", &[Value::Num(5.0)]).unwrap();
assert_eq!(a, b);
}
#[test]
fn string_s_width_pad() {
let s = awk_sprintf("%5s", &[Value::Str("hi".into())]).unwrap();
assert_eq!(s, " hi");
}
#[test]
fn float_negative_precision_two() {
let s = awk_sprintf("%.2f", &[Value::Num(-1.234)]).unwrap();
assert_eq!(s, "-1.23");
}
#[test]
fn percent_e_signed_two_digit_exponent() {
let s = awk_sprintf("%e\n", &[Value::Num(1234.5)]).unwrap();
assert_eq!(s, "1.234500e+03\n");
}
#[test]
fn percent_c_string_first_char() {
let s = awk_sprintf("[%c]\n", &[Value::Str("Z".into())]).unwrap();
assert_eq!(s, "[Z]\n");
}
#[test]
fn percent_o_octal_conversion() {
let s = awk_sprintf("%o", &[Value::Num(8.0)]).unwrap();
assert_eq!(s, "10");
}
#[test]
fn percent_u_unsigned_decimal() {
let s = awk_sprintf("%u", &[Value::Num(42.0)]).unwrap();
assert_eq!(s, "42");
}
#[test]
fn sprintf_empty_format_empty_string() {
let s = awk_sprintf("", &[]).unwrap();
assert!(s.is_empty());
}
#[test]
fn percent_u_negative_wraps_as_two_s_complement_u64() {
let s = awk_sprintf("%u", &[Value::Num(-9.0)]).unwrap();
assert_eq!(s, "18446744073709551607");
}
#[test]
fn percent_u_minus_one_is_all_ones_u64() {
let s = awk_sprintf("%u", &[Value::Num(-1.0)]).unwrap();
assert_eq!(s, "18446744073709551615");
}
#[test]
fn percent_s_zero_flag_pads_with_spaces_not_zeros() {
let s = awk_sprintf("[%05s]", &[Value::Str("ab".into())]).unwrap();
assert_eq!(s, "[ ab]");
}
#[test]
fn percent_c_zero_flag_pads_with_spaces() {
let s = awk_sprintf("[%05c]", &[Value::Num(65.0)]).unwrap();
assert_eq!(s, "[ A]");
}
#[test]
fn percent_g_nan_emits_gawk_style_plus_nan() {
let s = awk_sprintf("%g", &[Value::Num(f64::NAN)]).unwrap();
assert_eq!(s, "+nan");
}
#[test]
fn percent_g_infinity_emits_plus_inf() {
let s = awk_sprintf("%g", &[Value::Num(f64::INFINITY)]).unwrap();
assert_eq!(s, "+inf");
}
#[test]
fn percent_g_negative_infinity_emits_minus_inf() {
let s = awk_sprintf("%g", &[Value::Num(f64::NEG_INFINITY)]).unwrap();
assert_eq!(s, "-inf");
}
#[test]
fn percent_capital_g_negative_infinity_emits_minus_capital_inf() {
let s = awk_sprintf("%G", &[Value::Num(f64::NEG_INFINITY)]).unwrap();
assert_eq!(s, "-INF");
}
#[test]
fn percent_f_infinity_pads_to_width_with_spaces_not_zeros() {
let s = awk_sprintf("[%010f]", &[Value::Num(f64::INFINITY)]).unwrap();
assert_eq!(s, "[ +inf]");
}
#[test]
fn percent_e_negative_infinity_minus_inf() {
let s = awk_sprintf("%e", &[Value::Num(f64::NEG_INFINITY)]).unwrap();
assert_eq!(s, "-inf");
}
#[test]
fn percent_a_infinity_plus_inf() {
let s = awk_sprintf("%a", &[Value::Num(f64::INFINITY)]).unwrap();
assert_eq!(s, "+inf");
}
#[test]
fn percent_g_precision_one_emits_single_significant_digit() {
let s = awk_sprintf("%.1g", &[Value::Num(123.456)]).unwrap();
assert_eq!(s, "1e+02");
}
#[test]
fn percent_g_precision_zero_treated_as_one() {
let s = awk_sprintf("%.0g", &[Value::Num(123.456)]).unwrap();
assert_eq!(s, "1e+02");
}
#[test]
fn percent_g_precision_two_keeps_two_significant_digits() {
let s = awk_sprintf("%.2g", &[Value::Num(123.456)]).unwrap();
assert_eq!(s, "1.2e+02");
}
#[test]
fn unknown_conversion_emits_literal_does_not_consume_arg() {
let s = awk_sprintf("[%z][%s]", &[Value::Str("first".into()), Value::Num(2.0)]).unwrap();
assert_eq!(s, "[%z][first]");
}
#[test]
fn unknown_conversion_alone_emits_literal() {
let s = awk_sprintf("%q\n", &[Value::Str("ignored".into())]).unwrap();
assert_eq!(s, "%q\n");
}
#[test]
fn percent_s_min_field_width_right_pads_with_spaces() {
let s = awk_sprintf(">%5s<", &[Value::Str("ab".into())]).unwrap();
assert_eq!(s, "> ab<");
}
#[test]
fn percent_dot_precision_truncates_string_s() {
let s = awk_sprintf("%.3s", &[Value::Str("hello".into())]).unwrap();
assert_eq!(s, "hel");
}
#[test]
fn percent_left_justify_s_padding() {
let s = awk_sprintf("%-5s!", &[Value::Str("ab".into())]).unwrap();
assert_eq!(s, "ab !");
}
#[test]
fn percent_left_justify_d_padding() {
let s = awk_sprintf("%-4d!", &[Value::Num(7.0)]).unwrap();
assert_eq!(s, "7 !");
}
#[test]
fn percent_d_zero_pad_width() {
let s = awk_sprintf("%05d", &[Value::Num(7.0)]).unwrap();
assert_eq!(s, "00007");
}
#[test]
fn percent_f_width_and_precision() {
let s = awk_sprintf("%8.2f", &[Value::Num(1.2)]).unwrap();
assert_eq!(s, " 1.20");
}
#[test]
fn hex_float_precision_zero_rounds() {
let s = awk_sprintf("%.0a", &[Value::Num(1.5)]).unwrap();
assert_eq!(s, "0x2p+0");
}
#[test]
fn hex_float_precision_zero_truncates_below_half() {
let s = awk_sprintf("%.0a", &[Value::Num(1.25)]).unwrap();
assert_eq!(s, "0x1p+0");
}
#[test]
fn hex_float_precision_zero_even_no_round() {
let s = awk_sprintf("%.0a", &[Value::Num(2.0)]).unwrap();
assert_eq!(s, "0x1p+1");
}
#[test]
fn format_large_width() {
use crate::runtime::Value;
assert_eq!(
awk_sprintf("|%20s|", &[Value::Str("hi".into())]).unwrap(),
"| hi|"
);
}
#[test]
fn format_large_precision_float() {
use crate::runtime::Value;
assert_eq!(
awk_sprintf("%.20f", &[Value::Num(1.25)]).unwrap(),
"1.25000000000000000000"
);
}
#[test]
fn format_alternate_form_octal() {
use crate::runtime::Value;
assert_eq!(awk_sprintf("%#o", &[Value::Num(8.0)]).unwrap(), "010");
}
#[test]
fn format_alternate_form_hex_upper() {
use crate::runtime::Value;
assert_eq!(awk_sprintf("%#X", &[Value::Num(255.0)]).unwrap(), "0XFF");
}
#[test]
fn format_space_flag() {
use crate::runtime::Value;
assert_eq!(awk_sprintf("|% d|", &[Value::Num(42.0)]).unwrap(), "| 42|");
assert_eq!(awk_sprintf("|% d|", &[Value::Num(-42.0)]).unwrap(), "|-42|");
}
#[test]
fn format_plus_flag_overrides_space() {
use crate::runtime::Value;
assert_eq!(awk_sprintf("|%+ d|", &[Value::Num(42.0)]).unwrap(), "|+42|");
}
#[test]
fn format_zero_pad_with_left_justify_ignores_zero() {
use crate::runtime::Value;
assert_eq!(
awk_sprintf("|%-05d|", &[Value::Num(42.0)]).unwrap(),
"|42 |"
);
}
#[test]
fn format_char_from_string_first_char() {
use crate::runtime::Value;
assert_eq!(awk_sprintf("%c", &[Value::Str("abc".into())]).unwrap(), "a");
}
#[test]
fn format_percent_at_end_emits_literal_percent() {
let s = awk_sprintf("abc%", &[]).unwrap();
assert_eq!(s, "abc%");
}
#[test]
fn format_positional_out_of_bounds() {
let e = awk_sprintf("%2$d", &[Value::Num(1.0)]).unwrap_err();
assert!(e.contains("positional"), "{e}");
}
#[test]
fn format_mixed_positional_and_sequential_fails_consistently() {
let s = awk_sprintf("%d %1$d", &[Value::Num(1.0)]).unwrap();
assert_eq!(s, "1 1");
}
#[test]
fn format_star_width_positional_mismatch() {
let s = awk_sprintf("%*1$d", &[Value::Num(5.0), Value::Num(42.0)]).unwrap();
assert_eq!(s, " 42");
}
#[test]
fn format_positional_star_width_and_precision() {
let s = awk_sprintf(
"%*1$.*2$f",
&[
Value::Num(10.0),
Value::Num(2.0),
Value::Num(std::f64::consts::PI),
],
)
.unwrap();
assert_eq!(s, " 3.14");
}
#[test]
fn format_hex_float_alternate_form() {
assert_eq!(awk_sprintf("%#a", &[Value::Num(1.0)]).unwrap(), "0x1.p+0");
assert_eq!(awk_sprintf("%#.0a", &[Value::Num(1.0)]).unwrap(), "0x1.p+0");
}
#[test]
fn format_octal_alternate_form_zero() {
assert_eq!(awk_sprintf("%#o", &[Value::Num(0.0)]).unwrap(), "0");
}
#[test]
fn format_precision_zero_f() {
assert_eq!(awk_sprintf("%.0f", &[Value::Num(1.5)]).unwrap(), "2");
assert_eq!(awk_sprintf("%.0f", &[Value::Num(2.5)]).unwrap(), "2"); let s = awk_sprintf("%.0f", &[Value::Num(1.5)]).unwrap();
assert!(s == "1" || s == "2");
}
#[test]
fn format_scientific_exponent_normalization() {
let s = awk_sprintf("%e", &[Value::Num(1e10)]).unwrap();
assert!(s.contains("e+10") || s.contains("e+010")); }
#[test]
fn format_extremely_large_width() {
let s = awk_sprintf("%100s", &[Value::Str("x".into())]).unwrap();
assert_eq!(s.len(), 100);
assert!(s.ends_with('x'));
}
#[test]
fn format_string_padding_utf8() {
let s = awk_sprintf("%5s", &[Value::Str("π".into())]).unwrap();
assert_eq!(s, " π");
assert_eq!(s.chars().count(), 5);
assert_eq!(s.len(), 4 + 2); }
#[test]
fn format_complex_flags_and_width() {
assert_eq!(
awk_sprintf("%+10d", &[Value::Num(42.0)]).unwrap(),
" +42"
);
assert_eq!(
awk_sprintf("% 10d", &[Value::Num(42.0)]).unwrap(),
" 42"
);
assert_eq!(
awk_sprintf("%+-10d", &[Value::Num(42.0)]).unwrap(),
"+42 "
);
}
#[test]
fn format_c_char_conversions() {
assert_eq!(awk_sprintf("%c", &[Value::Num(65.0)]).unwrap(), "A");
assert_eq!(awk_sprintf("%c", &[Value::Str("abc".into())]).unwrap(), "a");
assert_eq!(awk_sprintf("%c", &[Value::Num(960.0)]).unwrap(), "π");
}
#[test]
fn format_alternate_form_octal_hex() {
assert_eq!(awk_sprintf("%#o", &[Value::Num(0.0)]).unwrap(), "0");
assert_eq!(awk_sprintf("%#o", &[Value::Num(8.0)]).unwrap(), "010");
assert_eq!(awk_sprintf("%#x", &[Value::Num(255.0)]).unwrap(), "0xff");
assert_eq!(awk_sprintf("%#X", &[Value::Num(255.0)]).unwrap(), "0XFF");
assert_eq!(awk_sprintf("%#x", &[Value::Num(0.0)]).unwrap(), "0");
assert_eq!(awk_sprintf("%#X", &[Value::Num(0.0)]).unwrap(), "0");
}
#[test]
fn format_precision_truncation_v2() {
assert_eq!(
awk_sprintf("%.3s", &[Value::Str("foobar".into())]).unwrap(),
"foo"
);
assert_eq!(
awk_sprintf("%.10s", &[Value::Str("foo".into())]).unwrap(),
"foo"
);
}
#[test]
fn format_percent_g_v2() {
assert_eq!(
awk_sprintf("%.4g", &[Value::Num(12.3456)]).unwrap(),
"12.35"
);
assert_eq!(
awk_sprintf("%.2g", &[Value::Num(1234.5)]).unwrap(),
"1.2e+03"
);
}
#[test]
fn format_positional_args_v2() {
assert_eq!(
awk_sprintf(
"%2$s %1$s",
&[Value::Str("a".into()), Value::Str("b".into())]
)
.unwrap(),
"b a"
);
}
#[test]
fn format_dynamic_width_precision_v2() {
assert_eq!(
awk_sprintf(
"%*.*f",
&[Value::Num(8.0), Value::Num(2.0), Value::Num(1.234)]
)
.unwrap(),
" 1.23"
);
}
#[test]
fn format_percent_o_leading_zero_v2() {
assert_eq!(awk_sprintf("%#o", &[Value::Num(7.0)]).unwrap(), "07");
}
#[test]
fn format_percent_e_v2() {
let s = awk_sprintf("%.2e", &[Value::Num(1234.5)]).unwrap();
assert!(s == "1.23e+03" || s == "1.23E+03");
}
#[test]
fn format_percent_c_v2() {
assert_eq!(awk_sprintf("%c", &[Value::Num(66.0)]).unwrap(), "B");
}
#[test]
fn format_combined_v2() {
assert_eq!(
awk_sprintf("%s=%d", &[Value::Str("x".into()), Value::Num(42.0)]).unwrap(),
"x=42"
);
}
#[test]
fn format_percent_d_v2() {
assert_eq!(awk_sprintf("%d", &[Value::Num(123.45)]).unwrap(), "123");
}
#[test]
fn format_percent_f_v2() {
assert_eq!(awk_sprintf("%.2f", &[Value::Num(1.234)]).unwrap(), "1.23");
}
#[test]
fn format_percent_x_v2() {
assert_eq!(awk_sprintf("%x", &[Value::Num(255.0)]).unwrap(), "ff");
}
#[test]
fn format_percent_o_v2() {
assert_eq!(awk_sprintf("%o", &[Value::Num(8.0)]).unwrap(), "10");
}
#[test]
fn format_alternate_hex_zero_v2() {
assert_eq!(awk_sprintf("%#x", &[Value::Num(0.0)]).unwrap(), "0");
}
#[test]
fn format_space_plus_flags_v2() {
assert_eq!(awk_sprintf("% +d", &[Value::Num(5.0)]).unwrap(), "+5");
}
#[test]
fn format_zero_pad_with_precision_v3() {
let s = awk_sprintf("%08.5d", &[Value::Num(123.0)]).unwrap();
assert_eq!(s, "00000123");
}
#[test]
fn format_percent_f_zero_precision_v2() {
assert_eq!(awk_sprintf("%.0f", &[Value::Num(1.23)]).unwrap(), "1");
assert_eq!(awk_sprintf("%.0f", &[Value::Num(1.67)]).unwrap(), "2");
}
#[test]
fn format_percent_e_precision_v2() {
let s = awk_sprintf("%.3e", &[Value::Num(123.4567)]).unwrap();
assert!(s == "1.235e+02" || s == "1.235E+02");
}
#[test]
fn format_percent_s_width_v2() {
assert_eq!(
awk_sprintf("%10s", &[Value::Str("abc".into())]).unwrap(),
" abc"
);
assert_eq!(
awk_sprintf("%-10s", &[Value::Str("abc".into())]).unwrap(),
"abc "
);
}
#[test]
fn format_percent_s_precision_v2() {
assert_eq!(
awk_sprintf("%.2s", &[Value::Str("abc".into())]).unwrap(),
"ab"
);
}
#[test]
fn format_percent_c_v3() {
assert_eq!(awk_sprintf("%c", &[Value::Num(97.0)]).unwrap(), "a");
}
#[test]
fn format_percent_percent_v2() {
assert_eq!(awk_sprintf("%%", &[]).unwrap(), "%");
}
#[test]
fn format_mixed_args_v3() {
assert_eq!(
awk_sprintf(
"%d %s %.1f",
&[Value::Num(1.0), Value::Str("x".into()), Value::Num(2.56)]
)
.unwrap(),
"1 x 2.6"
);
}
#[test]
fn format_positional_reorder_v3() {
assert_eq!(
awk_sprintf("%2$s %1$d", &[Value::Num(10.0), Value::Str("y".into())]).unwrap(),
"y 10"
);
}
#[test]
fn format_dynamic_width_v3() {
assert_eq!(
awk_sprintf("%*s", &[Value::Num(5.0), Value::Str("a".into())]).unwrap(),
" a"
);
}
#[test]
fn format_dynamic_precision_v3() {
assert_eq!(
awk_sprintf("%.*f", &[Value::Num(1.0), Value::Num(1.23)]).unwrap(),
"1.2"
);
}
#[test]
fn format_dynamic_both_v3() {
assert_eq!(
awk_sprintf(
"%*.*f",
&[Value::Num(5.0), Value::Num(1.0), Value::Num(1.23)]
)
.unwrap(),
" 1.2"
);
}
#[test]
fn format_plus_flag_negative_v2() {
assert_eq!(awk_sprintf("%+d", &[Value::Num(-5.0)]).unwrap(), "-5");
}
#[test]
fn format_space_flag_negative_v2() {
assert_eq!(awk_sprintf("% d", &[Value::Num(-5.0)]).unwrap(), "-5");
}
#[test]
fn format_hash_octal_v3() {
assert_eq!(awk_sprintf("%#o", &[Value::Num(8.0)]).unwrap(), "010");
assert_eq!(awk_sprintf("%#o", &[Value::Num(0.0)]).unwrap(), "0");
}
#[test]
fn format_hash_hex_v3() {
assert_eq!(awk_sprintf("%#x", &[Value::Num(16.0)]).unwrap(), "0x10");
assert_eq!(awk_sprintf("%#X", &[Value::Num(16.0)]).unwrap(), "0X10");
}
#[test]
fn format_zero_pad_width_v2() {
assert_eq!(awk_sprintf("%05d", &[Value::Num(42.0)]).unwrap(), "00042");
}
#[test]
fn format_zero_pad_negative_v3() {
assert_eq!(awk_sprintf("%05d", &[Value::Num(-42.0)]).unwrap(), "-0042");
}
#[test]
fn format_left_justify_v2() {
assert_eq!(awk_sprintf("%-5d", &[Value::Num(42.0)]).unwrap(), "42 ");
}
#[test]
fn format_precision_zero_integer_zero_v2() {
assert_eq!(awk_sprintf("%.0d", &[Value::Num(0.0)]).unwrap(), "");
}
#[test]
fn format_precision_zero_octal_zero_v2() {
assert_eq!(awk_sprintf("%.0o", &[Value::Num(0.0)]).unwrap(), "");
}
#[test]
fn format_precision_zero_hex_zero_v2() {
assert_eq!(awk_sprintf("%.0x", &[Value::Num(0.0)]).unwrap(), "");
}
#[test]
fn format_percent_g_precision_v3() {
assert_eq!(awk_sprintf("%.3g", &[Value::Num(1.2345)]).unwrap(), "1.23");
}
#[test]
fn format_percent_i_v2() {
assert_eq!(awk_sprintf("%i", &[Value::Num(42.0)]).unwrap(), "42");
}
#[test]
fn format_percent_u_v2() {
assert_eq!(awk_sprintf("%u", &[Value::Num(42.0)]).unwrap(), "42");
}
#[test]
fn format_percent_x_upper_v2() {
assert_eq!(awk_sprintf("%X", &[Value::Num(255.0)]).unwrap(), "FF");
}
#[test]
fn format_percent_s_long_v3() {
let s = "x".repeat(100);
assert_eq!(
awk_sprintf("%105s", &[Value::Str(s.clone())]).unwrap(),
format!(" {}", s)
);
}
#[test]
fn format_percent_f_long_v3() {
assert_eq!(
awk_sprintf("%.10f", &[Value::Num(1.0)]).unwrap(),
"1.0000000000"
);
}
#[test]
fn format_combined_many_v3() {
assert_eq!(
awk_sprintf(
"%d %s %x %o",
&[
Value::Num(1.0),
Value::Str("a".into()),
Value::Num(10.0),
Value::Num(8.0)
]
)
.unwrap(),
"1 a a 10"
);
}
#[test]
fn format_hash_hex_upper_v3() {
assert_eq!(awk_sprintf("%#X", &[Value::Num(16.0)]).unwrap(), "0X10");
}
#[test]
fn format_star_width_v4() {
assert_eq!(
awk_sprintf("%*d", &[Value::Num(5.0), Value::Num(1.0)]).unwrap(),
" 1"
);
}
#[test]
fn format_large_exponent_e_v2() {
let s = awk_sprintf("%e", &[Value::Num(1e100)]).unwrap();
assert!(s == "1.000000e+100" || s == "1.000000E+100");
}
#[test]
fn format_small_exponent_e_v2() {
let s = awk_sprintf("%e", &[Value::Num(1e-100)]).unwrap();
assert!(s == "1.000000e-100" || s == "1.000000E-100");
}
#[test]
fn format_large_float_f_v2() {
let s = awk_sprintf("%.1f", &[Value::Num(1e15)]).unwrap();
assert_eq!(s, "1000000000000000.0");
}
#[test]
fn format_percent_c_zero_v3() {
assert_eq!(awk_sprintf("%c", &[Value::Num(0.0)]).unwrap(), "\0");
}
#[test]
fn format_percent_c_negative_v3() {
let s = awk_sprintf("%c", &[Value::Num(-1.0)]).unwrap();
assert_eq!(s.len(), 1); }
#[test]
fn format_percent_s_empty_v3() {
assert_eq!(awk_sprintf("[%s]", &[Value::Str("".into())]).unwrap(), "[]");
}
#[test]
fn format_percent_s_width_empty_v3() {
assert_eq!(
awk_sprintf("[%5s]", &[Value::Str("".into())]).unwrap(),
"[ ]"
);
}
#[test]
fn format_positional_arg_out_of_bounds_v3() {
assert!(awk_sprintf("%2$s", &[Value::Num(1.0)]).is_err());
}
#[test]
fn format_dynamic_width_out_of_bounds_v3() {
assert!(awk_sprintf("%*s", &[Value::Num(1.0)]).is_err());
}
#[test]
fn format_dynamic_precision_out_of_bounds_v3() {
assert!(awk_sprintf("%.*s", &[Value::Num(1.0)]).is_err());
}
#[test]
fn format_missing_format_char_v3() {
assert_eq!(awk_sprintf("abc%", &[]).unwrap(), "abc%");
}
#[test]
fn format_unknown_format_char_v3() {
assert_eq!(awk_sprintf("%q", &[Value::Num(1.0)]).unwrap(), "%q");
}
#[test]
fn format_positional_arg_zero_v3() {
assert!(awk_sprintf("%0$s", &[Value::Num(1.0)]).is_err());
}
#[test]
fn format_star_positional_v3() {
assert_eq!(
awk_sprintf("%*1$d", &[Value::Num(5.0), Value::Num(42.0)]).unwrap(),
" 42"
);
}
#[test]
fn format_star_positional_precision_v3() {
assert_eq!(
awk_sprintf("%.*1$d", &[Value::Num(5.0), Value::Num(42.0)]).unwrap(),
"00042"
);
}
#[test]
fn format_star_positional_width_and_precision_v3() {
assert_eq!(
awk_sprintf(
"%*1$.*2$d",
&[Value::Num(8.0), Value::Num(5.0), Value::Num(42.0)]
)
.unwrap(),
" 00042"
);
}
#[test]
fn format_percent_c_multibyte_v3() {
assert_eq!(awk_sprintf("%c", &[Value::Str("π".into())]).unwrap(), "π");
}
#[test]
fn format_percent_d_float_v3() {
assert_eq!(awk_sprintf("%d", &[Value::Num(3.9)]).unwrap(), "3");
}
}