vrl/stdlib/
format_int.rs

1use std::collections::VecDeque;
2
3use crate::compiler::prelude::*;
4use std::sync::LazyLock;
5
6static DEFAULT_BASE: LazyLock<Value> = LazyLock::new(|| Value::Integer(10));
7
8static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
9    vec![
10        Parameter::required("value", kind::INTEGER, "The number to format."),
11        Parameter::optional(
12            "base",
13            kind::INTEGER,
14            "The base to format the number in. Must be between 2 and 36 (inclusive).",
15        )
16        .default(&DEFAULT_BASE),
17    ]
18});
19
20#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // TODO consider removal options
21fn format_int(value: Value, base: Value) -> Resolved {
22    let value = value.try_integer()?;
23    let base = base.try_integer()?;
24    if !(2..=36).contains(&base) {
25        return Err(format!("invalid base {base}: must be be between 2 and 36 (inclusive)").into());
26    }
27
28    let converted = format_radix(value, base as u32);
29    Ok(converted.into())
30}
31
32#[derive(Clone, Copy, Debug)]
33pub struct FormatInt;
34
35impl Function for FormatInt {
36    fn identifier(&self) -> &'static str {
37        "format_int"
38    }
39
40    fn usage(&self) -> &'static str {
41        "Formats the integer `value` into a string representation using the given base/radix."
42    }
43
44    fn category(&self) -> &'static str {
45        Category::Number.as_ref()
46    }
47
48    fn internal_failure_reasons(&self) -> &'static [&'static str] {
49        &["The base is not between 2 and 36."]
50    }
51
52    fn return_kind(&self) -> u16 {
53        kind::BYTES
54    }
55
56    fn parameters(&self) -> &'static [Parameter] {
57        PARAMETERS.as_slice()
58    }
59
60    fn compile(
61        &self,
62        _state: &state::TypeState,
63        _ctx: &mut FunctionCompileContext,
64        arguments: ArgumentList,
65    ) -> Compiled {
66        let value = arguments.required("value");
67        let base = arguments.optional("base");
68
69        Ok(FormatIntFn { value, base }.as_expr())
70    }
71
72    fn examples(&self) -> &'static [Example] {
73        &[
74            example! {
75                title: "Format as a hexadecimal integer",
76                source: "format_int!(42, 16)",
77                result: Ok("2a"),
78            },
79            example! {
80                title: "Format as a negative hexadecimal integer",
81                source: "format_int!(-42, 16)",
82                result: Ok("-2a"),
83            },
84            example! {
85                title: "Format as a decimal integer (default base)",
86                source: "format_int!(42)",
87                // extra "s are needed to avoid being read as an integer by tests
88                result: Ok("\"42\""),
89            },
90        ]
91    }
92}
93
94#[derive(Clone, Debug)]
95struct FormatIntFn {
96    value: Box<dyn Expression>,
97    base: Option<Box<dyn Expression>>,
98}
99
100impl FunctionExpression for FormatIntFn {
101    fn resolve(&self, ctx: &mut Context) -> Resolved {
102        let value = self.value.resolve(ctx)?;
103        let base = self
104            .base
105            .map_resolve_with_default(ctx, || DEFAULT_BASE.clone())?;
106
107        format_int(value, base)
108    }
109
110    fn type_def(&self, _: &state::TypeState) -> TypeDef {
111        TypeDef::bytes().fallible()
112    }
113}
114
115// Formats x in the provided radix
116//
117// Panics if radix is < 2 or > 36
118#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // TODO consider removal options
119fn format_radix(x: i64, radix: u32) -> String {
120    let mut result: VecDeque<char> = VecDeque::new();
121
122    let (mut x, negative) = if x < 0 {
123        (-x as u64, true)
124    } else {
125        (x as u64, false)
126    };
127
128    loop {
129        let m = (x % u64::from(radix)) as u32; // max of 35
130        x /= u64::from(radix);
131
132        result.push_front(std::char::from_digit(m, radix).unwrap());
133        if x == 0 {
134            break;
135        }
136    }
137
138    if negative {
139        result.push_front('-');
140    }
141
142    result.into_iter().collect()
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::value;
149    test_function![
150        format_int => FormatInt;
151
152        decimal {
153            args: func_args![value: 42],
154            want: Ok(value!("42")),
155            tdef: TypeDef::bytes().fallible(),
156        }
157
158        hexidecimal {
159            args: func_args![value: 42, base: 16],
160            want: Ok(value!("2a")),
161            tdef: TypeDef::bytes().fallible(),
162        }
163
164        negative_hexidecimal {
165            args: func_args![value: -42, base: 16],
166            want: Ok(value!("-2a")),
167            tdef: TypeDef::bytes().fallible(),
168        }
169    ];
170}