diff options
22 files changed, 1087 insertions, 148 deletions
@@ -2028,6 +2028,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2666,6 +2676,7 @@ dependencies = [ "rayon", "regex", "roxmltree", + "rust_decimal", "rustybuzz", "serde", "serde_json", @@ -90,6 +90,7 @@ rayon = "1.7.0" regex = "1" resvg = { version = "0.43", default-features = false, features = ["raster-images"] } roxmltree = "0.20" +rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] } rustybuzz = "0.18" same-file = "1" self-replace = "1.3.7" 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, diff --git a/tests/ref/decimal-display-round.png b/tests/ref/decimal-display-round.png Binary files differnew file mode 100644 index 00000000..506a5b88 --- /dev/null +++ b/tests/ref/decimal-display-round.png diff --git a/tests/ref/decimal-display.png b/tests/ref/decimal-display.png Binary files differnew file mode 100644 index 00000000..9109b195 --- /dev/null +++ b/tests/ref/decimal-display.png diff --git a/tests/ref/float-repr.png b/tests/ref/float-repr.png Binary files differdeleted file mode 100644 index e73ee328..00000000 --- a/tests/ref/float-repr.png +++ /dev/null diff --git a/tests/ref/int-repr.png b/tests/ref/int-repr.png Binary files differdeleted file mode 100644 index a2ee4ee0..00000000 --- a/tests/ref/int-repr.png +++ /dev/null diff --git a/tests/suite/foundations/calc.typ b/tests/suite/foundations/calc.typ index e702be9f..18cfa484 100644 --- a/tests/suite/foundations/calc.typ +++ b/tests/suite/foundations/calc.typ @@ -1,6 +1,21 @@ --- calc-round --- #test(calc.round(calc.e, digits: 2), 2.72) #test(calc.round(calc.pi, digits: 2), 3.14) +#test(type(calc.round(3.1415, digits: 2)), float) +#test(type(calc.round(5, digits: 2)), int) +#test(type(calc.round(decimal("3.1415"), digits: 2)), decimal) + +--- calc-round-large-inputs --- +#test(calc.round(31114, digits: 4000000000), 31114) +#test(calc.round(9223372036854775807, digits: 12), 9223372036854775807) +#test(calc.round(238959235.129590203, digits: 4000000000), 238959235.129590203) +#test(calc.round(1.7976931348623157e+308, digits: 12), 1.7976931348623157e+308) +#test(calc.round(decimal("238959235.129590203"), digits: 4000000000), decimal("238959235.129590203")) +#test(calc.round(decimal("79228162514264337593543950335"), digits: 12), decimal("79228162514264337593543950335")) + +--- calc-round-negative-digits --- +// Error: 29-31 number must be at least zero +#calc.round(243.32, digits: -2) --- calc-abs --- // Test the `abs` function. @@ -11,9 +26,11 @@ #test(calc.abs(-3.14), 3.14) #test(calc.abs(50%), 50%) #test(calc.abs(-25%), 25%) +#test(calc.abs(decimal("4932.493249324932")), decimal("4932.493249324932")) +#test(calc.abs(decimal("-12402.593295932041")), decimal("12402.593295932041")) ---- cals-abs-bad-type --- -// Error: 11-22 expected integer, float, length, angle, ratio, or fraction, found string +--- calc-abs-bad-type --- +// Error: 11-22 expected integer, float, length, angle, ratio, fraction, or decimal, found string #calc.abs("no number") --- calc-even-and-odd --- @@ -30,6 +47,13 @@ #test(calc.rem(5, -3), 2) #test(calc.rem(22.5, 10), 2.5) #test(calc.rem(9, 4.5), 0) +#test(calc.rem(decimal("5"), -3), decimal("2")) +#test(calc.rem(decimal("22.5"), decimal("10")), decimal("2.5")) +#test(calc.rem(9, decimal("4.5")), decimal("0")) +#test(calc.rem(decimal("7"), decimal("3")), decimal("1")) +#test(calc.rem(decimal("7"), decimal("-3")), decimal("1")) +#test(calc.rem(decimal("-7"), decimal("3")), decimal("-1")) +#test(calc.rem(decimal("-7"), decimal("-3")), decimal("-1")) --- calc-rem-divisor-zero-1 --- // Error: 14-15 divisor must not be zero @@ -39,6 +63,10 @@ // Error: 16-19 divisor must not be zero #calc.rem(3.0, 0.0) +--- calc-rem-divisor-zero-3 --- +// Error: 27-39 divisor must not be zero +#calc.rem(decimal("4.0"), decimal("0")) + --- calc-div-euclid --- // Test the `div-euclid` function. #test(calc.div-euclid(7, 3), 2) @@ -46,6 +74,11 @@ #test(calc.div-euclid(-7, 3), -3) #test(calc.div-euclid(-7, -3), 3) #test(calc.div-euclid(2.5, 2), 1) +#test(calc.div-euclid(decimal("7"), decimal("3")), decimal("2")) +#test(calc.div-euclid(decimal("7"), decimal("-3")), decimal("-2")) +#test(calc.div-euclid(decimal("-7"), decimal("3")), decimal("-3")) +#test(calc.div-euclid(decimal("-7"), decimal("-3")), decimal("3")) +#test(calc.div-euclid(decimal("2.5"), decimal("2")), decimal("1")) --- calc-div-euclid-divisor-zero-1 --- // Error: 21-22 divisor must not be zero @@ -55,6 +88,10 @@ // Error: 23-26 divisor must not be zero #calc.div-euclid(3.0, 0.0) +--- calc-div-euclid-divisor-zero-3 --- +// Error: 35-50 divisor must not be zero +#calc.div-euclid(decimal("3.00"), decimal("0.00")) + --- calc-rem-euclid --- // Test the `rem-euclid` function. #test(calc.rem-euclid(7, 3), 1) @@ -62,6 +99,11 @@ #test(calc.rem-euclid(-7, 3), 2) #test(calc.rem-euclid(-7, -3), 2) #test(calc.rem-euclid(2.5, 2), 0.5) +#test(calc.rem-euclid(decimal("7"), decimal("3")), decimal("1")) +#test(calc.rem-euclid(decimal("7"), decimal("-3")), decimal("1")) +#test(calc.rem-euclid(decimal("-7"), decimal("3")), decimal("2")) +#test(calc.rem-euclid(decimal("-7"), decimal("-3")), decimal("2")) +#test(calc.rem-euclid(decimal("2.5"), decimal("2")), decimal("0.5")) --- calc-rem-euclid-divisor-zero-1 --- // Error: 21-22 divisor must not be zero @@ -71,13 +113,19 @@ // Error: 23-26 divisor must not be zero #calc.rem-euclid(3.0, 0.0) +--- calc-rem-euclid-divisor-zero-3 --- +// Error: 35-50 divisor must not be zero +#calc.rem-euclid(decimal("3.00"), decimal("0.00")) + --- calc-quo --- // Test the `quo` function. #test(calc.quo(1, 1), 1) #test(calc.quo(5, 3), 1) #test(calc.quo(5, -3), -1) -#test(calc.quo(22.5, 10), 2) -#test(calc.quo(9, 4.5), 2) +#test(calc.quo(22.5, 10), 2.0) +#test(calc.quo(9, 4.5), 2.0) +#test(calc.quo(decimal("22.5"), 10), decimal("2")) +#test(calc.quo(decimal("9"), decimal("4.5")), decimal("2")) --- calc-quo-divisor-zero-1 --- // Error: 14-15 divisor must not be zero @@ -87,17 +135,24 @@ // Error: 16-19 divisor must not be zero #calc.quo(3.0, 0.0) +--- calc-quo-divisor-zero-3 --- +// Error: 27-41 divisor must not be zero +#calc.quo(decimal("4.0"), decimal("0.0")) + --- calc-min-and-max --- // Test the `min` and `max` functions. #test(calc.min(2, -4), -4) #test(calc.min(3.5, 1e2, -0.1, 3), -0.1) +#test(calc.min(decimal("3.5"), 4, decimal("-3213.99999")), decimal("-3213.99999")) #test(calc.max(-3, 11), 11) +#test(calc.max(decimal("3"), 45), 45) #test(calc.min("hi"), "hi") --- calc-pow-log-exp-ln --- // Test the `pow`, `log`, `exp`, and `ln` functions. #test(calc.pow(10, 0), 1) #test(calc.pow(2, 4), 16) +#test(calc.pow(decimal("0.5"), 18), decimal("0.000003814697265625")) #test(calc.exp(2), calc.pow(calc.e, 2)) #test(calc.ln(10), calc.log(10, base: calc.e)) @@ -156,6 +211,10 @@ // Error: 2-25 the result is too large #calc.pow(2, 2147483647) +--- calc-pow-too-large-decimal --- +// Error: 2-56 the result is too large +#calc.pow(decimal("2222222222222222222222222222"), 100) + --- calc-pow-bad-exponent --- // Error: 14-36 exponent may not be infinite, subnormal, or NaN #calc.pow(2, calc.pow(2.0, 10000.0)) @@ -248,6 +307,30 @@ // Error: 2-41 the result is too large #calc.lcm(15486487489457, 4874879896543) +--- calc-rounding-larger-than-max-int --- +#test(calc.round(decimal("9223372036854775809.5")), decimal("9223372036854775810")) +#test(calc.round(9223372036854775809.5), 9223372036854775810.0) +#test(calc.floor(decimal("9223372036854775809.5")), decimal("9223372036854775809")) +#test(calc.floor(9223372036854775809.5), 9223372036854775809.0) +#test(calc.ceil(decimal("9223372036854775809.5")), decimal("9223372036854775810")) +#test(calc.ceil(9223372036854775809.5), 9223372036854775810.0) +#test(calc.trunc(decimal("9223372036854775809.5")), decimal("9223372036854775809")) +#test(calc.trunc(9223372036854775809.5), 9223372036854775809.0) +#test(calc.quo(decimal("9223372036854775809.5"), 1), decimal("9223372036854775809")) +#test(calc.quo(9223372036854775809.5, 1), 9223372036854775809.0) + +--- calc-rounding-smaller-than-min-int --- +#test(calc.round(decimal("-9223372036854775809.5")), decimal("-9223372036854775810")) +#test(calc.round(-9223372036854775809.5), -9223372036854775810.0) +#test(calc.floor(decimal("-9223372036854775809.5")), decimal("-9223372036854775810")) +#test(calc.floor(-9223372036854775809.5), -9223372036854775810.0) +#test(calc.ceil(decimal("-9223372036854775809.5")), decimal("-9223372036854775809")) +#test(calc.ceil(-9223372036854775809.5), -9223372036854775809.0) +#test(calc.trunc(decimal("-9223372036854775809.5")), decimal("-9223372036854775809")) +#test(calc.trunc(-9223372036854775809.5), -9223372036854775809.0) +#test(calc.quo(decimal("-9223372036854775809.5"), 1), decimal("-9223372036854775810")) +#test(calc.quo(-9223372036854775809.5, 1), -9223372036854775810.0) + --- calc-min-nothing --- // Error: 2-12 expected at least one value #calc.min() @@ -259,3 +342,8 @@ --- calc-max-uncomparable --- // Error: 16-19 cannot compare 1pt with 1em #calc.max(1em, 1pt) + +--- calc-clamp-decimal-float --- +// Error: 2-37 cannot apply this operation to a decimal and a float +// Hint: 2-37 if loss of precision is acceptable, explicitly cast the decimal to a float with `float(value)` +#calc.clamp(decimal("10"), 5.5, 6.6) diff --git a/tests/suite/foundations/decimal.typ b/tests/suite/foundations/decimal.typ new file mode 100644 index 00000000..d5fd9444 --- /dev/null +++ b/tests/suite/foundations/decimal.typ @@ -0,0 +1,79 @@ +--- decimal-constructor --- +#test(decimal(10), decimal("10.0")) +#test(decimal("-7654.321"), decimal("-7654.321")) +#test(decimal("\u{2212}7654.321"), decimal("-7654.321")) +#test(decimal({ 3.141592653 }), decimal("3.141592653000000012752934707")) +#test(decimal({ -3.141592653 }), decimal("-3.141592653000000012752934707")) +#test(type(decimal(10)), decimal) + +--- decimal-constructor-bad-type --- +// Error: 10-17 expected integer, float, or string, found type +#decimal(decimal) + +--- decimal-constructor-bad-value --- +// Error: 10-17 invalid decimal: 1.2.3 +#decimal("1.2.3") + +--- decimal-constructor-float-literal --- +// Warning: 18-25 creating a decimal using imprecise float literal +// Hint: 18-25 use a string in the decimal constructor to avoid loss of precision: `decimal("1.32523")` +#let _ = decimal(1.32523) + +--- decimal-constructor-float-inf --- +// Error: 10-19 float is not a valid decimal: float.inf +#decimal(float.inf) + +--- decimal-constructor-float-negative-inf --- +// Error: 10-20 float is not a valid decimal: -float.inf +#decimal(-float.inf) + +--- decimal-constructor-float-nan --- +// Error: 10-19 float is not a valid decimal: float.nan +#decimal(float.nan) + +--- decimal-repr --- +// Test the `repr` function with decimals. +#test(repr(decimal("12.0")), "decimal(\"12.0\")") +#test(repr(decimal("3.14")), "decimal(\"3.14\")") +#test(repr(decimal("1234567890.0")), "decimal(\"1234567890.0\")") +#test(repr(decimal("0123456789.0")), "decimal(\"123456789.0\")") +#test(repr(decimal("0.0")), "decimal(\"0.0\")") +#test(repr(decimal("-0.0")), "decimal(\"0.0\")") +#test(repr(decimal("-1.0")), "decimal(\"-1.0\")") +#test(repr(decimal("-9876543210.0")), "decimal(\"-9876543210.0\")") +#test(repr(decimal("-0987654321.0")), "decimal(\"-987654321.0\")") +#test(repr(decimal("-3.14")), "decimal(\"-3.14\")") +#test(repr(decimal("-3.9191919191919191919191919195")), "decimal(\"-3.9191919191919191919191919195\")") +#test(repr(decimal("5.0000000000")), "decimal(\"5.0000000000\")") +#test(repr(decimal("4.0") - decimal("8.0")), "decimal(\"-4.0\")") + +--- decimal-display --- +// Test decimals. +#set page(width: auto) +#decimal("12.0") \ +#decimal("3.14") \ +#decimal("1234567890.0") \ +#decimal("0123456789.0") \ +#decimal("0.0") \ +#decimal("-0.0") \ +#decimal("-1.0") \ +#decimal("-9876543210.0") \ +#decimal("-0987654321.0") \ +#decimal("-3.14") \ +#decimal("-3.9191919191919191919191919195") \ +#decimal("5.0000000000") \ +#(decimal("4.0") - decimal("8.0")) + +--- decimal-display-round --- +// Display less digits. +#calc.round(decimal("-3.9191919191919191919191919195"), digits: 4) \ +#calc.round(decimal("5.0000000000"), digits: 4) + +--- decimal-expected-float-error --- +// Error: 11-25 expected integer, float, or angle, found decimal +// Hint: 11-25 if loss of precision is acceptable, explicitly cast the decimal to a float with `float(value)` +#calc.sin(decimal("1.1")) + +--- decimal-expected-integer-error --- +// Error: 11-25 expected integer, found decimal +#calc.odd(decimal("1.1")) diff --git a/tests/suite/foundations/float.typ b/tests/suite/foundations/float.typ index 67d4acbf..2e9e07f2 100644 --- a/tests/suite/foundations/float.typ +++ b/tests/suite/foundations/float.typ @@ -6,10 +6,14 @@ #test(float("3.1415"), 3.1415) #test(float("-7654.321"), -7654.321) #test(float("\u{2212}7654.321"), -7654.321) +#test(float(decimal("4.89")), 4.89) +#test(float(decimal("3.1234567891234567891234567891")), 3.123456789123457) +#test(float(decimal("79228162514264337593543950335")), 79228162514264340000000000000.0) +#test(float(decimal("-79228162514264337593543950335")), -79228162514264340000000000000.0) #test(type(float(10)), float) --- float-constructor-bad-type --- -// Error: 8-13 expected float, boolean, integer, ratio, or string, found type +// Error: 8-13 expected float, boolean, integer, decimal, ratio, or string, found type #float(float) --- float-constructor-bad-value --- @@ -55,20 +59,20 @@ --- float-repr --- // Test the `repr` function with floats. -#repr(12.0) \ -#repr(3.14) \ -#repr(1234567890.0) \ -#repr(0123456789.0) \ -#repr(0.0) \ -#repr(-0.0) \ -#repr(-1.0) \ -#repr(-9876543210.0) \ -#repr(-0987654321.0) \ -#repr(-3.14) \ -#repr(4.0 - 8.0) \ -#repr(float.inf) \ -#repr(-float.inf) \ -#repr(float.nan) +#test(repr(12.0), "12.0") +#test(repr(3.14), "3.14") +#test(repr(1234567890.0), "1234567890.0") +#test(repr(0123456789.0), "123456789.0") +#test(repr(0.0), "0.0") +#test(repr(-0.0), "-0.0") +#test(repr(-1.0), "-1.0") +#test(repr(-9876543210.0), "-9876543210.0") +#test(repr(-0987654321.0), "-987654321.0") +#test(repr(-3.14), "-3.14") +#test(repr(4.0 - 8.0), "-4.0") +#test(repr(float.inf), "float.inf") +#test(repr(-float.inf), "-float.inf") +#test(repr(float.nan), "float.nan") --- float-display --- // Test floats. diff --git a/tests/suite/foundations/int.typ b/tests/suite/foundations/int.typ index 1744ef88..9cb27cf5 100644 --- a/tests/suite/foundations/int.typ +++ b/tests/suite/foundations/int.typ @@ -21,15 +21,34 @@ #test(int("-834"), -834) #test(int("\u{2212}79"), -79) #test(int(10 / 3), 3) +#test(int(-58.34), -58) +#test(int(decimal("92492.193848921")), 92492) +#test(int(decimal("-224.342211")), -224) --- int-constructor-bad-type --- -// Error: 6-10 expected integer, boolean, float, or string, found length +// Error: 6-10 expected integer, boolean, float, decimal, or string, found length #int(10pt) --- int-constructor-bad-value --- // Error: 6-12 invalid integer: nope #int("nope") +--- int-constructor-float-too-large --- +// Error: 6-27 number too large +#int(9223372036854775809.5) + +--- int-constructor-float-too-large-min --- +// Error: 6-28 number too large +#int(-9223372036854775809.5) + +--- int-constructor-decimal-too-large --- +// Error: 6-38 number too large +#int(decimal("9223372036854775809.5")) + +--- int-constructor-decimal-too-large-min --- +// Error: 6-39 number too large +#int(decimal("-9223372036854775809.5")) + --- int-signum --- // Test int `signum()` #test(int(0).signum(), 0) @@ -58,15 +77,15 @@ --- int-repr --- // Test the `repr` function with integers. -#repr(12) \ -#repr(1234567890) \ -#repr(0123456789) \ -#repr(0) \ -#repr(-0) \ -#repr(-1) \ -#repr(-9876543210) \ -#repr(-0987654321) \ -#repr(4 - 8) +#test(repr(12), "12") +#test(repr(1234567890), "1234567890") +#test(repr(0123456789), "123456789") +#test(repr(0), "0") +#test(repr(-0), "0") +#test(repr(-1), "-1") +#test(repr(-9876543210), "-9876543210") +#test(repr(-0987654321), "-987654321") +#test(repr(4 - 8), "-4") --- int-display --- // Test integers. diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ index 025ec53d..0da11c31 100644 --- a/tests/suite/foundations/str.typ +++ b/tests/suite/foundations/str.typ @@ -23,6 +23,24 @@ #test(str(-3.14), "−3.14") #test(str(4.0 - 8.0), "−4") +--- str-from-decimal --- +// Test the `str` function with decimals. +#test(str(decimal("12")), "12") +#test(str(decimal("12.0")), "12.0") +#test(str(decimal("3.14")), "3.14") +#test(str(decimal("1234567890.0")), "1234567890.0") +#test(str(decimal("0123456789.0")), "123456789.0") +#test(str(decimal("0.0")), "0.0") +#test(str(decimal("-0.0")), "0.0") +#test(str(decimal("-1.0")), "−1.0") +#test(str(decimal("-9876543210.0")), "−9876543210.0") +#test(str(decimal("-0987654321.0")), "−987654321.0") +#test(str(decimal("-3.14")), "−3.14") +#test(str(decimal("-3.9191919191919191919191919195")), "−3.9191919191919191919191919195") +#test(str(decimal("5.0000000000")), "5.0000000000") +#test(str(decimal("4.0") - decimal("8.0")), "−4.0") +#test(str(decimal("4") - decimal("8")), "−4") + --- str-from-int --- // Test the `str` function with integers. #test(str(12), "12") @@ -36,7 +54,7 @@ #test(str(4 - 8), "−4") --- str-constructor-bad-type --- -// Error: 6-8 expected integer, float, version, bytes, label, type, or string, found content +// Error: 6-8 expected integer, float, decimal, version, bytes, label, type, or string, found content #str([]) --- str-constructor-bad-base --- diff --git a/tests/suite/scripting/ops.typ b/tests/suite/scripting/ops.typ index e0c94e02..d17c0117 100644 --- a/tests/suite/scripting/ops.typ +++ b/tests/suite/scripting/ops.typ @@ -8,7 +8,7 @@ // Test math operators. // Test plus and minus. -#for v in (1, 3.14, 12pt, 45deg, 90%, 13% + 10pt, 6.3fr) { +#for v in (1, 3.14, decimal("12.43"), 12pt, 45deg, 90%, 13% + 10pt, 6.3fr) { // Test plus. test(+v, v) @@ -60,6 +60,7 @@ // Mathematical identities. #let nums = ( 1, 3.14, + decimal("12.45"), 12pt, 3em, 12pt + 3em, 45deg, 90%, @@ -76,12 +77,12 @@ test(v - v, 0 * v) test(v + v, 2 * v) - // Integer addition does not give a float. - if type(v) != int { + // Integer or decimal addition does not give a float. + if type(v) not in (int, decimal) { test(v + v, 2.0 * v) } - if type(v) != relative and ("pt" not in repr(v) or "em" not in repr(v)) { + if type(v) not in (relative, decimal) and ("pt" not in repr(v) or "em" not in repr(v)) { test(v / v, 1.0) } } @@ -112,6 +113,46 @@ } } +--- ops-binary-decimal --- +// Addition. +#test(decimal("40.1") + decimal("13.2"), decimal("53.3")) +#test(decimal("12.34330") + decimal("45.96670"), decimal("58.31000")) +#test(decimal("451113.111111111111111111111") + decimal("23.222222222222222222324"), decimal("451136.333333333333333333435")) + +// Subtraction. +#test(decimal("40.1") - decimal("13.2"), decimal("26.9")) +#test(decimal("12.34330") - decimal("45.96670"), decimal("-33.62340")) +#test(decimal("1234.111111111111111111111") - decimal("0.222222222222222222324"), decimal("1233.888888888888888888787")) + +// Multiplication. +#test(decimal("40.5") * decimal("9.5"), decimal("384.75")) +#test(decimal("-0.1234567890123456789012345678") * decimal("-2.0"), decimal("0.2469135780246913578024691356")) + +// Division. +#test(decimal("1.0") / decimal("7.0"), decimal("0.1428571428571428571428571429")) +#test(decimal("9999991.6666") / decimal("3.0"), decimal("3333330.5555333333333333333333")) +#test(decimal("3253452.4034029359598214312040") / decimal("-49293591.4039493929532"), decimal("-0.0660015290170614346071165643")) + +--- ops-binary-decimal-int --- +// Operations between decimal and integer. +#test(decimal("2359.123456789123456789001234") + 2, decimal("2361.123456789123456789001234")) +#test(decimal("2359.123456789123456789001234") - 2, decimal("2357.123456789123456789001234")) +#test(decimal("2359.123456789123456789001234") * 2, decimal("4718.246913578246913578002468")) +#test(decimal("2359.123456789123456789001234") / 2, decimal("1179.561728394561728394500617")) + +--- ops-binary-decimal-multiplication-division-imprecision --- +// Test digit truncation by multiplication and division. +#test(decimal("0.7777777777777777777777777777") / 1000, decimal("0.0007777777777777777777777778")) +#test(decimal("0.7777777777777777777777777777") * decimal("0.001"), decimal("0.0007777777777777777777777778")) + +--- ops-add-too-large-decimal --- +// Error: 3-47 value is too large +#(decimal("79228162514264337593543950335") + 1) + +--- ops-subtract-too-large-decimal --- +// Error: 3-48 value is too large +#(decimal("-79228162514264337593543950335") - 1) + --- ops-multiply-inf-with-length --- // Test that multiplying infinite numbers by certain units does not crash. #(float("inf") * 1pt) @@ -164,6 +205,8 @@ #test((:) == (a: 1), false) #test((a: 2 - 1.0, b: 2) == (b: 2, a: 1), true) #test("a" != "a", false) +#test(decimal("1.234") == decimal("1.23400000000"), true) +#test(235 == decimal("235.0"), true) // Functions compare by identity. #test(test == test, true) @@ -202,6 +245,10 @@ #test(() >= (), true) #test(() <= (1,), true) #test((1,) <= (), false) +#test(decimal("123.0000000000000000000000001") > decimal("123.0"), true) +#test(decimal("123.5") < decimal("122.444"), false) +#test(decimal("459.9999999999999999999999999") < 460, true) +#test(decimal("128.50") > 460, false) --- ops-in --- // Test `in` operator. |
