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