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 /// ```example
315 /// Today's date is
316 /// #datetime.today().display().
317 /// ```
318 #[func]
319 pub fn today(
320 engine: &mut Engine,
321 /// An offset to apply to the current UTC date. If set to `{auto}`, the
322 /// offset will be the local offset.
323 #[named]
324 #[default]
325 offset: Smart<i64>,
326 ) -> StrResult<Datetime> {
327 Ok(engine
328 .world
329 .today(offset.custom())
330 .ok_or("unable to get the current date")?)
331 }
332
333 /// Displays the datetime in a specified format.
334 ///
335 /// Depending on whether you have defined just a date, a time or both, the
336 /// default format will be different. If you specified a date, it will be
337 /// `[[year]-[month]-[day]]`. If you specified a time, it will be
338 /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be
339 /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`.
340 ///
341 /// See the [format syntax]($datetime/#format) for more information.
342 #[func]
343 pub fn display(
344 &self,
345 /// The format used to display the datetime.
346 #[default]
347 pattern: Smart<DisplayPattern>,
348 ) -> StrResult<EcoString> {
349 let pat = |s| format_description::parse_borrowed::<2>(s).unwrap();
350 let result = match pattern {
351 Smart::Auto => match self {
352 Self::Date(date) => date.format(&pat("[year]-[month]-[day]")),
353 Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")),
354 Self::Datetime(datetime) => {
355 datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]"))
356 }
357 },
358
359 Smart::Custom(DisplayPattern(_, format)) => match self {
360 Self::Date(date) => date.format(&format),
361 Self::Time(time) => time.format(&format),
362 Self::Datetime(datetime) => datetime.format(&format),
363 },
364 };
365 result.map(EcoString::from).map_err(format_time_format_error)
366 }
367
368 /// The year if it was specified, or `{none}` for times without a date.
369 #[func]
370 pub fn year(&self) -> Option<i32> {
371 match self {
372 Self::Date(date) => Some(date.year()),
373 Self::Time(_) => None,
374 Self::Datetime(datetime) => Some(datetime.year()),
375 }
376 }
377
378 /// The month if it was specified, or `{none}` for times without a date.
379 #[func]
380 pub fn month(&self) -> Option<u8> {
381 match self {
382 Self::Date(date) => Some(date.month().into()),
383 Self::Time(_) => None,
384 Self::Datetime(datetime) => Some(datetime.month().into()),
385 }
386 }
387
388 /// The weekday (counting Monday as 1) or `{none}` for times without a date.
389 #[func]
390 pub fn weekday(&self) -> Option<u8> {
391 match self {
392 Self::Date(date) => Some(date.weekday().number_from_monday()),
393 Self::Time(_) => None,
394 Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
395 }
396 }
397
398 /// The day if it was specified, or `{none}` for times without a date.
399 #[func]
400 pub fn day(&self) -> Option<u8> {
401 match self {
402 Self::Date(date) => Some(date.day()),
403 Self::Time(_) => None,
404 Self::Datetime(datetime) => Some(datetime.day()),
405 }
406 }
407
408 /// The hour if it was specified, or `{none}` for dates without a time.
409 #[func]
410 pub fn hour(&self) -> Option<u8> {
411 match self {
412 Self::Date(_) => None,
413 Self::Time(time) => Some(time.hour()),
414 Self::Datetime(datetime) => Some(datetime.hour()),
415 }
416 }
417
418 /// The minute if it was specified, or `{none}` for dates without a time.
419 #[func]
420 pub fn minute(&self) -> Option<u8> {
421 match self {
422 Self::Date(_) => None,
423 Self::Time(time) => Some(time.minute()),
424 Self::Datetime(datetime) => Some(datetime.minute()),
425 }
426 }
427
428 /// The second if it was specified, or `{none}` for dates without a time.
429 #[func]
430 pub fn second(&self) -> Option<u8> {
431 match self {
432 Self::Date(_) => None,
433 Self::Time(time) => Some(time.second()),
434 Self::Datetime(datetime) => Some(datetime.second()),
435 }
436 }
437
438 /// The ordinal (day of the year), or `{none}` for times without a date.
439 #[func]
440 pub fn ordinal(&self) -> Option<u16> {
441 match self {
442 Self::Datetime(datetime) => Some(datetime.ordinal()),
443 Self::Date(date) => Some(date.ordinal()),
444 Self::Time(_) => None,
445 }
446 }
447}
448
449impl Repr for Datetime {
450 fn repr(&self) -> EcoString {
451 let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr()));
452 let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr()));
453 let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr()));
454 let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr()));
455 let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr()));
456 let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr()));
457 let filtered = [year, month, day, hour, minute, second]
458 .into_iter()
459 .flatten()
460 .collect::<EcoVec<_>>();
461
462 eco_format!("datetime{}", &repr::pretty_array_like(&filtered, false))
463 }
464}
465
466impl PartialOrd for Datetime {
467 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
468 match (self, other) {
469 (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b),
470 (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
471 (Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
472 _ => None,
473 }
474 }
475}
476
477impl Add<Duration> for Datetime {
478 type Output = Self;
479
480 fn add(self, rhs: Duration) -> Self::Output {
481 let rhs: time::Duration = rhs.into();
482 match self {
483 Self::Datetime(datetime) => Self::Datetime(datetime + rhs),
484 Self::Date(date) => Self::Date(date + rhs),
485 Self::Time(time) => Self::Time(time + rhs),
486 }
487 }
488}
489
490impl Sub<Duration> for Datetime {
491 type Output = Self;
492
493 fn sub(self, rhs: Duration) -> Self::Output {
494 let rhs: time::Duration = rhs.into();
495 match self {
496 Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
497 Self::Date(date) => Self::Date(date - rhs),
498 Self::Time(time) => Self::Time(time - rhs),
499 }
500 }
501}
502
503impl Sub for Datetime {
504 type Output = StrResult<Duration>;
505
506 fn sub(self, rhs: Self) -> Self::Output {
507 match (self, rhs) {
508 (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()),
509 (Self::Date(a), Self::Date(b)) => Ok((a - b).into()),
510 (Self::Time(a), Self::Time(b)) => Ok((a - b).into()),
511 (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
512 }
513 }
514}
515
516/// A format in which a datetime can be displayed.
517pub struct DisplayPattern(Str, format_description::OwnedFormatItem);
518
519cast! {
520 DisplayPattern,
521 self => self.0.into_value(),
522 v: Str => {
523 let item = format_description::parse_owned::<2>(&v)
524 .map_err(format_time_invalid_format_description_error)?;
525 Self(v, item)
526 }
527}
528
529cast! {
530 Month,
531 v: u8 => Self::try_from(v).map_err(|_| "month is invalid")?
532}
533
534/// Format the `Format` error of the time crate in an appropriate way.
535fn format_time_format_error(error: Format) -> EcoString {
536 match error {
537 Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name),
538 Format::InsufficientTypeInformation { .. } => {
539 "failed to format datetime (insufficient information)".into()
540 }
541 err => eco_format!("failed to format datetime in the requested format ({err})"),
542 }
543}
544
545/// Format the `InvalidFormatDescription` error of the time crate in an
546/// appropriate way.
547fn format_time_invalid_format_description_error(
548 error: InvalidFormatDescription,
549) -> EcoString {
550 match error {
551 InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => {
552 eco_format!("missing closing bracket for bracket at index {}", index)
553 }
554 InvalidFormatDescription::InvalidComponentName { name, index, .. } => {
555 eco_format!("invalid component name '{}' at index {}", name, index)
556 }
557 InvalidFormatDescription::InvalidModifier { value, index, .. } => {
558 eco_format!("invalid modifier '{}' at index {}", value, index)
559 }
560 InvalidFormatDescription::Expected { what, index, .. } => {
561 eco_format!("expected {} at index {}", what, index)
562 }
563 InvalidFormatDescription::MissingComponentName { index, .. } => {
564 eco_format!("expected component name at index {}", index)
565 }
566 InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => {
567 eco_format!(
568 "missing required modifier {} for component at index {}",
569 name,
570 index
571 )
572 }
573 InvalidFormatDescription::NotSupported { context, what, index, .. } => {
574 eco_format!("{} is not supported in {} at index {}", what, context, index)
575 }
576 err => eco_format!("failed to parse datetime format ({err})"),
577 }
578}