1use encoding_rs::{Encoding, UTF_8};
4use runmat_builtins::{
5 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
6 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
7 CharArray, LogicalArray, ObjectInstance, ResolveContext, StringArray, Tensor, Type, Value,
8};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::common::broadcast as matlab_broadcast;
12use crate::builtins::common::map_control_flow_with_builtin;
13use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
14use crate::{build_runtime_error, gather_if_needed_async, make_cell_with_shape, BuiltinResult};
15
16const PATTERN_CLASS: &str = "pattern";
17
18const OUT_ANY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
19 name: "out",
20 ty: BuiltinParamType::Any,
21 arity: BuiltinParamArity::Required,
22 default: None,
23 description: "Output value.",
24}];
25
26const OUT_BOOL: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27 name: "tf",
28 ty: BuiltinParamType::LogicalArray,
29 arity: BuiltinParamArity::Required,
30 default: None,
31 description: "Logical result.",
32}];
33
34const IN_VALUE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
35 name: "value",
36 ty: BuiltinParamType::Any,
37 arity: BuiltinParamArity::Required,
38 default: None,
39 description: "Input value.",
40}];
41
42const IN_TEXT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
43 name: "text",
44 ty: BuiltinParamType::Any,
45 arity: BuiltinParamArity::Required,
46 default: None,
47 description: "Input text.",
48}];
49
50const IN_BOUNDARY_TYPE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
51 name: "type",
52 ty: BuiltinParamType::StringScalar,
53 arity: BuiltinParamArity::Required,
54 default: Some("\"either\""),
55 description: "Boundary type: \"either\", \"start\", or \"end\".",
56}];
57
58const IN_TEXT_REST: [BuiltinParamDescriptor; 2] = [
59 BuiltinParamDescriptor {
60 name: "text",
61 ty: BuiltinParamType::Any,
62 arity: BuiltinParamArity::Required,
63 default: None,
64 description: "Input text.",
65 },
66 BuiltinParamDescriptor {
67 name: "arg",
68 ty: BuiltinParamType::Any,
69 arity: BuiltinParamArity::Variadic,
70 default: None,
71 description: "Additional arguments.",
72 },
73];
74
75const IN_A_B_N: [BuiltinParamDescriptor; 3] = [
76 BuiltinParamDescriptor {
77 name: "A",
78 ty: BuiltinParamType::Any,
79 arity: BuiltinParamArity::Required,
80 default: None,
81 description: "First text input.",
82 },
83 BuiltinParamDescriptor {
84 name: "B",
85 ty: BuiltinParamType::Any,
86 arity: BuiltinParamArity::Required,
87 default: None,
88 description: "Second text input.",
89 },
90 BuiltinParamDescriptor {
91 name: "N",
92 ty: BuiltinParamType::IntegerScalar,
93 arity: BuiltinParamArity::Required,
94 default: None,
95 description: "Number of leading characters to compare.",
96 },
97];
98
99const NO_INPUTS: [BuiltinParamDescriptor; 0] = [];
100const NO_ERRORS: [BuiltinErrorDescriptor; 0] = [];
101
102macro_rules! descriptor {
103 ($name:ident, $label:expr, $inputs:expr, $outputs:expr) => {
104 const $name: BuiltinDescriptor = BuiltinDescriptor {
105 signatures: &[BuiltinSignatureDescriptor {
106 label: $label,
107 inputs: $inputs,
108 outputs: $outputs,
109 }],
110 output_mode: BuiltinOutputMode::Fixed,
111 completion_policy: BuiltinCompletionPolicy::Public,
112 errors: &NO_ERRORS,
113 };
114 };
115}
116
117macro_rules! descriptor_by_outputs {
118 ($name:ident, $label:expr, $inputs:expr, $outputs:expr) => {
119 const $name: BuiltinDescriptor = BuiltinDescriptor {
120 signatures: &[BuiltinSignatureDescriptor {
121 label: $label,
122 inputs: $inputs,
123 outputs: $outputs,
124 }],
125 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
126 completion_policy: BuiltinCompletionPolicy::Public,
127 errors: &NO_ERRORS,
128 };
129 };
130}
131
132descriptor!(NEWLINE_DESCRIPTOR, "s = newline", &NO_INPUTS, &OUT_ANY);
133descriptor!(BLANKS_DESCRIPTOR, "s = blanks(n)", &IN_VALUE, &OUT_ANY);
134descriptor!(
135 IS_STRING_SCALAR_DESCRIPTOR,
136 "tf = isStringScalar(value)",
137 &IN_VALUE,
138 &OUT_BOOL
139);
140descriptor_by_outputs!(
141 CONVERT_STRINGS_TO_CHARS_DESCRIPTOR,
142 "[out1, ...] = convertStringsToChars(value1, ...)",
143 &IN_TEXT_REST,
144 &OUT_ANY
145);
146descriptor!(
147 CONVERT_CHARS_TO_STRINGS_DESCRIPTOR,
148 "out = convertCharsToStrings(value)",
149 &IN_VALUE,
150 &OUT_ANY
151);
152descriptor!(
153 CONVERT_CONTAINED_STRINGS_TO_CHARS_DESCRIPTOR,
154 "out = convertContainedStringsToChars(value)",
155 &IN_VALUE,
156 &OUT_ANY
157);
158descriptor!(
159 STRNCMPI_DESCRIPTOR,
160 "tf = strncmpi(A, B, N)",
161 &IN_A_B_N,
162 &OUT_BOOL
163);
164descriptor!(
165 ISSTRPROP_DESCRIPTOR,
166 "tf = isstrprop(text, category)",
167 &IN_TEXT_REST,
168 &OUT_BOOL
169);
170descriptor!(
171 ISLETTER_DESCRIPTOR,
172 "tf = isletter(text)",
173 &IN_TEXT,
174 &OUT_BOOL
175);
176descriptor!(
177 ISSPACE_DESCRIPTOR,
178 "tf = isspace(text)",
179 &IN_TEXT,
180 &OUT_BOOL
181);
182descriptor_by_outputs!(
183 STRTOK_DESCRIPTOR,
184 "[tok, rem] = strtok(text, delimiters)",
185 &IN_TEXT_REST,
186 &OUT_ANY
187);
188descriptor_by_outputs!(
189 STR2NUM_DESCRIPTOR,
190 "[x, tf] = str2num(text)",
191 &IN_TEXT,
192 &OUT_ANY
193);
194descriptor!(
195 MAT2STR_DESCRIPTOR,
196 "s = mat2str(A)",
197 &IN_TEXT_REST,
198 &OUT_ANY
199);
200descriptor!(
201 NATIVE2UNICODE_DESCRIPTOR,
202 "s = native2unicode(bytes, encoding)",
203 &IN_TEXT_REST,
204 &OUT_ANY
205);
206descriptor_by_outputs!(
207 SSCANF_DESCRIPTOR,
208 "[A, count, errmsg, nextindex] = sscanf(text, format, size)",
209 &IN_TEXT_REST,
210 &OUT_ANY
211);
212descriptor!(
213 PATTERN_DESCRIPTOR,
214 "pat = pattern(text)",
215 &IN_TEXT,
216 &OUT_ANY
217);
218descriptor!(
219 REGEXP_PATTERN_DESCRIPTOR,
220 "pat = regexpPattern(expr)",
221 &IN_TEXT,
222 &OUT_ANY
223);
224descriptor!(
225 DIGITS_PATTERN_DESCRIPTOR,
226 "pat = digitsPattern(N)",
227 &IN_TEXT_REST,
228 &OUT_ANY
229);
230descriptor!(
231 LETTERS_PATTERN_DESCRIPTOR,
232 "pat = lettersPattern(N)",
233 &IN_TEXT_REST,
234 &OUT_ANY
235);
236descriptor!(
237 WILDCARD_PATTERN_DESCRIPTOR,
238 "pat = wildcardPattern",
239 &IN_TEXT_REST,
240 &OUT_ANY
241);
242const TEXT_BOUNDARY_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
243 BuiltinSignatureDescriptor {
244 label: "pat = textBoundary",
245 inputs: &[],
246 outputs: &OUT_ANY,
247 },
248 BuiltinSignatureDescriptor {
249 label: "pat = textBoundary(type)",
250 inputs: &IN_BOUNDARY_TYPE,
251 outputs: &OUT_ANY,
252 },
253];
254pub const TEXT_BOUNDARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
255 signatures: &TEXT_BOUNDARY_SIGNATURES,
256 output_mode: BuiltinOutputMode::Fixed,
257 completion_policy: BuiltinCompletionPolicy::Public,
258 errors: &NO_ERRORS,
259};
260
261fn any_type(_args: &[Type], _context: &ResolveContext) -> Type {
262 Type::Unknown
263}
264
265fn string_type(_args: &[Type], _context: &ResolveContext) -> Type {
266 Type::String
267}
268
269fn bool_type(_args: &[Type], _context: &ResolveContext) -> Type {
270 Type::Bool
271}
272
273fn tensor_type(_args: &[Type], _context: &ResolveContext) -> Type {
274 Type::tensor()
275}
276
277fn compat_error(name: &str, message: impl Into<String>) -> crate::RuntimeError {
278 build_runtime_error(message).with_builtin(name).build()
279}
280
281fn map_flow(name: &'static str) -> impl Fn(crate::RuntimeError) -> crate::RuntimeError {
282 move |err| map_control_flow_with_builtin(err, name)
283}
284
285#[runtime_builtin(
286 name = "newline",
287 category = "strings/core",
288 summary = "Return a newline string scalar.",
289 keywords = "newline,string,text,line break",
290 accel = "metadata",
291 type_resolver(string_type),
292 descriptor(crate::builtins::strings::core::compat::NEWLINE_DESCRIPTOR),
293 builtin_path = "crate::builtins::strings::core::compat"
294)]
295fn newline_builtin() -> BuiltinResult<Value> {
296 Ok(Value::String("\n".to_string()))
297}
298
299#[runtime_builtin(
300 name = "blanks",
301 category = "strings/core",
302 summary = "Return a character row vector of spaces.",
303 keywords = "blanks,char,space,text",
304 accel = "metadata",
305 type_resolver(string_type),
306 descriptor(crate::builtins::strings::core::compat::BLANKS_DESCRIPTOR),
307 builtin_path = "crate::builtins::strings::core::compat"
308)]
309async fn blanks_builtin(n: Value) -> BuiltinResult<Value> {
310 let n = gather_if_needed_async(&n)
311 .await
312 .map_err(map_flow("blanks"))?;
313 let n = parse_nonnegative_usize(&n, "blanks")?;
314 Ok(Value::CharArray(CharArray::new_row(&" ".repeat(n))))
315}
316
317#[runtime_builtin(
318 name = "isStringScalar",
319 category = "strings/core",
320 summary = "Return true for a scalar MATLAB string.",
321 keywords = "isStringScalar,string scalar,type predicate",
322 accel = "metadata",
323 type_resolver(bool_type),
324 descriptor(crate::builtins::strings::core::compat::IS_STRING_SCALAR_DESCRIPTOR),
325 builtin_path = "crate::builtins::strings::core::compat"
326)]
327fn is_string_scalar_builtin(value: Value) -> BuiltinResult<Value> {
328 Ok(Value::Bool(match value {
329 Value::String(_) => true,
330 Value::StringArray(array) => array.data.len() == 1,
331 _ => false,
332 }))
333}
334
335#[runtime_builtin(
336 name = "convertStringsToChars",
337 category = "strings/core",
338 summary = "Convert string scalars and arrays to character vectors.",
339 keywords = "convertStringsToChars,string,char,compatibility",
340 accel = "sink",
341 type_resolver(any_type),
342 descriptor(crate::builtins::strings::core::compat::CONVERT_STRINGS_TO_CHARS_DESCRIPTOR),
343 builtin_path = "crate::builtins::strings::core::compat"
344)]
345async fn convert_strings_to_chars_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
346 let mut inputs = Vec::with_capacity(rest.len() + 1);
347 inputs.push(value);
348 inputs.extend(rest);
349
350 let mut outputs = Vec::with_capacity(inputs.len());
351 for value in inputs {
352 let value = gather_if_needed_async(&value)
353 .await
354 .map_err(map_flow("convertStringsToChars"))?;
355 outputs.push(convert_strings_to_chars(value, false)?);
356 }
357
358 match crate::output_count::current_output_count() {
359 Some(0) => Ok(Value::OutputList(Vec::new())),
360 Some(n) => Ok(crate::output_count::output_list_with_padding(n, outputs)),
361 None => Ok(outputs
362 .into_iter()
363 .next()
364 .unwrap_or(Value::String(String::new()))),
365 }
366}
367
368#[runtime_builtin(
369 name = "convertCharsToStrings",
370 category = "strings/core",
371 summary = "Convert character arrays and cellstr values to string arrays.",
372 keywords = "convertCharsToStrings,char,string,compatibility",
373 accel = "sink",
374 type_resolver(any_type),
375 descriptor(crate::builtins::strings::core::compat::CONVERT_CHARS_TO_STRINGS_DESCRIPTOR),
376 builtin_path = "crate::builtins::strings::core::compat"
377)]
378async fn convert_chars_to_strings_builtin(value: Value) -> BuiltinResult<Value> {
379 let value = gather_if_needed_async(&value)
380 .await
381 .map_err(map_flow("convertCharsToStrings"))?;
382 convert_chars_to_strings(value)
383}
384
385#[runtime_builtin(
386 name = "convertContainedStringsToChars",
387 category = "strings/core",
388 summary = "Convert string values contained in cells and structs to character vectors.",
389 keywords = "convertContainedStringsToChars,string,char,cell,struct",
390 accel = "sink",
391 type_resolver(any_type),
392 descriptor(
393 crate::builtins::strings::core::compat::CONVERT_CONTAINED_STRINGS_TO_CHARS_DESCRIPTOR
394 ),
395 builtin_path = "crate::builtins::strings::core::compat"
396)]
397async fn convert_contained_strings_to_chars_builtin(value: Value) -> BuiltinResult<Value> {
398 let value = gather_if_needed_async(&value)
399 .await
400 .map_err(map_flow("convertContainedStringsToChars"))?;
401 convert_strings_to_chars(value, true)
402}
403
404#[runtime_builtin(
405 name = "strncmpi",
406 category = "strings/core",
407 summary = "Compare text inputs case-insensitively up to N leading characters.",
408 keywords = "strncmpi,string compare,prefix,text equality",
409 accel = "sink",
410 type_resolver(bool_type),
411 descriptor(crate::builtins::strings::core::compat::STRNCMPI_DESCRIPTOR),
412 builtin_path = "crate::builtins::strings::core::compat"
413)]
414async fn strncmpi_builtin(a: Value, b: Value, n: Value) -> BuiltinResult<Value> {
415 let a = gather_if_needed_async(&a)
416 .await
417 .map_err(map_flow("strncmpi"))?;
418 let b = gather_if_needed_async(&b)
419 .await
420 .map_err(map_flow("strncmpi"))?;
421 let n = gather_if_needed_async(&n)
422 .await
423 .map_err(map_flow("strncmpi"))?;
424 let n = parse_nonnegative_usize(&n, "strncmpi")?;
425 let left = TextList::from_value(a, "strncmpi")?;
426 let right = TextList::from_value(b, "strncmpi")?;
427 let shape = broadcast_shape(&left.shape, &right.shape, "strncmpi")?;
428 let total: usize = shape.iter().product();
429 let mut out = Vec::with_capacity(total);
430 for idx in 0..total {
431 let li = broadcast_flat_index(idx, &shape, &left.shape);
432 let ri = broadcast_flat_index(idx, &shape, &right.shape);
433 let matched = match (&left.items[li], &right.items[ri]) {
434 (Some(a), Some(b)) => prefix_eq_ignore_case(a, b, n),
435 _ => false,
436 };
437 out.push(u8::from(matched));
438 }
439 logical_value(out, shape, "strncmpi")
440}
441
442#[runtime_builtin(
443 name = "isstrprop",
444 category = "strings/core",
445 summary = "Classify characters in text by character property.",
446 keywords = "isstrprop,isletter,isspace,char classification,text",
447 accel = "sink",
448 type_resolver(tensor_type),
449 descriptor(crate::builtins::strings::core::compat::ISSTRPROP_DESCRIPTOR),
450 builtin_path = "crate::builtins::strings::core::compat"
451)]
452async fn isstrprop_builtin(text: Value, prop: Value) -> BuiltinResult<Value> {
453 let text = gather_if_needed_async(&text)
454 .await
455 .map_err(map_flow("isstrprop"))?;
456 let prop = gather_if_needed_async(&prop)
457 .await
458 .map_err(map_flow("isstrprop"))?;
459 let prop = scalar_text(&prop, "isstrprop")?.to_ascii_lowercase();
460 classify_text_value(text, "isstrprop", |ch| char_matches_prop(ch, &prop))
461}
462
463#[runtime_builtin(
464 name = "isletter",
465 category = "strings/core",
466 summary = "Return true for letters in text.",
467 keywords = "isletter,letter,char classification,text",
468 accel = "sink",
469 type_resolver(tensor_type),
470 descriptor(crate::builtins::strings::core::compat::ISLETTER_DESCRIPTOR),
471 builtin_path = "crate::builtins::strings::core::compat"
472)]
473async fn isletter_builtin(text: Value) -> BuiltinResult<Value> {
474 let text = gather_if_needed_async(&text)
475 .await
476 .map_err(map_flow("isletter"))?;
477 classify_text_value(text, "isletter", |ch| ch.is_alphabetic())
478}
479
480#[runtime_builtin(
481 name = "isspace",
482 category = "strings/core",
483 summary = "Return true for whitespace characters in text.",
484 keywords = "isspace,whitespace,char classification,text",
485 accel = "sink",
486 type_resolver(tensor_type),
487 descriptor(crate::builtins::strings::core::compat::ISSPACE_DESCRIPTOR),
488 builtin_path = "crate::builtins::strings::core::compat"
489)]
490async fn isspace_builtin(text: Value) -> BuiltinResult<Value> {
491 let text = gather_if_needed_async(&text)
492 .await
493 .map_err(map_flow("isspace"))?;
494 classify_text_value(text, "isspace", |ch| ch.is_whitespace())
495}
496
497#[runtime_builtin(
498 name = "strtok",
499 category = "strings/core",
500 summary = "Return the first token from text using delimiter characters.",
501 keywords = "strtok,tokenize,delimiter,text",
502 accel = "sink",
503 type_resolver(any_type),
504 descriptor(crate::builtins::strings::core::compat::STRTOK_DESCRIPTOR),
505 builtin_path = "crate::builtins::strings::core::compat"
506)]
507async fn strtok_builtin(text: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
508 let text = gather_if_needed_async(&text)
509 .await
510 .map_err(map_flow("strtok"))?;
511 let delimiters = if let Some(value) = rest.first() {
512 let value = gather_if_needed_async(value)
513 .await
514 .map_err(map_flow("strtok"))?;
515 scalar_text(&value, "strtok")?
516 } else {
517 " \t\n\r".to_string()
518 };
519 let (tokens, remainders) =
520 map_text_pair_preserve(text, "strtok", |s| strtok_pair(s, &delimiters))?;
521 match crate::output_count::current_output_count() {
522 Some(0) => Ok(Value::OutputList(Vec::new())),
523 Some(1) => Ok(Value::OutputList(vec![tokens])),
524 Some(n) => Ok(crate::output_count::output_list_with_padding(
525 n,
526 vec![tokens, remainders],
527 )),
528 None => Ok(tokens),
529 }
530}
531
532#[runtime_builtin(
533 name = "str2num",
534 category = "strings/core",
535 summary = "Convert text containing numeric literals to a numeric array.",
536 keywords = "str2num,string numeric conversion,text",
537 accel = "sink",
538 type_resolver(tensor_type),
539 descriptor(crate::builtins::strings::core::compat::STR2NUM_DESCRIPTOR),
540 builtin_path = "crate::builtins::strings::core::compat"
541)]
542async fn str2num_builtin(text: Value) -> BuiltinResult<Value> {
543 let text = gather_if_needed_async(&text)
544 .await
545 .map_err(map_flow("str2num"))?;
546 let text = scalar_text(&text, "str2num")?;
547 let (value, ok) = parse_str2num_matrix(&text);
548 match crate::output_count::current_output_count() {
549 Some(0) => Ok(Value::OutputList(Vec::new())),
550 Some(1) => Ok(Value::OutputList(vec![value])),
551 Some(n) => Ok(crate::output_count::output_list_with_padding(
552 n,
553 vec![value, Value::Bool(ok)],
554 )),
555 None => Ok(value),
556 }
557}
558
559#[runtime_builtin(
560 name = "mat2str",
561 category = "strings/core",
562 summary = "Convert numeric, logical, character, and string values to MATLAB expression text.",
563 keywords = "mat2str,array string conversion,text",
564 accel = "sink",
565 type_resolver(string_type),
566 descriptor(crate::builtins::strings::core::compat::MAT2STR_DESCRIPTOR),
567 builtin_path = "crate::builtins::strings::core::compat"
568)]
569async fn mat2str_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
570 let value = gather_if_needed_async(&value)
571 .await
572 .map_err(map_flow("mat2str"))?;
573 let precision = if let Some(arg) = rest.first() {
574 let arg = gather_if_needed_async(arg)
575 .await
576 .map_err(map_flow("mat2str"))?;
577 Some(parse_nonnegative_usize(&arg, "mat2str")?)
578 } else {
579 None
580 };
581 Ok(Value::String(mat2str_value(&value, precision)))
582}
583
584#[runtime_builtin(
585 name = "native2unicode",
586 category = "strings/core",
587 summary = "Decode native byte values into Unicode text.",
588 keywords = "native2unicode,unicode,encoding,text,uint8",
589 accel = "sink",
590 type_resolver(string_type),
591 descriptor(crate::builtins::strings::core::compat::NATIVE2UNICODE_DESCRIPTOR),
592 builtin_path = "crate::builtins::strings::core::compat"
593)]
594async fn native2unicode_builtin(bytes: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
595 let bytes = gather_if_needed_async(&bytes)
596 .await
597 .map_err(map_flow("native2unicode"))?;
598 let encoding = if let Some(value) = rest.first() {
599 let value = gather_if_needed_async(value)
600 .await
601 .map_err(map_flow("native2unicode"))?;
602 scalar_text(&value, "native2unicode")?
603 } else {
604 "UTF-8".to_string()
605 };
606 let bytes = bytes_from_value(&bytes, "native2unicode")?;
607 decode_bytes(&bytes, &encoding)
608}
609
610#[runtime_builtin(
611 name = "sscanf",
612 category = "strings/core",
613 summary = "Parse formatted numeric values from text.",
614 keywords = "sscanf,scan,format,text,numeric",
615 accel = "sink",
616 type_resolver(tensor_type),
617 descriptor(crate::builtins::strings::core::compat::SSCANF_DESCRIPTOR),
618 builtin_path = "crate::builtins::strings::core::compat"
619)]
620async fn sscanf_builtin(text: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
621 let text = gather_if_needed_async(&text)
622 .await
623 .map_err(map_flow("sscanf"))?;
624 let text = scalar_text(&text, "sscanf")?;
625 let format = if let Some(fmt) = rest.first() {
626 let fmt = gather_if_needed_async(fmt)
627 .await
628 .map_err(map_flow("sscanf"))?;
629 scalar_text(&fmt, "sscanf")?
630 } else {
631 "%f".to_string()
632 };
633 let size = if let Some(size) = rest.get(1) {
634 let size = gather_if_needed_async(size)
635 .await
636 .map_err(map_flow("sscanf"))?;
637 Some(scan_size_from_value(&size)?)
638 } else {
639 None
640 };
641 let scan = sscanf_scan(&text, &format, size)?;
642 match crate::output_count::current_output_count() {
643 Some(0) => Ok(Value::OutputList(Vec::new())),
644 Some(1) => Ok(Value::OutputList(vec![scan.value])),
645 Some(n) => Ok(crate::output_count::output_list_with_padding(
646 n,
647 vec![
648 scan.value,
649 Value::Num(scan.count as f64),
650 Value::String(scan.errmsg),
651 Value::Num(scan.next_index as f64),
652 ],
653 )),
654 None => Ok(scan.value),
655 }
656}
657
658#[runtime_builtin(
659 name = "pattern",
660 category = "strings/pattern",
661 summary = "Create a literal string pattern.",
662 keywords = "pattern,string pattern,text",
663 accel = "metadata",
664 type_resolver(any_type),
665 descriptor(crate::builtins::strings::core::compat::PATTERN_DESCRIPTOR),
666 builtin_path = "crate::builtins::strings::core::compat"
667)]
668async fn pattern_builtin(text: Value) -> BuiltinResult<Value> {
669 let text = gather_if_needed_async(&text)
670 .await
671 .map_err(map_flow("pattern"))?;
672 Ok(pattern_object(®ex::escape(&scalar_text(
673 &text, "pattern",
674 )?)))
675}
676
677#[runtime_builtin(
678 name = "regexpPattern",
679 category = "strings/pattern",
680 summary = "Create a regular expression string pattern.",
681 keywords = "regexpPattern,pattern,regular expression,text",
682 accel = "metadata",
683 type_resolver(any_type),
684 descriptor(crate::builtins::strings::core::compat::REGEXP_PATTERN_DESCRIPTOR),
685 builtin_path = "crate::builtins::strings::core::compat"
686)]
687async fn regexp_pattern_builtin(text: Value) -> BuiltinResult<Value> {
688 let text = gather_if_needed_async(&text)
689 .await
690 .map_err(map_flow("regexpPattern"))?;
691 Ok(pattern_object(&scalar_text(&text, "regexpPattern")?))
692}
693
694#[runtime_builtin(
695 name = "digitsPattern",
696 category = "strings/pattern",
697 summary = "Create a pattern matching digit characters.",
698 keywords = "digitsPattern,pattern,digits,text",
699 accel = "metadata",
700 type_resolver(any_type),
701 descriptor(crate::builtins::strings::core::compat::DIGITS_PATTERN_DESCRIPTOR),
702 builtin_path = "crate::builtins::strings::core::compat"
703)]
704async fn digits_pattern_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
705 bounded_pattern(rest, "\\d", "digitsPattern").await
706}
707
708#[runtime_builtin(
709 name = "lettersPattern",
710 category = "strings/pattern",
711 summary = "Create a pattern matching letter characters.",
712 keywords = "lettersPattern,pattern,letters,text",
713 accel = "metadata",
714 type_resolver(any_type),
715 descriptor(crate::builtins::strings::core::compat::LETTERS_PATTERN_DESCRIPTOR),
716 builtin_path = "crate::builtins::strings::core::compat"
717)]
718async fn letters_pattern_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
719 bounded_pattern(rest, r"\p{Alphabetic}", "lettersPattern").await
720}
721
722#[runtime_builtin(
723 name = "wildcardPattern",
724 category = "strings/pattern",
725 summary = "Create a pattern matching arbitrary text.",
726 keywords = "wildcardPattern,pattern,wildcard,text",
727 accel = "metadata",
728 type_resolver(any_type),
729 descriptor(crate::builtins::strings::core::compat::WILDCARD_PATTERN_DESCRIPTOR),
730 builtin_path = "crate::builtins::strings::core::compat"
731)]
732async fn wildcard_pattern_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
733 if rest.is_empty() {
734 return Ok(pattern_object(".*"));
735 }
736 bounded_pattern(rest, ".", "wildcardPattern").await
737}
738
739#[runtime_builtin(
740 name = "textBoundary",
741 category = "strings/pattern",
742 summary = "Create a pattern matching the start or end of text.",
743 keywords = "textBoundary,pattern,boundary,start,end,text",
744 accel = "metadata",
745 type_resolver(any_type),
746 descriptor(crate::builtins::strings::core::compat::TEXT_BOUNDARY_DESCRIPTOR),
747 builtin_path = "crate::builtins::strings::core::compat"
748)]
749async fn text_boundary_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
750 let boundary_type = match rest.as_slice() {
751 [] => "either".to_string(),
752 [value] => {
753 let value = gather_if_needed_async(value)
754 .await
755 .map_err(map_flow("textBoundary"))?;
756 scalar_text(&value, "textBoundary")?
757 }
758 _ => {
759 return Err(compat_error(
760 "textBoundary",
761 "textBoundary: expected zero inputs or one boundary type",
762 ))
763 }
764 };
765 let regex = match boundary_type.to_ascii_lowercase().as_str() {
766 "either" => r"(?:^|$)",
767 "start" => r"^",
768 "end" => r"$",
769 other => {
770 return Err(compat_error(
771 "textBoundary",
772 format!("textBoundary: unsupported boundary type '{other}'"),
773 ))
774 }
775 };
776 Ok(pattern_object(regex))
777}
778
779pub(crate) fn scalar_text(value: &Value, fn_name: &str) -> BuiltinResult<String> {
780 match value {
781 Value::String(text) => Ok(text.clone()),
782 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
783 Value::CharArray(array) if array.rows == 0 => Ok(String::new()),
784 Value::CharArray(array) if array.rows == 1 => {
785 Ok(char_row_to_string_slice(&array.data, array.cols, 0))
786 }
787 other => Err(compat_error(
788 fn_name,
789 format!("{fn_name}: expected a text scalar, got {other:?}"),
790 )),
791 }
792}
793
794pub(crate) fn pattern_regex(value: &Value, fn_name: &str) -> BuiltinResult<String> {
795 match value {
796 Value::Object(object) if object.is_class(PATTERN_CLASS) => {
797 match object.properties.get("Regex") {
798 Some(Value::String(regex)) => Ok(regex.clone()),
799 _ => Err(compat_error(
800 fn_name,
801 format!("{fn_name}: invalid pattern object"),
802 )),
803 }
804 }
805 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => {
806 Ok(regex::escape(&scalar_text(value, fn_name)?))
807 }
808 other => Err(compat_error(
809 fn_name,
810 format!("{fn_name}: expected text or pattern, got {other:?}"),
811 )),
812 }
813}
814
815pub(crate) fn pattern_object(regex: &str) -> Value {
816 let mut object = ObjectInstance::new(PATTERN_CLASS.to_string());
817 object
818 .properties
819 .insert("Regex".to_string(), Value::String(regex.to_string()));
820 Value::Object(object)
821}
822
823pub(crate) fn text_items(value: Value, fn_name: &str) -> BuiltinResult<TextList> {
824 TextList::from_value(value, fn_name)
825}
826
827pub(crate) fn logical_value(
828 data: Vec<u8>,
829 shape: Vec<usize>,
830 fn_name: &str,
831) -> BuiltinResult<Value> {
832 if data.len() == 1 {
833 Ok(Value::Bool(data[0] != 0))
834 } else {
835 LogicalArray::new(data, shape)
836 .map(Value::LogicalArray)
837 .map_err(|e| compat_error(fn_name, format!("{fn_name}: {e}")))
838 }
839}
840
841pub(crate) struct TextList {
842 pub(crate) items: Vec<Option<String>>,
843 pub(crate) shape: Vec<usize>,
844}
845
846impl TextList {
847 fn from_value(value: Value, fn_name: &str) -> BuiltinResult<Self> {
848 match value {
849 Value::String(text) => Ok(Self {
850 items: vec![missing_to_none(text)],
851 shape: vec![1, 1],
852 }),
853 Value::StringArray(array) => Ok(Self {
854 items: array.data.into_iter().map(missing_to_none).collect(),
855 shape: array.shape,
856 }),
857 Value::CharArray(array) => {
858 let mut items = Vec::with_capacity(array.rows.max(1));
859 if array.rows == 0 {
860 return Ok(Self {
861 items,
862 shape: vec![0, 1],
863 });
864 }
865 for row in 0..array.rows {
866 items.push(Some(char_row_to_string_slice(&array.data, array.cols, row)));
867 }
868 Ok(Self {
869 items,
870 shape: vec![array.rows, 1],
871 })
872 }
873 Value::Cell(cell) => {
874 let mut items = Vec::with_capacity(cell.data.len());
875 for value in cell.data {
876 items.push(Some(scalar_text(&value, fn_name)?));
877 }
878 Ok(Self {
879 items,
880 shape: cell.shape,
881 })
882 }
883 other => Err(compat_error(
884 fn_name,
885 format!("{fn_name}: expected text input, got {other:?}"),
886 )),
887 }
888 }
889}
890
891pub(crate) fn broadcast_shape(
892 a: &[usize],
893 b: &[usize],
894 fn_name: &str,
895) -> BuiltinResult<Vec<usize>> {
896 matlab_broadcast::broadcast_shapes(fn_name, a, b).map_err(|err| compat_error(fn_name, err))
897}
898
899pub(crate) fn broadcast_flat_index(
900 linear: usize,
901 shape: &[usize],
902 source_shape: &[usize],
903) -> usize {
904 if source_shape.iter().product::<usize>() <= 1 {
905 return 0;
906 }
907 let mut extended = Vec::with_capacity(shape.len());
908 extended.extend(std::iter::repeat_n(
909 1,
910 shape.len().saturating_sub(source_shape.len()),
911 ));
912 extended.extend_from_slice(source_shape);
913 let strides = matlab_broadcast::compute_strides(&extended);
914 matlab_broadcast::broadcast_index(linear, shape, &extended, &strides)
915}
916
917fn missing_to_none(text: String) -> Option<String> {
918 if is_missing_string(&text) {
919 None
920 } else {
921 Some(text)
922 }
923}
924
925fn parse_nonnegative_usize(value: &Value, fn_name: &str) -> BuiltinResult<usize> {
926 let number = match value {
927 Value::Num(n) => *n,
928 Value::Int(i) => i.to_i64() as f64,
929 Value::Tensor(tensor) if tensor.data.len() == 1 => tensor.data[0],
930 _ => {
931 return Err(compat_error(
932 fn_name,
933 format!("{fn_name}: expected a nonnegative integer scalar"),
934 ))
935 }
936 };
937 if !number.is_finite() || number < 0.0 || number.fract() != 0.0 || number > usize::MAX as f64 {
938 return Err(compat_error(
939 fn_name,
940 format!("{fn_name}: expected a nonnegative integer scalar"),
941 ));
942 }
943 Ok(number as usize)
944}
945
946fn convert_strings_to_chars(value: Value, contained_only: bool) -> BuiltinResult<Value> {
947 match value {
948 Value::String(text) => Ok(Value::CharArray(CharArray::new_row(&text))),
949 Value::StringArray(array) if array.data.len() == 1 && !contained_only => {
950 Ok(Value::CharArray(CharArray::new_row(&array.data[0])))
951 }
952 Value::StringArray(array) if !contained_only => {
953 let values = array
954 .data
955 .into_iter()
956 .map(|text| Value::CharArray(CharArray::new_row(&text)))
957 .collect();
958 make_cell_with_shape(values, array.shape)
959 .map_err(|e| compat_error("convertStringsToChars", e))
960 }
961 Value::Cell(cell) => {
962 let values = cell
963 .data
964 .into_iter()
965 .map(|value| convert_strings_to_chars(value, true))
966 .collect::<BuiltinResult<Vec<_>>>()?;
967 make_cell_with_shape(values, cell.shape)
968 .map_err(|e| compat_error("convertContainedStringsToChars", e))
969 }
970 Value::Struct(mut st) => {
971 for value in st.fields.values_mut() {
972 *value = convert_strings_to_chars(value.clone(), true)?;
973 }
974 Ok(Value::Struct(st))
975 }
976 other => Ok(other),
977 }
978}
979
980fn convert_chars_to_strings(value: Value) -> BuiltinResult<Value> {
981 match value {
982 Value::CharArray(array) => {
983 let data = (0..array.rows)
984 .map(|row| {
985 char_row_to_string_slice(&array.data, array.cols, row)
986 .trim_end()
987 .to_string()
988 })
989 .collect::<Vec<_>>();
990 StringArray::new(data, vec![array.rows, 1])
991 .map(Value::StringArray)
992 .map_err(|e| compat_error("convertCharsToStrings", e))
993 }
994 Value::Cell(cell) => {
995 let values = cell
996 .data
997 .into_iter()
998 .map(convert_chars_to_strings)
999 .collect::<BuiltinResult<Vec<_>>>()?;
1000 make_cell_with_shape(values, cell.shape)
1001 .map_err(|e| compat_error("convertCharsToStrings", e))
1002 }
1003 other => Ok(other),
1004 }
1005}
1006
1007fn prefix_eq_ignore_case(a: &str, b: &str, n: usize) -> bool {
1008 a.chars()
1009 .take(n)
1010 .map(|ch| ch.to_lowercase().collect::<String>())
1011 .eq(b
1012 .chars()
1013 .take(n)
1014 .map(|ch| ch.to_lowercase().collect::<String>()))
1015 && a.chars().count() >= n
1016 && b.chars().count() >= n
1017}
1018
1019fn classify_text_value(
1020 value: Value,
1021 fn_name: &str,
1022 pred: impl Fn(char) -> bool + Copy,
1023) -> BuiltinResult<Value> {
1024 match value {
1025 Value::String(text) => {
1026 let data = text
1027 .chars()
1028 .map(|ch| u8::from(pred(ch)))
1029 .collect::<Vec<_>>();
1030 logical_value(data, vec![1, text.chars().count()], fn_name)
1031 }
1032 Value::StringArray(array) => {
1033 let values = array
1034 .data
1035 .into_iter()
1036 .map(|text| classify_text_value(Value::String(text), fn_name, pred))
1037 .collect::<BuiltinResult<Vec<_>>>()?;
1038 make_cell_with_shape(values, array.shape).map_err(|e| compat_error(fn_name, e))
1039 }
1040 Value::CharArray(array) => {
1041 let data = array
1042 .data
1043 .iter()
1044 .map(|ch| u8::from(pred(*ch)))
1045 .collect::<Vec<_>>();
1046 logical_value(data, vec![array.rows, array.cols], fn_name)
1047 }
1048 Value::Cell(cell) => {
1049 let values = cell
1050 .data
1051 .into_iter()
1052 .map(|value| classify_text_value(value, fn_name, pred))
1053 .collect::<BuiltinResult<Vec<_>>>()?;
1054 make_cell_with_shape(values, cell.shape).map_err(|e| compat_error(fn_name, e))
1055 }
1056 other => Err(compat_error(
1057 fn_name,
1058 format!("{fn_name}: expected text input, got {other:?}"),
1059 )),
1060 }
1061}
1062
1063fn char_matches_prop(ch: char, prop: &str) -> bool {
1064 match prop {
1065 "alpha" | "letter" | "walpha" => ch.is_alphabetic(),
1066 "alphanum" | "alphanumeric" | "walphanum" => ch.is_alphanumeric(),
1067 "digit" | "wdigit" => ch.is_ascii_digit(),
1068 "xdigit" => ch.is_ascii_hexdigit(),
1069 "space" | "wspace" => ch.is_whitespace(),
1070 "upper" | "wupper" => ch.is_uppercase(),
1071 "lower" | "wlower" => ch.is_lowercase(),
1072 "punct" | "wpunct" => ch.is_ascii_punctuation(),
1073 "cntrl" | "control" => ch.is_control(),
1074 "graphic" | "wgraphic" | "print" | "wprint" => !ch.is_control(),
1075 _ => false,
1076 }
1077}
1078
1079fn map_text_pair_preserve(
1080 value: Value,
1081 fn_name: &str,
1082 map: impl Fn(&str) -> (String, String) + Copy,
1083) -> BuiltinResult<(Value, Value)> {
1084 match value {
1085 Value::String(text) => {
1086 let (first, second) = map(&text);
1087 Ok((Value::String(first), Value::String(second)))
1088 }
1089 Value::StringArray(array) => {
1090 let mut first = Vec::with_capacity(array.data.len());
1091 let mut second = Vec::with_capacity(array.data.len());
1092 for text in array.data {
1093 let (a, b) = map(&text);
1094 first.push(a);
1095 second.push(b);
1096 }
1097 let first = StringArray::new(first, array.shape.clone())
1098 .map(Value::StringArray)
1099 .map_err(|e| compat_error(fn_name, e))?;
1100 let second = StringArray::new(second, array.shape)
1101 .map(Value::StringArray)
1102 .map_err(|e| compat_error(fn_name, e))?;
1103 Ok((first, second))
1104 }
1105 Value::CharArray(array) => {
1106 let mut first = Vec::with_capacity(array.rows);
1107 let mut second = Vec::with_capacity(array.rows);
1108 for row in 0..array.rows {
1109 let (a, b) = map(&char_row_to_string_slice(&array.data, array.cols, row));
1110 first.push(a);
1111 second.push(b);
1112 }
1113 Ok((
1114 char_rows_from_strings(first, fn_name)?,
1115 char_rows_from_strings(second, fn_name)?,
1116 ))
1117 }
1118 Value::Cell(cell) => {
1119 let mut first = Vec::with_capacity(cell.data.len());
1120 let mut second = Vec::with_capacity(cell.data.len());
1121 for value in cell.data {
1122 let (a, b) = map_text_pair_preserve(value, fn_name, map)?;
1123 first.push(a);
1124 second.push(b);
1125 }
1126 Ok((
1127 make_cell_with_shape(first, cell.shape.clone())
1128 .map_err(|e| compat_error(fn_name, e))?,
1129 make_cell_with_shape(second, cell.shape).map_err(|e| compat_error(fn_name, e))?,
1130 ))
1131 }
1132 other => Err(compat_error(
1133 fn_name,
1134 format!("{fn_name}: expected text input, got {other:?}"),
1135 )),
1136 }
1137}
1138
1139fn strtok_pair(text: &str, delimiters: &str) -> (String, String) {
1140 let start = text
1141 .char_indices()
1142 .find_map(|(idx, ch)| (!delimiters.contains(ch)).then_some(idx))
1143 .unwrap_or(text.len());
1144 if start == text.len() {
1145 return (String::new(), String::new());
1146 }
1147 let token_end = text[start..]
1148 .char_indices()
1149 .find_map(|(idx, ch)| delimiters.contains(ch).then_some(start + idx))
1150 .unwrap_or(text.len());
1151 (
1152 text[start..token_end].to_string(),
1153 text[token_end..].to_string(),
1154 )
1155}
1156
1157fn char_rows_from_strings(rows: Vec<String>, fn_name: &str) -> BuiltinResult<Value> {
1158 let row_count = rows.len();
1159 let cols = rows.iter().map(|s| s.chars().count()).max().unwrap_or(0);
1160 let mut data = Vec::with_capacity(row_count * cols);
1161 for row in rows {
1162 let mut chars = row.chars().collect::<Vec<_>>();
1163 chars.resize(cols, ' ');
1164 data.extend(chars);
1165 }
1166 CharArray::new(data, row_count, cols)
1167 .map(Value::CharArray)
1168 .map_err(|e| compat_error(fn_name, e))
1169}
1170
1171fn parse_str2num_matrix(text: &str) -> (Value, bool) {
1172 match parse_numeric_matrix(text, "str2num") {
1173 Ok(value) => (value, true),
1174 Err(_) => (Value::Tensor(Tensor::zeros(vec![0, 0])), false),
1175 }
1176}
1177
1178fn parse_numeric_matrix(text: &str, fn_name: &str) -> BuiltinResult<Value> {
1179 let text = text.trim();
1180 let text = text.strip_prefix('[').unwrap_or(text);
1181 let text = text.strip_suffix(']').unwrap_or(text).trim();
1182 let rows = text
1183 .split(';')
1184 .map(|row| {
1185 row.split(|ch: char| ch.is_whitespace() || ch == ',')
1186 .filter(|part| !part.is_empty())
1187 .map(|part| {
1188 part.parse::<f64>().map_err(|_| {
1189 compat_error(
1190 fn_name,
1191 format!("{fn_name}: invalid numeric literal '{part}'"),
1192 )
1193 })
1194 })
1195 .collect::<BuiltinResult<Vec<_>>>()
1196 })
1197 .collect::<BuiltinResult<Vec<_>>>()?;
1198 if rows.is_empty() || rows.iter().all(Vec::is_empty) {
1199 return Ok(Value::Tensor(Tensor::zeros(vec![0, 0])));
1200 }
1201 let cols = rows.iter().map(Vec::len).max().unwrap_or(0);
1202 if rows.iter().any(|row| row.len() != cols) {
1203 return Err(compat_error(
1204 fn_name,
1205 format!("{fn_name}: rows must have the same number of columns"),
1206 ));
1207 }
1208 let mut data = Vec::with_capacity(rows.len() * cols);
1209 for col in 0..cols {
1210 for row in &rows {
1211 data.push(row[col]);
1212 }
1213 }
1214 Tensor::new(data, vec![rows.len(), cols])
1215 .map(Value::Tensor)
1216 .map_err(|e| compat_error(fn_name, e))
1217}
1218
1219fn mat2str_value(value: &Value, precision: Option<usize>) -> String {
1220 match value {
1221 Value::Num(n) => format_number(*n, precision),
1222 Value::Int(i) => i.to_i64().to_string(),
1223 Value::Bool(b) => {
1224 if *b {
1225 "true".into()
1226 } else {
1227 "false".into()
1228 }
1229 }
1230 Value::String(text) => format!("\"{}\"", text.replace('"', "\"\"")),
1231 Value::CharArray(array) if array.rows <= 1 => {
1232 format!(
1233 "'{}'",
1234 char_row_to_string_slice(&array.data, array.cols, 0).replace('\'', "''")
1235 )
1236 }
1237 Value::Tensor(tensor) => {
1238 matrix_to_string(&tensor.data, tensor.rows(), tensor.cols(), precision)
1239 }
1240 Value::LogicalArray(array) => {
1241 let rows = array.shape.first().copied().unwrap_or(array.data.len());
1242 let cols = array.shape.get(1).copied().unwrap_or(1);
1243 let data = array
1244 .data
1245 .iter()
1246 .map(|v| f64::from(*v != 0))
1247 .collect::<Vec<_>>();
1248 matrix_to_string(&data, rows, cols, precision)
1249 }
1250 _ => value.to_string(),
1251 }
1252}
1253
1254fn matrix_to_string(data: &[f64], rows: usize, cols: usize, precision: Option<usize>) -> String {
1255 let mut out = String::from("[");
1256 for row in 0..rows {
1257 if row > 0 {
1258 out.push(';');
1259 }
1260 for col in 0..cols {
1261 if col > 0 {
1262 out.push(' ');
1263 }
1264 out.push_str(&format_number(data[row + col * rows], precision));
1265 }
1266 }
1267 out.push(']');
1268 out
1269}
1270
1271fn format_number(value: f64, precision: Option<usize>) -> String {
1272 if let Some(precision) = precision {
1273 format!("{value:.precision$}")
1274 } else if value.fract() == 0.0 && value.is_finite() {
1275 format!("{value:.0}")
1276 } else {
1277 format!("{value:.15}")
1278 .trim_end_matches('0')
1279 .trim_end_matches('.')
1280 .to_string()
1281 }
1282}
1283
1284fn bytes_from_value(value: &Value, fn_name: &str) -> BuiltinResult<Vec<u8>> {
1285 match value {
1286 Value::Tensor(tensor) => tensor
1287 .data
1288 .iter()
1289 .map(|n| byte_from_f64(*n, fn_name))
1290 .collect(),
1291 Value::Int(i) => Ok(vec![i.to_i64().clamp(0, 255) as u8]),
1292 Value::Num(n) => Ok(vec![byte_from_f64(*n, fn_name)?]),
1293 Value::CharArray(array) => {
1294 Ok(char_row_to_string_slice(&array.data, array.cols, 0).into_bytes())
1295 }
1296 Value::String(text) => Ok(text.as_bytes().to_vec()),
1297 other => Err(compat_error(
1298 fn_name,
1299 format!("{fn_name}: expected bytes or text, got {other:?}"),
1300 )),
1301 }
1302}
1303
1304fn byte_from_f64(value: f64, fn_name: &str) -> BuiltinResult<u8> {
1305 if !value.is_finite() {
1306 return Err(compat_error(
1307 fn_name,
1308 format!("{fn_name}: byte values must be finite"),
1309 ));
1310 }
1311 Ok(value.round().clamp(0.0, 255.0) as u8)
1312}
1313
1314fn decode_bytes(bytes: &[u8], encoding: &str) -> BuiltinResult<Value> {
1315 let encoding = Encoding::for_label(encoding.as_bytes()).unwrap_or(UTF_8);
1316 let (text, _, _) = encoding.decode(bytes);
1317 Ok(Value::String(text.into_owned()))
1318}
1319
1320struct SscanfResult {
1321 value: Value,
1322 count: usize,
1323 errmsg: String,
1324 next_index: usize,
1325}
1326
1327#[derive(Clone, Copy)]
1328enum ScanKind {
1329 Float,
1330 Integer,
1331 String,
1332 Char,
1333}
1334
1335#[derive(Clone)]
1336enum ScanToken {
1337 Whitespace,
1338 Literal(char),
1339 Spec {
1340 kind: ScanKind,
1341 width: Option<usize>,
1342 suppress: bool,
1343 },
1344}
1345
1346fn sscanf_scan(text: &str, format: &str, size: Option<Vec<usize>>) -> BuiltinResult<SscanfResult> {
1347 let tokens = parse_scan_format(format)?;
1348 if tokens.is_empty() {
1349 return Err(compat_error("sscanf", "sscanf: format must not be empty"));
1350 }
1351
1352 let mut values = Vec::new();
1353 let mut pos = 0usize;
1354 let mut last_success = 0usize;
1355 loop {
1356 let start_pos = pos;
1357 let start_len = values.len();
1358 let mut matched_all = true;
1359 for token in &tokens {
1360 match token {
1361 ScanToken::Whitespace => {
1362 pos = skip_whitespace(text, pos);
1363 }
1364 ScanToken::Literal(ch) => {
1365 let Some(next) = text[pos..].chars().next() else {
1366 matched_all = false;
1367 break;
1368 };
1369 if next != *ch {
1370 matched_all = false;
1371 break;
1372 }
1373 pos += next.len_utf8();
1374 }
1375 ScanToken::Spec {
1376 kind,
1377 width,
1378 suppress,
1379 } => {
1380 if !matches!(kind, ScanKind::Char) {
1381 pos = skip_whitespace(text, pos);
1382 }
1383 let Some((parsed, next_pos)) = scan_one(text, pos, *kind, *width) else {
1384 matched_all = false;
1385 break;
1386 };
1387 pos = next_pos;
1388 if !*suppress {
1389 values.extend(parsed);
1390 }
1391 }
1392 }
1393 }
1394 if !matched_all {
1395 break;
1396 }
1397 if pos == start_pos || values.len() == start_len && pos >= text.len() {
1398 break;
1399 }
1400 last_success = pos;
1401 if pos >= text.len() {
1402 break;
1403 }
1404 }
1405
1406 let count = values.len();
1407 let mut shape = size.unwrap_or_else(|| vec![count, 1]);
1408 let limit = shape.iter().product::<usize>();
1409 if limit > 0 && values.len() > limit {
1410 values.truncate(limit);
1411 }
1412 if shape.iter().product::<usize>() != values.len() {
1413 shape = vec![values.len(), 1];
1414 }
1415 let value = Tensor::new(values, shape)
1416 .map(Value::Tensor)
1417 .map_err(|e| compat_error("sscanf", e))?;
1418 Ok(SscanfResult {
1419 value,
1420 count,
1421 errmsg: String::new(),
1422 next_index: last_success.saturating_add(1),
1423 })
1424}
1425
1426fn parse_scan_format(format: &str) -> BuiltinResult<Vec<ScanToken>> {
1427 let mut chars = format.chars().peekable();
1428 let mut tokens = Vec::new();
1429 while let Some(ch) = chars.next() {
1430 if ch.is_whitespace() {
1431 while chars.peek().is_some_and(|next| next.is_whitespace()) {
1432 chars.next();
1433 }
1434 tokens.push(ScanToken::Whitespace);
1435 continue;
1436 }
1437 if ch != '%' {
1438 tokens.push(ScanToken::Literal(ch));
1439 continue;
1440 }
1441 if chars.peek() == Some(&'%') {
1442 chars.next();
1443 tokens.push(ScanToken::Literal('%'));
1444 continue;
1445 }
1446 let suppress = if chars.peek() == Some(&'*') {
1447 chars.next();
1448 true
1449 } else {
1450 false
1451 };
1452 let mut width = String::new();
1453 while chars.peek().is_some_and(|next| next.is_ascii_digit()) {
1454 width.push(chars.next().unwrap());
1455 }
1456 let width = if width.is_empty() {
1457 None
1458 } else {
1459 Some(
1460 width
1461 .parse::<usize>()
1462 .map_err(|_| compat_error("sscanf", "sscanf: invalid field width"))?,
1463 )
1464 };
1465 let Some(specifier) = chars.next() else {
1466 return Err(compat_error(
1467 "sscanf",
1468 "sscanf: incomplete format specifier",
1469 ));
1470 };
1471 let kind = match specifier {
1472 'f' | 'e' | 'E' | 'g' | 'G' => ScanKind::Float,
1473 'd' | 'i' | 'u' => ScanKind::Integer,
1474 's' => ScanKind::String,
1475 'c' => ScanKind::Char,
1476 other => {
1477 return Err(compat_error(
1478 "sscanf",
1479 format!("sscanf: unsupported format specifier %{other}"),
1480 ))
1481 }
1482 };
1483 tokens.push(ScanToken::Spec {
1484 kind,
1485 width,
1486 suppress,
1487 });
1488 }
1489 Ok(tokens)
1490}
1491
1492fn scan_one(
1493 text: &str,
1494 pos: usize,
1495 kind: ScanKind,
1496 width: Option<usize>,
1497) -> Option<(Vec<f64>, usize)> {
1498 if pos > text.len() {
1499 return None;
1500 }
1501 let end_limit = width
1502 .and_then(|w| byte_index_after_n_chars(&text[pos..], w).map(|idx| pos + idx))
1503 .unwrap_or(text.len());
1504 match kind {
1505 ScanKind::Float | ScanKind::Integer => {
1506 let fragment = &text[pos..end_limit];
1507 let len = numeric_prefix_len(fragment, matches!(kind, ScanKind::Integer))?;
1508 let token = &fragment[..len];
1509 let value = if matches!(kind, ScanKind::Integer) {
1510 token
1511 .parse::<i64>()
1512 .map(|value| value as f64)
1513 .or_else(|_| token.parse::<f64>())
1514 .ok()?
1515 } else {
1516 token.parse::<f64>().ok()?
1517 };
1518 Some((vec![value], pos + len))
1519 }
1520 ScanKind::String => {
1521 let fragment = &text[pos..end_limit];
1522 let len = fragment
1523 .char_indices()
1524 .find_map(|(idx, ch)| ch.is_whitespace().then_some(idx))
1525 .unwrap_or(fragment.len());
1526 if len == 0 {
1527 None
1528 } else {
1529 Some((
1530 fragment[..len].chars().map(|ch| ch as u32 as f64).collect(),
1531 pos + len,
1532 ))
1533 }
1534 }
1535 ScanKind::Char => {
1536 let count = width.unwrap_or(1);
1537 let len = byte_index_after_n_chars(&text[pos..], count)?;
1538 Some((
1539 text[pos..pos + len]
1540 .chars()
1541 .map(|ch| ch as u32 as f64)
1542 .collect(),
1543 pos + len,
1544 ))
1545 }
1546 }
1547}
1548
1549fn numeric_prefix_len(text: &str, integer: bool) -> Option<usize> {
1550 let mut end = 0usize;
1551 for (idx, ch) in text.char_indices() {
1552 let allowed = if integer {
1553 ch.is_ascii_digit() || ((ch == '+' || ch == '-') && idx == 0)
1554 } else {
1555 ch.is_ascii_digit() || matches!(ch, '+' | '-' | '.' | 'e' | 'E')
1556 };
1557 if !allowed {
1558 break;
1559 }
1560 end = idx + ch.len_utf8();
1561 }
1562 (end > 0).then_some(end)
1563}
1564
1565fn skip_whitespace(text: &str, mut pos: usize) -> usize {
1566 while pos < text.len() {
1567 let Some(ch) = text[pos..].chars().next() else {
1568 break;
1569 };
1570 if !ch.is_whitespace() {
1571 break;
1572 }
1573 pos += ch.len_utf8();
1574 }
1575 pos
1576}
1577
1578fn byte_index_after_n_chars(text: &str, count: usize) -> Option<usize> {
1579 if count == 0 {
1580 return Some(0);
1581 }
1582 text.char_indices()
1583 .nth(count)
1584 .map(|(idx, _)| idx)
1585 .or_else(|| (text.chars().count() == count).then_some(text.len()))
1586}
1587
1588fn scan_size_from_value(value: &Value) -> BuiltinResult<Vec<usize>> {
1589 match value {
1590 Value::Num(n) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => Ok(vec![*n as usize, 1]),
1591 Value::Tensor(tensor) if tensor.data.len() == 1 => {
1592 Ok(vec![scan_size_dim(tensor.data[0])?, 1])
1593 }
1594 Value::Tensor(tensor) if tensor.data.len() == 2 => Ok(vec![
1595 scan_size_dim(tensor.data[0])?,
1596 scan_size_dim(tensor.data[1])?,
1597 ]),
1598 other => Err(compat_error(
1599 "sscanf",
1600 format!("sscanf: invalid size argument {other:?}"),
1601 )),
1602 }
1603}
1604
1605fn scan_size_dim(value: f64) -> BuiltinResult<usize> {
1606 if value.is_infinite() && value.is_sign_positive() {
1607 return Ok(usize::MAX / 2);
1608 }
1609 if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
1610 return Err(compat_error(
1611 "sscanf",
1612 "sscanf: size dimensions must be nonnegative integers",
1613 ));
1614 }
1615 Ok(value as usize)
1616}
1617
1618async fn bounded_pattern(
1619 rest: Vec<Value>,
1620 atom: &str,
1621 fn_name: &'static str,
1622) -> BuiltinResult<Value> {
1623 let regex = if let Some(value) = rest.first() {
1624 let value = gather_if_needed_async(value)
1625 .await
1626 .map_err(map_flow(fn_name))?;
1627 let n = parse_nonnegative_usize(&value, fn_name)?;
1628 format!("{atom}{{{n}}}")
1629 } else {
1630 format!("{atom}+")
1631 };
1632 Ok(pattern_object(®ex))
1633}
1634
1635#[cfg(test)]
1636mod tests {
1637 use super::*;
1638 use runmat_builtins::NumericDType;
1639
1640 fn block(
1641 value: impl std::future::Future<Output = BuiltinResult<Value>>,
1642 ) -> BuiltinResult<Value> {
1643 futures::executor::block_on(value)
1644 }
1645
1646 #[test]
1647 fn basic_text_core_helpers_work() {
1648 assert_eq!(newline_builtin().unwrap(), Value::String("\n".into()));
1649 assert_eq!(
1650 block(blanks_builtin(Value::Num(3.0))).unwrap(),
1651 Value::CharArray(CharArray::new_row(" "))
1652 );
1653 assert_eq!(
1654 is_string_scalar_builtin(Value::String("x".into())).unwrap(),
1655 Value::Bool(true)
1656 );
1657 }
1658
1659 #[test]
1660 fn strncmpi_and_classifiers_work() {
1661 assert_eq!(
1662 block(strncmpi_builtin(
1663 Value::String("RunMat".into()),
1664 Value::String("runway".into()),
1665 Value::Num(3.0),
1666 ))
1667 .unwrap(),
1668 Value::Bool(true)
1669 );
1670 assert_eq!(
1671 block(isletter_builtin(Value::CharArray(CharArray::new_row("a1")))).unwrap(),
1672 Value::LogicalArray(LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap())
1673 );
1674 }
1675
1676 #[test]
1677 fn conversions_and_numeric_parsing_work() {
1678 assert_eq!(
1679 block(convert_strings_to_chars_builtin(
1680 Value::String("abc".into()),
1681 Vec::new(),
1682 ))
1683 .unwrap(),
1684 Value::CharArray(CharArray::new_row("abc"))
1685 );
1686 let _guard = crate::output_count::push_output_count(Some(2));
1687 assert!(matches!(
1688 block(convert_strings_to_chars_builtin(
1689 Value::String("a".into()),
1690 vec![Value::String("b".into())],
1691 ))
1692 .unwrap(),
1693 Value::OutputList(outputs) if outputs.len() == 2
1694 ));
1695 drop(_guard);
1696 assert_eq!(
1697 block(convert_chars_to_strings_builtin(Value::CharArray(
1698 CharArray::new_row("abc")
1699 )))
1700 .unwrap(),
1701 Value::StringArray(StringArray::new(vec!["abc".into()], vec![1, 1]).unwrap())
1702 );
1703 assert_eq!(
1704 block(str2num_builtin(Value::String("1 2; 3 4".into()))).unwrap(),
1705 Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap())
1706 );
1707 assert_eq!(
1708 block(mat2str_builtin(
1709 Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap()),
1710 Vec::new(),
1711 ))
1712 .unwrap(),
1713 Value::String("[1 2;3 4]".into())
1714 );
1715 }
1716
1717 #[test]
1718 fn tokenizing_encoding_and_scanning_work() {
1719 assert_eq!(
1720 block(strtok_builtin(
1721 Value::String(" alpha,beta".into()),
1722 vec![Value::String(" ,".into())],
1723 ))
1724 .unwrap(),
1725 Value::String("alpha".into())
1726 );
1727 assert_eq!(
1728 block(native2unicode_builtin(
1729 Value::Tensor(
1730 Tensor::new_with_dtype(vec![104.0, 105.0], vec![1, 2], NumericDType::U8)
1731 .unwrap()
1732 ),
1733 Vec::new(),
1734 ))
1735 .unwrap(),
1736 Value::String("hi".into())
1737 );
1738 assert_eq!(
1739 block(sscanf_builtin(
1740 Value::String("1 2 x".into()),
1741 vec![Value::String("%f".into())],
1742 ))
1743 .unwrap(),
1744 Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap())
1745 );
1746 }
1747
1748 #[test]
1749 fn pattern_constructors_store_regex() {
1750 let value = block(digits_pattern_builtin(vec![Value::Num(2.0)])).unwrap();
1751 assert_eq!(pattern_regex(&value, "test").unwrap(), "\\d{2}");
1752 assert_eq!(
1753 pattern_regex(&block(letters_pattern_builtin(Vec::new())).unwrap(), "test").unwrap(),
1754 r"\p{Alphabetic}+"
1755 );
1756 assert_eq!(
1757 pattern_regex(
1758 &block(wildcard_pattern_builtin(Vec::new())).unwrap(),
1759 "test"
1760 )
1761 .unwrap(),
1762 ".*"
1763 );
1764 assert_eq!(
1765 pattern_regex(&block(text_boundary_builtin(Vec::new())).unwrap(), "test").unwrap(),
1766 r"(?:^|$)"
1767 );
1768 assert_eq!(
1769 pattern_regex(
1770 &block(text_boundary_builtin(vec![Value::String("start".into())])).unwrap(),
1771 "test"
1772 )
1773 .unwrap(),
1774 r"^"
1775 );
1776 assert_eq!(
1777 pattern_regex(
1778 &block(text_boundary_builtin(vec![Value::String("end".into())])).unwrap(),
1779 "test"
1780 )
1781 .unwrap(),
1782 r"$"
1783 );
1784 }
1785
1786 #[test]
1787 fn text_boundary_rejects_invalid_type() {
1788 let err = block(text_boundary_builtin(vec![Value::String("middle".into())]))
1789 .expect_err("expected invalid boundary type");
1790 assert!(err.to_string().contains("unsupported boundary type"));
1791 }
1792
1793 #[test]
1794 fn text_boundary_pattern_works_with_replace() {
1795 let pattern = block(text_boundary_builtin(vec![Value::String("start".into())])).unwrap();
1796 let result = block(crate::call_builtin_async(
1797 "replace",
1798 &[
1799 Value::String("abc".into()),
1800 pattern,
1801 Value::String(">".into()),
1802 ],
1803 ))
1804 .expect("replace");
1805 assert_eq!(result, Value::String(">abc".into()));
1806 }
1807}