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}