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_builtins::{IntValue, LogicalArray, StringArray, Value};
8
9use crate::gather_if_needed;
10
11/// Stateful cursor over formatting arguments.
12#[derive(Debug)]
13pub struct ArgCursor<'a> {
14    args: &'a [Value],
15    index: usize,
16}
17
18impl<'a> ArgCursor<'a> {
19    pub fn new(args: &'a [Value]) -> Self {
20        Self { args, index: 0 }
21    }
22
23    pub fn remaining(&self) -> usize {
24        self.args.len().saturating_sub(self.index)
25    }
26
27    pub fn index(&self) -> usize {
28        self.index
29    }
30
31    fn next(&mut self) -> Result<Value, String> {
32        if self.index >= self.args.len() {
33            return Err("sprintf: not enough input arguments for format specifier".to_string());
34        }
35        let value = self.args[self.index].clone();
36        self.index += 1;
37        Ok(value)
38    }
39}
40
41/// Result of formatting a string with the current cursor state.
42#[derive(Debug, Default, Clone)]
43pub struct FormatStepResult {
44    pub output: String,
45    pub consumed: usize,
46}
47
48#[derive(Clone, Copy, Default)]
49struct FormatFlags {
50    alternate: bool,
51    zero_pad: bool,
52    left_align: bool,
53    sign_plus: bool,
54    sign_space: bool,
55}
56
57#[derive(Clone, Copy)]
58enum Count {
59    Value(isize),
60    FromArgument,
61}
62
63#[derive(Clone, Copy)]
64struct FormatSpec {
65    flags: FormatFlags,
66    width: Option<Count>,
67    precision: Option<Count>,
68    conversion: char,
69}
70
71/// Format a MATLAB-style format string with the provided arguments.
72///
73/// This function consumes arguments in column-major order, supports field widths,
74/// precision (including the `*` form), and honours the usual printf flags for the
75/// subset required by MATLAB builtins. It is intentionally strict: errors are
76/// reported when format specifiers cannot be satisfied by the provided arguments.
77pub fn format_variadic(fmt: &str, args: &[Value]) -> Result<String, String> {
78    let mut cursor = ArgCursor::new(args);
79    let step = format_variadic_with_cursor(fmt, &mut cursor)?;
80    Ok(step.output)
81}
82
83/// Format a string using the supplied cursor, returning the formatted text along
84/// with the number of arguments consumed during this pass.
85pub fn format_variadic_with_cursor(
86    fmt: &str,
87    cursor: &mut ArgCursor<'_>,
88) -> Result<FormatStepResult, String> {
89    format_once(fmt, cursor)
90}
91
92fn format_once(fmt: &str, cursor: &mut ArgCursor<'_>) -> Result<FormatStepResult, String> {
93    let mut chars = fmt.chars().peekable();
94    let mut out = String::with_capacity(fmt.len());
95    let mut consumed = 0usize;
96
97    while let Some(ch) = chars.next() {
98        if ch != '%' {
99            out.push(ch);
100            continue;
101        }
102
103        if let Some('%') = chars.peek() {
104            chars.next();
105            out.push('%');
106            continue;
107        }
108
109        let spec = parse_format_spec(&mut chars)?;
110        let (formatted, used) = apply_format_spec(spec, cursor)?;
111        consumed += used;
112        out.push_str(&formatted);
113    }
114
115    Ok(FormatStepResult {
116        output: out,
117        consumed,
118    })
119}
120
121fn parse_format_spec(chars: &mut Peekable<Chars<'_>>) -> Result<FormatSpec, String> {
122    let mut flags = FormatFlags::default();
123    loop {
124        match chars.peek().copied() {
125            Some('#') => {
126                flags.alternate = true;
127                chars.next();
128            }
129            Some('0') => {
130                flags.zero_pad = true;
131                chars.next();
132            }
133            Some('-') => {
134                flags.left_align = true;
135                chars.next();
136            }
137            Some(' ') => {
138                flags.sign_space = true;
139                chars.next();
140            }
141            Some('+') => {
142                flags.sign_plus = true;
143                chars.next();
144            }
145            _ => break,
146        }
147    }
148
149    let width = if let Some('*') = chars.peek() {
150        chars.next();
151        Some(Count::FromArgument)
152    } else {
153        parse_number(chars).map(Count::Value)
154    };
155
156    let precision = if let Some('.') = chars.peek() {
157        chars.next();
158        if let Some('*') = chars.peek() {
159            chars.next();
160            Some(Count::FromArgument)
161        } else {
162            Some(Count::Value(parse_number(chars).unwrap_or(0)))
163        }
164    } else {
165        None
166    };
167
168    // Length modifiers are ignored, but we must consume them to remain compatible.
169    if let Some(&('h' | 'l' | 'L' | 'z' | 'j' | 't')) = chars.peek() {
170        let current = chars.next().unwrap();
171        if matches!(current, 'h' | 'l') && chars.peek() == Some(&current) {
172            chars.next();
173        }
174    }
175
176    let conversion = chars
177        .next()
178        .ok_or_else(|| "sprintf: incomplete format specifier".to_string())?;
179
180    Ok(FormatSpec {
181        flags,
182        width,
183        precision,
184        conversion,
185    })
186}
187
188fn parse_number(chars: &mut Peekable<Chars<'_>>) -> Option<isize> {
189    let mut value: i128 = 0;
190    let mut seen = false;
191    while let Some(&ch) = chars.peek() {
192        if !ch.is_ascii_digit() {
193            break;
194        }
195        seen = true;
196        value = value * 10 + i128::from((ch as u8 - b'0') as i16);
197        chars.next();
198    }
199    if seen {
200        let capped = value
201            .clamp(isize::MIN as i128, isize::MAX as i128)
202            .try_into()
203            .unwrap_or(isize::MAX);
204        Some(capped)
205    } else {
206        None
207    }
208}
209
210fn apply_format_spec(
211    spec: FormatSpec,
212    cursor: &mut ArgCursor<'_>,
213) -> Result<(String, usize), String> {
214    let mut consumed = 0usize;
215    let mut flags = spec.flags;
216
217    let mut width = match spec.width {
218        Some(Count::Value(w)) => Some(w),
219        Some(Count::FromArgument) => {
220            let value = cursor.next()?;
221            consumed += 1;
222            let w = value_to_isize(&value)?;
223            Some(w)
224        }
225        None => None,
226    };
227
228    let precision = match spec.precision {
229        Some(Count::Value(p)) => Some(p),
230        Some(Count::FromArgument) => {
231            let value = cursor.next()?;
232            consumed += 1;
233            let p = value_to_isize(&value)?;
234            if p < 0 {
235                None
236            } else {
237                Some(p)
238            }
239        }
240        None => None,
241    };
242
243    if let Some(w) = width {
244        if w < 0 {
245            flags.left_align = true;
246            width = Some(-w);
247        }
248    }
249
250    let conversion = spec.conversion;
251    let formatted = match conversion {
252        'd' | 'i' => {
253            let value = cursor.next()?;
254            consumed += 1;
255            let int_value = value_to_i128(&value)?;
256            format_integer(
257                int_value,
258                int_value.is_negative(),
259                10,
260                flags,
261                width,
262                precision,
263                false,
264                false,
265            )
266        }
267        'u' => {
268            let value = cursor.next()?;
269            consumed += 1;
270            let uint_value = value_to_u128(&value)?;
271            format_unsigned(uint_value, 10, flags, width, precision, false, false)
272        }
273        'o' => {
274            let value = cursor.next()?;
275            consumed += 1;
276            let uint_value = value_to_u128(&value)?;
277            format_unsigned(
278                uint_value,
279                8,
280                flags,
281                width,
282                precision,
283                spec.flags.alternate,
284                false,
285            )
286        }
287        'x' => {
288            let value = cursor.next()?;
289            consumed += 1;
290            let uint_value = value_to_u128(&value)?;
291            format_unsigned(
292                uint_value,
293                16,
294                flags,
295                width,
296                precision,
297                spec.flags.alternate,
298                false,
299            )
300        }
301        'X' => {
302            let value = cursor.next()?;
303            consumed += 1;
304            let uint_value = value_to_u128(&value)?;
305            format_unsigned(
306                uint_value,
307                16,
308                flags,
309                width,
310                precision,
311                spec.flags.alternate,
312                true,
313            )
314        }
315        'b' => {
316            let value = cursor.next()?;
317            consumed += 1;
318            let uint_value = value_to_u128(&value)?;
319            format_unsigned(
320                uint_value,
321                2,
322                flags,
323                width,
324                precision,
325                spec.flags.alternate,
326                false,
327            )
328        }
329        'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
330            let value = cursor.next()?;
331            consumed += 1;
332            let float_value = value_to_f64(&value)?;
333            format_float(
334                float_value,
335                conversion,
336                flags,
337                width,
338                precision,
339                spec.flags.alternate,
340            )
341        }
342        's' => {
343            let value = cursor.next()?;
344            consumed += 1;
345            format_string(value, flags, width, precision)
346        }
347        'c' => {
348            let value = cursor.next()?;
349            consumed += 1;
350            format_char(value, flags, width)
351        }
352        other => {
353            return Err(format!("sprintf: unsupported format %{other}"));
354        }
355    }?;
356
357    Ok((formatted, consumed))
358}
359
360#[allow(clippy::too_many_arguments)]
361fn format_integer(
362    value: i128,
363    is_negative: bool,
364    base: u32,
365    mut flags: FormatFlags,
366    width: Option<isize>,
367    precision: Option<isize>,
368    alternate: bool,
369    uppercase: bool,
370) -> Result<String, String> {
371    let mut sign = String::new();
372    let abs_val = value.unsigned_abs();
373
374    if is_negative {
375        sign.push('-');
376    } else if flags.sign_plus {
377        sign.push('+');
378    } else if flags.sign_space {
379        sign.push(' ');
380    }
381
382    if precision.is_some() {
383        flags.zero_pad = false;
384    }
385
386    let mut digits = to_base_string(abs_val, base, uppercase);
387    let precision_value = precision.unwrap_or(-1);
388    if precision_value == 0 && abs_val == 0 {
389        digits.clear();
390    }
391    if precision_value > 0 {
392        let required = precision_value as usize;
393        if digits.len() < required {
394            let mut buf = String::with_capacity(required);
395            for _ in 0..(required - digits.len()) {
396                buf.push('0');
397            }
398            buf.push_str(&digits);
399            digits = buf;
400        }
401    }
402
403    let mut prefix = String::new();
404    if alternate && abs_val != 0 {
405        match base {
406            8 => prefix.push('0'),
407            16 => {
408                prefix.push('0');
409                prefix.push(if uppercase { 'X' } else { 'x' });
410            }
411            2 => {
412                prefix.push('0');
413                prefix.push('b');
414            }
415            _ => {}
416        }
417    }
418
419    apply_width(sign, prefix, digits, flags, width, flags.zero_pad)
420}
421
422fn format_unsigned(
423    value: u128,
424    base: u32,
425    mut flags: FormatFlags,
426    width: Option<isize>,
427    precision: Option<isize>,
428    alternate: bool,
429    uppercase: bool,
430) -> Result<String, String> {
431    if precision.is_some() {
432        flags.zero_pad = false;
433    }
434
435    let mut digits = to_base_string(value, base, uppercase);
436    let precision_value = precision.unwrap_or(-1);
437    if precision_value == 0 && value == 0 {
438        digits.clear();
439    }
440    if precision_value > 0 {
441        let required = precision_value as usize;
442        if digits.len() < required {
443            let mut buf = String::with_capacity(required);
444            for _ in 0..(required - digits.len()) {
445                buf.push('0');
446            }
447            buf.push_str(&digits);
448            digits = buf;
449        }
450    }
451
452    let mut prefix = String::new();
453    if alternate && value != 0 {
454        match base {
455            8 => prefix.push('0'),
456            16 => {
457                prefix.push_str(if uppercase { "0X" } else { "0x" });
458            }
459            2 => prefix.push_str("0b"),
460            _ => {}
461        }
462    }
463
464    apply_width(String::new(), prefix, digits, flags, width, flags.zero_pad)
465}
466
467fn format_float(
468    value: f64,
469    conversion: char,
470    flags: FormatFlags,
471    width: Option<isize>,
472    precision: Option<isize>,
473    alternate: bool,
474) -> Result<String, String> {
475    let mut sign = String::new();
476    let mut magnitude = value;
477
478    if value.is_nan() {
479        return apply_width(
480            String::new(),
481            String::new(),
482            "NaN".to_string(),
483            flags,
484            width,
485            false,
486        );
487    }
488
489    if value.is_infinite() {
490        if value.is_sign_negative() {
491            sign.push('-');
492        } else if flags.sign_plus {
493            sign.push('+');
494        } else if flags.sign_space {
495            sign.push(' ');
496        }
497        let text = "Inf".to_string();
498        return apply_width(sign, String::new(), text, flags, width, false);
499    }
500
501    if value.is_sign_negative() || (value == 0.0 && (1.0 / value).is_sign_negative()) {
502        sign.push('-');
503        magnitude = -value;
504    } else if flags.sign_plus {
505        sign.push('+');
506    } else if flags.sign_space {
507        sign.push(' ');
508    }
509
510    let prec = precision.unwrap_or(6).max(0) as usize;
511    let mut body = match conversion {
512        'f' | 'F' => format!("{magnitude:.prec$}"),
513        'e' => format!("{magnitude:.prec$e}"),
514        'E' => format!("{magnitude:.prec$E}"),
515        'g' | 'G' => format_float_general(magnitude, prec, conversion.is_uppercase()),
516        _ => {
517            return Err(format!(
518                "sprintf: unsupported float conversion %{}",
519                conversion
520            ))
521        }
522    };
523
524    if alternate && !body.contains('.') && matches!(conversion, 'f' | 'F' | 'g' | 'G') {
525        body.push('.');
526    }
527
528    let zero_pad_allowed = flags.zero_pad && !flags.left_align;
529    apply_width(sign, String::new(), body, flags, width, zero_pad_allowed)
530}
531
532fn format_float_general(value: f64, precision: usize, uppercase: bool) -> String {
533    if value == 0.0 {
534        if precision == 0 {
535            return "0".to_string();
536        }
537        let mut zero = String::from("0");
538        if precision > 0 {
539            zero.push('.');
540            zero.push_str(&"0".repeat(precision.saturating_sub(1)));
541        }
542        return zero;
543    }
544
545    let mut prec = precision;
546    if prec == 0 {
547        prec = 1;
548    }
549
550    let abs_val = value.abs();
551    let exp = abs_val.log10().floor() as i32;
552    let use_exp = exp < -4 || exp >= prec as i32;
553
554    if use_exp {
555        let mut s = format!("{:.*e}", prec - 1, value);
556        if uppercase {
557            s = s.to_uppercase();
558        }
559        trim_trailing_zeros(&mut s, true);
560        s
561    } else {
562        let mut s = format!("{:.*}", prec.max(1) - 1, value);
563        trim_trailing_zeros(&mut s, false);
564        s
565    }
566}
567
568fn trim_trailing_zeros(text: &mut String, keep_exponent: bool) {
569    if let Some(dot_idx) = text.find('.') {
570        let mut end = text.len();
571        while end > dot_idx + 1 {
572            let byte = text.as_bytes()[end - 1];
573            if byte == b'0' {
574                end -= 1;
575            } else {
576                break;
577            }
578        }
579        if end > dot_idx + 1 && text.as_bytes()[end - 1] == b'.' {
580            end -= 1;
581        }
582        if keep_exponent {
583            if let Some(exp_idx) = text.find(['e', 'E']) {
584                let exponent = text[exp_idx..].to_string();
585                text.truncate(end.min(exp_idx));
586                text.push_str(&exponent);
587                return;
588            }
589        }
590        text.truncate(end);
591    }
592}
593
594fn format_string(
595    value: Value,
596    flags: FormatFlags,
597    width: Option<isize>,
598    precision: Option<isize>,
599) -> Result<String, String> {
600    let mut text = value_to_string(&value)?;
601    if let Some(p) = precision {
602        if p >= 0 {
603            let mut chars = text.chars();
604            let mut truncated = String::with_capacity(text.len());
605            for _ in 0..(p as usize) {
606                if let Some(ch) = chars.next() {
607                    truncated.push(ch);
608                } else {
609                    break;
610                }
611            }
612            text = truncated;
613        }
614    }
615
616    apply_width(String::new(), String::new(), text, flags, width, false)
617}
618
619fn format_char(value: Value, flags: FormatFlags, width: Option<isize>) -> Result<String, String> {
620    let ch = value_to_char(&value)?;
621    let text = ch.to_string();
622    apply_width(String::new(), String::new(), text, flags, width, false)
623}
624
625fn apply_width(
626    sign: String,
627    prefix: String,
628    digits: String,
629    flags: FormatFlags,
630    width: Option<isize>,
631    zero_pad: bool,
632) -> Result<String, String> {
633    let mut result = String::new();
634    let sign_prefix_len = sign.len() + prefix.len();
635    let total_len = sign_prefix_len + digits.len();
636    let target_width = width.unwrap_or(0).max(0) as usize;
637
638    if target_width <= total_len {
639        result.push_str(&sign);
640        result.push_str(&prefix);
641        result.push_str(&digits);
642        return Ok(result);
643    }
644
645    let pad_len = target_width - total_len;
646    if flags.left_align {
647        result.push_str(&sign);
648        result.push_str(&prefix);
649        result.push_str(&digits);
650        for _ in 0..pad_len {
651            result.push(' ');
652        }
653        return Ok(result);
654    }
655
656    if zero_pad {
657        result.push_str(&sign);
658        result.push_str(&prefix);
659        for _ in 0..pad_len {
660            result.push('0');
661        }
662        result.push_str(&digits);
663    } else {
664        for _ in 0..pad_len {
665            result.push(' ');
666        }
667        result.push_str(&sign);
668        result.push_str(&prefix);
669        result.push_str(&digits);
670    }
671    Ok(result)
672}
673
674fn value_to_isize(value: &Value) -> Result<isize, String> {
675    match value {
676        Value::Int(i) => Ok(i.to_i64().clamp(isize::MIN as i64, isize::MAX as i64) as isize),
677        Value::Num(n) => {
678            if !n.is_finite() {
679                return Err("sprintf: width/precision specifier must be finite".to_string());
680            }
681            Ok(n.trunc().clamp(isize::MIN as f64, isize::MAX as f64) as isize)
682        }
683        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
684        other => Err(format!(
685            "sprintf: width/precision specifier expects numeric value, got {other:?}"
686        )),
687    }
688}
689
690fn value_to_i128(value: &Value) -> Result<i128, String> {
691    match value {
692        Value::Int(i) => Ok(match i {
693            IntValue::I8(v) => i128::from(*v),
694            IntValue::I16(v) => i128::from(*v),
695            IntValue::I32(v) => i128::from(*v),
696            IntValue::I64(v) => i128::from(*v),
697            IntValue::U8(v) => i128::from(*v),
698            IntValue::U16(v) => i128::from(*v),
699            IntValue::U32(v) => i128::from(*v),
700            IntValue::U64(v) => i128::from(*v),
701        }),
702        Value::Num(n) => {
703            if !n.is_finite() {
704                return Err("sprintf: numeric conversion requires finite input".to_string());
705            }
706            Ok(n.trunc().clamp(i128::MIN as f64, i128::MAX as f64) as i128)
707        }
708        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
709        other => Err(format!("sprintf: expected numeric argument, got {other:?}")),
710    }
711}
712
713fn value_to_u128(value: &Value) -> Result<u128, String> {
714    match value {
715        Value::Int(i) => match i {
716            IntValue::I8(v) if *v < 0 => Err("sprintf: expected non-negative value".to_string()),
717            IntValue::I16(v) if *v < 0 => Err("sprintf: expected non-negative value".to_string()),
718            IntValue::I32(v) if *v < 0 => Err("sprintf: expected non-negative value".to_string()),
719            IntValue::I64(v) if *v < 0 => Err("sprintf: expected non-negative value".to_string()),
720            IntValue::I8(v) => Ok((*v) as u128),
721            IntValue::I16(v) => Ok((*v) as u128),
722            IntValue::I32(v) => Ok((*v) as u128),
723            IntValue::I64(v) => Ok((*v) as u128),
724            IntValue::U8(v) => Ok((*v) as u128),
725            IntValue::U16(v) => Ok((*v) as u128),
726            IntValue::U32(v) => Ok((*v) as u128),
727            IntValue::U64(v) => Ok((*v) as u128),
728        },
729        Value::Num(n) => {
730            if !n.is_finite() {
731                return Err("sprintf: numeric conversion requires finite input".to_string());
732            }
733            if *n < 0.0 {
734                return Err("sprintf: expected non-negative value".to_string());
735            }
736            Ok(n.trunc().clamp(0.0, u128::MAX as f64) as u128)
737        }
738        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
739        other => Err(format!(
740            "sprintf: expected non-negative numeric value, got {other:?}"
741        )),
742    }
743}
744
745fn value_to_f64(value: &Value) -> Result<f64, String> {
746    match value {
747        Value::Num(n) => Ok(*n),
748        Value::Int(i) => Ok(i.to_f64()),
749        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
750        other => Err(format!("sprintf: expected numeric value, got {other:?}")),
751    }
752}
753
754fn value_to_string(value: &Value) -> Result<String, String> {
755    match value {
756        Value::String(s) => Ok(s.clone()),
757        Value::CharArray(ca) => {
758            let mut s = String::with_capacity(ca.data.len());
759            for ch in &ca.data {
760                s.push(*ch);
761            }
762            Ok(s)
763        }
764        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
765        Value::Num(n) => Ok(Value::Num(*n).to_string()),
766        Value::Int(i) => Ok(i.to_i64().to_string()),
767        Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
768        Value::Complex(re, im) => Ok(Value::Complex(*re, *im).to_string()),
769        other => Err(format!(
770            "sprintf: expected text or scalar value for %s conversion, got {other:?}"
771        )),
772    }
773}
774
775fn value_to_char(value: &Value) -> Result<char, String> {
776    match value {
777        Value::String(s) => s
778            .chars()
779            .next()
780            .ok_or_else(|| "sprintf: %c conversion requires non-empty character input".to_string()),
781        Value::CharArray(ca) => ca
782            .data
783            .first()
784            .copied()
785            .ok_or_else(|| "sprintf: %c conversion requires non-empty char input".to_string()),
786        Value::Num(n) => {
787            if !n.is_finite() {
788                return Err("sprintf: %c conversion needs finite numeric value".to_string());
789            }
790            let code = n.trunc() as u32;
791            std::char::from_u32(code)
792                .ok_or_else(|| "sprintf: numeric value outside valid character range".to_string())
793        }
794        Value::Int(i) => {
795            let code = i.to_i64();
796            if code < 0 {
797                return Err("sprintf: negative value for %c conversion".to_string());
798            }
799            std::char::from_u32(code as u32)
800                .ok_or_else(|| "sprintf: numeric value outside valid character range".to_string())
801        }
802        other => Err(format!(
803            "sprintf: %c conversion expects character data, got {other:?}"
804        )),
805    }
806}
807
808fn to_base_string(mut value: u128, base: u32, uppercase: bool) -> String {
809    if value == 0 {
810        return "0".to_string();
811    }
812    let mut buf = Vec::new();
813    while value > 0 {
814        let digit = (value % base as u128) as u8;
815        let ch = match digit {
816            0..=9 => b'0' + digit,
817            _ => {
818                if uppercase {
819                    b'A' + (digit - 10)
820                } else {
821                    b'a' + (digit - 10)
822                }
823            }
824        };
825        buf.push(ch as char);
826        value /= base as u128;
827    }
828    buf.iter().rev().collect()
829}
830
831/// Extract a printf-style format string from a MATLAB value, validating that it
832/// is a character row vector or string scalar.
833pub fn extract_format_string(value: &Value, context: &str) -> Result<String, String> {
834    match value {
835        Value::String(s) => Ok(s.clone()),
836        Value::CharArray(ca) => {
837            if ca.rows != 1 {
838                return Err(format!(
839                    "{context}: formatSpec must be a character row vector or string scalar"
840                ));
841            }
842            Ok(ca.data.iter().collect())
843        }
844        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
845        _ => Err(format!(
846            "{context}: formatSpec must be a character row vector or string scalar"
847        )),
848    }
849}
850
851/// Decode MATLAB-compatible escape sequences within a format specification.
852pub fn decode_escape_sequences(context: &str, input: &str) -> Result<String, String> {
853    let mut result = String::with_capacity(input.len());
854    let mut chars = input.chars().peekable();
855    while let Some(ch) = chars.next() {
856        if ch != '\\' {
857            result.push(ch);
858            continue;
859        }
860        let Some(next) = chars.next() else {
861            result.push('\\');
862            break;
863        };
864        match next {
865            '\\' => result.push('\\'),
866            'a' => result.push('\u{0007}'),
867            'b' => result.push('\u{0008}'),
868            'f' => result.push('\u{000C}'),
869            'n' => result.push('\n'),
870            'r' => result.push('\r'),
871            't' => result.push('\t'),
872            'v' => result.push('\u{000B}'),
873            'x' => {
874                let mut hex = String::new();
875                for _ in 0..2 {
876                    match chars.peek().copied() {
877                        Some(c) if c.is_ascii_hexdigit() => {
878                            hex.push(chars.next().unwrap());
879                        }
880                        _ => break,
881                    }
882                }
883                if hex.is_empty() {
884                    result.push('\\');
885                    result.push('x');
886                } else {
887                    let value = u32::from_str_radix(&hex, 16)
888                        .map_err(|_| format!("{context}: invalid hexadecimal escape \\x{hex}"))?;
889                    if let Some(chr) = char::from_u32(value) {
890                        result.push(chr);
891                    } else {
892                        return Err(format!(
893                            "{context}: \\x{hex} escape outside valid Unicode range"
894                        ));
895                    }
896                }
897            }
898            '0'..='7' => {
899                let mut oct = String::new();
900                oct.push(next);
901                for _ in 0..2 {
902                    match chars.peek().copied() {
903                        Some(c) if ('0'..='7').contains(&c) => {
904                            oct.push(chars.next().unwrap());
905                        }
906                        _ => break,
907                    }
908                }
909                let value = u32::from_str_radix(&oct, 8)
910                    .map_err(|_| format!("{context}: invalid octal escape \\{oct}"))?;
911                if let Some(chr) = char::from_u32(value) {
912                    result.push(chr);
913                } else {
914                    return Err(format!(
915                        "{context}: \\{oct} escape outside valid Unicode range"
916                    ));
917                }
918            }
919            other => {
920                result.push('\\');
921                result.push(other);
922            }
923        }
924    }
925    Ok(result)
926}
927
928/// Flatten MATLAB argument values into a linear vector suitable for repeated
929/// printf-style formatting. Arrays are traversed in column-major order and GPU
930/// tensors are gathered back to the host.
931pub fn flatten_arguments(args: &[Value], context: &str) -> Result<Vec<Value>, String> {
932    let mut flattened = Vec::new();
933    for value in args {
934        let gathered = gather_if_needed(value).map_err(|e| format!("{context}: {e}"))?;
935        flatten_value(gathered, &mut flattened, context)?;
936    }
937    Ok(flattened)
938}
939
940fn flatten_value(value: Value, output: &mut Vec<Value>, context: &str) -> Result<(), String> {
941    match value {
942        Value::Num(_)
943        | Value::Int(_)
944        | Value::Bool(_)
945        | Value::String(_)
946        | Value::Complex(_, _) => {
947            output.push(value);
948        }
949        Value::Tensor(tensor) => {
950            for &elem in &tensor.data {
951                output.push(Value::Num(elem));
952            }
953        }
954        Value::ComplexTensor(tensor) => {
955            for &(re, im) in &tensor.data {
956                output.push(Value::Complex(re, im));
957            }
958        }
959        Value::LogicalArray(LogicalArray { data, .. }) => {
960            for byte in data {
961                output.push(Value::Bool(byte != 0));
962            }
963        }
964        Value::StringArray(StringArray { data, .. }) => {
965            for s in data {
966                output.push(Value::String(s));
967            }
968        }
969        Value::CharArray(ca) => {
970            if ca.rows == 1 {
971                output.push(Value::String(ca.data.iter().collect()));
972            } else {
973                for row in 0..ca.rows {
974                    let mut line = String::with_capacity(ca.cols);
975                    for col in 0..ca.cols {
976                        line.push(ca.data[row * ca.cols + col]);
977                    }
978                    output.push(Value::String(line));
979                }
980            }
981        }
982        Value::Cell(cell) => {
983            for col in 0..cell.cols {
984                for row in 0..cell.rows {
985                    let idx = row * cell.cols + col;
986                    let inner = (*cell.data[idx]).clone();
987                    let gathered =
988                        gather_if_needed(&inner).map_err(|e| format!("{context}: {e}"))?;
989                    flatten_value(gathered, output, context)?;
990                }
991            }
992        }
993        Value::GpuTensor(handle) => {
994            let gathered = gather_if_needed(&Value::GpuTensor(handle))
995                .map_err(|e| format!("{context}: {e}"))?;
996            flatten_value(gathered, output, context)?;
997        }
998        Value::MException(_)
999        | Value::HandleObject(_)
1000        | Value::Listener(_)
1001        | Value::Object(_)
1002        | Value::Struct(_)
1003        | Value::FunctionHandle(_)
1004        | Value::Closure(_)
1005        | Value::ClassRef(_) => {
1006            return Err(format!("{context}: unsupported argument type"));
1007        }
1008    }
1009    Ok(())
1010}