1use runmat_builtins::{CellArray, CharArray, StringArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::spec::{
7 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
8 ReductionNaN, ResidencyPolicy, ShapeRequirements,
9};
10use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
11#[cfg(feature = "doc_export")]
12use crate::register_builtin_doc_text;
13use crate::{gather_if_needed, make_cell, register_builtin_fusion_spec, register_builtin_gpu_spec};
14
15#[cfg(feature = "doc_export")]
16pub const DOC_MD: &str = r#"---
17title: "pad"
18category: "strings/transform"
19keywords: ["pad", "pad string", "left pad", "right pad", "center text", "character arrays"]
20summary: "Pad strings, character arrays, and cell arrays to a target length using MATLAB-compatible options."
21references:
22 - https://www.mathworks.com/help/matlab/ref/pad.html
23gpu_support:
24 elementwise: false
25 reduction: false
26 precisions: []
27 broadcasting: "none"
28 notes: "Executes on the CPU; GPU-resident inputs are gathered before padding so behaviour matches MATLAB."
29fusion:
30 elementwise: false
31 reduction: false
32 max_inputs: 1
33 constants: "inline"
34requires_feature: null
35tested:
36 unit: "builtins::strings::transform::pad::tests"
37 integration: "builtins::strings::transform::pad::tests::pad_cell_array_mixed_content"
38---
39
40# What does the `pad` function do in MATLAB / RunMat?
41`pad` adds characters to the beginning, end, or both sides of strings so that each element reaches a
42specified length. It mirrors MATLAB semantics for string arrays, character arrays, and cell arrays of
43character vectors, including direction keywords, default space padding, and optional custom characters.
44
45## How does the `pad` function behave in MATLAB / RunMat?
46- Without a target length, `pad` extends each element to match the longest text in the input.
47- Providing a numeric target guarantees a minimum length; existing text that already meets or exceeds
48 the target is returned unchanged.
49- Direction keywords (`'left'`, `'right'`, `'both'`) are case-insensitive; `'right'` is the default.
50 When an odd number of pad characters is required for `'both'`, the extra character is appended to the end.
51- `padChar` must be a single character (string scalar or 1×1 char array). The default is a space.
52- Character arrays remain rectangular. Each row is padded independently and then widened with spaces so
53 the array keeps MATLAB’s column-major layout.
54- Cell arrays preserve their structure. Elements must be string scalars or 1×N character vectors and are
55 padded while keeping their original type.
56- Missing strings (`string(missing)`) and empty character vectors pass through unchanged, preserving metadata.
57
58## `pad` Function GPU Execution Behaviour
59`pad` always executes on the CPU. When an argument (or a value nested inside a cell array) lives on the GPU,
60RunMat gathers it, performs the padding step, and produces a host result or re-wraps the padded value inside
61the cell. No provider hooks exist yet for string padding, so providers and fusion planners treat `pad` as a
62sink that terminates device residency.
63
64## GPU residency in RunMat (Do I need `gpuArray`?)
65No. Text data in RunMat lives on the host today. If text happens to originate from a GPU computation,
66`pad` automatically gathers it before padding, so you never have to manage residency manually for this
67builtin.
68
69## Examples of using the `pad` function in MATLAB / RunMat
70
71### Pad Strings To A Common Width
72```matlab
73labels = ["GPU"; "Accelerate"; "RunMat"];
74aligned = pad(labels);
75```
76Expected output:
77```matlab
78aligned =
79 3×1 string
80 "GPU "
81 "Accelerate"
82 "RunMat "
83```
84
85### Pad Strings On The Left With Zeros
86```matlab
87ids = ["42"; "7"; "512"];
88zero_padded = pad(ids, 4, 'left', '0');
89```
90Expected output:
91```matlab
92zero_padded =
93 3×1 string
94 "0042"
95 "0007"
96 "0512"
97```
98
99### Center Text With Both-Sided Padding
100```matlab
101titles = ["core"; "planner"];
102centered = pad(titles, 10, 'both', '*');
103```
104Expected output:
105```matlab
106centered =
107 2×1 string
108 "***core***"
109 "*planner**"
110```
111
112### Pad Character Array Rows
113```matlab
114chars = char("GPU", "RunMat");
115out = pad(chars, 8);
116```
117Expected output:
118```matlab
119out =
120
121 2×8 char array
122
123 'GPU '
124 'RunMat '
125```
126
127### Pad A Cell Array Of Character Vectors
128```matlab
129C = {'solver', "planner", 'jit'};
130cell_out = pad(C, 'right', '.');
131```
132Expected output:
133```matlab
134cell_out = 1×3 cell array
135 {'solver.'} {"planner"} {'jit....'}
136```
137
138### Leave Missing Strings Unchanged
139```matlab
140values = ["RunMat", "<missing>", "GPU"];
141kept = pad(values, 8);
142```
143Expected output:
144```matlab
145kept =
146 1×3 string
147 "RunMat " <missing> "GPU "
148```
149
150## FAQ
151
152### What inputs does `pad` accept?
153String scalars, string arrays, character arrays, and cell arrays containing string scalars or character
154vectors. Other types raise MATLAB-compatible errors.
155
156### How are direction keywords interpreted?
157`'left'`, `'right'`, and `'both'` are supported (case-insensitive). `'right'` is the default. With `'both'`,
158extra characters are added to the end when an odd number of padding characters is required.
159
160### Can I shorten text with `pad`?
161No. When the existing text is already longer than the requested target length, it is returned unchanged.
162
163### What happens when I supply a custom padding character?
164The character must be length one. RunMat repeats it as many times as needed in the specified direction.
165
166### Do missing strings get padded?
167Missing strings (`<missing>`) are passed through untouched so downstream code that checks for missing
168values continues to work.
169
170### How are cell array elements returned?
171Each cell retains its type: string scalars remain strings and character vectors remain 1×N character
172arrays after padding.
173
174### Does `pad` change the orientation of row or column string arrays?
175No. The shape of the input array is preserved exactly; only element lengths change.
176
177### Will `pad` run on the GPU in the future?
178Possibly, but today it always gathers to the CPU. Providers may add device-side implementations later,
179and the behaviour documented here will remain the reference.
180
181## See Also
182[strip](./strip), [strcat](./strcat), [lower](./lower), [upper](./upper), [compose](../core/compose)
183
184## Source & Feedback
185- Implementation: [`crates/runmat-runtime/src/builtins/strings/transform/pad.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/strings/transform/pad.rs)
186- Found an issue? Please [open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal reproduction.
187"#;
188
189pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
190 name: "pad",
191 op_kind: GpuOpKind::Custom("string-transform"),
192 supported_precisions: &[],
193 broadcast: BroadcastSemantics::None,
194 provider_hooks: &[],
195 constant_strategy: ConstantStrategy::InlineLiteral,
196 residency: ResidencyPolicy::GatherImmediately,
197 nan_mode: ReductionNaN::Include,
198 two_pass_threshold: None,
199 workgroup_size: None,
200 accepts_nan_mode: false,
201 notes: "Executes on the CPU; GPU-resident inputs are gathered before padding to preserve MATLAB semantics.",
202};
203
204register_builtin_gpu_spec!(GPU_SPEC);
205
206pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
207 name: "pad",
208 shape: ShapeRequirements::Any,
209 constant_strategy: ConstantStrategy::InlineLiteral,
210 elementwise: None,
211 reduction: None,
212 emits_nan: false,
213 notes: "String transformation builtin; always gathers inputs and is not eligible for fusion.",
214};
215
216register_builtin_fusion_spec!(FUSION_SPEC);
217
218#[cfg(feature = "doc_export")]
219register_builtin_doc_text!("pad", DOC_MD);
220
221const ARG_TYPE_ERROR: &str =
222 "pad: first argument must be a string array, character array, or cell array of character vectors";
223const LENGTH_ERROR: &str = "pad: target length must be a non-negative integer scalar";
224const DIRECTION_ERROR: &str = "pad: direction must be 'left', 'right', or 'both'";
225const PAD_CHAR_ERROR: &str =
226 "pad: padding character must be a string scalar or character vector containing one character";
227const CELL_ELEMENT_ERROR: &str =
228 "pad: cell array elements must be string scalars or character vectors";
229const ARGUMENT_CONFIG_ERROR: &str = "pad: unable to interpret input arguments";
230
231#[derive(Clone, Copy, Eq, PartialEq)]
232enum PadDirection {
233 Left,
234 Right,
235 Both,
236}
237
238#[derive(Clone, Copy)]
239enum PadTarget {
240 Auto,
241 Length(usize),
242}
243
244#[derive(Clone, Copy)]
245struct PadOptions {
246 target: PadTarget,
247 direction: PadDirection,
248 pad_char: char,
249}
250
251impl Default for PadOptions {
252 fn default() -> Self {
253 Self {
254 target: PadTarget::Auto,
255 direction: PadDirection::Right,
256 pad_char: ' ',
257 }
258 }
259}
260
261impl PadOptions {
262 fn base_target(&self, auto_target: usize) -> usize {
263 match self.target {
264 PadTarget::Auto => auto_target,
265 PadTarget::Length(len) => len,
266 }
267 }
268}
269
270#[runtime_builtin(
271 name = "pad",
272 category = "strings/transform",
273 summary = "Pad strings, character arrays, and cell arrays to a target length.",
274 keywords = "pad,align,strings,character array",
275 accel = "sink"
276)]
277fn pad_builtin(value: Value, rest: Vec<Value>) -> Result<Value, String> {
278 let options = parse_arguments(&rest)?;
279 let gathered = gather_if_needed(&value).map_err(|e| format!("pad: {e}"))?;
280 match gathered {
281 Value::String(text) => pad_string(text, options),
282 Value::StringArray(array) => pad_string_array(array, options),
283 Value::CharArray(array) => pad_char_array(array, options),
284 Value::Cell(cell) => pad_cell_array(cell, options),
285 _ => Err(ARG_TYPE_ERROR.to_string()),
286 }
287}
288
289fn pad_string(text: String, options: PadOptions) -> Result<Value, String> {
290 if is_missing_string(&text) {
291 return Ok(Value::String(text));
292 }
293 let char_count = string_length(&text);
294 let base_target = options.base_target(char_count);
295 let target_len = element_target_length(&options, base_target, char_count);
296 let padded = apply_padding_owned(text, char_count, target_len, &options);
297 Ok(Value::String(padded))
298}
299
300fn pad_string_array(array: StringArray, options: PadOptions) -> Result<Value, String> {
301 let StringArray { data, shape, .. } = array;
302 let mut auto_len: usize = 0;
303 if matches!(options.target, PadTarget::Auto) {
304 for text in &data {
305 if !is_missing_string(text) {
306 auto_len = auto_len.max(string_length(text));
307 }
308 }
309 }
310 let base_target = options.base_target(auto_len);
311 let mut padded: Vec<String> = Vec::with_capacity(data.len());
312 for text in data.into_iter() {
313 if is_missing_string(&text) {
314 padded.push(text);
315 continue;
316 }
317 let char_count = string_length(&text);
318 let target_len = element_target_length(&options, base_target, char_count);
319 let new_text = apply_padding_owned(text, char_count, target_len, &options);
320 padded.push(new_text);
321 }
322 let result = StringArray::new(padded, shape).map_err(|e| format!("pad: {e}"))?;
323 Ok(Value::StringArray(result))
324}
325
326fn pad_char_array(array: CharArray, options: PadOptions) -> Result<Value, String> {
327 let CharArray { data, rows, cols } = array;
328 if rows == 0 {
329 return Ok(Value::CharArray(CharArray { data, rows, cols }));
330 }
331
332 let mut rows_text: Vec<String> = Vec::with_capacity(rows);
333 let mut auto_len = 0usize;
334 for row in 0..rows {
335 let text = char_row_to_string_slice(&data, cols, row);
336 auto_len = auto_len.max(string_length(&text));
337 rows_text.push(text);
338 }
339
340 let base_target = options.base_target(auto_len);
341 let mut padded_rows: Vec<String> = Vec::with_capacity(rows);
342 let mut final_cols: usize = 0;
343 for row_text in rows_text.into_iter() {
344 let char_count = string_length(&row_text);
345 let target_len = element_target_length(&options, base_target, char_count);
346 let padded = apply_padding_owned(row_text, char_count, target_len, &options);
347 final_cols = final_cols.max(string_length(&padded));
348 padded_rows.push(padded);
349 }
350
351 let mut new_data: Vec<char> = Vec::with_capacity(rows * final_cols);
352 for row_text in padded_rows.into_iter() {
353 let mut chars: Vec<char> = row_text.chars().collect();
354 if chars.len() < final_cols {
355 chars.resize(final_cols, ' ');
356 }
357 new_data.extend(chars.into_iter());
358 }
359
360 CharArray::new(new_data, rows, final_cols)
361 .map(Value::CharArray)
362 .map_err(|e| format!("pad: {e}"))
363}
364
365fn pad_cell_array(cell: CellArray, options: PadOptions) -> Result<Value, String> {
366 let rows = cell.rows;
367 let cols = cell.cols;
368 let total = rows * cols;
369 let mut items: Vec<CellItem> = Vec::with_capacity(total);
370 let mut auto_len = 0usize;
371
372 for idx in 0..total {
373 let value = &cell.data[idx];
374 let gathered = gather_if_needed(value).map_err(|e| format!("pad: {e}"))?;
375 let item = match gathered {
376 Value::String(text) => {
377 let is_missing = is_missing_string(&text);
378 let len = if is_missing { 0 } else { string_length(&text) };
379 if !is_missing {
380 auto_len = auto_len.max(len);
381 }
382 CellItem {
383 kind: CellKind::String,
384 text,
385 char_count: len,
386 is_missing,
387 }
388 }
389 Value::StringArray(sa) if sa.data.len() == 1 => {
390 let text = sa.data.into_iter().next().unwrap_or_default();
391 let is_missing = is_missing_string(&text);
392 let len = if is_missing { 0 } else { string_length(&text) };
393 if !is_missing {
394 auto_len = auto_len.max(len);
395 }
396 CellItem {
397 kind: CellKind::String,
398 text,
399 char_count: len,
400 is_missing,
401 }
402 }
403 Value::CharArray(ca) if ca.rows <= 1 => {
404 let text = if ca.rows == 0 {
405 String::new()
406 } else {
407 char_row_to_string_slice(&ca.data, ca.cols, 0)
408 };
409 let len = string_length(&text);
410 auto_len = auto_len.max(len);
411 CellItem {
412 kind: CellKind::Char { rows: ca.rows },
413 text,
414 char_count: len,
415 is_missing: false,
416 }
417 }
418 Value::CharArray(_) => return Err(CELL_ELEMENT_ERROR.to_string()),
419 _ => return Err(CELL_ELEMENT_ERROR.to_string()),
420 };
421 items.push(item);
422 }
423
424 let base_target = options.base_target(auto_len);
425 let mut results: Vec<Value> = Vec::with_capacity(total);
426 for item in items.into_iter() {
427 if item.is_missing {
428 results.push(Value::String(item.text));
429 continue;
430 }
431 let target_len = element_target_length(&options, base_target, item.char_count);
432 let padded = apply_padding_owned(item.text, item.char_count, target_len, &options);
433 match item.kind {
434 CellKind::String => results.push(Value::String(padded)),
435 CellKind::Char { rows } => {
436 let chars: Vec<char> = padded.chars().collect();
437 let cols = chars.len();
438 let array = CharArray::new(chars, rows, cols).map_err(|e| format!("pad: {e}"))?;
439 results.push(Value::CharArray(array));
440 }
441 }
442 }
443
444 make_cell(results, rows, cols).map_err(|e| format!("pad: {e}"))
445}
446
447#[derive(Clone)]
448struct CellItem {
449 kind: CellKind,
450 text: String,
451 char_count: usize,
452 is_missing: bool,
453}
454
455#[derive(Clone)]
456enum CellKind {
457 String,
458 Char { rows: usize },
459}
460
461fn parse_arguments(args: &[Value]) -> Result<PadOptions, String> {
462 let mut options = PadOptions::default();
463 match args.len() {
464 0 => Ok(options),
465 1 => {
466 if let Some(length) = parse_length(&args[0])? {
467 options.target = PadTarget::Length(length);
468 return Ok(options);
469 }
470 if let Some(direction) = try_parse_direction(&args[0], false)? {
471 options.direction = direction;
472 return Ok(options);
473 }
474 let pad_char = parse_pad_char(&args[0])?;
475 options.pad_char = pad_char;
476 Ok(options)
477 }
478 2 => {
479 if let Some(length) = parse_length(&args[0])? {
480 options.target = PadTarget::Length(length);
481 if let Some(direction) = try_parse_direction(&args[1], false)? {
482 options.direction = direction;
483 } else {
484 match parse_pad_char(&args[1]) {
485 Ok(pad_char) => options.pad_char = pad_char,
486 Err(_) => return Err(DIRECTION_ERROR.to_string()),
487 }
488 }
489 Ok(options)
490 } else if let Some(direction) = try_parse_direction(&args[0], false)? {
491 options.direction = direction;
492 let pad_char = parse_pad_char(&args[1])?;
493 options.pad_char = pad_char;
494 Ok(options)
495 } else {
496 Err(ARGUMENT_CONFIG_ERROR.to_string())
497 }
498 }
499 3 => {
500 let length = parse_length(&args[0])?.ok_or_else(|| LENGTH_ERROR.to_string())?;
501 let direction =
502 try_parse_direction(&args[1], true)?.ok_or_else(|| DIRECTION_ERROR.to_string())?;
503 let pad_char = parse_pad_char(&args[2])?;
504 options.target = PadTarget::Length(length);
505 options.direction = direction;
506 options.pad_char = pad_char;
507 Ok(options)
508 }
509 _ => Err("pad: too many input arguments".to_string()),
510 }
511}
512
513fn parse_length(value: &Value) -> Result<Option<usize>, String> {
514 match value {
515 Value::Num(n) => {
516 if !n.is_finite() || *n < 0.0 {
517 return Err(LENGTH_ERROR.to_string());
518 }
519 if (n.fract()).abs() > f64::EPSILON {
520 return Err(LENGTH_ERROR.to_string());
521 }
522 Ok(Some(*n as usize))
523 }
524 Value::Int(i) => {
525 let val = i.to_i64();
526 if val < 0 {
527 return Err(LENGTH_ERROR.to_string());
528 }
529 Ok(Some(val as usize))
530 }
531 _ => Ok(None),
532 }
533}
534
535fn try_parse_direction(value: &Value, strict: bool) -> Result<Option<PadDirection>, String> {
536 let Some(text) = value_to_single_string(value) else {
537 return if strict {
538 Err(DIRECTION_ERROR.to_string())
539 } else {
540 Ok(None)
541 };
542 };
543 let lowered = text.trim().to_ascii_lowercase();
544 if lowered.is_empty() {
545 return if strict {
546 Err(DIRECTION_ERROR.to_string())
547 } else {
548 Ok(None)
549 };
550 }
551 let direction = match lowered.as_str() {
552 "left" => PadDirection::Left,
553 "right" => PadDirection::Right,
554 "both" => PadDirection::Both,
555 _ => {
556 return if strict {
557 Err(DIRECTION_ERROR.to_string())
558 } else {
559 Ok(None)
560 };
561 }
562 };
563 Ok(Some(direction))
564}
565
566fn parse_pad_char(value: &Value) -> Result<char, String> {
567 let text = value_to_single_string(value).ok_or_else(|| PAD_CHAR_ERROR.to_string())?;
568 let mut chars = text.chars();
569 let Some(first) = chars.next() else {
570 return Err(PAD_CHAR_ERROR.to_string());
571 };
572 if chars.next().is_some() {
573 return Err(PAD_CHAR_ERROR.to_string());
574 }
575 Ok(first)
576}
577
578fn value_to_single_string(value: &Value) -> Option<String> {
579 match value {
580 Value::String(text) => Some(text.clone()),
581 Value::StringArray(sa) => {
582 if sa.data.len() == 1 {
583 Some(sa.data[0].clone())
584 } else {
585 None
586 }
587 }
588 Value::CharArray(ca) if ca.rows <= 1 => {
589 if ca.rows == 0 {
590 Some(String::new())
591 } else {
592 Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
593 }
594 }
595 _ => None,
596 }
597}
598
599fn string_length(text: &str) -> usize {
600 text.chars().count()
601}
602
603fn element_target_length(options: &PadOptions, base_target: usize, current_len: usize) -> usize {
604 match options.target {
605 PadTarget::Auto => base_target.max(current_len),
606 PadTarget::Length(_) => base_target.max(current_len),
607 }
608}
609
610fn apply_padding_owned(
611 text: String,
612 current_len: usize,
613 target_len: usize,
614 options: &PadOptions,
615) -> String {
616 if current_len >= target_len {
617 return text;
618 }
619 let delta = target_len - current_len;
620 let (left_pad, right_pad) = match options.direction {
621 PadDirection::Left => (delta, 0),
622 PadDirection::Right => (0, delta),
623 PadDirection::Both => {
624 let left = delta / 2;
625 (left, delta - left)
626 }
627 };
628 let mut result = String::with_capacity(text.len() + delta * options.pad_char.len_utf8());
629 for _ in 0..left_pad {
630 result.push(options.pad_char);
631 }
632 result.push_str(&text);
633 for _ in 0..right_pad {
634 result.push(options.pad_char);
635 }
636 result
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[cfg(any(feature = "doc_export", feature = "wgpu"))]
644 use crate::builtins::common::test_support;
645
646 #[test]
647 fn pad_string_length_right() {
648 let result = pad_builtin(Value::String("GPU".into()), vec![Value::Num(5.0)]).expect("pad");
649 assert_eq!(result, Value::String("GPU ".into()));
650 }
651
652 #[test]
653 fn pad_string_left_with_custom_char() {
654 let result = pad_builtin(
655 Value::String("42".into()),
656 vec![
657 Value::Num(4.0),
658 Value::String("left".into()),
659 Value::String("0".into()),
660 ],
661 )
662 .expect("pad");
663 assert_eq!(result, Value::String("0042".into()));
664 }
665
666 #[test]
667 fn pad_string_both_with_odd_count() {
668 let result = pad_builtin(
669 Value::String("core".into()),
670 vec![
671 Value::Num(9.0),
672 Value::String("both".into()),
673 Value::String("*".into()),
674 ],
675 )
676 .expect("pad");
677 assert_eq!(result, Value::String("**core***".into()));
678 }
679
680 #[test]
681 fn pad_string_array_auto_uses_longest_element() {
682 let strings =
683 StringArray::new(vec!["GPU".into(), "Accelerate".into()], vec![2, 1]).unwrap();
684 let result = pad_builtin(Value::StringArray(strings), Vec::new()).expect("pad");
685 match result {
686 Value::StringArray(sa) => {
687 assert_eq!(sa.data[0], "GPU ");
688 assert_eq!(sa.data[1], "Accelerate");
689 }
690 other => panic!("expected string array, got {other:?}"),
691 }
692 }
693
694 #[test]
695 fn pad_string_array_pad_character_only() {
696 let strings = StringArray::new(vec!["A".into(), "Run".into()], vec![2, 1]).unwrap();
697 let result =
698 pad_builtin(Value::StringArray(strings), vec![Value::String("*".into())]).expect("pad");
699 match result {
700 Value::StringArray(sa) => {
701 assert_eq!(sa.data[0], "A**");
702 assert_eq!(sa.data[1], "Run");
703 }
704 other => panic!("expected string array, got {other:?}"),
705 }
706 }
707
708 #[test]
709 fn pad_string_array_length_with_pad_character() {
710 let strings = StringArray::new(vec!["7".into(), "512".into()], vec![2, 1]).unwrap();
711 let result = pad_builtin(
712 Value::StringArray(strings),
713 vec![Value::Num(4.0), Value::String("0".into())],
714 )
715 .expect("pad");
716 match result {
717 Value::StringArray(sa) => {
718 assert_eq!(sa.data[0], "7000");
719 assert_eq!(sa.data[1], "5120");
720 }
721 other => panic!("expected string array, got {other:?}"),
722 }
723 }
724
725 #[test]
726 fn pad_string_array_direction_only() {
727 let strings =
728 StringArray::new(vec!["Mary".into(), "Elizabeth".into()], vec![2, 1]).unwrap();
729 let result = pad_builtin(
730 Value::StringArray(strings),
731 vec![Value::String("left".into())],
732 )
733 .expect("pad");
734 match result {
735 Value::StringArray(sa) => {
736 assert_eq!(sa.data[0], " Mary");
737 assert_eq!(sa.data[1], "Elizabeth");
738 }
739 other => panic!("expected string array, got {other:?}"),
740 }
741 }
742
743 #[test]
744 fn pad_single_string_pad_character_only_leaves_length() {
745 let result =
746 pad_builtin(Value::String("GPU".into()), vec![Value::String("-".into())]).expect("pad");
747 assert_eq!(result, Value::String("GPU".into()));
748 }
749
750 #[test]
751 fn pad_char_array_resizes_columns() {
752 let chars: Vec<char> = "GPUrun".chars().collect();
753 let array = CharArray::new(chars, 2, 3).unwrap();
754 let result = pad_builtin(Value::CharArray(array), vec![Value::Num(5.0)]).expect("pad");
755 match result {
756 Value::CharArray(ca) => {
757 assert_eq!(ca.rows, 2);
758 assert_eq!(ca.cols, 5);
759 let expected: Vec<char> = "GPU run ".chars().collect();
760 assert_eq!(ca.data, expected);
761 }
762 other => panic!("expected char array, got {other:?}"),
763 }
764 }
765
766 #[test]
767 fn pad_cell_array_mixed_content() {
768 let cell = CellArray::new(
769 vec![
770 Value::String("solver".into()),
771 Value::CharArray(CharArray::new_row("jit")),
772 Value::String("planner".into()),
773 ],
774 1,
775 3,
776 )
777 .unwrap();
778 let result = pad_builtin(
779 Value::Cell(cell),
780 vec![Value::String("right".into()), Value::String(".".into())],
781 )
782 .expect("pad");
783 match result {
784 Value::Cell(out) => {
785 assert_eq!(out.rows, 1);
786 assert_eq!(out.cols, 3);
787 assert_eq!(out.get(0, 0).unwrap(), Value::String("solver.".into()));
788 assert_eq!(
789 out.get(0, 1).unwrap(),
790 Value::CharArray(CharArray::new_row("jit...."))
791 );
792 assert_eq!(out.get(0, 2).unwrap(), Value::String("planner".into()));
793 }
794 other => panic!("expected cell array, got {other:?}"),
795 }
796 }
797
798 #[test]
799 fn pad_preserves_missing_string() {
800 let result =
801 pad_builtin(Value::String("<missing>".into()), vec![Value::Num(8.0)]).expect("pad");
802 assert_eq!(result, Value::String("<missing>".into()));
803 }
804
805 #[test]
806 fn pad_errors_on_invalid_input_type() {
807 let err = pad_builtin(Value::Num(1.0), Vec::new()).unwrap_err();
808 assert_eq!(err, ARG_TYPE_ERROR);
809 }
810
811 #[test]
812 fn pad_errors_on_negative_length() {
813 let err = pad_builtin(Value::String("data".into()), vec![Value::Num(-1.0)]).unwrap_err();
814 assert_eq!(err, LENGTH_ERROR);
815 }
816
817 #[test]
818 fn pad_errors_on_invalid_direction() {
819 let err = pad_builtin(
820 Value::String("data".into()),
821 vec![Value::Num(6.0), Value::String("around".into())],
822 )
823 .unwrap_err();
824 assert_eq!(err, DIRECTION_ERROR);
825 }
826
827 #[test]
828 fn pad_errors_on_invalid_pad_character() {
829 let err = pad_builtin(
830 Value::String("data".into()),
831 vec![Value::String("left".into()), Value::String("##".into())],
832 )
833 .unwrap_err();
834 assert_eq!(err, PAD_CHAR_ERROR);
835 }
836
837 #[test]
838 #[cfg(feature = "wgpu")]
839 fn pad_works_with_wgpu_provider_active() {
840 test_support::with_test_provider(|_| {
841 let result =
842 pad_builtin(Value::String("GPU".into()), vec![Value::Num(6.0)]).expect("pad");
843 assert_eq!(result, Value::String("GPU ".into()));
844 });
845 }
846
847 #[test]
848 #[cfg(feature = "doc_export")]
849 fn doc_examples_present() {
850 let blocks = test_support::doc_examples(DOC_MD);
851 assert!(!blocks.is_empty());
852 }
853}