summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorBeiri22 <beier1@hs-mittweida.de>2023-08-30 12:28:53 +0200
committerGitHub <noreply@github.com>2023-08-30 12:28:53 +0200
commitf616302496c824dd6a9fb2721c7df0c57b09708d (patch)
treecc6f3fc95d876ea66eebdd6d99885b917e56c620 /crates
parent35c785ea118d13112c771e4c00e6f9a21dd6ba65 (diff)
Duration type, simple date-duration-calculations and comparisons (#1843)
Diffstat (limited to 'crates')
-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
8 files changed, 350 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(),