Skip to main content

runmat_runtime/builtins/common/
format.rs

1//! Shared formatting helpers for string-producing builtins.
2
3use std::char;
4use std::iter::Peekable;
5use std::str::Chars;
6
7use runmat_value::{IntValue, IntegerStorage, LogicalArray, StringArray, Value};
8
9use crate::builtins::common::tensor;
10use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
11
12const FORMAT_UNSUPPORTED_SPECIFIER_IDENTIFIER: &str = "RunMat:format:UnsupportedSpecifier";
13
14/// Stateful cursor over formatting arguments.
15#[derive(Debug)]
16pub struct ArgCursor<'a> {
17    args: &'a [Value],
18    index: usize,
19}
20
21impl<'a> ArgCursor<'a> {
22    pub fn new(args: &'a [Value]) -> Self {
23        Self { args, index: 0 }
24    }
25
26    pub fn remaining(&self) -> usize {
27        self.args.len().saturating_sub(self.index)
28    }
29
30    pub fn index(&self) -> usize {
31        self.index
32    }
33
34    fn next(&mut self) -> BuiltinResult<Value> {
35        if self.index >= self.args.len() {
36            return Err(format_error(
37                "sprintf: not enough input arguments for format specifier",
38            ));
39        }
40        let value = self.args[self.index].clone();
41        self.index += 1;
42        Ok(value)
43    }
44}
45
46fn format_error(message: impl Into<String>) -> RuntimeError {
47    build_runtime_error(message).build()
48}
49
50fn format_error_with_identifier(
51    message: impl Into<String>,
52    identifier: &'static str,
53) -> RuntimeError {
54    build_runtime_error(message)
55        .with_identifier(identifier)
56        .build()
57}
58
59fn map_control_flow_with_context(err: RuntimeError, context: &str) -> RuntimeError {
60    crate::builtins::common::map_control_flow_with_builtin(err, context)
61}
62
63/// Result of formatting a string with the current cursor state.
64#[derive(Debug, Default, Clone)]
65pub struct FormatStepResult {
66    pub output: String,
67    pub consumed: usize,
68}
69
70#[derive(Clone, Copy, Default)]
71struct FormatFlags {
72    alternate: bool,
73    zero_pad: bool,
74    left_align: bool,
75    sign_plus: bool,
76    sign_space: bool,
77    grouping: bool,
78}
79
80#[derive(Clone, Copy)]
81enum Count {
82    Value(isize),
83    FromArgument,
84}
85
86#[derive(Clone, Copy)]
87struct FormatSpec {
88    flags: FormatFlags,
89    width: Option<Count>,
90    precision: Option<Count>,
91    conversion: char,
92}
93
94/// Format a MATLAB-style format string with the provided arguments.
95///
96/// This function consumes arguments in column-major order, supports field widths,
97/// precision (including the `*` form), and honours the usual printf flags for the
98/// subset required by MATLAB builtins. It is intentionally strict: errors are
99/// reported when format specifiers cannot be satisfied by the provided arguments.
100pub fn format_variadic(fmt: &str, args: &[Value]) -> BuiltinResult<String> {
101    let mut cursor = ArgCursor::new(args);
102    let step = format_variadic_with_cursor(fmt, &mut cursor)?;
103    Ok(step.output)
104}
105
106/// Format a string using the supplied cursor, returning the formatted text along
107/// with the number of arguments consumed during this pass.
108pub fn format_variadic_with_cursor(
109    fmt: &str,
110    cursor: &mut ArgCursor<'_>,
111) -> BuiltinResult<FormatStepResult> {
112    format_once(fmt, cursor)
113}
114
115fn format_once(fmt: &str, cursor: &mut ArgCursor<'_>) -> BuiltinResult<FormatStepResult> {
116    let mut chars = fmt.chars().peekable();
117    let mut out = String::with_capacity(fmt.len());
118    let mut consumed = 0usize;
119
120    while let Some(ch) = chars.next() {
121        if ch != '%' {
122            out.push(ch);
123            continue;
124        }
125
126        if let Some('%') = chars.peek() {
127            chars.next();
128            out.push('%');
129            continue;
130        }
131
132        let spec = parse_format_spec(&mut chars)?;
133        let (formatted, used) = apply_format_spec(spec, cursor)?;
134        consumed += used;
135        out.push_str(&formatted);
136    }
137
138    Ok(FormatStepResult {
139        output: out,
140        consumed,
141    })
142}
143
144fn parse_format_spec(chars: &mut Peekable<Chars<'_>>) -> BuiltinResult<FormatSpec> {
145    let mut flags = FormatFlags::default();
146    loop {
147        match chars.peek().copied() {
148            Some('#') => {
149                flags.alternate = true;
150                chars.next();
151            }
152            Some('0') => {
153                flags.zero_pad = true;
154                chars.next();
155            }
156            Some('-') => {
157                flags.left_align = true;
158                chars.next();
159            }
160            Some(' ') => {
161                flags.sign_space = true;
162                chars.next();
163            }
164            Some('+') => {
165                flags.sign_plus = true;
166                chars.next();
167            }
168            Some('\'') => {
169                flags.grouping = true;
170                chars.next();
171            }
172            Some('I') => {
173                // Locale-specific alternative digits are not implemented yet.
174                // Consume the flag to keep format parsing compatible.
175                chars.next();
176            }
177            _ => break,
178        }
179    }
180
181    let width = if let Some('*') = chars.peek() {
182        chars.next();
183        Some(Count::FromArgument)
184    } else {
185        parse_number(chars).map(Count::Value)
186    };
187
188    let precision = if let Some('.') = chars.peek() {
189        chars.next();
190        if let Some('*') = chars.peek() {
191            chars.next();
192            Some(Count::FromArgument)
193        } else {
194            Some(Count::Value(parse_number(chars).unwrap_or(0)))
195        }
196    } else {
197        None
198    };
199
200    // Length modifiers are ignored, but we must consume them to remain compatible.
201    if let Some(&('h' | 'l' | 'L' | 'z' | 'j' | 't')) = chars.peek() {
202        let current = chars.next().unwrap();
203        if matches!(current, 'h' | 'l') && chars.peek() == Some(&current) {
204            chars.next();
205        }
206    }
207
208    let conversion = chars
209        .next()
210        .ok_or_else(|| format_error("sprintf: incomplete format specifier"))?;
211
212    Ok(FormatSpec {
213        flags,
214        width,
215        precision,
216        conversion,
217    })
218}
219
220fn parse_number(chars: &mut Peekable<Chars<'_>>) -> Option<isize> {
221    let mut value: i128 = 0;
222    let mut seen = false;
223    while let Some(&ch) = chars.peek() {
224        if !ch.is_ascii_digit() {
225            break;
226        }
227        seen = true;
228        value = value * 10 + i128::from((ch as u8 - b'0') as i16);
229        chars.next();
230    }
231    if seen {
232        let capped = value
233            .clamp(isize::MIN as i128, isize::MAX as i128)
234            .try_into()
235            .unwrap_or(isize::MAX);
236        Some(capped)
237    } else {
238        None
239    }
240}
241
242fn apply_format_spec(
243    spec: FormatSpec,
244    cursor: &mut ArgCursor<'_>,
245) -> BuiltinResult<(String, usize)> {
246    let mut consumed = 0usize;
247    let mut flags = spec.flags;
248
249    let mut width = match spec.width {
250        Some(Count::Value(w)) => Some(w),
251        Some(Count::FromArgument) => {
252            let value = cursor.next()?;
253            consumed += 1;
254            let w = value_to_isize(&value)?;
255            Some(w)
256        }
257        None => None,
258    };
259
260    let precision = match spec.precision {
261        Some(Count::Value(p)) => Some(p),
262        Some(Count::FromArgument) => {
263            let value = cursor.next()?;
264            consumed += 1;
265            let p = value_to_isize(&value)?;
266            if p < 0 {
267                None
268            } else {
269                Some(p)
270            }
271        }
272        None => None,
273    };
274
275    if let Some(w) = width {
276        if w < 0 {
277            flags.left_align = true;
278            width = Some(-w);
279        }
280    }
281
282    let conversion = spec.conversion;
283    let formatted = match conversion {
284        'd' | 'i' => {
285            let value = cursor.next()?;
286            consumed += 1;
287            let int_value = value_to_i128(&value)?;
288            format_integer(
289                int_value,
290                int_value.is_negative(),
291                10,
292                flags,
293                width,
294                precision,
295                false,
296                false,
297            )
298        }
299        'u' => {
300            let value = cursor.next()?;
301            consumed += 1;
302            let uint_value = value_to_u128(&value)?;
303            format_unsigned(uint_value, 10, flags, width, precision, false, false)
304        }
305        'o' => {
306            let value = cursor.next()?;
307            consumed += 1;
308            let uint_value = value_to_u128(&value)?;
309            format_unsigned(
310                uint_value,
311                8,
312                flags,
313                width,
314                precision,
315                spec.flags.alternate,
316                false,
317            )
318        }
319        'x' => {
320            let value = cursor.next()?;
321            consumed += 1;
322            let uint_value = value_to_u128(&value)?;
323            format_unsigned(
324                uint_value,
325                16,
326                flags,
327                width,
328                precision,
329                spec.flags.alternate,
330                false,
331            )
332        }
333        'X' => {
334            let value = cursor.next()?;
335            consumed += 1;
336            let uint_value = value_to_u128(&value)?;
337            format_unsigned(
338                uint_value,
339                16,
340                flags,
341                width,
342                precision,
343                spec.flags.alternate,
344                true,
345            )
346        }
347        'b' => {
348            let value = cursor.next()?;
349            consumed += 1;
350            let uint_value = value_to_u128(&value)?;
351            format_unsigned(
352                uint_value,
353                2,
354                flags,
355                width,
356                precision,
357                spec.flags.alternate,
358                false,
359            )
360        }
361        'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
362            let value = cursor.next()?;
363            consumed += 1;
364            let float_value = value_to_f64(&value)?;
365            format_float(
366                float_value,
367                conversion,
368                flags,
369                width,
370                precision,
371                spec.flags.alternate,
372            )
373        }
374        's' => {
375            let value = cursor.next()?;
376            consumed += 1;
377            format_string(value, flags, width, precision)
378        }
379        'c' => {
380            let value = cursor.next()?;
381            consumed += 1;
382            format_char(value, flags, width)
383        }
384        other => {
385            return Err(format_error_with_identifier(
386                format!("sprintf: unsupported format %{other}"),
387                FORMAT_UNSUPPORTED_SPECIFIER_IDENTIFIER,
388            ));
389        }
390    }?;
391
392    Ok((formatted, consumed))
393}
394
395#[allow(clippy::too_many_arguments)]
396fn format_integer(
397    value: i128,
398    is_negative: bool,
399    base: u32,
400    mut flags: FormatFlags,
401    width: Option<isize>,
402    precision: Option<isize>,
403    alternate: bool,
404    uppercase: bool,
405) -> BuiltinResult<String> {
406    let mut sign = String::new();
407    let abs_val = value.unsigned_abs();
408
409    if is_negative {
410        sign.push('-');
411    } else if flags.sign_plus {
412        sign.push('+');
413    } else if flags.sign_space {
414        sign.push(' ');
415    }
416
417    if precision.is_some() {
418        flags.zero_pad = false;
419    }
420
421    let mut digits = to_base_string(abs_val, base, uppercase);
422    let precision_value = precision.unwrap_or(-1);
423    if precision_value == 0 && abs_val == 0 {
424        digits.clear();
425    }
426    if precision_value > 0 {
427        let required = precision_value as usize;
428        if digits.len() < required {
429            let mut buf = String::with_capacity(required);
430            for _ in 0..(required - digits.len()) {
431                buf.push('0');
432            }
433            buf.push_str(&digits);
434            digits = buf;
435        }
436    }
437
438    let mut prefix = String::new();
439    if alternate && abs_val != 0 {
440        match base {
441            8 => prefix.push('0'),
442            16 => {
443                prefix.push('0');
444                prefix.push(if uppercase { 'X' } else { 'x' });
445            }
446            2 => {
447                prefix.push('0');
448                prefix.push('b');
449            }
450            _ => {}
451        }
452    }
453
454    if flags.grouping && base == 10 {
455        digits = group_decimal_digits(&digits);
456    }
457
458    apply_width(sign, prefix, digits, flags, width, flags.zero_pad)
459}
460
461fn format_unsigned(
462    value: u128,
463    base: u32,
464    mut flags: FormatFlags,
465    width: Option<isize>,
466    precision: Option<isize>,
467    alternate: bool,
468    uppercase: bool,
469) -> BuiltinResult<String> {
470    if precision.is_some() {
471        flags.zero_pad = false;
472    }
473
474    let mut digits = to_base_string(value, base, uppercase);
475    let precision_value = precision.unwrap_or(-1);
476    if precision_value == 0 && value == 0 {
477        digits.clear();
478    }
479    if precision_value > 0 {
480        let required = precision_value as usize;
481        if digits.len() < required {
482            let mut buf = String::with_capacity(required);
483            for _ in 0..(required - digits.len()) {
484                buf.push('0');
485            }
486            buf.push_str(&digits);
487            digits = buf;
488        }
489    }
490
491    let mut prefix = String::new();
492    if alternate && value != 0 {
493        match base {
494            8 => prefix.push('0'),
495            16 => {
496                prefix.push_str(if uppercase { "0X" } else { "0x" });
497            }
498            2 => prefix.push_str("0b"),
499            _ => {}
500        }
501    }
502
503    if flags.grouping && base == 10 {
504        digits = group_decimal_digits(&digits);
505    }
506
507    apply_width(String::new(), prefix, digits, flags, width, flags.zero_pad)
508}
509
510fn format_float(
511    value: f64,
512    conversion: char,
513    flags: FormatFlags,
514    width: Option<isize>,
515    precision: Option<isize>,
516    alternate: bool,
517) -> BuiltinResult<String> {
518    let mut sign = String::new();
519    let mut magnitude = value;
520
521    if value.is_nan() {
522        return apply_width(
523            String::new(),
524            String::new(),
525            "NaN".to_string(),
526            flags,
527            width,
528            false,
529        );
530    }
531
532    if value.is_infinite() {
533        if value.is_sign_negative() {
534            sign.push('-');
535        } else if flags.sign_plus {
536            sign.push('+');
537        } else if flags.sign_space {
538            sign.push(' ');
539        }
540        let text = "Inf".to_string();
541        return apply_width(sign, String::new(), text, flags, width, false);
542    }
543
544    if value.is_sign_negative() || (value == 0.0 && (1.0 / value).is_sign_negative()) {
545        sign.push('-');
546        magnitude = -value;
547    } else if flags.sign_plus {
548        sign.push('+');
549    } else if flags.sign_space {
550        sign.push(' ');
551    }
552
553    let prec = precision.unwrap_or(6).max(0) as usize;
554    let mut body = match conversion {
555        'f' | 'F' => format!("{magnitude:.prec$}"),
556        'e' => format!("{magnitude:.prec$e}"),
557        'E' => format!("{magnitude:.prec$E}"),
558        'g' | 'G' => format_float_general(magnitude, prec, conversion.is_uppercase()),
559        _ => {
560            return Err(format_error(format!(
561                "sprintf: unsupported float conversion %{}",
562                conversion
563            )))
564        }
565    };
566
567    if alternate && !body.contains('.') && matches!(conversion, 'f' | 'F' | 'g' | 'G') {
568        body.push('.');
569    }
570
571    if flags.grouping && matches!(conversion, 'f' | 'F' | 'g' | 'G') {
572        body = group_float_mantissa(&body);
573    }
574
575    let zero_pad_allowed = flags.zero_pad && !flags.left_align;
576    apply_width(sign, String::new(), body, flags, width, zero_pad_allowed)
577}
578
579fn format_float_general(value: f64, precision: usize, uppercase: bool) -> String {
580    if value == 0.0 {
581        if precision == 0 {
582            return "0".to_string();
583        }
584        let mut zero = String::from("0");
585        if precision > 0 {
586            zero.push('.');
587            zero.push_str(&"0".repeat(precision.saturating_sub(1)));
588        }
589        return zero;
590    }
591
592    let mut prec = precision;
593    if prec == 0 {
594        prec = 1;
595    }
596
597    let abs_val = value.abs();
598    let exp = abs_val.log10().floor() as i32;
599    let use_exp = exp < -4 || exp >= prec as i32;
600
601    if use_exp {
602        let mut s = format!("{:.*e}", prec - 1, value);
603        if uppercase {
604            s = s.to_uppercase();
605        }
606        trim_trailing_zeros(&mut s, true);
607        s
608    } else {
609        let mut s = format!("{:.*}", prec.max(1) - 1, value);
610        trim_trailing_zeros(&mut s, false);
611        s
612    }
613}
614
615fn trim_trailing_zeros(text: &mut String, keep_exponent: bool) {
616    if let Some(dot_idx) = text.find('.') {
617        let mut end = text.len();
618        while end > dot_idx + 1 {
619            let byte = text.as_bytes()[end - 1];
620            if byte == b'0' {
621                end -= 1;
622            } else {
623                break;
624            }
625        }
626        if end > dot_idx + 1 && text.as_bytes()[end - 1] == b'.' {
627            end -= 1;
628        }
629        if keep_exponent {
630            if let Some(exp_idx) = text.find(['e', 'E']) {
631                let exponent = text[exp_idx..].to_string();
632                text.truncate(end.min(exp_idx));
633                text.push_str(&exponent);
634                return;
635            }
636        }
637        text.truncate(end);
638    }
639}
640
641fn format_string(
642    value: Value,
643    flags: FormatFlags,
644    width: Option<isize>,
645    precision: Option<isize>,
646) -> BuiltinResult<String> {
647    let mut text = value_to_string(&value)?;
648    if let Some(p) = precision {
649        if p >= 0 {
650            let mut chars = text.chars();
651            let mut truncated = String::with_capacity(text.len());
652            for _ in 0..(p as usize) {
653                if let Some(ch) = chars.next() {
654                    truncated.push(ch);
655                } else {
656                    break;
657                }
658            }
659            text = truncated;
660        }
661    }
662
663    apply_width(String::new(), String::new(), text, flags, width, false)
664}
665
666fn format_char(value: Value, flags: FormatFlags, width: Option<isize>) -> BuiltinResult<String> {
667    let ch = value_to_char(&value)?;
668    let text = ch.to_string();
669    apply_width(String::new(), String::new(), text, flags, width, false)
670}
671
672fn apply_width(
673    sign: String,
674    prefix: String,
675    digits: String,
676    flags: FormatFlags,
677    width: Option<isize>,
678    zero_pad: bool,
679) -> BuiltinResult<String> {
680    let mut result = String::new();
681    let sign_prefix_len = sign.len() + prefix.len();
682    let total_len = sign_prefix_len + digits.len();
683    let target_width = width.unwrap_or(0).max(0) as usize;
684
685    if target_width <= total_len {
686        result.push_str(&sign);
687        result.push_str(&prefix);
688        result.push_str(&digits);
689        return Ok(result);
690    }
691
692    let pad_len = target_width - total_len;
693    if flags.left_align {
694        result.push_str(&sign);
695        result.push_str(&prefix);
696        result.push_str(&digits);
697        for _ in 0..pad_len {
698            result.push(' ');
699        }
700        return Ok(result);
701    }
702
703    if zero_pad {
704        result.push_str(&sign);
705        result.push_str(&prefix);
706        for _ in 0..pad_len {
707            result.push('0');
708        }
709        result.push_str(&digits);
710    } else {
711        for _ in 0..pad_len {
712            result.push(' ');
713        }
714        result.push_str(&sign);
715        result.push_str(&prefix);
716        result.push_str(&digits);
717    }
718    Ok(result)
719}
720
721fn value_to_isize(value: &Value) -> BuiltinResult<isize> {
722    match value {
723        Value::Int(i) => Ok(i.to_i64().clamp(isize::MIN as i64, isize::MAX as i64) as isize),
724        Value::Num(n) => {
725            if !n.is_finite() {
726                return Err(format_error(
727                    "sprintf: width/precision specifier must be finite",
728                ));
729            }
730            Ok(n.trunc().clamp(isize::MIN as f64, isize::MAX as f64) as isize)
731        }
732        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
733        other => Err(format_error(format!(
734            "sprintf: width/precision specifier expects numeric value, got {other:?}"
735        ))),
736    }
737}
738
739fn value_to_i128(value: &Value) -> BuiltinResult<i128> {
740    match value {
741        Value::Int(i) => Ok(match i {
742            IntValue::I8(v) => i128::from(*v),
743            IntValue::I16(v) => i128::from(*v),
744            IntValue::I32(v) => i128::from(*v),
745            IntValue::I64(v) => i128::from(*v),
746            IntValue::U8(v) => i128::from(*v),
747            IntValue::U16(v) => i128::from(*v),
748            IntValue::U32(v) => i128::from(*v),
749            IntValue::U64(v) => i128::from(*v),
750        }),
751        Value::Num(n) => {
752            if !n.is_finite() {
753                return Err(format_error(
754                    "sprintf: numeric conversion requires finite input",
755                ));
756            }
757            Ok(n.trunc().clamp(i128::MIN as f64, i128::MAX as f64) as i128)
758        }
759        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
760        other => Err(format_error(format!(
761            "sprintf: expected numeric argument, got {other:?}"
762        ))),
763    }
764}
765
766fn value_to_u128(value: &Value) -> BuiltinResult<u128> {
767    match value {
768        Value::Int(i) => match i {
769            IntValue::I8(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
770            IntValue::I16(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
771            IntValue::I32(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
772            IntValue::I64(v) if *v < 0 => Err(format_error("sprintf: expected non-negative value")),
773            IntValue::I8(v) => Ok((*v) as u128),
774            IntValue::I16(v) => Ok((*v) as u128),
775            IntValue::I32(v) => Ok((*v) as u128),
776            IntValue::I64(v) => Ok((*v) as u128),
777            IntValue::U8(v) => Ok((*v) as u128),
778            IntValue::U16(v) => Ok((*v) as u128),
779            IntValue::U32(v) => Ok((*v) as u128),
780            IntValue::U64(v) => Ok((*v) as u128),
781        },
782        Value::Num(n) => {
783            if !n.is_finite() {
784                return Err(format_error(
785                    "sprintf: numeric conversion requires finite input",
786                ));
787            }
788            if *n < 0.0 {
789                return Err(format_error("sprintf: expected non-negative value"));
790            }
791            Ok(n.trunc().clamp(0.0, u128::MAX as f64) as u128)
792        }
793        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
794        other => Err(format_error(format!(
795            "sprintf: expected non-negative numeric value, got {other:?}"
796        ))),
797    }
798}
799
800fn value_to_f64(value: &Value) -> BuiltinResult<f64> {
801    match value {
802        Value::Num(n) => Ok(*n),
803        Value::Int(i) => Ok(i.to_f64()),
804        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
805        other => Err(format_error(format!(
806            "sprintf: expected numeric value, got {other:?}"
807        ))),
808    }
809}
810
811pub(crate) fn number_to_string(value: f64) -> String {
812    if value.is_nan() {
813        return "NaN".to_string();
814    }
815    if value.is_infinite() {
816        return if value.is_sign_negative() {
817            "-Inf".to_string()
818        } else {
819            "Inf".to_string()
820        };
821    }
822    if value == 0.0 {
823        return "0".to_string();
824    }
825    value.to_string()
826}
827
828pub(crate) fn complex_to_string(re: f64, im: f64) -> String {
829    if im == 0.0 {
830        number_to_string(re)
831    } else if re == 0.0 {
832        format!("{}i", number_to_string(im))
833    } else if im < 0.0 {
834        format!("{}-{}i", number_to_string(re), number_to_string(im.abs()))
835    } else {
836        format!("{}+{}i", number_to_string(re), number_to_string(im))
837    }
838}
839
840fn value_to_string(value: &Value) -> BuiltinResult<String> {
841    match value {
842        Value::String(s) => Ok(s.clone()),
843        Value::CharArray(ca) => {
844            let mut s = String::with_capacity(ca.data.len());
845            for ch in &ca.data {
846                s.push(*ch);
847            }
848            Ok(s)
849        }
850        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
851        Value::Num(n) => Ok(number_to_string(*n)),
852        Value::Int(i) => Ok(int_value_to_decimal(i)),
853        Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
854        Value::Complex(re, im) => Ok(complex_to_string(*re, *im)),
855        other => Err(format_error(format!(
856            "sprintf: expected text or scalar value for %s conversion, got {other:?}"
857        ))),
858    }
859}
860
861fn value_to_char(value: &Value) -> BuiltinResult<char> {
862    match value {
863        Value::String(s) => s.chars().next().ok_or_else(|| {
864            format_error("sprintf: %c conversion requires non-empty character input")
865        }),
866        Value::CharArray(ca) => ca
867            .data
868            .first()
869            .copied()
870            .ok_or_else(|| format_error("sprintf: %c conversion requires non-empty char input")),
871        Value::Num(n) => {
872            if !n.is_finite() {
873                return Err(format_error(
874                    "sprintf: %c conversion needs finite numeric value",
875                ));
876            }
877            let code = n.trunc() as u32;
878            std::char::from_u32(code)
879                .ok_or_else(|| format_error("sprintf: numeric value outside valid character range"))
880        }
881        Value::Int(i) => {
882            let code = i.to_i64();
883            if code < 0 {
884                return Err(format_error("sprintf: negative value for %c conversion"));
885            }
886            std::char::from_u32(code as u32)
887                .ok_or_else(|| format_error("sprintf: numeric value outside valid character range"))
888        }
889        other => Err(format_error(format!(
890            "sprintf: %c conversion expects character data, got {other:?}"
891        ))),
892    }
893}
894
895fn to_base_string(mut value: u128, base: u32, uppercase: bool) -> String {
896    if value == 0 {
897        return "0".to_string();
898    }
899    let mut buf = Vec::new();
900    while value > 0 {
901        let digit = (value % base as u128) as u8;
902        let ch = match digit {
903            0..=9 => b'0' + digit,
904            _ => {
905                if uppercase {
906                    b'A' + (digit - 10)
907                } else {
908                    b'a' + (digit - 10)
909                }
910            }
911        };
912        buf.push(ch as char);
913        value /= base as u128;
914    }
915    buf.iter().rev().collect()
916}
917
918fn group_decimal_digits(digits: &str) -> String {
919    if digits.len() <= 3 {
920        return digits.to_string();
921    }
922    let chars: Vec<char> = digits.chars().collect();
923    let mut out = String::with_capacity(digits.len() + (digits.len() - 1) / 3);
924    for (idx, ch) in chars.iter().enumerate() {
925        if idx > 0 && (chars.len() - idx).is_multiple_of(3) {
926            out.push(',');
927        }
928        out.push(*ch);
929    }
930    out
931}
932
933fn group_float_mantissa(text: &str) -> String {
934    let (mantissa, exponent) = match text.find(['e', 'E']) {
935        Some(idx) => (&text[..idx], &text[idx..]),
936        None => (text, ""),
937    };
938
939    let mut parts = mantissa.splitn(2, '.');
940    let int_part = parts.next().unwrap_or_default();
941    let frac_part = parts.next();
942    let grouped_int = group_decimal_digits(int_part);
943
944    let mut out = grouped_int;
945    if let Some(frac) = frac_part {
946        out.push('.');
947        out.push_str(frac);
948    }
949    out.push_str(exponent);
950    out
951}
952
953/// Extract a printf-style format string from a MATLAB value, validating that it
954/// is a character row vector or string scalar.
955pub fn extract_format_string(value: &Value, context: &str) -> BuiltinResult<String> {
956    match value {
957        Value::String(s) => Ok(s.clone()),
958        Value::CharArray(ca) => {
959            if ca.rows != 1 {
960                return Err(format_error(format!(
961                    "{context}: formatSpec must be a character row vector or string scalar"
962                )));
963            }
964            Ok(ca.data.iter().collect())
965        }
966        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
967        _ => Err(format_error(format!(
968            "{context}: formatSpec must be a character row vector or string scalar"
969        ))),
970    }
971}
972
973/// Decode MATLAB-compatible escape sequences within a format specification.
974pub fn decode_escape_sequences(context: &str, input: &str) -> BuiltinResult<String> {
975    let mut result = String::with_capacity(input.len());
976    let mut chars = input.chars().peekable();
977    while let Some(ch) = chars.next() {
978        if ch != '\\' {
979            result.push(ch);
980            continue;
981        }
982        let Some(next) = chars.next() else {
983            result.push('\\');
984            break;
985        };
986        match next {
987            '\\' => result.push('\\'),
988            'a' => result.push('\u{0007}'),
989            'b' => result.push('\u{0008}'),
990            'f' => result.push('\u{000C}'),
991            'n' => result.push('\n'),
992            'r' => result.push('\r'),
993            't' => result.push('\t'),
994            'v' => result.push('\u{000B}'),
995            'x' => {
996                let mut hex = String::new();
997                for _ in 0..2 {
998                    match chars.peek().copied() {
999                        Some(c) if c.is_ascii_hexdigit() => {
1000                            hex.push(chars.next().unwrap());
1001                        }
1002                        _ => break,
1003                    }
1004                }
1005                if hex.is_empty() {
1006                    result.push('\\');
1007                    result.push('x');
1008                } else {
1009                    let value = u32::from_str_radix(&hex, 16).map_err(|_| {
1010                        format_error(format!("{context}: invalid hexadecimal escape \\x{hex}"))
1011                    })?;
1012                    if let Some(chr) = char::from_u32(value) {
1013                        result.push(chr);
1014                    } else {
1015                        return Err(format_error(format!(
1016                            "{context}: \\x{hex} escape outside valid Unicode range"
1017                        )));
1018                    }
1019                }
1020            }
1021            '0'..='7' => {
1022                let mut oct = String::new();
1023                oct.push(next);
1024                for _ in 0..2 {
1025                    match chars.peek().copied() {
1026                        Some(c) if ('0'..='7').contains(&c) => {
1027                            oct.push(chars.next().unwrap());
1028                        }
1029                        _ => break,
1030                    }
1031                }
1032                let value = u32::from_str_radix(&oct, 8).map_err(|_| {
1033                    format_error(format!("{context}: invalid octal escape \\{oct}"))
1034                })?;
1035                if let Some(chr) = char::from_u32(value) {
1036                    result.push(chr);
1037                } else {
1038                    return Err(format_error(format!(
1039                        "{context}: \\{oct} escape outside valid Unicode range"
1040                    )));
1041                }
1042            }
1043            other => {
1044                result.push('\\');
1045                result.push(other);
1046            }
1047        }
1048    }
1049    Ok(result)
1050}
1051
1052/// Flatten MATLAB argument values into a linear vector suitable for repeated
1053/// printf-style formatting. Arrays are traversed in column-major order and GPU
1054/// tensors are gathered back to the host.
1055pub async fn flatten_arguments(args: &[Value], context: &str) -> BuiltinResult<Vec<Value>> {
1056    let mut flattened = Vec::new();
1057    for value in args {
1058        let gathered = gather_if_needed_async(value)
1059            .await
1060            .map_err(|flow| map_control_flow_with_context(flow, context))?;
1061        flatten_value(gathered, &mut flattened, context).await?;
1062    }
1063    Ok(flattened)
1064}
1065
1066#[async_recursion::async_recursion(?Send)]
1067async fn flatten_value(value: Value, output: &mut Vec<Value>, context: &str) -> BuiltinResult<()> {
1068    match value {
1069        Value::Num(_)
1070        | Value::Int(_)
1071        | Value::Bool(_)
1072        | Value::String(_)
1073        | Value::Complex(_, _)
1074        | Value::Symbolic(_) => {
1075            output.push(value);
1076        }
1077        Value::SymbolicArray(array) => {
1078            for expr in array.data {
1079                output.push(Value::Symbolic(expr));
1080            }
1081        }
1082        Value::Tensor(tensor) => {
1083            if let Some(storage) = tensor.integer_storage() {
1084                for index in 0..storage.len() {
1085                    output.push(Value::Int(integer_storage_value(storage, index)));
1086                }
1087            } else {
1088                let values = tensor::tensor_values_f64_cow(&tensor);
1089                for &elem in values.as_ref() {
1090                    output.push(Value::Num(elem));
1091                }
1092            }
1093        }
1094        Value::ComplexTensor(tensor) => {
1095            if tensor.integer_storage().is_some() {
1096                for index in 0..tensor::complex_tensor_element_len(&tensor) {
1097                    output.push(Value::String(tensor.format_element(index)));
1098                }
1099            } else {
1100                for &(re, im) in &tensor.materialize_f64() {
1101                    output.push(Value::Complex(re, im));
1102                }
1103            }
1104        }
1105        Value::LogicalArray(LogicalArray { data, .. }) => {
1106            for byte in data {
1107                output.push(Value::Bool(byte != 0));
1108            }
1109        }
1110        Value::StringArray(StringArray { data, .. }) => {
1111            for s in data {
1112                output.push(Value::String(s));
1113            }
1114        }
1115        Value::CharArray(ca) => {
1116            if ca.rows == 1 {
1117                output.push(Value::String(ca.data.iter().collect()));
1118            } else {
1119                for row in 0..ca.rows {
1120                    let mut line = String::with_capacity(ca.cols);
1121                    for col in 0..ca.cols {
1122                        line.push(ca.data[row * ca.cols + col]);
1123                    }
1124                    output.push(Value::String(line));
1125                }
1126            }
1127        }
1128        Value::Cell(cell) => {
1129            for col in 0..cell.cols {
1130                for row in 0..cell.rows {
1131                    let idx = row * cell.cols + col;
1132                    let inner = cell.data[idx].clone();
1133                    let gathered = gather_if_needed_async(&inner)
1134                        .await
1135                        .map_err(|flow| map_control_flow_with_context(flow, context))?;
1136                    flatten_value(gathered, output, context).await?;
1137                }
1138            }
1139        }
1140        Value::GpuTensor(handle) => {
1141            let gathered = gather_if_needed_async(&Value::GpuTensor(handle))
1142                .await
1143                .map_err(|flow| map_control_flow_with_context(flow, context))?;
1144            flatten_value(gathered, output, context).await?;
1145        }
1146        Value::OutputList(values) => {
1147            for value in values {
1148                flatten_value(value, output, context).await?;
1149            }
1150        }
1151        Value::MException(_)
1152        | Value::HandleObject(_)
1153        | Value::Listener(_)
1154        | Value::ObjectArray(_)
1155        | Value::Object(_)
1156        | Value::SparseTensor(_)
1157        | Value::Struct(_)
1158        | Value::FunctionHandle(_)
1159        | Value::ExternalFunctionHandle(_)
1160        | Value::MethodFunctionHandle(_)
1161        | Value::BoundFunctionHandle { .. }
1162        | Value::Closure(_)
1163        | Value::ClassRef(_)
1164        | Value::Future(_)
1165        | Value::Task(_)
1166        | Value::Pool(_)
1167        | Value::Job(_)
1168        | Value::Foreign(_) => {
1169            return Err(format_error(format!(
1170                "{context}: unsupported argument type"
1171            )));
1172        }
1173    }
1174    Ok(())
1175}
1176
1177fn integer_storage_value(storage: &IntegerStorage, index: usize) -> IntValue {
1178    match storage {
1179        IntegerStorage::I8(values) => IntValue::I8(values[index]),
1180        IntegerStorage::I16(values) => IntValue::I16(values[index]),
1181        IntegerStorage::I32(values) => IntValue::I32(values[index]),
1182        IntegerStorage::I64(values) => IntValue::I64(values[index]),
1183        IntegerStorage::U8(values) => IntValue::U8(values[index]),
1184        IntegerStorage::U16(values) => IntValue::U16(values[index]),
1185        IntegerStorage::U32(values) => IntValue::U32(values[index]),
1186        IntegerStorage::U64(values) => IntValue::U64(values[index]),
1187    }
1188}
1189
1190fn int_value_to_decimal(value: &IntValue) -> String {
1191    match value {
1192        IntValue::I8(value) => value.to_string(),
1193        IntValue::I16(value) => value.to_string(),
1194        IntValue::I32(value) => value.to_string(),
1195        IntValue::I64(value) => value.to_string(),
1196        IntValue::U8(value) => value.to_string(),
1197        IntValue::U16(value) => value.to_string(),
1198        IntValue::U32(value) => value.to_string(),
1199        IntValue::U64(value) => value.to_string(),
1200    }
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205    use super::*;
1206    use runmat_value::{
1207        get_display_format, set_display_format, FormatMode, IntegerComplexStorage, NumericStorage,
1208        Tensor,
1209    };
1210
1211    #[test]
1212    fn format_variadic_supports_thousands_grouping_flag() {
1213        let out = format_variadic("%'d %'.2f", &[Value::Num(1234567.0), Value::Num(12345.5)])
1214            .expect("grouped formatting should succeed");
1215        assert_eq!(out, "1,234,567 12,345.50");
1216    }
1217
1218    #[test]
1219    fn format_variadic_consumes_i_flag_without_error() {
1220        let out = format_variadic("%Id", &[Value::Int(IntValue::I32(42))])
1221            .expect("I flag should be accepted as compatibility no-op");
1222        assert_eq!(out, "42");
1223    }
1224
1225    #[test]
1226    fn percent_s_numeric_and_complex_ignore_display_format() {
1227        let previous = get_display_format();
1228        set_display_format(FormatMode::Hex);
1229        let result = format_variadic(
1230            "%s %s",
1231            &[Value::Num(std::f64::consts::PI), Value::Complex(1.5, -2.0)],
1232        );
1233        set_display_format(previous);
1234
1235        let out = result.expect("%s formatting should succeed");
1236        assert_eq!(out, "3.141592653589793 1.5-2i");
1237    }
1238
1239    #[test]
1240    fn typed_integer_tensors_keep_exact_values_through_formatting() {
1241        let tensor = runmat_value::Tensor::new_integer(
1242            IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]),
1243            vec![1, 2],
1244        )
1245        .expect("integer tensor");
1246        let flattened =
1247            futures::executor::block_on(flatten_arguments(&[Value::Tensor(tensor)], "sprintf"))
1248                .expect("flattened arguments");
1249
1250        assert_eq!(
1251            format_variadic(
1252                "%u %x %s",
1253                &[
1254                    flattened[0].clone(),
1255                    flattened[0].clone(),
1256                    flattened[1].clone()
1257                ]
1258            )
1259            .expect("formatted integer values"),
1260            format!("{} ffffffffffffffff {}", u64::MAX, 1_u64 << 63)
1261        );
1262    }
1263
1264    #[test]
1265    fn native_single_tensors_flatten_from_authoritative_storage() {
1266        let tensor =
1267            Tensor::from_numeric_storage(NumericStorage::F32(vec![1.25, -2.5]), vec![1, 2])
1268                .expect("single tensor");
1269        let flattened =
1270            futures::executor::block_on(flatten_arguments(&[Value::Tensor(tensor)], "sprintf"))
1271                .expect("flattened arguments");
1272        assert_eq!(flattened, vec![Value::Num(1.25), Value::Num(-2.5)]);
1273    }
1274
1275    #[test]
1276    fn typed_complex_integer_tensors_keep_exact_values_through_string_formatting() {
1277        let storage = IntegerComplexStorage::new(
1278            IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]),
1279            IntegerStorage::U64(vec![7, 0]),
1280        )
1281        .expect("matching complex integer storage");
1282        let tensor = runmat_value::ComplexTensor::new_integer(storage, vec![1, 2])
1283            .expect("complex integer tensor");
1284        let flattened = futures::executor::block_on(flatten_arguments(
1285            &[Value::ComplexTensor(tensor)],
1286            "sprintf",
1287        ))
1288        .expect("flattened arguments");
1289
1290        assert_eq!(
1291            format_variadic("%s %s", &flattened).expect("formatted complex integer values"),
1292            format!("{}+7i {}", u64::MAX, 1_u64 << 63)
1293        );
1294    }
1295}