1use runmat_builtins::Value;
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::broadcast::{broadcast_index, broadcast_shapes, compute_strides};
7use crate::builtins::common::spec::{
8 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
9 ReductionNaN, ResidencyPolicy, ShapeRequirements,
10};
11use crate::builtins::common::tensor;
12use crate::builtins::strings::search::text_utils::{logical_result, TextCollection, TextElement};
13#[cfg(feature = "doc_export")]
14use crate::register_builtin_doc_text;
15use crate::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
16
17#[cfg(feature = "doc_export")]
18pub const DOC_MD: &str = r#"---
19title: "strcmp"
20category: "strings/core"
21keywords: ["strcmp", "string compare", "text equality", "cell array", "character vector"]
22summary: "Compare text inputs for exact equality with MATLAB-compatible implicit expansion across text types."
23references:
24 - https://www.mathworks.com/help/matlab/ref/strcmp.html
25gpu_support:
26 elementwise: false
27 reduction: false
28 precisions: []
29 broadcasting: "matlab"
30 notes: "Executes on the CPU; GPU-resident inputs are gathered automatically so results match MATLAB behaviour exactly."
31fusion:
32 elementwise: false
33 reduction: false
34 max_inputs: 2
35 constants: "inline"
36requires_feature: null
37tested:
38 unit: "builtins::strings::core::strcmp::tests"
39 integration: "builtins::strings::core::strcmp::tests::strcmp_cell_array_scalar"
40---
41
42# What does the `strcmp` function do in MATLAB / RunMat?
43`strcmp(a, b)` returns logical `true` when two pieces of text match exactly and `false` otherwise.
44It accepts string arrays, character vectors/arrays, and cell arrays of character vectors, mirroring MATLAB semantics.
45
46## How does the `strcmp` function behave in MATLAB / RunMat?
47- **Accepted text types**: Works with string scalars/arrays, character vectors or matrices created with `char`, and cell arrays of character vectors. Mixed combinations are converted automatically, matching MATLAB.
48- **Implicit expansion**: Scalar inputs expand to the shape of the other operand, producing element-wise comparisons for higher-dimensional arrays.
49- **Character arrays**: Rows are compared individually. The result is a column vector whose length equals the number of rows in the character array.
50- **Cell arrays**: Each cell is treated as a text scalar. Scalar cell arrays expand across the other operand before comparison.
51- **Missing strings**: String elements equal to `missing` compare unequal to everything, including other `missing` values.
52- **Result form**: A single logical scalar is returned for scalar comparisons; otherwise you receive a logical array using column-major MATLAB layout.
53- **Case sensitivity**: Matching is case-sensitive. Use `strcmpi` for case-insensitive comparisons.
54
55## `strcmp` Function GPU Execution Behaviour
56`strcmp` is registered as an acceleration sink. When either input resides on the GPU, RunMat gathers both
57operands back to host memory before comparing them so the results match MATLAB exactly. Providers do not
58need to implement custom kernels, and the logical result is always returned on the CPU.
59
60## Examples of using the `strcmp` function in MATLAB / RunMat
61
62### Compare Two Equal Strings
63```matlab
64tf = strcmp("RunMat", "RunMat");
65```
66Expected output:
67```matlab
68tf = logical
69 1
70```
71
72### Compare String Array With Scalar Text
73```matlab
74names = ["red" "green" "blue"];
75tf = strcmp(names, "green");
76```
77Expected output:
78```matlab
79tf = 1×3 logical array
80 0 1 0
81```
82
83### Compare Character Array Rows
84```matlab
85labels = char("cat", "dog", "cat");
86tf = strcmp(labels, "cat");
87```
88Expected output:
89```matlab
90tf = 3×1 logical array
91 1
92 0
93 1
94```
95
96### Compare Two Cell Arrays Of Character Vectors
97```matlab
98C1 = {'apple', 'pear', 'grape'};
99C2 = {'apple', 'peach', 'grape'};
100tf = strcmp(C1, C2);
101```
102Expected output:
103```matlab
104tf = 1×3 logical array
105 1 0 1
106```
107
108### Handle Missing Strings
109```matlab
110vals = ["alpha" missing];
111tf = strcmp(vals, "alpha");
112```
113Expected output:
114```matlab
115tf = 1×2 logical array
116 1 0
117```
118
119### Implicit Expansion With Column Vector Text
120```matlab
121patterns = char("north", "south");
122tf = strcmp(patterns, ["north" "east"]);
123```
124Expected output:
125```matlab
126tf = 2×2 logical array
127 1 0
128 0 0
129```
130
131## GPU residency in RunMat (Do I need `gpuArray`?)
132You normally do **not** need to call `gpuArray`. If you do, RunMat gathers the operands before computing `strcmp`
133so the output matches MATLAB. The result always lives on the host because this builtin inspects text data.
134
135## FAQ
136
137### What types can I pass to `strcmp`?
138Use string arrays, character vectors/arrays, or cell arrays of character vectors. Mixed combinations are accepted and follow MATLAB's implicit expansion rules.
139
140### Does `strcmp` ignore letter case?
141No. `strcmp` is case-sensitive. Use `strcmpi` for case-insensitive comparisons.
142
143### What happens when the inputs contain missing strings?
144Missing string scalars compare unequal to every value (including other missing strings), so the result is `false`.
145
146### Can `strcmp` compare matrices of characters?
147Yes. Character arrays compare row-by-row, returning a column vector whose entries tell you whether each row matches.
148
149### Does `strcmp` return numeric or logical results?
150It returns logical results. Scalars become logical scalars (`Value::Bool`), while arrays are returned as logical arrays.
151
152## See Also
153[string](./string), [char](./char), [contains](../../search/contains), [startswith](../../search/startswith), [strlength](./strlength)
154
155## Source & Feedback
156- Implementation: [`crates/runmat-runtime/src/builtins/strings/core/strcmp.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/strings/core/strcmp.rs)
157- Found a bug? Please [open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal reproduction.
158"#;
159
160pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
161 name: "strcmp",
162 op_kind: GpuOpKind::Custom("string-compare"),
163 supported_precisions: &[],
164 broadcast: BroadcastSemantics::Matlab,
165 provider_hooks: &[],
166 constant_strategy: ConstantStrategy::InlineLiteral,
167 residency: ResidencyPolicy::GatherImmediately,
168 nan_mode: ReductionNaN::Include,
169 two_pass_threshold: None,
170 workgroup_size: None,
171 accepts_nan_mode: false,
172 notes: "Performs host-side text comparisons; GPU operands are gathered automatically before evaluation.",
173};
174
175register_builtin_gpu_spec!(GPU_SPEC);
176
177pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
178 name: "strcmp",
179 shape: ShapeRequirements::Any,
180 constant_strategy: ConstantStrategy::InlineLiteral,
181 elementwise: None,
182 reduction: None,
183 emits_nan: false,
184 notes: "Produces logical results on the host; not eligible for GPU fusion.",
185};
186
187register_builtin_fusion_spec!(FUSION_SPEC);
188
189#[cfg(feature = "doc_export")]
190register_builtin_doc_text!("strcmp", DOC_MD);
191
192#[runtime_builtin(
193 name = "strcmp",
194 category = "strings/core",
195 summary = "Compare text inputs for exact matches (case-sensitive).",
196 keywords = "strcmp,string compare,text equality",
197 accel = "sink"
198)]
199fn strcmp_builtin(a: Value, b: Value) -> Result<Value, String> {
200 let a = gather_if_needed(&a).map_err(|e| format!("strcmp: {e}"))?;
201 let b = gather_if_needed(&b).map_err(|e| format!("strcmp: {e}"))?;
202 let left = TextCollection::from_argument("strcmp", a, "first argument")?;
203 let right = TextCollection::from_argument("strcmp", b, "second argument")?;
204 evaluate_strcmp(&left, &right)
205}
206
207fn evaluate_strcmp(left: &TextCollection, right: &TextCollection) -> Result<Value, String> {
208 let shape = broadcast_shapes("strcmp", &left.shape, &right.shape)?;
209 let total = tensor::element_count(&shape);
210 if total == 0 {
211 return logical_result("strcmp", Vec::new(), shape);
212 }
213 let left_strides = compute_strides(&left.shape);
214 let right_strides = compute_strides(&right.shape);
215 let mut data = Vec::with_capacity(total);
216 for linear in 0..total {
217 let li = broadcast_index(linear, &shape, &left.shape, &left_strides);
218 let ri = broadcast_index(linear, &shape, &right.shape, &right_strides);
219 let equal = match (&left.elements[li], &right.elements[ri]) {
220 (TextElement::Missing, _) => false,
221 (_, TextElement::Missing) => false,
222 (TextElement::Text(lhs), TextElement::Text(rhs)) => lhs == rhs,
223 };
224 data.push(if equal { 1 } else { 0 });
225 }
226 logical_result("strcmp", data, shape)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 #[cfg(feature = "doc_export")]
233 use crate::builtins::common::test_support;
234 use runmat_builtins::{CellArray, CharArray, LogicalArray, StringArray};
235
236 #[test]
237 fn strcmp_string_scalar_true() {
238 let result = strcmp_builtin(
239 Value::String("RunMat".into()),
240 Value::String("RunMat".into()),
241 )
242 .expect("strcmp");
243 assert_eq!(result, Value::Bool(true));
244 }
245
246 #[test]
247 fn strcmp_string_scalar_false() {
248 let result = strcmp_builtin(
249 Value::String("RunMat".into()),
250 Value::String("runmat".into()),
251 )
252 .expect("strcmp");
253 assert_eq!(result, Value::Bool(false));
254 }
255
256 #[test]
257 fn strcmp_string_array_broadcast_scalar() {
258 let array = StringArray::new(
259 vec!["red".into(), "green".into(), "blue".into()],
260 vec![1, 3],
261 )
262 .unwrap();
263 let result =
264 strcmp_builtin(Value::StringArray(array), Value::String("green".into())).expect("cmp");
265 let expected = LogicalArray::new(vec![0, 1, 0], vec![1, 3]).unwrap();
266 assert_eq!(result, Value::LogicalArray(expected));
267 }
268
269 #[test]
270 fn strcmp_char_array_row_compare() {
271 let chars = CharArray::new(vec!['c', 'a', 't', 'd', 'o', 'g'], 2, 3).unwrap();
272 let result =
273 strcmp_builtin(Value::CharArray(chars), Value::String("cat".into())).expect("cmp");
274 let expected = LogicalArray::new(vec![1, 0], vec![2, 1]).unwrap();
275 assert_eq!(result, Value::LogicalArray(expected));
276 }
277
278 #[test]
279 fn strcmp_char_array_to_char_array() {
280 let left = CharArray::new(vec!['a', 'b', 'c', 'd'], 2, 2).unwrap();
281 let right = CharArray::new(vec!['a', 'b', 'x', 'y'], 2, 2).unwrap();
282 let result =
283 strcmp_builtin(Value::CharArray(left), Value::CharArray(right)).expect("strcmp");
284 let expected = LogicalArray::new(vec![1, 0], vec![2, 1]).unwrap();
285 assert_eq!(result, Value::LogicalArray(expected));
286 }
287
288 #[test]
289 fn strcmp_cell_array_scalar() {
290 let cell = CellArray::new(
291 vec![
292 Value::from("apple"),
293 Value::from("pear"),
294 Value::from("grape"),
295 ],
296 1,
297 3,
298 )
299 .unwrap();
300 let result =
301 strcmp_builtin(Value::Cell(cell), Value::String("grape".into())).expect("strcmp");
302 let expected = LogicalArray::new(vec![0, 0, 1], vec![1, 3]).unwrap();
303 assert_eq!(result, Value::LogicalArray(expected));
304 }
305
306 #[test]
307 fn strcmp_cell_array_to_cell_array_broadcasts() {
308 let left = CellArray::new(vec![Value::from("red"), Value::from("blue")], 2, 1).unwrap();
309 let right = CellArray::new(vec![Value::from("red")], 1, 1).unwrap();
310 let result = strcmp_builtin(Value::Cell(left), Value::Cell(right)).expect("strcmp");
311 let expected = LogicalArray::new(vec![1, 0], vec![2, 1]).unwrap();
312 assert_eq!(result, Value::LogicalArray(expected));
313 }
314
315 #[test]
316 fn strcmp_string_array_multi_dimensional_broadcast() {
317 let left = StringArray::new(vec!["north".into(), "south".into()], vec![2, 1]).unwrap();
318 let right = StringArray::new(
319 vec!["north".into(), "east".into(), "south".into()],
320 vec![1, 3],
321 )
322 .unwrap();
323 let result =
324 strcmp_builtin(Value::StringArray(left), Value::StringArray(right)).expect("strcmp");
325 let expected = LogicalArray::new(vec![1, 0, 0, 0, 0, 1], vec![2, 3]).unwrap();
326 assert_eq!(result, Value::LogicalArray(expected));
327 }
328
329 #[test]
330 fn strcmp_char_array_trailing_space_is_not_equal() {
331 let chars = CharArray::new(vec!['c', 'a', 't', ' '], 1, 4).unwrap();
332 let result =
333 strcmp_builtin(Value::CharArray(chars), Value::String("cat".into())).expect("strcmp");
334 assert_eq!(result, Value::Bool(false));
335 }
336
337 #[test]
338 fn strcmp_char_array_empty_rows_returns_empty() {
339 let chars = CharArray::new(Vec::new(), 0, 0).unwrap();
340 let result = strcmp_builtin(Value::CharArray(chars), Value::String("anything".into()))
341 .expect("strcmp");
342 match result {
343 Value::LogicalArray(array) => {
344 assert_eq!(array.shape, vec![0, 1]);
345 assert!(array.data.is_empty());
346 }
347 other => panic!("expected empty logical array, got {other:?}"),
348 }
349 }
350
351 #[test]
352 fn strcmp_missing_strings_compare_false() {
353 let strings = StringArray::new(vec!["<missing>".into()], vec![1, 1]).unwrap();
354 let result = strcmp_builtin(
355 Value::StringArray(strings.clone()),
356 Value::StringArray(strings),
357 )
358 .expect("strcmp");
359 assert_eq!(result, Value::Bool(false));
360 }
361
362 #[test]
363 fn strcmp_missing_string_false() {
364 let array = StringArray::new(vec!["alpha".into(), "<missing>".into()], vec![1, 2]).unwrap();
365 let result =
366 strcmp_builtin(Value::StringArray(array), Value::String("alpha".into())).expect("cmp");
367 let expected = LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap();
368 assert_eq!(result, Value::LogicalArray(expected));
369 }
370
371 #[test]
372 fn strcmp_size_mismatch_error() {
373 let left = StringArray::new(vec!["a".into(), "b".into()], vec![2, 1]).unwrap();
374 let right = StringArray::new(vec!["a".into(), "b".into(), "c".into()], vec![3, 1]).unwrap();
375 let err = strcmp_builtin(Value::StringArray(left), Value::StringArray(right))
376 .expect_err("size mismatch");
377 assert!(err.contains("size mismatch"));
378 }
379
380 #[test]
381 fn strcmp_invalid_argument_type() {
382 let err =
383 strcmp_builtin(Value::Num(1.0), Value::String("a".into())).expect_err("invalid type");
384 assert!(err.contains("first argument must be text"));
385 }
386
387 #[test]
388 #[cfg(feature = "doc_export")]
389 fn doc_examples_present() {
390 let blocks = test_support::doc_examples(DOC_MD);
391 assert!(!blocks.is_empty());
392 }
393}