Skip to main content

time_macros/
date.rs

1use std::iter::Peekable;
2
3use num_conv::Truncate;
4use proc_macro::token_stream;
5use time_core::util::{days_in_year, weeks_in_year};
6
7use crate::Error;
8use crate::helpers::{consume_number, consume_punct, days_in_year_month, ymd_to_yo, ywd_to_yo};
9use crate::to_tokens::ToTokenStream;
10
11pub(crate) const MIN_YEAR: i32 = -MAX_YEAR;
12
13#[cfg(feature = "large-dates")]
14pub(crate) const MAX_YEAR: i32 = 999_999;
15#[cfg(not(feature = "large-dates"))]
16pub(crate) const MAX_YEAR: i32 = 9_999;
17
18/// A parsed date.
19///
20/// The year must be in `MIN_YEAR..=MAX_YEAR`, and the ordinal must be in `1..=days_in_year(year)`.
21/// Both ranges are required by the unchecked `Date` constructor used when emitting this value.
22pub(crate) struct Date {
23    pub(crate) year: i32,
24    pub(crate) ordinal: u16,
25}
26
27pub(crate) fn parse(chars: &mut Peekable<token_stream::IntoIter>) -> Result<Date, Error> {
28    let (year_sign_span, year_sign, explicit_sign) = if let Ok(span) = consume_punct('-', chars) {
29        (Some(span), -1, true)
30    } else if let Ok(span) = consume_punct('+', chars) {
31        (Some(span), 1, true)
32    } else {
33        (None, 1, false)
34    };
35    let (year_span, mut year) = consume_number::<i32>("year", chars)?;
36    year *= year_sign;
37    if year.abs() > MAX_YEAR {
38        return Err(Error::InvalidComponent {
39            name: "year",
40            value: year.to_string(),
41            span_start: Some(year_sign_span.unwrap_or_else(|| year_span.start())),
42            span_end: Some(year_span.end()),
43        });
44    }
45    if !explicit_sign && year.abs() >= 10_000 {
46        return Err(Error::Custom {
47            message: "years with more than four digits must have an explicit sign".into(),
48            span_start: Some(year_sign_span.unwrap_or_else(|| year_span.start())),
49            span_end: Some(year_span.end()),
50        });
51    }
52
53    consume_punct('-', chars)?;
54
55    // year-week-day
56    if let Some(proc_macro::TokenTree::Ident(ident)) = chars.peek()
57        && let s = ident.to_string()
58        && s.starts_with('W')
59    {
60        let w_span = ident.span();
61        drop(chars.next()); // consume 'W' and possibly the week number
62
63        let (week_span, week, day_span, day);
64
65        if s.len() == 1 {
66            (week_span, week) = consume_number::<u8>("week", chars)?;
67            consume_punct('-', chars)?;
68            (day_span, day) = consume_number::<u8>("day", chars)?;
69        } else {
70            let presumptive_week = &s[1..];
71            if presumptive_week.bytes().all(|d| d.is_ascii_digit())
72                && let Ok(week_number) = presumptive_week.replace('_', "").parse()
73            {
74                (week_span, week) = (w_span, week_number);
75                consume_punct('-', chars)?;
76                (day_span, day) = consume_number::<u8>("day", chars)?;
77            } else {
78                return Err(Error::InvalidComponent {
79                    name: "week",
80                    value: presumptive_week.to_string(),
81                    span_start: Some(w_span.start()),
82                    span_end: Some(w_span.end()),
83                });
84            }
85        };
86
87        if week > weeks_in_year(year) {
88            return Err(Error::InvalidComponent {
89                name: "week",
90                value: week.to_string(),
91                span_start: Some(w_span.start()),
92                span_end: Some(week_span.end()),
93            });
94        }
95        if day == 0 || day > 7 {
96            return Err(Error::InvalidComponent {
97                name: "day",
98                value: day.to_string(),
99                span_start: Some(day_span.start()),
100                span_end: Some(day_span.end()),
101            });
102        }
103
104        let (year, ordinal) = ywd_to_yo(year, week, day, day_span)?;
105
106        return Ok(Date { year, ordinal });
107    }
108
109    // We don't yet know whether it's year-month-day or year-ordinal.
110    let (month_or_ordinal_span, month_or_ordinal) =
111        consume_number::<u16>("month or ordinal", chars)?;
112
113    // year-month-day
114    if consume_punct('-', chars).is_ok() {
115        let (month_span, month) = (month_or_ordinal_span, month_or_ordinal);
116        let (day_span, day) = consume_number::<u8>("day", chars)?;
117
118        if month == 0 || month > 12 {
119            return Err(Error::InvalidComponent {
120                name: "month",
121                value: month.to_string(),
122                span_start: Some(month_span.start()),
123                span_end: Some(month_span.end()),
124            });
125        }
126        let month = month.truncate();
127        if day == 0 || day > days_in_year_month(year, month) {
128            return Err(Error::InvalidComponent {
129                name: "day",
130                value: day.to_string(),
131                span_start: Some(day_span.start()),
132                span_end: Some(day_span.end()),
133            });
134        }
135
136        let (year, ordinal) = ymd_to_yo(year, month, day);
137
138        Ok(Date { year, ordinal })
139    }
140    // year-ordinal
141    else {
142        let (ordinal_span, ordinal) = (month_or_ordinal_span, month_or_ordinal);
143
144        if ordinal == 0 || ordinal > days_in_year(year) {
145            return Err(Error::InvalidComponent {
146                name: "ordinal",
147                value: ordinal.to_string(),
148                span_start: Some(ordinal_span.start()),
149                span_end: Some(ordinal_span.end()),
150            });
151        }
152
153        Ok(Date { year, ordinal })
154    }
155}
156
157impl ToTokenStream for Date {
158    fn into_token_stream(self) -> proc_macro::TokenStream {
159        // Safety: The parser checks both ranges before constructing `Date`. ISO week conversion
160        // also rejects dates beyond the maximum supported year.
161        quote! {
162            unsafe {
163                ::time::Date::__from_ordinal_date_unchecked(
164                    #(self.year),
165                    #(self.ordinal),
166                )
167            }
168        }
169    }
170}