typst_library/foundations/
datetime.rs

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