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)] fn 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 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#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] fn 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; 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}