Skip to main content

runmat_runtime/builtins/strings/transform/
erasebetween.rs

1//! MATLAB-compatible `eraseBetween` builtin with GPU-aware semantics for RunMat.
2
3use 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}