use std::char;
use std::iter::Peekable;
use std::str::Chars;
use runmat_builtins::{IntValue, LogicalArray, StringArray, Value};
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
#[derive(Debug)]
pub struct ArgCursor<'a> {
args: &'a [Value],
index: usize,
}
impl<'a> ArgCursor<'a> {
pub fn new(args: &'a [Value]) -> Self {
Self { args, index: 0 }
}
pub fn remaining(&self) -> usize {
self.args.len().saturating_sub(self.index)
}
pub fn index(&self) -> usize {
self.index
}
fn next(&mut self) -> BuiltinResult<Value> {
if self.index >= self.args.len() {
return Err(format_error(
"sprintf: not enough input arguments for format specifier",
));
}
let value = self.args[self.index].clone();
self.index += 1;
Ok(value)
}
}
fn format_error(message: impl Into<String>) -> RuntimeError {
build_runtime_error(message).build()
}
fn map_control_flow_with_context(err: RuntimeError, context: &str) -> RuntimeError {
crate::builtins::common::map_control_flow_with_builtin(err, context)
}
#[derive(Debug, Default, Clone)]
pub struct FormatStepResult {
pub output: String,
pub consumed: usize,
}
#[derive(Clone, Copy, Default)]
struct FormatFlags {
alternate: bool,
zero_pad: bool,
left_align: bool,
sign_plus: bool,
sign_space: bool,
grouping: bool,
}
#[derive(Clone, Copy)]
enum Count {
Value(isize),
FromArgument,
}
#[derive(Clone, Copy)]
struct FormatSpec {
flags: FormatFlags,
width: Option<Count>,
precision: Option<Count>,
conversion: char,
}
pub fn format_variadic(fmt: &str, args: &[Value]) -> BuiltinResult<String> {
let mut cursor = ArgCursor::new(args);
let step = format_variadic_with_cursor(fmt, &mut cursor)?;
Ok(step.output)
}
pub fn format_variadic_with_cursor(
fmt: &str,
cursor: &mut ArgCursor<'_>,
) -> BuiltinResult<FormatStepResult> {
format_once(fmt, cursor)
}
fn format_once(fmt: &str, cursor: &mut ArgCursor<'_>) -> BuiltinResult<FormatStepResult> {
let mut chars = fmt.chars().peekable();
let mut out = String::with_capacity(fmt.len());
let mut consumed = 0usize;
while let Some(ch) = chars.next() {
if ch != '%' {
out.push(ch);
continue;
}
if let Some('%') = chars.peek() {
chars.next();
out.push('%');
continue;
}
let spec = parse_format_spec(&mut chars)?;
let (formatted, used) = apply_format_spec(spec, cursor)?;
consumed += used;
out.push_str(&formatted);
}
Ok(FormatStepResult {
output: out,
consumed,
})
}
fn parse_format_spec(chars: &mut Peekable<Chars<'_>>) -> BuiltinResult<FormatSpec> {
let mut flags = FormatFlags::default();
loop {
match chars.peek().copied() {
Some('#') => {
flags.alternate = true;
chars.next();
}
Some('0') => {
flags.zero_pad = true;
chars.next();
}
Some('-') => {
flags.left_align = true;
chars.next();
}
Some(' ') => {
flags.sign_space = true;
chars.next();
}
Some('+') => {
flags.sign_plus = true;
chars.next();
}
Some('\'') => {
flags.grouping = true;
chars.next();
}
Some('I') => {
chars.next();
}
_ => break,
}
}
let width = if let Some('*') = chars.peek() {
chars.next();
Some(Count::FromArgument)
} else {
parse_number(chars).map(Count::Value)
};
let precision = if let Some('.') = chars.peek() {
chars.next();
if let Some('*') = chars.peek() {
chars.next();
Some(Count::FromArgument)
} else {
Some(Count::Value(parse_number(chars).unwrap_or(0)))
}
} else {
None
};
if let Some(&('h' | 'l' | 'L' | 'z' | 'j' | 't')) = chars.peek() {
let current = chars.next().unwrap();
if matches!(current, 'h' | 'l') && chars.peek() == Some(¤t) {
chars.next();
}
}
let conversion = chars
.next()
.ok_or_else(|| format_error("sprintf: incomplete format specifier"))?;
Ok(FormatSpec {
flags,
width,
precision,
conversion,
})
}
fn parse_number(chars: &mut Peekable<Chars<'_>>) -> Option<isize> {
let mut value: i128 = 0;
let mut seen = false;
while let Some(&ch) = chars.peek() {
if !ch.is_ascii_digit() {
break;
}
seen = true;
value = value * 10 + i128::from((ch as u8 - b'0') as i16);
chars.next();
}
if seen {
let capped = value
.clamp(isize::MIN as i128, isize::MAX as i128)
.try_into()
.unwrap_or(isize::MAX);
Some(capped)
} else {
None
}
}
fn apply_format_spec(
spec: FormatSpec,
cursor: &mut ArgCursor<'_>,
) -> BuiltinResult<(String, usize)> {
let mut consumed = 0usize;
let mut flags = spec.flags;
let mut width = match spec.width {
Some(Count::Value(w)) => Some(w),
Some(Count::FromArgument) => {
let value = cursor.next()?;
consumed += 1;
let w = value_to_isize(&value)?;
Some(w)
}
None => None,
};
let precision = match spec.precision {
Some(Count::Value(p)) => Some(p),
Some(Count::FromArgument) => {
let value = cursor.next()?;
consumed += 1;
let p = value_to_isize(&value)?;
if p < 0 {
None
} else {
Some(p)
}
}
None => None,
};
if let Some(w) = width {
if w < 0 {
flags.left_align = true;
width = Some(-w);
}
}
let conversion = spec.conversion;
let formatted = match conversion {
'd' | 'i' => {
let value = cursor.next()?;
consumed += 1;
let int_value = value_to_i128(&value)?;
format_integer(
int_value,
int_value.is_negative(),
10,
flags,
width,
precision,
false,
false,
)
}
'u' => {
let value = cursor.next()?;
consumed += 1;
let uint_value = value_to_u128(&value)?;
format_unsigned(uint_value, 10, flags, width, precision, false, false)
}
'o' => {
let value = cursor.next()?;
consumed += 1;
let uint_value = value_to_u128(&value)?;
format_unsigned(
uint_value,
8,
flags,
width,
precision,
spec.flags.alternate,
false,
)
}
'x' => {
let value = cursor.next()?;
consumed += 1;
let uint_value = value_to_u128(&value)?;
format_unsigned(
uint_value,
16,
flags,
width,
precision,
spec.flags.alternate,
false,
)
}
'X' => {
let value = cursor.next()?;
consumed += 1;
let uint_value = value_to_u128(&value)?;
format_unsigned(
uint_value,
16,
flags,
width,
precision,
spec.flags.alternate,
true,
)
}
'b' => {
let value = cursor.next()?;
consumed += 1;
let uint_value = value_to_u128(&value)?;
format_unsigned(
uint_value,
2,
flags,
width,
precision,
spec.flags.alternate,
false,
)
}
'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
let value = cursor.next()?;
consumed += 1;
let float_value = value_to_f64(&value)?;
format_float(
float_value,
conversion,
flags,
width,
precision,
spec.flags.alternate,
)
}
's' => {
let value = cursor.next()?;
consumed += 1;
format_string(value, flags, width, precision)
}
'c' => {
let value = cursor.next()?;
consumed += 1;
format_char(value, flags, width)
}
other => {
return Err(format_error(format!(
"sprintf: unsupported format %{other}"
)));
}
}?;
Ok((formatted, consumed))
}
#[allow(clippy::too_many_arguments)]
fn format_integer(
value: i128,
is_negative: bool,
base: u32,
mut flags: FormatFlags,
width: Option<isize>,
precision: Option<isize>,
alternate: bool,
uppercase: bool,
) -> BuiltinResult<String> {
let mut sign = String::new();
let abs_val = value.unsigned_abs();
if is_negative {
sign.push('-');
} else if flags.sign_plus {
sign.push('+');
} else if flags.sign_space {
sign.push(' ');
}
if precision.is_some() {
flags.zero_pad = false;
}
let mut digits = to_base_string(abs_val, base, uppercase);
let precision_value = precision.unwrap_or(-1);
if precision_value == 0 && abs_val == 0 {
digits.clear();
}
if precision_value > 0 {
let required = precision_value as usize;
if digits.len() < required {
let mut buf = String::with_capacity(required);
for _ in 0..(required - digits.len()) {
buf.push('0');
}
buf.push_str(&digits);
digits = buf;
}
}
let mut prefix = String::new();
if alternate && abs_val != 0 {
match base {
8 => prefix.push('0'),
16 => {
prefix.push('0');
prefix.push(if uppercase { 'X' } else { 'x' });
}
2 => {
prefix.push('0');
prefix.push('b');
}
_ => {}
}
}
if flags.grouping && base == 10 {
digits = group_decimal_digits(&digits);
}
apply_width(sign, prefix, digits, flags, width, flags.zero_pad)
}
fn format_unsigned(
value: u128,
base: u32,
mut flags: FormatFlags,
width: Option<isize>,
precision: Option<isize>,
alternate: bool,
uppercase: bool,
) -> BuiltinResult<String> {
if precision.is_some() {
flags.zero_pad = false;
}
let mut digits = to_base_string(value, base, uppercase);
let precision_value = precision.unwrap_or(-1);
if precision_value == 0 && value == 0 {
digits.clear();
}
if precision_value > 0 {
let required = precision_value as usize;
if digits.len() < required {
let mut buf = String::with_capacity(required);
for _ in 0..(required - digits.len()) {
buf.push('0');
}
buf.push_str(&digits);
digits = buf;
}
}
let mut prefix = String::new();
if alternate && value != 0 {
match base {
8 => prefix.push('0'),
16 => {
prefix.push_str(if uppercase { "0X" } else { "0x" });
}
2 => prefix.push_str("0b"),
_ => {}
}
}
if flags.grouping && base == 10 {
digits = group_decimal_digits(&digits);
}
apply_width(String::new(), prefix, digits, flags, width, flags.zero_pad)
}
fn format_float(
value: f64,
conversion: char,
flags: FormatFlags,
width: Option<isize>,
precision: Option<isize>,
alternate: bool,
) -> BuiltinResult<String> {
let mut sign = String::new();
let mut magnitude = value;
if value.is_nan() {
return apply_width(
String::new(),
String::new(),
"NaN".to_string(),
flags,
width,
false,
);
}
if value.is_infinite() {
if value.is_sign_negative() {
sign.push('-');
} else if flags.sign_plus {
sign.push('+');
} else if flags.sign_space {
sign.push(' ');
}
let text = "Inf".to_string();
return apply_width(sign, String::new(), text, flags, width, false);
}
if value.is_sign_negative() || (value == 0.0 && (1.0 / value).is_sign_negative()) {
sign.push('-');
magnitude = -value;
} else if flags.sign_plus {
sign.push('+');
} else if flags.sign_space {
sign.push(' ');
}
let prec = precision.unwrap_or(6).max(0) as usize;
let mut body = match conversion {
'f' | 'F' => format!("{magnitude:.prec$}"),
'e' => format!("{magnitude:.prec$e}"),
'E' => format!("{magnitude:.prec$E}"),
'g' | 'G' => format_float_general(magnitude, prec, conversion.is_uppercase()),
_ => {
return Err(format_error(format!(
"sprintf: unsupported float conversion %{}",
conversion
)))
}
};
if alternate && !body.contains('.') && matches!(conversion, 'f' | 'F' | 'g' | 'G') {
body.push('.');
}
if flags.grouping && matches!(conversion, 'f' | 'F' | 'g' | 'G') {
body = group_float_mantissa(&body);
}
let zero_pad_allowed = flags.zero_pad && !flags.left_align;
apply_width(sign, String::new(), body, flags, width, zero_pad_allowed)
}
fn format_float_general(value: f64, precision: usize, uppercase: bool) -> String {
if value == 0.0 {
if precision == 0 {
return "0".to_string();
}
let mut zero = String::from("0");
if precision > 0 {
zero.push('.');
zero.push_str(&"0".repeat(precision.saturating_sub(1)));
}
return zero;
}
let mut prec = precision;
if prec == 0 {
prec = 1;
}
let abs_val = value.abs();
let exp = abs_val.log10().floor() as i32;
let use_exp = exp < -4 || exp >= prec as i32;
if use_exp {
let mut s = format!("{:.*e}", prec - 1, value);
if uppercase {
s = s.to_uppercase();
}
trim_trailing_zeros(&mut s, true);
s
} else {
let mut s = format!("{:.*}", prec.max(1) - 1, value);
trim_trailing_zeros(&mut s, false);
s
}
}
fn trim_trailing_zeros(text: &mut String, keep_exponent: bool) {
if let Some(dot_idx) = text.find('.') {
let mut end = text.len();
while end > dot_idx + 1 {
let byte = text.as_bytes()[end - 1];
if byte == b'0' {
end -= 1;
} else {
break;
}
}
if end > dot_idx + 1 && text.as_bytes()[end - 1] == b'.' {
end -= 1;
}
if keep_exponent {
if let Some(exp_idx) = text.find(['e', 'E']) {
let exponent = text[exp_idx..].to_string();
text.truncate(end.min(exp_idx));
text.push_str(&exponent);
return;
}
}
text.truncate(end);
}
}
fn format_string(
value: Value,
flags: FormatFlags,
width: Option<isize>,
precision: Option<isize>,
) -> BuiltinResult<String> {
let mut text = value_to_string(&value)?;
if let Some(p) = precision {
if p >= 0 {
let mut chars = text.chars();
let mut truncated = String::with_capacity(text.len());
for _ in 0..(p as usize) {
if let Some(ch) = chars.next() {
truncated.push(ch);
} else {
break;
}
}
text = truncated;
}
}
apply_width(String::new(), String::new(), text, flags, width, false)
}
fn format_char(value: Value, flags: FormatFlags, width: Option<isize>) -> BuiltinResult<String> {
let ch = value_to_char(&value)?;
let text = ch.to_string();
apply_width(String::new(), String::new(), text, flags, width, false)
}
fn apply_width(
sign: String,
prefix: String,
digits: String,
flags: FormatFlags,
width: Option<isize>,
zero_pad: bool,
) -> BuiltinResult<String> {
let mut result = String::new();
let sign_prefix_len = sign.len() + prefix.len();
let total_len = sign_prefix_len + digits.len();
let target_width = width.unwrap_or(0).max(0) as usize;
if target_width <= total_len {
result.push_str(&sign);
result.push_str(&prefix);
result.push_str(&digits);
return Ok(result);
}
let pad_len = target_width - total_len;
if flags.left_align {
result.push_str(&sign);
result.push_str(&prefix);
result.push_str(&digits);
for _ in 0..pad_len {
result.push(' ');
}
return Ok(result);
}
if zero_pad {
result.push_str(&sign);
result.push_str(&prefix);
for _ in 0..pad_len {
result.push('0');
}
result.push_str(&digits);
} else {
for _ in 0..pad_len {
result.push(' ');
}
result.push_str(&sign);
result.push_str(&prefix);
result.push_str(&digits);
}
Ok(result)
}
fn value_to_isize(value: &Value) -> BuiltinResult<isize> {
match value {
Value::Int(i) => Ok(i.to_i64().clamp(isize::MIN as i64, isize::MAX as i64) as isize),
Value::Num(n) => {
if !n.is_finite() {
return Err(format_error(
"sprintf: width/precision specifier must be finite",
));
}
Ok(n.trunc().clamp(isize::MIN as f64, isize::MAX as f64) as isize)
}
Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
other => Err(format_error(format!(
"sprintf: width/precision specifier expects numeric value, got {other:?}"
))),
}
}
fn value_to_i128(value: &Value) -> BuiltinResult<i128> {
match value {
Value::Int(i) => Ok(match i {
IntValue::I8(v) => i128::from(*v),
IntValue::I16(v) => i128::from(*v),
IntValue::I32(v) => i128::from(*v),
IntValue::I64(v) => i128::from(*v),
IntValue::U8(v) => i128::from(*v),
IntValue::U16(v) => i128::from(*v),
IntValue::U32(v) => i128::from(*v),
IntValue::U64(v) => i128::from(*v),
}),
Value::Num(n) => {
if !n.is_finite() {
return Err(format_error(
"sprintf: numeric conversion requires finite input",
));
}
Ok(n.trunc().clamp(i128::MIN as f64, i128::MAX as f64) as i128)
}
Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
other => Err(format_error(format!(
"sprintf: expected numeric argument, got {other:?}"
))),
}
}
fn value_to_u128(value: &Value) -> BuiltinResult<u128> {
match value {
Value::Int(i) => match i {
IntValue::I8(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
IntValue::I16(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
IntValue::I32(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
IntValue::I64(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
IntValue::I8(v) => Ok((*v) as u128),
IntValue::I16(v) => Ok((*v) as u128),
IntValue::I32(v) => Ok((*v) as u128),
IntValue::I64(v) => Ok((*v) as u128),
IntValue::U8(v) => Ok((*v) as u128),
IntValue::U16(v) => Ok((*v) as u128),
IntValue::U32(v) => Ok((*v) as u128),
IntValue::U64(v) => Ok((*v) as u128),
},
Value::Num(n) => {
if !n.is_finite() {
return Err(format_error(
"sprintf: numeric conversion requires finite input",
));
}
if *n < 0.0 {
return Err(format_error("sprintf: expected non-negative value"));
}
Ok(n.trunc().clamp(0.0, u128::MAX as f64) as u128)
}
Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
other => Err(format_error(format!(
"sprintf: expected non-negative numeric value, got {other:?}"
))),
}
}
fn value_to_f64(value: &Value) -> BuiltinResult<f64> {
match value {
Value::Num(n) => Ok(*n),
Value::Int(i) => Ok(i.to_f64()),
Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
other => Err(format_error(format!(
"sprintf: expected numeric value, got {other:?}"
))),
}
}
fn value_to_string(value: &Value) -> BuiltinResult<String> {
match value {
Value::String(s) => Ok(s.clone()),
Value::CharArray(ca) => {
let mut s = String::with_capacity(ca.data.len());
for ch in &ca.data {
s.push(*ch);
}
Ok(s)
}
Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
Value::Num(n) => Ok(Value::Num(*n).to_string()),
Value::Int(i) => Ok(i.to_i64().to_string()),
Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
Value::Complex(re, im) => Ok(Value::Complex(*re, *im).to_string()),
other => Err(format_error(format!(
"sprintf: expected text or scalar value for %s conversion, got {other:?}"
))),
}
}
fn value_to_char(value: &Value) -> BuiltinResult<char> {
match value {
Value::String(s) => s.chars().next().ok_or_else(|| {
format_error("sprintf: %c conversion requires non-empty character input")
}),
Value::CharArray(ca) => ca
.data
.first()
.copied()
.ok_or_else(|| format_error("sprintf: %c conversion requires non-empty char input")),
Value::Num(n) => {
if !n.is_finite() {
return Err(format_error(
"sprintf: %c conversion needs finite numeric value",
));
}
let code = n.trunc() as u32;
std::char::from_u32(code)
.ok_or_else(|| format_error("sprintf: numeric value outside valid character range"))
}
Value::Int(i) => {
let code = i.to_i64();
if code < 0 {
return Err(format_error("sprintf: negative value for %c conversion"));
}
std::char::from_u32(code as u32)
.ok_or_else(|| format_error("sprintf: numeric value outside valid character range"))
}
other => Err(format_error(format!(
"sprintf: %c conversion expects character data, got {other:?}"
))),
}
}
fn to_base_string(mut value: u128, base: u32, uppercase: bool) -> String {
if value == 0 {
return "0".to_string();
}
let mut buf = Vec::new();
while value > 0 {
let digit = (value % base as u128) as u8;
let ch = match digit {
0..=9 => b'0' + digit,
_ => {
if uppercase {
b'A' + (digit - 10)
} else {
b'a' + (digit - 10)
}
}
};
buf.push(ch as char);
value /= base as u128;
}
buf.iter().rev().collect()
}
fn group_decimal_digits(digits: &str) -> String {
if digits.len() <= 3 {
return digits.to_string();
}
let chars: Vec<char> = digits.chars().collect();
let mut out = String::with_capacity(digits.len() + (digits.len() - 1) / 3);
for (idx, ch) in chars.iter().enumerate() {
if idx > 0 && (chars.len() - idx).is_multiple_of(3) {
out.push(',');
}
out.push(*ch);
}
out
}
fn group_float_mantissa(text: &str) -> String {
let (mantissa, exponent) = match text.find(['e', 'E']) {
Some(idx) => (&text[..idx], &text[idx..]),
None => (text, ""),
};
let mut parts = mantissa.splitn(2, '.');
let int_part = parts.next().unwrap_or_default();
let frac_part = parts.next();
let grouped_int = group_decimal_digits(int_part);
let mut out = grouped_int;
if let Some(frac) = frac_part {
out.push('.');
out.push_str(frac);
}
out.push_str(exponent);
out
}
pub fn extract_format_string(value: &Value, context: &str) -> BuiltinResult<String> {
match value {
Value::String(s) => Ok(s.clone()),
Value::CharArray(ca) => {
if ca.rows != 1 {
return Err(format_error(format!(
"{context}: formatSpec must be a character row vector or string scalar"
)));
}
Ok(ca.data.iter().collect())
}
Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
_ => Err(format_error(format!(
"{context}: formatSpec must be a character row vector or string scalar"
))),
}
}
pub fn decode_escape_sequences(context: &str, input: &str) -> BuiltinResult<String> {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '\\' {
result.push(ch);
continue;
}
let Some(next) = chars.next() else {
result.push('\\');
break;
};
match next {
'\\' => result.push('\\'),
'a' => result.push('\u{0007}'),
'b' => result.push('\u{0008}'),
'f' => result.push('\u{000C}'),
'n' => result.push('\n'),
'r' => result.push('\r'),
't' => result.push('\t'),
'v' => result.push('\u{000B}'),
'x' => {
let mut hex = String::new();
for _ in 0..2 {
match chars.peek().copied() {
Some(c) if c.is_ascii_hexdigit() => {
hex.push(chars.next().unwrap());
}
_ => break,
}
}
if hex.is_empty() {
result.push('\\');
result.push('x');
} else {
let value = u32::from_str_radix(&hex, 16).map_err(|_| {
format_error(format!("{context}: invalid hexadecimal escape \\x{hex}"))
})?;
if let Some(chr) = char::from_u32(value) {
result.push(chr);
} else {
return Err(format_error(format!(
"{context}: \\x{hex} escape outside valid Unicode range"
)));
}
}
}
'0'..='7' => {
let mut oct = String::new();
oct.push(next);
for _ in 0..2 {
match chars.peek().copied() {
Some(c) if ('0'..='7').contains(&c) => {
oct.push(chars.next().unwrap());
}
_ => break,
}
}
let value = u32::from_str_radix(&oct, 8).map_err(|_| {
format_error(format!("{context}: invalid octal escape \\{oct}"))
})?;
if let Some(chr) = char::from_u32(value) {
result.push(chr);
} else {
return Err(format_error(format!(
"{context}: \\{oct} escape outside valid Unicode range"
)));
}
}
other => {
result.push('\\');
result.push(other);
}
}
}
Ok(result)
}
pub async fn flatten_arguments(args: &[Value], context: &str) -> BuiltinResult<Vec<Value>> {
let mut flattened = Vec::new();
for value in args {
let gathered = gather_if_needed_async(value)
.await
.map_err(|flow| map_control_flow_with_context(flow, context))?;
flatten_value(gathered, &mut flattened, context).await?;
}
Ok(flattened)
}
#[async_recursion::async_recursion(?Send)]
async fn flatten_value(value: Value, output: &mut Vec<Value>, context: &str) -> BuiltinResult<()> {
match value {
Value::Num(_)
| Value::Int(_)
| Value::Bool(_)
| Value::String(_)
| Value::Complex(_, _) => {
output.push(value);
}
Value::Tensor(tensor) => {
for &elem in &tensor.data {
output.push(Value::Num(elem));
}
}
Value::ComplexTensor(tensor) => {
for &(re, im) in &tensor.data {
output.push(Value::Complex(re, im));
}
}
Value::LogicalArray(LogicalArray { data, .. }) => {
for byte in data {
output.push(Value::Bool(byte != 0));
}
}
Value::StringArray(StringArray { data, .. }) => {
for s in data {
output.push(Value::String(s));
}
}
Value::CharArray(ca) => {
if ca.rows == 1 {
output.push(Value::String(ca.data.iter().collect()));
} else {
for row in 0..ca.rows {
let mut line = String::with_capacity(ca.cols);
for col in 0..ca.cols {
line.push(ca.data[row * ca.cols + col]);
}
output.push(Value::String(line));
}
}
}
Value::Cell(cell) => {
for col in 0..cell.cols {
for row in 0..cell.rows {
let idx = row * cell.cols + col;
let inner = (*cell.data[idx]).clone();
let gathered = gather_if_needed_async(&inner)
.await
.map_err(|flow| map_control_flow_with_context(flow, context))?;
flatten_value(gathered, output, context).await?;
}
}
}
Value::GpuTensor(handle) => {
let gathered = gather_if_needed_async(&Value::GpuTensor(handle))
.await
.map_err(|flow| map_control_flow_with_context(flow, context))?;
flatten_value(gathered, output, context).await?;
}
Value::OutputList(values) => {
for value in values {
flatten_value(value, output, context).await?;
}
}
Value::MException(_)
| Value::HandleObject(_)
| Value::Listener(_)
| Value::Object(_)
| Value::Struct(_)
| Value::FunctionHandle(_)
| Value::Closure(_)
| Value::ClassRef(_) => {
return Err(format_error(format!(
"{context}: unsupported argument type"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_variadic_supports_thousands_grouping_flag() {
let out = format_variadic("%'d %'.2f", &[Value::Num(1234567.0), Value::Num(12345.5)])
.expect("grouped formatting should succeed");
assert_eq!(out, "1,234,567 12,345.50");
}
#[test]
fn format_variadic_consumes_i_flag_without_error() {
let out = format_variadic("%Id", &[Value::Int(IntValue::I32(42))])
.expect("I flag should be accepted as compatibility no-op");
assert_eq!(out, "42");
}
}