1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6 CellArray, CharArray, StringArray, Value,
7};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::map_control_flow_with_builtin;
11use crate::builtins::common::spec::{
12 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13 ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::strings::common::{char_row_to_string_slice, is_missing_string};
16use crate::builtins::strings::type_resolvers::text_preserve_type;
17use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
18
19#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::transform::pad")]
20pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
21 name: "pad",
22 op_kind: GpuOpKind::Custom("string-transform"),
23 supported_precisions: &[],
24 broadcast: BroadcastSemantics::None,
25 provider_hooks: &[],
26 constant_strategy: ConstantStrategy::InlineLiteral,
27 residency: ResidencyPolicy::GatherImmediately,
28 nan_mode: ReductionNaN::Include,
29 two_pass_threshold: None,
30 workgroup_size: None,
31 accepts_nan_mode: false,
32 notes: "Executes on the CPU; GPU-resident inputs are gathered before padding to preserve MATLAB semantics.",
33};
34
35#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::transform::pad")]
36pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
37 name: "pad",
38 shape: ShapeRequirements::Any,
39 constant_strategy: ConstantStrategy::InlineLiteral,
40 elementwise: None,
41 reduction: None,
42 emits_nan: false,
43 notes: "String transformation builtin; always gathers inputs and is not eligible for fusion.",
44};
45
46const BUILTIN_NAME: &str = "pad";
47const PAD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
48 name: "out",
49 ty: BuiltinParamType::Any,
50 arity: BuiltinParamArity::Required,
51 default: None,
52 description: "Padded text preserving input container kind and shape.",
53}];
54
55const PAD_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
56 name: "str",
57 ty: BuiltinParamType::Any,
58 arity: BuiltinParamArity::Required,
59 default: None,
60 description: "Input text (string/char/cell).",
61}];
62
63const PAD_INPUTS_LENGTH: [BuiltinParamDescriptor; 2] = [
64 BuiltinParamDescriptor {
65 name: "str",
66 ty: BuiltinParamType::Any,
67 arity: BuiltinParamArity::Required,
68 default: None,
69 description: "Input text (string/char/cell).",
70 },
71 BuiltinParamDescriptor {
72 name: "len",
73 ty: BuiltinParamType::IntegerScalar,
74 arity: BuiltinParamArity::Required,
75 default: None,
76 description: "Target length (non-negative integer).",
77 },
78];
79
80const PAD_INPUTS_DIRECTION: [BuiltinParamDescriptor; 2] = [
81 BuiltinParamDescriptor {
82 name: "str",
83 ty: BuiltinParamType::Any,
84 arity: BuiltinParamArity::Required,
85 default: None,
86 description: "Input text (string/char/cell).",
87 },
88 BuiltinParamDescriptor {
89 name: "direction",
90 ty: BuiltinParamType::StringScalar,
91 arity: BuiltinParamArity::Required,
92 default: Some("\"right\""),
93 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
94 },
95];
96
97const PAD_INPUTS_PADCHAR: [BuiltinParamDescriptor; 2] = [
98 BuiltinParamDescriptor {
99 name: "str",
100 ty: BuiltinParamType::Any,
101 arity: BuiltinParamArity::Required,
102 default: None,
103 description: "Input text (string/char/cell).",
104 },
105 BuiltinParamDescriptor {
106 name: "padCharacter",
107 ty: BuiltinParamType::StringScalar,
108 arity: BuiltinParamArity::Required,
109 default: Some("\" \""),
110 description: "Single-character padding value.",
111 },
112];
113
114const PAD_INPUTS_LENGTH_DIRECTION: [BuiltinParamDescriptor; 3] = [
115 BuiltinParamDescriptor {
116 name: "str",
117 ty: BuiltinParamType::Any,
118 arity: BuiltinParamArity::Required,
119 default: None,
120 description: "Input text (string/char/cell).",
121 },
122 BuiltinParamDescriptor {
123 name: "len",
124 ty: BuiltinParamType::IntegerScalar,
125 arity: BuiltinParamArity::Required,
126 default: None,
127 description: "Target length (non-negative integer).",
128 },
129 BuiltinParamDescriptor {
130 name: "direction",
131 ty: BuiltinParamType::StringScalar,
132 arity: BuiltinParamArity::Required,
133 default: Some("\"right\""),
134 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
135 },
136];
137
138const PAD_INPUTS_LENGTH_PADCHAR: [BuiltinParamDescriptor; 3] = [
139 BuiltinParamDescriptor {
140 name: "str",
141 ty: BuiltinParamType::Any,
142 arity: BuiltinParamArity::Required,
143 default: None,
144 description: "Input text (string/char/cell).",
145 },
146 BuiltinParamDescriptor {
147 name: "len",
148 ty: BuiltinParamType::IntegerScalar,
149 arity: BuiltinParamArity::Required,
150 default: None,
151 description: "Target length (non-negative integer).",
152 },
153 BuiltinParamDescriptor {
154 name: "padCharacter",
155 ty: BuiltinParamType::StringScalar,
156 arity: BuiltinParamArity::Required,
157 default: Some("\" \""),
158 description: "Single-character padding value.",
159 },
160];
161
162const PAD_INPUTS_DIRECTION_PADCHAR: [BuiltinParamDescriptor; 3] = [
163 BuiltinParamDescriptor {
164 name: "str",
165 ty: BuiltinParamType::Any,
166 arity: BuiltinParamArity::Required,
167 default: None,
168 description: "Input text (string/char/cell).",
169 },
170 BuiltinParamDescriptor {
171 name: "direction",
172 ty: BuiltinParamType::StringScalar,
173 arity: BuiltinParamArity::Required,
174 default: Some("\"right\""),
175 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
176 },
177 BuiltinParamDescriptor {
178 name: "padCharacter",
179 ty: BuiltinParamType::StringScalar,
180 arity: BuiltinParamArity::Required,
181 default: Some("\" \""),
182 description: "Single-character padding value.",
183 },
184];
185
186const PAD_INPUTS_LENGTH_DIRECTION_PADCHAR: [BuiltinParamDescriptor; 4] = [
187 BuiltinParamDescriptor {
188 name: "str",
189 ty: BuiltinParamType::Any,
190 arity: BuiltinParamArity::Required,
191 default: None,
192 description: "Input text (string/char/cell).",
193 },
194 BuiltinParamDescriptor {
195 name: "len",
196 ty: BuiltinParamType::IntegerScalar,
197 arity: BuiltinParamArity::Required,
198 default: None,
199 description: "Target length (non-negative integer).",
200 },
201 BuiltinParamDescriptor {
202 name: "direction",
203 ty: BuiltinParamType::StringScalar,
204 arity: BuiltinParamArity::Required,
205 default: Some("\"right\""),
206 description: "Padding direction (`\"left\"|\"right\"|\"both\"`).",
207 },
208 BuiltinParamDescriptor {
209 name: "padCharacter",
210 ty: BuiltinParamType::StringScalar,
211 arity: BuiltinParamArity::Required,
212 default: Some("\" \""),
213 description: "Single-character padding value.",
214 },
215];
216
217const PAD_SIGNATURES: [BuiltinSignatureDescriptor; 8] = [
218 BuiltinSignatureDescriptor {
219 label: "out = pad(str)",
220 inputs: &PAD_INPUTS_BASE,
221 outputs: &PAD_OUTPUT,
222 },
223 BuiltinSignatureDescriptor {
224 label: "out = pad(str, len)",
225 inputs: &PAD_INPUTS_LENGTH,
226 outputs: &PAD_OUTPUT,
227 },
228 BuiltinSignatureDescriptor {
229 label: "out = pad(str, direction)",
230 inputs: &PAD_INPUTS_DIRECTION,
231 outputs: &PAD_OUTPUT,
232 },
233 BuiltinSignatureDescriptor {
234 label: "out = pad(str, padCharacter)",
235 inputs: &PAD_INPUTS_PADCHAR,
236 outputs: &PAD_OUTPUT,
237 },
238 BuiltinSignatureDescriptor {
239 label: "out = pad(str, len, direction)",
240 inputs: &PAD_INPUTS_LENGTH_DIRECTION,
241 outputs: &PAD_OUTPUT,
242 },
243 BuiltinSignatureDescriptor {
244 label: "out = pad(str, len, padCharacter)",
245 inputs: &PAD_INPUTS_LENGTH_PADCHAR,
246 outputs: &PAD_OUTPUT,
247 },
248 BuiltinSignatureDescriptor {
249 label: "out = pad(str, direction, padCharacter)",
250 inputs: &PAD_INPUTS_DIRECTION_PADCHAR,
251 outputs: &PAD_OUTPUT,
252 },
253 BuiltinSignatureDescriptor {
254 label: "out = pad(str, len, direction, padCharacter)",
255 inputs: &PAD_INPUTS_LENGTH_DIRECTION_PADCHAR,
256 outputs: &PAD_OUTPUT,
257 },
258];
259
260const PAD_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
261 code: "RM.PAD.INVALID_INPUT",
262 identifier: Some("RunMat:pad:InvalidInput"),
263 when: "First argument is not a string array, char array, or cell array of text scalars.",
264 message:
265 "pad: first argument must be a string array, character array, or cell array of character vectors",
266};
267
268const PAD_ERROR_LENGTH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
269 code: "RM.PAD.LENGTH",
270 identifier: Some("RunMat:pad:Length"),
271 when: "Length argument is not a non-negative integer scalar.",
272 message: "pad: target length must be a non-negative integer scalar",
273};
274
275const PAD_ERROR_DIRECTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
276 code: "RM.PAD.DIRECTION",
277 identifier: Some("RunMat:pad:Direction"),
278 when: "Direction argument is not one of left/right/both.",
279 message: "pad: direction must be 'left', 'right', or 'both'",
280};
281
282const PAD_ERROR_PAD_CHAR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
283 code: "RM.PAD.PAD_CHAR",
284 identifier: Some("RunMat:pad:PadChar"),
285 when: "Padding character is not a single-character string/char scalar.",
286 message:
287 "pad: padding character must be a string scalar or character vector containing one character",
288};
289
290const PAD_ERROR_CELL_ELEMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
291 code: "RM.PAD.CELL_ELEMENT",
292 identifier: Some("RunMat:pad:CellElement"),
293 when: "Cell arrays contain non-text elements or non-row char arrays.",
294 message: "pad: cell array elements must be string scalars or character vectors",
295};
296
297const PAD_ERROR_ARGUMENT_CONFIG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
298 code: "RM.PAD.ARGUMENT_CONFIG",
299 identifier: Some("RunMat:pad:ArgumentConfig"),
300 when: "Second/third arguments cannot be interpreted as valid pad argument combinations.",
301 message: "pad: unable to interpret input arguments",
302};
303
304const PAD_ERROR_ARG_COUNT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
305 code: "RM.PAD.ARG_COUNT",
306 identifier: Some("RunMat:pad:ArgCount"),
307 when: "More than four total arguments are supplied.",
308 message: "pad: too many input arguments",
309};
310
311const PAD_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
312 code: "RM.PAD.INTERNAL",
313 identifier: Some("RunMat:pad:InternalError"),
314 when: "Internal output container construction failed.",
315 message: "pad: internal error",
316};
317
318const PAD_ERRORS: [BuiltinErrorDescriptor; 8] = [
319 PAD_ERROR_INVALID_INPUT,
320 PAD_ERROR_LENGTH,
321 PAD_ERROR_DIRECTION,
322 PAD_ERROR_PAD_CHAR,
323 PAD_ERROR_CELL_ELEMENT,
324 PAD_ERROR_ARGUMENT_CONFIG,
325 PAD_ERROR_ARG_COUNT,
326 PAD_ERROR_INTERNAL,
327];
328
329pub const PAD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
330 signatures: &PAD_SIGNATURES,
331 output_mode: BuiltinOutputMode::Fixed,
332 completion_policy: BuiltinCompletionPolicy::Public,
333 errors: &PAD_ERRORS,
334};
335
336fn map_flow(err: RuntimeError) -> RuntimeError {
337 map_control_flow_with_builtin(err, BUILTIN_NAME)
338}
339
340fn pad_error_with_message(
341 message: impl Into<String>,
342 error: &'static BuiltinErrorDescriptor,
343) -> RuntimeError {
344 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
345 if let Some(identifier) = error.identifier {
346 builder = builder.with_identifier(identifier);
347 }
348 builder.build()
349}
350
351fn pad_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
352 pad_error_with_message(error.message, error)
353}
354
355#[derive(Clone, Copy, Eq, PartialEq)]
356enum PadDirection {
357 Left,
358 Right,
359 Both,
360}
361
362#[derive(Clone, Copy)]
363enum PadTarget {
364 Auto,
365 Length(usize),
366}
367
368#[derive(Clone, Copy)]
369struct PadOptions {
370 target: PadTarget,
371 direction: PadDirection,
372 pad_char: char,
373}
374
375impl Default for PadOptions {
376 fn default() -> Self {
377 Self {
378 target: PadTarget::Auto,
379 direction: PadDirection::Right,
380 pad_char: ' ',
381 }
382 }
383}
384
385impl PadOptions {
386 fn base_target(&self, auto_target: usize) -> usize {
387 match self.target {
388 PadTarget::Auto => auto_target,
389 PadTarget::Length(len) => len,
390 }
391 }
392}
393
394#[runtime_builtin(
395 name = "pad",
396 category = "strings/transform",
397 summary = "Pad text values to target lengths with configurable direction and fill characters.",
398 keywords = "pad,align,strings,character array",
399 accel = "sink",
400 type_resolver(text_preserve_type),
401 descriptor(crate::builtins::strings::transform::pad::PAD_DESCRIPTOR),
402 builtin_path = "crate::builtins::strings::transform::pad"
403)]
404async fn pad_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
405 let options = parse_arguments(&rest)?;
406 let gathered = gather_if_needed_async(&value).await.map_err(map_flow)?;
407 match gathered {
408 Value::String(text) => pad_string(text, options),
409 Value::StringArray(array) => pad_string_array(array, options),
410 Value::CharArray(array) => pad_char_array(array, options),
411 Value::Cell(cell) => pad_cell_array(cell, options).await,
412 _ => Err(pad_error(&PAD_ERROR_INVALID_INPUT)),
413 }
414}
415
416fn pad_string(text: String, options: PadOptions) -> BuiltinResult<Value> {
417 if is_missing_string(&text) {
418 return Ok(Value::String(text));
419 }
420 let char_count = string_length(&text);
421 let base_target = options.base_target(char_count);
422 let target_len = element_target_length(&options, base_target, char_count);
423 let padded = apply_padding_owned(text, char_count, target_len, &options);
424 Ok(Value::String(padded))
425}
426
427fn pad_string_array(array: StringArray, options: PadOptions) -> BuiltinResult<Value> {
428 let StringArray { data, shape, .. } = array;
429 let mut auto_len: usize = 0;
430 if matches!(options.target, PadTarget::Auto) {
431 for text in &data {
432 if !is_missing_string(text) {
433 auto_len = auto_len.max(string_length(text));
434 }
435 }
436 }
437 let base_target = options.base_target(auto_len);
438 let mut padded: Vec<String> = Vec::with_capacity(data.len());
439 for text in data.into_iter() {
440 if is_missing_string(&text) {
441 padded.push(text);
442 continue;
443 }
444 let char_count = string_length(&text);
445 let target_len = element_target_length(&options, base_target, char_count);
446 let new_text = apply_padding_owned(text, char_count, target_len, &options);
447 padded.push(new_text);
448 }
449 let result = StringArray::new(padded, shape)
450 .map_err(|e| pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL))?;
451 Ok(Value::StringArray(result))
452}
453
454fn pad_char_array(array: CharArray, options: PadOptions) -> BuiltinResult<Value> {
455 let CharArray { data, rows, cols } = array;
456 if rows == 0 {
457 return Ok(Value::CharArray(CharArray { data, rows, cols }));
458 }
459
460 let mut rows_text: Vec<String> = Vec::with_capacity(rows);
461 let mut auto_len = 0usize;
462 for row in 0..rows {
463 let text = char_row_to_string_slice(&data, cols, row);
464 auto_len = auto_len.max(string_length(&text));
465 rows_text.push(text);
466 }
467
468 let base_target = options.base_target(auto_len);
469 let mut padded_rows: Vec<String> = Vec::with_capacity(rows);
470 let mut final_cols: usize = 0;
471 for row_text in rows_text.into_iter() {
472 let char_count = string_length(&row_text);
473 let target_len = element_target_length(&options, base_target, char_count);
474 let padded = apply_padding_owned(row_text, char_count, target_len, &options);
475 final_cols = final_cols.max(string_length(&padded));
476 padded_rows.push(padded);
477 }
478
479 let mut new_data: Vec<char> = Vec::with_capacity(rows * final_cols);
480 for row_text in padded_rows.into_iter() {
481 let mut chars: Vec<char> = row_text.chars().collect();
482 if chars.len() < final_cols {
483 chars.resize(final_cols, ' ');
484 }
485 new_data.extend(chars.into_iter());
486 }
487
488 CharArray::new(new_data, rows, final_cols)
489 .map(Value::CharArray)
490 .map_err(|e| pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL))
491}
492
493async fn pad_cell_array(cell: CellArray, options: PadOptions) -> BuiltinResult<Value> {
494 let rows = cell.rows;
495 let cols = cell.cols;
496 let total = rows * cols;
497 let mut items: Vec<CellItem> = Vec::with_capacity(total);
498 let mut auto_len = 0usize;
499
500 for idx in 0..total {
501 let value = &cell.data[idx];
502 let gathered = gather_if_needed_async(value).await.map_err(map_flow)?;
503 let item = match gathered {
504 Value::String(text) => {
505 let is_missing = is_missing_string(&text);
506 let len = if is_missing { 0 } else { string_length(&text) };
507 if !is_missing {
508 auto_len = auto_len.max(len);
509 }
510 CellItem {
511 kind: CellKind::String,
512 text,
513 char_count: len,
514 is_missing,
515 }
516 }
517 Value::StringArray(sa) if sa.data.len() == 1 => {
518 let text = sa.data.into_iter().next().unwrap_or_default();
519 let is_missing = is_missing_string(&text);
520 let len = if is_missing { 0 } else { string_length(&text) };
521 if !is_missing {
522 auto_len = auto_len.max(len);
523 }
524 CellItem {
525 kind: CellKind::String,
526 text,
527 char_count: len,
528 is_missing,
529 }
530 }
531 Value::CharArray(ca) if ca.rows <= 1 => {
532 let text = if ca.rows == 0 {
533 String::new()
534 } else {
535 char_row_to_string_slice(&ca.data, ca.cols, 0)
536 };
537 let len = string_length(&text);
538 auto_len = auto_len.max(len);
539 CellItem {
540 kind: CellKind::Char { rows: ca.rows },
541 text,
542 char_count: len,
543 is_missing: false,
544 }
545 }
546 Value::CharArray(_) => return Err(pad_error(&PAD_ERROR_CELL_ELEMENT)),
547 _ => return Err(pad_error(&PAD_ERROR_CELL_ELEMENT)),
548 };
549 items.push(item);
550 }
551
552 let base_target = options.base_target(auto_len);
553 let mut results: Vec<Value> = Vec::with_capacity(total);
554 for item in items.into_iter() {
555 if item.is_missing {
556 results.push(Value::String(item.text));
557 continue;
558 }
559 let target_len = element_target_length(&options, base_target, item.char_count);
560 let padded = apply_padding_owned(item.text, item.char_count, target_len, &options);
561 match item.kind {
562 CellKind::String => results.push(Value::String(padded)),
563 CellKind::Char { rows } => {
564 let chars: Vec<char> = padded.chars().collect();
565 let cols = chars.len();
566 let array = CharArray::new(chars, rows, cols).map_err(|e| {
567 pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL)
568 })?;
569 results.push(Value::CharArray(array));
570 }
571 }
572 }
573
574 make_cell(results, rows, cols)
575 .map_err(|e| pad_error_with_message(format!("{BUILTIN_NAME}: {e}"), &PAD_ERROR_INTERNAL))
576}
577
578#[derive(Clone)]
579struct CellItem {
580 kind: CellKind,
581 text: String,
582 char_count: usize,
583 is_missing: bool,
584}
585
586#[derive(Clone)]
587enum CellKind {
588 String,
589 Char { rows: usize },
590}
591
592fn parse_arguments(args: &[Value]) -> BuiltinResult<PadOptions> {
593 let mut options = PadOptions::default();
594 match args.len() {
595 0 => Ok(options),
596 1 => {
597 if let Some(length) = parse_length(&args[0])? {
598 options.target = PadTarget::Length(length);
599 return Ok(options);
600 }
601 if let Some(direction) = try_parse_direction(&args[0], false)? {
602 options.direction = direction;
603 return Ok(options);
604 }
605 let pad_char = parse_pad_char(&args[0])?;
606 options.pad_char = pad_char;
607 Ok(options)
608 }
609 2 => {
610 if let Some(length) = parse_length(&args[0])? {
611 options.target = PadTarget::Length(length);
612 if let Some(direction) = try_parse_direction(&args[1], false)? {
613 options.direction = direction;
614 } else {
615 match parse_pad_char(&args[1]) {
616 Ok(pad_char) => options.pad_char = pad_char,
617 Err(_) => return Err(pad_error(&PAD_ERROR_DIRECTION)),
618 }
619 }
620 Ok(options)
621 } else if let Some(direction) = try_parse_direction(&args[0], false)? {
622 options.direction = direction;
623 let pad_char = parse_pad_char(&args[1])?;
624 options.pad_char = pad_char;
625 Ok(options)
626 } else {
627 Err(pad_error(&PAD_ERROR_ARGUMENT_CONFIG))
628 }
629 }
630 3 => {
631 let length = parse_length(&args[0])?.ok_or_else(|| pad_error(&PAD_ERROR_LENGTH))?;
632 let direction = try_parse_direction(&args[1], true)?
633 .ok_or_else(|| pad_error(&PAD_ERROR_DIRECTION))?;
634 let pad_char = parse_pad_char(&args[2])?;
635 options.target = PadTarget::Length(length);
636 options.direction = direction;
637 options.pad_char = pad_char;
638 Ok(options)
639 }
640 _ => Err(pad_error(&PAD_ERROR_ARG_COUNT)),
641 }
642}
643
644fn parse_length(value: &Value) -> BuiltinResult<Option<usize>> {
645 match value {
646 Value::Num(n) => {
647 if !n.is_finite() || *n < 0.0 {
648 return Err(pad_error(&PAD_ERROR_LENGTH));
649 }
650 if (n.fract()).abs() > f64::EPSILON {
651 return Err(pad_error(&PAD_ERROR_LENGTH));
652 }
653 Ok(Some(*n as usize))
654 }
655 Value::Int(i) => {
656 let val = i.to_i64();
657 if val < 0 {
658 return Err(pad_error(&PAD_ERROR_LENGTH));
659 }
660 Ok(Some(val as usize))
661 }
662 _ => Ok(None),
663 }
664}
665
666fn try_parse_direction(value: &Value, strict: bool) -> BuiltinResult<Option<PadDirection>> {
667 let Some(text) = value_to_single_string(value) else {
668 return if strict {
669 Err(pad_error(&PAD_ERROR_DIRECTION))
670 } else {
671 Ok(None)
672 };
673 };
674 let lowered = text.trim().to_ascii_lowercase();
675 if lowered.is_empty() {
676 return if strict {
677 Err(pad_error(&PAD_ERROR_DIRECTION))
678 } else {
679 Ok(None)
680 };
681 }
682 let direction = match lowered.as_str() {
683 "left" => PadDirection::Left,
684 "right" => PadDirection::Right,
685 "both" => PadDirection::Both,
686 _ => {
687 return if strict {
688 Err(pad_error(&PAD_ERROR_DIRECTION))
689 } else {
690 Ok(None)
691 };
692 }
693 };
694 Ok(Some(direction))
695}
696
697fn parse_pad_char(value: &Value) -> BuiltinResult<char> {
698 let text = value_to_single_string(value).ok_or_else(|| pad_error(&PAD_ERROR_PAD_CHAR))?;
699 let mut chars = text.chars();
700 let Some(first) = chars.next() else {
701 return Err(pad_error(&PAD_ERROR_PAD_CHAR));
702 };
703 if chars.next().is_some() {
704 return Err(pad_error(&PAD_ERROR_PAD_CHAR));
705 }
706 Ok(first)
707}
708
709fn value_to_single_string(value: &Value) -> Option<String> {
710 match value {
711 Value::String(text) => Some(text.clone()),
712 Value::StringArray(sa) => {
713 if sa.data.len() == 1 {
714 Some(sa.data[0].clone())
715 } else {
716 None
717 }
718 }
719 Value::CharArray(ca) if ca.rows <= 1 => {
720 if ca.rows == 0 {
721 Some(String::new())
722 } else {
723 Some(char_row_to_string_slice(&ca.data, ca.cols, 0))
724 }
725 }
726 _ => None,
727 }
728}
729
730fn string_length(text: &str) -> usize {
731 text.chars().count()
732}
733
734fn element_target_length(options: &PadOptions, base_target: usize, current_len: usize) -> usize {
735 match options.target {
736 PadTarget::Auto => base_target.max(current_len),
737 PadTarget::Length(_) => base_target.max(current_len),
738 }
739}
740
741fn apply_padding_owned(
742 text: String,
743 current_len: usize,
744 target_len: usize,
745 options: &PadOptions,
746) -> String {
747 if current_len >= target_len {
748 return text;
749 }
750 let delta = target_len - current_len;
751 let (left_pad, right_pad) = match options.direction {
752 PadDirection::Left => (delta, 0),
753 PadDirection::Right => (0, delta),
754 PadDirection::Both => {
755 let left = delta / 2;
756 (left, delta - left)
757 }
758 };
759 let mut result = String::with_capacity(text.len() + delta * options.pad_char.len_utf8());
760 for _ in 0..left_pad {
761 result.push(options.pad_char);
762 }
763 result.push_str(&text);
764 for _ in 0..right_pad {
765 result.push(options.pad_char);
766 }
767 result
768}
769
770#[cfg(test)]
771pub(crate) mod tests {
772 use super::*;
773 #[cfg(feature = "wgpu")]
774 use crate::builtins::common::test_support;
775 use runmat_builtins::{ResolveContext, Type};
776
777 fn pad_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
778 futures::executor::block_on(super::pad_builtin(value, rest))
779 }
780
781 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
782 #[test]
783 fn pad_string_length_right() {
784 let result = pad_builtin(Value::String("GPU".into()), vec![Value::Num(5.0)]).expect("pad");
785 assert_eq!(result, Value::String("GPU ".into()));
786 }
787
788 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
789 #[test]
790 fn pad_string_left_with_custom_char() {
791 let result = pad_builtin(
792 Value::String("42".into()),
793 vec![
794 Value::Num(4.0),
795 Value::String("left".into()),
796 Value::String("0".into()),
797 ],
798 )
799 .expect("pad");
800 assert_eq!(result, Value::String("0042".into()));
801 }
802
803 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
804 #[test]
805 fn pad_string_both_with_odd_count() {
806 let result = pad_builtin(
807 Value::String("core".into()),
808 vec![
809 Value::Num(9.0),
810 Value::String("both".into()),
811 Value::String("*".into()),
812 ],
813 )
814 .expect("pad");
815 assert_eq!(result, Value::String("**core***".into()));
816 }
817
818 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
819 #[test]
820 fn pad_string_array_auto_uses_longest_element() {
821 let strings =
822 StringArray::new(vec!["GPU".into(), "Accelerate".into()], vec![2, 1]).unwrap();
823 let result = pad_builtin(Value::StringArray(strings), Vec::new()).expect("pad");
824 match result {
825 Value::StringArray(sa) => {
826 assert_eq!(sa.data[0], "GPU ");
827 assert_eq!(sa.data[1], "Accelerate");
828 }
829 other => panic!("expected string array, got {other:?}"),
830 }
831 }
832
833 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
834 #[test]
835 fn pad_string_array_pad_character_only() {
836 let strings = StringArray::new(vec!["A".into(), "Run".into()], vec![2, 1]).unwrap();
837 let result =
838 pad_builtin(Value::StringArray(strings), vec![Value::String("*".into())]).expect("pad");
839 match result {
840 Value::StringArray(sa) => {
841 assert_eq!(sa.data[0], "A**");
842 assert_eq!(sa.data[1], "Run");
843 }
844 other => panic!("expected string array, got {other:?}"),
845 }
846 }
847
848 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
849 #[test]
850 fn pad_string_array_length_with_pad_character() {
851 let strings = StringArray::new(vec!["7".into(), "512".into()], vec![2, 1]).unwrap();
852 let result = pad_builtin(
853 Value::StringArray(strings),
854 vec![Value::Num(4.0), Value::String("0".into())],
855 )
856 .expect("pad");
857 match result {
858 Value::StringArray(sa) => {
859 assert_eq!(sa.data[0], "7000");
860 assert_eq!(sa.data[1], "5120");
861 }
862 other => panic!("expected string array, got {other:?}"),
863 }
864 }
865
866 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
867 #[test]
868 fn pad_string_array_direction_only() {
869 let strings =
870 StringArray::new(vec!["Mary".into(), "Elizabeth".into()], vec![2, 1]).unwrap();
871 let result = pad_builtin(
872 Value::StringArray(strings),
873 vec![Value::String("left".into())],
874 )
875 .expect("pad");
876 match result {
877 Value::StringArray(sa) => {
878 assert_eq!(sa.data[0], " Mary");
879 assert_eq!(sa.data[1], "Elizabeth");
880 }
881 other => panic!("expected string array, got {other:?}"),
882 }
883 }
884
885 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
886 #[test]
887 fn pad_single_string_pad_character_only_leaves_length() {
888 let result =
889 pad_builtin(Value::String("GPU".into()), vec![Value::String("-".into())]).expect("pad");
890 assert_eq!(result, Value::String("GPU".into()));
891 }
892
893 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
894 #[test]
895 fn pad_char_array_resizes_columns() {
896 let chars: Vec<char> = "GPUrun".chars().collect();
897 let array = CharArray::new(chars, 2, 3).unwrap();
898 let result = pad_builtin(Value::CharArray(array), vec![Value::Num(5.0)]).expect("pad");
899 match result {
900 Value::CharArray(ca) => {
901 assert_eq!(ca.rows, 2);
902 assert_eq!(ca.cols, 5);
903 let expected: Vec<char> = "GPU run ".chars().collect();
904 assert_eq!(ca.data, expected);
905 }
906 other => panic!("expected char array, got {other:?}"),
907 }
908 }
909
910 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
911 #[test]
912 fn pad_cell_array_mixed_content() {
913 let cell = CellArray::new(
914 vec![
915 Value::String("solver".into()),
916 Value::CharArray(CharArray::new_row("jit")),
917 Value::String("planner".into()),
918 ],
919 1,
920 3,
921 )
922 .unwrap();
923 let result = pad_builtin(
924 Value::Cell(cell),
925 vec![Value::String("right".into()), Value::String(".".into())],
926 )
927 .expect("pad");
928 match result {
929 Value::Cell(out) => {
930 assert_eq!(out.rows, 1);
931 assert_eq!(out.cols, 3);
932 assert_eq!(out.get(0, 0).unwrap(), Value::String("solver.".into()));
933 assert_eq!(
934 out.get(0, 1).unwrap(),
935 Value::CharArray(CharArray::new_row("jit...."))
936 );
937 assert_eq!(out.get(0, 2).unwrap(), Value::String("planner".into()));
938 }
939 other => panic!("expected cell array, got {other:?}"),
940 }
941 }
942
943 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
944 #[test]
945 fn pad_preserves_missing_string() {
946 let result =
947 pad_builtin(Value::String("<missing>".into()), vec![Value::Num(8.0)]).expect("pad");
948 assert_eq!(result, Value::String("<missing>".into()));
949 }
950
951 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
952 #[test]
953 fn pad_errors_on_invalid_input_type() {
954 let err = pad_builtin(Value::Num(1.0), Vec::new()).unwrap_err();
955 assert_eq!(err.to_string(), PAD_ERROR_INVALID_INPUT.message);
956 }
957
958 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
959 #[test]
960 fn pad_errors_on_negative_length() {
961 let err = pad_builtin(Value::String("data".into()), vec![Value::Num(-1.0)]).unwrap_err();
962 assert_eq!(err.to_string(), PAD_ERROR_LENGTH.message);
963 }
964
965 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
966 #[test]
967 fn pad_errors_on_invalid_direction() {
968 let err = pad_builtin(
969 Value::String("data".into()),
970 vec![Value::Num(6.0), Value::String("around".into())],
971 )
972 .unwrap_err();
973 assert_eq!(err.to_string(), PAD_ERROR_DIRECTION.message);
974 }
975
976 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
977 #[test]
978 fn pad_errors_on_invalid_pad_character() {
979 let err = pad_builtin(
980 Value::String("data".into()),
981 vec![Value::String("left".into()), Value::String("##".into())],
982 )
983 .unwrap_err();
984 assert_eq!(err.to_string(), PAD_ERROR_PAD_CHAR.message);
985 }
986
987 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
988 #[test]
989 #[cfg(feature = "wgpu")]
990 fn pad_works_with_wgpu_provider_active() {
991 test_support::with_test_provider(|_| {
992 let result =
993 pad_builtin(Value::String("GPU".into()), vec![Value::Num(6.0)]).expect("pad");
994 assert_eq!(result, Value::String("GPU ".into()));
995 });
996 }
997
998 #[test]
999 fn pad_type_preserves_text() {
1000 assert_eq!(
1001 text_preserve_type(&[Type::String], &ResolveContext::new(Vec::new())),
1002 Type::String
1003 );
1004 }
1005}