summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/foundations/datetime.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/foundations/datetime.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/foundations/datetime.rs')
-rw-r--r--crates/typst-library/src/foundations/datetime.rs580
1 files changed, 580 insertions, 0 deletions
diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs
new file mode 100644
index 00000000..d15cd417
--- /dev/null
+++ b/crates/typst-library/src/foundations/datetime.rs
@@ -0,0 +1,580 @@
+use std::cmp::Ordering;
+use std::hash::Hash;
+use std::ops::{Add, Sub};
+
+use ecow::{eco_format, EcoString, EcoVec};
+use time::error::{Format, InvalidFormatDescription};
+use time::macros::format_description;
+use time::{format_description, Month, PrimitiveDateTime};
+
+use crate::diag::{bail, StrResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value,
+};
+use crate::World;
+
+/// Represents a date, a time, or a combination of both.
+///
+/// Can be created by either specifying a custom datetime using this type's
+/// constructor function or getting the current date with
+/// [`datetime.today`]($datetime.today).
+///
+/// # Example
+/// ```example
+/// #let date = datetime(
+/// year: 2020,
+/// month: 10,
+/// day: 4,
+/// )
+///
+/// #date.display() \
+/// #date.display(
+/// "y:[year repr:last_two]"
+/// )
+///
+/// #let time = datetime(
+/// hour: 18,
+/// minute: 2,
+/// second: 23,
+/// )
+///
+/// #time.display() \
+/// #time.display(
+/// "h:[hour repr:12][period]"
+/// )
+/// ```
+///
+/// # Datetime and Duration
+/// You can get a [duration] by subtracting two datetime:
+/// ```example
+/// #let first-of-march = datetime(day: 1, month: 3, year: 2024)
+/// #let first-of-jan = datetime(day: 1, month: 1, year: 2024)
+/// #let distance = first-of-march - first-of-jan
+/// #distance.hours()
+/// ```
+///
+/// You can also add/subtract a datetime and a duration to retrieve a new,
+/// offset datetime:
+/// ```example
+/// #let date = datetime(day: 1, month: 3, year: 2024)
+/// #let two-days = duration(days: 2)
+/// #let two-days-earlier = date - two-days
+/// #let two-days-later = date + two-days
+///
+/// #date.display() \
+/// #two-days-earlier.display() \
+/// #two-days-later.display()
+/// ```
+///
+/// # Format
+/// You can specify a customized formatting using the
+/// [`display`]($datetime.display) method. The format of a datetime is
+/// specified by providing _components_ with a specified number of _modifiers_.
+/// A component represents a certain part of the datetime that you want to
+/// display, and with the help of modifiers you can define how you want to
+/// display that component. In order to display a component, you wrap the name
+/// of the component in square brackets (e.g. `[[year]]` will display the year).
+/// In order to add modifiers, you add a space after the component name followed
+/// by the name of the modifier, a colon and the value of the modifier (e.g.
+/// `[[month repr:short]]` will display the short representation of the month).
+///
+/// The possible combination of components and their respective modifiers is as
+/// follows:
+///
+/// - `year`: Displays the year of the datetime.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// year is padded.
+/// - `repr` Can be either `full` in which case the full year is displayed or
+/// `last_two` in which case only the last two digits are displayed.
+/// - `sign`: Can be either `automatic` or `mandatory`. Specifies when the
+/// sign should be displayed.
+/// - `month`: Displays the month of the datetime.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// month is padded.
+/// - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the
+/// month should be displayed as a number or a word. Unfortunately, when
+/// choosing the word representation, it can currently only display the
+/// English version. In the future, it is planned to support localization.
+/// - `day`: Displays the day of the datetime.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// day is padded.
+/// - `week_number`: Displays the week number of the datetime.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// week number is padded.
+/// - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`,
+/// week numbers are between 1 and 53, while the other ones are between 0
+/// and 53.
+/// - `weekday`: Displays the weekday of the date.
+/// - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case
+/// of `long` and `short`, the corresponding English name will be displayed
+/// (same as for the month, other languages are currently not supported). In
+/// the case of `sunday` and `monday`, the numerical value will be displayed
+/// (assuming Sunday and Monday as the first day of the week, respectively).
+/// - `one_indexed`: Can be either `true` or `false`. Defines whether the
+/// numerical representation of the week starts with 0 or 1.
+/// - `hour`: Displays the hour of the date.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// hour is padded.
+/// - `repr`: Can be either `24` or `12`. Changes whether the hour is
+/// displayed in the 24-hour or 12-hour format.
+/// - `period`: The AM/PM part of the hour
+/// - `case`: Can be `lower` to display it in lower case and `upper` to
+/// display it in upper case.
+/// - `minute`: Displays the minute of the date.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// minute is padded.
+/// - `second`: Displays the second of the date.
+/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
+/// second is padded.
+///
+/// Keep in mind that not always all components can be used. For example, if you
+/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it
+/// will be stored as a plain date internally, meaning that you cannot use
+/// components such as `hour` or `minute`, which would only work on datetimes
+/// that have a specified time.
+#[ty(scope, cast)]
+#[derive(Debug, Clone, Copy, PartialEq, Hash)]
+pub enum Datetime {
+ /// Representation as a date.
+ Date(time::Date),
+ /// Representation as a time.
+ Time(time::Time),
+ /// Representation as a combination of date and time.
+ Datetime(time::PrimitiveDateTime),
+}
+
+impl Datetime {
+ /// Create a datetime from year, month, and day.
+ pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
+ Some(Datetime::Date(
+ time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
+ .ok()?,
+ ))
+ }
+
+ /// Create a datetime from hour, minute, and second.
+ pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
+ Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
+ }
+
+ /// Create a datetime from day and time.
+ pub fn from_ymd_hms(
+ year: i32,
+ month: u8,
+ day: u8,
+ hour: u8,
+ minute: u8,
+ second: u8,
+ ) -> Option<Self> {
+ let date =
+ time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
+ .ok()?;
+ let time = time::Time::from_hms(hour, minute, second).ok()?;
+ Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
+ }
+
+ /// Try to parse a dictionary as a TOML date.
+ pub fn from_toml_dict(dict: &Dict) -> Option<Self> {
+ if dict.len() != 1 {
+ return None;
+ }
+
+ let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else {
+ return None;
+ };
+
+ if let Ok(d) = time::PrimitiveDateTime::parse(
+ string,
+ &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"),
+ ) {
+ Self::from_ymd_hms(
+ d.year(),
+ d.month() as u8,
+ d.day(),
+ d.hour(),
+ d.minute(),
+ d.second(),
+ )
+ } else if let Ok(d) = time::PrimitiveDateTime::parse(
+ string,
+ &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
+ ) {
+ Self::from_ymd_hms(
+ d.year(),
+ d.month() as u8,
+ d.day(),
+ d.hour(),
+ d.minute(),
+ d.second(),
+ )
+ } else if let Ok(d) =
+ time::Date::parse(string, &format_description!("[year]-[month]-[day]"))
+ {
+ Self::from_ymd(d.year(), d.month() as u8, d.day())
+ } else if let Ok(d) =
+ time::Time::parse(string, &format_description!("[hour]:[minute]:[second]"))
+ {
+ Self::from_hms(d.hour(), d.minute(), d.second())
+ } else {
+ None
+ }
+ }
+
+ /// Which kind of variant this datetime stores.
+ pub fn kind(&self) -> &'static str {
+ match self {
+ Datetime::Datetime(_) => "datetime",
+ Datetime::Date(_) => "date",
+ Datetime::Time(_) => "time",
+ }
+ }
+}
+
+#[scope]
+impl Datetime {
+ /// Creates a new datetime.
+ ///
+ /// You can specify the [datetime] using a year, month, day, hour, minute,
+ /// and second.
+ ///
+ /// _Note_: Depending on which components of the datetime you specify, Typst
+ /// will store it in one of the following three ways:
+ /// * If you specify year, month and day, Typst will store just a date.
+ /// * If you specify hour, minute and second, Typst will store just a time.
+ /// * If you specify all of year, month, day, hour, minute and second, Typst
+ /// will store a full datetime.
+ ///
+ /// Depending on how it is stored, the [`display`]($datetime.display) method
+ /// will choose a different formatting by default.
+ ///
+ /// ```example
+ /// #datetime(
+ /// year: 2012,
+ /// month: 8,
+ /// day: 3,
+ /// ).display()
+ /// ```
+ #[func(constructor)]
+ pub fn construct(
+ /// The year of the datetime.
+ #[named]
+ year: Option<i32>,
+ /// The month of the datetime.
+ #[named]
+ month: Option<Month>,
+ /// The day of the datetime.
+ #[named]
+ day: Option<u8>,
+ /// The hour of the datetime.
+ #[named]
+ hour: Option<u8>,
+ /// The minute of the datetime.
+ #[named]
+ minute: Option<u8>,
+ /// The second of the datetime.
+ #[named]
+ second: Option<u8>,
+ ) -> StrResult<Datetime> {
+ let time = match (hour, minute, second) {
+ (Some(hour), Some(minute), Some(second)) => {
+ match time::Time::from_hms(hour, minute, second) {
+ Ok(time) => Some(time),
+ Err(_) => bail!("time is invalid"),
+ }
+ }
+ (None, None, None) => None,
+ _ => bail!("time is incomplete"),
+ };
+
+ let date = match (year, month, day) {
+ (Some(year), Some(month), Some(day)) => {
+ match time::Date::from_calendar_date(year, month, day) {
+ Ok(date) => Some(date),
+ Err(_) => bail!("date is invalid"),
+ }
+ }
+ (None, None, None) => None,
+ _ => bail!("date is incomplete"),
+ };
+
+ Ok(match (date, time) {
+ (Some(date), Some(time)) => {
+ Datetime::Datetime(PrimitiveDateTime::new(date, time))
+ }
+ (Some(date), None) => Datetime::Date(date),
+ (None, Some(time)) => Datetime::Time(time),
+ (None, None) => {
+ bail!("at least one of date or time must be fully specified")
+ }
+ })
+ }
+
+ /// Returns the current date.
+ ///
+ /// ```example
+ /// Today's date is
+ /// #datetime.today().display().
+ /// ```
+ #[func]
+ pub fn today(
+ /// The engine.
+ engine: &mut Engine,
+ /// An offset to apply to the current UTC date. If set to `{auto}`, the
+ /// offset will be the local offset.
+ #[named]
+ #[default]
+ offset: Smart<i64>,
+ ) -> StrResult<Datetime> {
+ Ok(engine
+ .world
+ .today(offset.custom())
+ .ok_or("unable to get the current date")?)
+ }
+
+ /// Displays the datetime in a specified format.
+ ///
+ /// Depending on whether you have defined just a date, a time or both, the
+ /// default format will be different. If you specified a date, it will be
+ /// `[[year]-[month]-[day]]`. If you specified a time, it will be
+ /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be
+ /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`.
+ ///
+ /// See the [format syntax]($datetime/#format) for more information.
+ #[func]
+ pub fn display(
+ &self,
+ /// The format used to display the datetime.
+ #[default]
+ pattern: Smart<DisplayPattern>,
+ ) -> StrResult<EcoString> {
+ let pat = |s| format_description::parse_borrowed::<2>(s).unwrap();
+ let result = match pattern {
+ Smart::Auto => match self {
+ Self::Date(date) => date.format(&pat("[year]-[month]-[day]")),
+ Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")),
+ Self::Datetime(datetime) => {
+ datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]"))
+ }
+ },
+
+ Smart::Custom(DisplayPattern(_, format)) => match self {
+ Self::Date(date) => date.format(&format),
+ Self::Time(time) => time.format(&format),
+ Self::Datetime(datetime) => datetime.format(&format),
+ },
+ };
+ result.map(EcoString::from).map_err(format_time_format_error)
+ }
+
+ /// The year if it was specified, or `{none}` for times without a date.
+ #[func]
+ pub fn year(&self) -> Option<i32> {
+ match self {
+ Self::Date(date) => Some(date.year()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.year()),
+ }
+ }
+
+ /// The month if it was specified, or `{none}` for times without a date.
+ #[func]
+ pub fn month(&self) -> Option<u8> {
+ match self {
+ Self::Date(date) => Some(date.month().into()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.month().into()),
+ }
+ }
+
+ /// The weekday (counting Monday as 1) or `{none}` for times without a date.
+ #[func]
+ pub fn weekday(&self) -> Option<u8> {
+ match self {
+ Self::Date(date) => Some(date.weekday().number_from_monday()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
+ }
+ }
+
+ /// The day if it was specified, or `{none}` for times without a date.
+ #[func]
+ pub fn day(&self) -> Option<u8> {
+ match self {
+ Self::Date(date) => Some(date.day()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.day()),
+ }
+ }
+
+ /// The hour if it was specified, or `{none}` for dates without a time.
+ #[func]
+ pub fn hour(&self) -> Option<u8> {
+ match self {
+ Self::Date(_) => None,
+ Self::Time(time) => Some(time.hour()),
+ Self::Datetime(datetime) => Some(datetime.hour()),
+ }
+ }
+
+ /// The minute if it was specified, or `{none}` for dates without a time.
+ #[func]
+ pub fn minute(&self) -> Option<u8> {
+ match self {
+ Self::Date(_) => None,
+ Self::Time(time) => Some(time.minute()),
+ Self::Datetime(datetime) => Some(datetime.minute()),
+ }
+ }
+
+ /// The second if it was specified, or `{none}` for dates without a time.
+ #[func]
+ pub fn second(&self) -> Option<u8> {
+ match self {
+ Self::Date(_) => None,
+ Self::Time(time) => Some(time.second()),
+ Self::Datetime(datetime) => Some(datetime.second()),
+ }
+ }
+
+ /// The ordinal (day of the year), or `{none}` for times without a date.
+ #[func]
+ pub fn ordinal(&self) -> Option<u16> {
+ match self {
+ Self::Datetime(datetime) => Some(datetime.ordinal()),
+ Self::Date(date) => Some(date.ordinal()),
+ Self::Time(_) => None,
+ }
+ }
+}
+
+impl Repr for Datetime {
+ fn repr(&self) -> EcoString {
+ let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr()));
+ let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr()));
+ let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr()));
+ let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr()));
+ let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr()));
+ let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr()));
+ let filtered = [year, month, day, hour, minute, second]
+ .into_iter()
+ .flatten()
+ .collect::<EcoVec<_>>();
+
+ eco_format!("datetime{}", &repr::pretty_array_like(&filtered, false))
+ }
+}
+
+impl PartialOrd for Datetime {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ match (self, other) {
+ (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b),
+ (Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
+ (Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
+ _ => None,
+ }
+ }
+}
+
+impl Add<Duration> for Datetime {
+ type Output = Self;
+
+ fn add(self, rhs: Duration) -> Self::Output {
+ let rhs: time::Duration = rhs.into();
+ match self {
+ Self::Datetime(datetime) => Self::Datetime(datetime + rhs),
+ Self::Date(date) => Self::Date(date + rhs),
+ Self::Time(time) => Self::Time(time + rhs),
+ }
+ }
+}
+
+impl Sub<Duration> for Datetime {
+ type Output = Self;
+
+ fn sub(self, rhs: Duration) -> Self::Output {
+ let rhs: time::Duration = rhs.into();
+ match self {
+ Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
+ Self::Date(date) => Self::Date(date - rhs),
+ Self::Time(time) => Self::Time(time - rhs),
+ }
+ }
+}
+
+impl Sub for Datetime {
+ type Output = StrResult<Duration>;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ match (self, rhs) {
+ (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()),
+ (Self::Date(a), Self::Date(b)) => Ok((a - b).into()),
+ (Self::Time(a), Self::Time(b)) => Ok((a - b).into()),
+ (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
+ }
+ }
+}
+
+/// A format in which a datetime can be displayed.
+pub struct DisplayPattern(Str, format_description::OwnedFormatItem);
+
+cast! {
+ DisplayPattern,
+ self => self.0.into_value(),
+ v: Str => {
+ let item = format_description::parse_owned::<2>(&v)
+ .map_err(format_time_invalid_format_description_error)?;
+ Self(v, item)
+ }
+}
+
+cast! {
+ Month,
+ v: u8 => Self::try_from(v).map_err(|_| "month is invalid")?
+}
+
+/// Format the `Format` error of the time crate in an appropriate way.
+fn format_time_format_error(error: Format) -> EcoString {
+ match error {
+ Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name),
+ Format::InsufficientTypeInformation { .. } => {
+ "failed to format datetime (insufficient information)".into()
+ }
+ err => eco_format!("failed to format datetime in the requested format ({err})"),
+ }
+}
+
+/// Format the `InvalidFormatDescription` error of the time crate in an
+/// appropriate way.
+fn format_time_invalid_format_description_error(
+ error: InvalidFormatDescription,
+) -> EcoString {
+ match error {
+ InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => {
+ eco_format!("missing closing bracket for bracket at index {}", index)
+ }
+ InvalidFormatDescription::InvalidComponentName { name, index, .. } => {
+ eco_format!("invalid component name '{}' at index {}", name, index)
+ }
+ InvalidFormatDescription::InvalidModifier { value, index, .. } => {
+ eco_format!("invalid modifier '{}' at index {}", value, index)
+ }
+ InvalidFormatDescription::Expected { what, index, .. } => {
+ eco_format!("expected {} at index {}", what, index)
+ }
+ InvalidFormatDescription::MissingComponentName { index, .. } => {
+ eco_format!("expected component name at index {}", index)
+ }
+ InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => {
+ eco_format!(
+ "missing required modifier {} for component at index {}",
+ name,
+ index
+ )
+ }
+ InvalidFormatDescription::NotSupported { context, what, index, .. } => {
+ eco_format!("{} is not supported in {} at index {}", what, context, index)
+ }
+ err => eco_format!("failed to parse datetime format ({err})"),
+ }
+}