Skip to main content

typst_kit/
datetime.rs

1//! Date and time manipulation.
2//!
3//! In particular, this module provides the necessary building pieces for
4//! [`World::today`](typst_library::World::today).
5
6#![cfg(feature = "datetime")]
7
8use std::sync::OnceLock;
9
10use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveTime, Utc};
11use chrono::{NaiveDate, NaiveDateTime};
12
13use typst_library::diag::{StrResult, bail};
14use typst_library::foundations::{Datetime, Duration};
15
16/// The current date and time.
17pub struct Time(TimeInner);
18
19/// The internal representation of a [`Time`].
20enum TimeInner {
21    /// A fixed date and time.
22    Fixed(DateTime<Utc>),
23    /// The current date and time if the time is not externally fixed.
24    System(OnceLock<DateTime<Utc>>),
25}
26
27impl Time {
28    /// Use a predefined fixed date and time to provide the current date. Used
29    /// for reproducible builds.
30    ///
31    /// Returns an error if `datetime` is only a time.
32    pub fn fixed(datetime: Datetime) -> StrResult<Self> {
33        let date = match datetime {
34            Datetime::Date(d) => d,
35            Datetime::Datetime(dt) => dt.date(),
36            _ => bail!("fixed datetime must specify a date"),
37        };
38
39        Ok(Time(TimeInner::Fixed(DateTime::from_naive_utc_and_offset(
40            NaiveDateTime::new(
41                NaiveDate::from_ymd_opt(
42                    date.year(),
43                    date.month() as u32,
44                    date.day() as u32,
45                )
46                .ok_or("provided fixed date is invalid")?,
47                NaiveTime::from_hms_opt(
48                    datetime.hour().unwrap_or(0) as u32,
49                    datetime.minute().unwrap_or(0) as u32,
50                    datetime.second().unwrap_or(0) as u32,
51                )
52                .ok_or("provided fixed time is invalid")?,
53            ),
54            Utc,
55        ))))
56    }
57
58    /// Use a fixed timestamp to provide the current date. Used for reproducible
59    /// builds.
60    ///
61    /// This timestamp is usually provided using the `SOURCE_DATE_EPOCH`
62    /// environment variable.
63    ///
64    /// Returns an error if the timestamp is out of range.
65    pub fn fixed_timestamp(timestamp: i64) -> StrResult<Self> {
66        Ok(Time(TimeInner::Fixed(
67            DateTime::from_timestamp(timestamp, 0).ok_or("timestamp is out of range")?,
68        )))
69    }
70
71    /// Rely on the system to provide the current date.
72    pub fn system() -> Self {
73        Time(TimeInner::System(OnceLock::new()))
74    }
75
76    /// The current date.
77    ///
78    /// A timezone offset can be given to obtain the current date in this
79    /// timezone.
80    ///
81    /// This can directly be used to implement
82    /// [`World::today`](typst_library::World::today).
83    pub fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
84        let now = match &self.0 {
85            TimeInner::Fixed(time) => time.fixed_offset(),
86            TimeInner::System(time) => {
87                let now_utc = time.get_or_init(Utc::now);
88                if offset.is_some() {
89                    // Actual offset will be applied later.
90                    now_utc.fixed_offset()
91                } else {
92                    now_utc.with_timezone(&Local).fixed_offset()
93                }
94            }
95        };
96
97        // The time with the specified UTC offset.
98        let with_offset = match offset {
99            None => now,
100            Some(offset) => {
101                let seconds = offset.seconds().trunc();
102                // Check whether we can convert seconds from f64 to i32
103                if !seconds.is_finite()
104                    || seconds < f64::from(i32::MIN)
105                    || seconds > f64::from(i32::MAX)
106                {
107                    return None;
108                }
109                now.with_timezone(&FixedOffset::east_opt(seconds as i32)?)
110            }
111        };
112
113        Datetime::from_ymd(
114            with_offset.year(),
115            with_offset.month().try_into().ok()?,
116            with_offset.day().try_into().ok()?,
117        )
118    }
119
120    /// If not a fixed time, resets the memoized time fetched from the system.
121    ///
122    /// It will be fetched again the next time [`today`](Self::today) is called.
123    /// This is usually called in between compilations.
124    pub fn reset(&mut self) {
125        if let TimeInner::System(ref mut time_lock) = self.0 {
126            time_lock.take();
127        }
128    }
129}