Duration type, simple date-duration-calculations and comparisons (#1843)

This commit is contained in:
Beiri22 2023-08-30 12:28:53 +02:00 committed by GitHub
parent 35c785ea11
commit f616302496
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 527 additions and 23 deletions

View File

@ -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

View File

@ -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());

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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),

View File

@ -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};

View File

@ -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) {

View File

@ -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(),

View File

@ -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.

View File

@ -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)

View File

@ -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);