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::{
13 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
14 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
15 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
16 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
17 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
18 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
19};
20use runmat_macros::runtime_builtin;
21use runmat_value::{CharArray, IntValue, NumericScalar, StringArray, Value};
22
23use crate::builtins::common::spec::{
24 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
25 ReductionNaN, ResidencyPolicy, ShapeRequirements,
26};
27
28#[runmat_macros::register_gpu_spec(
29 builtin_path = "crate::builtins::strings::transform::extractbetween"
30)]
31pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
32 name: "extractBetween",
33 op_kind: GpuOpKind::Custom("string-transform"),
34 supported_precisions: &[],
35 broadcast: BroadcastSemantics::Matlab,
36 provider_hooks: &[],
37 constant_strategy: ConstantStrategy::InlineLiteral,
38 residency: ResidencyPolicy::GatherImmediately,
39 nan_mode: ReductionNaN::Include,
40 two_pass_threshold: None,
41 workgroup_size: None,
42 accepts_nan_mode: false,
43 notes: "Runs on the CPU; GPU-resident inputs are gathered before extraction and outputs are returned on the host.",
44};
45
46#[runmat_macros::register_fusion_spec(
47 builtin_path = "crate::builtins::strings::transform::extractbetween"
48)]
49pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
50 name: "extractBetween",
51 shape: ShapeRequirements::Any,
52 constant_strategy: ConstantStrategy::InlineLiteral,
53 elementwise: None,
54 reduction: None,
55 emits_nan: false,
56 notes: "Pure string manipulation builtin; excluded from fusion plans and gathers GPU inputs immediately.",
57};
58
59const BUILTIN_NAME: &str = "extractBetween";
60
61const FULL_BROADCAST_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
62 id: "extractbetween-full-broadcast",
63 mode: BuiltinExtensionMode::RunMatOnly,
64 description: "extractBetween with non-scalar boundary expansion is a RunMat extension",
65 error_identifier: Some("RunMat:compatibility:ExtractBetweenFullBroadcastExtension"),
66};
67const RESIDENT_POSITION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
68 id: "extractbetween-resident-position",
69 mode: BuiltinExtensionMode::RunMatOnly,
70 description: "extractBetween with resident numeric positions is a RunMat extension",
71 error_identifier: Some("RunMat:compatibility:ExtractBetweenResidentPositionExtension"),
72};
73const CHAR_MATRIX_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
74 id: "extractbetween-char-matrix",
75 mode: BuiltinExtensionMode::RunMatOnly,
76 description: "extractBetween row-wise character-matrix input is a RunMat extension",
77 error_identifier: Some("RunMat:compatibility:ExtractBetweenCharMatrixExtension"),
78};
79const STRING_CELL_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
80 id: "extractbetween-string-cell",
81 mode: BuiltinExtensionMode::RunMatOnly,
82 description: "extractBetween cells containing string scalars are a RunMat extension",
83 error_identifier: Some("RunMat:compatibility:ExtractBetweenStringCellExtension"),
84};
85const EXTENSIONS: [BuiltinExtensionDescriptor; 4] = [
86 FULL_BROADCAST_EXTENSION,
87 RESIDENT_POSITION_EXTENSION,
88 CHAR_MATRIX_EXTENSION,
89 STRING_CELL_EXTENSION,
90];
91const INTEGER_POSITION_INPUTS: [BuiltinIntegerInputCapability; 2] = [
92 BuiltinIntegerInputCapability {
93 name: "startPos",
94 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
95 availability: BuiltinIntegerInputAvailability::Documented,
96 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
97 notes: "The public numeric position accepts every built-in integer class and is read exactly with one-based indexing.",
98 },
99 BuiltinIntegerInputCapability {
100 name: "endPos",
101 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
102 availability: BuiltinIntegerInputAvailability::Documented,
103 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
104 notes: "The public numeric position accepts every built-in integer class and is read exactly with one-based indexing.",
105 },
106];
107const INTEGER_TEXT_INPUTS: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
108 name: "str",
109 classes: &[],
110 availability: BuiltinIntegerInputAvailability::Rejected,
111 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
112 notes: "The first argument is text; integer data rejects before provider access.",
113}];
114pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
115 BuiltinIntegerCapabilityDescriptor {
116 form: "newStr = extractBetween(str, integer_startPos, integer_endPos)",
117 inputs: &INTEGER_POSITION_INPUTS,
118 computation_domain: BuiltinIntegerComputationDomain::Structural,
119 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
120 overflow: BuiltinIntegerOverflowRule::Error,
121 backend: BuiltinIntegerBackendRule::HostOnly,
122 overload: BuiltinIntegerOverloadKind::SameSizeOrScalar,
123 notes: "Strict compatibility accepts scalar positions or arrays exactly the same size as str; string input returns string and other documented text input returns cellstr.",
124 },
125 BuiltinIntegerCapabilityDescriptor {
126 form: "newStr = extractBetween(integer_str, start, end)",
127 inputs: &INTEGER_TEXT_INPUTS,
128 computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
129 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
130 overflow: BuiltinIntegerOverflowRule::NotApplicable,
131 backend: BuiltinIntegerBackendRule::HostOnly,
132 overload: BuiltinIntegerOverloadKind::FunctionSpecific,
133 notes: "Integer text input is outside the public domain and rejects without numeric-to-text conversion.",
134 },
135];
136
137const EXTRACT_BETWEEN_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
138 name: "newText",
139 ty: BuiltinParamType::Any,
140 arity: BuiltinParamArity::Required,
141 default: None,
142 description: "Extracted text preserving scalar/array/cell text container semantics.",
143}];
144
145const EXTRACT_BETWEEN_INPUTS_BASE: [BuiltinParamDescriptor; 3] = [
146 BuiltinParamDescriptor {
147 name: "str",
148 ty: BuiltinParamType::Any,
149 arity: BuiltinParamArity::Required,
150 default: None,
151 description: "Input text scalar/array/cell.",
152 },
153 BuiltinParamDescriptor {
154 name: "start",
155 ty: BuiltinParamType::Any,
156 arity: BuiltinParamArity::Required,
157 default: None,
158 description: "Start boundary marker text or positive integer position(s).",
159 },
160 BuiltinParamDescriptor {
161 name: "end",
162 ty: BuiltinParamType::Any,
163 arity: BuiltinParamArity::Required,
164 default: None,
165 description: "End boundary marker text or positive integer position(s).",
166 },
167];
168
169const EXTRACT_BETWEEN_INPUTS_NAME_VALUE: [BuiltinParamDescriptor; 5] = [
170 BuiltinParamDescriptor {
171 name: "str",
172 ty: BuiltinParamType::Any,
173 arity: BuiltinParamArity::Required,
174 default: None,
175 description: "Input text scalar/array/cell.",
176 },
177 BuiltinParamDescriptor {
178 name: "start",
179 ty: BuiltinParamType::Any,
180 arity: BuiltinParamArity::Required,
181 default: None,
182 description: "Start boundary marker text or positive integer position(s).",
183 },
184 BuiltinParamDescriptor {
185 name: "end",
186 ty: BuiltinParamType::Any,
187 arity: BuiltinParamArity::Required,
188 default: None,
189 description: "End boundary marker text or positive integer position(s).",
190 },
191 BuiltinParamDescriptor {
192 name: "Name",
193 ty: BuiltinParamType::StringScalar,
194 arity: BuiltinParamArity::Required,
195 default: None,
196 description: "Option name (`Boundaries`).",
197 },
198 BuiltinParamDescriptor {
199 name: "Value",
200 ty: BuiltinParamType::Any,
201 arity: BuiltinParamArity::Variadic,
202 default: None,
203 description: "Option value and additional Name/Value pairs.",
204 },
205];
206
207const EXTRACT_BETWEEN_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
208 BuiltinSignatureDescriptor {
209 label: "newText = extractBetween(str, start, end)",
210 inputs: &EXTRACT_BETWEEN_INPUTS_BASE,
211 outputs: &EXTRACT_BETWEEN_OUTPUT,
212 },
213 BuiltinSignatureDescriptor {
214 label: "newText = extractBetween(str, start, end, Name, Value, ...)",
215 inputs: &EXTRACT_BETWEEN_INPUTS_NAME_VALUE,
216 outputs: &EXTRACT_BETWEEN_OUTPUT,
217 },
218];
219
220const EXTRACT_BETWEEN_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
221 code: "RM.EXTRACT_BETWEEN.INVALID_INPUT",
222 identifier: Some("RunMat:extractBetween:InvalidInput"),
223 when: "First argument is not a string array, character array, or cell array of text scalars.",
224 message:
225 "extractBetween: first argument must be a string array, character array, or cell array of character vectors",
226};
227
228const EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
229 code: "RM.EXTRACT_BETWEEN.BOUNDARY_TYPE",
230 identifier: Some("RunMat:extractBetween:BoundaryType"),
231 when: "Start/end boundaries are mixed text/numeric domains or use unsupported boundary types.",
232 message:
233 "extractBetween: start and end arguments must both be text or both be numeric positions",
234};
235
236const EXTRACT_BETWEEN_ERROR_POSITION_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
237 code: "RM.EXTRACT_BETWEEN.POSITION_TYPE",
238 identifier: Some("RunMat:extractBetween:PositionType"),
239 when: "Numeric boundary positions are not positive finite integers.",
240 message: "extractBetween: position arguments must be positive integers",
241};
242
243const EXTRACT_BETWEEN_ERROR_NAME_VALUE_PAIR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
244 code: "RM.EXTRACT_BETWEEN.NAME_VALUE_PAIR",
245 identifier: Some("RunMat:extractBetween:NameValuePair"),
246 when: "Name/value options are not supplied in complete pairs.",
247 message: "extractBetween: name-value arguments must appear in pairs",
248};
249
250const EXTRACT_BETWEEN_ERROR_OPTION_NAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
251 code: "RM.EXTRACT_BETWEEN.OPTION_NAME",
252 identifier: Some("RunMat:extractBetween:OptionName"),
253 when: "An option name other than `Boundaries` was supplied.",
254 message: "extractBetween: unrecognized parameter name",
255};
256
257const EXTRACT_BETWEEN_ERROR_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
258 code: "RM.EXTRACT_BETWEEN.OPTION_VALUE",
259 identifier: Some("RunMat:extractBetween:OptionValue"),
260 when: "`Boundaries` option value is not `inclusive` or `exclusive`.",
261 message: "extractBetween: 'Boundaries' must be either 'inclusive' or 'exclusive'",
262};
263
264const EXTRACT_BETWEEN_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
265 code: "RM.EXTRACT_BETWEEN.CELL_ELEMENT",
266 identifier: Some("RunMat:extractBetween:CellElement"),
267 when: "Cell text input/boundary contains non-text values or non-row char arrays.",
268 message: "extractBetween: cell array elements must be string scalars or character vectors",
269};
270
271const EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
272 code: "RM.EXTRACT_BETWEEN.SIZE_MISMATCH",
273 identifier: Some("RunMat:extractBetween:SizeMismatch"),
274 when: "Text/boundary inputs are not broadcast-compatible for extraction.",
275 message: "extractBetween: boundary sizes must be compatible with the text input",
276};
277
278const EXTRACT_BETWEEN_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
279 code: "RM.EXTRACT_BETWEEN.INTERNAL",
280 identifier: Some("RunMat:extractBetween:InternalError"),
281 when: "Internal output construction failed.",
282 message: "extractBetween: internal error",
283};
284
285const EXTRACT_BETWEEN_ERRORS: [BuiltinErrorDescriptor; 9] = [
286 EXTRACT_BETWEEN_ERROR_INVALID_INPUT,
287 EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE,
288 EXTRACT_BETWEEN_ERROR_POSITION_TYPE,
289 EXTRACT_BETWEEN_ERROR_NAME_VALUE_PAIR,
290 EXTRACT_BETWEEN_ERROR_OPTION_NAME,
291 EXTRACT_BETWEEN_ERROR_OPTION_VALUE,
292 EXTRACT_BETWEEN_ERROR_CELL_ELEMENT,
293 EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH,
294 EXTRACT_BETWEEN_ERROR_INTERNAL,
295];
296
297pub const EXTRACT_BETWEEN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
298 signatures: &EXTRACT_BETWEEN_SIGNATURES,
299 output_mode: BuiltinOutputMode::Fixed,
300 completion_policy: BuiltinCompletionPolicy::Public,
301 errors: &EXTRACT_BETWEEN_ERRORS,
302};
303
304fn extract_between_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
305 extract_between_error_with_message(error.message, error)
306}
307
308fn extract_between_error_with_message(
309 message: impl Into<String>,
310 error: &'static BuiltinErrorDescriptor,
311) -> RuntimeError {
312 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
313 if let Some(identifier) = error.identifier {
314 builder = builder.with_identifier(identifier);
315 }
316 builder.build()
317}
318
319fn map_flow(err: RuntimeError) -> RuntimeError {
320 map_control_flow_with_builtin(err, BUILTIN_NAME)
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324enum BoundariesMode {
325 Exclusive,
326 Inclusive,
327}
328
329#[runtime_builtin(
330 name = "extractBetween",
331 category = "strings/transform",
332 summary = "Extract substrings between boundary markers.",
333 keywords = "extractBetween,substring,boundaries,strings",
334 accel = "sink",
335 extensions(EXTENSIONS),
336 integer_capabilities(INTEGER_CAPABILITIES),
337 type_resolver(text_preserve_type),
338 descriptor(crate::builtins::strings::transform::extractbetween::EXTRACT_BETWEEN_DESCRIPTOR),
339 builtin_path = "crate::builtins::strings::transform::extractbetween"
340)]
341async fn extract_between_builtin(
342 text: Value,
343 start: Value,
344 stop: Value,
345 rest: Vec<Value>,
346) -> BuiltinResult<Value> {
347 if is_numeric_or_resident(&text) || contains_nested_numeric_or_resident(&text) {
348 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_INVALID_INPUT));
349 }
350 if is_resident_or_contains_resident(&start) || is_resident_or_contains_resident(&stop) {
351 crate::compatibility::ensure_builtin_extension_enabled(
352 &RESIDENT_POSITION_EXTENSION,
353 BUILTIN_NAME,
354 )?;
355 }
356 if is_char_matrix(&text) || is_char_matrix(&start) || is_char_matrix(&stop) {
357 crate::compatibility::ensure_builtin_extension_enabled(
358 &CHAR_MATRIX_EXTENSION,
359 BUILTIN_NAME,
360 )?;
361 }
362 if cell_contains_string(&text) || cell_contains_string(&start) || cell_contains_string(&stop) {
363 crate::compatibility::ensure_builtin_extension_enabled(
364 &STRING_CELL_EXTENSION,
365 BUILTIN_NAME,
366 )?;
367 }
368 if raw_boundary_uses_full_broadcast(&text, &start)
369 || raw_boundary_uses_full_broadcast(&text, &stop)
370 {
371 crate::compatibility::ensure_builtin_extension_enabled(
372 &FULL_BROADCAST_EXTENSION,
373 BUILTIN_NAME,
374 )?;
375 }
376 if rest
377 .iter()
378 .any(|value| is_numeric_or_resident(value) || contains_nested_numeric_or_resident(value))
379 {
380 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_OPTION_VALUE));
381 }
382 let text = gather_if_needed_async(&text).await.map_err(map_flow)?;
383 let start = gather_if_needed_async(&start).await.map_err(map_flow)?;
384 let stop = gather_if_needed_async(&stop).await.map_err(map_flow)?;
385
386 let mode_override = parse_boundaries_option(&rest).await?;
387
388 let normalized_text = NormalizedText::from_value(text)?;
389 let start_boundary = BoundaryArg::from_value(start)?;
390 let stop_boundary = BoundaryArg::from_value(stop)?;
391
392 if start_boundary.kind() != stop_boundary.kind() {
393 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE));
394 }
395 let boundary_kind = start_boundary.kind();
396 let effective_mode = mode_override.unwrap_or(match boundary_kind {
397 BoundaryKind::Text => BoundariesMode::Exclusive,
398 BoundaryKind::Position => BoundariesMode::Inclusive,
399 });
400
401 let start_shape = start_boundary.shape();
402 let stop_shape = stop_boundary.shape();
403 let text_shape = normalized_text.shape();
404
405 if !shape_is_scalar_or_same(start_shape, text_shape)
406 || !shape_is_scalar_or_same(stop_shape, text_shape)
407 {
408 crate::compatibility::ensure_builtin_extension_enabled(
409 &FULL_BROADCAST_EXTENSION,
410 BUILTIN_NAME,
411 )?;
412 }
413
414 let shape_ts = broadcast_shapes(BUILTIN_NAME, text_shape, start_shape).map_err(|err| {
415 extract_between_error_with_message(
416 format!("{}: {err}", EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH.message),
417 &EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH,
418 )
419 })?;
420 let output_shape = broadcast_shapes(BUILTIN_NAME, &shape_ts, stop_shape).map_err(|err| {
421 extract_between_error_with_message(
422 format!("{}: {err}", EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH.message),
423 &EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH,
424 )
425 })?;
426 if !normalized_text.supports_shape(&output_shape) {
427 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH));
428 }
429
430 let total: usize = output_shape.iter().copied().product();
431 if total == 0 {
432 return normalized_text.into_value(Vec::new(), output_shape);
433 }
434
435 let text_strides = compute_strides(text_shape);
436 let start_strides = compute_strides(start_shape);
437 let stop_strides = compute_strides(stop_shape);
438
439 let mut results = Vec::with_capacity(total);
440
441 for idx in 0..total {
442 let text_idx = broadcast_index(idx, &output_shape, text_shape, &text_strides);
443 let start_idx = broadcast_index(idx, &output_shape, start_shape, &start_strides);
444 let stop_idx = broadcast_index(idx, &output_shape, stop_shape, &stop_strides);
445
446 let result = match boundary_kind {
447 BoundaryKind::Text => {
448 let text_value = normalized_text.data(text_idx);
449 let start_value = start_boundary.text(start_idx);
450 let stop_value = stop_boundary.text(stop_idx);
451 extract_with_text_boundaries(text_value, start_value, stop_value, effective_mode)
452 }
453 BoundaryKind::Position => {
454 let text_value = normalized_text.data(text_idx);
455 let start_value = start_boundary.position(start_idx);
456 let stop_value = stop_boundary.position(stop_idx);
457 extract_with_positions(text_value, start_value, stop_value, effective_mode)
458 }
459 };
460 results.push(result);
461 }
462
463 normalized_text.into_value(results, output_shape)
464}
465
466fn is_numeric_or_resident(value: &Value) -> bool {
467 matches!(
468 value,
469 Value::Num(_)
470 | Value::Int(_)
471 | Value::Bool(_)
472 | Value::Tensor(_)
473 | Value::LogicalArray(_)
474 | Value::Complex(_, _)
475 | Value::ComplexTensor(_)
476 | Value::GpuTensor(_)
477 )
478}
479
480fn contains_nested_numeric_or_resident(value: &Value) -> bool {
481 match value {
482 Value::Cell(cell) => cell.data.iter().any(|value| {
483 is_numeric_or_resident(value) || contains_nested_numeric_or_resident(value)
484 }),
485 _ => false,
486 }
487}
488
489fn is_resident_or_contains_resident(value: &Value) -> bool {
490 match value {
491 Value::GpuTensor(_) => true,
492 Value::Cell(cell) => cell.data.iter().any(is_resident_or_contains_resident),
493 _ => false,
494 }
495}
496
497fn is_char_matrix(value: &Value) -> bool {
498 matches!(value, Value::CharArray(array) if array.rows > 1)
499}
500
501fn cell_contains_string(value: &Value) -> bool {
502 match value {
503 Value::Cell(cell) => cell.data.iter().any(|value| {
504 matches!(value, Value::String(_) | Value::StringArray(_)) || cell_contains_string(value)
505 }),
506 _ => false,
507 }
508}
509
510fn raw_text_shape(value: &Value) -> Option<Vec<usize>> {
511 match value {
512 Value::String(_) => Some(vec![1, 1]),
513 Value::StringArray(array) => Some(array.shape.clone()),
514 Value::CharArray(array) if array.rows <= 1 => Some(vec![1, 1]),
515 Value::CharArray(array) => Some(vec![array.rows, 1]),
516 Value::Cell(cell) => Some(cell.shape.clone()),
517 _ => None,
518 }
519}
520
521fn raw_boundary_shape(value: &Value) -> Option<Vec<usize>> {
522 match value {
523 Value::String(_) | Value::Num(_) | Value::Int(_) | Value::Object(_) => Some(vec![1, 1]),
524 Value::StringArray(array) => Some(array.shape.clone()),
525 Value::CharArray(array) if array.rows <= 1 => Some(vec![1, 1]),
526 Value::CharArray(array) => Some(vec![array.rows, 1]),
527 Value::Cell(cell) => Some(cell.shape.clone()),
528 Value::Tensor(tensor) => Some(tensor.shape.clone()),
529 Value::GpuTensor(handle) => Some(handle.shape.clone()),
530 _ => None,
531 }
532}
533
534fn raw_boundary_uses_full_broadcast(text: &Value, boundary: &Value) -> bool {
535 let (Some(text_shape), Some(boundary_shape)) =
536 (raw_text_shape(text), raw_boundary_shape(boundary))
537 else {
538 return false;
539 };
540 !shape_is_scalar_or_same(&boundary_shape, &text_shape)
541}
542
543fn shape_is_scalar_or_same(shape: &[usize], text_shape: &[usize]) -> bool {
544 shape
545 .iter()
546 .try_fold(1usize, |acc, dim| acc.checked_mul(*dim))
547 == Some(1)
548 || shape == text_shape
549}
550
551async fn parse_boundaries_option(args: &[Value]) -> BuiltinResult<Option<BoundariesMode>> {
552 if args.is_empty() {
553 return Ok(None);
554 }
555 if !args.len().is_multiple_of(2) {
556 return Err(extract_between_error(
557 &EXTRACT_BETWEEN_ERROR_NAME_VALUE_PAIR,
558 ));
559 }
560
561 let mut mode: Option<BoundariesMode> = None;
562 let mut idx = 0;
563 while idx < args.len() {
564 let name_value = gather_if_needed_async(&args[idx]).await.map_err(map_flow)?;
565 let name = value_to_string(&name_value)
566 .ok_or_else(|| extract_between_error(&EXTRACT_BETWEEN_ERROR_OPTION_NAME))?;
567 if !name.eq_ignore_ascii_case("boundaries") {
568 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_OPTION_NAME));
569 }
570 let value = gather_if_needed_async(&args[idx + 1])
571 .await
572 .map_err(map_flow)?;
573 let value_str = value_to_string(&value)
574 .ok_or_else(|| extract_between_error(&EXTRACT_BETWEEN_ERROR_OPTION_VALUE))?;
575 let parsed_mode = if value_str.eq_ignore_ascii_case("inclusive") {
576 BoundariesMode::Inclusive
577 } else if value_str.eq_ignore_ascii_case("exclusive") {
578 BoundariesMode::Exclusive
579 } else {
580 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_OPTION_VALUE));
581 };
582 mode = Some(parsed_mode);
583 idx += 2;
584 }
585 Ok(mode)
586}
587
588fn value_to_string(value: &Value) -> Option<String> {
589 match value {
590 Value::String(s) => Some(s.clone()),
591 Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
592 Value::CharArray(ca) if ca.rows <= 1 => {
593 if ca.rows == 0 {
594 Some(String::new())
595 } else {
596 Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
597 }
598 }
599 Value::CharArray(_) => None,
600 Value::Cell(cell) if cell.data.len() == 1 => {
601 let element = &cell.data[0];
602 value_to_string(element)
603 }
604 _ => None,
605 }
606}
607
608#[derive(Clone)]
609struct ExtractResult {
610 text: String,
611}
612
613impl ExtractResult {
614 fn missing() -> Self {
615 Self {
616 text: "<missing>".to_string(),
617 }
618 }
619
620 fn text(text: String) -> Self {
621 Self { text }
622 }
623}
624
625fn extract_with_text_boundaries(
626 text: &str,
627 start: &str,
628 stop: &str,
629 mode: BoundariesMode,
630) -> ExtractResult {
631 if is_missing_string(text) || is_missing_string(start) || is_missing_string(stop) {
632 return ExtractResult::missing();
633 }
634
635 if let Some(start_idx) = text.find(start) {
636 let search_start = start_idx + start.len();
637 if search_start > text.len() {
638 return ExtractResult::text(String::new());
639 }
640 if let Some(relative_end) = text[search_start..].find(stop) {
641 let end_idx = search_start + relative_end;
642 match mode {
643 BoundariesMode::Inclusive => {
644 let end_capture = min(text.len(), end_idx + stop.len());
645 let slice = &text[start_idx..end_capture];
646 ExtractResult::text(slice.to_string())
647 }
648 BoundariesMode::Exclusive => {
649 if end_idx < search_start {
650 ExtractResult::text(String::new())
651 } else {
652 let slice = &text[search_start..end_idx];
653 ExtractResult::text(slice.to_string())
654 }
655 }
656 }
657 } else {
658 ExtractResult::text(String::new())
659 }
660 } else {
661 ExtractResult::text(String::new())
662 }
663}
664
665fn extract_with_positions(
666 text: &str,
667 start: usize,
668 stop: usize,
669 mode: BoundariesMode,
670) -> ExtractResult {
671 if is_missing_string(text) {
672 return ExtractResult::missing();
673 }
674 if text.is_empty() {
675 return ExtractResult::text(String::new());
676 }
677 let chars: Vec<char> = text.chars().collect();
678 let len = chars.len();
679 if len == 0 {
680 return ExtractResult::text(String::new());
681 }
682
683 if start == 0 || stop == 0 {
684 return ExtractResult::text(String::new());
685 }
686
687 if start > len {
688 return ExtractResult::text(String::new());
689 }
690 let stop_clamped = stop.min(len);
691 if stop_clamped == 0 {
692 return ExtractResult::text(String::new());
693 }
694
695 match mode {
696 BoundariesMode::Inclusive => {
697 if start > stop_clamped {
698 return ExtractResult::text(String::new());
699 }
700 let start_idx = start - 1;
701 let end_idx = stop_clamped - 1;
702 if start_idx >= len || end_idx >= len || start_idx > end_idx {
703 ExtractResult::text(String::new())
704 } else {
705 let slice: String = chars[start_idx..=end_idx].iter().collect();
706 ExtractResult::text(slice)
707 }
708 }
709 BoundariesMode::Exclusive => {
710 if start + 1 >= stop_clamped {
711 return ExtractResult::text(String::new());
712 }
713 let start_idx = start;
714 let end_idx = stop_clamped - 2;
715 if start_idx >= len || end_idx >= len || start_idx > end_idx {
716 ExtractResult::text(String::new())
717 } else {
718 let slice: String = chars[start_idx..=end_idx].iter().collect();
719 ExtractResult::text(slice)
720 }
721 }
722 }
723}
724
725#[derive(Clone, Debug)]
726struct CellInfo {
727 shape: Vec<usize>,
728 element_kinds: Vec<CellElementKind>,
729}
730
731#[derive(Clone, Debug)]
732enum CellElementKind {
733 String,
734 Char,
735}
736
737#[derive(Clone, Debug)]
738enum TextKind {
739 StringScalar,
740 StringArray,
741 CharArray { rows: usize },
742 CellArray(CellInfo),
743}
744
745#[derive(Clone, Debug)]
746struct NormalizedText {
747 data: Vec<String>,
748 shape: Vec<usize>,
749 kind: TextKind,
750}
751
752impl NormalizedText {
753 fn from_value(value: Value) -> BuiltinResult<Self> {
754 match value {
755 Value::String(s) => Ok(Self {
756 data: vec![s],
757 shape: vec![1, 1],
758 kind: TextKind::StringScalar,
759 }),
760 Value::StringArray(sa) => Ok(Self {
761 data: sa.data.clone(),
762 shape: sa.shape.clone(),
763 kind: TextKind::StringArray,
764 }),
765 Value::CharArray(ca) => {
766 let rows = ca.rows.max(1);
767 let mut data = Vec::with_capacity(rows);
768 if ca.rows == 0 {
769 data.push(String::new());
770 } else {
771 for row in 0..ca.rows {
772 data.push(char_row_to_string_slice(&ca.data, ca.cols, row));
773 }
774 }
775 Ok(Self {
776 data,
777 shape: if ca.rows <= 1 {
778 vec![1, 1]
779 } else {
780 vec![rows, 1]
781 },
782 kind: TextKind::CharArray { rows },
783 })
784 }
785 Value::Cell(cell) => {
786 let shape = cell.shape.clone();
787 let mut data = Vec::with_capacity(cell.data.len());
788 let mut kinds = Vec::with_capacity(cell.data.len());
789 for element in &cell.data {
790 match &element {
791 Value::String(s) => {
792 data.push(s.clone());
793 kinds.push(CellElementKind::String);
794 }
795 Value::StringArray(sa) if sa.data.len() == 1 => {
796 data.push(sa.data[0].clone());
797 kinds.push(CellElementKind::String);
798 }
799 Value::CharArray(ca) if ca.rows <= 1 => {
800 if ca.rows == 0 {
801 data.push(String::new());
802 } else {
803 data.push(char_row_to_string_slice(&ca.data, ca.cols, 0));
804 }
805 kinds.push(CellElementKind::Char);
806 }
807 Value::CharArray(_) => {
808 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_CELL_ELEMENT))
809 }
810 _ => {
811 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_CELL_ELEMENT))
812 }
813 }
814 }
815 Ok(Self {
816 data,
817 shape: shape.clone(),
818 kind: TextKind::CellArray(CellInfo {
819 shape,
820 element_kinds: kinds,
821 }),
822 })
823 }
824 _ => Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_INVALID_INPUT)),
825 }
826 }
827
828 fn shape(&self) -> &[usize] {
829 &self.shape
830 }
831
832 fn data(&self, idx: usize) -> &str {
833 &self.data[idx]
834 }
835
836 fn supports_shape(&self, output_shape: &[usize]) -> bool {
837 match &self.kind {
838 TextKind::StringScalar => true,
839 TextKind::StringArray => true,
840 TextKind::CharArray { .. } => output_shape == self.shape,
841 TextKind::CellArray(info) => output_shape == info.shape,
842 }
843 }
844
845 fn into_value(
846 self,
847 results: Vec<ExtractResult>,
848 output_shape: Vec<usize>,
849 ) -> BuiltinResult<Value> {
850 match self.kind {
851 TextKind::StringScalar => {
852 if results.len() <= 1 {
853 let value = results
854 .into_iter()
855 .next()
856 .unwrap_or_else(|| ExtractResult::text(String::new()));
857 Ok(Value::String(value.text))
858 } else {
859 let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
860 let array = StringArray::new(data, output_shape).map_err(|e| {
861 extract_between_error_with_message(
862 format!("{BUILTIN_NAME}: {e}"),
863 &EXTRACT_BETWEEN_ERROR_INTERNAL,
864 )
865 })?;
866 Ok(Value::StringArray(array))
867 }
868 }
869 TextKind::StringArray => {
870 let data = results.into_iter().map(|r| r.text).collect::<Vec<_>>();
871 let array = StringArray::new(data, output_shape).map_err(|e| {
872 extract_between_error_with_message(
873 format!("{BUILTIN_NAME}: {e}"),
874 &EXTRACT_BETWEEN_ERROR_INTERNAL,
875 )
876 })?;
877 Ok(Value::StringArray(array))
878 }
879 TextKind::CharArray { rows } => {
880 if results.len() != rows {
881 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH));
882 }
883 if rows == 1 {
884 let text = results
885 .into_iter()
886 .next()
887 .unwrap_or_else(|| ExtractResult::text(String::new()))
888 .text;
889 return make_cell_with_shape(
890 vec![Value::CharArray(CharArray::new_row(&text))],
891 vec![1, 1],
892 )
893 .map_err(|e| {
894 extract_between_error_with_message(
895 format!("{BUILTIN_NAME}: {e}"),
896 &EXTRACT_BETWEEN_ERROR_INTERNAL,
897 )
898 });
899 }
900 let mut max_width = 0usize;
901 let mut row_strings = Vec::with_capacity(rows);
902 for result in &results {
903 let width = result.text.chars().count();
904 max_width = max_width.max(width);
905 row_strings.push(result.text.clone());
906 }
907 let mut flattened = Vec::with_capacity(rows * max_width);
908 for row in row_strings {
909 let mut chars: Vec<char> = row.chars().collect();
910 if chars.len() < max_width {
911 chars.resize(max_width, ' ');
912 }
913 flattened.extend(chars);
914 }
915 CharArray::new(flattened, rows, max_width)
916 .map(Value::CharArray)
917 .map_err(|e| {
918 extract_between_error_with_message(
919 format!("{BUILTIN_NAME}: {e}"),
920 &EXTRACT_BETWEEN_ERROR_INTERNAL,
921 )
922 })
923 }
924 TextKind::CellArray(info) => {
925 if results.len() != info.element_kinds.len() {
926 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_SIZE_MISMATCH));
927 }
928 let mut values = Vec::with_capacity(results.len());
929 for (idx, result) in results.into_iter().enumerate() {
930 match info.element_kinds[idx] {
931 CellElementKind::String => values.push(Value::String(result.text)),
932 CellElementKind::Char => {
933 let ca = CharArray::new_row(&result.text);
934 values.push(Value::CharArray(ca));
935 }
936 }
937 }
938 make_cell_with_shape(values, info.shape).map_err(|e| {
939 extract_between_error_with_message(
940 format!("{BUILTIN_NAME}: {e}"),
941 &EXTRACT_BETWEEN_ERROR_INTERNAL,
942 )
943 })
944 }
945 }
946 }
947}
948
949#[derive(Clone, Debug, PartialEq, Eq)]
950enum BoundaryKind {
951 Text,
952 Position,
953}
954
955#[derive(Clone, Debug)]
956enum BoundaryArg {
957 Text(BoundaryText),
958 Position(BoundaryPositions),
959}
960
961impl BoundaryArg {
962 fn from_value(value: Value) -> BuiltinResult<Self> {
963 match value {
964 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) | Value::Cell(_) => {
965 BoundaryText::from_value(value).map(BoundaryArg::Text)
966 }
967 Value::Num(_) | Value::Int(_) | Value::Tensor(_) => {
968 BoundaryPositions::from_value(value).map(BoundaryArg::Position)
969 }
970 other => Err(extract_between_error_with_message(
971 format!(
972 "{}: unsupported argument {other:?}",
973 EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE.message
974 ),
975 &EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE,
976 )),
977 }
978 }
979
980 fn kind(&self) -> BoundaryKind {
981 match self {
982 BoundaryArg::Text(_) => BoundaryKind::Text,
983 BoundaryArg::Position(_) => BoundaryKind::Position,
984 }
985 }
986
987 fn shape(&self) -> &[usize] {
988 match self {
989 BoundaryArg::Text(text) => &text.shape,
990 BoundaryArg::Position(pos) => &pos.shape,
991 }
992 }
993
994 fn text(&self, idx: usize) -> &str {
995 match self {
996 BoundaryArg::Text(text) => &text.data[idx],
997 BoundaryArg::Position(_) => unreachable!(),
998 }
999 }
1000
1001 fn position(&self, idx: usize) -> usize {
1002 match self {
1003 BoundaryArg::Position(pos) => pos.data[idx],
1004 BoundaryArg::Text(_) => unreachable!(),
1005 }
1006 }
1007}
1008
1009#[derive(Clone, Debug)]
1010struct BoundaryText {
1011 data: Vec<String>,
1012 shape: Vec<usize>,
1013}
1014
1015impl BoundaryText {
1016 fn from_value(value: Value) -> BuiltinResult<Self> {
1017 match value {
1018 Value::String(s) => Ok(Self {
1019 data: vec![s],
1020 shape: vec![1, 1],
1021 }),
1022 Value::StringArray(sa) => Ok(Self {
1023 data: sa.data.clone(),
1024 shape: sa.shape.clone(),
1025 }),
1026 Value::CharArray(ca) => {
1027 let mut data = Vec::with_capacity(ca.rows);
1028 for row in 0..ca.rows {
1029 data.push(char_row_to_string_slice(&ca.data, ca.cols, row));
1030 }
1031 Ok(Self {
1032 data,
1033 shape: vec![ca.rows, 1],
1034 })
1035 }
1036 Value::Cell(cell) => {
1037 let shape = cell.shape.clone();
1038 let mut data = Vec::with_capacity(cell.data.len());
1039 for element in &cell.data {
1040 match &element {
1041 Value::String(s) => data.push(s.clone()),
1042 Value::StringArray(sa) if sa.data.len() == 1 => {
1043 data.push(sa.data[0].clone());
1044 }
1045 Value::CharArray(ca) if ca.rows <= 1 => {
1046 if ca.rows == 0 {
1047 data.push(String::new());
1048 } else {
1049 data.push(char_row_to_string_slice(&ca.data, ca.cols, 0));
1050 }
1051 }
1052 Value::CharArray(_) => {
1053 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_CELL_ELEMENT))
1054 }
1055 _ => {
1056 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_CELL_ELEMENT))
1057 }
1058 }
1059 }
1060 Ok(Self { data, shape })
1061 }
1062 _ => Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE)),
1063 }
1064 }
1065}
1066
1067#[derive(Clone, Debug)]
1068struct BoundaryPositions {
1069 data: Vec<usize>,
1070 shape: Vec<usize>,
1071}
1072
1073impl BoundaryPositions {
1074 fn from_value(value: Value) -> BuiltinResult<Self> {
1075 match value {
1076 Value::Num(n) => Ok(Self {
1077 data: vec![parse_position(n)?],
1078 shape: vec![1, 1],
1079 }),
1080 Value::Int(i) => Ok(Self {
1081 data: vec![parse_position_int(i)?],
1082 shape: vec![1, 1],
1083 }),
1084 Value::Tensor(t) => {
1085 let mut data = Vec::with_capacity(t.len());
1086 for idx in 0..t.len() {
1087 let entry = t.numeric_value_at(idx).ok_or_else(|| {
1088 extract_between_error(&EXTRACT_BETWEEN_ERROR_POSITION_TYPE)
1089 })?;
1090 data.push(match entry {
1091 NumericScalar::F64(value) => parse_position(value)?,
1092 NumericScalar::F32(value) => parse_position(f64::from(value))?,
1093 value => parse_position_int(
1094 value
1095 .into_int_value()
1096 .expect("non-floating numeric scalar is integer"),
1097 )?,
1098 });
1099 }
1100 Ok(Self {
1101 data,
1102 shape: if t.shape.is_empty() {
1103 vec![t.rows, t.cols.max(1)]
1104 } else {
1105 t.shape
1106 },
1107 })
1108 }
1109 _ => Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE)),
1110 }
1111 }
1112}
1113
1114fn parse_position(value: f64) -> BuiltinResult<usize> {
1115 if !value.is_finite() || value < 1.0 {
1116 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_POSITION_TYPE));
1117 }
1118 if (value.fract()).abs() > f64::EPSILON {
1119 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_POSITION_TYPE));
1120 }
1121 if value > usize::MAX.saturating_sub(1) as f64 {
1122 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_POSITION_TYPE));
1123 }
1124 let parsed = value as usize;
1125 if parsed as f64 != value || parsed == usize::MAX {
1126 return Err(extract_between_error(&EXTRACT_BETWEEN_ERROR_POSITION_TYPE));
1127 }
1128 Ok(parsed)
1129}
1130
1131fn parse_position_int(value: IntValue) -> BuiltinResult<usize> {
1132 value
1133 .try_to_usize()
1134 .filter(|position| *position > 0)
1135 .ok_or_else(|| extract_between_error(&EXTRACT_BETWEEN_ERROR_POSITION_TYPE))
1136}
1137
1138#[cfg(test)]
1139pub(crate) mod tests {
1140 #![allow(non_snake_case)]
1141
1142 use super::*;
1143 use runmat_builtins::{ResolveContext, Type};
1144 use runmat_value::{CellArray, IntegerStorage, NumericStorage, Tensor};
1145
1146 fn extract_between_builtin(
1147 text: Value,
1148 start: Value,
1149 stop: Value,
1150 rest: Vec<Value>,
1151 ) -> BuiltinResult<Value> {
1152 futures::executor::block_on(super::extract_between_builtin(text, start, stop, rest))
1153 }
1154
1155 #[test]
1156 fn extractBetween_position_vectors_read_typed_integer_storage_exactly() {
1157 for storage in [
1158 IntegerStorage::I8(vec![2, 4]),
1159 IntegerStorage::I16(vec![2, 4]),
1160 IntegerStorage::I32(vec![2, 4]),
1161 IntegerStorage::I64(vec![2, 4]),
1162 IntegerStorage::U8(vec![2, 4]),
1163 IntegerStorage::U16(vec![2, 4]),
1164 IntegerStorage::U32(vec![2, 4]),
1165 IntegerStorage::U64(vec![2, 4]),
1166 ] {
1167 let positions = Tensor::new_integer(storage, vec![1, 2]).expect("positions");
1168 let parsed = BoundaryPositions::from_value(Value::Tensor(positions)).unwrap();
1169 assert_eq!(parsed.data, vec![2, 4]);
1170 assert_eq!(parsed.shape, vec![1, 2]);
1171 }
1172 }
1173
1174 #[test]
1175 fn extractBetween_position_vectors_read_native_single_storage() {
1176 let positions =
1177 Tensor::from_numeric_storage(NumericStorage::F32(vec![2.0, 4.0]), vec![1, 2])
1178 .expect("positions");
1179 let parsed = BoundaryPositions::from_value(Value::Tensor(positions)).unwrap();
1180 assert_eq!(parsed.data, vec![2, 4]);
1181 }
1182
1183 #[test]
1184 fn extractBetween_position_vectors_reject_nonpositive_integer_storage() {
1185 let positions =
1186 Tensor::new_integer(IntegerStorage::I16(vec![1, 0]), vec![1, 2]).expect("positions");
1187
1188 assert!(BoundaryPositions::from_value(Value::Tensor(positions)).is_err());
1189 }
1190
1191 #[test]
1192 fn extractBetween_position_vectors_reject_oversized_double_positions() {
1193 assert!(BoundaryPositions::from_value(Value::Num(1.0e300)).is_err());
1194 }
1195
1196 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1197 #[test]
1198 fn extractBetween_basic_string() {
1199 let result = extract_between_builtin(
1200 Value::String("RunMat accelerates MATLAB".into()),
1201 Value::String("RunMat ".into()),
1202 Value::String(" MATLAB".into()),
1203 Vec::new(),
1204 )
1205 .expect("extractBetween");
1206 assert_eq!(result, Value::String("accelerates".into()));
1207 }
1208
1209 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1210 #[test]
1211 fn extractBetween_inclusive_option() {
1212 let result = extract_between_builtin(
1213 Value::String("a[b]c".into()),
1214 Value::String("[".into()),
1215 Value::String("]".into()),
1216 vec![
1217 Value::String("Boundaries".into()),
1218 Value::String("inclusive".into()),
1219 ],
1220 )
1221 .expect("extractBetween");
1222 assert_eq!(result, Value::String("[b]".into()));
1223 }
1224
1225 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1226 #[test]
1227 fn extractBetween_numeric_positions() {
1228 let result = extract_between_builtin(
1229 Value::String("Accelerator".into()),
1230 Value::Num(3.0),
1231 Value::Num(7.0),
1232 Vec::new(),
1233 )
1234 .expect("extractBetween");
1235 assert_eq!(result, Value::String("celer".into()));
1236 }
1237
1238 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1239 #[test]
1240 fn extractBetween_numeric_positions_exclusive_option() {
1241 let result = extract_between_builtin(
1242 Value::String("Accelerator".into()),
1243 Value::Num(3.0),
1244 Value::Num(7.0),
1245 vec![
1246 Value::String("Boundaries".into()),
1247 Value::String("exclusive".into()),
1248 ],
1249 )
1250 .expect("extractBetween");
1251 assert_eq!(result, Value::String("ele".into()));
1252 }
1253
1254 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1255 #[test]
1256 fn extractBetween_numeric_positions_clamps_stop() {
1257 let result = extract_between_builtin(
1258 Value::String("Accelerator".into()),
1259 Value::Num(3.0),
1260 Value::Num(100.0),
1261 Vec::new(),
1262 )
1263 .expect("extractBetween");
1264 assert_eq!(result, Value::String("celerator".into()));
1265 }
1266
1267 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1268 #[test]
1269 fn extractBetween_numeric_positions_start_past_length() {
1270 let result = extract_between_builtin(
1271 Value::String("abc".into()),
1272 Value::Num(10.0),
1273 Value::Num(12.0),
1274 Vec::new(),
1275 )
1276 .expect("extractBetween");
1277 assert_eq!(result, Value::String(String::new()));
1278 }
1279
1280 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1281 #[test]
1282 fn extractBetween_string_array_broadcast() {
1283 let array = StringArray::new(
1284 vec!["runmat_accel.rs".into(), "runmat_gc.rs".into()],
1285 vec![2, 1],
1286 )
1287 .unwrap();
1288 let result = extract_between_builtin(
1289 Value::StringArray(array),
1290 Value::String("runmat_".into()),
1291 Value::String(".rs".into()),
1292 Vec::new(),
1293 )
1294 .expect("extractBetween");
1295 match result {
1296 Value::StringArray(sa) => {
1297 assert_eq!(sa.data, vec!["accel".to_string(), "gc".to_string()]);
1298 assert_eq!(sa.shape, vec![2, 1]);
1299 }
1300 other => panic!("expected string array, got {other:?}"),
1301 }
1302 }
1303
1304 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1305 #[test]
1306 fn extractBetween_character_vector_returns_scalar_cellstr() {
1307 let chars = CharArray::new(
1308 "GPUAccelerateVM".chars().collect(),
1309 1,
1310 "GPUAccelerateVM".len(),
1311 )
1312 .unwrap();
1313 let result = extract_between_builtin(
1314 Value::CharArray(chars),
1315 Value::String("GPU".into()),
1316 Value::String("VM".into()),
1317 Vec::new(),
1318 )
1319 .expect("extractBetween");
1320 match result {
1321 Value::Cell(out) => {
1322 assert_eq!(out.shape, vec![1, 1]);
1323 let Value::CharArray(chars) = &out.data[0] else {
1324 panic!("expected character vector element")
1325 };
1326 let text: String = chars.data.iter().collect();
1327 assert_eq!(text.trim_end(), "Accelerate");
1328 }
1329 other => panic!("expected scalar cellstr, got {other:?}"),
1330 }
1331 }
1332
1333 #[test]
1334 fn extractBetween_integer_capabilities_and_strict_shape_are_declared() {
1335 assert_eq!(INTEGER_CAPABILITIES.len(), 2);
1336 assert_eq!(INTEGER_POSITION_INPUTS[0].classes.len(), 8);
1337 let text = Value::StringArray(
1338 StringArray::new(vec!["abcd".into(), "wxyz".into()], vec![2, 1]).unwrap(),
1339 );
1340 let start = Value::Tensor(
1341 Tensor::new_integer(IntegerStorage::U64(vec![1, 2]), vec![1, 2]).unwrap(),
1342 );
1343 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1344 let error = extract_between_builtin(text, start, Value::Num(3.0), Vec::new())
1345 .expect_err("non-same-size expansion must be gated");
1346 assert_eq!(
1347 error.identifier(),
1348 FULL_BROADCAST_EXTENSION.error_identifier
1349 );
1350 }
1351
1352 #[test]
1353 fn extractBetween_strict_mode_gates_resident_positions_before_gather() {
1354 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1355 let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
1356 shape: vec![1, 1],
1357 device_id: u32::MAX,
1358 buffer_id: u64::MAX,
1359 descriptor: Default::default(),
1360 });
1361 let error = extract_between_builtin(
1362 Value::String("abcd".into()),
1363 resident,
1364 Value::Num(3.0),
1365 Vec::new(),
1366 )
1367 .expect_err("resident position gate");
1368 assert_eq!(
1369 error.identifier(),
1370 RESIDENT_POSITION_EXTENSION.error_identifier
1371 );
1372 }
1373
1374 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1375 #[test]
1376 fn extractBetween_cell_array_preserves_types() {
1377 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1378 let cell = CellArray::new(
1379 vec![
1380 Value::CharArray(CharArray::new_row("A[B]C")),
1381 Value::String("Planner<GPU>".into()),
1382 ],
1383 1,
1384 2,
1385 )
1386 .unwrap();
1387 let result = extract_between_builtin(
1388 Value::Cell(cell),
1389 Value::String("[".into()),
1390 Value::String("]".into()),
1391 Vec::new(),
1392 )
1393 .expect("extractBetween");
1394 match result {
1395 Value::Cell(out) => {
1396 let first = out.get(0, 0).unwrap();
1397 let second = out.get(0, 1).unwrap();
1398 assert_eq!(first, Value::CharArray(CharArray::new_row("B")));
1399 assert_eq!(second, Value::String(String::new()));
1400 }
1401 other => panic!("expected cell array, got {other:?}"),
1402 }
1403 }
1404
1405 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1406 #[test]
1407 fn extractBetween_missing_string_propagates() {
1408 let strings = StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap();
1409 let result = extract_between_builtin(
1410 Value::StringArray(strings),
1411 Value::String("[".into()),
1412 Value::String("]".into()),
1413 Vec::new(),
1414 )
1415 .expect("extractBetween");
1416 assert_eq!(
1417 result,
1418 Value::StringArray(StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap())
1419 );
1420 }
1421
1422 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1423 #[test]
1424 fn extractBetween_position_type_error() {
1425 let err = extract_between_builtin(
1426 Value::String("abc".into()),
1427 Value::Num(0.5),
1428 Value::Num(2.0),
1429 Vec::new(),
1430 )
1431 .unwrap_err();
1432 assert_eq!(err.to_string(), EXTRACT_BETWEEN_ERROR_POSITION_TYPE.message);
1433 assert_eq!(
1434 err.identifier(),
1435 EXTRACT_BETWEEN_ERROR_POSITION_TYPE.identifier
1436 );
1437 }
1438
1439 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1440 #[test]
1441 fn extractBetween_mixed_boundary_error() {
1442 let err = extract_between_builtin(
1443 Value::String("abc".into()),
1444 Value::String("a".into()),
1445 Value::Num(3.0),
1446 Vec::new(),
1447 )
1448 .unwrap_err();
1449 assert_eq!(err.to_string(), EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE.message);
1450 assert_eq!(
1451 err.identifier(),
1452 EXTRACT_BETWEEN_ERROR_BOUNDARY_TYPE.identifier
1453 );
1454 }
1455
1456 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1457 #[test]
1458 fn extractBetween_numeric_tensor_broadcast() {
1459 let text = StringArray::new(vec!["abcd".into(), "wxyz".into()], vec![2, 1]).unwrap();
1460 let start = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
1461 let stop = Tensor::new(vec![3.0, 4.0], vec![2, 1]).unwrap();
1462 let result = extract_between_builtin(
1463 Value::StringArray(text),
1464 Value::Tensor(start),
1465 Value::Tensor(stop),
1466 Vec::new(),
1467 )
1468 .expect("extractBetween");
1469 match result {
1470 Value::StringArray(sa) => {
1471 assert_eq!(sa.data, vec!["abc".to_string(), "xyz".to_string()]);
1472 assert_eq!(sa.shape, vec![2, 1]);
1473 }
1474 other => panic!("expected string array, got {other:?}"),
1475 }
1476 }
1477
1478 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1479 #[test]
1480 fn extractBetween_option_invalid_value() {
1481 let err = extract_between_builtin(
1482 Value::String("abc".into()),
1483 Value::String("a".into()),
1484 Value::String("c".into()),
1485 vec![
1486 Value::String("Boundaries".into()),
1487 Value::String("middle".into()),
1488 ],
1489 )
1490 .unwrap_err();
1491 assert_eq!(err.to_string(), EXTRACT_BETWEEN_ERROR_OPTION_VALUE.message);
1492 assert_eq!(
1493 err.identifier(),
1494 EXTRACT_BETWEEN_ERROR_OPTION_VALUE.identifier
1495 );
1496 }
1497
1498 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1499 #[test]
1500 fn extractBetween_option_name_error() {
1501 let err = extract_between_builtin(
1502 Value::String("abc".into()),
1503 Value::String("a".into()),
1504 Value::String("c".into()),
1505 vec![
1506 Value::String("Padding".into()),
1507 Value::String("inclusive".into()),
1508 ],
1509 )
1510 .unwrap_err();
1511 assert_eq!(err.to_string(), EXTRACT_BETWEEN_ERROR_OPTION_NAME.message);
1512 assert_eq!(
1513 err.identifier(),
1514 EXTRACT_BETWEEN_ERROR_OPTION_NAME.identifier
1515 );
1516 }
1517
1518 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1519 #[test]
1520 fn extractBetween_option_pair_error() {
1521 let err = extract_between_builtin(
1522 Value::String("abc".into()),
1523 Value::String("a".into()),
1524 Value::String("b".into()),
1525 vec![Value::String("Boundaries".into())],
1526 )
1527 .unwrap_err();
1528 assert_eq!(
1529 err.to_string(),
1530 EXTRACT_BETWEEN_ERROR_NAME_VALUE_PAIR.message
1531 );
1532 assert_eq!(
1533 err.identifier(),
1534 EXTRACT_BETWEEN_ERROR_NAME_VALUE_PAIR.identifier
1535 );
1536 }
1537
1538 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1539 #[test]
1540 fn extractBetween_missing_boundary_propagates() {
1541 let result = extract_between_builtin(
1542 Value::String("Planner<GPU>".into()),
1543 Value::String("<missing>".into()),
1544 Value::String(">".into()),
1545 Vec::new(),
1546 )
1547 .expect("extractBetween");
1548 assert_eq!(result, Value::String("<missing>".into()));
1549 }
1550
1551 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1552 #[test]
1553 fn extractBetween_cell_boundary_arguments() {
1554 let text =
1555 CellArray::new(vec![Value::CharArray(CharArray::new_row("A<GPU>"))], 1, 1).unwrap();
1556 let start = CellArray::new(vec![Value::CharArray(CharArray::new_row("<"))], 1, 1).unwrap();
1557 let stop = CellArray::new(vec![Value::CharArray(CharArray::new_row(">"))], 1, 1).unwrap();
1558 let result = extract_between_builtin(
1559 Value::Cell(text),
1560 Value::Cell(start),
1561 Value::Cell(stop),
1562 Vec::new(),
1563 )
1564 .expect("extractBetween");
1565 match result {
1566 Value::Cell(out) => {
1567 let value = out.get(0, 0).unwrap();
1568 assert_eq!(value, Value::CharArray(CharArray::new_row("GPU")));
1569 }
1570 other => panic!("expected cell array, got {other:?}"),
1571 }
1572 }
1573
1574 #[test]
1575 fn extract_between_type_preserves_text() {
1576 assert_eq!(
1577 text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
1578 Type::String
1579 );
1580 }
1581}