1use std::cmp::min;
4
5use crate::builtins::common::broadcast::{broadcast_index, broadcast_shapes, compute_strides};
6use crate::builtins::common::map_control_flow_with_builtin;
7use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
8use crate::builtins::strings::type_resolvers::text_preserve_type;
9use crate::{
10 build_runtime_error, gather_if_needed_async, make_cell_with_shape, BuiltinResult, RuntimeError,
11};
12use runmat_builtins::{CharArray, IntValue, StringArray, Value};
13use runmat_macros::runtime_builtin;
14
15use crate::builtins::common::spec::{
16 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17 ReductionNaN, ResidencyPolicy, ShapeRequirements,
18};
19
20#[runmat_macros::register_gpu_spec(
21 builtin_path = "crate::builtins::strings::transform::erasebetween"
22)]
23pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
24 name: "eraseBetween",
25 op_kind: GpuOpKind::Custom("string-transform"),
26 supported_precisions: &[],
27 broadcast: BroadcastSemantics::Matlab,
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: "Runs on the CPU; GPU-resident inputs are gathered before deletion and outputs remain on the host.",
36};
37
38#[runmat_macros::register_fusion_spec(
39 builtin_path = "crate::builtins::strings::transform::erasebetween"
40)]
41pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
42 name: "eraseBetween",
43 shape: ShapeRequirements::Any,
44 constant_strategy: ConstantStrategy::InlineLiteral,
45 elementwise: None,
46 reduction: None,
47 emits_nan: false,
48 notes: "Pure string manipulation builtin; excluded from fusion plans and gathers GPU inputs immediately.",
49};
50
51const FN_NAME: &str = "eraseBetween";
52const ARG_TYPE_ERROR: &str = "eraseBetween: first argument must be a string array, character array, or cell array of character vectors";
53const BOUNDARY_TYPE_ERROR: &str =
54 "eraseBetween: start and end arguments must both be text or both be numeric positions";
55const POSITION_TYPE_ERROR: &str = "eraseBetween: position arguments must be positive integers";
56const OPTION_PAIR_ERROR: &str = "eraseBetween: name-value arguments must appear in pairs";
57const OPTION_NAME_ERROR: &str = "eraseBetween: unrecognized parameter name";
58const OPTION_VALUE_ERROR: &str =
59 "eraseBetween: 'Boundaries' must be either 'inclusive' or 'exclusive'";
60const CELL_ELEMENT_ERROR: &str =
61 "eraseBetween: cell array elements must be string scalars or character vectors";
62const SIZE_MISMATCH_ERROR: &str =
63 "eraseBetween: boundary sizes must be compatible with the text input";
64
65fn runtime_error_for(message: impl Into<String>) -> RuntimeError {
66 build_runtime_error(message).with_builtin(FN_NAME).build()
67}
68
69fn map_flow(err: RuntimeError) -> RuntimeError {
70 map_control_flow_with_builtin(err, FN_NAME)
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74enum BoundariesMode {
75 Exclusive,
76 Inclusive,
77}
78
79#[runtime_builtin(
80 name = "eraseBetween",
81 category = "strings/transform",
82 summary = "Delete text between boundary markers with MATLAB-compatible semantics.",
83 keywords = "eraseBetween,delete,boundaries,strings",
84 accel = "sink",
85 type_resolver(text_preserve_type),
86 builtin_path = "crate::builtins::strings::transform::erasebetween"
87)]
88async fn erase_between_builtin(
89 text: Value,
90 start: Value,
91 stop: Value,
92 rest: Vec<Value>,
93) -> BuiltinResult<Value> {
94 let text = gather_if_needed_async(&text).await.map_err(map_flow)?;
95 let start = gather_if_needed_async(&start).await.map_err(map_flow)?;
96 let stop = gather_if_needed_async(&stop).await.map_err(map_flow)?;
97
98 let mode_override = parse_boundaries_option(&rest).await?;
99
100 let normalized_text = NormalizedText::from_value(text)?;
101 let start_boundary = BoundaryArg::from_value(start)?;
102 let stop_boundary = BoundaryArg::from_value(stop)?;
103
104 if start_boundary.kind() != stop_boundary.kind() {
105 return Err(runtime_error_for(BOUNDARY_TYPE_ERROR));
106 }
107 let boundary_kind = start_boundary.kind();
108 let effective_mode = mode_override.unwrap_or(match boundary_kind {
109 BoundaryKind::Text => BoundariesMode::Exclusive,
110 BoundaryKind::Position => BoundariesMode::Inclusive,
111 });
112
113 let start_shape = start_boundary.shape();
114 let stop_shape = stop_boundary.shape();
115 let text_shape = normalized_text.shape();
116
117 let shape_ts = broadcast_shapes(FN_NAME, text_shape, start_shape).map_err(runtime_error_for)?;
118 let output_shape =
119 broadcast_shapes(FN_NAME, &shape_ts, stop_shape).map_err(runtime_error_for)?;
120 if !normalized_text.supports_shape(&output_shape) {
121 return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
122 }
123
124 let total: usize = output_shape.iter().copied().product();
125 if total == 0 {
126 return normalized_text.into_value(Vec::new(), output_shape);
127 }
128
129 let text_strides = compute_strides(text_shape);
130 let start_strides = compute_strides(start_shape);
131 let stop_strides = compute_strides(stop_shape);
132
133 let mut results = Vec::with_capacity(total);
134
135 for idx in 0..total {
136 let text_idx = broadcast_index(idx, &output_shape, text_shape, &text_strides);
137 let start_idx = broadcast_index(idx, &output_shape, start_shape, &start_strides);
138 let stop_idx = broadcast_index(idx, &output_shape, stop_shape, &stop_strides);
139
140 let result = match boundary_kind {
141 BoundaryKind::Text => {
142 let text_value = normalized_text.data(text_idx);
143 let start_value = start_boundary.text(start_idx);
144 let stop_value = stop_boundary.text(stop_idx);
145 erase_with_text_boundaries(text_value, start_value, stop_value, effective_mode)
146 }
147 BoundaryKind::Position => {
148 let text_value = normalized_text.data(text_idx);
149 let start_value = start_boundary.position(start_idx);
150 let stop_value = stop_boundary.position(stop_idx);
151 erase_with_positions(text_value, start_value, stop_value, effective_mode)
152 }
153 };
154 results.push(result);
155 }
156
157 normalized_text.into_value(results, output_shape)
158}
159
160async fn parse_boundaries_option(args: &[Value]) -> BuiltinResult<Option<BoundariesMode>> {
161 if args.is_empty() {
162 return Ok(None);
163 }
164 if !args.len().is_multiple_of(2) {
165 return Err(runtime_error_for(OPTION_PAIR_ERROR));
166 }
167
168 let mut mode: Option<BoundariesMode> = None;
169 let mut idx = 0;
170 while idx < args.len() {
171 let name_value = gather_if_needed_async(&args[idx]).await.map_err(map_flow)?;
172 let name =
173 value_to_string(&name_value).ok_or_else(|| runtime_error_for(OPTION_NAME_ERROR))?;
174 if !name.eq_ignore_ascii_case("boundaries") {
175 return Err(runtime_error_for(OPTION_NAME_ERROR));
176 }
177 let value = gather_if_needed_async(&args[idx + 1])
178 .await
179 .map_err(map_flow)?;
180 let value_str =
181 value_to_string(&value).ok_or_else(|| runtime_error_for(OPTION_VALUE_ERROR))?;
182 let parsed_mode = if value_str.eq_ignore_ascii_case("inclusive") {
183 BoundariesMode::Inclusive
184 } else if value_str.eq_ignore_ascii_case("exclusive") {
185 BoundariesMode::Exclusive
186 } else {
187 return Err(runtime_error_for(OPTION_VALUE_ERROR));
188 };
189 mode = Some(parsed_mode);
190 idx += 2;
191 }
192 Ok(mode)
193}
194
195fn value_to_string(value: &Value) -> Option<String> {
196 match value {
197 Value::String(s) => Some(s.clone()),
198 Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
199 Value::CharArray(ca) if ca.rows <= 1 => {
200 if ca.rows == 0 {
201 Some(String::new())
202 } else {
203 Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
204 }
205 }
206 Value::CharArray(_) => None,
207 Value::Cell(cell) if cell.data.len() == 1 => {
208 let element = &cell.data[0];
209 value_to_string(element)
210 }
211 _ => None,
212 }
213}
214
215#[derive(Clone)]
216struct EraseResult {
217 text: String,
218}
219
220impl EraseResult {
221 fn missing() -> Self {
222 Self {
223 text: "<missing>".to_string(),
224 }
225 }
226
227 fn text(text: String) -> Self {
228 Self { text }
229 }
230}
231
232fn erase_with_text_boundaries(
233 text: &str,
234 start: &str,
235 stop: &str,
236 mode: BoundariesMode,
237) -> EraseResult {
238 if is_missing_string(text) || is_missing_string(start) || is_missing_string(stop) {
239 return EraseResult::missing();
240 }
241
242 if let Some(start_idx) = text.find(start) {
243 let search_start = start_idx + start.len();
244 if search_start > text.len() {
245 return EraseResult::text(text.to_string());
246 }
247 if let Some(relative_end) = text[search_start..].find(stop) {
248 let end_idx = search_start + relative_end;
249 match mode {
250 BoundariesMode::Inclusive => {
251 let end_capture = min(text.len(), end_idx + stop.len());
252 let mut result = String::with_capacity(text.len());
253 result.push_str(&text[..start_idx]);
254 result.push_str(&text[end_capture..]);
255 EraseResult::text(result)
256 }
257 BoundariesMode::Exclusive => {
258 let mut result = String::with_capacity(text.len());
259 result.push_str(&text[..search_start]);
260 result.push_str(&text[end_idx..]);
261 EraseResult::text(result)
262 }
263 }
264 } else {
265 EraseResult::text(text.to_string())
266 }
267 } else {
268 EraseResult::text(text.to_string())
269 }
270}
271
272fn erase_with_positions(
273 text: &str,
274 start: usize,
275 stop: usize,
276 mode: BoundariesMode,
277) -> EraseResult {
278 if is_missing_string(text) {
279 return EraseResult::missing();
280 }
281 if text.is_empty() {
282 return EraseResult::text(String::new());
283 }
284 let chars: Vec<char> = text.chars().collect();
285 let len = chars.len();
286 if len == 0 {
287 return EraseResult::text(String::new());
288 }
289
290 if start == 0 || stop == 0 {
291 return EraseResult::text(text.to_string());
292 }
293
294 if start > len {
295 return EraseResult::text(text.to_string());
296 }
297 let stop_clamped = stop.min(len);
298
299 match mode {
300 BoundariesMode::Inclusive => {
301 if stop_clamped < start {
302 return EraseResult::text(text.to_string());
303 }
304 let start_idx = start - 1;
305 let end_idx = stop_clamped - 1;
306 if start_idx >= len || end_idx >= len || start_idx > end_idx {
307 EraseResult::text(text.to_string())
308 } else {
309 let mut result = String::with_capacity(len);
310 for (idx, ch) in chars.iter().enumerate() {
311 if idx < start_idx || idx > end_idx {
312 result.push(*ch);
313 }
314 }
315 EraseResult::text(result)
316 }
317 }
318 BoundariesMode::Exclusive => {
319 if start + 1 >= stop_clamped {
320 return EraseResult::text(text.to_string());
321 }
322 let start_idx = start;
323 let end_idx = stop_clamped - 2;
324 if start_idx >= len || end_idx >= len || start_idx > end_idx {
325 EraseResult::text(text.to_string())
326 } else {
327 let mut result = String::with_capacity(len);
328 for (idx, ch) in chars.iter().enumerate() {
329 if idx >= start_idx && idx <= end_idx {
330 continue;
331 }
332 result.push(*ch);
333 }
334 EraseResult::text(result)
335 }
336 }
337 }
338}
339
340#[derive(Clone, Debug)]
341struct CellInfo {
342 shape: Vec<usize>,
343 element_kinds: Vec<CellElementKind>,
344}
345
346#[derive(Clone, Debug)]
347enum CellElementKind {
348 String,
349 Char,
350}
351
352#[derive(Clone, Debug)]
353enum TextKind {
354 StringScalar,
355 StringArray,
356 CharArray { rows: usize },
357 CellArray(CellInfo),
358}
359
360#[derive(Clone, Debug)]
361struct NormalizedText {
362 data: Vec<String>,
363 shape: Vec<usize>,
364 kind: TextKind,
365}
366
367impl NormalizedText {
368 fn from_value(value: Value) -> BuiltinResult<Self> {
369 match value {
370 Value::String(s) => Ok(Self {
371 data: vec![s],
372 shape: vec![1, 1],
373 kind: TextKind::StringScalar,
374 }),
375 Value::StringArray(sa) => Ok(Self {
376 data: sa.data.clone(),
377 shape: sa.shape.clone(),
378 kind: TextKind::StringArray,
379 }),
380 Value::CharArray(ca) => {
381 let rows = ca.rows;
382 let mut data = Vec::with_capacity(rows);
383 for row in 0..rows {
384 data.push(char_row_to_string_slice(&ca.data, ca.cols, row));
385 }
386 Ok(Self {
387 data,
388 shape: vec![rows, 1],
389 kind: TextKind::CharArray { rows },
390 })
391 }
392 Value::Cell(cell) => {
393 let shape = cell.shape.clone();
394 let mut data = Vec::with_capacity(cell.data.len());
395 let mut kinds = Vec::with_capacity(cell.data.len());
396 for element in &cell.data {
397 match &**element {
398 Value::String(s) => {
399 data.push(s.clone());
400 kinds.push(CellElementKind::String);
401 }
402 Value::StringArray(sa) if sa.data.len() == 1 => {
403 data.push(sa.data[0].clone());
404 kinds.push(CellElementKind::String);
405 }
406 Value::CharArray(ca) if ca.rows <= 1 => {
407 if ca.rows == 0 {
408 data.push(String::new());
409 } else {
410 data.push(char_row_to_string_slice(&ca.data, ca.cols, 0));
411 }
412 kinds.push(CellElementKind::Char);
413 }
414 Value::CharArray(_) => return Err(runtime_error_for(CELL_ELEMENT_ERROR)),
415 _ => return Err(runtime_error_for(CELL_ELEMENT_ERROR)),
416 }
417 }
418 Ok(Self {
419 data,
420 shape: shape.clone(),
421 kind: TextKind::CellArray(CellInfo {
422 shape,
423 element_kinds: kinds,
424 }),
425 })
426 }
427 _ => Err(runtime_error_for(ARG_TYPE_ERROR)),
428 }
429 }
430
431 fn shape(&self) -> &[usize] {
432 &self.shape
433 }
434
435 fn data(&self, idx: usize) -> &str {
436 &self.data[idx]
437 }
438
439 fn supports_shape(&self, output_shape: &[usize]) -> bool {
440 match &self.kind {
441 TextKind::StringScalar => true,
442 TextKind::StringArray => true,
443 TextKind::CharArray { .. } => output_shape == self.shape,
444 TextKind::CellArray(info) => output_shape == info.shape,
445 }
446 }
447
448 fn into_value(
449 self,
450 results: Vec<EraseResult>,
451 output_shape: Vec<usize>,
452 ) -> BuiltinResult<Value> {
453 match self.kind {
454 TextKind::StringScalar => {
455 let total: usize = output_shape.iter().product();
456 if total == 0 {
457 let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
458 let array = StringArray::new(data, output_shape)
459 .map_err(|e| runtime_error_for(format!("{FN_NAME}: {e}")))?;
460 return Ok(Value::StringArray(array));
461 }
462
463 if results.len() <= 1 {
464 let value = results
465 .into_iter()
466 .next()
467 .unwrap_or_else(|| EraseResult::text(String::new()));
468 Ok(Value::String(value.text))
469 } else {
470 let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
471 let array = StringArray::new(data, output_shape)
472 .map_err(|e| runtime_error_for(format!("{FN_NAME}: {e}")))?;
473 Ok(Value::StringArray(array))
474 }
475 }
476 TextKind::StringArray => {
477 let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
478 let array = StringArray::new(data, output_shape)
479 .map_err(|e| runtime_error_for(format!("{FN_NAME}: {e}")))?;
480 Ok(Value::StringArray(array))
481 }
482 TextKind::CharArray { rows } => {
483 if rows == 0 {
484 return CharArray::new(Vec::new(), 0, 0)
485 .map(Value::CharArray)
486 .map_err(|e| runtime_error_for(format!("{FN_NAME}: {e}")));
487 }
488 if results.len() != rows {
489 return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
490 }
491 let mut max_width = 0usize;
492 let mut row_strings = Vec::with_capacity(rows);
493 for result in &results {
494 let width = result.text.chars().count();
495 max_width = max_width.max(width);
496 row_strings.push(result.text.clone());
497 }
498 let mut flattened = Vec::with_capacity(rows * max_width);
499 for row in row_strings {
500 let mut chars: Vec<char> = row.chars().collect();
501 if chars.len() < max_width {
502 chars.resize(max_width, ' ');
503 }
504 flattened.extend(chars);
505 }
506 CharArray::new(flattened, rows, max_width)
507 .map(Value::CharArray)
508 .map_err(|e| runtime_error_for(format!("{FN_NAME}: {e}")))
509 }
510 TextKind::CellArray(info) => {
511 if results.len() != info.element_kinds.len() {
512 return Err(runtime_error_for(SIZE_MISMATCH_ERROR));
513 }
514 let mut values = Vec::with_capacity(results.len());
515 for (idx, result) in results.into_iter().enumerate() {
516 match info.element_kinds[idx] {
517 CellElementKind::String => values.push(Value::String(result.text)),
518 CellElementKind::Char => {
519 let ca = CharArray::new_row(&result.text);
520 values.push(Value::CharArray(ca));
521 }
522 }
523 }
524 make_cell_with_shape(values, info.shape)
525 .map_err(|e| runtime_error_for(format!("{FN_NAME}: {e}")))
526 }
527 }
528 }
529}
530
531#[derive(Clone, Debug, PartialEq, Eq)]
532enum BoundaryKind {
533 Text,
534 Position,
535}
536
537#[derive(Clone, Debug)]
538enum BoundaryArg {
539 Text(BoundaryText),
540 Position(BoundaryPositions),
541}
542
543impl BoundaryArg {
544 fn from_value(value: Value) -> BuiltinResult<Self> {
545 match value {
546 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) | Value::Cell(_) => {
547 BoundaryText::from_value(value).map(BoundaryArg::Text)
548 }
549 Value::Num(_) | Value::Int(_) | Value::Tensor(_) => {
550 BoundaryPositions::from_value(value).map(BoundaryArg::Position)
551 }
552 other => Err(runtime_error_for(format!(
553 "{BOUNDARY_TYPE_ERROR}: unsupported argument {other:?}"
554 ))),
555 }
556 }
557
558 fn kind(&self) -> BoundaryKind {
559 match self {
560 BoundaryArg::Text(_) => BoundaryKind::Text,
561 BoundaryArg::Position(_) => BoundaryKind::Position,
562 }
563 }
564
565 fn shape(&self) -> &[usize] {
566 match self {
567 BoundaryArg::Text(text) => &text.shape,
568 BoundaryArg::Position(pos) => &pos.shape,
569 }
570 }
571
572 fn text(&self, idx: usize) -> &str {
573 match self {
574 BoundaryArg::Text(text) => &text.data[idx],
575 BoundaryArg::Position(_) => unreachable!(),
576 }
577 }
578
579 fn position(&self, idx: usize) -> usize {
580 match self {
581 BoundaryArg::Position(pos) => pos.data[idx],
582 BoundaryArg::Text(_) => unreachable!(),
583 }
584 }
585}
586
587#[derive(Clone, Debug)]
588struct BoundaryText {
589 data: Vec<String>,
590 shape: Vec<usize>,
591}
592
593impl BoundaryText {
594 fn from_value(value: Value) -> BuiltinResult<Self> {
595 match value {
596 Value::String(s) => Ok(Self {
597 data: vec![s],
598 shape: vec![1, 1],
599 }),
600 Value::StringArray(sa) => Ok(Self {
601 data: sa.data.clone(),
602 shape: sa.shape.clone(),
603 }),
604 Value::CharArray(ca) => {
605 let mut data = Vec::with_capacity(ca.rows);
606 for row in 0..ca.rows {
607 data.push(char_row_to_string_slice(&ca.data, ca.cols, row));
608 }
609 Ok(Self {
610 data,
611 shape: vec![ca.rows, 1],
612 })
613 }
614 Value::Cell(cell) => {
615 let shape = cell.shape.clone();
616 let mut data = Vec::with_capacity(cell.data.len());
617 for element in &cell.data {
618 match &**element {
619 Value::String(s) => data.push(s.clone()),
620 Value::StringArray(sa) if sa.data.len() == 1 => {
621 data.push(sa.data[0].clone());
622 }
623 Value::CharArray(ca) if ca.rows <= 1 => {
624 if ca.rows == 0 {
625 data.push(String::new());
626 } else {
627 data.push(char_row_to_string_slice(&ca.data, ca.cols, 0));
628 }
629 }
630 Value::CharArray(_) => return Err(runtime_error_for(CELL_ELEMENT_ERROR)),
631 _ => return Err(runtime_error_for(CELL_ELEMENT_ERROR)),
632 }
633 }
634 Ok(Self { data, shape })
635 }
636 _ => Err(runtime_error_for(BOUNDARY_TYPE_ERROR)),
637 }
638 }
639}
640
641#[derive(Clone, Debug)]
642struct BoundaryPositions {
643 data: Vec<usize>,
644 shape: Vec<usize>,
645}
646
647impl BoundaryPositions {
648 fn from_value(value: Value) -> BuiltinResult<Self> {
649 match value {
650 Value::Num(n) => Ok(Self {
651 data: vec![parse_position(n)?],
652 shape: vec![1, 1],
653 }),
654 Value::Int(i) => Ok(Self {
655 data: vec![parse_position_int(i)?],
656 shape: vec![1, 1],
657 }),
658 Value::Tensor(t) => {
659 let mut data = Vec::with_capacity(t.data.len());
660 for &entry in &t.data {
661 data.push(parse_position(entry)?);
662 }
663 Ok(Self {
664 data,
665 shape: if t.shape.is_empty() {
666 vec![t.rows, t.cols.max(1)]
667 } else {
668 t.shape
669 },
670 })
671 }
672 _ => Err(runtime_error_for(BOUNDARY_TYPE_ERROR)),
673 }
674 }
675}
676
677fn parse_position(value: f64) -> BuiltinResult<usize> {
678 if !value.is_finite() || value < 1.0 {
679 return Err(runtime_error_for(POSITION_TYPE_ERROR));
680 }
681 if (value.fract()).abs() > f64::EPSILON {
682 return Err(runtime_error_for(POSITION_TYPE_ERROR));
683 }
684 if value > (usize::MAX as f64) {
685 return Err(runtime_error_for(POSITION_TYPE_ERROR));
686 }
687 Ok(value as usize)
688}
689
690fn parse_position_int(value: IntValue) -> BuiltinResult<usize> {
691 let val = value.to_i64();
692 if val <= 0 {
693 return Err(runtime_error_for(POSITION_TYPE_ERROR));
694 }
695 Ok(val as usize)
696}
697
698#[cfg(test)]
699pub(crate) mod tests {
700 #![allow(non_snake_case)]
701
702 use super::*;
703 use runmat_builtins::{CellArray, CharArray, ResolveContext, StringArray, Tensor, Type};
704
705 fn erase_between_builtin(
706 text: Value,
707 start: Value,
708 stop: Value,
709 rest: Vec<Value>,
710 ) -> BuiltinResult<Value> {
711 futures::executor::block_on(super::erase_between_builtin(text, start, stop, rest))
712 }
713
714 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
715 #[test]
716 fn eraseBetween_text_default_exclusive() {
717 let result = erase_between_builtin(
718 Value::String("The quick brown fox".into()),
719 Value::String("quick".into()),
720 Value::String(" fox".into()),
721 Vec::new(),
722 )
723 .expect("eraseBetween");
724 assert_eq!(result, Value::String("The quick fox".into()));
725 }
726
727 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
728 #[test]
729 fn eraseBetween_text_inclusive_option() {
730 let result = erase_between_builtin(
731 Value::String("The quick brown fox jumps over the lazy dog".into()),
732 Value::String(" brown".into()),
733 Value::String("lazy".into()),
734 vec![
735 Value::String("Boundaries".into()),
736 Value::String("inclusive".into()),
737 ],
738 )
739 .expect("eraseBetween");
740 assert_eq!(result, Value::String("The quick dog".into()));
741 }
742
743 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
744 #[test]
745 fn eraseBetween_numeric_positions_default_inclusive() {
746 let result = erase_between_builtin(
747 Value::String("Edgar Allen Poe".into()),
748 Value::Num(6.0),
749 Value::Num(11.0),
750 Vec::new(),
751 )
752 .expect("eraseBetween");
753 assert_eq!(result, Value::String("Edgar Poe".into()));
754 }
755
756 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
757 #[test]
758 fn eraseBetween_numeric_positions_int_inputs() {
759 let result = erase_between_builtin(
760 Value::String("abcdef".into()),
761 Value::Int(IntValue::I32(2)),
762 Value::Int(IntValue::I32(5)),
763 Vec::new(),
764 )
765 .expect("eraseBetween");
766 assert_eq!(result, Value::String("af".into()));
767 }
768
769 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
770 #[test]
771 fn eraseBetween_numeric_positions_exclusive_option() {
772 let result = erase_between_builtin(
773 Value::String("small|medium|large".into()),
774 Value::Num(6.0),
775 Value::Num(13.0),
776 vec![
777 Value::String("Boundaries".into()),
778 Value::String("exclusive".into()),
779 ],
780 )
781 .expect("eraseBetween");
782 assert_eq!(result, Value::String("small||large".into()));
783 }
784
785 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
786 #[test]
787 fn eraseBetween_start_not_found_returns_original() {
788 let result = erase_between_builtin(
789 Value::String("RunMat Accelerate".into()),
790 Value::String("<".into()),
791 Value::String(">".into()),
792 Vec::new(),
793 )
794 .expect("eraseBetween");
795 assert_eq!(result, Value::String("RunMat Accelerate".into()));
796 }
797
798 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
799 #[test]
800 fn eraseBetween_stop_not_found_returns_original() {
801 let result = erase_between_builtin(
802 Value::String("Device<GPU>".into()),
803 Value::String("<".into()),
804 Value::String(")".into()),
805 Vec::new(),
806 )
807 .expect("eraseBetween");
808 assert_eq!(result, Value::String("Device<GPU>".into()));
809 }
810
811 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
812 #[test]
813 fn eraseBetween_missing_string_propagates() {
814 let strings = StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap();
815 let result = erase_between_builtin(
816 Value::StringArray(strings),
817 Value::String("<".into()),
818 Value::String(">".into()),
819 Vec::new(),
820 )
821 .expect("eraseBetween");
822 assert_eq!(
823 result,
824 Value::StringArray(StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap())
825 );
826 }
827
828 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
829 #[test]
830 fn eraseBetween_zero_sized_broadcast_produces_empty_array() {
831 let start = StringArray::new(Vec::new(), vec![0, 1]).unwrap();
832 let stop = StringArray::new(Vec::new(), vec![0, 1]).unwrap();
833 let result = erase_between_builtin(
834 Value::String("abc".into()),
835 Value::StringArray(start),
836 Value::StringArray(stop),
837 Vec::new(),
838 )
839 .expect("eraseBetween");
840 match result {
841 Value::StringArray(sa) => {
842 assert_eq!(sa.data.len(), 0);
843 assert_eq!(sa.shape, vec![0, 1]);
844 }
845 other => panic!("expected string array, got {other:?}"),
846 }
847 }
848
849 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
850 #[test]
851 fn eraseBetween_numeric_positions_array() {
852 let text = StringArray::new(vec!["abcd".into(), "wxyz".into()], vec![2, 1]).unwrap();
853 let start = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
854 let stop = Tensor::new(vec![3.0, 4.0], vec![2, 1]).unwrap();
855 let result = erase_between_builtin(
856 Value::StringArray(text),
857 Value::Tensor(start),
858 Value::Tensor(stop),
859 Vec::new(),
860 )
861 .expect("eraseBetween");
862 match result {
863 Value::StringArray(sa) => {
864 assert_eq!(sa.data, vec!["d".to_string(), "w".to_string()]);
865 assert_eq!(sa.shape, vec![2, 1]);
866 }
867 other => panic!("expected string array, got {other:?}"),
868 }
869 }
870
871 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
872 #[test]
873 fn eraseBetween_cell_array_preserves_types() {
874 let cell = CellArray::new(
875 vec![
876 Value::CharArray(CharArray::new_row("A[B]C")),
877 Value::String("Planner<GPU>".into()),
878 ],
879 1,
880 2,
881 )
882 .unwrap();
883 let start = CellArray::new(
884 vec![Value::String("[".into()), Value::String("<".into())],
885 1,
886 2,
887 )
888 .unwrap();
889 let stop = CellArray::new(
890 vec![Value::String("]".into()), Value::String(">".into())],
891 1,
892 2,
893 )
894 .unwrap();
895 let result = erase_between_builtin(
896 Value::Cell(cell),
897 Value::Cell(start),
898 Value::Cell(stop),
899 vec![
900 Value::String("Boundaries".into()),
901 Value::String("inclusive".into()),
902 ],
903 )
904 .expect("eraseBetween");
905 match result {
906 Value::Cell(out) => {
907 let first = out.get(0, 0).unwrap();
908 let second = out.get(0, 1).unwrap();
909 assert_eq!(first, Value::CharArray(CharArray::new_row("AC")));
910 assert_eq!(second, Value::String("Planner".into()));
911 }
912 other => panic!("expected cell array, got {other:?}"),
913 }
914 }
915
916 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
917 #[test]
918 fn eraseBetween_char_array_default_and_inclusive() {
919 let chars =
920 CharArray::new("Device<GPU>".chars().collect(), 1, "Device<GPU>".len()).unwrap();
921 let default = erase_between_builtin(
922 Value::CharArray(chars.clone()),
923 Value::String("<".into()),
924 Value::String(">".into()),
925 Vec::new(),
926 )
927 .expect("eraseBetween");
928 match default {
929 Value::CharArray(out) => {
930 let text: String = out.data.iter().collect();
931 assert_eq!(text.trim_end(), "Device<>");
932 }
933 other => panic!("expected char array, got {other:?}"),
934 }
935
936 let inclusive = erase_between_builtin(
937 Value::CharArray(chars),
938 Value::String("<".into()),
939 Value::String(">".into()),
940 vec![
941 Value::String("Boundaries".into()),
942 Value::String("inclusive".into()),
943 ],
944 )
945 .expect("eraseBetween");
946 match inclusive {
947 Value::CharArray(out) => {
948 let text: String = out.data.iter().collect();
949 assert_eq!(text.trim_end(), "Device");
950 }
951 other => panic!("expected char array, got {other:?}"),
952 }
953 }
954
955 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
956 #[test]
957 fn eraseBetween_option_with_char_arrays_case_insensitive() {
958 let result = erase_between_builtin(
959 Value::String("A<mid>B".into()),
960 Value::String("<".into()),
961 Value::String(">".into()),
962 vec![
963 Value::CharArray(CharArray::new_row("Boundaries")),
964 Value::CharArray(CharArray::new_row("INCLUSIVE")),
965 ],
966 )
967 .expect("eraseBetween");
968 assert_eq!(result, Value::String("AB".into()));
969 }
970
971 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
972 #[test]
973 fn eraseBetween_text_scalar_broadcast() {
974 let text =
975 StringArray::new(vec!["alpha[GPU]".into(), "beta[GPU]".into()], vec![2, 1]).unwrap();
976 let result = erase_between_builtin(
977 Value::StringArray(text),
978 Value::String("[".into()),
979 Value::String("]".into()),
980 Vec::new(),
981 )
982 .expect("eraseBetween");
983 match result {
984 Value::StringArray(sa) => {
985 assert_eq!(sa.data, vec!["alpha[]".to_string(), "beta[]".to_string()]);
986 }
987 other => panic!("expected string array, got {other:?}"),
988 }
989 }
990
991 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
992 #[test]
993 fn eraseBetween_option_invalid_value() {
994 let err = erase_between_builtin(
995 Value::String("abc".into()),
996 Value::String("a".into()),
997 Value::String("c".into()),
998 vec![
999 Value::String("Boundaries".into()),
1000 Value::String("middle".into()),
1001 ],
1002 )
1003 .unwrap_err();
1004 assert_eq!(err.to_string(), OPTION_VALUE_ERROR);
1005 }
1006
1007 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1008 #[test]
1009 fn eraseBetween_option_name_error() {
1010 let err = erase_between_builtin(
1011 Value::String("abc".into()),
1012 Value::String("a".into()),
1013 Value::String("c".into()),
1014 vec![
1015 Value::String("Padding".into()),
1016 Value::String("inclusive".into()),
1017 ],
1018 )
1019 .unwrap_err();
1020 assert_eq!(err.to_string(), OPTION_NAME_ERROR);
1021 }
1022
1023 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1024 #[test]
1025 fn eraseBetween_option_pair_error() {
1026 let err = erase_between_builtin(
1027 Value::String("abc".into()),
1028 Value::String("a".into()),
1029 Value::String("b".into()),
1030 vec![Value::String("Boundaries".into())],
1031 )
1032 .unwrap_err();
1033 assert_eq!(err.to_string(), OPTION_PAIR_ERROR);
1034 }
1035
1036 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1037 #[test]
1038 fn eraseBetween_position_type_error() {
1039 let err = erase_between_builtin(
1040 Value::String("abc".into()),
1041 Value::Num(0.5),
1042 Value::Num(2.0),
1043 Vec::new(),
1044 )
1045 .unwrap_err();
1046 assert_eq!(err.to_string(), POSITION_TYPE_ERROR);
1047 }
1048
1049 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1050 #[test]
1051 fn eraseBetween_mixed_boundary_error() {
1052 let err = erase_between_builtin(
1053 Value::String("abc".into()),
1054 Value::String("a".into()),
1055 Value::Num(3.0),
1056 Vec::new(),
1057 )
1058 .unwrap_err();
1059 assert_eq!(err.to_string(), BOUNDARY_TYPE_ERROR);
1060 }
1061
1062 #[test]
1063 fn erase_between_type_preserves_text() {
1064 assert_eq!(
1065 text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
1066 Type::String
1067 );
1068 }
1069}