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