summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/typst/Cargo.toml1
-rw-r--r--crates/typst/src/eval/ops.rs54
-rw-r--r--crates/typst/src/foundations/calc.rs423
-rw-r--r--crates/typst/src/foundations/cast.rs7
-rw-r--r--crates/typst/src/foundations/decimal.rs369
-rw-r--r--crates/typst/src/foundations/float.rs3
-rw-r--r--crates/typst/src/foundations/int.rs20
-rw-r--r--crates/typst/src/foundations/mod.rs3
-rw-r--r--crates/typst/src/foundations/str.rs5
-rw-r--r--crates/typst/src/foundations/value.rs15
10 files changed, 786 insertions, 114 deletions
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index 2b9c5d1a..a3e21123 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -52,6 +52,7 @@ portable-atomic = { workspace = true }
rayon = { workspace = true }
regex = { workspace = true }
roxmltree = { workspace = true }
+rust_decimal = { workspace = true }
rustybuzz = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs
index c9c8a24e..52032ae9 100644
--- a/crates/typst/src/eval/ops.rs
+++ b/crates/typst/src/eval/ops.rs
@@ -139,6 +139,7 @@ pub fn pos(value: Value) -> HintedStrResult<Value> {
Ok(match value {
Int(v) => Int(v),
Float(v) => Float(v),
+ Decimal(v) => Decimal(v),
Length(v) => Length(v),
Angle(v) => Angle(v),
Ratio(v) => Ratio(v),
@@ -164,6 +165,7 @@ pub fn neg(value: Value) -> HintedStrResult<Value> {
Ok(match value {
Int(v) => Int(v.checked_neg().ok_or_else(too_large)?),
Float(v) => Float(-v),
+ Decimal(v) => Decimal(-v),
Length(v) => Length(-v),
Angle(v) => Angle(-v),
Ratio(v) => Ratio(-v),
@@ -187,6 +189,17 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Float(a), Int(b)) => Float(a + b as f64),
(Float(a), Float(b)) => Float(a + b),
+ (Decimal(a), Decimal(b)) => Decimal(a.checked_add(b).ok_or_else(too_large)?),
+ (Decimal(a), Int(b)) => Decimal(
+ a.checked_add(crate::foundations::Decimal::from(b))
+ .ok_or_else(too_large)?,
+ ),
+ (Int(a), Decimal(b)) => Decimal(
+ crate::foundations::Decimal::from(a)
+ .checked_add(b)
+ .ok_or_else(too_large)?,
+ ),
+
(Angle(a), Angle(b)) => Angle(a + b),
(Length(a), Length(b)) => Length(a + b),
@@ -260,6 +273,17 @@ pub fn sub(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Float(a), Int(b)) => Float(a - b as f64),
(Float(a), Float(b)) => Float(a - b),
+ (Decimal(a), Decimal(b)) => Decimal(a.checked_sub(b).ok_or_else(too_large)?),
+ (Decimal(a), Int(b)) => Decimal(
+ a.checked_sub(crate::foundations::Decimal::from(b))
+ .ok_or_else(too_large)?,
+ ),
+ (Int(a), Decimal(b)) => Decimal(
+ crate::foundations::Decimal::from(a)
+ .checked_sub(b)
+ .ok_or_else(too_large)?,
+ ),
+
(Angle(a), Angle(b)) => Angle(a - b),
(Length(a), Length(b)) => Length(a - b),
@@ -293,6 +317,17 @@ pub fn mul(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Float(a), Int(b)) => Float(a * b as f64),
(Float(a), Float(b)) => Float(a * b),
+ (Decimal(a), Decimal(b)) => Decimal(a.checked_mul(b).ok_or_else(too_large)?),
+ (Decimal(a), Int(b)) => Decimal(
+ a.checked_mul(crate::foundations::Decimal::from(b))
+ .ok_or_else(too_large)?,
+ ),
+ (Int(a), Decimal(b)) => Decimal(
+ crate::foundations::Decimal::from(a)
+ .checked_mul(b)
+ .ok_or_else(too_large)?,
+ ),
+
(Length(a), Int(b)) => Length(a * b as f64),
(Length(a), Float(b)) => Length(a * b),
(Length(a), Ratio(b)) => Length(a * b.get()),
@@ -356,6 +391,17 @@ pub fn div(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Float(a), Int(b)) => Float(a / b as f64),
(Float(a), Float(b)) => Float(a / b),
+ (Decimal(a), Decimal(b)) => Decimal(a.checked_div(b).ok_or_else(too_large)?),
+ (Decimal(a), Int(b)) => Decimal(
+ a.checked_div(crate::foundations::Decimal::from(b))
+ .ok_or_else(too_large)?,
+ ),
+ (Int(a), Decimal(b)) => Decimal(
+ crate::foundations::Decimal::from(a)
+ .checked_div(b)
+ .ok_or_else(too_large)?,
+ ),
+
(Length(a), Int(b)) => Length(a / b as f64),
(Length(a), Float(b)) => Length(a / b),
(Length(a), Length(b)) => Float(try_div_length(a, b)?),
@@ -394,6 +440,7 @@ fn is_zero(v: &Value) -> bool {
match *v {
Int(v) => v == 0,
Float(v) => v == 0.0,
+ Decimal(v) => v.is_zero(),
Length(v) => v.is_zero(),
Angle(v) => v.is_zero(),
Ratio(v) => v.is_zero(),
@@ -474,6 +521,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Bool(a), Bool(b)) => a == b,
(Int(a), Int(b)) => a == b,
(Float(a), Float(b)) => a == b,
+ (Decimal(a), Decimal(b)) => a == b,
(Length(a), Length(b)) => a == b,
(Angle(a), Angle(b)) => a == b,
(Ratio(a), Ratio(b)) => a == b,
@@ -499,6 +547,9 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
// Some technically different things should compare equal.
(&Int(i), &Float(f)) | (&Float(f), &Int(i)) => i as f64 == f,
+ (&Int(i), &Decimal(d)) | (&Decimal(d), &Int(i)) => {
+ crate::foundations::Decimal::from(i) == d
+ }
(&Length(len), &Relative(rel)) | (&Relative(rel), &Length(len)) => {
len == rel.abs && rel.rel.is_zero()
}
@@ -520,6 +571,7 @@ pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> {
(Bool(a), Bool(b)) => a.cmp(b),
(Int(a), Int(b)) => a.cmp(b),
(Float(a), Float(b)) => try_cmp_values(a, b)?,
+ (Decimal(a), Decimal(b)) => a.cmp(b),
(Length(a), Length(b)) => try_cmp_values(a, b)?,
(Angle(a), Angle(b)) => a.cmp(b),
(Ratio(a), Ratio(b)) => a.cmp(b),
@@ -531,6 +583,8 @@ pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> {
// Some technically different things should be comparable.
(Int(a), Float(b)) => try_cmp_values(&(*a as f64), b)?,
(Float(a), Int(b)) => try_cmp_values(a, &(*b as f64))?,
+ (Int(a), Decimal(b)) => crate::foundations::Decimal::from(*a).cmp(b),
+ (Decimal(a), Int(b)) => a.cmp(&crate::foundations::Decimal::from(*b)),
(Length(a), Relative(b)) if b.rel.is_zero() => try_cmp_values(a, &b.abs)?,
(Ratio(a), Relative(b)) if b.abs.is_zero() => a.cmp(&b.rel),
(Relative(a), Length(b)) if a.rel.is_zero() => try_cmp_values(&a.abs, b)?,
diff --git a/crates/typst/src/foundations/calc.rs b/crates/typst/src/foundations/calc.rs
index 287bc828..f1faa73d 100644
--- a/crates/typst/src/foundations/calc.rs
+++ b/crates/typst/src/foundations/calc.rs
@@ -2,11 +2,12 @@
use std::cmp;
use std::cmp::Ordering;
-use std::ops::{Div, Rem};
-use crate::diag::{bail, At, SourceResult, StrResult};
+use az::SaturatingAs;
+
+use crate::diag::{bail, At, HintedString, SourceResult, StrResult};
use crate::eval::ops;
-use crate::foundations::{cast, func, IntoValue, Module, Scope, Value};
+use crate::foundations::{cast, func, Decimal, IntoValue, Module, Scope, Value};
use crate::layout::{Angle, Fr, Length, Ratio};
use crate::syntax::{Span, Spanned};
@@ -61,7 +62,8 @@ pub fn module() -> Module {
/// ```example
/// #calc.abs(-5) \
/// #calc.abs(5pt - 2cm) \
-/// #calc.abs(2fr)
+/// #calc.abs(2fr) \
+/// #calc.abs(decimal("-342.440"))
/// ```
#[func(title = "Absolute")]
pub fn abs(
@@ -83,24 +85,28 @@ cast! {
v: Angle => Self(Value::Angle(v.abs())),
v: Ratio => Self(Value::Ratio(v.abs())),
v: Fr => Self(Value::Fraction(v.abs())),
+ v: Decimal => Self(Value::Decimal(v.abs()))
}
/// Raises a value to some exponent.
///
/// ```example
/// #calc.pow(2, 3)
+/// #calc.pow(decimal("2.5"), 2)
/// ```
#[func(title = "Power")]
pub fn pow(
/// The callsite span.
span: Span,
/// The base of the power.
- base: Num,
+ ///
+ /// If this is a [`decimal`], the exponent can only be an [integer]($int).
+ base: DecNum,
/// The exponent of the power.
exponent: Spanned<Num>,
-) -> SourceResult<Num> {
+) -> SourceResult<DecNum> {
match exponent.v {
- _ if exponent.v.float() == 0.0 && base.float() == 0.0 => {
+ _ if exponent.v.float() == 0.0 && base.is_zero() => {
bail!(span, "zero to the power of zero is undefined")
}
Num::Int(i) if i32::try_from(i).is_err() => {
@@ -112,26 +118,37 @@ pub fn pow(
_ => {}
};
- let result = match (base, exponent.v) {
- (Num::Int(a), Num::Int(b)) if b >= 0 => {
- a.checked_pow(b as u32).map(Num::Int).ok_or_else(too_large).at(span)?
+ match (base, exponent.v) {
+ (DecNum::Int(a), Num::Int(b)) if b >= 0 => a
+ .checked_pow(b as u32)
+ .map(DecNum::Int)
+ .ok_or_else(too_large)
+ .at(span),
+ (DecNum::Decimal(a), Num::Int(b)) => {
+ a.checked_powi(b).map(DecNum::Decimal).ok_or_else(too_large).at(span)
+ }
+ (a, b) => {
+ let Some(a) = a.float() else {
+ return Err(cant_apply_to_decimal_and_float()).at(span);
+ };
+
+ let result = if a == std::f64::consts::E {
+ b.float().exp()
+ } else if a == 2.0 {
+ b.float().exp2()
+ } else if let Num::Int(b) = b {
+ a.powi(b as i32)
+ } else {
+ a.powf(b.float())
+ };
+
+ if result.is_nan() {
+ bail!(span, "the result is not a real number")
+ }
+
+ Ok(DecNum::Float(result))
}
- (a, b) => Num::Float(if a.float() == std::f64::consts::E {
- b.float().exp()
- } else if a.float() == 2.0 {
- b.float().exp2()
- } else if let Num::Int(b) = b {
- a.float().powi(b as i32)
- } else {
- a.float().powf(b.float())
- }),
- };
-
- if result.float().is_nan() {
- bail!(span, "the result is not a real number")
}
-
- Ok(result)
}
/// Raises a value to some exponent of e.
@@ -154,7 +171,7 @@ pub fn exp(
bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
}
_ => {}
- };
+ }
let result = exponent.v.float().exp();
if result.is_nan() {
@@ -598,19 +615,28 @@ pub fn lcm(
///
/// If the number is already an integer, it is returned unchanged.
///
+/// Note that this function will return the same type as the operand. That is,
+/// applying `floor` to a [`float`] will return a `float`, and to a [`decimal`],
+/// another `decimal`. You may explicitly convert the output of this function to
+/// an integer with [`int`], but note that such a conversion will error if the
+/// `float` or `decimal` is larger than the maximum 64-bit signed integer or
+/// smaller than the minimum integer.
+///
/// ```example
-/// #assert(calc.floor(3.14) == 3)
/// #assert(calc.floor(3) == 3)
+/// #assert(calc.floor(3.14) == 3.0)
+/// #assert(calc.floor(decimal("-3.14")) == decimal("-4"))
/// #calc.floor(500.1)
/// ```
#[func]
pub fn floor(
/// The number to round down.
- value: Num,
-) -> i64 {
+ value: DecNum,
+) -> DecNum {
match value {
- Num::Int(n) => n,
- Num::Float(n) => n.floor() as i64,
+ DecNum::Int(n) => DecNum::Int(n),
+ DecNum::Float(n) => DecNum::Float(n.floor()),
+ DecNum::Decimal(n) => DecNum::Decimal(n.floor()),
}
}
@@ -618,19 +644,28 @@ pub fn floor(
///
/// If the number is already an integer, it is returned unchanged.
///
+/// Note that this function will return the same type as the operand. That is,
+/// applying `ceil` to a [`float`] will return a `float`, and to a [`decimal`],
+/// another `decimal`. You may explicitly convert the output of this function to
+/// an integer with [`int`], but note that such a conversion will error if the
+/// `float` or `decimal` is larger than the maximum 64-bit signed integer or
+/// smaller than the minimum integer.
+///
/// ```example
-/// #assert(calc.ceil(3.14) == 4)
/// #assert(calc.ceil(3) == 3)
+/// #assert(calc.ceil(3.14) == 4)
+/// #assert(calc.ceil(decimal("-3.14")) == decimal("-3"))
/// #calc.ceil(500.1)
/// ```
#[func]
pub fn ceil(
/// The number to round up.
- value: Num,
-) -> i64 {
+ value: DecNum,
+) -> DecNum {
match value {
- Num::Int(n) => n,
- Num::Float(n) => n.ceil() as i64,
+ DecNum::Int(n) => DecNum::Int(n),
+ DecNum::Float(n) => DecNum::Float(n.ceil()),
+ DecNum::Decimal(n) => DecNum::Decimal(n.ceil()),
}
}
@@ -638,19 +673,28 @@ pub fn ceil(
///
/// If the number is already an integer, it is returned unchanged.
///
+/// Note that this function will return the same type as the operand. That is,
+/// applying `trunc` to a [`float`] will return a `float`, and to a [`decimal`],
+/// another `decimal`. You may explicitly convert the output of this function to
+/// an integer with [`int`], but note that such a conversion will error if the
+/// `float` or `decimal` is larger than the maximum 64-bit signed integer or
+/// smaller than the minimum integer.
+///
/// ```example
/// #assert(calc.trunc(3) == 3)
-/// #assert(calc.trunc(-3.7) == -3)
+/// #assert(calc.trunc(-3.7) == -3.0)
+/// #assert(calc.trunc(decimal("8493.12949582390")) == decimal("8493"))
/// #calc.trunc(15.9)
/// ```
#[func(title = "Truncate")]
pub fn trunc(
/// The number to truncate.
- value: Num,
-) -> i64 {
+ value: DecNum,
+) -> DecNum {
match value {
- Num::Int(n) => n,
- Num::Float(n) => n.trunc() as i64,
+ DecNum::Int(n) => DecNum::Int(n),
+ DecNum::Float(n) => DecNum::Float(n.trunc()),
+ DecNum::Decimal(n) => DecNum::Decimal(n.trunc()),
}
}
@@ -660,16 +704,18 @@ pub fn trunc(
///
/// ```example
/// #assert(calc.fract(3) == 0)
+/// #assert(calc.fract(decimal("234.23949211")) == decimal("0.23949211"))
/// #calc.fract(-3.1)
/// ```
#[func(title = "Fractional")]
pub fn fract(
/// The number to truncate.
- value: Num,
-) -> Num {
+ value: DecNum,
+) -> DecNum {
match value {
- Num::Int(_) => Num::Int(0),
- Num::Float(n) => Num::Float(n.fract()),
+ DecNum::Int(_) => DecNum::Int(0),
+ DecNum::Float(n) => DecNum::Float(n.fract()),
+ DecNum::Decimal(n) => DecNum::Decimal(n.fract()),
}
}
@@ -677,27 +723,37 @@ pub fn fract(
///
/// Optionally, a number of decimal places can be specified.
///
+/// Note that this function will return the same type as the operand. That is,
+/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`],
+/// another `decimal`. You may explicitly convert the output of this function to
+/// an integer with [`int`], but note that such a conversion will error if the
+/// `float` or `decimal` is larger than the maximum 64-bit signed integer or
+/// smaller than the minimum integer.
+///
/// ```example
+/// #assert(calc.round(3) == 3)
/// #assert(calc.round(3.14) == 3)
-/// #assert(calc.round(3.5) == 4)
+/// #assert(calc.round(3.5) == 4.0)
+/// #assert(calc.round(decimal("-6.5")) == decimal("-7"))
+/// #assert(calc.round(decimal("7.123456789"), digits: 6) == decimal("7.123457"))
/// #calc.round(3.1415, digits: 2)
/// ```
#[func]
pub fn round(
/// The number to round.
- value: Num,
- /// The number of decimal places.
+ value: DecNum,
+ /// The number of decimal places. Must not be negative.
#[named]
#[default(0)]
- digits: i64,
-) -> Num {
+ digits: u32,
+) -> DecNum {
match value {
- Num::Int(n) if digits == 0 => Num::Int(n),
- _ => {
- let n = value.float();
- let factor = 10.0_f64.powi(digits as i32);
- Num::Float((n * factor).round() / factor)
- }
+ DecNum::Int(n) => DecNum::Int(n),
+ DecNum::Float(n) => DecNum::Float(crate::utils::format::round_with_precision(
+ n,
+ digits.saturating_as::<u8>(),
+ )),
+ DecNum::Decimal(n) => DecNum::Decimal(n.round(digits)),
}
}
@@ -706,21 +762,35 @@ pub fn round(
/// ```example
/// #assert(calc.clamp(5, 0, 10) == 5)
/// #assert(calc.clamp(5, 6, 10) == 6)
+/// #assert(calc.clamp(decimal("5.45"), 2, decimal("45.9")) == decimal("5.45"))
+/// #assert(calc.clamp(decimal("5.45"), decimal("6.75"), 12) == decimal("6.75"))
/// #calc.clamp(5, 0, 4)
/// ```
#[func]
pub fn clamp(
+ /// The callsite span.
+ span: Span,
/// The number to clamp.
- value: Num,
+ value: DecNum,
/// The inclusive minimum value.
- min: Num,
+ min: DecNum,
/// The inclusive maximum value.
- max: Spanned<Num>,
-) -> SourceResult<Num> {
- if max.v.float() < min.float() {
+ max: Spanned<DecNum>,
+) -> SourceResult<DecNum> {
+ // Ignore if there are incompatible types (decimal and float) since that
+ // will cause `apply3` below to error before calling clamp, avoiding a
+ // panic.
+ if min
+ .apply2(max.v, |min, max| max < min, |min, max| max < min, |min, max| max < min)
+ .unwrap_or(false)
+ {
bail!(max.span, "max must be greater than or equal to min")
}
- Ok(value.apply3(min, max.v, i64::clamp, f64::clamp))
+
+ value
+ .apply3(min, max.v, i64::clamp, f64::clamp, Decimal::clamp)
+ .ok_or_else(cant_apply_to_decimal_and_float)
+ .at(span)
}
/// Determines the minimum of a sequence of values.
@@ -815,6 +885,9 @@ pub fn odd(
/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller
/// in magnitude than `y`.
///
+/// This can error if given a [`decimal`] input and the dividend is too small in
+/// magnitude compared to the divisor.
+///
/// ```example
/// #calc.rem(7, 3) \
/// #calc.rem(7, -3) \
@@ -824,15 +897,28 @@ pub fn odd(
/// ```
#[func(title = "Remainder")]
pub fn rem(
+ /// The span of the function call.
+ span: Span,
/// The dividend of the remainder.
- dividend: Num,
+ dividend: DecNum,
/// The divisor of the remainder.
- divisor: Spanned<Num>,
-) -> SourceResult<Num> {
- if divisor.v.float() == 0.0 {
+ divisor: Spanned<DecNum>,
+) -> SourceResult<DecNum> {
+ if divisor.v.is_zero() {
bail!(divisor.span, "divisor must not be zero");
}
- Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem))
+
+ dividend
+ .apply2(
+ divisor.v,
+ |a, b| Some(DecNum::Int(a % b)),
+ |a, b| Some(DecNum::Float(a % b)),
+ |a, b| a.checked_rem(b).map(DecNum::Decimal),
+ )
+ .ok_or_else(cant_apply_to_decimal_and_float)
+ .at(span)?
+ .ok_or("dividend too small compared to divisor")
+ .at(span)
}
/// Performs euclidean division of two numbers.
@@ -845,45 +931,77 @@ pub fn rem(
/// #calc.div-euclid(7, -3) \
/// #calc.div-euclid(-7, 3) \
/// #calc.div-euclid(-7, -3) \
-/// #calc.div-euclid(1.75, 0.5)
+/// #calc.div-euclid(1.75, 0.5) \
+/// #calc.div-euclid(decimal("1.75"), decimal("0.5"))
/// ```
#[func(title = "Euclidean Division")]
pub fn div_euclid(
+ /// The callsite span.
+ span: Span,
/// The dividend of the division.
- dividend: Num,
+ dividend: DecNum,
/// The divisor of the division.
- divisor: Spanned<Num>,
-) -> SourceResult<Num> {
- if divisor.v.float() == 0.0 {
+ divisor: Spanned<DecNum>,
+) -> SourceResult<DecNum> {
+ if divisor.v.is_zero() {
bail!(divisor.span, "divisor must not be zero");
}
- Ok(dividend.apply2(divisor.v, i64::div_euclid, f64::div_euclid))
+
+ dividend
+ .apply2(
+ divisor.v,
+ |a, b| Some(DecNum::Int(a.div_euclid(b))),
+ |a, b| Some(DecNum::Float(a.div_euclid(b))),
+ |a, b| a.checked_div_euclid(b).map(DecNum::Decimal),
+ )
+ .ok_or_else(cant_apply_to_decimal_and_float)
+ .at(span)?
+ .ok_or_else(too_large)
+ .at(span)
}
/// This calculates the least nonnegative remainder of a division.
///
-/// Warning: Due to a floating point round-off error, the remainder may equal the absolute
-/// value of the divisor if the dividend is much smaller in magnitude than the divisor
-/// and the dividend is negative. This only applies for floating point inputs.
+/// Warning: Due to a floating point round-off error, the remainder may equal
+/// the absolute value of the divisor if the dividend is much smaller in
+/// magnitude than the divisor and the dividend is negative. This only applies
+/// for floating point inputs.
+///
+/// In addition, this can error if given a [`decimal`] input and the dividend is
+/// too small in magnitude compared to the divisor.
///
/// ```example
/// #calc.rem-euclid(7, 3) \
/// #calc.rem-euclid(7, -3) \
/// #calc.rem-euclid(-7, 3) \
/// #calc.rem-euclid(-7, -3) \
-/// #calc.rem(1.75, 0.5)
+/// #calc.rem-euclid(1.75, 0.5)
+/// #calc.rem-euclid(decimal("1.75"), decimal("0.5"))
/// ```
#[func(title = "Euclidean Remainder")]
pub fn rem_euclid(
+ /// The callsite span.
+ span: Span,
/// The dividend of the remainder.
- dividend: Num,
+ dividend: DecNum,
/// The divisor of the remainder.
- divisor: Spanned<Num>,
-) -> SourceResult<Num> {
- if divisor.v.float() == 0.0 {
+ divisor: Spanned<DecNum>,
+) -> SourceResult<DecNum> {
+ if divisor.v.is_zero() {
bail!(divisor.span, "divisor must not be zero");
}
- Ok(dividend.apply2(divisor.v, i64::rem_euclid, f64::rem_euclid))
+
+ dividend
+ .apply2(
+ divisor.v,
+ |a, b| Some(DecNum::Int(a.rem_euclid(b))),
+ |a, b| Some(DecNum::Float(a.rem_euclid(b))),
+ |a, b| a.checked_rem_euclid(b).map(DecNum::Decimal),
+ )
+ .ok_or_else(cant_apply_to_decimal_and_float)
+ .at(span)?
+ .ok_or("dividend too small compared to divisor")
+ .at(span)
}
/// Calculates the quotient (floored division) of two numbers.
@@ -895,16 +1013,30 @@ pub fn rem_euclid(
/// ```
#[func(title = "Quotient")]
pub fn quo(
+ /// The span of the function call.
+ span: Span,
/// The dividend of the quotient.
- dividend: Num,
+ dividend: DecNum,
/// The divisor of the quotient.
- divisor: Spanned<Num>,
-) -> SourceResult<i64> {
- if divisor.v.float() == 0.0 {
+ divisor: Spanned<DecNum>,
+) -> SourceResult<DecNum> {
+ if divisor.v.is_zero() {
bail!(divisor.span, "divisor must not be zero");
}
- Ok(floor(dividend.apply2(divisor.v, Div::div, Div::div)))
+ let divided = dividend
+ .apply2(
+ divisor.v,
+ |a, b| Some(DecNum::Int(a / b)),
+ |a, b| Some(DecNum::Float(a / b)),
+ |a, b| a.checked_div(b).map(DecNum::Decimal),
+ )
+ .ok_or_else(cant_apply_to_decimal_and_float)
+ .at(span)?
+ .ok_or_else(too_large)
+ .at(span)?;
+
+ Ok(floor(divided))
}
/// A value which can be passed to functions that work with integers and floats.
@@ -915,47 +1047,119 @@ pub enum Num {
}
impl Num {
- fn apply2(
+ fn float(self) -> f64 {
+ match self {
+ Self::Int(v) => v as f64,
+ Self::Float(v) => v,
+ }
+ }
+}
+
+cast! {
+ Num,
+ self => match self {
+ Self::Int(v) => v.into_value(),
+ Self::Float(v) => v.into_value(),
+ },
+ v: i64 => Self::Int(v),
+ v: f64 => Self::Float(v),
+}
+
+/// A value which can be passed to functions that work with integers, floats,
+/// and decimals.
+#[derive(Debug, Copy, Clone)]
+pub enum DecNum {
+ Int(i64),
+ Float(f64),
+ Decimal(Decimal),
+}
+
+impl DecNum {
+ /// Checks if this number is equivalent to zero.
+ fn is_zero(self) -> bool {
+ match self {
+ Self::Int(i) => i == 0,
+ Self::Float(f) => f == 0.0,
+ Self::Decimal(d) => d.is_zero(),
+ }
+ }
+
+ /// If this `DecNum` holds an integer or float, returns a float.
+ /// Otherwise, returns `None`.
+ fn float(self) -> Option<f64> {
+ match self {
+ Self::Int(i) => Some(i as f64),
+ Self::Float(f) => Some(f),
+ Self::Decimal(_) => None,
+ }
+ }
+
+ /// If this `DecNum` holds an integer or decimal, returns a decimal.
+ /// Otherwise, returns `None`.
+ fn decimal(self) -> Option<Decimal> {
+ match self {
+ Self::Int(i) => Some(Decimal::from(i)),
+ Self::Float(_) => None,
+ Self::Decimal(d) => Some(d),
+ }
+ }
+
+ /// Tries to apply a function to two decimal or numeric arguments.
+ ///
+ /// Fails with `None` if one is a float and the other is a decimal.
+ fn apply2<T>(
self,
other: Self,
- int: impl FnOnce(i64, i64) -> i64,
- float: impl FnOnce(f64, f64) -> f64,
- ) -> Num {
+ int: impl FnOnce(i64, i64) -> T,
+ float: impl FnOnce(f64, f64) -> T,
+ decimal: impl FnOnce(Decimal, Decimal) -> T,
+ ) -> Option<T> {
match (self, other) {
- (Self::Int(a), Self::Int(b)) => Num::Int(int(a, b)),
- (a, b) => Num::Float(float(a.float(), b.float())),
+ (Self::Int(a), Self::Int(b)) => Some(int(a, b)),
+ (Self::Decimal(a), Self::Decimal(b)) => Some(decimal(a, b)),
+ (Self::Decimal(a), Self::Int(b)) => Some(decimal(a, Decimal::from(b))),
+ (Self::Int(a), Self::Decimal(b)) => Some(decimal(Decimal::from(a), b)),
+ (a, b) => Some(float(a.float()?, b.float()?)),
}
}
+ /// Tries to apply a function to three decimal or numeric arguments.
+ ///
+ /// Fails with `None` if one is a float and the other is a decimal.
fn apply3(
self,
other: Self,
third: Self,
int: impl FnOnce(i64, i64, i64) -> i64,
float: impl FnOnce(f64, f64, f64) -> f64,
- ) -> Num {
+ decimal: impl FnOnce(Decimal, Decimal, Decimal) -> Decimal,
+ ) -> Option<Self> {
match (self, other, third) {
- (Self::Int(a), Self::Int(b), Self::Int(c)) => Num::Int(int(a, b, c)),
- (a, b, c) => Num::Float(float(a.float(), b.float(), c.float())),
- }
- }
-
- fn float(self) -> f64 {
- match self {
- Self::Int(v) => v as f64,
- Self::Float(v) => v,
+ (Self::Int(a), Self::Int(b), Self::Int(c)) => Some(Self::Int(int(a, b, c))),
+ (Self::Decimal(a), b, c) => {
+ Some(Self::Decimal(decimal(a, b.decimal()?, c.decimal()?)))
+ }
+ (a, Self::Decimal(b), c) => {
+ Some(Self::Decimal(decimal(a.decimal()?, b, c.decimal()?)))
+ }
+ (a, b, Self::Decimal(c)) => {
+ Some(Self::Decimal(decimal(a.decimal()?, b.decimal()?, c)))
+ }
+ (a, b, c) => Some(Self::Float(float(a.float()?, b.float()?, c.float()?))),
}
}
}
cast! {
- Num,
+ DecNum,
self => match self {
Self::Int(v) => v.into_value(),
Self::Float(v) => v.into_value(),
+ Self::Decimal(v) => v.into_value(),
},
v: i64 => Self::Int(v),
v: f64 => Self::Float(v),
+ v: Decimal => Self::Decimal(v),
}
/// A value that can be passed to a trigonometric function.
@@ -977,3 +1181,14 @@ cast! {
fn too_large() -> &'static str {
"the result is too large"
}
+
+/// The hinted error message when trying to apply an operation to decimal and
+/// float operands.
+#[cold]
+fn cant_apply_to_decimal_and_float() -> HintedString {
+ HintedString::new("cannot apply this operation to a decimal and a float".into())
+ .with_hint(
+ "if loss of precision is acceptable, explicitly cast the \
+ decimal to a float with `float(value)`",
+ )
+}
diff --git a/crates/typst/src/foundations/cast.rs b/crates/typst/src/foundations/cast.rs
index 51e36128..2dc69236 100644
--- a/crates/typst/src/foundations/cast.rs
+++ b/crates/typst/src/foundations/cast.rs
@@ -345,6 +345,13 @@ impl CastInfo {
msg.hint(eco_format!("use `label({})` to create a label", s.repr()));
}
}
+ } else if let Value::Decimal(_) = found {
+ if !matching_type && parts.iter().any(|p| p == "float") {
+ msg.hint(eco_format!(
+ "if loss of precision is acceptable, explicitly cast the \
+ decimal to a float with `float(value)`"
+ ));
+ }
}
msg
diff --git a/crates/typst/src/foundations/decimal.rs b/crates/typst/src/foundations/decimal.rs
new file mode 100644
index 00000000..12d6499f
--- /dev/null
+++ b/crates/typst/src/foundations/decimal.rs
@@ -0,0 +1,369 @@
+use std::fmt::{self, Display, Formatter};
+use std::ops::Neg;
+use std::str::FromStr;
+
+use ecow::{eco_format, EcoString};
+use rust_decimal::MathematicalOps;
+
+use crate::diag::{warning, At, SourceResult};
+use crate::foundations::{cast, func, repr, scope, ty, Engine, Repr, Str};
+use crate::syntax::{ast, Span, Spanned};
+use crate::World;
+
+/// A fixed-point decimal number type.
+///
+/// This type should be used when highly precise arithmetic operations are
+/// needed, such as for finance. Typical operations between `{decimal}`
+/// numbers, such as addition, multiplication, and [power]($calc.pow) to an
+/// integer, will be highly precise due to their fixed-point representation.
+/// Note, however, that multiplication and division may not preserve all digits
+/// in some edge cases: while they are considered precise, digits past the
+/// limits specified below are rounded off and lost, so some loss of precision
+/// beyond the maximum representable digits is possible. Note that this
+/// behavior can be observed not only when dividing, but also when multiplying
+/// by numbers between 0 and 1, as both operations can push a number's
+/// fractional digits beyond the limits described below, leading to rounding.
+/// When those two operations do not surpass the digit limits, they are fully
+/// precise.
+///
+/// # Limits
+/// A `{decimal}` number has a limit of 28 to 29 significant base-10 digits.
+/// This includes the sum of digits before and after the decimal point. As
+/// such, numbers with more fractional digits have a smaller range. The maximum
+/// and minimum `{decimal}` numbers have a value of
+/// `{79228162514264337593543950335}` and `{-79228162514264337593543950335}`
+/// respectively. In contrast with [`{float}`]($float), this type does not
+/// support infinity or NaN, so overflowing or underflowing operations will
+/// raise an error.
+///
+/// # Construction and casts
+/// To create a decimal number, use the `{decimal(string)}` constructor, such
+/// as with `{decimal("3.141592653")}` **(note the double quotes!)**. This
+/// constructor preserves all given fractional digits, provided they are
+/// representable as per the limits above (otherwise, an error is raised). One
+/// may also convert any [integer]($int) to a decimal with the
+/// `{decimal(int)}` constructor, e.g. `{decimal(59)}`. However, note that
+/// constructing a decimal from a [floating-point number]($float), while
+/// supported, **is an imprecise conversion and therefore discouraged.** A
+/// warning will be raised if Typst detects that there was an accidental
+/// `{float}` to `{decimal}` cast through its constructor (e.g. if writing
+/// `{decimal(3.14)}` - note the lack of double quotes, indicating this is
+/// an accidental `{float}` cast and therefore imprecise). The precision of a
+/// `{float}` to `{decimal}` cast can be slightly improved by rounding the
+/// result to 15 digits with [`calc.round`]($calc.round), but there are still
+/// no precision guarantees for that kind of conversion.
+///
+/// In order to guard against accidental loss of precision, built-in operations
+/// between `{float}` and `{decimal}` are not supported and will raise an
+/// error. Certain `calc` functions, such as trigonometric functions and power
+/// between two real numbers, are also only supported for `{float}` (although
+/// raising `{decimal}` to integer exponents is supported). You can opt into
+/// potentially imprecise operations with the `{float(decimal)}` constructor,
+/// which casts the `{decimal}` number into a `{float}`, allowing for
+/// operations without precision guarantees.
+///
+/// # Example
+/// ```example
+/// #decimal("3.14159265358979323846264338") \
+/// #(decimal("0.000000000000000000001") + decimal("0.000000000000000000002"))
+/// #(decimal("0.00002") * decimal("49.25652565")) \
+/// #(decimal("1") / 2048)
+/// ```
+#[ty(scope, cast)]
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Decimal(rust_decimal::Decimal);
+
+impl Decimal {
+ pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO);
+ pub const ONE: Self = Self(rust_decimal::Decimal::ONE);
+
+ /// Whether this decimal value is zero.
+ pub const fn is_zero(self) -> bool {
+ self.0.is_zero()
+ }
+
+ /// Whether this decimal value is negative.
+ pub const fn is_negative(self) -> bool {
+ self.0.is_sign_negative()
+ }
+
+ /// Whether this decimal has fractional part equal to zero (is an integer).
+ pub fn is_integer(self) -> bool {
+ self.0.is_integer()
+ }
+
+ /// Computes the absolute value of this decimal.
+ pub fn abs(self) -> Self {
+ Self(self.0.abs())
+ }
+
+ /// Computes the largest integer less than or equal to this decimal.
+ ///
+ /// A decimal is returned as this may not be within `i64`'s range of
+ /// values.
+ pub fn floor(self) -> Self {
+ Self(self.0.floor())
+ }
+
+ /// Computes the smallest integer greater than or equal to this decimal.
+ ///
+ /// A decimal is returned as this may not be within `i64`'s range of
+ /// values.
+ pub fn ceil(self) -> Self {
+ Self(self.0.ceil())
+ }
+
+ /// Returns the integer part of this decimal.
+ pub fn trunc(self) -> Self {
+ Self(self.0.trunc())
+ }
+
+ /// Returns the fractional part of this decimal (with the integer part set
+ /// to zero).
+ pub fn fract(self) -> Self {
+ Self(self.0.fract())
+ }
+
+ /// Rounds this decimal up to the specified amount of digits with the
+ /// traditional rounding rules, using the "midpoint away from zero"
+ /// strategy (6.5 -> 7, -6.5 -> -7).
+ pub fn round(self, digits: u32) -> Self {
+ Self(self.0.round_dp_with_strategy(
+ digits,
+ rust_decimal::RoundingStrategy::MidpointAwayFromZero,
+ ))
+ }
+
+ /// Attempts to add two decimals.
+ ///
+ /// Returns `None` on overflow or underflow.
+ pub fn checked_add(self, other: Self) -> Option<Self> {
+ self.0.checked_add(other.0).map(Self)
+ }
+
+ /// Attempts to subtract a decimal from another.
+ ///
+ /// Returns `None` on overflow or underflow.
+ pub fn checked_sub(self, other: Self) -> Option<Self> {
+ self.0.checked_sub(other.0).map(Self)
+ }
+
+ /// Attempts to multiply two decimals.
+ ///
+ /// Returns `None` on overflow or underflow.
+ pub fn checked_mul(self, other: Self) -> Option<Self> {
+ self.0.checked_mul(other.0).map(Self)
+ }
+
+ /// Attempts to divide two decimals.
+ ///
+ /// Returns `None` if `other` is zero, as well as on overflow or underflow.
+ pub fn checked_div(self, other: Self) -> Option<Self> {
+ self.0.checked_div(other.0).map(Self)
+ }
+
+ /// Attempts to obtain the quotient of Euclidean division between two
+ /// decimals. Implemented similarly to [`f64::div_euclid`].
+ ///
+ /// The returned quotient is truncated and adjusted if the remainder was
+ /// negative.
+ ///
+ /// Returns `None` if `other` is zero, as well as on overflow or underflow.
+ pub fn checked_div_euclid(self, other: Self) -> Option<Self> {
+ let q = self.0.checked_div(other.0)?.trunc();
+ if self
+ .0
+ .checked_rem(other.0)
+ .as_ref()
+ .is_some_and(rust_decimal::Decimal::is_sign_negative)
+ {
+ return if other.0.is_sign_positive() {
+ q.checked_sub(rust_decimal::Decimal::ONE).map(Self)
+ } else {
+ q.checked_add(rust_decimal::Decimal::ONE).map(Self)
+ };
+ }
+ Some(Self(q))
+ }
+
+ /// Attempts to obtain the remainder of Euclidean division between two
+ /// decimals. Implemented similarly to [`f64::rem_euclid`].
+ ///
+ /// The returned decimal `r` is non-negative within the range
+ /// `0.0 <= r < other.abs()`.
+ ///
+ /// Returns `None` if `other` is zero, as well as on overflow or underflow.
+ pub fn checked_rem_euclid(self, other: Self) -> Option<Self> {
+ let r = self.0.checked_rem(other.0)?;
+ Some(Self(if r.is_sign_negative() { r.checked_add(other.0.abs())? } else { r }))
+ }
+
+ /// Attempts to calculate the remainder of the division of two decimals.
+ ///
+ /// Returns `None` if `other` is zero, as well as on overflow or underflow.
+ pub fn checked_rem(self, other: Self) -> Option<Self> {
+ self.0.checked_rem(other.0).map(Self)
+ }
+
+ /// Attempts to take one decimal to the power of an integer.
+ ///
+ /// Returns `None` for invalid operands, as well as on overflow or
+ /// underflow.
+ pub fn checked_powi(self, other: i64) -> Option<Self> {
+ self.0.checked_powi(other).map(Self)
+ }
+}
+
+#[scope]
+impl Decimal {
+ /// Converts a value to a `{decimal}`.
+ ///
+ /// It is recommended to use a string to construct the decimal number, or
+ /// an [integer]($int) (if desired). The string must contain a number in
+ /// the format `"3.14159"` (or `"-3.141519"` for negative numbers). The
+ /// fractional digits are fully preserved; if that's not possible due to
+ /// the limit of significant digits (around 28 to 29) having been reached,
+ /// an error is raised as the given decimal number wouldn't be
+ /// representable. For example, `{decimal("1.222222222222222")}` is a valid
+ /// decimal number.
+ ///
+ /// While this constructor can be used with
+ /// [floating-point numbers]($float) to cast them to `{decimal}`, doing so
+ /// is **discouraged** as **this cast is inherently imprecise.** It is easy
+ /// to accidentally perform this cast by writing `{decimal(1.234)}` (note
+ /// the lack of double quotes), which is why Typst will emit a warning in
+ /// that case. Please write `{decimal("1.234")}` instead for that
+ /// particular case (initialization of a constant decimal). Also note that
+ /// floats equal to NaN and infinity cannot be cast to decimals and will
+ /// raise an error.
+ #[func(constructor)]
+ pub fn construct(
+ engine: &mut Engine,
+ value: Spanned<ToDecimal>,
+ ) -> SourceResult<Decimal> {
+ match value.v {
+ ToDecimal::Str(str) => Self::from_str(&str.replace(repr::MINUS_SIGN, "-"))
+ .map_err(|_| eco_format!("invalid decimal: {str}"))
+ .at(value.span),
+ ToDecimal::Int(int) => Ok(Self::from(int)),
+ ToDecimal::Float(float) => {
+ warn_on_float_literal(engine, value.span);
+ Self::try_from(float)
+ .map_err(|_| {
+ eco_format!(
+ "float is not a valid decimal: {}",
+ repr::format_float(float, None, true, "")
+ )
+ })
+ .at(value.span)
+ }
+ }
+ }
+}
+
+/// Emits a warning when a decimal is constructed from a float literal.
+fn warn_on_float_literal(engine: &mut Engine, span: Span) -> Option<()> {
+ let id = span.id()?;
+ let source = engine.world.source(id).ok()?;
+ let node = source.find(span)?;
+ if node.is::<ast::Float>() {
+ engine.sink.warn(warning!(
+ span,
+ "creating a decimal using imprecise float literal";
+ hint: "use a string in the decimal constructor to avoid loss \
+ of precision: `decimal({})`",
+ node.text().repr()
+ ));
+ }
+ Some(())
+}
+
+impl FromStr for Decimal {
+ type Err = rust_decimal::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ rust_decimal::Decimal::from_str_exact(s).map(Self)
+ }
+}
+
+impl From<i64> for Decimal {
+ fn from(value: i64) -> Self {
+ Self(rust_decimal::Decimal::from(value))
+ }
+}
+
+impl TryFrom<f64> for Decimal {
+ type Error = ();
+
+ /// Attempts to convert a Decimal to a float.
+ ///
+ /// This can fail if the float is infinite or NaN, or otherwise cannot be
+ /// represented by a decimal number.
+ fn try_from(value: f64) -> Result<Self, Self::Error> {
+ rust_decimal::Decimal::from_f64_retain(value).map(Self).ok_or(())
+ }
+}
+
+impl TryFrom<Decimal> for f64 {
+ type Error = rust_decimal::Error;
+
+ /// Attempts to convert a Decimal to a float.
+ ///
+ /// This should in principle be infallible according to the implementation,
+ /// but we mirror the decimal implementation's API either way.
+ fn try_from(value: Decimal) -> Result<Self, Self::Error> {
+ value.0.try_into()
+ }
+}
+
+impl TryFrom<Decimal> for i64 {
+ type Error = rust_decimal::Error;
+
+ /// Attempts to convert a Decimal to an integer.
+ ///
+ /// Returns an error if the decimal has a fractional part, or if there
+ /// would be overflow or underflow.
+ fn try_from(value: Decimal) -> Result<Self, Self::Error> {
+ value.0.try_into()
+ }
+}
+
+impl Display for Decimal {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ if self.0.is_sign_negative() {
+ f.write_str(repr::MINUS_SIGN)?;
+ }
+ self.0.abs().fmt(f)
+ }
+}
+
+impl Repr for Decimal {
+ fn repr(&self) -> EcoString {
+ eco_format!("decimal({})", eco_format!("{}", self.0).repr())
+ }
+}
+
+impl Neg for Decimal {
+ type Output = Self;
+
+ fn neg(self) -> Self {
+ Self(-self.0)
+ }
+}
+
+/// A value that can be cast to a decimal.
+pub enum ToDecimal {
+ /// A string with the decimal's representation.
+ Str(EcoString),
+ /// An integer to be converted to the equivalent decimal.
+ Int(i64),
+ /// A float to be converted to the equivalent decimal.
+ Float(f64),
+}
+
+cast! {
+ ToDecimal,
+ v: i64 => Self::Int(v),
+ v: f64 => Self::Float(v),
+ v: Str => Self::Str(EcoString::from(v)),
+}
diff --git a/crates/typst/src/foundations/float.rs b/crates/typst/src/foundations/float.rs
index ff3e62a5..8318641a 100644
--- a/crates/typst/src/foundations/float.rs
+++ b/crates/typst/src/foundations/float.rs
@@ -4,7 +4,7 @@ use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::foundations::{
- bail, cast, func, repr, scope, ty, Bytes, Endianness, Repr, Str,
+ bail, cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str,
};
use crate::layout::Ratio;
@@ -177,6 +177,7 @@ cast! {
v: f64 => Self(v),
v: bool => Self(v as i64 as f64),
v: i64 => Self(v as f64),
+ v: Decimal => Self(f64::try_from(v).map_err(|_| eco_format!("invalid float: {}", v))?),
v: Ratio => Self(v.get()),
v: Str => Self(
parse_float(v.clone().into())
diff --git a/crates/typst/src/foundations/int.rs b/crates/typst/src/foundations/int.rs
index 79a8a86f..1f29c2a6 100644
--- a/crates/typst/src/foundations/int.rs
+++ b/crates/typst/src/foundations/int.rs
@@ -4,7 +4,7 @@ use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::foundations::{
- bail, cast, func, repr, scope, ty, Bytes, Cast, Repr, Str, Value,
+ bail, cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value,
};
/// A whole number.
@@ -33,16 +33,19 @@ type i64;
#[scope]
impl i64 {
- /// Converts a value to an integer.
+ /// Converts a value to an integer. Raises an error if there is an attempt
+ /// to produce an integer larger than the maximum 64-bit signed integer
+ /// or smaller than the minimum 64-bit signed integer.
///
/// - Booleans are converted to `0` or `1`.
- /// - Floats are floored to the next 64-bit integer.
+ /// - Floats and decimals are truncated to the next 64-bit integer.
/// - Strings are parsed in base 10.
///
/// ```example
/// #int(false) \
/// #int(true) \
/// #int(2.7) \
+ /// #int(decimal("3.8")) \
/// #(int("27") + int("4"))
/// ```
#[func(constructor)]
@@ -360,10 +363,19 @@ cast! {
ToInt,
v: i64 => Self(v),
v: bool => Self(v as i64),
- v: f64 => Self(v as i64),
+ v: f64 => Self(convert_float_to_int(v)?),
+ v: Decimal => Self(i64::try_from(v).map_err(|_| eco_format!("number too large"))?),
v: Str => Self(parse_int(&v).map_err(|_| eco_format!("invalid integer: {}", v))?),
}
+fn convert_float_to_int(f: f64) -> StrResult<i64> {
+ if f <= i64::MIN as f64 - 1.0 || f >= i64::MAX as f64 + 1.0 {
+ Err(eco_format!("number too large"))
+ } else {
+ Ok(f as i64)
+ }
+}
+
fn parse_int(mut s: &str) -> Result<i64, ParseIntError> {
let mut sign = 1;
if let Some(rest) = s.strip_prefix('-').or_else(|| s.strip_prefix(repr::MINUS_SIGN)) {
diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst/src/foundations/mod.rs
index 2e1f5c82..dc939932 100644
--- a/crates/typst/src/foundations/mod.rs
+++ b/crates/typst/src/foundations/mod.rs
@@ -15,6 +15,7 @@ mod cast;
mod content;
mod context;
mod datetime;
+mod decimal;
mod dict;
mod duration;
mod element;
@@ -43,6 +44,7 @@ pub use self::cast::*;
pub use self::content::*;
pub use self::context::*;
pub use self::datetime::*;
+pub use self::decimal::*;
pub use self::dict::*;
pub use self::duration::*;
pub use self::element::*;
@@ -105,6 +107,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) {
global.define_type::<Regex>();
global.define_type::<Selector>();
global.define_type::<Datetime>();
+ global.define_type::<Decimal>();
global.define_type::<Duration>();
global.define_type::<Version>();
global.define_type::<Plugin>();
diff --git a/crates/typst/src/foundations/str.rs b/crates/typst/src/foundations/str.rs
index 61116d85..6091fb2e 100644
--- a/crates/typst/src/foundations/str.rs
+++ b/crates/typst/src/foundations/str.rs
@@ -11,8 +11,8 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
- cast, dict, func, repr, scope, ty, Array, Bytes, Context, Dict, Func, IntoValue,
- Label, Repr, Type, Value, Version,
+ cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func,
+ IntoValue, Label, Repr, Type, Value, Version,
};
use crate::layout::Alignment;
use crate::syntax::{Span, Spanned};
@@ -775,6 +775,7 @@ cast! {
ToStr,
v: i64 => Self::Int(v),
v: f64 => Self::Str(repr::display_float(v).into()),
+ v: Decimal => Self::Str(format_str!("{}", v)),
v: Version => Self::Str(format_str!("{}", v)),
v: Bytes => Self::Str(
std::str::from_utf8(&v)
diff --git a/crates/typst/src/foundations/value.rs b/crates/typst/src/foundations/value.rs
index 78769900..cabe5647 100644
--- a/crates/typst/src/foundations/value.rs
+++ b/crates/typst/src/foundations/value.rs
@@ -12,9 +12,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::diag::{HintedStrResult, HintedString, StrResult};
use crate::eval::ops;
use crate::foundations::{
- fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Dict,
- Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType,
- NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type, Version,
+ fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal,
+ Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement,
+ NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type,
+ Version,
};
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::symbols::Symbol;
@@ -65,6 +66,8 @@ pub enum Value {
Label(Label),
/// A datetime
Datetime(Datetime),
+ /// A decimal value: `decimal("123.4500")`
+ Decimal(Decimal),
/// A duration
Duration(Duration),
/// A content value: `[*Hi* there]`.
@@ -136,6 +139,7 @@ impl Value {
Self::Bytes(_) => Type::of::<Bytes>(),
Self::Label(_) => Type::of::<Label>(),
Self::Datetime(_) => Type::of::<Datetime>(),
+ Self::Decimal(_) => Type::of::<Decimal>(),
Self::Duration(_) => Type::of::<Duration>(),
Self::Content(_) => Type::of::<Content>(),
Self::Styles(_) => Type::of::<Styles>(),
@@ -204,6 +208,7 @@ impl Value {
Self::None => Content::empty(),
Self::Int(v) => TextElem::packed(repr::format_int_with_base(v, 10)),
Self::Float(v) => TextElem::packed(repr::display_float(v)),
+ Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
Self::Str(v) => TextElem::packed(v),
Self::Version(v) => TextElem::packed(eco_format!("{v}")),
Self::Symbol(v) => TextElem::packed(v.get()),
@@ -248,6 +253,7 @@ impl Debug for Value {
Self::Bytes(v) => Debug::fmt(v, f),
Self::Label(v) => Debug::fmt(v, f),
Self::Datetime(v) => Debug::fmt(v, f),
+ Self::Decimal(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),
@@ -285,6 +291,7 @@ impl Repr for Value {
Self::Bytes(v) => v.repr(),
Self::Label(v) => v.repr(),
Self::Datetime(v) => v.repr(),
+ Self::Decimal(v) => v.repr(),
Self::Duration(v) => v.repr(),
Self::Content(v) => v.repr(),
Self::Styles(v) => v.repr(),
@@ -337,6 +344,7 @@ impl Hash for Value {
Self::Content(v) => v.hash(state),
Self::Styles(v) => v.hash(state),
Self::Datetime(v) => v.hash(state),
+ Self::Decimal(v) => v.hash(state),
Self::Duration(v) => v.hash(state),
Self::Array(v) => v.hash(state),
Self::Dict(v) => v.hash(state),
@@ -645,6 +653,7 @@ primitive! {
primitive! { Bytes: "bytes", Bytes }
primitive! { Label: "label", Label }
primitive! { Datetime: "datetime", Datetime }
+primitive! { Decimal: "decimal", Decimal }
primitive! { Duration: "duration", Duration }
primitive! { Content: "content",
Content,