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}