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