1use runmat_builtins::{CellArray, CharArray, StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::spec::{
7 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
8 ReductionNaN, ResidencyPolicy, ShapeRequirements,
9};
10use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
11#[cfg(feature = "doc_export")]
12use crate::register_builtin_doc_text;
13use crate::{gather_if_needed, make_cell, register_builtin_fusion_spec, register_builtin_gpu_spec};
14
15#[cfg(feature = "doc_export")]
16pub const DOC_MD: &str = r###"---
17title: "strip"
18category: "strings/transform"
19keywords: ["strip", "trim", "whitespace", "leading characters", "trailing characters", "character arrays"]
20summary: "Remove leading and trailing characters from strings, character arrays, and cell arrays."
21references:
22 - https://www.mathworks.com/help/matlab/ref/strip.html
23gpu_support:
24 elementwise: false
25 reduction: false
26 precisions: []
27 broadcasting: "none"
28 notes: "Executes on the CPU; GPU-resident inputs are gathered before trimming to match MATLAB semantics."
29fusion:
30 elementwise: false
31 reduction: false
32 max_inputs: 1
33 constants: "inline"
34requires_feature: null
35tested:
36 unit: "builtins::strings::transform::strip::tests"
37 integration: "builtins::strings::transform::strip::tests::strip_cell_array_mixed_content"
38---
39
40# What does the `strip` function do in MATLAB / RunMat?
41`strip(text)` removes consecutive whitespace characters from the beginning and end of `text`. The
42input can be a string scalar, string array, character array, or a cell array of character vectors,
43mirroring MATLAB behaviour. Optional arguments let you control which side to trim (`'left'`,
44`'right'`, or `'both'`) and provide custom characters to remove instead of whitespace.
45
46## How does the `strip` function behave in MATLAB / RunMat?
47- By default, `strip` removes leading and trailing whitespace determined by `isspace`.
48- Direction keywords are case-insensitive. `'left'`/`'leading'` trim the beginning, `'right'`/`'trailing'`
49 trim the end, and `'both'` removes characters on both sides.
50- Provide a second argument containing characters to remove to strip those characters instead of
51 whitespace. Supply a scalar string/char vector to apply the same rule to every element or a string /
52 cell array matching the input size to specify element-wise character sets.
53- Missing string scalars remain `<missing>`.
54- Character arrays shrink or retain their width to match the longest stripped row; shorter rows are
55 padded with spaces so the output stays rectangular.
56- Cell arrays must contain string scalars or character vectors. Results preserve the original cell
57 layout with trimmed elements.
58
59## `strip` Function GPU Execution Behaviour
60`strip` executes on the CPU. When the input or any nested element resides on the GPU, RunMat gathers
61those values to host memory before trimming so the results match MATLAB exactly. Providers do not need
62to implement device kernels for this builtin today.
63
64## GPU residency in RunMat (Do I need `gpuArray`?)
65Text data typically lives on the host. If you deliberately store text on the GPU (for example, by
66keeping character code points in device buffers), RunMat gathers them automatically when `strip` runs.
67You do not need to call `gpuArray` or `gather` manually for this builtin.
68
69## Examples of using the `strip` function in MATLAB / RunMat
70
71### Remove Leading And Trailing Spaces From A String Scalar
72```matlab
73name = " RunMat ";
74clean = strip(name);
75```
76Expected output:
77```matlab
78clean = "RunMat"
79```
80
81### Trim Only The Right Side Of Each String
82```matlab
83labels = [" Alpha "; " Beta "; " Gamma "];
84right_stripped = strip(labels, 'right');
85```
86Expected output:
87```matlab
88right_stripped = 3×1 string
89 " Alpha"
90 " Beta"
91 " Gamma"
92```
93
94### Remove Leading Zeros While Preserving Trailing Digits
95```matlab
96codes = ["00095"; "00137"; "00420"];
97numeric = strip(codes, 'left', '0');
98```
99Expected output:
100```matlab
101numeric = 3×1 string
102 "95"
103 "137"
104 "420"
105```
106
107### Strip Character Arrays And Preserve Rectangular Shape
108```matlab
109animals = char(" cat ", " dog", "cow ");
110trimmed = strip(animals);
111```
112Expected output:
113```matlab
114trimmed =
115
116 3×4 char array
117
118 'cat '
119 'dog '
120 'cow '
121```
122
123### Supply Per-Element Characters To Remove
124```matlab
125metrics = ["##pass##", "--warn--", "**fail**"];
126per_char = ["#"; "-"; "*"];
127normalized = strip(metrics, 'both', per_char);
128```
129Expected output:
130```matlab
131normalized = 3×1 string
132 "pass"
133 "warn"
134 "fail"
135```
136
137### Trim Cell Array Elements With Mixed Types
138```matlab
139pieces = {' GPU ', " Accelerate", 'RunMat '};
140out = strip(pieces);
141```
142Expected output:
143```matlab
144out = 1×3 cell array
145 {'GPU'} {"Accelerate"} {'RunMat'}
146```
147
148## FAQ
149
150### Which direction keywords are supported?
151`'left'` and `'leading'` trim the beginning of the text, `'right'` and `'trailing'` trim the end, and
152`'both'` (the default) trims both sides.
153
154### How do I remove characters other than whitespace?
155Provide a second argument containing the characters to remove, for example `strip(str, "xyz")` removes
156any leading or trailing `x`, `y`, or `z` characters. Combine it with a direction argument to control
157which side is affected.
158
159### Can I specify different characters for each element?
160Yes. Pass a string array or cell array of character vectors that matches the size of the input. Each
161element is trimmed using the corresponding character set.
162
163### What happens to missing strings?
164Missing string scalars (`string(missing)`) remain `<missing>` exactly as in MATLAB.
165
166### Does `strip` change the shape of character arrays?
167Only the width can change. `strip` keeps the same number of rows and pads shorter rows with spaces so
168the array stays rectangular.
169
170### Will `strip` run on the GPU?
171Not currently. RunMat gathers GPU-resident inputs automatically and performs trimming on the CPU to
172maintain MATLAB compatibility.
173
174## See Also
175[lower](./lower), [upper](./upper), [string](../core/string), [char](../core/char), [compose](../core/compose)
176
177## Source & Feedback
178- Implementation: [`crates/runmat-runtime/src/builtins/strings/transform/strip.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/strings/transform/strip.rs)
179- Found an issue? Please [open a GitHub issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal reproduction.
180"###;
181
182pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
183 name: "strip",
184 op_kind: GpuOpKind::Custom("string-transform"),
185 supported_precisions: &[],
186 broadcast: BroadcastSemantics::None,
187 provider_hooks: &[],
188 constant_strategy: ConstantStrategy::InlineLiteral,
189 residency: ResidencyPolicy::GatherImmediately,
190 nan_mode: ReductionNaN::Include,
191 two_pass_threshold: None,
192 workgroup_size: None,
193 accepts_nan_mode: false,
194 notes:
195 "Executes on the CPU; GPU-resident inputs are gathered to host memory before trimming characters.",
196};
197
198register_builtin_gpu_spec!(GPU_SPEC);
199
200pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
201 name: "strip",
202 shape: ShapeRequirements::Any,
203 constant_strategy: ConstantStrategy::InlineLiteral,
204 elementwise: None,
205 reduction: None,
206 emits_nan: false,
207 notes: "String transformation builtin; not eligible for fusion and always gathers GPU inputs.",
208};
209
210register_builtin_fusion_spec!(FUSION_SPEC);
211
212#[cfg(feature = "doc_export")]
213register_builtin_doc_text!("strip", DOC_MD);
214
215const ARG_TYPE_ERROR: &str =
216 "strip: first argument must be a string array, character array, or cell array of character vectors";
217const CELL_ELEMENT_ERROR: &str =
218 "strip: cell array elements must be string scalars or character vectors";
219const DIRECTION_ERROR: &str = "strip: direction must be 'left', 'right', or 'both'";
220const CHARACTERS_ERROR: &str =
221 "strip: characters to remove must be a string array, character vector, or cell array of character vectors";
222const SIZE_MISMATCH_ERROR: &str =
223 "strip: stripCharacters must be the same size as the input when supplying multiple values";
224
225#[derive(Clone, Copy, Eq, PartialEq)]
226enum StripDirection {
227 Both,
228 Left,
229 Right,
230}
231
232enum PatternSpec {
233 Default,
234 Scalar(Vec<char>),
235 PerElement(Vec<Vec<char>>),
236}
237
238enum PatternRef<'a> {
239 Default,
240 Custom(&'a [char]),
241}
242
243#[derive(Clone)]
244struct PatternExpectation {
245 len: usize,
246 shape: Option<Vec<usize>>,
247}
248
249impl PatternExpectation {
250 fn scalar() -> Self {
251 Self {
252 len: 1,
253 shape: None,
254 }
255 }
256
257 fn with_len(len: usize) -> Self {
258 Self { len, shape: None }
259 }
260
261 fn with_shape(len: usize, shape: &[usize]) -> Self {
262 Self {
263 len,
264 shape: Some(shape.to_vec()),
265 }
266 }
267
268 fn len(&self) -> usize {
269 self.len
270 }
271
272 fn shape(&self) -> Option<&[usize]> {
273 self.shape.as_deref()
274 }
275}
276
277impl PatternSpec {
278 fn pattern_for_index(&self, idx: usize) -> PatternRef<'_> {
279 match self {
280 PatternSpec::Default => PatternRef::Default,
281 PatternSpec::Scalar(chars) => PatternRef::Custom(chars),
282 PatternSpec::PerElement(patterns) => patterns
283 .get(idx)
284 .map(|chars| PatternRef::Custom(chars))
285 .unwrap_or(PatternRef::Default),
286 }
287 }
288}
289
290#[runtime_builtin(
291 name = "strip",
292 category = "strings/transform",
293 summary = "Remove leading and trailing characters from strings, character arrays, and cell arrays.",
294 keywords = "strip,trim,strings,character array,text",
295 accel = "sink"
296)]
297fn strip_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
298 let gathered = gather_if_needed(&value).map_err(|e| format!("strip: {e}"))?;
299 match gathered {
300 Value::String(text) => strip_string(text, &rest),
301 Value::StringArray(array) => strip_string_array(array, &rest),
302 Value::CharArray(array) => strip_char_array(array, &rest),
303 Value::Cell(cell) => strip_cell_array(cell, &rest),
304 _ => Err(ARG_TYPE_ERROR.to_string()),
305 }
306}
307
308fn strip_string(text: String, args: &[Value]) -> Result<Value, String> {
309 if is_missing_string(&text) {
310 return Ok(Value::String(text));
311 }
312 let expectation = PatternExpectation::scalar();
313 let (direction, pattern_spec) = parse_arguments(args, &expectation)?;
314 let stripped = strip_text(&text, direction, pattern_spec.pattern_for_index(0));
315 Ok(Value::String(stripped))
316}
317
318fn strip_string_array(array: StringArray, args: &[Value]) -> Result<Value, String> {
319 let expected_len = array.data.len();
320 let expectation = PatternExpectation::with_shape(expected_len, &array.shape);
321 let (direction, pattern_spec) = parse_arguments(args, &expectation)?;
322 let StringArray { data, shape, .. } = array;
323 let mut stripped: Vec<String> = Vec::with_capacity(expected_len);
324 for (idx, text) in data.into_iter().enumerate() {
325 if is_missing_string(&text) {
326 stripped.push(text);
327 } else {
328 let pattern = pattern_spec.pattern_for_index(idx);
329 stripped.push(strip_text(&text, direction, pattern));
330 }
331 }
332 let result = StringArray::new(stripped, shape).map_err(|e| format!("strip: {e}"))?;
333 Ok(Value::StringArray(result))
334}
335
336fn strip_char_array(array: CharArray, args: &[Value]) -> Result<Value, String> {
337 let CharArray { data, rows, cols } = array;
338 let expectation = PatternExpectation::with_len(rows);
339 let (direction, pattern_spec) = parse_arguments(args, &expectation)?;
340
341 if rows == 0 {
342 return Ok(Value::CharArray(CharArray { data, rows, cols }));
343 }
344
345 let mut stripped_rows: Vec<String> = Vec::with_capacity(rows);
346 let mut target_cols: usize = 0;
347 for row in 0..rows {
348 let text = char_row_to_string_slice(&data, cols, row);
349 let pattern = pattern_spec.pattern_for_index(row);
350 let stripped = strip_text(&text, direction, pattern);
351 let len = stripped.chars().count();
352 target_cols = target_cols.max(len);
353 stripped_rows.push(stripped);
354 }
355
356 let mut new_data: Vec<char> = Vec::with_capacity(rows * target_cols);
357 for row_text in stripped_rows {
358 let mut chars: Vec<char> = row_text.chars().collect();
359 if chars.len() < target_cols {
360 chars.resize(target_cols, ' ');
361 }
362 new_data.extend(chars.into_iter());
363 }
364
365 CharArray::new(new_data, rows, target_cols)
366 .map(Value::CharArray)
367 .map_err(|e| format!("strip: {e}"))
368}
369
370fn strip_cell_array(cell: CellArray, args: &[Value]) -> Result<Value, String> {
371 let rows = cell.rows;
372 let cols = cell.cols;
373 let dims = [rows, cols];
374 let expectation = PatternExpectation::with_shape(rows * cols, &dims);
375 let (direction, pattern_spec) = parse_arguments(args, &expectation)?;
376 let total = rows * cols;
377 let mut stripped_values: Vec<Value> = Vec::with_capacity(total);
378 for idx in 0..total {
379 let value = &cell.data[idx];
380 let pattern = pattern_spec.pattern_for_index(idx);
381 let stripped = strip_cell_element(value, direction, pattern)?;
382 stripped_values.push(stripped);
383 }
384 make_cell(stripped_values, rows, cols).map_err(|e| format!("strip: {e}"))
385}
386
387fn strip_cell_element(
388 value: &Value,
389 direction: StripDirection,
390 pattern: PatternRef<'_>,
391) -> Result<Value, String> {
392 let gathered = gather_if_needed(value).map_err(|e| format!("strip: {e}"))?;
393 match gathered {
394 Value::String(text) => {
395 if is_missing_string(&text) {
396 Ok(Value::String(text))
397 } else {
398 let stripped = strip_text(&text, direction, pattern);
399 Ok(Value::String(stripped))
400 }
401 }
402 Value::StringArray(sa) if sa.data.len() == 1 => {
403 let text = sa.data.into_iter().next().unwrap();
404 if is_missing_string(&text) {
405 Ok(Value::String(text))
406 } else {
407 let stripped = strip_text(&text, direction, pattern);
408 Ok(Value::String(stripped))
409 }
410 }
411 Value::CharArray(ca) if ca.rows <= 1 => {
412 let source = if ca.rows == 0 {
413 String::new()
414 } else {
415 char_row_to_string_slice(&ca.data, ca.cols, 0)
416 };
417 let stripped = strip_text(&source, direction, pattern);
418 let len = stripped.chars().count();
419 let data: Vec<char> = stripped.chars().collect();
420 let rows = ca.rows;
421 let cols = if rows == 0 { ca.cols } else { len };
422 CharArray::new(data, rows, cols)
423 .map(Value::CharArray)
424 .map_err(|e| format!("strip: {e}"))
425 }
426 Value::CharArray(_) => Err(CELL_ELEMENT_ERROR.to_string()),
427 _ => Err(CELL_ELEMENT_ERROR.to_string()),
428 }
429}
430
431fn parse_arguments(
432 args: &[Value],
433 expectation: &PatternExpectation,
434) -> Result<(StripDirection, PatternSpec), String> {
435 match args.len() {
436 0 => Ok((StripDirection::Both, PatternSpec::Default)),
437 1 => {
438 if let Some(direction) = try_parse_direction(&args[0], false)? {
439 Ok((direction, PatternSpec::Default))
440 } else {
441 let pattern = parse_pattern(&args[0], expectation)?;
442 Ok((StripDirection::Both, pattern))
443 }
444 }
445 2 => {
446 let direction = match try_parse_direction(&args[0], true)? {
447 Some(dir) => dir,
448 None => return Err(DIRECTION_ERROR.to_string()),
449 };
450 let pattern = parse_pattern(&args[1], expectation)?;
451 Ok((direction, pattern))
452 }
453 _ => Err("strip: too many input arguments".to_string()),
454 }
455}
456
457fn try_parse_direction(value: &Value, strict: bool) -> Result<Option<StripDirection>, String> {
458 let Some(text) = value_to_single_string(value) else {
459 return Ok(None);
460 };
461 let trimmed = text.trim();
462 if trimmed.is_empty() {
463 return if strict {
464 Err(DIRECTION_ERROR.to_string())
465 } else {
466 Ok(None)
467 };
468 }
469 let lowered = trimmed.to_ascii_lowercase();
470 let direction = match lowered.as_str() {
471 "both" => Some(StripDirection::Both),
472 "left" | "leading" => Some(StripDirection::Left),
473 "right" | "trailing" => Some(StripDirection::Right),
474 _ => {
475 if strict {
476 return Err(DIRECTION_ERROR.to_string());
477 }
478 None
479 }
480 };
481 Ok(direction)
482}
483
484fn value_to_single_string(value: &Value) -> Option<String> {
485 match value {
486 Value::String(text) => Some(text.clone()),
487 Value::StringArray(sa) => {
488 if sa.data.len() == 1 {
489 Some(sa.data[0].clone())
490 } else {
491 None
492 }
493 }
494 Value::CharArray(ca) => {
495 if ca.rows <= 1 {
496 Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
497 } else {
498 None
499 }
500 }
501 _ => None,
502 }
503}
504
505fn parse_pattern(value: &Value, expectation: &PatternExpectation) -> Result<PatternSpec, String> {
506 let expected_len = expectation.len();
507 match value {
508 Value::String(text) => Ok(PatternSpec::Scalar(text.chars().collect())),
509 Value::StringArray(sa) => {
510 if sa.data.len() <= 1 {
511 if let Some(first) = sa.data.first() {
512 Ok(PatternSpec::Scalar(first.chars().collect()))
513 } else {
514 Ok(PatternSpec::Scalar(Vec::new()))
515 }
516 } else if sa.data.len() == expected_len {
517 if let Some(shape) = expectation.shape() {
518 if sa.shape != shape {
519 return Err(SIZE_MISMATCH_ERROR.to_string());
520 }
521 }
522 let mut patterns = Vec::with_capacity(sa.data.len());
523 for text in &sa.data {
524 patterns.push(text.chars().collect());
525 }
526 Ok(PatternSpec::PerElement(patterns))
527 } else {
528 Err(SIZE_MISMATCH_ERROR.to_string())
529 }
530 }
531 Value::CharArray(ca) => {
532 if ca.rows <= 1 {
533 if ca.rows == 0 {
534 Ok(PatternSpec::Scalar(Vec::new()))
535 } else {
536 let chars = char_row_to_string_slice(&ca.data, ca.cols, 0);
537 Ok(PatternSpec::Scalar(chars.chars().collect()))
538 }
539 } else if ca.rows == expected_len {
540 let mut patterns = Vec::with_capacity(ca.rows);
541 for row in 0..ca.rows {
542 let text = char_row_to_string_slice(&ca.data, ca.cols, row);
543 patterns.push(text.chars().collect());
544 }
545 Ok(PatternSpec::PerElement(patterns))
546 } else {
547 Err(SIZE_MISMATCH_ERROR.to_string())
548 }
549 }
550 Value::Cell(cell) => parse_pattern_cell(cell, expectation),
551 _ => Err(CHARACTERS_ERROR.to_string()),
552 }
553}
554
555fn parse_pattern_cell(
556 cell: &CellArray,
557 expectation: &PatternExpectation,
558) -> Result<PatternSpec, String> {
559 let len = cell.rows * cell.cols;
560 if len == 0 {
561 return Ok(PatternSpec::Scalar(Vec::new()));
562 }
563 if len == 1 {
564 let chars = pattern_chars_from_value(&cell.data[0])?;
565 return Ok(PatternSpec::Scalar(chars));
566 }
567 if len != expectation.len() {
568 return Err(SIZE_MISMATCH_ERROR.to_string());
569 }
570 if let Some(shape) = expectation.shape() {
571 match shape.len() {
572 0 => {}
573 1 => {
574 if cell.rows != shape[0] || cell.cols != 1 {
575 return Err(SIZE_MISMATCH_ERROR.to_string());
576 }
577 }
578 _ => {
579 if cell.rows != shape[0] || cell.cols != shape[1] {
580 return Err(SIZE_MISMATCH_ERROR.to_string());
581 }
582 }
583 }
584 }
585 let mut patterns = Vec::with_capacity(len);
586 for value in &cell.data {
587 patterns.push(pattern_chars_from_value(value)?);
588 }
589 Ok(PatternSpec::PerElement(patterns))
590}
591
592fn pattern_chars_from_value(value: &Value) -> Result<Vec<char>, String> {
593 let gathered = gather_if_needed(value).map_err(|e| format!("strip: {e}"))?;
594 match gathered {
595 Value::String(text) => Ok(text.chars().collect()),
596 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].chars().collect()),
597 Value::CharArray(ca) if ca.rows <= 1 => {
598 if ca.rows == 0 {
599 Ok(Vec::new())
600 } else {
601 let text = char_row_to_string_slice(&ca.data, ca.cols, 0);
602 Ok(text.chars().collect())
603 }
604 }
605 Value::CharArray(_) => Err(CHARACTERS_ERROR.to_string()),
606 _ => Err(CHARACTERS_ERROR.to_string()),
607 }
608}
609
610fn strip_text(text: &str, direction: StripDirection, pattern: PatternRef<'_>) -> String {
611 match pattern {
612 PatternRef::Default => strip_text_with_predicate(text, direction, char::is_whitespace),
613 PatternRef::Custom(chars) => {
614 strip_text_with_predicate(text, direction, |c| chars.contains(&c))
615 }
616 }
617}
618
619fn strip_text_with_predicate<F>(text: &str, direction: StripDirection, mut predicate: F) -> String
620where
621 F: FnMut(char) -> bool,
622{
623 let chars: Vec<char> = text.chars().collect();
624 if chars.is_empty() {
625 return String::new();
626 }
627
628 let mut start = 0usize;
629 let mut end = chars.len();
630
631 if direction != StripDirection::Right {
632 while start < end && predicate(chars[start]) {
633 start += 1;
634 }
635 }
636
637 if direction != StripDirection::Left {
638 while end > start && predicate(chars[end - 1]) {
639 end -= 1;
640 }
641 }
642
643 chars[start..end].iter().collect()
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 #[cfg(feature = "doc_export")]
650 use crate::builtins::common::test_support;
651
652 #[test]
653 fn strip_string_scalar_default() {
654 let result = strip_builtin(Value::String(" RunMat ".into()), Vec::new()).expect("strip");
655 assert_eq!(result, Value::String("RunMat".into()));
656 }
657
658 #[test]
659 fn strip_string_scalar_direction() {
660 let result = strip_builtin(
661 Value::String("...data".into()),
662 vec![Value::String("left".into()), Value::String(".".into())],
663 )
664 .expect("strip");
665 assert_eq!(result, Value::String("data".into()));
666 }
667
668 #[test]
669 fn strip_string_scalar_custom_characters() {
670 let result = strip_builtin(
671 Value::String("00052".into()),
672 vec![Value::String("left".into()), Value::String("0".into())],
673 )
674 .expect("strip");
675 assert_eq!(result, Value::String("52".into()));
676 }
677
678 #[test]
679 fn strip_string_scalar_pattern_only() {
680 let result = strip_builtin(
681 Value::String("xxaccelerationxx".into()),
682 vec![Value::String("x".into())],
683 )
684 .expect("strip");
685 assert_eq!(result, Value::String("acceleration".into()));
686 }
687
688 #[test]
689 fn strip_empty_pattern_returns_original() {
690 let result = strip_builtin(
691 Value::String("abc".into()),
692 vec![Value::String(String::new())],
693 )
694 .expect("strip");
695 assert_eq!(result, Value::String("abc".into()));
696 }
697
698 #[test]
699 fn strip_supports_leading_synonym() {
700 let result = strip_builtin(
701 Value::String(" data".into()),
702 vec![Value::String("leading".into())],
703 )
704 .expect("strip");
705 assert_eq!(result, Value::String("data".into()));
706 }
707
708 #[test]
709 fn strip_supports_trailing_synonym() {
710 let result = strip_builtin(
711 Value::String("data ".into()),
712 vec![Value::String("trailing".into())],
713 )
714 .expect("strip");
715 assert_eq!(result, Value::String("data".into()));
716 }
717
718 #[test]
719 fn strip_string_array_per_element_characters() {
720 let strings = StringArray::new(
721 vec!["##ok##".into(), "--warn--".into(), "**fail**".into()],
722 vec![3, 1],
723 )
724 .unwrap();
725 let chars = CharArray::new(vec!['#', '#', '-', '-', '*', '*'], 3, 2).unwrap();
726 let result = strip_builtin(
727 Value::StringArray(strings),
728 vec![Value::String("both".into()), Value::CharArray(chars)],
729 )
730 .expect("strip");
731 match result {
732 Value::StringArray(sa) => {
733 assert_eq!(
734 sa.data,
735 vec![
736 String::from("ok"),
737 String::from("warn"),
738 String::from("fail")
739 ]
740 );
741 }
742 other => panic!("expected string array, got {other:?}"),
743 }
744 }
745
746 #[test]
747 fn strip_string_array_cell_pattern_per_element() {
748 let strings =
749 StringArray::new(vec!["__pass__".into(), "--warn--".into()], vec![2, 1]).unwrap();
750 let patterns = CellArray::new(
751 vec![Value::String("_".into()), Value::String("-".into())],
752 2,
753 1,
754 )
755 .unwrap();
756 let result =
757 strip_builtin(Value::StringArray(strings), vec![Value::Cell(patterns)]).expect("strip");
758 match result {
759 Value::StringArray(sa) => {
760 assert_eq!(sa.data, vec![String::from("pass"), String::from("warn")]);
761 }
762 other => panic!("expected string array, got {other:?}"),
763 }
764 }
765
766 #[test]
767 fn strip_string_array_preserves_missing() {
768 let strings =
769 StringArray::new(vec![" data ".into(), "<missing>".into()], vec![2, 1]).unwrap();
770 let result = strip_builtin(Value::StringArray(strings), Vec::new()).expect("strip");
771 match result {
772 Value::StringArray(sa) => {
773 assert_eq!(sa.data[0], "data");
774 assert_eq!(sa.data[1], "<missing>");
775 }
776 other => panic!("expected string array, got {other:?}"),
777 }
778 }
779
780 #[test]
781 fn strip_char_array_shrinks_width() {
782 let source = " cat dog ";
783 let chars: Vec<char> = source.chars().collect();
784 let array = CharArray::new(chars, 1, source.chars().count()).unwrap();
785 let result = strip_builtin(Value::CharArray(array), Vec::new()).expect("strip");
786 match result {
787 Value::CharArray(ca) => {
788 assert_eq!(ca.rows, 1);
789 assert_eq!(ca.cols, 8);
790 let expected: Vec<char> = "cat dog".chars().collect();
791 assert_eq!(ca.data, expected);
792 }
793 other => panic!("expected char array, got {other:?}"),
794 }
795 }
796
797 #[test]
798 fn strip_char_array_supports_trailing_direction() {
799 let array = CharArray::new_row("gpu ");
800 let result = strip_builtin(
801 Value::CharArray(array),
802 vec![Value::String("trailing".into())],
803 )
804 .expect("strip");
805 match result {
806 Value::CharArray(ca) => {
807 assert_eq!(ca.rows, 1);
808 assert_eq!(ca.cols, 3);
809 let expected: Vec<char> = "gpu".chars().collect();
810 assert_eq!(ca.data, expected);
811 }
812 other => panic!("expected char array, got {other:?}"),
813 }
814 }
815
816 #[test]
817 fn strip_cell_array_mixed_content() {
818 let cell = CellArray::new(
819 vec![
820 Value::CharArray(CharArray::new_row(" GPU ")),
821 Value::String(" Accelerate".into()),
822 Value::String("RunMat ".into()),
823 ],
824 1,
825 3,
826 )
827 .unwrap();
828 let result = strip_builtin(Value::Cell(cell), Vec::new()).expect("strip");
829 match result {
830 Value::Cell(out) => {
831 assert_eq!(out.rows, 1);
832 assert_eq!(out.cols, 3);
833 assert_eq!(
834 out.get(0, 0).unwrap(),
835 Value::CharArray(CharArray::new_row("GPU"))
836 );
837 assert_eq!(out.get(0, 1).unwrap(), Value::String("Accelerate".into()));
838 assert_eq!(out.get(0, 2).unwrap(), Value::String("RunMat".into()));
839 }
840 other => panic!("expected cell array, got {other:?}"),
841 }
842 }
843
844 #[test]
845 fn strip_preserves_missing_string() {
846 let result = strip_builtin(Value::String("<missing>".into()), Vec::new()).expect("strip");
847 assert_eq!(result, Value::String("<missing>".into()));
848 }
849
850 #[test]
851 fn strip_errors_on_invalid_input() {
852 let err = strip_builtin(Value::Num(1.0), Vec::new()).unwrap_err();
853 assert_eq!(err, ARG_TYPE_ERROR);
854 }
855
856 #[test]
857 fn strip_errors_on_invalid_pattern_type() {
858 let err = strip_builtin(Value::String("abc".into()), vec![Value::Num(1.0)]).unwrap_err();
859 assert_eq!(err, CHARACTERS_ERROR);
860 }
861
862 #[test]
863 fn strip_errors_on_invalid_direction() {
864 let err = strip_builtin(
865 Value::String("abc".into()),
866 vec![Value::String("sideways".into()), Value::String("a".into())],
867 )
868 .unwrap_err();
869 assert_eq!(err, DIRECTION_ERROR);
870 }
871
872 #[test]
873 fn strip_errors_on_pattern_size_mismatch() {
874 let strings = StringArray::new(vec!["one".into(), "two".into()], vec![2, 1]).unwrap();
875 let pattern =
876 StringArray::new(vec!["x".into(), "y".into(), "z".into()], vec![3, 1]).unwrap();
877 let err = strip_builtin(
878 Value::StringArray(strings),
879 vec![Value::StringArray(pattern)],
880 )
881 .unwrap_err();
882 assert_eq!(err, SIZE_MISMATCH_ERROR);
883 }
884
885 #[test]
886 fn strip_errors_on_pattern_shape_mismatch() {
887 let strings = StringArray::new(vec!["one".into(), "two".into()], vec![1, 2]).unwrap();
888 let pattern = StringArray::new(vec!["x".into(), "y".into()], vec![2, 1]).unwrap();
889 let err = strip_builtin(
890 Value::StringArray(strings),
891 vec![Value::StringArray(pattern)],
892 )
893 .unwrap_err();
894 assert_eq!(err, SIZE_MISMATCH_ERROR);
895 }
896
897 #[test]
898 fn strip_errors_on_cell_pattern_shape_mismatch() {
899 let strings = StringArray::new(vec!["aa".into(), "bb".into()], vec![1, 2]).unwrap();
900 let cell_pattern = CellArray::new(
901 vec![Value::String("a".into()), Value::String("b".into())],
902 2,
903 1,
904 )
905 .unwrap();
906 let err = strip_builtin(Value::StringArray(strings), vec![Value::Cell(cell_pattern)])
907 .unwrap_err();
908 assert_eq!(err, SIZE_MISMATCH_ERROR);
909 }
910
911 #[test]
912 fn strip_errors_on_too_many_arguments() {
913 let err = strip_builtin(
914 Value::String("abc".into()),
915 vec![
916 Value::String("both".into()),
917 Value::String("a".into()),
918 Value::String("b".into()),
919 ],
920 )
921 .unwrap_err();
922 assert_eq!(err, "strip: too many input arguments");
923 }
924
925 #[test]
926 #[cfg(feature = "wgpu")]
927 fn strip_gpu_tensor_errors() {
928 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
929 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
930 );
931 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
932 let host_data = [1.0f64, 2.0];
933 let host_shape = [2usize, 1usize];
934 let handle = provider
935 .upload(&runmat_accelerate_api::HostTensorView {
936 data: &host_data,
937 shape: &host_shape,
938 })
939 .expect("upload");
940 let err = strip_builtin(Value::GpuTensor(handle.clone()), Vec::new()).unwrap_err();
941 assert_eq!(err, ARG_TYPE_ERROR);
942 provider.free(&handle).ok();
943 }
944
945 #[test]
946 #[cfg(feature = "doc_export")]
947 fn doc_examples_present() {
948 let blocks = test_support::doc_examples(DOC_MD);
949 assert!(!blocks.is_empty());
950 }
951}