1use 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}