vrl/stdlib/
parse_int.rs

1use crate::compiler::prelude::*;
2
3fn parse_int(value: &Value, base: Option<Value>) -> Resolved {
4    let string = value.try_bytes_utf8_lossy()?;
5    let (base, index) = match base {
6        Some(base) => {
7            let base = base.try_integer()?;
8
9            if !(2..=36).contains(&base) {
10                return Err(format!(
11                    "invalid base {value}: must be be between 2 and 36 (inclusive)"
12                )
13                .into());
14            }
15            // TODO consider removal options
16            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
17            (base as u32, 0)
18        }
19        None => match string.chars().next() {
20            Some('0') => match string.chars().nth(1) {
21                Some('b') => (2, 2),
22                Some('o') => (8, 2),
23                Some('x') => (16, 2),
24                _ => (8, 0),
25            },
26            Some(_) => (10u32, 0),
27            None => return Err("value is empty".into()),
28        },
29    };
30    let converted = i64::from_str_radix(&string[index..], base)
31        .map_err(|err| format!("could not parse integer: {err}"))?;
32
33    Ok(converted.into())
34}
35
36#[derive(Clone, Copy, Debug)]
37pub struct ParseInt;
38
39impl Function for ParseInt {
40    fn identifier(&self) -> &'static str {
41        "parse_int"
42    }
43
44    fn usage(&self) -> &'static str {
45        "Parses the string `value` representing a number in an optional base/radix to an integer."
46    }
47
48    fn category(&self) -> &'static str {
49        Category::Parse.as_ref()
50    }
51
52    fn internal_failure_reasons(&self) -> &'static [&'static str] {
53        &[
54            "The base is not between 2 and 36.",
55            "The number cannot be parsed in the base.",
56        ]
57    }
58
59    fn return_kind(&self) -> u16 {
60        kind::INTEGER
61    }
62
63    fn parameters(&self) -> &'static [Parameter] {
64        const PARAMETERS: &[Parameter] = &[
65            Parameter::required("value", kind::BYTES, "The string to parse."),
66            Parameter::optional(
67                "base",
68                kind::INTEGER,
69                "The base the number is in. Must be between 2 and 36 (inclusive).
70
71If unspecified, the string prefix is used to
72determine the base: \"0b\", 8 for \"0\" or \"0o\", 16 for \"0x\",
73and 10 otherwise.",
74            ),
75        ];
76        PARAMETERS
77    }
78
79    fn examples(&self) -> &'static [Example] {
80        &[
81            example! {
82                title: "Parse decimal",
83                source: r#"parse_int!("-42")"#,
84                result: Ok("-42"),
85            },
86            example! {
87                title: "Parse binary",
88                source: r#"parse_int!("0b1001")"#,
89                result: Ok("9"),
90            },
91            example! {
92                title: "Parse octal",
93                source: r#"parse_int!("0o42")"#,
94                result: Ok("34"),
95            },
96            example! {
97                title: "Parse hexadecimal",
98                source: r#"parse_int!("0x2a")"#,
99                result: Ok("42"),
100            },
101            example! {
102                title: "Parse explicit base",
103                source: r#"parse_int!("2a", 17)"#,
104                result: Ok("44"),
105            },
106        ]
107    }
108
109    fn compile(
110        &self,
111        _state: &state::TypeState,
112        _ctx: &mut FunctionCompileContext,
113        arguments: ArgumentList,
114    ) -> Compiled {
115        let value = arguments.required("value");
116        let base = arguments.optional("base");
117
118        Ok(ParseIntFn { value, base }.as_expr())
119    }
120}
121
122#[derive(Debug, Clone)]
123struct ParseIntFn {
124    value: Box<dyn Expression>,
125    base: Option<Box<dyn Expression>>,
126}
127
128impl FunctionExpression for ParseIntFn {
129    fn resolve(&self, ctx: &mut Context) -> Resolved {
130        let value = self.value.resolve(ctx)?;
131        let base = self
132            .base
133            .as_ref()
134            .map(|expr| expr.resolve(ctx))
135            .transpose()?;
136
137        parse_int(&value, base)
138    }
139
140    fn type_def(&self, _: &state::TypeState) -> TypeDef {
141        TypeDef::integer().fallible()
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    test_function![
150        parse_int => ParseInt;
151
152        decimal {
153            args: func_args![value: "-42"],
154            want: Ok(-42),
155            tdef: TypeDef::integer().fallible(),
156        }
157
158        binary {
159            args: func_args![value: "0b1001"],
160            want: Ok(9),
161            tdef: TypeDef::integer().fallible(),
162        }
163
164        octal {
165            args: func_args![value: "042"],
166            want: Ok(34),
167            tdef: TypeDef::integer().fallible(),
168        }
169
170        hexadecimal {
171            args: func_args![value: "0x2a"],
172            want: Ok(42),
173            tdef: TypeDef::integer().fallible(),
174        }
175
176        zero {
177            args: func_args![value: "0"],
178            want: Ok(0),
179            tdef: TypeDef::integer().fallible(),
180        }
181
182        explicit_hexadecimal {
183            args: func_args![value: "2a", base: 16],
184            want: Ok(42),
185            tdef: TypeDef::integer().fallible(),
186        }
187    ];
188}