typst_library/foundations/
datetime.rs

1use std::cmp::Ordering;
2use std::hash::Hash;
3use std::ops::{Add, Sub};
4
5use ecow::{eco_format, EcoString, EcoVec};
6use time::error::{Format, InvalidFormatDescription};
7use time::macros::format_description;
8use time::{format_description, Month, PrimitiveDateTime};
9
10use crate::diag::{bail, StrResult};
11use crate::engine::Engine;
12use crate::foundations::{
13    cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value,
14};
15use crate::World;
16
17/// Represents a date, a time, or a combination of both.
18///
19/// Can be created by either specifying a custom datetime using this type's
20/// constructor function or getting the current date with
21/// [`datetime.today`]($datetime.today).
22///
23/// # Example
24/// ```example
25/// #let date = datetime(
26///   year: 2020,
27///   month: 10,
28///   day: 4,
29/// )
30///
31/// #date.display() \
32/// #date.display(
33///   "y:[year repr:last_two]"
34/// )
35///
36/// #let time = datetime(
37///   hour: 18,
38///   minute: 2,
39///   second: 23,
40/// )
41///
42/// #time.display() \
43/// #time.display(
44///   "h:[hour repr:12][period]"
45/// )
46/// ```
47///
48/// # Datetime and Duration
49/// You can get a [duration] by subtracting two datetime:
50/// ```example
51/// #let first-of-march = datetime(day: 1, month: 3, year: 2024)
52/// #let first-of-jan = datetime(day: 1, month: 1, year: 2024)
53/// #let distance = first-of-march - first-of-jan
54/// #distance.hours()
55/// ```
56///
57/// You can also add/subtract a datetime and a duration to retrieve a new,
58/// offset datetime:
59/// ```example
60/// #let date = datetime(day: 1, month: 3, year: 2024)
61/// #let two-days = duration(days: 2)
62/// #let two-days-earlier = date - two-days
63/// #let two-days-later = date + two-days
64///
65/// #date.display() \
66/// #two-days-earlier.display() \
67/// #two-days-later.display()
68/// ```
69///
70/// # Format
71/// You can specify a customized formatting using the
72/// [`display`]($datetime.display) method. The format of a datetime is
73/// specified by providing _components_ with a specified number of _modifiers_.
74/// A component represents a certain part of the datetime that you want to
75/// display, and with the help of modifiers you can define how you want to
76/// display that component. In order to display a component, you wrap the name
77/// of the component in square brackets (e.g. `[[year]]` will display the year).
78/// In order to add modifiers, you add a space after the component name followed
79/// by the name of the modifier, a colon and the value of the modifier (e.g.
80/// `[[month repr:short]]` will display the short representation of the month).
81///
82/// The possible combination of components and their respective modifiers is as
83/// follows:
84///
85/// - `year`: Displays the year of the datetime.
86///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
87///     year is padded.
88///   - `repr` Can be either `full` in which case the full year is displayed or
89///     `last_two` in which case only the last two digits are displayed.
90///   - `sign`: Can be either `automatic` or `mandatory`. Specifies when the
91///     sign should be displayed.
92/// - `month`: Displays the month of the datetime.
93///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
94///     month is padded.
95///   - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the
96///     month should be displayed as a number or a word. Unfortunately, when
97///     choosing the word representation, it can currently only display the
98///     English version. In the future, it is planned to support localization.
99/// - `day`: Displays the day of the datetime.
100///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
101///     day is padded.
102/// - `week_number`: Displays the week number of the datetime.
103///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
104///     week number is padded.
105///   - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`,
106///      week numbers are between 1 and 53, while the other ones are between 0
107///      and 53.
108/// - `weekday`: Displays the weekday of the date.
109///   - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case
110///     of `long` and `short`, the corresponding English name will be displayed
111///     (same as for the month, other languages are currently not supported). In
112///     the case of `sunday` and `monday`, the numerical value will be displayed
113///     (assuming Sunday and Monday as the first day of the week, respectively).
114///   - `one_indexed`: Can be either `true` or `false`. Defines whether the
115///     numerical representation of the week starts with 0 or 1.
116/// - `hour`: Displays the hour of the date.
117///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
118///     hour is padded.
119///   - `repr`: Can be either `24` or `12`. Changes whether the hour is
120///     displayed in the 24-hour or 12-hour format.
121/// - `period`: The AM/PM part of the hour
122///   - `case`: Can be `lower` to display it in lower case and `upper` to
123///     display it in upper case.
124/// - `minute`: Displays the minute of the date.
125///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
126///     minute is padded.
127/// - `second`: Displays the second of the date.
128///   - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
129///     second is padded.
130///
131/// Keep in mind that not always all components can be used. For example, if you
132/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it
133/// will be stored as a plain date internally, meaning that you cannot use
134/// components such as `hour` or `minute`, which would only work on datetimes
135/// that have a specified time.
136#[ty(scope, cast)]
137#[derive(Debug, Clone, Copy, PartialEq, Hash)]
138pub enum Datetime {
139    /// Representation as a date.
140    Date(time::Date),
141    /// Representation as a time.
142    Time(time::Time),
143    /// Representation as a combination of date and time.
144    Datetime(time::PrimitiveDateTime),
145}
146
147impl Datetime {
148    /// Create a datetime from year, month, and day.
149    pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
150        Some(Datetime::Date(
151            time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
152                .ok()?,
153        ))
154    }
155
156    /// Create a datetime from hour, minute, and second.
157    pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
158        Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
159    }
160
161    /// Create a datetime from day and time.
162    pub fn from_ymd_hms(
163        year: i32,
164        month: u8,
165        day: u8,
166        hour: u8,
167        minute: u8,
168        second: u8,
169    ) -> Option<Self> {
170        let date =
171            time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
172                .ok()?;
173        let time = time::Time::from_hms(hour, minute, second).ok()?;
174        Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
175    }
176
177    /// Try to parse a dictionary as a TOML date.
178    pub fn from_toml_dict(dict: &Dict) -> Option<Self> {
179        if dict.len() != 1 {
180            return None;
181        }
182
183        let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else {
184            return None;
185        };
186
187        if let Ok(d) = time::PrimitiveDateTime::parse(
188            string,
189            &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"),
190        ) {
191            Self::from_ymd_hms(
192                d.year(),
193                d.month() as u8,
194                d.day(),
195                d.hour(),
196                d.minute(),
197                d.second(),
198            )
199        } else if let Ok(d) = time::PrimitiveDateTime::parse(
200            string,
201            &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
202        ) {
203            Self::from_ymd_hms(
204                d.year(),
205                d.month() as u8,
206                d.day(),
207                d.hour(),
208                d.minute(),
209                d.second(),
210            )
211        } else if let Ok(d) =
212            time::Date::parse(string, &format_description!("[year]-[month]-[day]"))
213        {
214            Self::from_ymd(d.year(), d.month() as u8, d.day())
215        } else if let Ok(d) =
216            time::Time::parse(string, &format_description!("[hour]:[minute]:[second]"))
217        {
218            Self::from_hms(d.hour(), d.minute(), d.second())
219        } else {
220            None
221        }
222    }
223
224    /// Which kind of variant this datetime stores.
225    pub fn kind(&self) -> &'static str {
226        match self {
227            Datetime::Datetime(_) => "datetime",
228            Datetime::Date(_) => "date",
229            Datetime::Time(_) => "time",
230        }
231    }
232}
233
234#[scope]
235impl Datetime {
236    /// Creates a new datetime.
237    ///
238    /// You can specify the [datetime] using a year, month, day, hour, minute,
239    /// and second.
240    ///
241    /// _Note_: Depending on which components of the datetime you specify, Typst
242    /// will store it in one of the following three ways:
243    /// * If you specify year, month and day, Typst will store just a date.
244    /// * If you specify hour, minute and second, Typst will store just a time.
245    /// * If you specify all of year, month, day, hour, minute and second, Typst
246    ///   will store a full datetime.
247    ///
248    /// Depending on how it is stored, the [`display`]($datetime.display) method
249    /// will choose a different formatting by default.
250    ///
251    /// ```example
252    /// #datetime(
253    ///   year: 2012,
254    ///   month: 8,
255    ///   day: 3,
256    /// ).display()
257    /// ```
258    #[func(constructor)]
259    pub fn construct(
260        /// The year of the datetime.
261        #[named]
262        year: Option<i32>,
263        /// The month of the datetime.
264        #[named]
265        month: Option<Month>,
266        /// The day of the datetime.
267        #[named]
268        day: Option<u8>,
269        /// The hour of the datetime.
270        #[named]
271        hour: Option<u8>,
272        /// The minute of the datetime.
273        #[named]
274        minute: Option<u8>,
275        /// The second of the datetime.
276        #[named]
277        second: Option<u8>,
278    ) -> StrResult<Datetime> {
279        let time = match (hour, minute, second) {
280            (Some(hour), Some(minute), Some(second)) => {
281                match time::Time::from_hms(hour, minute, second) {
282                    Ok(time) => Some(time),
283                    Err(_) => bail!("time is invalid"),
284                }
285            }
286            (None, None, None) => None,
287            _ => bail!("time is incomplete"),
288        };
289
290        let date = match (year, month, day) {
291            (Some(year), Some(month), Some(day)) => {
292                match time::Date::from_calendar_date(year, month, day) {
293                    Ok(date) => Some(date),
294                    Err(_) => bail!("date is invalid"),
295                }
296            }
297            (None, None, None) => None,
298            _ => bail!("date is incomplete"),
299        };
300
301        Ok(match (date, time) {
302            (Some(date), Some(time)) => {
303                Datetime::Datetime(PrimitiveDateTime::new(date, time))
304            }
305            (Some(date), None) => Datetime::Date(date),
306            (None, Some(time)) => Datetime::Time(time),
307            (None, None) => {
308                bail!("at least one of date or time must be fully specified")
309            }
310        })
311    }
312
313    /// Returns the current date.
314    ///
315    /// ```example
316    /// Today's date is
317    /// #datetime.today().display().
318    /// ```
319    #[func]
320    pub fn today(
321        engine: &mut Engine,
322        /// An offset to apply to the current UTC date. If set to `{auto}`, the
323        /// offset will be the local offset.
324        #[named]
325        #[default]
326        offset: Smart<i64>,
327    ) -> StrResult<Datetime> {
328        Ok(engine
329            .world
330            .today(offset.custom())
331            .ok_or("unable to get the current date")?)
332    }
333
334    /// Displays the datetime in a specified format.
335    ///
336    /// Depending on whether you have defined just a date, a time or both, the
337    /// default format will be different. If you specified a date, it will be
338    /// `[[year]-[month]-[day]]`. If you specified a time, it will be
339    /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be
340    /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`.
341    ///
342    /// See the [format syntax]($datetime/#format) for more information.
343    #[func]
344    pub fn display(
345        &self,
346        /// The format used to display the datetime.
347        #[default]
348        pattern: Smart<DisplayPattern>,
349    ) -> StrResult<EcoString> {
350        let pat = |s| format_description::parse_borrowed::<2>(s).unwrap();
351        let result = match pattern {
352            Smart::Auto => match self {
353                Self::Date(date) => date.format(&pat("[year]-[month]-[day]")),
354                Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")),
355                Self::Datetime(datetime) => {
356                    datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]"))
357                }
358            },
359
360            Smart::Custom(DisplayPattern(_, format)) => match self {
361                Self::Date(date) => date.format(&format),
362                Self::Time(time) => time.format(&format),
363                Self::Datetime(datetime) => datetime.format(&format),
364            },
365        };
366        result.map(EcoString::from).map_err(format_time_format_error)
367    }
368
369    /// The year if it was specified, or `{none}` for times without a date.
370    #[func]
371    pub fn year(&self) -> Option<i32> {
372        match self {
373            Self::Date(date) => Some(date.year()),
374            Self::Time(_) => None,
375            Self::Datetime(datetime) => Some(datetime.year()),
376        }
377    }
378
379    /// The month if it was specified, or `{none}` for times without a date.
380    #[func]
381    pub fn month(&self) -> Option<u8> {
382        match self {
383            Self::Date(date) => Some(date.month().into()),
384            Self::Time(_) => None,
385            Self::Datetime(datetime) => Some(datetime.month().into()),
386        }
387    }
388
389    /// The weekday (counting Monday as 1) or `{none}` for times without a date.
390    #[func]
391    pub fn weekday(&self) -> Option<u8> {
392        match self {
393            Self::Date(date) => Some(date.weekday().number_from_monday()),
394            Self::Time(_) => None,
395            Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
396        }
397    }
398
399    /// The day if it was specified, or `{none}` for times without a date.
400    #[func]
401    pub fn day(&self) -> Option<u8> {
402        match self {
403            Self::Date(date) => Some(date.day()),
404            Self::Time(_) => None,
405            Self::Datetime(datetime) => Some(datetime.day()),
406        }
407    }
408
409    /// The hour if it was specified, or `{none}` for dates without a time.
410    #[func]
411    pub fn hour(&self) -> Option<u8> {
412        match self {
413            Self::Date(_) => None,
414            Self::Time(time) => Some(time.hour()),
415            Self::Datetime(datetime) => Some(datetime.hour()),
416        }
417    }
418
419    /// The minute if it was specified, or `{none}` for dates without a time.
420    #[func]
421    pub fn minute(&self) -> Option<u8> {
422        match self {
423            Self::Date(_) => None,
424            Self::Time(time) => Some(time.minute()),
425            Self::Datetime(datetime) => Some(datetime.minute()),
426        }
427    }
428
429    /// The second if it was specified, or `{none}` for dates without a time.
430    #[func]
431    pub fn second(&self) -> Option<u8> {
432        match self {
433            Self::Date(_) => None,
434            Self::Time(time) => Some(time.second()),
435            Self::Datetime(datetime) => Some(datetime.second()),
436        }
437    }
438
439    /// The ordinal (day of the year), or `{none}` for times without a date.
440    #[func]
441    pub fn ordinal(&self) -> Option<u16> {
442        match self {
443            Self::Datetime(datetime) => Some(datetime.ordinal()),
444            Self::Date(date) => Some(date.ordinal()),
445            Self::Time(_) => None,
446        }
447    }
448}
449
450impl Repr for Datetime {
451    fn repr(&self) -> EcoString {
452        let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr()));
453        let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr()));
454        let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr()));
455        let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr()));
456        let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr()));
457        let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr()));
458        let filtered = [year, month, day, hour, minute, second]
459            .into_iter()
460            .flatten()
461            .collect::<EcoVec<_>>();
462
463        eco_format!("datetime{}", &repr::pretty_array_like(&filtered, false))
464    }
465}
466
467impl PartialOrd for Datetime {
468    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
469        match (self, other) {
470            (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b),
471            (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
472            (Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
473            _ => None,
474        }
475    }
476}
477
478impl Add<Duration> for Datetime {
479    type Output = Self;
480
481    fn add(self, rhs: Duration) -> Self::Output {
482        let rhs: time::Duration = rhs.into();
483        match self {
484            Self::Datetime(datetime) => Self::Datetime(datetime + rhs),
485            Self::Date(date) => Self::Date(date + rhs),
486            Self::Time(time) => Self::Time(time + rhs),
487        }
488    }
489}
490
491impl Sub<Duration> for Datetime {
492    type Output = Self;
493
494    fn sub(self, rhs: Duration) -> Self::Output {
495        let rhs: time::Duration = rhs.into();
496        match self {
497            Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
498            Self::Date(date) => Self::Date(date - rhs),
499            Self::Time(time) => Self::Time(time - rhs),
500        }
501    }
502}
503
504impl Sub for Datetime {
505    type Output = StrResult<Duration>;
506
507    fn sub(self, rhs: Self) -> Self::Output {
508        match (self, rhs) {
509            (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()),
510            (Self::Date(a), Self::Date(b)) => Ok((a - b).into()),
511            (Self::Time(a), Self::Time(b)) => Ok((a - b).into()),
512            (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
513        }
514    }
515}
516
517/// A format in which a datetime can be displayed.
518pub struct DisplayPattern(Str, format_description::OwnedFormatItem);
519
520cast! {
521    DisplayPattern,
522    self => self.0.into_value(),
523    v: Str => {
524        let item = format_description::parse_owned::<2>(&v)
525            .map_err(format_time_invalid_format_description_error)?;
526        Self(v, item)
527    }
528}
529
530cast! {
531    Month,
532    v: u8 => Self::try_from(v).map_err(|_| "month is invalid")?
533}
534
535/// Format the `Format` error of the time crate in an appropriate way.
536fn format_time_format_error(error: Format) -> EcoString {
537    match error {
538        Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name),
539        Format::InsufficientTypeInformation { .. } => {
540            "failed to format datetime (insufficient information)".into()
541        }
542        err => eco_format!("failed to format datetime in the requested format ({err})"),
543    }
544}
545
546/// Format the `InvalidFormatDescription` error of the time crate in an
547/// appropriate way.
548fn format_time_invalid_format_description_error(
549    error: InvalidFormatDescription,
550) -> EcoString {
551    match error {
552        InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => {
553            eco_format!("missing closing bracket for bracket at index {}", index)
554        }
555        InvalidFormatDescription::InvalidComponentName { name, index, .. } => {
556            eco_format!("invalid component name '{}' at index {}", name, index)
557        }
558        InvalidFormatDescription::InvalidModifier { value, index, .. } => {
559            eco_format!("invalid modifier '{}' at index {}", value, index)
560        }
561        InvalidFormatDescription::Expected { what, index, .. } => {
562            eco_format!("expected {} at index {}", what, index)
563        }
564        InvalidFormatDescription::MissingComponentName { index, .. } => {
565            eco_format!("expected component name at index {}", index)
566        }
567        InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => {
568            eco_format!(
569                "missing required modifier {} for component at index {}",
570                name,
571                index
572            )
573        }
574        InvalidFormatDescription::NotSupported { context, what, index, .. } => {
575            eco_format!("{} is not supported in {} at index {}", what, context, index)
576        }
577        err => eco_format!("failed to parse datetime format ({err})"),
578    }
579}