From f616302496c824dd6a9fb2721c7df0c57b09708d Mon Sep 17 00:00:00 2001 From: Beiri22 Date: Wed, 30 Aug 2023 12:28:53 +0200 Subject: [PATCH] Duration type, simple date-duration-calculations and comparisons (#1843) --- crates/typst-library/src/compute/construct.rs | 50 ++++++- crates/typst-library/src/compute/mod.rs | 1 + crates/typst/src/eval/datetime.rs | 79 +++++++++- crates/typst/src/eval/duration.rs | 140 ++++++++++++++++++ crates/typst/src/eval/methods.rs | 56 +++++-- crates/typst/src/eval/mod.rs | 2 + crates/typst/src/eval/ops.rs | 30 ++++ crates/typst/src/eval/value.rs | 15 +- docs/reference/types.md | 65 ++++++++ tests/typ/compiler/duration.typ | 104 +++++++++++++ tests/typ/compiler/methods.typ | 8 + 11 files changed, 527 insertions(+), 23 deletions(-) create mode 100644 crates/typst/src/eval/duration.rs create mode 100644 tests/typ/compiler/duration.typ diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs index 23f0c225d..cee021bf6 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 5e7e8d46b..599ac72fb 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 f3c4a5a1c..96127e9d4 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) -> Result { 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 { + 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 { Some(Datetime::Date( @@ -136,6 +157,56 @@ impl Datetime { } } +impl PartialOrd for Datetime { + fn partial_cmp(&self, other: &Self) -> Option { + 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 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 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; + + 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 000000000..500ce209d --- /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 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 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 for Duration { + type Output = Duration; + + fn mul(self, rhs: f64) -> Self::Output { + Duration(self.0 * rhs) + } +} + +impl Div 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 ed11d8a7b..018e80b07 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::("field")?).into_value(), @@ -257,20 +279,6 @@ pub fn call( } _ => return missing(), } - } else if let Some(&datetime) = dynamic.downcast::() { - 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::() { 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 63eed5ac9..706f6b019 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 f57f743fa..2dc3c8f9a 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 { 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 { }) } + (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 { (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 { (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 { (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 { (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(a: &T, b: &T) -> StrResult { .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 { + 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 { if let Some(b) = contains(&lhs, &rhs) { diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs index 759f55f08..4e870ab42 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: ``. 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 365593f3d..09cc29093 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 000000000..1d831a6fa --- /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 c0ad5b1db..ace1dc606 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);