use std::fmt::Write;
use std::iter::{repeat_n, Peekable};
use std::str;
use std::str::Chars;
use crate::numeric::{format_float, str_to_i64, Numeric};
use crate::types::Value;
use crate::vdbe::Register;
#[derive(Default, Clone)]
struct FormatFlags {
left_justify: bool,
force_sign: bool,
space_sign: bool,
zero_pad: bool,
alternate: bool,
comma_sep: bool,
alt_form_2: bool,
}
struct FormatSpec {
flags: FormatFlags,
width: Option<usize>,
precision: Option<usize>,
spec_type: char,
}
fn parse_flags(chars: &mut Peekable<Chars>) -> FormatFlags {
let mut flags = FormatFlags::default();
while let Some(&c) = chars.peek() {
match c {
'-' => {
flags.left_justify = true;
}
'+' => {
flags.force_sign = true;
flags.space_sign = false;
}
' ' => {
flags.space_sign = true;
flags.force_sign = false;
}
'0' => {
flags.zero_pad = true;
}
'#' => {
flags.alternate = true;
}
',' => {
flags.comma_sep = true;
}
'!' => {
flags.alt_form_2 = true;
}
_ => break,
}
chars.next();
}
flags
}
fn parse_number(chars: &mut Peekable<Chars>) -> Option<usize> {
let mut n: usize = 0;
let mut found = false;
while let Some(&c) = chars.peek() {
if let Some(d) = c.to_digit(10) {
n = n.saturating_mul(10).saturating_add(d as usize);
found = true;
chars.next();
} else {
break;
}
}
if found {
Some(n)
} else {
None
}
}
const MAX_WIDTH: usize = 1_000_000;
fn parse_format_spec(
chars: &mut Peekable<Chars>,
values: &[Register],
args_index: &mut usize,
) -> FormatSpec {
let mut flags = parse_flags(chars);
let width = if chars.peek() == Some(&'*') {
chars.next();
let w = get_arg_i64(values, args_index) as i32;
if w < 0 {
flags.left_justify = true;
if w > i32::MIN {
Some(((-w) as usize).min(MAX_WIDTH))
} else {
Some(0)
}
} else {
Some((w as usize).min(MAX_WIDTH))
}
} else {
parse_number(chars).map(|w| w.min(MAX_WIDTH))
};
let precision = if chars.peek() == Some(&'.') {
chars.next();
if chars.peek() == Some(&'*') {
chars.next();
let p = get_arg_i64(values, args_index) as i32;
let p = if p < 0 { p.wrapping_neg() } else { p };
Some((p.max(0) as usize).min(MAX_WIDTH))
} else {
Some(parse_number(chars).unwrap_or(0).min(MAX_WIDTH))
}
} else {
None
};
while matches!(chars.peek(), Some(&'l') | Some(&'h')) {
chars.next();
}
let spec_type = chars.next().unwrap_or('\0');
FormatSpec {
flags,
width,
precision,
spec_type,
}
}
fn get_arg_i64(values: &[Register], args_index: &mut usize) -> i64 {
if *args_index >= values.len() {
return 0;
}
let val = coerce_to_i64(values[*args_index].get_value());
*args_index += 1;
val
}
fn coerce_to_i64(value: &Value) -> i64 {
match value {
Value::Numeric(Numeric::Integer(i)) => *i,
Value::Numeric(Numeric::Float(f)) => f64::from(*f) as i64,
Value::Text(t) => str_to_i64(t.as_str()).unwrap_or(0),
Value::Blob(b) => {
let s = String::from_utf8_lossy(b);
str_to_i64(s.as_ref()).unwrap_or(0)
}
Value::Null => 0,
}
}
fn coerce_to_f64(value: &Value) -> f64 {
match value {
Value::Numeric(Numeric::Float(f)) => f64::from(*f),
_ => match Option::<Numeric>::from(value) {
Some(Numeric::Integer(i)) => i as f64,
Some(Numeric::Float(f)) => f.into(),
None => 0.0,
},
}
}
fn coerce_to_string(value: &Value) -> String {
match value {
Value::Null => String::new(),
Value::Numeric(Numeric::Integer(i)) => i.to_string(),
Value::Numeric(Numeric::Float(f)) => format_float(f64::from(*f)),
Value::Text(t) => t.as_str().to_string(),
Value::Blob(b) => String::from_utf8_lossy(b).to_string(),
}
}
fn insert_commas(digits: &str) -> String {
let bytes = digits.as_bytes();
let len = bytes.len();
if len <= 3 {
return digits.to_string();
}
let mut result = String::with_capacity(len + len / 3);
let first_group = len % 3;
if first_group > 0 {
result.push_str(&digits[..first_group]);
}
for chunk in digits.as_bytes()[first_group..].chunks(3) {
if !result.is_empty() {
result.push(',');
}
result.push_str(str::from_utf8(chunk).expect("digit string is ASCII"));
}
result
}
fn apply_width(
output: &mut String,
sign_prefix: &str,
content: &str,
width: Option<usize>,
flags: &FormatFlags,
zero_overrides_left: bool,
) {
let total_len = sign_prefix.chars().count() + content.chars().count();
let w = width.unwrap_or(0);
if total_len >= w {
output.push_str(sign_prefix);
output.push_str(content);
return;
}
let pad_len = w - total_len;
if flags.zero_pad && (zero_overrides_left || !flags.left_justify) {
output.push_str(sign_prefix);
for _ in 0..pad_len {
output.push('0');
}
output.push_str(content);
} else if flags.left_justify {
output.push_str(sign_prefix);
output.push_str(content);
for _ in 0..pad_len {
output.push(' ');
}
} else {
for _ in 0..pad_len {
output.push(' ');
}
output.push_str(sign_prefix);
output.push_str(content);
}
}
fn sign_prefix(negative: bool, flags: &FormatFlags) -> &'static str {
if negative {
"-"
} else if flags.force_sign {
"+"
} else if flags.space_sign {
" "
} else {
""
}
}
fn ensure_decimal_strip_zeros(s: &str) -> String {
if s.contains('.') {
let trimmed = s.trim_end_matches('0');
if trimmed.ends_with('.') {
format!("{trimmed}0")
} else {
trimmed.to_string()
}
} else {
format!("{s}.0")
}
}
fn build_exp_mantissa(digits: &[u8], precision: usize, flags: &FormatFlags) -> (String, bool) {
let mut mantissa = String::new();
mantissa.push((b'0' + digits.first().copied().unwrap_or(0)) as char);
let flag_dp = precision > 0 || flags.alternate || flags.alt_form_2;
if flag_dp {
mantissa.push('.');
}
let mut j = 1_usize;
for _ in 0..precision {
if j < digits.len() {
mantissa.push((b'0' + digits[j]) as char);
j += 1;
} else {
mantissa.push('0');
}
}
(mantissa, flag_dp)
}
fn strip_trailing_zeros(s: &mut String, keep_dot_zero: bool) {
let trimmed = s.trim_end_matches('0');
*s = if trimmed.ends_with('.') {
if keep_dot_zero {
format!("{trimmed}0")
} else {
trimmed.trim_end_matches('.').to_string()
}
} else {
trimmed.to_string()
};
}
fn pad_with_precision(digits: String, precision: Option<usize>) -> String {
let min_digits = precision.unwrap_or(1);
if digits.len() < min_digits {
"0".repeat(min_digits - digits.len()) + &digits
} else {
digits
}
}
fn dekker_mul2(x: &mut [f64; 2], y: f64, yy: f64) {
let hx = f64::from_bits(x[0].to_bits() & 0xffff_ffff_fc00_0000);
let tx = x[0] - hx;
let hy = f64::from_bits(y.to_bits() & 0xffff_ffff_fc00_0000);
let ty = y - hy;
let p = hx * hy;
let q = hx * ty + tx * hy;
let c = p + q;
let cc = p - c + q + tx * ty;
let cc = x[0] * yy + x[1] * y + cc;
x[0] = c + cc;
x[1] = c - x[0] + cc;
}
fn fp_decode(r: f64, i_round: i32, max_round: usize) -> (Vec<u8>, i32) {
debug_assert!(r > 0.0);
if r.is_infinite() {
return (vec![9], 1000);
}
let mut rr = [r, 0.0_f64];
let mut exp: i32 = 0;
#[allow(clippy::excessive_precision)]
if rr[0] > 9.223_372_036_854_774_784e+18 {
while rr[0] > 9.223_372_036_854_774_784e+118 {
exp += 100;
dekker_mul2(&mut rr, 1.0e-100, -1.999_189_980_260_288_361_96e-117);
}
while rr[0] > 9.223_372_036_854_774_784e+28 {
exp += 10;
dekker_mul2(&mut rr, 1.0e-10, -3.643_219_731_549_774_157_9e-27);
}
while rr[0] > 9.223_372_036_854_774_784e+18 {
exp += 1;
dekker_mul2(&mut rr, 1.0e-01, -5.551_115_123_125_782_702_1e-18);
}
} else {
while rr[0] < 9.223_372_036_854_774_784e-83 {
exp -= 100;
dekker_mul2(&mut rr, 1.0e+100, -1.590_289_110_975_991_804_6e+83);
}
while rr[0] < 9.223_372_036_854_774_784e+07 {
exp -= 10;
dekker_mul2(&mut rr, 1.0e+10, 0.0);
}
while rr[0] < 9.223_372_036_854_774_78e+17 {
exp -= 1;
dekker_mul2(&mut rr, 1.0e+01, 0.0);
}
}
let v: u64 = if rr[1] < 0.0 {
(rr[0] as u64).wrapping_sub((-rr[1]) as u64)
} else {
(rr[0] as u64).wrapping_add(rr[1] as u64)
};
let mut buf = Vec::with_capacity(20);
let mut temp = v;
while temp > 0 {
buf.push((temp % 10) as u8);
temp /= 10;
}
buf.reverse();
let n = buf.len();
let mut dp = n as i32 + exp;
let mut i_round = i_round;
if i_round <= 0 {
i_round = dp - i_round;
if i_round == 0 && !buf.is_empty() && buf[0] >= 5 {
buf.insert(0, 0);
dp += 1;
i_round = 1;
}
}
let n = buf.len();
if i_round > 0 && ((i_round as usize) < n || n > max_round) {
let i_round = if (i_round as usize) > max_round {
max_round
} else {
i_round as usize
};
let mut carried_past = false;
if i_round < n && buf[i_round] >= 5 {
let mut j = i_round;
loop {
if j == 0 {
buf.insert(0, 1);
dp += 1;
carried_past = true;
break;
}
j -= 1;
buf[j] += 1;
if buf[j] <= 9 {
break;
}
buf[j] = 0;
}
}
let keep = if carried_past { i_round + 1 } else { i_round };
buf.truncate(keep);
}
while buf.len() > 1 && *buf.last().unwrap() == 0 {
buf.pop();
}
(buf, dp)
}
fn format_fixed_from_digits(abs_f: f64, precision: usize, max_sig: usize) -> String {
if abs_f == 0.0 {
return if precision == 0 {
"0".to_string()
} else {
format!("0.{}", "0".repeat(precision))
};
}
let i_round = -(precision as i32);
let (digits, dp) = fp_decode(abs_f, i_round, max_sig);
let mut result = String::new();
let mut j: usize = 0;
if dp <= 0 {
result.push('0');
} else {
for _ in 0..dp {
if j < digits.len() {
result.push((b'0' + digits[j]) as char);
j += 1;
} else {
result.push('0');
}
}
}
if precision == 0 {
return result;
}
result.push('.');
let mut e2 = dp - 1;
let mut frac_remaining = precision;
e2 += 1; while e2 < 0 && frac_remaining > 0 {
result.push('0');
frac_remaining -= 1;
e2 += 1;
}
while frac_remaining > 0 {
if j < digits.len() {
result.push((b'0' + digits[j]) as char);
j += 1;
} else {
result.push('0');
}
frac_remaining -= 1;
}
result
}
#[cfg(test)]
fn limit_significant_digits(s: &str, max_sig: usize) -> String {
let chars: Vec<char> = s.chars().collect();
let mut result: Vec<char> = chars.clone();
let mut digit_positions: Vec<usize> = Vec::new();
let mut sig_count = 0;
let mut first_nonzero = false;
for (i, &c) in chars.iter().enumerate() {
if !c.is_ascii_digit() {
continue;
}
if c != '0' || first_nonzero {
first_nonzero = true;
sig_count += 1;
}
digit_positions.push(i);
}
if sig_count <= max_sig {
return s.to_string();
}
let mut sig_seen = 0;
let mut round_pos = None; let mut last_sig_pos = None; let mut first_nonzero2 = false;
for &pos in &digit_positions {
let c = chars[pos];
if c != '0' || first_nonzero2 {
first_nonzero2 = true;
sig_seen += 1;
}
if sig_seen == max_sig {
last_sig_pos = Some(pos);
}
if sig_seen == max_sig + 1 {
round_pos = Some(pos);
break;
}
}
let (Some(round_pos), Some(_last_sig_pos)) = (round_pos, last_sig_pos) else {
return s.to_string();
};
let round_digit = chars[round_pos].to_digit(10).unwrap();
for &pos in &digit_positions {
if pos >= round_pos {
result[pos] = '0';
}
}
if round_digit >= 5 {
let mut carry = true;
for &pos in digit_positions.iter().rev() {
if pos >= round_pos {
continue;
}
if !carry {
break;
}
let d = result[pos].to_digit(10).unwrap() + 1;
if d >= 10 {
result[pos] = '0';
} else {
result[pos] = char::from_digit(d, 10).unwrap();
carry = false;
}
}
if carry {
let first_digit_pos = digit_positions[0];
result.insert(first_digit_pos, '1');
}
}
result.into_iter().collect()
}
fn format_special_float(output: &mut String, f: f64, spec: &FormatSpec) {
let mut space_flags = spec.flags.clone();
space_flags.zero_pad = false;
if f.is_nan() {
let text = if spec.flags.zero_pad { "null" } else { "NaN" };
apply_width(output, "", text, spec.width, &space_flags, false);
return;
}
let prefix = sign_prefix(f < 0.0, &spec.flags);
apply_width(output, prefix, "Inf", spec.width, &space_flags, false);
}
fn format_signed_int(output: &mut String, value: &Value, spec: &FormatSpec) {
let i = coerce_to_i64(value);
let negative = i < 0;
let digits = i.unsigned_abs().to_string();
let mut padded = pad_with_precision(digits, spec.precision);
let prefix = sign_prefix(negative, &spec.flags);
if spec.flags.comma_sep && spec.flags.zero_pad {
let w = spec.width.unwrap_or(0);
let digit_target = w.saturating_sub(prefix.len());
if padded.len() < digit_target {
padded = "0".repeat(digit_target - padded.len()) + &padded;
}
output.push_str(prefix);
output.push_str(&insert_commas(&padded));
} else if spec.flags.comma_sep {
padded = insert_commas(&padded);
apply_width(output, prefix, &padded, spec.width, &spec.flags, true);
} else {
apply_width(output, prefix, &padded, spec.width, &spec.flags, true);
}
}
fn format_unsigned_int(output: &mut String, value: &Value, spec: &FormatSpec) {
let i = coerce_to_i64(value);
let u = i as u64;
let digits = u.to_string();
let mut padded = pad_with_precision(digits, spec.precision);
if spec.flags.comma_sep && spec.flags.zero_pad {
let w = spec.width.unwrap_or(0);
if padded.len() < w {
padded = "0".repeat(w - padded.len()) + &padded;
}
output.push_str(&insert_commas(&padded));
} else if spec.flags.comma_sep {
padded = insert_commas(&padded);
apply_width(output, "", &padded, spec.width, &spec.flags, true);
} else {
apply_width(output, "", &padded, spec.width, &spec.flags, true);
}
}
fn format_hex(output: &mut String, value: &Value, spec: &FormatSpec, uppercase: bool) {
let i = coerce_to_i64(value);
let u = i as u64;
let digits = if uppercase {
format!("{u:X}")
} else {
format!("{u:x}")
};
let padded = pad_with_precision(digits, spec.precision);
let prefix = if spec.flags.alternate && u != 0 {
if uppercase && spec.spec_type != 'p' {
"0X"
} else {
"0x"
}
} else {
""
};
if spec.flags.alternate && spec.flags.zero_pad && !prefix.is_empty() {
let w = spec.width.unwrap_or(0);
let zero_padded = if padded.len() < w {
"0".repeat(w - padded.len()) + &padded
} else {
padded
};
output.push_str(prefix);
output.push_str(&zero_padded);
} else {
apply_width(output, prefix, &padded, spec.width, &spec.flags, true);
}
}
fn format_octal(output: &mut String, value: &Value, spec: &FormatSpec) {
let i = coerce_to_i64(value);
let u = i as u64;
let digits = format!("{u:o}");
let padded = pad_with_precision(digits, spec.precision);
let prefix = if spec.flags.alternate && u != 0 {
"0"
} else {
""
};
if spec.flags.alternate && spec.flags.zero_pad && !prefix.is_empty() {
let w = spec.width.unwrap_or(0);
let zero_padded = if padded.len() < w {
"0".repeat(w - padded.len()) + &padded
} else {
padded
};
output.push_str(prefix);
output.push_str(&zero_padded);
} else {
apply_width(output, prefix, &padded, spec.width, &spec.flags, true);
}
}
fn format_float_decimal(output: &mut String, value: &Value, spec: &FormatSpec) {
let f = coerce_to_f64(value);
if f.is_nan() || (f.is_infinite() && !spec.flags.zero_pad) {
format_special_float(output, f, spec);
return;
}
let precision = spec.precision.unwrap_or(6).min(1000);
let negative = f < 0.0;
let abs_f = f.abs();
let max_sig = if spec.flags.alt_form_2 { 26 } else { 16 };
let formatted = if spec.flags.alt_form_2 && precision == 0 {
let mut s = format_fixed_from_digits(abs_f, 0, max_sig);
s.push_str(".0");
s
} else if precision == 0 {
let mut s = format_fixed_from_digits(abs_f, 0, max_sig);
if spec.flags.alternate {
s.push('.');
}
s
} else {
let s = format_fixed_from_digits(abs_f, precision, max_sig);
if spec.flags.alt_form_2 {
ensure_decimal_strip_zeros(&s)
} else {
s
}
};
let content = if spec.flags.comma_sep {
if let Some(dot_pos) = formatted.find('.') {
let int_part = &formatted[..dot_pos];
let frac_part = &formatted[dot_pos..];
insert_commas(int_part) + frac_part
} else {
insert_commas(&formatted)
}
} else {
formatted
};
let negative =
if negative && spec.flags.alternate && !spec.flags.force_sign && !spec.flags.space_sign {
!content.bytes().all(|b| b == b'0' || b == b'.' || b == b',')
} else {
negative
};
let prefix = sign_prefix(negative, &spec.flags);
apply_width(output, prefix, &content, spec.width, &spec.flags, false);
}
fn format_exponential(output: &mut String, value: &Value, spec: &FormatSpec, uppercase: bool) {
let f = coerce_to_f64(value);
if f.is_nan() || (f.is_infinite() && !spec.flags.zero_pad) {
format_special_float(output, f, spec);
return;
}
format_exponential_inner(output, f, spec, uppercase);
}
fn format_exponential_inner(output: &mut String, f: f64, spec: &FormatSpec, uppercase: bool) {
let precision = spec.precision.unwrap_or(6).min(1000);
let negative = f < 0.0;
let abs_f = f.abs();
let e_char = if uppercase { 'E' } else { 'e' };
let max_sig = if spec.flags.alt_form_2 { 26 } else { 16 };
if abs_f == 0.0 {
let flag_dp = precision > 0 || spec.flags.alternate || spec.flags.alt_form_2;
let mut mantissa = "0".to_string();
if flag_dp {
mantissa.push('.');
for _ in 0..precision {
mantissa.push('0');
}
}
if spec.flags.alt_form_2 {
mantissa = ensure_decimal_strip_zeros(&mantissa);
}
let content = format!("{mantissa}{e_char}+00");
let prefix = sign_prefix(negative, &spec.flags);
apply_width(output, prefix, &content, spec.width, &spec.flags, false);
return;
}
let i_round = (precision + 1) as i32;
let (digits, dp) = fp_decode(abs_f, i_round, max_sig);
let exp = dp - 1;
let (mut mantissa, flag_dp) = build_exp_mantissa(&digits, precision, &spec.flags);
if spec.flags.alt_form_2 && flag_dp {
strip_trailing_zeros(&mut mantissa, true);
}
let content = format!("{mantissa}{e_char}{exp:+03}");
let prefix = sign_prefix(negative, &spec.flags);
apply_width(output, prefix, &content, spec.width, &spec.flags, false);
}
fn format_general(output: &mut String, value: &Value, spec: &FormatSpec, uppercase: bool) {
let f = coerce_to_f64(value);
if f.is_nan() || (f.is_infinite() && !spec.flags.zero_pad) {
format_special_float(output, f, spec);
return;
}
format_general_inner(output, f, spec, uppercase);
}
fn format_general_inner(output: &mut String, f: f64, spec: &FormatSpec, uppercase: bool) {
let precision = spec.precision.unwrap_or(6).clamp(1, 1000);
let negative = f < 0.0;
let abs_f = f.abs();
let e_char = if uppercase { 'E' } else { 'e' };
let max_sig = if spec.flags.alt_form_2 { 26 } else { 16 };
if abs_f == 0.0 {
let flag_rtz = !spec.flags.alternate;
let flag_dp = precision > 1 || spec.flags.alternate || spec.flags.alt_form_2;
let mut s = "0".to_string();
if flag_dp {
s.push('.');
for _ in 1..precision {
s.push('0');
}
}
if flag_rtz && flag_dp {
strip_trailing_zeros(&mut s, spec.flags.alt_form_2);
}
let prefix = sign_prefix(negative, &spec.flags);
apply_width(output, prefix, &s, spec.width, &spec.flags, false);
return;
}
let (digits, dp) = fp_decode(abs_f, precision as i32, max_sig);
let exp = dp - 1;
let precision = precision - 1;
let flag_rtz = !spec.flags.alternate;
let use_exp = exp < -4 || exp > precision as i32;
let content = if use_exp {
let (mut mantissa, flag_dp) = build_exp_mantissa(&digits, precision, &spec.flags);
if flag_rtz && flag_dp {
strip_trailing_zeros(&mut mantissa, spec.flags.alt_form_2);
}
format!("{mantissa}{e_char}{exp:+03}")
} else {
let frac_precision = if precision as i32 > exp {
(precision as i32 - exp) as usize
} else {
0
};
let mut s = String::new();
let mut j: usize = 0;
if dp <= 0 {
s.push('0');
} else {
for _ in 0..dp {
if j < digits.len() {
s.push((b'0' + digits[j]) as char);
j += 1;
} else {
s.push('0');
}
}
}
let flag_dp = frac_precision > 0 || spec.flags.alternate || spec.flags.alt_form_2;
if flag_dp {
s.push('.');
}
let mut e2 = dp - 1;
let mut frac_remaining = frac_precision;
e2 += 1;
while e2 < 0 && frac_remaining > 0 {
s.push('0');
frac_remaining -= 1;
e2 += 1;
}
while frac_remaining > 0 {
if j < digits.len() {
s.push((b'0' + digits[j]) as char);
j += 1;
} else {
s.push('0');
}
frac_remaining -= 1;
}
if flag_rtz && flag_dp {
strip_trailing_zeros(&mut s, spec.flags.alt_form_2);
}
s
};
let content = if spec.flags.comma_sep {
if let Some(dot_pos) = content.find('.') {
let int_part = &content[..dot_pos];
let frac_part = &content[dot_pos..];
insert_commas(int_part) + frac_part
} else if !content.contains('e') && !content.contains('E') {
insert_commas(&content)
} else {
content
}
} else {
content
};
let prefix = sign_prefix(negative, &spec.flags);
apply_width(output, prefix, &content, spec.width, &spec.flags, false);
}
fn format_string(output: &mut String, value: &Value, spec: &FormatSpec) {
let s = match value {
Value::Blob(b) => {
let end = b.iter().position(|&byte| byte == 0).unwrap_or(b.len());
String::from_utf8_lossy(&b[..end]).to_string()
}
_ => coerce_to_string(value),
};
let truncated = if let Some(prec) = spec.precision {
if let Some((byte_idx, _)) = s.char_indices().nth(prec) {
&s[..byte_idx]
} else {
&s
}
} else {
&s
};
let mut flags = spec.flags.clone();
flags.zero_pad = false;
apply_width(output, "", truncated, spec.width, &flags, false);
}
fn format_char(output: &mut String, value: &Value, spec: &FormatSpec) {
let c = match value {
Value::Text(t) => t.value.chars().next().unwrap_or('\0'),
_ => {
let s = coerce_to_string(value);
s.chars().next().unwrap_or('\0')
}
};
if c == '\0' {
return;
}
let repeat = spec.precision.unwrap_or(1).max(1);
let s: String = repeat_n(c, repeat).collect();
let mut flags = spec.flags.clone();
flags.zero_pad = false;
apply_width(output, "", &s, spec.width, &flags, false);
}
fn escape_control_chars(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_control() {
write!(result, "\\u{:04x}", c as u32).unwrap();
} else if c == '\\' {
result.push_str("\\\\");
} else {
result.push(c);
}
}
result
}
fn format_sql_quote(output: &mut String, value: &Value, spec: &FormatSpec) {
let mut flags = spec.flags.clone();
flags.zero_pad = false;
match value {
Value::Null => {
let truncated = truncate_to_precision("(NULL)", spec.precision);
apply_width(output, "", truncated, spec.width, &flags, false);
}
_ => {
let s = coerce_to_string(value);
let truncated = truncate_to_precision(&s, spec.precision);
let mut escaped = truncated.replace('\'', "''");
if spec.flags.alternate {
escaped = escape_control_chars(&escaped);
}
apply_width(output, "", &escaped, spec.width, &flags, false);
}
}
}
fn format_sql_quote_wrap(output: &mut String, value: &Value, spec: &FormatSpec) {
let mut flags = spec.flags.clone();
flags.zero_pad = false;
match value {
Value::Null => {
let truncated = truncate_to_precision("NULL", spec.precision);
apply_width(output, "", truncated, spec.width, &flags, false);
}
_ => {
let s = coerce_to_string(value);
let truncated = truncate_to_precision(&s, spec.precision);
let mut escaped = truncated.replace('\'', "''");
let use_unistr = if spec.flags.alternate {
let has_ctrl = escaped.bytes().any(|b| b <= 0x1f);
if has_ctrl {
escaped = escape_control_chars(&escaped);
true
} else {
false
}
} else {
false
};
let mut quoted = String::with_capacity(escaped.len() + 10);
if use_unistr {
quoted.push_str("unistr('");
} else {
quoted.push('\'');
}
quoted.push_str(&escaped);
if use_unistr {
quoted.push_str("')");
} else {
quoted.push('\'');
}
apply_width(output, "", "ed, spec.width, &flags, false);
}
}
}
fn format_sql_identifier(output: &mut String, value: &Value, spec: &FormatSpec) {
let mut flags = spec.flags.clone();
flags.zero_pad = false;
match value {
Value::Null => {
let truncated = truncate_to_precision("(NULL)", spec.precision);
apply_width(output, "", truncated, spec.width, &flags, false);
}
_ => {
let s = coerce_to_string(value);
let truncated = truncate_to_precision(&s, spec.precision);
let escaped = truncated.replace('"', "\"\"");
apply_width(output, "", &escaped, spec.width, &flags, false);
}
}
}
fn format_ordinal(output: &mut String, value: &Value, spec: &FormatSpec) {
let i = coerce_to_i64(value);
let negative = i < 0;
let abs = i.unsigned_abs();
let suffix = match (abs % 100, abs % 10) {
(11..=13, _) => "th",
(_, 1) => "st",
(_, 2) => "nd",
(_, 3) => "rd",
_ => "th",
};
let mut digits = abs.to_string();
let digit_prec = spec.precision.map(|p| p.saturating_sub(suffix.len()));
digits = pad_with_precision(digits, digit_prec);
let prefix = sign_prefix(negative, &spec.flags);
if spec.flags.zero_pad {
let w = spec.width.unwrap_or(0);
let content_chars = prefix.len() + digits.len() + suffix.len();
if content_chars < w {
digits = "0".repeat(w - content_chars) + &digits;
}
output.push_str(prefix);
output.push_str(&digits);
output.push_str(suffix);
} else {
let content = format!("{digits}{suffix}");
apply_width(output, prefix, &content, spec.width, &spec.flags, false);
}
}
fn truncate_to_precision(s: &str, precision: Option<usize>) -> &str {
match precision {
Some(prec) => {
if let Some((byte_idx, _)) = s.char_indices().nth(prec) {
&s[..byte_idx]
} else {
s
}
}
_ => s,
}
}
pub fn exec_printf(values: &[Register]) -> crate::Result<Value> {
if values.is_empty() {
return Ok(Value::Null);
}
let format_value = values[0].get_value();
let fmt_owned: String;
let format_str = match &format_value {
Value::Text(t) => t.as_str(),
Value::Null => return Ok(Value::Null),
Value::Numeric(Numeric::Integer(i)) => {
fmt_owned = i.to_string();
fmt_owned.as_str()
}
Value::Numeric(Numeric::Float(f)) => {
fmt_owned = format_float(f64::from(*f));
fmt_owned.as_str()
}
Value::Blob(b) => {
fmt_owned = String::from_utf8_lossy(b).to_string();
fmt_owned.as_str()
}
};
let mut result = String::new();
let mut args_index = 1;
let mut chars = format_str.chars().peekable();
let mut touched = false;
while let Some(c) = chars.next() {
if c != '%' {
touched = true;
result.push(c);
continue;
}
if chars.peek() == Some(&'%') {
touched = true;
chars.next();
result.push('%');
continue;
}
if chars.peek().is_none() {
result.push('%');
break;
}
let spec = parse_format_spec(&mut chars, values, &mut args_index);
let needs_arg = !matches!(spec.spec_type, 'n' | '\0');
let null_val = Value::Null;
let arg = if needs_arg {
if args_index < values.len() {
let v = values[args_index].get_value();
args_index += 1;
v
} else {
&null_val
}
} else {
&null_val
};
match spec.spec_type {
'd' | 'i' => format_signed_int(&mut result, arg, &spec),
'u' => format_unsigned_int(&mut result, arg, &spec),
'f' => format_float_decimal(&mut result, arg, &spec),
'e' => format_exponential(&mut result, arg, &spec, false),
'E' => format_exponential(&mut result, arg, &spec, true),
'g' => format_general(&mut result, arg, &spec, false),
'G' => format_general(&mut result, arg, &spec, true),
'x' => format_hex(&mut result, arg, &spec, false),
'X' => format_hex(&mut result, arg, &spec, true),
'o' => format_octal(&mut result, arg, &spec),
'p' => format_hex(&mut result, arg, &spec, true),
's' | 'z' => format_string(&mut result, arg, &spec),
'c' => format_char(&mut result, arg, &spec),
'q' => format_sql_quote(&mut result, arg, &spec),
'Q' => format_sql_quote_wrap(&mut result, arg, &spec),
'w' => format_sql_identifier(&mut result, arg, &spec),
'r' => format_ordinal(&mut result, arg, &spec),
'n' => { }
_ => {
if !touched {
return Ok(Value::Null);
}
break;
}
}
touched = true;
}
Ok(Value::build_text(result))
}
#[cfg(test)]
mod tests {
use super::*;
fn text(value: &str) -> Register {
Register::Value(Value::build_text(value.to_string()))
}
fn integer(value: i64) -> Register {
Register::Value(Value::from_i64(value))
}
fn float(value: f64) -> Register {
Register::Value(Value::from_f64(value))
}
#[test]
fn test_printf_no_args() {
assert_eq!(exec_printf(&[]).unwrap(), Value::Null);
}
#[test]
fn test_printf_basic_string() {
assert_eq!(
exec_printf(&[text("Hello World")]).unwrap(),
*text("Hello World").get_value()
);
}
#[test]
fn test_printf_string_formatting() {
let test_cases = vec![
(
vec![text("Hello, %s!"), text("World")],
text("Hello, World!"),
),
(
vec![text("%s %s!"), text("Hello"), text("World")],
text("Hello World!"),
),
(
vec![text("Hello, %s!"), Register::Value(Value::Null)],
text("Hello, !"),
),
(vec![text("Value: %s"), integer(42)], text("Value: 42")),
(vec![text("100%% complete")], text("100% complete")),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_value());
}
}
#[test]
fn test_printf_integer_formatting() {
let test_cases = vec![
(vec![text("Number: %d"), integer(42)], text("Number: 42")),
(vec![text("Number: %d"), integer(-42)], text("Number: -42")),
(
vec![text("%d + %d = %d"), integer(2), integer(3), integer(5)],
text("2 + 3 = 5"),
),
(
vec![text("Number: %d"), text("not a number")],
text("Number: 0"),
),
(
vec![text("Truncated float: %d"), float(3.9)],
text("Truncated float: 3"),
),
(vec![text("Number: %i"), integer(42)], text("Number: 42")),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_value());
}
}
#[test]
fn test_printf_unsigned_integer_formatting() {
let test_cases = vec![
(vec![text("Number: %u"), integer(42)], text("Number: 42")),
(
vec![text("Negative: %u"), integer(-1)],
text("Negative: 18446744073709551615"),
),
(vec![text("NaN: %u"), text("not a number")], text("NaN: 0")),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_value());
}
}
#[test]
fn test_printf_float_formatting() {
let test_cases = vec![
(
vec![text("Number: %f"), float(42.5)],
text("Number: 42.500000"),
),
(
vec![text("Number: %f"), float(-42.5)],
text("Number: -42.500000"),
),
(
vec![text("Number: %f"), integer(42)],
text("Number: 42.000000"),
),
(
vec![text("Number: %f"), text("not a number")],
text("Number: 0.000000"),
),
];
let huge = exec_printf(&[text("%f"), float(1e308)]).unwrap();
let huge_str = match &huge {
Value::Text(t) => t.as_str().to_string(),
_ => panic!("expected text"),
};
assert!(huge_str.starts_with("9999999999999999"));
assert!(huge_str.ends_with(".000000"));
assert!(!huge_str.contains("inf"));
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_width_precision() {
let test_cases = vec![
(vec![text("%.2f"), float(4.002)], text("4.00")),
(vec![text("%05d"), integer(42)], text("00042")),
(vec![text("%.5d"), integer(42)], text("00042")),
(vec![text("%+d"), integer(42)], text("+42")),
(vec![text("%.3s"), text("hello")], text("hel")),
(vec![text("%08x"), integer(255)], text("000000ff")),
(vec![text("%#x"), integer(255)], text("0xff")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_dynamic_width() {
assert_eq!(
exec_printf(&[text("%.*f"), integer(2), float(3.14258)]).unwrap(),
*text("3.14").get_value()
);
}
#[test]
fn test_printf_character_formatting() {
let test_cases = vec![
(vec![text("character: %c"), text("a")], text("character: a")),
(
vec![text("character: %c"), text("this is a test")],
text("character: t"),
),
(
vec![text("character: %c"), integer(123)],
text("character: 1"),
),
(
vec![text("character: %c"), float(42.5)],
text("character: 4"),
),
(vec![text("character: %c"), text("")], text("character: ")),
(
vec![text("character: %c"), Register::Value(Value::Null)],
text("character: "),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_exponential_formatting() {
let test_cases = vec![
(
vec![text("Exp: %e"), float(23000000.0)],
text("Exp: 2.300000e+07"),
),
(
vec![text("Exp: %e"), float(-23000000.0)],
text("Exp: -2.300000e+07"),
),
(vec![text("Exp: %e"), float(0.0)], text("Exp: 0.000000e+00")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_general_formatting() {
let test_cases = vec![
(vec![text("%g"), float(100.0)], text("100")),
(vec![text("%g"), float(0.00123)], text("0.00123")),
(vec![text("%g"), float(1.0)], text("1")),
(vec![text("%g"), float(1.5)], text("1.5")),
(vec![text("%g"), float(0.0)], text("0")),
(vec![text("%g"), integer(42)], text("42")),
(vec![text("%,G"), integer(1000)], text("1,000")),
(
vec![text("%,.20G"), float(1234567.89)],
text("1,234,567.89"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_sql_quoting() {
assert_eq!(
exec_printf(&[text("%q"), text("it's")]).unwrap(),
*text("it''s").get_value()
);
assert_eq!(
exec_printf(&[text("%Q"), text("it's")]).unwrap(),
*text("'it''s'").get_value()
);
assert_eq!(
exec_printf(&[text("%Q"), Register::Value(Value::Null)]).unwrap(),
*text("NULL").get_value()
);
assert_eq!(
exec_printf(&[text("%q"), Register::Value(Value::Null)]).unwrap(),
*text("(NULL)").get_value()
);
}
#[test]
fn test_printf_comma_separator() {
assert_eq!(
exec_printf(&[text("%,d"), integer(1234567)]).unwrap(),
*text("1,234,567").get_value()
);
assert_eq!(
exec_printf(&[text("%,d"), integer(-1234567)]).unwrap(),
*text("-1,234,567").get_value()
);
}
#[test]
fn test_printf_edge_cases() {
let test_cases = vec![
(vec![text("")], text("")),
(vec![text("%%%%")], text("%%")),
(vec![text("No substitutions")], text("No substitutions")),
(
vec![text("%d%d%d"), integer(1), integer(2), integer(3)],
text("123"),
),
(vec![text("test%")], text("test%")),
(vec![text("%d%j"), integer(42)], text("42")),
(vec![text("hello%j")], text("hello")),
(vec![text("%n%j")], text("")),
(vec![text("%f"), float(-0.0)], text("0.000000")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_hexadecimal_formatting() {
let test_cases = vec![
(vec![text("hex: %x"), integer(4)], text("hex: 4")),
(
vec![text("hex: %X"), integer(15565303546)],
text("hex: 39FC3AEFA"),
),
(
vec![text("hex: %x"), integer(-15565303546)],
text("hex: fffffffc603c5106"),
),
(vec![text("hex: %x"), float(42.5)], text("hex: 2a")),
(vec![text("hex: %x"), text("42")], text("hex: 2a")),
(vec![text("hex: %x"), text("")], text("hex: 0")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_printf_octal_formatting() {
let test_cases = vec![
(vec![text("octal: %o"), integer(4)], text("octal: 4")),
(vec![text("octal: %o"), float(42.5)], text("octal: 52")),
(vec![text("octal: %o"), text("42")], text("octal: 52")),
(vec![text("%#o"), integer(8)], text("010")),
(vec![text("%#o"), integer(0)], text("0")),
(vec![text("%#.5o"), integer(8)], text("000010")),
(
vec![text("%#.20o"), integer(1000)],
text("000000000000000001750"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_value());
}
}
#[test]
fn test_rounding_half_away_from_zero() {
assert_eq!(
exec_printf(&[text("%.0f"), float(0.5)]).unwrap(),
*text("1").get_value()
);
assert_eq!(
exec_printf(&[text("%.0f"), float(2.5)]).unwrap(),
*text("3").get_value()
);
assert_eq!(
exec_printf(&[text("%.0f"), float(-0.5)]).unwrap(),
*text("-1").get_value()
);
assert_eq!(
exec_printf(&[text("%.0e"), float(2.5)]).unwrap(),
*text("3e+00").get_value()
);
}
#[test]
fn test_alt_hex_zero_pad_width() {
assert_eq!(
exec_printf(&[text("%#08x"), integer(255)]).unwrap(),
*text("0x000000ff").get_value()
);
assert_eq!(
exec_printf(&[text("%#04x"), integer(255)]).unwrap(),
*text("0x00ff").get_value()
);
assert_eq!(
exec_printf(&[text("%#08o"), integer(8)]).unwrap(),
*text("000000010").get_value()
);
}
#[test]
fn test_alt_flag_forces_decimal_point() {
assert_eq!(
exec_printf(&[text("%#.0e"), float(1.0)]).unwrap(),
*text("1.e+00").get_value()
);
assert_eq!(
exec_printf(&[text("%#.0g"), float(1.0)]).unwrap(),
*text("1.").get_value()
);
assert_eq!(
exec_printf(&[text("%#g"), float(100000.0)]).unwrap(),
*text("100000.").get_value()
);
}
#[test]
fn test_g_threshold_rounding() {
assert_eq!(
exec_printf(&[text("%g"), float(999999.5)]).unwrap(),
*text("1e+06").get_value()
);
assert_eq!(
exec_printf(&[text("%.1g"), float(9.5)]).unwrap(),
*text("1e+01").get_value()
);
}
#[test]
fn test_zero_pad_ignored_for_strings() {
assert_eq!(
exec_printf(&[text("%05s"), text("hi")]).unwrap(),
*text(" hi").get_value()
);
assert_eq!(
exec_printf(&[text("%05c"), text("A")]).unwrap(),
*text(" A").get_value()
);
}
#[test]
fn test_q_width_precision() {
assert_eq!(
exec_printf(&[text("%.2q"), text("hello")]).unwrap(),
*text("he").get_value()
);
assert_eq!(
exec_printf(&[text("%10q"), text("hi")]).unwrap(),
*text(" hi").get_value()
);
assert_eq!(
exec_printf(&[text("%10Q"), text("hi")]).unwrap(),
*text(" 'hi'").get_value()
);
}
#[test]
fn test_infinity_handling() {
let inf_f = exec_printf(&[text("%020f"), float(f64::INFINITY)]).unwrap();
let inf_str = match &inf_f {
Value::Text(t) => t.as_str().to_string(),
_ => panic!("expected text"),
};
assert!(inf_str.starts_with("9000"));
assert_eq!(inf_str.len(), 1007);
assert_eq!(
exec_printf(&[text("%020e"), float(f64::INFINITY)]).unwrap(),
*text("00000009.000000e+999").get_value()
);
assert_eq!(
exec_printf(&[text("%020g"), float(f64::INFINITY)]).unwrap(),
*text("000000000000009e+999").get_value()
);
assert_eq!(
exec_printf(&[text("%e"), float(f64::INFINITY)]).unwrap(),
*text("Inf").get_value()
);
assert_eq!(
exec_printf(&[text("%G"), float(f64::INFINITY)]).unwrap(),
*text("Inf").get_value()
);
assert_eq!(
exec_printf(&[text("%f"), float(f64::INFINITY)]).unwrap(),
*text("Inf").get_value()
);
assert_eq!(
exec_printf(&[text("%0G"), float(f64::INFINITY)]).unwrap(),
*text("9E+999").get_value()
);
assert_eq!(
exec_printf(&[text("%0,G"), float(f64::INFINITY)]).unwrap(),
*text("9E+999").get_value()
);
assert_eq!(
exec_printf(&[text("%e"), float(f64::NEG_INFINITY)]).unwrap(),
*text("-Inf").get_value()
);
assert_eq!(
exec_printf(&[text("%020e"), float(f64::NEG_INFINITY)]).unwrap(),
*text("-0000009.000000e+999").get_value()
);
assert_eq!(
exec_printf(&[text("%#0g"), float(f64::INFINITY)]).unwrap(),
*text("9.00000e+999").get_value()
);
assert_eq!(
exec_printf(&[text("%!0e"), float(f64::INFINITY)]).unwrap(),
*text("9.0e+999").get_value()
);
let inf_bang_f = exec_printf(&[text("%!0f"), float(f64::INFINITY)]).unwrap();
let inf_bang_str = match &inf_bang_f {
Value::Text(t) => t.as_str().to_string(),
_ => panic!("expected text"),
};
assert!(
inf_bang_str.ends_with(".0"),
"Infinity with %!0f should end with .0, got: ...{}",
&inf_bang_str[inf_bang_str.len().saturating_sub(10)..]
);
}
#[test]
fn test_significant_digits_limiting() {
assert_eq!(
exec_printf(&[text("%.20f"), float(1.0 / 3.0)]).unwrap(),
*text("0.33333333333333330000").get_value()
);
assert_eq!(
exec_printf(&[text("%.20e"), float(1.0 / 3.0)]).unwrap(),
*text("3.33333333333333300000e-01").get_value()
);
assert_eq!(
exec_printf(&[text("%!.20f"), float(1.0 / 3.0)]).unwrap(),
*text("0.3333333333333333148").get_value()
);
}
#[test]
fn test_nan_handling() {
assert_eq!(
exec_printf(&[text("%f"), float(f64::NAN)]).unwrap(),
*text("0.000000").get_value()
);
assert_eq!(
exec_printf(&[text("%e"), float(f64::NAN)]).unwrap(),
*text("0.000000e+00").get_value()
);
assert_eq!(
exec_printf(&[text("%g"), float(f64::NAN)]).unwrap(),
*text("0").get_value()
);
}
#[test]
fn test_blob_nul_truncation() {
let blob_val = Register::Value(Value::Blob(vec![0x48, 0x00, 0x4C])); let result = exec_printf(&[text("%s"), blob_val]).unwrap();
assert_eq!(result, *text("H").get_value());
let blob_hello = Register::Value(Value::Blob(b"Hello".to_vec()));
assert_eq!(
exec_printf(&[text("%s"), blob_hello]).unwrap(),
*text("Hello").get_value()
);
}
#[test]
fn test_limit_significant_digits_rounding() {
assert_eq!(limit_significant_digits("123456789", 5), "123460000");
assert_eq!(limit_significant_digits("1.23456789", 5), "1.23460000");
assert_eq!(limit_significant_digits("0.001234", 3), "0.001230");
assert_eq!(limit_significant_digits("9.9999", 3), "10.0000");
assert_eq!(limit_significant_digits("0.099999", 4), "0.100000");
}
#[test]
fn test_i32_star_precision_wrapping() {
assert_eq!(
exec_printf(&[text("%.*d"), integer(-2147483648), integer(42)]).unwrap(),
*text("42").get_value()
);
assert_eq!(
exec_printf(&[text("%.*d"), integer(4294967295), integer(42)]).unwrap(),
*text("42").get_value()
);
}
#[test]
fn test_comma_zero_pad_interaction() {
assert_eq!(
exec_printf(&[text("%0,15d"), integer(42)]).unwrap(),
*text("000,000,000,000,042").get_value()
);
assert_eq!(
exec_printf(&[text("%0,15u"), integer(42)]).unwrap(),
*text("000,000,000,000,042").get_value()
);
assert_eq!(
exec_printf(&[text("%-0,15d"), integer(42)]).unwrap(),
*text("000,000,000,000,042").get_value()
);
}
#[test]
fn test_ordinal_format() {
assert_eq!(
exec_printf(&[text("%r"), integer(1)]).unwrap(),
*text("1st").get_value()
);
assert_eq!(
exec_printf(&[text("%r"), integer(2)]).unwrap(),
*text("2nd").get_value()
);
assert_eq!(
exec_printf(&[text("%r"), integer(3)]).unwrap(),
*text("3rd").get_value()
);
assert_eq!(
exec_printf(&[text("%r"), integer(11)]).unwrap(),
*text("11th").get_value()
);
assert_eq!(
exec_printf(&[text("%r"), integer(112)]).unwrap(),
*text("112th").get_value()
);
assert_eq!(
exec_printf(&[text("%.5r"), integer(-39)]).unwrap(),
*text("-039th").get_value()
);
assert_eq!(
exec_printf(&[text("% r"), integer(42)]).unwrap(),
*text(" 42nd").get_value()
);
assert_eq!(
exec_printf(&[text("%010r"), integer(0)]).unwrap(),
*text("00000000th").get_value()
);
}
#[test]
fn test_q_null_precision_truncation() {
assert_eq!(
exec_printf(&[text("%.0q"), Register::Value(Value::Null)]).unwrap(),
*text("").get_value()
);
assert_eq!(
exec_printf(&[text("%.3q"), Register::Value(Value::Null)]).unwrap(),
*text("(NU").get_value()
);
assert_eq!(
exec_printf(&[text("%.0Q"), Register::Value(Value::Null)]).unwrap(),
*text("").get_value()
);
}
#[test]
fn test_q_null_width_padding() {
assert_eq!(
exec_printf(&[text("%-10q"), Register::Value(Value::Null)]).unwrap(),
*text("(NULL) ").get_value()
);
}
#[test]
fn test_unknown_specifier_returns_early() {
assert_eq!(
exec_printf(&[text("%b"), integer(42)]).unwrap(),
Value::Null,
);
assert_eq!(
exec_printf(&[text("hello%b"), integer(42)]).unwrap(),
*text("hello").get_value()
);
assert_eq!(
exec_printf(&[text("%n%b"), integer(42)]).unwrap(),
*text("").get_value(),
);
}
#[test]
fn test_control_char_escaping_with_hash_q() {
assert_eq!(
exec_printf(&[text("%#q"), text("a\nb")]).unwrap(),
*text("a\\u000ab").get_value()
);
assert_eq!(
exec_printf(&[text("%#q"), text("a\tb")]).unwrap(),
*text("a\\u0009b").get_value()
);
assert_eq!(
exec_printf(&[text("%#q"), text("a\\b")]).unwrap(),
*text("a\\\\b").get_value()
);
}
#[test]
fn test_hash_q_upper_unistr_wrapping() {
assert_eq!(
exec_printf(&[text("%#Q"), text("a\nb")]).unwrap(),
*text("unistr('a\\u000ab')").get_value()
);
assert_eq!(
exec_printf(&[text("%#Q"), text("hello")]).unwrap(),
*text("'hello'").get_value()
);
assert_eq!(
exec_printf(&[text("%Q"), text("a\nb")]).unwrap(),
*text("'a\nb'").get_value()
);
}
#[test]
fn test_very_small_float_no_nan() {
let result = exec_printf(&[text("%.*G"), integer(10), float(1e-300)]).unwrap();
assert_eq!(result, *text("1E-300").get_value());
let result = exec_printf(&[text("%.10e"), float(1e-300)]).unwrap();
assert!(
!result.to_string().contains("NaN"),
"1e-300 with %e should not produce NaN"
);
let result = exec_printf(&[text("%.10g"), float(1e-300)]).unwrap();
assert!(
!result.to_string().contains("NaN"),
"1e-300 with %g should not produce NaN"
);
}
#[test]
fn test_large_float_f_format() {
let result = exec_printf(&[text("%.0f"), float(1e308)]).unwrap();
let s = result.to_string();
assert!(
s.starts_with("99999999999999990"),
"1e308 with %.0f should start with 9999..., got: {}",
&s[..s.len().min(40)]
);
let result = exec_printf(&[text("%,f"), float(1e308)]).unwrap();
let s = result.to_string();
assert!(
s.starts_with("99,999,999,999,999,990"),
"1e308 with %,f should start with 99,999..., got: {}",
&s[..s.len().min(40)]
);
}
#[test]
fn test_negative_zero_suppression() {
let result = exec_printf(&[text("%#f"), float(-0.0000001)]).unwrap();
assert_eq!(result.to_string(), "0.000000");
let result = exec_printf(&[text("%f"), float(-0.0000001)]).unwrap();
assert_eq!(result.to_string(), "-0.000000");
let result = exec_printf(&[text("%#+f"), float(-0.0000001)]).unwrap();
assert_eq!(result.to_string(), "-0.000000");
let result = exec_printf(&[text("%#.0f"), float(-0.5)]).unwrap();
assert_eq!(result.to_string(), "-1.");
let result = exec_printf(&[text("%#.0f"), float(-0.4)]).unwrap();
assert_eq!(result.to_string(), "0.");
let result = exec_printf(&[text("%#,f"), float(-0.0000001)]).unwrap();
assert_eq!(result.to_string(), "0.000000");
}
}