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