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