rust_decimal_macros/
lib.rs

1//!
2//! A helpful macro for instantiating `Decimal` numbers.
3//!
4//! By default, this requires `rust_decimal` to be available at the project root. e.g. the macro
5//! will effectively produce:
6//!
7//! ```ignore
8//! ::rust_decimal::Decimal::from_parts(12345, 0, 0, false, 4)
9//! ```
10//!
11//! While this is convenient for most use cases, it is sometimes not desired behavior when looking
12//! to reexport the library. Consequently, this behavior can be modified by enabling the feature
13//! `reexportable`. When this feature is enabled, the macro will instead reproduce the functional
14//! equivalent of:
15//!
16//! ```ignore
17//! Decimal::from_parts(12345, 0, 0, false, 4)
18//! ```
19//!
20//! # Examples
21//!
22//! ```rust
23//! use rust_decimal_macros::dec;
24//!
25//! // If the reexportable feature is enabled, `Decimal` needs to be in scope
26//! #[cfg(feature = "reexportable")]
27//! use rust_decimal::Decimal;
28//!
29//! let number = dec!(1.2345);
30//! assert_eq!("1.2345", number.to_string());
31//! let number = dec!(-5.4321);
32//! assert_eq!("-5.4321", number.to_string());
33//! ```
34//!
35
36mod str;
37
38use proc_macro::TokenStream;
39use quote::quote;
40use syn::{
41    parse::{Parse, ParseStream},
42    parse_macro_input, Expr, Ident, LitInt, Result, Token,
43};
44
45/// Transform a literal number directly to a `Decimal` at compile time.
46///
47/// Any Rust number format works, for example:
48///
49/// - `dec!(1)`, `dec!(-1)`, `dec!(1_999)`, `dec!(- 1_999)`
50/// - `dec!(0b1)`, `dec!(-0b1_1111)`, `dec!(0o1)`, `dec!(-0o1_777)`, `dec!(0x1)`, `dec!(-0x1_Ffff)`
51/// - `dec!(1.)`, `dec!(-1.111_009)`, `dec!(1e6)`, `dec!(-1.2e+6)`, `dec!(12e-6)`, `dec!(-1.2e-6)`
52///
53/// ### Option `radix`
54///
55/// You can give it integers (not float-like) in any radix from 2 to 36 inclusive, using the letters too:
56/// `dec!(100, radix 2) == 4`, `dec!(-1_222, radix 3) == -53`, `dec!(z1, radix 36) == 1261`,
57/// `dec!(-1_xyz, radix 36) == -90683`
58///
59/// ### Option `exp`
60///
61/// This is the same as the `e` 10's exponent in float syntax (except as a Rust expression it doesn't accept
62/// a unary `+`.) You need this for other radixes. Currently, it must be between -28 and +28 inclusive:
63/// `dec!(10, radix 2, exp 5) == 200_000`, `dec!( -1_777, exp -3, radix 8) == dec!(-1.023)`
64///
65/// # Example
66///
67/// ```rust
68/// use rust_decimal_macros::dec;
69///
70/// // If the reexportable feature is enabled, `Decimal` needs to be in scope
71/// #[cfg(feature = "reexportable")]
72/// use rust_decimal::Decimal;
73///
74/// let number = dec!(1.2345);
75/// assert_eq!("1.2345", number.to_string());
76/// let number = dec!(-5.4321);
77/// assert_eq!("-5.4321", number.to_string());
78/// let number = dec!(-0o1_777);
79/// assert_eq!("-1023", number.to_string());
80/// let number = dec!(-1_777, radix 8);
81/// assert_eq!("-1023", number.to_string());
82/// ```
83///
84#[proc_macro]
85pub fn dec(input: TokenStream) -> TokenStream {
86    // Parse the input using our custom parser
87    let dec_input = parse_macro_input!(input as DecInputParser);
88    let value = dec_input.value.as_str();
89
90    // Process the parsed input
91    let result = if let Some(radix) = dec_input.radix {
92        str::parse_decimal_with_radix(value, dec_input.exp.unwrap_or_default(), radix)
93    } else {
94        str::parse_decimal(value, dec_input.exp.unwrap_or_default())
95    };
96
97    let unpacked = match result {
98        Ok(d) => d,
99        Err(e) => panic!("{}", e),
100    };
101
102    expand(
103        unpacked.lo(),
104        unpacked.mid(),
105        unpacked.hi(),
106        unpacked.negative(),
107        unpacked.scale,
108    )
109}
110
111#[cfg(not(feature = "reexportable"))]
112fn expand(lo: u32, mid: u32, hi: u32, negative: bool, scale: u32) -> TokenStream {
113    let expanded = quote! {
114        ::rust_decimal::Decimal::from_parts(#lo, #mid, #hi, #negative, #scale)
115    };
116    expanded.into()
117}
118
119#[cfg(feature = "reexportable")]
120fn expand(lo: u32, mid: u32, hi: u32, negative: bool, scale: u32) -> TokenStream {
121    let expanded = quote! {
122        Decimal::from_parts(#lo, #mid, #hi, #negative, #scale)
123    };
124    expanded.into()
125}
126
127/// Custom parser for the dec! macro input
128struct DecInputParser {
129    radix: Option<u32>,
130    exp: Option<i32>,
131    value: String,
132}
133
134impl Parse for DecInputParser {
135    fn parse(input: ParseStream) -> Result<Self> {
136        let mut radix = None;
137        let mut exp = None;
138        let mut value = None;
139
140        // Parse the first item which could be a value or a parameter
141        if !input.is_empty() {
142            // Try to parse as an identifier (parameter name)
143            if input.peek(Ident) {
144                let ident = input.parse::<Ident>()?;
145                let ident_str = ident.to_string();
146
147                match ident_str.as_str() {
148                    "radix" => {
149                        if let Some(value) = parse_radix(input)? {
150                            radix = Some(value);
151                        }
152                    }
153                    "exp" => {
154                        if let Some(value) = parse_exp(input)? {
155                            exp = Some(value);
156                        }
157                    }
158                    _ => {
159                        // This is not a parameter but a value
160                        value = Some(ident_str);
161                    }
162                }
163            } else {
164                // It's a value
165                let expr = input.parse::<Expr>()?;
166                value = Some(quote!(#expr).to_string());
167            }
168        }
169
170        // Parse the remaining tokens
171        while !input.is_empty() {
172            // We expect a comma between tokens
173            if input.peek(Token![,]) {
174                let _ = input.parse::<Token![,]>()?;
175            }
176
177            // Check if we're at the end
178            if input.is_empty() {
179                break;
180            }
181
182            // Parse the next token
183            if input.peek(Ident) {
184                let ident = input.parse::<Ident>()?;
185                let ident_str = ident.to_string();
186
187                match ident_str.as_str() {
188                    "radix" => {
189                        if radix.is_some() {
190                            panic!("Duplicate radix parameter");
191                        }
192                        if let Some(value) = parse_radix(input)? {
193                            radix = Some(value);
194                        }
195                    }
196                    "exp" => {
197                        if exp.is_some() {
198                            panic!("Duplicate exp parameter");
199                        }
200                        if let Some(value) = parse_exp(input)? {
201                            exp = Some(value);
202                        }
203                    }
204                    _ => {
205                        // This is not a parameter but a value
206                        if value.is_none() {
207                            value = Some(ident_str);
208                        } else {
209                            panic!("Unknown parameter or duplicate value: {}", ident_str);
210                        }
211                    }
212                }
213            } else {
214                // Parse as an expression (value)
215                if value.is_none() {
216                    let expr = input.parse::<Expr>()?;
217                    value = Some(quote!(#expr).to_string());
218                } else {
219                    panic!("Duplicate value found");
220                }
221            }
222        }
223
224        // Ensure we have a value
225        let value = value.unwrap_or_else(|| panic!("Expected a decimal value"));
226
227        Ok(DecInputParser { radix, exp, value })
228    }
229}
230
231fn parse_radix(input: ParseStream) -> Result<Option<u32>> {
232    // Parse the value after the parameter name
233    if input.peek(LitInt) {
234        let lit_int = input.parse::<LitInt>()?;
235        return Ok(Some(lit_int.base10_parse::<u32>()?));
236    }
237    let expr = input.parse::<Expr>()?;
238    match expr {
239        Expr::Lit(lit) => {
240            if let syn::Lit::Int(lit_int) = lit.lit {
241                return Ok(Some(lit_int.base10_parse::<u32>()?));
242            }
243        }
244        _ => panic!("Expected a literal integer for radix"),
245    }
246
247    Ok(None)
248}
249
250fn parse_exp(input: ParseStream) -> Result<Option<i32>> {
251    // Parse the value after the parameter name
252    if input.peek(LitInt) {
253        let lit_int = input.parse::<LitInt>()?;
254        return Ok(Some(lit_int.base10_parse::<i32>()?));
255    }
256    let expr = input.parse::<Expr>()?;
257    match expr {
258        Expr::Lit(lit) => {
259            if let syn::Lit::Int(lit_int) = lit.lit {
260                return Ok(Some(lit_int.base10_parse::<i32>()?));
261            }
262        }
263        Expr::Unary(unary) => {
264            if let Expr::Lit(lit) = *unary.expr {
265                if let syn::Lit::Int(lit_int) = lit.lit {
266                    let mut val = lit_int.base10_parse::<i32>()?;
267                    if let syn::UnOp::Neg(_) = unary.op {
268                        val = -val;
269                    }
270                    return Ok(Some(val));
271                }
272            }
273        }
274        _ => panic!("Expected a literal integer for exp"),
275    }
276    Ok(None)
277}