time-macros-impl 0.1.2

Procedural macros for the time crate.
Documentation
use crate::ext::LitIntExtension;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
    parse::{Parse, ParseStream},
    LitFloat, LitInt, Result, Token,
};

#[derive(PartialEq)]
enum AmPm {
    Am,
    Pm,
}

impl Parse for AmPm {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        use crate::kw::{am, pm, AM, PM};
        if input.peek(am) {
            input.parse::<am>()?;
            Ok(AmPm::Am)
        } else if input.peek(AM) {
            input.parse::<AM>()?;
            Ok(AmPm::Am)
        } else if input.peek(pm) {
            input.parse::<pm>()?;
            Ok(AmPm::Pm)
        } else if input.peek(PM) {
            input.parse::<PM>()?;
            Ok(AmPm::Pm)
        } else {
            error!("expected am or pm")
        }
    }
}

pub(crate) struct Time {
    pub(crate) hour: LitInt,
    pub(crate) minute: LitInt,
    pub(crate) second: LitInt,
    pub(crate) nanosecond: LitInt,
}

impl Parse for Time {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let mut hour = input.parse::<LitInt>()?;
        input.parse::<Token![:]>()?;
        let minute = input.parse::<LitInt>()?;

        // Seconds and nanoseconds are optional, defaulting to zero.
        let (second, nanosecond) = if input.peek(Token![:]) {
            input.parse::<Token![:]>()?;

            if input.peek(LitFloat) {
                let float = input.parse::<LitFloat>()?;

                // Temporary value to satisfy the compiler.
                let float_str = float.to_string();
                let parts: Vec<_> = float_str.splitn(2, '.').collect();

                let seconds = LitInt::new(parts[0], float.span());
                let nanoseconds = {
                    // Prepend a zero to avoid having an empty string.
                    // Strip the suffix to avoid syn thinking it's a float.
                    let raw_padded = format!("0{}", parts[1].trim_end_matches(float.suffix()));

                    // Take an extra digit due to the padding.
                    let digits: String = raw_padded
                        .chars()
                        .filter(char::is_ascii_digit)
                        .take(10)
                        .collect();

                    // Scale the value based on how many digits were provided.
                    let value = LitInt::new(&digits, float.span()).base10_parse::<usize>()?
                        * 10_usize.pow(10 - digits.len() as u32);

                    LitInt::create(value).with_span(float.span())
                };

                (seconds, nanoseconds)
            } else {
                (input.parse::<LitInt>()?, LitInt::create(0))
            }
        } else {
            (LitInt::create(0), LitInt::create(0))
        };

        let am_pm = input.parse::<AmPm>().ok();

        // Ensure none of the components are out of range.
        match am_pm {
            Some(am_pm) => {
                hour.ensure_in_range(1..=12)?;
                // Adjust the hour if necessary.
                hour = match (hour.value()?, am_pm) {
                    (12, AmPm::Am) => LitInt::create(0).with_span(hour.span()),
                    (value, AmPm::Pm) if value != 12 => {
                        LitInt::create(value + 12).with_span(hour.span())
                    }
                    _ => hour,
                }
            }
            None => hour.ensure_in_range(0..24)?,
        }
        minute.ensure_in_range(0..60)?;
        second.ensure_in_range(0..60)?;
        // This likely isn't necessary, but it can't hurt.
        nanosecond.ensure_in_range(0..1_000_000_000)?;

        Ok(Self {
            hour,
            minute,
            second,
            nanosecond,
        })
    }
}

impl ToTokens for Time {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let Self {
            hour,
            minute,
            second,
            nanosecond,
        } = self;

        tokens.extend(quote! {
            ::time::internals::Time::from_hms_nanos_unchecked(#hour, #minute, #second, #nanosecond)
        });
    }
}