use crate::types::Value;
use crate::vdbe::Register;
use crate::LimboError;
#[inline(always)]
pub fn exec_printf(values: &[Register]) -> crate::Result<Value> {
if values.is_empty() {
return Ok(Value::Null);
}
let format_str = match &values[0].get_owned_value() {
Value::Text(t) => t.as_str(),
_ => return Ok(Value::Null),
};
let mut result = String::new();
let mut args_index = 1;
let mut chars = format_str.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
result.push(c);
continue;
}
match chars.next() {
Some('%') => {
result.push('%');
continue;
}
Some('d') | Some('i') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let value = &values[args_index].get_owned_value();
match value {
Value::Integer(_) => result.push_str(&format!("{}", value)),
Value::Float(_) => result.push_str(&format!("{}", value)),
_ => result.push_str("0".into()),
}
args_index += 1;
}
Some('s') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
match &values[args_index].get_owned_value() {
Value::Text(t) => result.push_str(t.as_str()),
Value::Null => result.push_str("(null)"),
v => result.push_str(&format!("{}", v)),
}
args_index += 1;
}
Some('f') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let value = &values[args_index].get_owned_value();
match value {
Value::Float(f) => result.push_str(&format!("{:.6}", f)),
Value::Integer(i) => result.push_str(&format!("{:.6}", *i as f64)),
_ => result.push_str("0.0".into()),
}
args_index += 1;
}
Some('x') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let n = printf_integer_arg(values[args_index].get_owned_value());
result.push_str(&format!("{:x}", n as u64));
args_index += 1;
}
Some('X') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let n = printf_integer_arg(values[args_index].get_owned_value());
result.push_str(&format!("{:X}", n as u64));
args_index += 1;
}
Some('o') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let n = printf_integer_arg(values[args_index].get_owned_value());
result.push_str(&format!("{:o}", n as u64));
args_index += 1;
}
Some('c') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let value = values[args_index].get_owned_value();
if let Some(first_char) = format!("{value}").chars().next() {
result.push(first_char);
}
args_index += 1;
}
Some('e') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let value = &values[args_index].get_owned_value();
let f = match value {
Value::Float(f) => *f,
Value::Integer(i) => *i as f64,
_ => 0.0,
};
result.push_str(&format_scientific(f, false));
args_index += 1;
}
Some('E') => {
if args_index >= values.len() {
return Err(LimboError::InvalidArgument("not enough arguments".into()));
}
let value = &values[args_index].get_owned_value();
let f = match value {
Value::Float(f) => *f,
Value::Integer(i) => *i as f64,
_ => 0.0,
};
result.push_str(&format_scientific(f, true));
args_index += 1;
}
None => {
return Err(LimboError::InvalidArgument(
"incomplete format specifier".into(),
))
}
_ => {
return Err(LimboError::InvalidFormatter(
"this formatter is not supported".into(),
));
}
}
}
Ok(Value::build_text(&result))
}
#[inline(always)]
fn printf_integer_arg(value: &Value) -> i64 {
match value {
Value::Integer(i) => *i,
Value::Float(f) => *f as i64,
_ => 0,
}
}
#[inline(always)]
fn format_scientific(value: f64, uppercase: bool) -> String {
let value = if value == 0.0 { 0.0 } else { value };
let formatted = if uppercase {
format!("{value:.6E}")
} else {
format!("{value:.6e}")
};
let exp_marker = if uppercase { 'E' } else { 'e' };
match formatted.split_once(exp_marker) {
Some((mantissa, exponent)) => {
let (sign, digits) = match exponent.strip_prefix('-') {
Some(rest) => ('-', rest),
None => ('+', exponent),
};
format!("{mantissa}{exp_marker}{sign}{digits:0>2}")
}
None => formatted,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn text(value: &str) -> Register {
Register::Value(Value::build_text(value))
}
fn integer(value: i64) -> Register {
Register::Value(Value::Integer(value))
}
fn float(value: f64) -> Register {
Register::Value(Value::Float(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_owned_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, (null)!"),
),
(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_owned_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"),
),
];
for (input, output) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *output.get_owned_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("%f + %f = %f"), float(2.5), float(3.5), float(6.0)],
text("2.500000 + 3.500000 = 6.000000"),
),
(
vec![text("Number: %f"), text("not a number")],
text("Number: 0.0"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_i_is_alias_for_d() {
let test_cases = vec![
(vec![text("Number: %i"), integer(42)], text("Number: 42")),
(vec![text("Number: %i"), integer(-42)], text("Number: -42")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_hex_lowercase() {
let test_cases = vec![
(vec![text("%x"), integer(255)], text("ff")),
(vec![text("%x"), integer(16)], text("10")),
(vec![text("%x"), integer(-1)], text("ffffffffffffffff")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_hex_uppercase() {
let test_cases = vec![
(vec![text("%X"), integer(255)], text("FF")),
(vec![text("%X"), integer(-1)], text("FFFFFFFFFFFFFFFF")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_octal() {
let test_cases = vec![
(vec![text("%o"), integer(8)], text("10")),
(vec![text("%o"), integer(64)], text("100")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_char() {
let test_cases = vec![
(vec![text("%c"), integer(65)], text("6")),
(vec![text("%c"), text("ABC")], text("A")),
(vec![text("%c"), Register::Value(Value::Null)], text("")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_scientific_lowercase() {
let test_cases = vec![
(vec![text("%e"), float(12345.6789)], text("1.234568e+04")),
(vec![text("%e"), float(0.000123)], text("1.230000e-04")),
(vec![text("%e"), float(-12345.6789)], text("-1.234568e+04")),
(vec![text("%e"), float(0.0)], text("0.000000e+00")),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_scientific_uppercase() {
let test_cases = vec![(vec![text("%E"), float(12345.6789)], text("1.234568E+04"))];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_mixed_formatting() {
let test_cases = vec![
(
vec![text("%s: %d"), text("Count"), integer(42)],
text("Count: 42"),
),
(
vec![
text("%s: %d (%f%%)"),
text("Progress"),
integer(75),
float(75.5),
],
text("Progress: 75 (75.500000%)"),
),
(
vec![
text("Name: %s, ID: %d, Score: %f"),
text("John"),
integer(123),
float(95.5),
],
text("Name: John, ID: 123, Score: 95.500000"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
#[test]
fn test_printf_error_cases() {
let error_cases = vec![
vec![text("%d %d"), integer(42)],
vec![text("%z"), integer(42)],
vec![text("incomplete %")],
];
for case in error_cases {
assert!(exec_printf(&case).is_err());
}
}
#[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("Special chars: %s"), text("\n\t\r")],
text("Special chars: \n\t\r"),
),
];
for (input, expected) in test_cases {
assert_eq!(exec_printf(&input).unwrap(), *expected.get_owned_value());
}
}
}