Skip to main content

runmat_runtime/builtins/strings/transform/
strip.rs

1//! MATLAB-compatible `strip` builtin with GPU-aware semantics for RunMat.
2
3use runmat_builtins::{CellArray, CharArray, StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::map_control_flow_with_builtin;
7use crate::builtins::common::spec::{
8    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
9    ReductionNaN, ResidencyPolicy, ShapeRequirements,
10};
11use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
12use crate::builtins::strings::type_resolvers::text_preserve_type;
13use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
14
15#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::strip")]
16pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
17    name: "strip",
18    op_kind: GpuOpKind::Custom("string-transform"),
19    supported_precisions: &[],
20    broadcast: BroadcastSemantics::None,
21    provider_hooks: &[],
22    constant_strategy: ConstantStrategy::InlineLiteral,
23    residency: ResidencyPolicy::GatherImmediately,
24    nan_mode: ReductionNaN::Include,
25    two_pass_threshold: None,
26    workgroup_size: None,
27    accepts_nan_mode: false,
28    notes:
29        "Executes on the CPU; GPU-resident inputs are gathered to host memory before trimming characters.",
30};
31
32#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::strip")]
33pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
34    name: "strip",
35    shape: ShapeRequirements::Any,
36    constant_strategy: ConstantStrategy::InlineLiteral,
37    elementwise: None,
38    reduction: None,
39    emits_nan: false,
40    notes: "String transformation builtin; not eligible for fusion and always gathers GPU inputs.",
41};
42
43const BUILTIN_NAME: &str = "strip";
44const ARG_TYPE_ERROR: &str =
45    "strip: first argument must be a string array, character array, or cell array of character vectors";
46const CELL_ELEMENT_ERROR: &str =
47    "strip: cell array elements must be string scalars or character vectors";
48const DIRECTION_ERROR: &str = "strip: direction must be 'left', 'right', or 'both'";
49const CHARACTERS_ERROR: &str =
50    "strip: characters to remove must be a string array, character vector, or cell array of character vectors";
51const SIZE_MISMATCH_ERROR: &str =
52    "strip: stripCharacters must be the same size as the input when supplying multiple values";
53
54fn runtime_error_for(message: impl Into<String>) -> RuntimeError {
55    build_runtime_error(message)
56        .with_builtin(BUILTIN_NAME)
57        .build()
58}
59
60fn map_flow(err: RuntimeError) -> RuntimeError {
61    map_control_flow_with_builtin(err, BUILTIN_NAME)
62}
63
64#[derive(Clone, Copy, Eq, PartialEq)]
65enum StripDirection {
66    Both,
67    Left,
68    Right,
69}
70
71enum PatternSpec {
72    Default,
73    Scalar(Vec<char>),
74    PerElement(Vec<Vec<char>>),
75}
76
77enum PatternRef<'a> {
78    Default,
79    Custom(&'a [char]),
80}
81
82#[derive(Clone)]
83struct PatternExpectation {
84    len: usize,
85    shape: Option<Vec<usize>>,
86}
87
88impl PatternExpectation {
89    fn scalar() -> Self {
90        Self {
91            len: 1,
92            shape: None,
93        }
94    }
95
96    fn with_len(len: usize) -> Self {
97        Self { len, shape: None }
98    }
99
100    fn with_shape(len: usize, shape: &[usize]) -> Self {
101        Self {
102            len,
103            shape: Some(shape.to_vec()),
104        }
105    }
106
107    fn len(&self) -> usize {
108        self.len
109    }
110
111    fn shape(&self) -> Option<&[usize]> {
112        self.shape.as_deref()
113    }
114}
115
116impl PatternSpec {
117    fn pattern_for_index(&self, idx: usize) -> PatternRef<'_> {
118        match self {
119            PatternSpec::Default => PatternRef::Default,
120            PatternSpec::Scalar(chars) => PatternRef::Custom(chars),
121            PatternSpec::PerElement(patterns) => patterns
122                .get(idx)
123                .map(|chars| PatternRef::Custom(chars))
124                .unwrap_or(PatternRef::Default),
125        }
126    }
127}
128
129#[runtime_builtin(
130    name = "strip",
131    category = "strings/transform",
132    summary = "Remove leading and trailing characters from strings, character arrays, and cell arrays.",
133    keywords = "strip,trim,strings,character array,text",
134    accel = "sink",
135    type_resolver(text_preserve_type),
136    builtin_path = "crate::builtins::strings::transform::strip"
137)]
138async fn strip_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
139    let gathered = gather_if_needed_async(&value).await.map_err(map_flow)?;
140    match gathered {
141        Value::String(text) => strip_string(text, &rest).await,
142        Value::StringArray(array) => strip_string_array(array, &rest).await,
143        Value::CharArray(array) => strip_char_array(array, &rest).await,
144        Value::Cell(cell) => strip_cell_array(cell, &rest).await,
145        _ => Err(runtime_error_for(ARG_TYPE_ERROR)),
146    }
147}
148
149async fn strip_string(text: String, args: &[Value]) -> BuiltinResult<Value> {
150    if is_missing_string(&text) {
151        return Ok(Value::String(text));
152    }
153    let expectation = PatternExpectation::scalar();
154    let (direction, pattern_spec) = parse_arguments(args, &expectation).await?;
155    let stripped = strip_text(&text, direction, pattern_spec.pattern_for_index(0));
156    Ok(Value::String(stripped))
157}
158
159async fn strip_string_array(array: StringArray, args: &[Value]) -> BuiltinResult<Value> {
160    let expected_len = array.data.len();
161    let expectation = PatternExpectation::with_shape(expected_len, &array.shape);
162    let (direction, pattern_spec) = parse_arguments(args, &expectation).await?;
163    let StringArray { data, shape, .. } = array;
164    let mut stripped: Vec<String> = Vec::with_capacity(expected_len);
165    for (idx, text) in data.into_iter().enumerate() {
166        if is_missing_string(&text) {
167            stripped.push(text);
168        } else {
169            let pattern = pattern_spec.pattern_for_index(idx);
170            stripped.push(strip_text(&text, direction, pattern));
171        }
172    }
173    let result = StringArray::new(stripped, shape)
174        .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))?;
175    Ok(Value::StringArray(result))
176}
177
178async fn strip_char_array(array: CharArray, args: &[Value]) -> BuiltinResult<Value> {
179    let CharArray { data, rows, cols } = array;
180    let expectation = PatternExpectation::with_len(rows);
181    let (direction, pattern_spec) = parse_arguments(args, &expectation).await?;
182
183    if rows == 0 {
184        return Ok(Value::CharArray(CharArray { data, rows, cols }));
185    }
186
187    let mut stripped_rows: Vec<String> = Vec::with_capacity(rows);
188    let mut target_cols: usize = 0;
189    for row in 0..rows {
190        let text = char_row_to_string_slice(&data, cols, row);
191        let pattern = pattern_spec.pattern_for_index(row);
192        let stripped = strip_text(&text, direction, pattern);
193        let len = stripped.chars().count();
194        target_cols = target_cols.max(len);
195        stripped_rows.push(stripped);
196    }
197
198    let mut new_data: Vec<char> = Vec::with_capacity(rows * target_cols);
199    for row_text in stripped_rows {
200        let mut chars: Vec<char> = row_text.chars().collect();
201        if chars.len() < target_cols {
202            chars.resize(target_cols, ' ');
203        }
204        new_data.extend(chars.into_iter());
205    }
206
207    CharArray::new(new_data, rows, target_cols)
208        .map(Value::CharArray)
209        .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))
210}
211
212async fn strip_cell_array(cell: CellArray, args: &[Value]) -> BuiltinResult<Value> {
213    let rows = cell.rows;
214    let cols = cell.cols;
215    let dims = [rows, cols];
216    let expectation = PatternExpectation::with_shape(rows * cols, &dims);
217    let (direction, pattern_spec) = parse_arguments(args, &expectation).await?;
218    let total = rows * cols;
219    let mut stripped_values: Vec<Value> = Vec::with_capacity(total);
220    for idx in 0..total {
221        let value = &cell.data[idx];
222        let pattern = pattern_spec.pattern_for_index(idx);
223        let stripped = strip_cell_element(value, direction, pattern).await?;
224        stripped_values.push(stripped);
225    }
226    make_cell(stripped_values, rows, cols)
227        .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))
228}
229
230async fn strip_cell_element(
231    value: &Value,
232    direction: StripDirection,
233    pattern: PatternRef<'_>,
234) -> BuiltinResult<Value> {
235    let gathered = gather_if_needed_async(value).await.map_err(map_flow)?;
236    match gathered {
237        Value::String(text) => {
238            if is_missing_string(&text) {
239                Ok(Value::String(text))
240            } else {
241                let stripped = strip_text(&text, direction, pattern);
242                Ok(Value::String(stripped))
243            }
244        }
245        Value::StringArray(sa) if sa.data.len() == 1 => {
246            let text = sa.data.into_iter().next().unwrap();
247            if is_missing_string(&text) {
248                Ok(Value::String(text))
249            } else {
250                let stripped = strip_text(&text, direction, pattern);
251                Ok(Value::String(stripped))
252            }
253        }
254        Value::CharArray(ca) if ca.rows <= 1 => {
255            let source = if ca.rows == 0 {
256                String::new()
257            } else {
258                char_row_to_string_slice(&ca.data, ca.cols, 0)
259            };
260            let stripped = strip_text(&source, direction, pattern);
261            let len = stripped.chars().count();
262            let data: Vec<char> = stripped.chars().collect();
263            let rows = ca.rows;
264            let cols = if rows == 0 { ca.cols } else { len };
265            CharArray::new(data, rows, cols)
266                .map(Value::CharArray)
267                .map_err(|e| runtime_error_for(format!("{BUILTIN_NAME}: {e}")))
268        }
269        Value::CharArray(_) => Err(runtime_error_for(CELL_ELEMENT_ERROR)),
270        _ => Err(runtime_error_for(CELL_ELEMENT_ERROR)),
271    }
272}
273
274async fn parse_arguments(
275    args: &[Value],
276    expectation: &PatternExpectation,
277) -> BuiltinResult<(StripDirection, PatternSpec)> {
278    match args.len() {
279        0 => Ok((StripDirection::Both, PatternSpec::Default)),
280        1 => {
281            if let Some(direction) = try_parse_direction(&args[0], false)? {
282                Ok((direction, PatternSpec::Default))
283            } else {
284                let pattern = parse_pattern(&args[0], expectation).await?;
285                Ok((StripDirection::Both, pattern))
286            }
287        }
288        2 => {
289            let direction = match try_parse_direction(&args[0], true)? {
290                Some(dir) => dir,
291                None => return Err(runtime_error_for(DIRECTION_ERROR)),
292            };
293            let pattern = parse_pattern(&args[1], expectation).await?;
294            Ok((direction, pattern))
295        }
296        _ => Err(runtime_error_for("strip: too many input arguments")),
297    }
298}
299
300fn try_parse_direction(value: &Value, strict: bool) -> BuiltinResult<Option<StripDirection>> {
301    let Some(text) = value_to_single_string(value) else {
302        return Ok(None);
303    };
304    let trimmed = text.trim();
305    if trimmed.is_empty() {
306        return if strict {
307            Err(runtime_error_for(DIRECTION_ERROR))
308        } else {
309            Ok(None)
310        };
311    }
312    let lowered = trimmed.to_ascii_lowercase();
313    let direction = match lowered.as_str() {
314        "both" => Some(StripDirection::Both),
315        "left" | "leading" => Some(StripDirection::Left),
316        "right" | "trailing" => Some(StripDirection::Right),
317        _ => {
318            if strict {
319                return Err(runtime_error_for(DIRECTION_ERROR));
320            }
321            None
322        }
323    };
324    Ok(direction)
325}
326
327fn value_to_single_string(value: &Value) -> Option<String> {
328    match value {
329        Value::String(text) => Some(text.clone()),
330        Value::StringArray(sa) => {
331            if sa.data.len() == 1 {
332                Some(sa.data[0].clone())
333            } else {
334                None
335            }
336        }
337        Value::CharArray(ca) => {
338            if ca.rows <= 1 {
339                Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
340            } else {
341                None
342            }
343        }
344        _ => None,
345    }
346}
347
348async fn parse_pattern(
349    value: &Value,
350    expectation: &PatternExpectation,
351) -> BuiltinResult<PatternSpec> {
352    let expected_len = expectation.len();
353    match value {
354        Value::String(text) => Ok(PatternSpec::Scalar(text.chars().collect())),
355        Value::StringArray(sa) => {
356            if sa.data.len() <= 1 {
357                if let Some(first) = sa.data.first() {
358                    Ok(PatternSpec::Scalar(first.chars().collect()))
359                } else {
360                    Ok(PatternSpec::Scalar(Vec::new()))
361                }
362            } else if sa.data.len() == expected_len {
363                if let Some(shape) = expectation.shape() {
364                    if sa.shape != shape {
365                        return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
366                    }
367                }
368                let mut patterns = Vec::with_capacity(sa.data.len());
369                for text in &sa.data {
370                    patterns.push(text.chars().collect());
371                }
372                Ok(PatternSpec::PerElement(patterns))
373            } else {
374                Err(runtime_error_for(SIZE_MISMATCH_ERROR))
375            }
376        }
377        Value::CharArray(ca) => {
378            if ca.rows <= 1 {
379                if ca.rows == 0 {
380                    Ok(PatternSpec::Scalar(Vec::new()))
381                } else {
382                    let chars = char_row_to_string_slice(&ca.data, ca.cols, 0);
383                    Ok(PatternSpec::Scalar(chars.chars().collect()))
384                }
385            } else if ca.rows == expected_len {
386                let mut patterns = Vec::with_capacity(ca.rows);
387                for row in 0..ca.rows {
388                    let text = char_row_to_string_slice(&ca.data, ca.cols, row);
389                    patterns.push(text.chars().collect());
390                }
391                Ok(PatternSpec::PerElement(patterns))
392            } else {
393                Err(runtime_error_for(SIZE_MISMATCH_ERROR))
394            }
395        }
396        Value::Cell(cell) => parse_pattern_cell(cell, expectation).await,
397        _ => Err(runtime_error_for(CHARACTERS_ERROR)),
398    }
399}
400
401async fn parse_pattern_cell(
402    cell: &CellArray,
403    expectation: &PatternExpectation,
404) -> BuiltinResult<PatternSpec> {
405    let len = cell.rows * cell.cols;
406    if len == 0 {
407        return Ok(PatternSpec::Scalar(Vec::new()));
408    }
409    if len == 1 {
410        let chars = pattern_chars_from_value(&cell.data[0]).await?;
411        return Ok(PatternSpec::Scalar(chars));
412    }
413    if len != expectation.len() {
414        return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
415    }
416    if let Some(shape) = expectation.shape() {
417        match shape.len() {
418            0 => {}
419            1 => {
420                if cell.rows != shape[0] || cell.cols != 1 {
421                    return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
422                }
423            }
424            _ => {
425                if cell.rows != shape[0] || cell.cols != shape[1] {
426                    return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
427                }
428            }
429        }
430    }
431    let mut patterns = Vec::with_capacity(len);
432    for value in &cell.data {
433        patterns.push(pattern_chars_from_value(value).await?);
434    }
435    Ok(PatternSpec::PerElement(patterns))
436}
437
438async fn pattern_chars_from_value(value: &Value) -> BuiltinResult<Vec<char>> {
439    let gathered = gather_if_needed_async(value).await.map_err(map_flow)?;
440    match gathered {
441        Value::String(text) => Ok(text.chars().collect()),
442        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].chars().collect()),
443        Value::CharArray(ca) if ca.rows <= 1 => {
444            if ca.rows == 0 {
445                Ok(Vec::new())
446            } else {
447                let text = char_row_to_string_slice(&ca.data, ca.cols, 0);
448                Ok(text.chars().collect())
449            }
450        }
451        Value::CharArray(_) => Err(runtime_error_for(CHARACTERS_ERROR)),
452        _ => Err(runtime_error_for(CHARACTERS_ERROR)),
453    }
454}
455
456fn strip_text(text: &str, direction: StripDirection, pattern: PatternRef<'_>) -> String {
457    match pattern {
458        PatternRef::Default => strip_text_with_predicate(text, direction, char::is_whitespace),
459        PatternRef::Custom(chars) => {
460            strip_text_with_predicate(text, direction, |c| chars.contains(&c))
461        }
462    }
463}
464
465fn strip_text_with_predicate<F>(text: &str, direction: StripDirection, mut predicate: F) -> String
466where
467    F: FnMut(char) -> bool,
468{
469    let chars: Vec<char> = text.chars().collect();
470    if chars.is_empty() {
471        return String::new();
472    }
473
474    let mut start = 0usize;
475    let mut end = chars.len();
476
477    if direction != StripDirection::Right {
478        while start < end && predicate(chars[start]) {
479            start += 1;
480        }
481    }
482
483    if direction != StripDirection::Left {
484        while end > start && predicate(chars[end - 1]) {
485            end -= 1;
486        }
487    }
488
489    chars[start..end].iter().collect()
490}
491
492#[cfg(test)]
493pub(crate) mod tests {
494    use super::*;
495    use runmat_builtins::{ResolveContext, Type};
496
497    fn run_strip(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
498        futures::executor::block_on(strip_builtin(value, rest))
499    }
500
501    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
502    #[test]
503    fn strip_string_scalar_default() {
504        let result = run_strip(Value::String("  RunMat  ".into()), Vec::new()).expect("strip");
505        assert_eq!(result, Value::String("RunMat".into()));
506    }
507
508    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
509    #[test]
510    fn strip_string_scalar_direction() {
511        let result = run_strip(
512            Value::String("...data".into()),
513            vec![Value::String("left".into()), Value::String(".".into())],
514        )
515        .expect("strip");
516        assert_eq!(result, Value::String("data".into()));
517    }
518
519    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
520    #[test]
521    fn strip_string_scalar_custom_characters() {
522        let result = run_strip(
523            Value::String("00052".into()),
524            vec![Value::String("left".into()), Value::String("0".into())],
525        )
526        .expect("strip");
527        assert_eq!(result, Value::String("52".into()));
528    }
529
530    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
531    #[test]
532    fn strip_string_scalar_pattern_only() {
533        let result = run_strip(
534            Value::String("xxaccelerationxx".into()),
535            vec![Value::String("x".into())],
536        )
537        .expect("strip");
538        assert_eq!(result, Value::String("acceleration".into()));
539    }
540
541    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
542    #[test]
543    fn strip_empty_pattern_returns_original() {
544        let result = run_strip(
545            Value::String("abc".into()),
546            vec![Value::String(String::new())],
547        )
548        .expect("strip");
549        assert_eq!(result, Value::String("abc".into()));
550    }
551
552    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
553    #[test]
554    fn strip_supports_leading_synonym() {
555        let result = run_strip(
556            Value::String("   data".into()),
557            vec![Value::String("leading".into())],
558        )
559        .expect("strip");
560        assert_eq!(result, Value::String("data".into()));
561    }
562
563    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
564    #[test]
565    fn strip_supports_trailing_synonym() {
566        let result = run_strip(
567            Value::String("data   ".into()),
568            vec![Value::String("trailing".into())],
569        )
570        .expect("strip");
571        assert_eq!(result, Value::String("data".into()));
572    }
573
574    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
575    #[test]
576    fn strip_string_array_per_element_characters() {
577        let strings = StringArray::new(
578            vec!["##ok##".into(), "--warn--".into(), "**fail**".into()],
579            vec![3, 1],
580        )
581        .unwrap();
582        let chars = CharArray::new(vec!['#', '#', '-', '-', '*', '*'], 3, 2).unwrap();
583        let result = run_strip(
584            Value::StringArray(strings),
585            vec![Value::String("both".into()), Value::CharArray(chars)],
586        )
587        .expect("strip");
588        match result {
589            Value::StringArray(sa) => {
590                assert_eq!(
591                    sa.data,
592                    vec![
593                        String::from("ok"),
594                        String::from("warn"),
595                        String::from("fail")
596                    ]
597                );
598            }
599            other => panic!("expected string array, got {other:?}"),
600        }
601    }
602
603    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
604    #[test]
605    fn strip_string_array_cell_pattern_per_element() {
606        let strings =
607            StringArray::new(vec!["__pass__".into(), "--warn--".into()], vec![2, 1]).unwrap();
608        let patterns = CellArray::new(
609            vec![Value::String("_".into()), Value::String("-".into())],
610            2,
611            1,
612        )
613        .unwrap();
614        let result =
615            run_strip(Value::StringArray(strings), vec![Value::Cell(patterns)]).expect("strip");
616        match result {
617            Value::StringArray(sa) => {
618                assert_eq!(sa.data, vec![String::from("pass"), String::from("warn")]);
619            }
620            other => panic!("expected string array, got {other:?}"),
621        }
622    }
623
624    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
625    #[test]
626    fn strip_string_array_preserves_missing() {
627        let strings =
628            StringArray::new(vec!["   data   ".into(), "<missing>".into()], vec![2, 1]).unwrap();
629        let result = run_strip(Value::StringArray(strings), Vec::new()).expect("strip");
630        match result {
631            Value::StringArray(sa) => {
632                assert_eq!(sa.data[0], "data");
633                assert_eq!(sa.data[1], "<missing>");
634            }
635            other => panic!("expected string array, got {other:?}"),
636        }
637    }
638
639    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
640    #[test]
641    fn strip_char_array_shrinks_width() {
642        let source = "  cat  dog  ";
643        let chars: Vec<char> = source.chars().collect();
644        let array = CharArray::new(chars, 1, source.chars().count()).unwrap();
645        let result = run_strip(Value::CharArray(array), Vec::new()).expect("strip");
646        match result {
647            Value::CharArray(ca) => {
648                assert_eq!(ca.rows, 1);
649                assert_eq!(ca.cols, 8);
650                let expected: Vec<char> = "cat  dog".chars().collect();
651                assert_eq!(ca.data, expected);
652            }
653            other => panic!("expected char array, got {other:?}"),
654        }
655    }
656
657    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
658    #[test]
659    fn strip_char_array_supports_trailing_direction() {
660        let array = CharArray::new_row("gpu   ");
661        let result = run_strip(
662            Value::CharArray(array),
663            vec![Value::String("trailing".into())],
664        )
665        .expect("strip");
666        match result {
667            Value::CharArray(ca) => {
668                assert_eq!(ca.rows, 1);
669                assert_eq!(ca.cols, 3);
670                let expected: Vec<char> = "gpu".chars().collect();
671                assert_eq!(ca.data, expected);
672            }
673            other => panic!("expected char array, got {other:?}"),
674        }
675    }
676
677    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
678    #[test]
679    fn strip_cell_array_mixed_content() {
680        let cell = CellArray::new(
681            vec![
682                Value::CharArray(CharArray::new_row("  GPU ")),
683                Value::String("   Accelerate".into()),
684                Value::String("RunMat   ".into()),
685            ],
686            1,
687            3,
688        )
689        .unwrap();
690        let result = run_strip(Value::Cell(cell), Vec::new()).expect("strip");
691        match result {
692            Value::Cell(out) => {
693                assert_eq!(out.rows, 1);
694                assert_eq!(out.cols, 3);
695                assert_eq!(
696                    out.get(0, 0).unwrap(),
697                    Value::CharArray(CharArray::new_row("GPU"))
698                );
699                assert_eq!(out.get(0, 1).unwrap(), Value::String("Accelerate".into()));
700                assert_eq!(out.get(0, 2).unwrap(), Value::String("RunMat".into()));
701            }
702            other => panic!("expected cell array, got {other:?}"),
703        }
704    }
705
706    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
707    #[test]
708    fn strip_preserves_missing_string() {
709        let result = run_strip(Value::String("<missing>".into()), Vec::new()).expect("strip");
710        assert_eq!(result, Value::String("<missing>".into()));
711    }
712
713    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
714    #[test]
715    fn strip_errors_on_invalid_input() {
716        let err = run_strip(Value::Num(1.0), Vec::new()).unwrap_err();
717        assert_eq!(err.to_string(), ARG_TYPE_ERROR);
718    }
719
720    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721    #[test]
722    fn strip_errors_on_invalid_pattern_type() {
723        let err = run_strip(Value::String("abc".into()), vec![Value::Num(1.0)]).unwrap_err();
724        assert_eq!(err.to_string(), CHARACTERS_ERROR);
725    }
726
727    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
728    #[test]
729    fn strip_errors_on_invalid_direction() {
730        let err = run_strip(
731            Value::String("abc".into()),
732            vec![Value::String("sideways".into()), Value::String("a".into())],
733        )
734        .unwrap_err();
735        assert_eq!(err.to_string(), DIRECTION_ERROR);
736    }
737
738    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
739    #[test]
740    fn strip_errors_on_pattern_size_mismatch() {
741        let strings = StringArray::new(vec!["one".into(), "two".into()], vec![2, 1]).unwrap();
742        let pattern =
743            StringArray::new(vec!["x".into(), "y".into(), "z".into()], vec![3, 1]).unwrap();
744        let err = run_strip(
745            Value::StringArray(strings),
746            vec![Value::StringArray(pattern)],
747        )
748        .unwrap_err();
749        assert_eq!(err.to_string(), SIZE_MISMATCH_ERROR);
750    }
751
752    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
753    #[test]
754    fn strip_errors_on_pattern_shape_mismatch() {
755        let strings = StringArray::new(vec!["one".into(), "two".into()], vec![1, 2]).unwrap();
756        let pattern = StringArray::new(vec!["x".into(), "y".into()], vec![2, 1]).unwrap();
757        let err = run_strip(
758            Value::StringArray(strings),
759            vec![Value::StringArray(pattern)],
760        )
761        .unwrap_err();
762        assert_eq!(err.to_string(), SIZE_MISMATCH_ERROR);
763    }
764
765    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
766    #[test]
767    fn strip_errors_on_cell_pattern_shape_mismatch() {
768        let strings = StringArray::new(vec!["aa".into(), "bb".into()], vec![1, 2]).unwrap();
769        let cell_pattern = CellArray::new(
770            vec![Value::String("a".into()), Value::String("b".into())],
771            2,
772            1,
773        )
774        .unwrap();
775        let err =
776            run_strip(Value::StringArray(strings), vec![Value::Cell(cell_pattern)]).unwrap_err();
777        assert_eq!(err.to_string(), SIZE_MISMATCH_ERROR);
778    }
779
780    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
781    #[test]
782    fn strip_errors_on_too_many_arguments() {
783        let err = run_strip(
784            Value::String("abc".into()),
785            vec![
786                Value::String("both".into()),
787                Value::String("a".into()),
788                Value::String("b".into()),
789            ],
790        )
791        .unwrap_err();
792        assert_eq!(err.to_string(), "strip: too many input arguments");
793    }
794
795    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
796    #[test]
797    #[cfg(feature = "wgpu")]
798    fn strip_gpu_tensor_errors() {
799        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
800            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
801        );
802        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
803        let host_data = [1.0f64, 2.0];
804        let host_shape = [2usize, 1usize];
805        let handle = provider
806            .upload(&runmat_accelerate_api::HostTensorView {
807                data: &host_data,
808                shape: &host_shape,
809            })
810            .expect("upload");
811        let err = run_strip(Value::GpuTensor(handle.clone()), Vec::new()).unwrap_err();
812        assert_eq!(err.to_string(), ARG_TYPE_ERROR);
813        provider.free(&handle).ok();
814    }
815
816    #[test]
817    fn strip_type_preserves_text() {
818        assert_eq!(
819            text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
820            Type::String
821        );
822    }
823}