datetime_rs_codegen/
lib.rs

1//! Macro for converting from a domain-specific interval language to nanoseconds.
2//!
3//! This crate is an implementation detail for `datetime-rs`. You should not depend on it directly,
4//! and its contents are subject to change.
5
6use std::sync::LazyLock;
7
8use proc_macro2::TokenStream;
9use proc_macro2::TokenTree;
10use quote::quote;
11use regex::Regex;
12use syn::Result;
13use syn::Token;
14use syn::parse::Parse;
15use syn::parse::ParseStream;
16
17/// Create an expression of seconds and microseconds from a domain-specific language.
18///
19/// This macro is private API that powers the `datetime::time_interval!` macro. It should not be
20/// used directly.
21pub fn nanoseconds(tokens: TokenStream) -> TokenStream {
22  let delta = match syn::parse2::<Delta>(tokens) {
23    Ok(delta) => delta,
24    Err(err) => return err.into_compile_error(),
25  };
26  let nanos = delta.nanoseconds;
27
28  // **Note:** This mechanic where we siphon off the negative sign only to reattach it in a
29  // const expression immediately below likely appears unnecessary. We're doing it because it
30  // works around a false positive error that rust-analyzer emits when using `nanoseconds!` with
31  // negative numbers. Should the issue with rust-analyzer's static analysis be fixed in the
32  // future, this otherwise-unnecessary logic can be removed.
33  match nanos < 0 {
34    true => {
35      let nanos = nanos.abs();
36      quote! { const { - #nanos }}
37    },
38    false => quote! { #nanos },
39  }
40}
41
42pub struct Delta {
43  nanoseconds: i128,
44}
45
46impl Delta {
47  /// The number of seconds in this delta.
48  pub const fn seconds(&self) -> i64 {
49    self.nanoseconds.div_euclid(1_000_000_000) as i64
50  }
51
52  /// The number of nanos in this delta.
53  pub const fn nanos(&self) -> u32 {
54    self.nanoseconds.rem_euclid(1_000_000_000) as u32
55  }
56}
57
58#[derive(Debug, Default)]
59struct Pieces {
60  days: i64,
61  hours: i64,
62  minutes: i64,
63  seconds: i64,
64  nanos: u32,
65}
66
67impl Pieces {
68  fn as_seconds(&self) -> i64 {
69    (self.days * 86_400) + (self.hours * 3_600) + (self.minutes * 60) + self.seconds
70  }
71}
72
73impl From<Pieces> for Delta {
74  fn from(p: Pieces) -> Self {
75    Self { nanoseconds: p.as_seconds() as i128 * 1_000_000_000 + p.nanos as i128 }
76  }
77}
78
79impl Parse for Delta {
80  fn parse(input: ParseStream) -> Result<Self> {
81    // Do we have an operator? Determine our multiplier.
82    let signum = match input.peek(Token![+]) || input.peek(Token![-]) {
83      true => match input.parse::<syn::BinOp>()? {
84        syn::BinOp::Add(_) => 1,
85        syn::BinOp::Sub(_) => -1,
86        _ => unreachable!("Token must be + or -."),
87      },
88      false => 1,
89    };
90
91    macro_rules! err {
92      ($span:expr, $msg:literal $(,)? $($args:expr),*) => {
93        syn::Error::new($span, format!($msg, $($args),*))
94      }
95    }
96
97    // Parse out the strings of the individual deltas.
98    let mut pieces = Pieces::default();
99    while let Ok(token) = input.parse::<TokenTree>() {
100      let delta = token.to_string();
101      let captures = (DELTA_STRING.captures(delta.as_str()))
102        .ok_or_else(|| err!(token.span(), "Invalid duration string: {delta}"))?;
103
104      // Add the individual captured components to the total seconds and nanos.
105      macro_rules! capture_piece {
106      ($captures:ident[$index:literal] $trim:literal $p:ident $tokens:ident $unit:ident) => {{
107        let secs = $captures.get($index)
108          .map(|i| i.as_str().trim_end_matches($trim))
109          .map(|s| s.parse::<i64>().map_err(|_| {
110            let err_msg = stringify!(invalid $unit);
111            err!($tokens.span(), "{err_msg}")
112          }))
113          .transpose()?
114          .unwrap_or_default();
115        let current = $p.as_seconds();
116        if current != 0 && secs > current.abs() {
117          return Err(err!($tokens.span(), "Place only larger units of time before {}.", stringify!($unit)))?;
118        }
119        if secs != 0 && $p.$unit != 0 {
120          return Err(err!($tokens.span(), "Only declare {} once.", stringify!($unit)));
121        }
122        secs
123        }};
124      }
125      pieces.days += capture_piece!(captures[1] 'd' pieces token days) * signum;
126      pieces.hours += capture_piece!(captures[2] 'h' pieces token hours) * signum;
127      pieces.minutes += capture_piece!(captures[3] 'm' pieces token minutes) * signum;
128      let (secs, nanoseconds) = captures
129        .get(4)
130        .map(|s| -> syn::Result<(i64, u32)> {
131          let split = s.as_str().trim_end_matches('s').split('.').collect::<Vec<&str>>();
132          match split.len() == 1 {
133            true => Ok((
134              split[0].parse::<i64>().map_err(|_| err!(token.span(), "invalid seconds"))? * signum,
135              0,
136            )),
137            false => {
138              if split[1].len() > 9 {
139                Err(err!(token.span(), "Offset precision greater than nanoseconds"))?;
140              }
141              let mut s = split[0].parse::<i64>().unwrap() * signum;
142              let mut n = split[1].parse::<u32>().unwrap() * 10u32.pow(9 - split[1].len() as u32);
143              // The nanos aren't signum-aware, so if this is a negative delta, invert the nanos.
144              if signum == -1 && n != 0 {
145                s -= 1;
146                n = 1_000_000_000 - n;
147              }
148              Ok((s, n))
149            },
150          }
151        })
152        .transpose()?
153        .unwrap_or_default();
154
155      // Make sure separate pieces come in the right order.
156      if pieces.as_seconds() != 0 && secs > pieces.as_seconds().abs() {
157        Err(err!(token.span(), "Place only larger units of time before seconds."))?;
158      }
159      if pieces.nanos > 0 && nanoseconds > 0 {
160        Err(err!(token.span(), "Fractional seconds may only be declared once."))?;
161      }
162
163      // Increment total seconds and nanos.
164      pieces.seconds += secs;
165      pieces.nanos += nanoseconds;
166
167      // This interval may be being used in a meta attribute alongside other key-value arguments.
168      // If we see a comma, that's a signal to stop.
169      if input.peek(Token![,]) {
170        break;
171      }
172    }
173
174    // Done; return the seconds and nanoseconds.
175    Ok(pieces.into())
176  }
177}
178
179static DELTA_STRING: LazyLock<Regex> = LazyLock::new(|| {
180  Regex::new(r"^([\d]+d)?([\d]+h)?([\d]+m)?([\d]+\.?[\d]*s)?$").expect("valid regex")
181});