summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-library/src/compute/construct.rs50
-rw-r--r--crates/typst-library/src/compute/mod.rs1
-rw-r--r--crates/typst/src/eval/datetime.rs79
-rw-r--r--crates/typst/src/eval/duration.rs140
-rw-r--r--crates/typst/src/eval/methods.rs56
-rw-r--r--crates/typst/src/eval/mod.rs2
-rw-r--r--crates/typst/src/eval/ops.rs30
-rw-r--r--crates/typst/src/eval/value.rs15
-rw-r--r--docs/reference/types.md65
-rw-r--r--tests/typ/compiler/duration.typ104
-rw-r--r--tests/typ/compiler/methods.typ8
11 files changed, 527 insertions, 23 deletions
diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs
index 23f0c225..cee021bf 100644
--- a/crates/typst-library/src/compute/construct.rs
+++ b/crates/typst-library/src/compute/construct.rs
@@ -3,7 +3,7 @@ use std::str::FromStr;
use time::{Month, PrimitiveDateTime};
-use typst::eval::{Bytes, Datetime, Module, Plugin, Reflect, Regex};
+use typst::eval::{Bytes, Datetime, Duration, Module, Plugin, Reflect, Regex};
use crate::prelude::*;
@@ -334,6 +334,54 @@ pub fn datetime_today(
.ok_or("unable to get the current date")?)
}
+/// Creates a new duration.
+///
+/// You can specify the [duration]($type/duration) using weeks, days, hours,
+/// minutes and seconds. You can also get a duration by subtracting two
+/// [datetimes]($type/datetime).
+///
+/// ## Example
+/// ```example
+/// #duration(
+/// days: 3,
+/// hours: 12,
+/// ).hours()
+/// ```
+///
+/// Display: Duration
+/// Category: construct
+#[func]
+pub fn duration(
+ /// The number of seconds.
+ #[named]
+ #[default(0)]
+ seconds: i64,
+ /// The number of minutes.
+ #[named]
+ #[default(0)]
+ minutes: i64,
+ /// The number of hours.
+ #[named]
+ #[default(0)]
+ hours: i64,
+ /// The number of days.
+ #[named]
+ #[default(0)]
+ days: i64,
+ /// The number of weeks.
+ #[named]
+ #[default(0)]
+ weeks: i64,
+) -> Duration {
+ Duration::from(
+ time::Duration::seconds(seconds)
+ + time::Duration::minutes(minutes)
+ + time::Duration::hours(hours)
+ + time::Duration::days(days)
+ + time::Duration::weeks(weeks),
+ )
+}
+
/// Creates a CMYK color.
///
/// This is useful if you want to target a specific printer. The conversion
diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs
index 5e7e8d46..599ac72f 100644
--- a/crates/typst-library/src/compute/mod.rs
+++ b/crates/typst-library/src/compute/mod.rs
@@ -25,6 +25,7 @@ pub(super) fn define(global: &mut Scope) {
global.define("cmyk", cmyk_func());
global.define("color", color_module());
global.define("datetime", datetime_func());
+ global.define("duration", duration_func());
global.define("symbol", symbol_func());
global.define("str", str_func());
global.define("bytes", bytes_func());
diff --git a/crates/typst/src/eval/datetime.rs b/crates/typst/src/eval/datetime.rs
index f3c4a5a1..96127e9d 100644
--- a/crates/typst/src/eval/datetime.rs
+++ b/crates/typst/src/eval/datetime.rs
@@ -1,17 +1,20 @@
+use std::cmp::Ordering;
use std::fmt;
use std::fmt::{Debug, Formatter};
use std::hash::Hash;
+use std::ops::{Add, Sub};
use ecow::{eco_format, EcoString, EcoVec};
use time::error::{Format, InvalidFormatDescription};
use time::{format_description, PrimitiveDateTime};
-use crate::eval::cast;
+use crate::diag::bail;
+use crate::eval::{Duration, StrResult};
use crate::util::pretty_array_like;
/// A datetime object that represents either a date, a time or a combination of
/// both.
-#[derive(Clone, Copy, PartialEq, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Datetime {
/// Representation as a date.
Date(time::Date),
@@ -22,6 +25,15 @@ pub enum Datetime {
}
impl Datetime {
+ /// Which kind of variant this datetime stores.
+ pub fn kind(&self) -> &'static str {
+ match self {
+ Datetime::Datetime(_) => "datetime",
+ Datetime::Date(_) => "date",
+ Datetime::Time(_) => "time",
+ }
+ }
+
/// Display the date and/or time in a certain format.
pub fn display(&self, pattern: Option<EcoString>) -> Result<EcoString, EcoString> {
let pattern = pattern.as_ref().map(EcoString::as_str).unwrap_or(match self {
@@ -106,6 +118,15 @@ impl Datetime {
}
}
+ /// Return the ordinal (day of the year), if existing.
+ pub fn ordinal(&self) -> Option<u16> {
+ match self {
+ Datetime::Datetime(datetime) => Some(datetime.ordinal()),
+ Datetime::Date(date) => Some(date.ordinal()),
+ Datetime::Time(_) => None,
+ }
+ }
+
/// Create a datetime from year, month, and day.
pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
Some(Datetime::Date(
@@ -136,6 +157,56 @@ impl Datetime {
}
}
+impl PartialOrd for Datetime {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ match (self, other) {
+ (Datetime::Datetime(a), Datetime::Datetime(b)) => a.partial_cmp(b),
+ (Datetime::Date(a), Datetime::Date(b)) => a.partial_cmp(b),
+ (Datetime::Time(a), Datetime::Time(b)) => a.partial_cmp(b),
+ _ => None,
+ }
+ }
+}
+
+impl Add<Duration> for Datetime {
+ type Output = Datetime;
+
+ fn add(self, rhs: Duration) -> Self::Output {
+ let rhs: time::Duration = rhs.into();
+ match self {
+ Datetime::Datetime(datetime) => Self::Datetime(datetime + rhs),
+ Datetime::Date(date) => Self::Date(date + rhs),
+ Datetime::Time(time) => Self::Time(time + rhs),
+ }
+ }
+}
+
+impl Sub<Duration> for Datetime {
+ type Output = Datetime;
+
+ fn sub(self, rhs: Duration) -> Self::Output {
+ let rhs: time::Duration = rhs.into();
+ match self {
+ Datetime::Datetime(datetime) => Self::Datetime(datetime - rhs),
+ Datetime::Date(date) => Self::Date(date - rhs),
+ Datetime::Time(time) => Self::Time(time - rhs),
+ }
+ }
+}
+
+impl Sub for Datetime {
+ type Output = StrResult<Duration>;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ match (self, rhs) {
+ (Datetime::Datetime(a), Datetime::Datetime(b)) => Ok((a - b).into()),
+ (Datetime::Date(a), Datetime::Date(b)) => Ok((a - b).into()),
+ (Datetime::Time(a), Datetime::Time(b)) => Ok((a - b).into()),
+ (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
+ }
+ }
+}
+
impl Debug for Datetime {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let year = self.year().map(|y| eco_format!("year: {y}"));
@@ -153,10 +224,6 @@ impl Debug for Datetime {
}
}
-cast! {
- type Datetime: "datetime",
-}
-
/// Format the `Format` error of the time crate in an appropriate way.
fn format_time_format_error(error: Format) -> EcoString {
match error {
diff --git a/crates/typst/src/eval/duration.rs b/crates/typst/src/eval/duration.rs
new file mode 100644
index 00000000..500ce209
--- /dev/null
+++ b/crates/typst/src/eval/duration.rs
@@ -0,0 +1,140 @@
+use crate::util::pretty_array_like;
+use ecow::eco_format;
+use std::fmt;
+use std::fmt::{Debug, Formatter};
+use std::ops::{Add, Div, Mul, Neg, Sub};
+use time::ext::NumericalDuration;
+
+/// Represents a positive or negative span of time.
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Duration(time::Duration);
+
+impl Duration {
+ /// Whether the duration is empty / zero.
+ pub fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+
+ /// The duration expressed in seconds.
+ pub fn seconds(&self) -> f64 {
+ self.0.as_seconds_f64()
+ }
+
+ /// The duration expressed in minutes.
+ pub fn minutes(&self) -> f64 {
+ self.seconds() / 60.0
+ }
+
+ /// The duration expressed in hours.
+ pub fn hours(&self) -> f64 {
+ self.seconds() / 3_600.0
+ }
+
+ /// The duration expressed in days.
+ pub fn days(&self) -> f64 {
+ self.seconds() / 86_400.0
+ }
+
+ /// The duration expressed in weeks.
+ pub fn weeks(&self) -> f64 {
+ self.seconds() / 604_800.0
+ }
+}
+
+impl From<time::Duration> for Duration {
+ fn from(value: time::Duration) -> Self {
+ Self(value)
+ }
+}
+
+impl Debug for Duration {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ let mut tmp = self.0;
+ let mut vec = Vec::with_capacity(5);
+
+ let weeks = tmp.whole_seconds() / 604_800.0 as i64;
+ if weeks != 0 {
+ vec.push(eco_format!("weeks: {weeks}"));
+ }
+ tmp -= weeks.weeks();
+
+ let days = tmp.whole_days();
+ if days != 0 {
+ vec.push(eco_format!("days: {days}"));
+ }
+ tmp -= days.days();
+
+ let hours = tmp.whole_hours();
+ if hours != 0 {
+ vec.push(eco_format!("hours: {hours}"));
+ }
+ tmp -= hours.hours();
+
+ let minutes = tmp.whole_minutes();
+ if minutes != 0 {
+ vec.push(eco_format!("minutes: {minutes}"));
+ }
+ tmp -= minutes.minutes();
+
+ let seconds = tmp.whole_seconds();
+ if seconds != 0 {
+ vec.push(eco_format!("seconds: {seconds}"));
+ }
+
+ write!(f, "duration{}", &pretty_array_like(&vec, false))
+ }
+}
+
+impl From<Duration> for time::Duration {
+ fn from(value: Duration) -> Self {
+ value.0
+ }
+}
+
+impl Add for Duration {
+ type Output = Duration;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Duration(self.0 + rhs.0)
+ }
+}
+
+impl Sub for Duration {
+ type Output = Duration;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Duration(self.0 - rhs.0)
+ }
+}
+
+impl Neg for Duration {
+ type Output = Duration;
+
+ fn neg(self) -> Self::Output {
+ Duration(-self.0)
+ }
+}
+
+impl Mul<f64> for Duration {
+ type Output = Duration;
+
+ fn mul(self, rhs: f64) -> Self::Output {
+ Duration(self.0 * rhs)
+ }
+}
+
+impl Div<f64> for Duration {
+ type Output = Duration;
+
+ fn div(self, rhs: f64) -> Self::Output {
+ Duration(self.0 / rhs)
+ }
+}
+
+impl Div for Duration {
+ type Output = f64;
+
+ fn div(self, rhs: Self) -> Self::Output {
+ self.0 / rhs.0
+ }
+}
diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs
index ed11d8a7..018e80b0 100644
--- a/crates/typst/src/eval/methods.rs
+++ b/crates/typst/src/eval/methods.rs
@@ -4,7 +4,7 @@ use ecow::{eco_format, EcoString};
use super::{Args, Bytes, IntoValue, Plugin, Str, Value, Vm};
use crate::diag::{At, Hint, SourceResult};
-use crate::eval::{bail, Datetime};
+use crate::eval::bail;
use crate::geom::{Align, Axes, Color, Dir, Em, GenAlign};
use crate::model::{Location, Selector};
use crate::syntax::Span;
@@ -107,6 +107,28 @@ pub fn call(
_ => return missing(),
},
+ Value::Datetime(datetime) => match method {
+ "display" => datetime.display(args.eat()?).at(args.span)?.into_value(),
+ "year" => datetime.year().into_value(),
+ "month" => datetime.month().into_value(),
+ "weekday" => datetime.weekday().into_value(),
+ "day" => datetime.day().into_value(),
+ "hour" => datetime.hour().into_value(),
+ "minute" => datetime.minute().into_value(),
+ "second" => datetime.second().into_value(),
+ "ordinal" => datetime.ordinal().into_value(),
+ _ => return missing(),
+ },
+
+ Value::Duration(duration) => match method {
+ "seconds" => duration.seconds().into_value(),
+ "minutes" => duration.minutes().into_value(),
+ "hours" => duration.hours().into_value(),
+ "days" => duration.days().into_value(),
+ "weeks" => duration.weeks().into_value(),
+ _ => return missing(),
+ },
+
Value::Content(content) => match method {
"func" => content.func().into_value(),
"has" => content.has(&args.expect::<EcoString>("field")?).into_value(),
@@ -257,20 +279,6 @@ pub fn call(
}
_ => return missing(),
}
- } else if let Some(&datetime) = dynamic.downcast::<Datetime>() {
- match method {
- "display" => {
- datetime.display(args.eat()?).at(args.span)?.into_value()
- }
- "year" => datetime.year().into_value(),
- "month" => datetime.month().into_value(),
- "weekday" => datetime.weekday().into_value(),
- "day" => datetime.day().into_value(),
- "hour" => datetime.hour().into_value(),
- "minute" => datetime.minute().into_value(),
- "second" => datetime.second().into_value(),
- _ => return missing(),
- }
} else if let Some(direction) = dynamic.downcast::<Dir>() {
match method {
"axis" => direction.axis().description().into_value(),
@@ -426,6 +434,24 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("trim", true),
],
"bytes" => &[("len", false), ("at", true), ("slice", true)],
+ "datetime" => &[
+ ("display", true),
+ ("year", false),
+ ("month", false),
+ ("weekday", false),
+ ("day", false),
+ ("hour", false),
+ ("minute", false),
+ ("second", false),
+ ("ordinal", false),
+ ],
+ "duration" => &[
+ ("seconds", false),
+ ("minutes", false),
+ ("hours", false),
+ ("days", false),
+ ("weeks", false),
+ ],
"content" => &[
("func", false),
("has", true),
diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs
index 63eed5ac..706f6b01 100644
--- a/crates/typst/src/eval/mod.rs
+++ b/crates/typst/src/eval/mod.rs
@@ -16,6 +16,7 @@ mod args;
mod auto;
mod bytes;
mod datetime;
+mod duration;
mod fields;
mod func;
mod int;
@@ -48,6 +49,7 @@ pub use self::cast::{
};
pub use self::datetime::Datetime;
pub use self::dict::{dict, Dict};
+pub use self::duration::Duration;
pub use self::fields::fields_on;
pub use self::func::{Func, FuncInfo, NativeFunc, ParamInfo};
pub use self::library::{set_lang_items, LangItems, Library};
diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs
index f57f743f..2dc3c8f9 100644
--- a/crates/typst/src/eval/ops.rs
+++ b/crates/typst/src/eval/ops.rs
@@ -62,6 +62,7 @@ pub fn neg(value: Value) -> StrResult<Value> {
Ratio(v) => Ratio(-v),
Relative(v) => Relative(-v),
Fraction(v) => Fraction(-v),
+ Duration(v) => Duration(-v),
v => mismatch!("cannot apply '-' to {}", v),
})
}
@@ -115,6 +116,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
})
}
+ (Duration(a), Duration(b)) => Duration(a + b),
+ (Datetime(a), Duration(b)) => Datetime(a + b),
+ (Duration(a), Datetime(b)) => Datetime(b + a),
+
(Dyn(a), Dyn(b)) => {
// 1D alignments can be summed into 2D alignments.
if let (Some(&a), Some(&b)) =
@@ -161,6 +166,10 @@ pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> {
(Fraction(a), Fraction(b)) => Fraction(a - b),
+ (Duration(a), Duration(b)) => Duration(a - b),
+ (Datetime(a), Duration(b)) => Datetime(a - b),
+ (Datetime(a), Datetime(b)) => Duration((a - b)?),
+
(a, b) => mismatch!("cannot subtract {1} from {0}", a, b),
})
}
@@ -214,6 +223,11 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
(Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)),
(a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)),
+ (Int(a), Duration(b)) => Duration(b * (a as f64)),
+ (Float(a), Duration(b)) => Duration(b * a),
+ (Duration(a), Int(b)) => Duration(a * (b as f64)),
+ (Duration(a), Float(b)) => Duration(a * b),
+
(a, b) => mismatch!("cannot multiply {} with {}", a, b),
})
}
@@ -254,6 +268,10 @@ pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> {
(Fraction(a), Float(b)) => Fraction(a / b),
(Fraction(a), Fraction(b)) => Float(a / b),
+ (Duration(a), Int(b)) => Duration(a / (b as f64)),
+ (Duration(a), Float(b)) => Duration(a / b),
+ (Duration(a), Duration(b)) => Float(a / b),
+
(a, b) => mismatch!("cannot divide {} by {}", a, b),
})
}
@@ -268,6 +286,7 @@ fn is_zero(v: &Value) -> bool {
Ratio(v) => v.is_zero(),
Relative(v) => v.is_zero(),
Fraction(v) => v.is_zero(),
+ Duration(v) => v.is_zero(),
_ => false,
}
}
@@ -357,6 +376,8 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Func(a), Func(b)) => a == b,
(Args(a), Args(b)) => a == b,
(Module(a), Module(b)) => a == b,
+ (Datetime(a), Datetime(b)) => a == b,
+ (Duration(a), Duration(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b,
// Some technically different things should compare equal.
@@ -392,6 +413,9 @@ pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> {
(Relative(a), Length(b)) if a.rel.is_zero() => try_cmp_values(&a.abs, b)?,
(Relative(a), Ratio(b)) if a.abs.is_zero() => a.rel.cmp(b),
+ (Duration(a), Duration(b)) => a.cmp(b),
+ (Datetime(a), Datetime(b)) => try_cmp_datetimes(a, b)?,
+
_ => mismatch!("cannot compare {} and {}", lhs, rhs),
})
}
@@ -402,6 +426,12 @@ fn try_cmp_values<T: PartialOrd + Debug>(a: &T, b: &T) -> StrResult<Ordering> {
.ok_or_else(|| eco_format!("cannot compare {:?} with {:?}", a, b))
}
+/// Try to compare two datetimes.
+fn try_cmp_datetimes(a: &super::Datetime, b: &super::Datetime) -> StrResult<Ordering> {
+ a.partial_cmp(b)
+ .ok_or_else(|| eco_format!("cannot compare {} and {}", a.kind(), b.kind()))
+}
+
/// Test whether one value is "in" another one.
pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs
index 759f55f0..4e870ab4 100644
--- a/crates/typst/src/eval/value.rs
+++ b/crates/typst/src/eval/value.rs
@@ -10,6 +10,7 @@ use serde::de::{Error, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use siphasher::sip128::{Hasher128, SipHasher13};
use time::macros::format_description;
+use typst::eval::Duration;
use super::{
cast, fields, format_str, ops, Args, Array, Bytes, CastInfo, Content, Dict,
@@ -55,6 +56,10 @@ pub enum Value {
Bytes(Bytes),
/// A label: `<intro>`.
Label(Label),
+ /// A datetime
+ Datetime(Datetime),
+ /// A duration
+ Duration(Duration),
/// A content value: `[*Hi* there]`.
Content(Content),
// Content styles.
@@ -116,6 +121,8 @@ impl Value {
Self::Str(_) => Str::TYPE_NAME,
Self::Bytes(_) => Bytes::TYPE_NAME,
Self::Label(_) => Label::TYPE_NAME,
+ Self::Datetime(_) => Datetime::TYPE_NAME,
+ Self::Duration(_) => Duration::TYPE_NAME,
Self::Content(_) => Content::TYPE_NAME,
Self::Styles(_) => Styles::TYPE_NAME,
Self::Array(_) => Array::TYPE_NAME,
@@ -200,6 +207,8 @@ impl Debug for Value {
Self::Str(v) => Debug::fmt(v, f),
Self::Bytes(v) => Debug::fmt(v, f),
Self::Label(v) => Debug::fmt(v, f),
+ Self::Datetime(v) => Debug::fmt(v, f),
+ Self::Duration(v) => Debug::fmt(v, f),
Self::Content(v) => Debug::fmt(v, f),
Self::Styles(v) => Debug::fmt(v, f),
Self::Array(v) => Debug::fmt(v, f),
@@ -245,6 +254,8 @@ impl Hash for Value {
Self::Label(v) => v.hash(state),
Self::Content(v) => v.hash(state),
Self::Styles(v) => v.hash(state),
+ Self::Datetime(v) => v.hash(state),
+ Self::Duration(v) => v.hash(state),
Self::Array(v) => v.hash(state),
Self::Dict(v) => v.hash(state),
Self::Func(v) => v.hash(state),
@@ -435,7 +446,7 @@ impl<'de> Visitor<'de> for ValueVisitor {
let dict = Dict::deserialize(MapAccessDeserializer::new(map))?;
Ok(match parse_toml_date(&dict) {
None => dict.into_value(),
- Some(dt) => Value::dynamic(dt),
+ Some(datetime) => datetime.into_value(),
})
}
}
@@ -610,6 +621,8 @@ primitive! {
}
primitive! { Bytes: "bytes", Bytes }
primitive! { Label: "label", Label }
+primitive! { Datetime: "datetime", Datetime }
+primitive! { Duration: "duration", Duration }
primitive! { Content: "content",
Content,
None => Content::empty(),
diff --git a/docs/reference/types.md b/docs/reference/types.md
index 365593f3..09cc2909 100644
--- a/docs/reference/types.md
+++ b/docs/reference/types.md
@@ -431,6 +431,71 @@ Returns the second of the datetime, if it exists. Otherwise, it returns
- returns: integer or none
+### ordinal()
+Returns the ordinal (day of the year) of the datetime, if it exists.
+Otherwise, it returns `{none}`.
+
+- returns: integer or none
+
+# Duration
+Represents a span of time. Can be created by either specifying a custom
+duration using the [`duration`]($func/datetime) function or by subtracting two
+[datetimes]($type/datetime).
+
+## Example
+```example
+#let duration = duration(
+ days: 4,
+ hours: 2,
+ minutes: 10,
+)
+
+#duration.hours()
+```
+
+## Methods
+### seconds()
+Returns the duration in seconds as a floating-point value.
+
+This function returns the total duration represented in seconds as a
+floating-point number. It does not provide the second component of the duration,
+but rather gives the overall duration in terms of seconds.
+
+- returns: float
+
+### minutes()
+Returns the duration in minutes as a floating-point value.
+
+This function returns the total duration represented in minutes as a
+floating-point number. It does not provide the minute component of the duration,
+but rather gives the overall duration in terms of minutes.
+
+- returns: float
+
+### hours()
+Returns the duration in hours as a floating-point value.
+
+This function returns the total duration represented in hours as a
+floating-point number. It does not provide the hour component of the duration,
+but rather gives the overall duration in terms of hours.
+- returns: float
+
+### days()
+Returns the duration in days as a floating-point value.
+
+This function returns the total duration represented in days as a
+floating-point number. It does not provide the day component of the duration,
+but rather gives the overall duration in terms of days.
+- returns: float
+
+### weeks()
+Returns the duration in weeks as a floating-point value.
+
+This function returns the total duration represented in weeks as a
+floating-point number. It does not provide the week component of the duration,
+but rather gives the overall duration in terms of weeks.
+- returns: float
+
# Symbol
A Unicode symbol.
diff --git a/tests/typ/compiler/duration.typ b/tests/typ/compiler/duration.typ
new file mode 100644
index 00000000..1d831a6f
--- /dev/null
+++ b/tests/typ/compiler/duration.typ
@@ -0,0 +1,104 @@
+// Test durations.
+// Ref: false
+
+---
+// Test negating durations.
+#test(-duration(hours: 2), duration(hours: -2))
+
+---
+// Test adding and subtracting durations.
+#test(duration(weeks: 1, hours: 1), duration(weeks: 1) + duration(hours: 1))
+#test(duration(weeks: 1, hours: -1), duration(weeks: 1) - duration(hours: 1))
+#test(duration(days: 6, hours: 23), duration(weeks: 1) - duration(hours: 1))
+
+---
+// Test adding and subtracting durations and dates.
+#let d = datetime(day: 1, month: 1, year: 2000)
+#let d2 = datetime(day: 1, month: 2, year: 2000)
+#test(d + duration(weeks: 2), datetime(day: 15, month: 1, year: 2000))
+#test(d + duration(days: 3), datetime(day: 4, month: 1, year: 2000))
+#test(d + duration(weeks: 1, days: 3), datetime(day: 11, month: 1, year: 2000))
+#test(d2 + duration(days: -1), datetime(day: 31, month: 1, year: 2000))
+#test(d2 + duration(days: -3), datetime(day: 29, month: 1, year: 2000))
+#test(d2 + duration(weeks: -1), datetime(day: 25, month: 1, year: 2000))
+#test(d + duration(days: -1), datetime(day: 31, month: 12, year: 1999))
+#test(d + duration(weeks: 1, days: -7), datetime(day: 1, month: 1, year: 2000))
+#test(d2 - duration(days: 1), datetime(day: 31, month: 1, year: 2000))
+#test(d2 - duration(days: 3), datetime(day: 29, month: 1, year: 2000))
+#test(d2 - duration(weeks: 1), datetime(day: 25, month: 1, year: 2000))
+#test(d - duration(days: 1), datetime(day: 31, month: 12, year: 1999))
+#test(datetime(day: 31, month: 1, year: 2000) + duration(days: 1), d2)
+#test(
+ datetime(day: 31, month: 12, year: 2000) + duration(days: 1),
+ datetime(day: 1, month: 1, year: 2001),
+)
+
+---
+// Test adding and subtracting durations and times.
+#let a = datetime(hour: 12, minute: 0, second: 0)
+#test(a + duration(hours: 1, minutes: -60), datetime(hour: 12, minute: 0, second: 0))
+#test(a + duration(hours: 2), datetime(hour: 14, minute: 0, second: 0))
+#test(a + duration(minutes: 10), datetime(hour: 12, minute: 10, second: 0))
+#test(a + duration(seconds: 30), datetime(hour: 12, minute: 0, second: 30))
+#test(a + duration(hours: -2), datetime(hour: 10, minute: 0, second: 0))
+#test(a - duration(hours: 2), datetime(hour: 10, minute: 0, second: 0))
+#test(a + duration(minutes: -10), datetime(hour: 11, minute: 50, second: 0))
+#test(a - duration(minutes: 10), datetime(hour: 11, minute: 50, second: 0))
+#test(a + duration(seconds: -30), datetime(hour: 11, minute: 59, second: 30))
+#test(a - duration(seconds: 30), datetime(hour: 11, minute: 59, second: 30))
+#test(
+ a + duration(hours: 1, minutes: 13, seconds: 13),
+ datetime(hour: 13, minute: 13, second: 13),
+)
+
+---
+// Test adding and subtracting durations and datetimes.
+#test(
+ datetime(day: 1, month: 1, year: 2000, hour: 12, minute: 0, second: 0)
+ + duration(weeks: 1, days: 3, hours: -13, minutes: 10, seconds: -10 ),
+ datetime(day: 10, month: 1, year: 2000, hour: 23, minute: 9, second: 50),
+)
+#test(
+ datetime(day: 1, month: 1, year: 2000, hour: 12, minute: 0, second: 0)
+ + duration(weeks: 1, days: 3, minutes: 10)
+ - duration(hours: 13, seconds: 10),
+ datetime(day: 10, month: 1, year: 2000, hour: 23, minute: 9, second: 50),
+)
+
+---
+// Test subtracting dates.
+#let a = datetime(hour: 12, minute: 0, second: 0)
+#let b = datetime(day: 1, month: 1, year: 2000)
+#test(datetime(hour: 14, minute: 0, second: 0) - a, duration(hours: 2))
+#test(datetime(hour: 14, minute: 0, second: 0) - a, duration(minutes: 120))
+#test(datetime(hour: 13, minute: 0, second: 0) - a, duration(seconds: 3600))
+#test(datetime(day: 1, month: 2, year: 2000) - b, duration(days: 31))
+#test(datetime(day: 15, month: 1, year: 2000) - b, duration(weeks: 2))
+
+---
+// Test multiplying and dividing durations with numbers.
+#test(duration(minutes: 10) * 6, duration(hours: 1))
+#test(duration(minutes: 10) * 2, duration(minutes: 20))
+#test(duration(minutes: 10) * 2.5, duration(minutes: 25))
+#test(duration(minutes: 10) / 2, duration(minutes: 5))
+#test(duration(minutes: 10) / 2.5, duration(minutes: 4))
+
+---
+// Test dividing durations with durations
+#test(duration(minutes: 20) / duration(hours: 1), 1 / 3)
+#test(duration(minutes: 20) / duration(minutes: 10), 2)
+#test(duration(minutes: 20) / duration(minutes: 8), 2.5)
+
+---
+// Test comparing durations
+#test(duration(minutes: 20) > duration(minutes: 10), true)
+#test(duration(minutes: 20) >= duration(minutes: 10), true)
+#test(duration(minutes: 10) < duration(minutes: 20), true)
+#test(duration(minutes: 10) <= duration(minutes: 20), true)
+#test(duration(minutes: 10) == duration(minutes: 10), true)
+#test(duration(minutes: 10) != duration(minutes: 20), true)
+#test(duration(minutes: 10) <= duration(minutes: 10), true)
+#test(duration(minutes: 10) >= duration(minutes: 10), true)
+#test(duration(minutes: 20) < duration(minutes: 10), false)
+#test(duration(minutes: 20) <= duration(minutes: 10), false)
+#test(duration(minutes: 20) == duration(minutes: 10), false)
diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ
index c0ad5b1d..ace1dc60 100644
--- a/tests/typ/compiler/methods.typ
+++ b/tests/typ/compiler/methods.typ
@@ -198,3 +198,11 @@
#test(2deg.deg(), 2.0)
#test(2.94deg.deg(), 2.94)
#test(0rad.deg(), 0.0)
+
+---
+// Test date methods.
+#test(datetime(day: 1, month: 1, year: 2000).ordinal(), 1);
+#test(datetime(day: 1, month: 3, year: 2000).ordinal(), 31 + 29 + 1);
+#test(datetime(day: 31, month: 12, year: 2000).ordinal(), 366);
+#test(datetime(day: 1, month: 3, year: 2001).ordinal(), 31 + 28 + 1);
+#test(datetime(day: 31, month: 12, year: 2001).ordinal(), 365);