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