Skip to main content

runmat_runtime/builtins/strings/transform/
pad.rs

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