1use 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#[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#[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
82pub 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
94pub 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 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 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(¤t) {
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
798fn value_to_string(value: &Value) -> BuiltinResult<String> {
799 match value {
800 Value::String(s) => Ok(s.clone()),
801 Value::CharArray(ca) => {
802 let mut s = String::with_capacity(ca.data.len());
803 for ch in &ca.data {
804 s.push(*ch);
805 }
806 Ok(s)
807 }
808 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
809 Value::Num(n) => Ok(Value::Num(*n).to_string()),
810 Value::Int(i) => Ok(i.to_i64().to_string()),
811 Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
812 Value::Complex(re, im) => Ok(Value::Complex(*re, *im).to_string()),
813 other => Err(format_error(format!(
814 "sprintf: expected text or scalar value for %s conversion, got {other:?}"
815 ))),
816 }
817}
818
819fn value_to_char(value: &Value) -> BuiltinResult<char> {
820 match value {
821 Value::String(s) => s.chars().next().ok_or_else(|| {
822 format_error("sprintf: %c conversion requires non-empty character input")
823 }),
824 Value::CharArray(ca) => ca
825 .data
826 .first()
827 .copied()
828 .ok_or_else(|| format_error("sprintf: %c conversion requires non-empty char input")),
829 Value::Num(n) => {
830 if !n.is_finite() {
831 return Err(format_error(
832 "sprintf: %c conversion needs finite numeric value",
833 ));
834 }
835 let code = n.trunc() as u32;
836 std::char::from_u32(code)
837 .ok_or_else(|| format_error("sprintf: numeric value outside valid character range"))
838 }
839 Value::Int(i) => {
840 let code = i.to_i64();
841 if code < 0 {
842 return Err(format_error("sprintf: negative value for %c conversion"));
843 }
844 std::char::from_u32(code as u32)
845 .ok_or_else(|| format_error("sprintf: numeric value outside valid character range"))
846 }
847 other => Err(format_error(format!(
848 "sprintf: %c conversion expects character data, got {other:?}"
849 ))),
850 }
851}
852
853fn to_base_string(mut value: u128, base: u32, uppercase: bool) -> String {
854 if value == 0 {
855 return "0".to_string();
856 }
857 let mut buf = Vec::new();
858 while value > 0 {
859 let digit = (value % base as u128) as u8;
860 let ch = match digit {
861 0..=9 => b'0' + digit,
862 _ => {
863 if uppercase {
864 b'A' + (digit - 10)
865 } else {
866 b'a' + (digit - 10)
867 }
868 }
869 };
870 buf.push(ch as char);
871 value /= base as u128;
872 }
873 buf.iter().rev().collect()
874}
875
876fn group_decimal_digits(digits: &str) -> String {
877 if digits.len() <= 3 {
878 return digits.to_string();
879 }
880 let chars: Vec<char> = digits.chars().collect();
881 let mut out = String::with_capacity(digits.len() + (digits.len() - 1) / 3);
882 for (idx, ch) in chars.iter().enumerate() {
883 if idx > 0 && (chars.len() - idx).is_multiple_of(3) {
884 out.push(',');
885 }
886 out.push(*ch);
887 }
888 out
889}
890
891fn group_float_mantissa(text: &str) -> String {
892 let (mantissa, exponent) = match text.find(['e', 'E']) {
893 Some(idx) => (&text[..idx], &text[idx..]),
894 None => (text, ""),
895 };
896
897 let mut parts = mantissa.splitn(2, '.');
898 let int_part = parts.next().unwrap_or_default();
899 let frac_part = parts.next();
900 let grouped_int = group_decimal_digits(int_part);
901
902 let mut out = grouped_int;
903 if let Some(frac) = frac_part {
904 out.push('.');
905 out.push_str(frac);
906 }
907 out.push_str(exponent);
908 out
909}
910
911pub fn extract_format_string(value: &Value, context: &str) -> BuiltinResult<String> {
914 match value {
915 Value::String(s) => Ok(s.clone()),
916 Value::CharArray(ca) => {
917 if ca.rows != 1 {
918 return Err(format_error(format!(
919 "{context}: formatSpec must be a character row vector or string scalar"
920 )));
921 }
922 Ok(ca.data.iter().collect())
923 }
924 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
925 _ => Err(format_error(format!(
926 "{context}: formatSpec must be a character row vector or string scalar"
927 ))),
928 }
929}
930
931pub fn decode_escape_sequences(context: &str, input: &str) -> BuiltinResult<String> {
933 let mut result = String::with_capacity(input.len());
934 let mut chars = input.chars().peekable();
935 while let Some(ch) = chars.next() {
936 if ch != '\\' {
937 result.push(ch);
938 continue;
939 }
940 let Some(next) = chars.next() else {
941 result.push('\\');
942 break;
943 };
944 match next {
945 '\\' => result.push('\\'),
946 'a' => result.push('\u{0007}'),
947 'b' => result.push('\u{0008}'),
948 'f' => result.push('\u{000C}'),
949 'n' => result.push('\n'),
950 'r' => result.push('\r'),
951 't' => result.push('\t'),
952 'v' => result.push('\u{000B}'),
953 'x' => {
954 let mut hex = String::new();
955 for _ in 0..2 {
956 match chars.peek().copied() {
957 Some(c) if c.is_ascii_hexdigit() => {
958 hex.push(chars.next().unwrap());
959 }
960 _ => break,
961 }
962 }
963 if hex.is_empty() {
964 result.push('\\');
965 result.push('x');
966 } else {
967 let value = u32::from_str_radix(&hex, 16).map_err(|_| {
968 format_error(format!("{context}: invalid hexadecimal escape \\x{hex}"))
969 })?;
970 if let Some(chr) = char::from_u32(value) {
971 result.push(chr);
972 } else {
973 return Err(format_error(format!(
974 "{context}: \\x{hex} escape outside valid Unicode range"
975 )));
976 }
977 }
978 }
979 '0'..='7' => {
980 let mut oct = String::new();
981 oct.push(next);
982 for _ in 0..2 {
983 match chars.peek().copied() {
984 Some(c) if ('0'..='7').contains(&c) => {
985 oct.push(chars.next().unwrap());
986 }
987 _ => break,
988 }
989 }
990 let value = u32::from_str_radix(&oct, 8).map_err(|_| {
991 format_error(format!("{context}: invalid octal escape \\{oct}"))
992 })?;
993 if let Some(chr) = char::from_u32(value) {
994 result.push(chr);
995 } else {
996 return Err(format_error(format!(
997 "{context}: \\{oct} escape outside valid Unicode range"
998 )));
999 }
1000 }
1001 other => {
1002 result.push('\\');
1003 result.push(other);
1004 }
1005 }
1006 }
1007 Ok(result)
1008}
1009
1010pub async fn flatten_arguments(args: &[Value], context: &str) -> BuiltinResult<Vec<Value>> {
1014 let mut flattened = Vec::new();
1015 for value in args {
1016 let gathered = gather_if_needed_async(value)
1017 .await
1018 .map_err(|flow| map_control_flow_with_context(flow, context))?;
1019 flatten_value(gathered, &mut flattened, context).await?;
1020 }
1021 Ok(flattened)
1022}
1023
1024#[async_recursion::async_recursion(?Send)]
1025async fn flatten_value(value: Value, output: &mut Vec<Value>, context: &str) -> BuiltinResult<()> {
1026 match value {
1027 Value::Num(_)
1028 | Value::Int(_)
1029 | Value::Bool(_)
1030 | Value::String(_)
1031 | Value::Complex(_, _) => {
1032 output.push(value);
1033 }
1034 Value::Tensor(tensor) => {
1035 for &elem in &tensor.data {
1036 output.push(Value::Num(elem));
1037 }
1038 }
1039 Value::ComplexTensor(tensor) => {
1040 for &(re, im) in &tensor.data {
1041 output.push(Value::Complex(re, im));
1042 }
1043 }
1044 Value::LogicalArray(LogicalArray { data, .. }) => {
1045 for byte in data {
1046 output.push(Value::Bool(byte != 0));
1047 }
1048 }
1049 Value::StringArray(StringArray { data, .. }) => {
1050 for s in data {
1051 output.push(Value::String(s));
1052 }
1053 }
1054 Value::CharArray(ca) => {
1055 if ca.rows == 1 {
1056 output.push(Value::String(ca.data.iter().collect()));
1057 } else {
1058 for row in 0..ca.rows {
1059 let mut line = String::with_capacity(ca.cols);
1060 for col in 0..ca.cols {
1061 line.push(ca.data[row * ca.cols + col]);
1062 }
1063 output.push(Value::String(line));
1064 }
1065 }
1066 }
1067 Value::Cell(cell) => {
1068 for col in 0..cell.cols {
1069 for row in 0..cell.rows {
1070 let idx = row * cell.cols + col;
1071 let inner = (*cell.data[idx]).clone();
1072 let gathered = gather_if_needed_async(&inner)
1073 .await
1074 .map_err(|flow| map_control_flow_with_context(flow, context))?;
1075 flatten_value(gathered, output, context).await?;
1076 }
1077 }
1078 }
1079 Value::GpuTensor(handle) => {
1080 let gathered = gather_if_needed_async(&Value::GpuTensor(handle))
1081 .await
1082 .map_err(|flow| map_control_flow_with_context(flow, context))?;
1083 flatten_value(gathered, output, context).await?;
1084 }
1085 Value::OutputList(values) => {
1086 for value in values {
1087 flatten_value(value, output, context).await?;
1088 }
1089 }
1090 Value::MException(_)
1091 | Value::HandleObject(_)
1092 | Value::Listener(_)
1093 | Value::Object(_)
1094 | Value::Struct(_)
1095 | Value::FunctionHandle(_)
1096 | Value::Closure(_)
1097 | Value::ClassRef(_) => {
1098 return Err(format_error(format!(
1099 "{context}: unsupported argument type"
1100 )));
1101 }
1102 }
1103 Ok(())
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109
1110 #[test]
1111 fn format_variadic_supports_thousands_grouping_flag() {
1112 let out = format_variadic("%'d %'.2f", &[Value::Num(1234567.0), Value::Num(12345.5)])
1113 .expect("grouped formatting should succeed");
1114 assert_eq!(out, "1,234,567 12,345.50");
1115 }
1116
1117 #[test]
1118 fn format_variadic_consumes_i_flag_without_error() {
1119 let out = format_variadic("%Id", &[Value::Int(IntValue::I32(42))])
1120 .expect("I flag should be accepted as compatibility no-op");
1121 assert_eq!(out, "42");
1122 }
1123}