vrl/stdlib/
format_number.rs

1use crate::compiler::prelude::*;
2use rust_decimal::{Decimal, prelude::FromPrimitive};
3use std::sync::LazyLock;
4
5static DEFAULT_DECIMAL_SEPARATOR: LazyLock<Value> =
6    LazyLock::new(|| Value::Bytes(Bytes::from(".")));
7
8static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
9    vec![
10        Parameter::required(
11            "value",
12            kind::INTEGER | kind::FLOAT,
13            "The number to format as a string.",
14        ),
15        Parameter::optional(
16            "scale",
17            kind::INTEGER,
18            "The number of decimal places to display.",
19        ),
20        Parameter::optional(
21            "decimal_separator",
22            kind::BYTES,
23            "The character to use between the whole and decimal parts of the number.",
24        )
25        .default(&DEFAULT_DECIMAL_SEPARATOR),
26        Parameter::optional(
27            "grouping_separator",
28            kind::BYTES,
29            "The character to use between each thousands part of the number.",
30        ),
31    ]
32});
33
34fn format_number(
35    value: Value,
36    scale: Option<Value>,
37    grouping_separator: Option<Value>,
38    decimal_separator: Value,
39) -> Resolved {
40    let value: Decimal = match value {
41        Value::Integer(v) => v.into(),
42        Value::Float(v) => Decimal::from_f64(*v).expect("not NaN"),
43        value => {
44            return Err(ValueError::Expected {
45                got: value.kind(),
46                expected: Kind::integer() | Kind::float(),
47            }
48            .into());
49        }
50    };
51    let scale = match scale {
52        Some(expr) => Some(expr.try_integer()?),
53        None => None,
54    };
55    let grouping_separator = match grouping_separator {
56        Some(expr) => Some(expr.try_bytes()?),
57        None => None,
58    };
59    let decimal_separator = decimal_separator.try_bytes()?;
60    // Split integral and fractional part of float.
61    let mut parts = value
62        .to_string()
63        .split('.')
64        .map(ToOwned::to_owned)
65        .collect::<Vec<String>>();
66    debug_assert!(parts.len() <= 2);
67    // Manipulate fractional part based on configuration.
68    match scale {
69        Some(0) => parts.truncate(1),
70        Some(i) => {
71            // TODO consider removal options
72            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
73            let i = i as usize;
74
75            if parts.len() == 1 {
76                parts.push(String::new());
77            }
78
79            if i > parts[1].len() {
80                for _ in 0..i - parts[1].len() {
81                    parts[1].push('0');
82                }
83            } else {
84                parts[1].truncate(i);
85            }
86        }
87        None => {}
88    }
89    // Manipulate integral part based on configuration.
90    if let Some(sep) = grouping_separator.as_deref() {
91        let sep = String::from_utf8_lossy(sep);
92        let start = parts[0].len() % 3;
93
94        let positions: Vec<usize> = parts[0]
95            .chars()
96            .skip(start)
97            .enumerate()
98            .map(|(i, _)| i)
99            .filter(|i| i % 3 == 0)
100            .collect();
101
102        for (i, pos) in positions.iter().enumerate() {
103            parts[0].insert_str(pos + (i * sep.len()) + start, &sep);
104        }
105    }
106    // Join results, using configured decimal separator.
107    Ok(parts
108        .join(&String::from_utf8_lossy(&decimal_separator[..]))
109        .into())
110}
111
112#[derive(Clone, Copy, Debug)]
113pub struct FormatNumber;
114
115impl Function for FormatNumber {
116    fn identifier(&self) -> &'static str {
117        "format_number"
118    }
119
120    fn usage(&self) -> &'static str {
121        "Formats the `value` into a string representation of the number."
122    }
123
124    fn category(&self) -> &'static str {
125        Category::Number.as_ref()
126    }
127
128    fn return_kind(&self) -> u16 {
129        kind::BYTES
130    }
131
132    fn parameters(&self) -> &'static [Parameter] {
133        PARAMETERS.as_slice()
134    }
135
136    fn compile(
137        &self,
138        _state: &state::TypeState,
139        _ctx: &mut FunctionCompileContext,
140        arguments: ArgumentList,
141    ) -> Compiled {
142        let value = arguments.required("value");
143        let scale = arguments.optional("scale");
144        let decimal_separator = arguments.optional("decimal_separator");
145        let grouping_separator = arguments.optional("grouping_separator");
146
147        Ok(FormatNumberFn {
148            value,
149            scale,
150            decimal_separator,
151            grouping_separator,
152        }
153        .as_expr())
154    }
155
156    fn examples(&self) -> &'static [Example] {
157        &[
158            example! {
159                title: "Format a number (3 decimals)",
160                source: r#"format_number(1234567.89, 3, decimal_separator: ".", grouping_separator: ",")"#,
161                result: Ok("1,234,567.890"),
162            },
163            example! {
164                title: "Format a number with European-style separators",
165                source: r#"format_number(4672.4, decimal_separator: ",", grouping_separator: "_")"#,
166                result: Ok("4_672,4"),
167            },
168            example! {
169                title: "Format a number with a middle dot separator",
170                source: r#"format_number(4321.09, 3, decimal_separator: "·")"#,
171                result: Ok("4321·090"),
172            },
173        ]
174    }
175}
176
177#[derive(Clone, Debug)]
178struct FormatNumberFn {
179    value: Box<dyn Expression>,
180    scale: Option<Box<dyn Expression>>,
181    decimal_separator: Option<Box<dyn Expression>>,
182    grouping_separator: Option<Box<dyn Expression>>,
183}
184
185impl FunctionExpression for FormatNumberFn {
186    fn resolve(&self, ctx: &mut Context) -> Resolved {
187        let value = self.value.resolve(ctx)?;
188        let scale = self
189            .scale
190            .as_ref()
191            .map(|expr| expr.resolve(ctx))
192            .transpose()?;
193        let grouping_separator = self
194            .grouping_separator
195            .as_ref()
196            .map(|expr| expr.resolve(ctx))
197            .transpose()?;
198        let decimal_separator = self
199            .decimal_separator
200            .map_resolve_with_default(ctx, || DEFAULT_DECIMAL_SEPARATOR.clone())?;
201
202        format_number(value, scale, grouping_separator, decimal_separator)
203    }
204
205    fn type_def(&self, _: &state::TypeState) -> TypeDef {
206        TypeDef::bytes().infallible()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::value;
214
215    test_function![
216        format_number => FormatNumber;
217
218        number {
219            args: func_args![value: 1234.567],
220            want: Ok(value!("1234.567")),
221            tdef: TypeDef::bytes().infallible(),
222        }
223
224        precision {
225            args: func_args![value: 1234.567,
226                             scale: 2],
227            want: Ok(value!("1234.56")),
228            tdef: TypeDef::bytes().infallible(),
229        }
230
231
232        separator {
233            args: func_args![value: 1234.567,
234                             scale: 2,
235                             decimal_separator: ","],
236            want: Ok(value!("1234,56")),
237            tdef: TypeDef::bytes().infallible(),
238        }
239
240        more_separators {
241            args: func_args![value: 1234.567,
242                             scale: 2,
243                             decimal_separator: ",",
244                             grouping_separator: " "],
245            want: Ok(value!("1 234,56")),
246            tdef: TypeDef::bytes().infallible(),
247        }
248
249        big_number {
250            args: func_args![value: 11_222_333_444.567_89,
251                             scale: 3,
252                             decimal_separator: ",",
253                             grouping_separator: "."],
254            want: Ok(value!("11.222.333.444,567")),
255            tdef: TypeDef::bytes().infallible(),
256        }
257
258        integer {
259            args: func_args![value: 100.0],
260            want: Ok(value!("100")),
261            tdef: TypeDef::bytes().infallible(),
262        }
263
264        integer_decimals {
265            args: func_args![value: 100.0,
266                             scale: 2],
267            want: Ok(value!("100.00")),
268            tdef: TypeDef::bytes().infallible(),
269        }
270
271        float_no_decimals {
272            args: func_args![value: 123.45,
273                             scale: 0],
274            want: Ok(value!("123")),
275            tdef: TypeDef::bytes().infallible(),
276        }
277
278        integer_no_decimals {
279            args: func_args![value: 12345,
280                             scale: 2],
281            want: Ok(value!("12345.00")),
282            tdef: TypeDef::bytes().infallible(),
283        }
284    ];
285}