mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Fixed-point decimal type (#4900)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
a69ada7889
commit
320c28844f
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -2027,6 +2027,16 @@ version = "0.20.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
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]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -2666,6 +2676,7 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
|
"rust_decimal",
|
||||||
"rustybuzz",
|
"rustybuzz",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -90,6 +90,7 @@ rayon = "1.7.0"
|
|||||||
regex = "1"
|
regex = "1"
|
||||||
resvg = { version = "0.43", default-features = false, features = ["raster-images"] }
|
resvg = { version = "0.43", default-features = false, features = ["raster-images"] }
|
||||||
roxmltree = "0.20"
|
roxmltree = "0.20"
|
||||||
|
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
|
||||||
rustybuzz = "0.18"
|
rustybuzz = "0.18"
|
||||||
same-file = "1"
|
same-file = "1"
|
||||||
self-replace = "1.3.7"
|
self-replace = "1.3.7"
|
||||||
|
@ -52,6 +52,7 @@ portable-atomic = { workspace = true }
|
|||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
roxmltree = { workspace = true }
|
roxmltree = { workspace = true }
|
||||||
|
rust_decimal = { workspace = true }
|
||||||
rustybuzz = { workspace = true }
|
rustybuzz = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
@ -139,6 +139,7 @@ pub fn pos(value: Value) -> HintedStrResult<Value> {
|
|||||||
Ok(match value {
|
Ok(match value {
|
||||||
Int(v) => Int(v),
|
Int(v) => Int(v),
|
||||||
Float(v) => Float(v),
|
Float(v) => Float(v),
|
||||||
|
Decimal(v) => Decimal(v),
|
||||||
Length(v) => Length(v),
|
Length(v) => Length(v),
|
||||||
Angle(v) => Angle(v),
|
Angle(v) => Angle(v),
|
||||||
Ratio(v) => Ratio(v),
|
Ratio(v) => Ratio(v),
|
||||||
@ -164,6 +165,7 @@ pub fn neg(value: Value) -> HintedStrResult<Value> {
|
|||||||
Ok(match value {
|
Ok(match value {
|
||||||
Int(v) => Int(v.checked_neg().ok_or_else(too_large)?),
|
Int(v) => Int(v.checked_neg().ok_or_else(too_large)?),
|
||||||
Float(v) => Float(-v),
|
Float(v) => Float(-v),
|
||||||
|
Decimal(v) => Decimal(-v),
|
||||||
Length(v) => Length(-v),
|
Length(v) => Length(-v),
|
||||||
Angle(v) => Angle(-v),
|
Angle(v) => Angle(-v),
|
||||||
Ratio(v) => Ratio(-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), Int(b)) => Float(a + b as f64),
|
||||||
(Float(a), Float(b)) => Float(a + b),
|
(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),
|
(Angle(a), Angle(b)) => Angle(a + b),
|
||||||
|
|
||||||
(Length(a), Length(b)) => Length(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), Int(b)) => Float(a - b as f64),
|
||||||
(Float(a), Float(b)) => Float(a - b),
|
(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),
|
(Angle(a), Angle(b)) => Angle(a - b),
|
||||||
|
|
||||||
(Length(a), Length(b)) => Length(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), Int(b)) => Float(a * b as f64),
|
||||||
(Float(a), Float(b)) => Float(a * b),
|
(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), Int(b)) => Length(a * b as f64),
|
||||||
(Length(a), Float(b)) => Length(a * b),
|
(Length(a), Float(b)) => Length(a * b),
|
||||||
(Length(a), Ratio(b)) => Length(a * b.get()),
|
(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), Int(b)) => Float(a / b as f64),
|
||||||
(Float(a), Float(b)) => Float(a / b),
|
(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), Int(b)) => Length(a / b as f64),
|
||||||
(Length(a), Float(b)) => Length(a / b),
|
(Length(a), Float(b)) => Length(a / b),
|
||||||
(Length(a), Length(b)) => Float(try_div_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 {
|
match *v {
|
||||||
Int(v) => v == 0,
|
Int(v) => v == 0,
|
||||||
Float(v) => v == 0.0,
|
Float(v) => v == 0.0,
|
||||||
|
Decimal(v) => v.is_zero(),
|
||||||
Length(v) => v.is_zero(),
|
Length(v) => v.is_zero(),
|
||||||
Angle(v) => v.is_zero(),
|
Angle(v) => v.is_zero(),
|
||||||
Ratio(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,
|
(Bool(a), Bool(b)) => a == b,
|
||||||
(Int(a), Int(b)) => a == b,
|
(Int(a), Int(b)) => a == b,
|
||||||
(Float(a), Float(b)) => a == b,
|
(Float(a), Float(b)) => a == b,
|
||||||
|
(Decimal(a), Decimal(b)) => a == b,
|
||||||
(Length(a), Length(b)) => a == b,
|
(Length(a), Length(b)) => a == b,
|
||||||
(Angle(a), Angle(b)) => a == b,
|
(Angle(a), Angle(b)) => a == b,
|
||||||
(Ratio(a), Ratio(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.
|
// Some technically different things should compare equal.
|
||||||
(&Int(i), &Float(f)) | (&Float(f), &Int(i)) => i as f64 == f,
|
(&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)) => {
|
(&Length(len), &Relative(rel)) | (&Relative(rel), &Length(len)) => {
|
||||||
len == rel.abs && rel.rel.is_zero()
|
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),
|
(Bool(a), Bool(b)) => a.cmp(b),
|
||||||
(Int(a), Int(b)) => a.cmp(b),
|
(Int(a), Int(b)) => a.cmp(b),
|
||||||
(Float(a), Float(b)) => try_cmp_values(a, 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)?,
|
(Length(a), Length(b)) => try_cmp_values(a, b)?,
|
||||||
(Angle(a), Angle(b)) => a.cmp(b),
|
(Angle(a), Angle(b)) => a.cmp(b),
|
||||||
(Ratio(a), Ratio(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.
|
// Some technically different things should be comparable.
|
||||||
(Int(a), Float(b)) => try_cmp_values(&(*a as f64), b)?,
|
(Int(a), Float(b)) => try_cmp_values(&(*a as f64), b)?,
|
||||||
(Float(a), Int(b)) => try_cmp_values(a, &(*b as f64))?,
|
(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)?,
|
(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),
|
(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)?,
|
(Relative(a), Length(b)) if a.rel.is_zero() => try_cmp_values(&a.abs, b)?,
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::cmp::Ordering;
|
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::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::layout::{Angle, Fr, Length, Ratio};
|
||||||
use crate::syntax::{Span, Spanned};
|
use crate::syntax::{Span, Spanned};
|
||||||
|
|
||||||
@ -61,7 +62,8 @@ pub fn module() -> Module {
|
|||||||
/// ```example
|
/// ```example
|
||||||
/// #calc.abs(-5) \
|
/// #calc.abs(-5) \
|
||||||
/// #calc.abs(5pt - 2cm) \
|
/// #calc.abs(5pt - 2cm) \
|
||||||
/// #calc.abs(2fr)
|
/// #calc.abs(2fr) \
|
||||||
|
/// #calc.abs(decimal("-342.440"))
|
||||||
/// ```
|
/// ```
|
||||||
#[func(title = "Absolute")]
|
#[func(title = "Absolute")]
|
||||||
pub fn abs(
|
pub fn abs(
|
||||||
@ -83,24 +85,28 @@ cast! {
|
|||||||
v: Angle => Self(Value::Angle(v.abs())),
|
v: Angle => Self(Value::Angle(v.abs())),
|
||||||
v: Ratio => Self(Value::Ratio(v.abs())),
|
v: Ratio => Self(Value::Ratio(v.abs())),
|
||||||
v: Fr => Self(Value::Fraction(v.abs())),
|
v: Fr => Self(Value::Fraction(v.abs())),
|
||||||
|
v: Decimal => Self(Value::Decimal(v.abs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Raises a value to some exponent.
|
/// Raises a value to some exponent.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #calc.pow(2, 3)
|
/// #calc.pow(2, 3)
|
||||||
|
/// #calc.pow(decimal("2.5"), 2)
|
||||||
/// ```
|
/// ```
|
||||||
#[func(title = "Power")]
|
#[func(title = "Power")]
|
||||||
pub fn pow(
|
pub fn pow(
|
||||||
/// The callsite span.
|
/// The callsite span.
|
||||||
span: Span,
|
span: Span,
|
||||||
/// The base of the power.
|
/// 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.
|
/// The exponent of the power.
|
||||||
exponent: Spanned<Num>,
|
exponent: Spanned<Num>,
|
||||||
) -> SourceResult<Num> {
|
) -> SourceResult<DecNum> {
|
||||||
match exponent.v {
|
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")
|
bail!(span, "zero to the power of zero is undefined")
|
||||||
}
|
}
|
||||||
Num::Int(i) if i32::try_from(i).is_err() => {
|
Num::Int(i) if i32::try_from(i).is_err() => {
|
||||||
@ -112,26 +118,37 @@ pub fn pow(
|
|||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = match (base, exponent.v) {
|
match (base, exponent.v) {
|
||||||
(Num::Int(a), Num::Int(b)) if b >= 0 => {
|
(DecNum::Int(a), Num::Int(b)) if b >= 0 => a
|
||||||
a.checked_pow(b as u32).map(Num::Int).ok_or_else(too_large).at(span)?
|
.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) => Num::Float(if a.float() == std::f64::consts::E {
|
(a, b) => {
|
||||||
b.float().exp()
|
let Some(a) = a.float() else {
|
||||||
} else if a.float() == 2.0 {
|
return Err(cant_apply_to_decimal_and_float()).at(span);
|
||||||
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() {
|
let result = if a == std::f64::consts::E {
|
||||||
bail!(span, "the result is not a real number")
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Raises a value to some exponent of e.
|
/// 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")
|
bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
}
|
||||||
|
|
||||||
let result = exponent.v.float().exp();
|
let result = exponent.v.float().exp();
|
||||||
if result.is_nan() {
|
if result.is_nan() {
|
||||||
@ -598,19 +615,28 @@ pub fn lcm(
|
|||||||
///
|
///
|
||||||
/// If the number is already an integer, it is returned unchanged.
|
/// 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
|
/// ```example
|
||||||
/// #assert(calc.floor(3.14) == 3)
|
|
||||||
/// #assert(calc.floor(3) == 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)
|
/// #calc.floor(500.1)
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func]
|
||||||
pub fn floor(
|
pub fn floor(
|
||||||
/// The number to round down.
|
/// The number to round down.
|
||||||
value: Num,
|
value: DecNum,
|
||||||
) -> i64 {
|
) -> DecNum {
|
||||||
match value {
|
match value {
|
||||||
Num::Int(n) => n,
|
DecNum::Int(n) => DecNum::Int(n),
|
||||||
Num::Float(n) => n.floor() as i64,
|
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.
|
/// 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
|
/// ```example
|
||||||
/// #assert(calc.ceil(3.14) == 4)
|
|
||||||
/// #assert(calc.ceil(3) == 3)
|
/// #assert(calc.ceil(3) == 3)
|
||||||
|
/// #assert(calc.ceil(3.14) == 4)
|
||||||
|
/// #assert(calc.ceil(decimal("-3.14")) == decimal("-3"))
|
||||||
/// #calc.ceil(500.1)
|
/// #calc.ceil(500.1)
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func]
|
||||||
pub fn ceil(
|
pub fn ceil(
|
||||||
/// The number to round up.
|
/// The number to round up.
|
||||||
value: Num,
|
value: DecNum,
|
||||||
) -> i64 {
|
) -> DecNum {
|
||||||
match value {
|
match value {
|
||||||
Num::Int(n) => n,
|
DecNum::Int(n) => DecNum::Int(n),
|
||||||
Num::Float(n) => n.ceil() as i64,
|
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.
|
/// 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
|
/// ```example
|
||||||
/// #assert(calc.trunc(3) == 3)
|
/// #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)
|
/// #calc.trunc(15.9)
|
||||||
/// ```
|
/// ```
|
||||||
#[func(title = "Truncate")]
|
#[func(title = "Truncate")]
|
||||||
pub fn trunc(
|
pub fn trunc(
|
||||||
/// The number to truncate.
|
/// The number to truncate.
|
||||||
value: Num,
|
value: DecNum,
|
||||||
) -> i64 {
|
) -> DecNum {
|
||||||
match value {
|
match value {
|
||||||
Num::Int(n) => n,
|
DecNum::Int(n) => DecNum::Int(n),
|
||||||
Num::Float(n) => n.trunc() as i64,
|
DecNum::Float(n) => DecNum::Float(n.trunc()),
|
||||||
|
DecNum::Decimal(n) => DecNum::Decimal(n.trunc()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,16 +704,18 @@ pub fn trunc(
|
|||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #assert(calc.fract(3) == 0)
|
/// #assert(calc.fract(3) == 0)
|
||||||
|
/// #assert(calc.fract(decimal("234.23949211")) == decimal("0.23949211"))
|
||||||
/// #calc.fract(-3.1)
|
/// #calc.fract(-3.1)
|
||||||
/// ```
|
/// ```
|
||||||
#[func(title = "Fractional")]
|
#[func(title = "Fractional")]
|
||||||
pub fn fract(
|
pub fn fract(
|
||||||
/// The number to truncate.
|
/// The number to truncate.
|
||||||
value: Num,
|
value: DecNum,
|
||||||
) -> Num {
|
) -> DecNum {
|
||||||
match value {
|
match value {
|
||||||
Num::Int(_) => Num::Int(0),
|
DecNum::Int(_) => DecNum::Int(0),
|
||||||
Num::Float(n) => Num::Float(n.fract()),
|
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.
|
/// 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
|
/// ```example
|
||||||
|
/// #assert(calc.round(3) == 3)
|
||||||
/// #assert(calc.round(3.14) == 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)
|
/// #calc.round(3.1415, digits: 2)
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func]
|
||||||
pub fn round(
|
pub fn round(
|
||||||
/// The number to round.
|
/// The number to round.
|
||||||
value: Num,
|
value: DecNum,
|
||||||
/// The number of decimal places.
|
/// The number of decimal places. Must not be negative.
|
||||||
#[named]
|
#[named]
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
digits: i64,
|
digits: u32,
|
||||||
) -> Num {
|
) -> DecNum {
|
||||||
match value {
|
match value {
|
||||||
Num::Int(n) if digits == 0 => Num::Int(n),
|
DecNum::Int(n) => DecNum::Int(n),
|
||||||
_ => {
|
DecNum::Float(n) => DecNum::Float(crate::utils::format::round_with_precision(
|
||||||
let n = value.float();
|
n,
|
||||||
let factor = 10.0_f64.powi(digits as i32);
|
digits.saturating_as::<u8>(),
|
||||||
Num::Float((n * factor).round() / factor)
|
)),
|
||||||
}
|
DecNum::Decimal(n) => DecNum::Decimal(n.round(digits)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -706,21 +762,35 @@ pub fn round(
|
|||||||
/// ```example
|
/// ```example
|
||||||
/// #assert(calc.clamp(5, 0, 10) == 5)
|
/// #assert(calc.clamp(5, 0, 10) == 5)
|
||||||
/// #assert(calc.clamp(5, 6, 10) == 6)
|
/// #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)
|
/// #calc.clamp(5, 0, 4)
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func]
|
||||||
pub fn clamp(
|
pub fn clamp(
|
||||||
|
/// The callsite span.
|
||||||
|
span: Span,
|
||||||
/// The number to clamp.
|
/// The number to clamp.
|
||||||
value: Num,
|
value: DecNum,
|
||||||
/// The inclusive minimum value.
|
/// The inclusive minimum value.
|
||||||
min: Num,
|
min: DecNum,
|
||||||
/// The inclusive maximum value.
|
/// The inclusive maximum value.
|
||||||
max: Spanned<Num>,
|
max: Spanned<DecNum>,
|
||||||
) -> SourceResult<Num> {
|
) -> SourceResult<DecNum> {
|
||||||
if max.v.float() < min.float() {
|
// 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")
|
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.
|
/// 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
|
/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller
|
||||||
/// in magnitude than `y`.
|
/// 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
|
/// ```example
|
||||||
/// #calc.rem(7, 3) \
|
/// #calc.rem(7, 3) \
|
||||||
/// #calc.rem(7, -3) \
|
/// #calc.rem(7, -3) \
|
||||||
@ -824,15 +897,28 @@ pub fn odd(
|
|||||||
/// ```
|
/// ```
|
||||||
#[func(title = "Remainder")]
|
#[func(title = "Remainder")]
|
||||||
pub fn rem(
|
pub fn rem(
|
||||||
|
/// The span of the function call.
|
||||||
|
span: Span,
|
||||||
/// The dividend of the remainder.
|
/// The dividend of the remainder.
|
||||||
dividend: Num,
|
dividend: DecNum,
|
||||||
/// The divisor of the remainder.
|
/// The divisor of the remainder.
|
||||||
divisor: Spanned<Num>,
|
divisor: Spanned<DecNum>,
|
||||||
) -> SourceResult<Num> {
|
) -> SourceResult<DecNum> {
|
||||||
if divisor.v.float() == 0.0 {
|
if divisor.v.is_zero() {
|
||||||
bail!(divisor.span, "divisor must not be 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.
|
/// 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(-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")]
|
#[func(title = "Euclidean Division")]
|
||||||
pub fn div_euclid(
|
pub fn div_euclid(
|
||||||
|
/// The callsite span.
|
||||||
|
span: Span,
|
||||||
/// The dividend of the division.
|
/// The dividend of the division.
|
||||||
dividend: Num,
|
dividend: DecNum,
|
||||||
/// The divisor of the division.
|
/// The divisor of the division.
|
||||||
divisor: Spanned<Num>,
|
divisor: Spanned<DecNum>,
|
||||||
) -> SourceResult<Num> {
|
) -> SourceResult<DecNum> {
|
||||||
if divisor.v.float() == 0.0 {
|
if divisor.v.is_zero() {
|
||||||
bail!(divisor.span, "divisor must not be 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.
|
/// This calculates the least nonnegative remainder of a division.
|
||||||
///
|
///
|
||||||
/// Warning: Due to a floating point round-off error, the remainder may equal the absolute
|
/// Warning: Due to a floating point round-off error, the remainder may equal
|
||||||
/// value of the divisor if the dividend is much smaller in magnitude than the divisor
|
/// the absolute value of the divisor if the dividend is much smaller in
|
||||||
/// and the dividend is negative. This only applies for floating point inputs.
|
/// 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
|
/// ```example
|
||||||
/// #calc.rem-euclid(7, 3) \
|
/// #calc.rem-euclid(7, 3) \
|
||||||
/// #calc.rem-euclid(7, -3) \
|
/// #calc.rem-euclid(7, -3) \
|
||||||
/// #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")]
|
#[func(title = "Euclidean Remainder")]
|
||||||
pub fn rem_euclid(
|
pub fn rem_euclid(
|
||||||
|
/// The callsite span.
|
||||||
|
span: Span,
|
||||||
/// The dividend of the remainder.
|
/// The dividend of the remainder.
|
||||||
dividend: Num,
|
dividend: DecNum,
|
||||||
/// The divisor of the remainder.
|
/// The divisor of the remainder.
|
||||||
divisor: Spanned<Num>,
|
divisor: Spanned<DecNum>,
|
||||||
) -> SourceResult<Num> {
|
) -> SourceResult<DecNum> {
|
||||||
if divisor.v.float() == 0.0 {
|
if divisor.v.is_zero() {
|
||||||
bail!(divisor.span, "divisor must not be 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.
|
/// Calculates the quotient (floored division) of two numbers.
|
||||||
@ -895,16 +1013,30 @@ pub fn rem_euclid(
|
|||||||
/// ```
|
/// ```
|
||||||
#[func(title = "Quotient")]
|
#[func(title = "Quotient")]
|
||||||
pub fn quo(
|
pub fn quo(
|
||||||
|
/// The span of the function call.
|
||||||
|
span: Span,
|
||||||
/// The dividend of the quotient.
|
/// The dividend of the quotient.
|
||||||
dividend: Num,
|
dividend: DecNum,
|
||||||
/// The divisor of the quotient.
|
/// The divisor of the quotient.
|
||||||
divisor: Spanned<Num>,
|
divisor: Spanned<DecNum>,
|
||||||
) -> SourceResult<i64> {
|
) -> SourceResult<DecNum> {
|
||||||
if divisor.v.float() == 0.0 {
|
if divisor.v.is_zero() {
|
||||||
bail!(divisor.span, "divisor must not be 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.
|
/// A value which can be passed to functions that work with integers and floats.
|
||||||
@ -915,31 +1047,6 @@ pub enum Num {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Num {
|
impl Num {
|
||||||
fn apply2(
|
|
||||||
self,
|
|
||||||
other: Self,
|
|
||||||
int: impl FnOnce(i64, i64) -> i64,
|
|
||||||
float: impl FnOnce(f64, f64) -> f64,
|
|
||||||
) -> Num {
|
|
||||||
match (self, other) {
|
|
||||||
(Self::Int(a), Self::Int(b)) => Num::Int(int(a, b)),
|
|
||||||
(a, b) => Num::Float(float(a.float(), b.float())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply3(
|
|
||||||
self,
|
|
||||||
other: Self,
|
|
||||||
third: Self,
|
|
||||||
int: impl FnOnce(i64, i64, i64) -> i64,
|
|
||||||
float: impl FnOnce(f64, f64, f64) -> f64,
|
|
||||||
) -> Num {
|
|
||||||
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 {
|
fn float(self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Int(v) => v as f64,
|
Self::Int(v) => v as f64,
|
||||||
@ -958,6 +1065,103 @@ cast! {
|
|||||||
v: f64 => Self::Float(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) -> T,
|
||||||
|
float: impl FnOnce(f64, f64) -> T,
|
||||||
|
decimal: impl FnOnce(Decimal, Decimal) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
match (self, other) {
|
||||||
|
(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,
|
||||||
|
decimal: impl FnOnce(Decimal, Decimal, Decimal) -> Decimal,
|
||||||
|
) -> Option<Self> {
|
||||||
|
match (self, other, third) {
|
||||||
|
(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! {
|
||||||
|
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.
|
/// A value that can be passed to a trigonometric function.
|
||||||
pub enum AngleLike {
|
pub enum AngleLike {
|
||||||
Int(i64),
|
Int(i64),
|
||||||
@ -977,3 +1181,14 @@ cast! {
|
|||||||
fn too_large() -> &'static str {
|
fn too_large() -> &'static str {
|
||||||
"the result is too large"
|
"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)`",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -345,6 +345,13 @@ impl CastInfo {
|
|||||||
msg.hint(eco_format!("use `label({})` to create a label", s.repr()));
|
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
|
msg
|
||||||
|
369
crates/typst/src/foundations/decimal.rs
Normal file
369
crates/typst/src/foundations/decimal.rs
Normal file
@ -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)),
|
||||||
|
}
|
@ -4,7 +4,7 @@ use ecow::{eco_format, EcoString};
|
|||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::StrResult;
|
||||||
use crate::foundations::{
|
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;
|
use crate::layout::Ratio;
|
||||||
|
|
||||||
@ -177,6 +177,7 @@ cast! {
|
|||||||
v: f64 => Self(v),
|
v: f64 => Self(v),
|
||||||
v: bool => Self(v as i64 as f64),
|
v: bool => Self(v as i64 as f64),
|
||||||
v: i64 => Self(v 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: Ratio => Self(v.get()),
|
||||||
v: Str => Self(
|
v: Str => Self(
|
||||||
parse_float(v.clone().into())
|
parse_float(v.clone().into())
|
||||||
|
@ -4,7 +4,7 @@ use ecow::{eco_format, EcoString};
|
|||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::StrResult;
|
||||||
use crate::foundations::{
|
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.
|
/// A whole number.
|
||||||
@ -33,16 +33,19 @@ type i64;
|
|||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
impl i64 {
|
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`.
|
/// - 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.
|
/// - Strings are parsed in base 10.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #int(false) \
|
/// #int(false) \
|
||||||
/// #int(true) \
|
/// #int(true) \
|
||||||
/// #int(2.7) \
|
/// #int(2.7) \
|
||||||
|
/// #int(decimal("3.8")) \
|
||||||
/// #(int("27") + int("4"))
|
/// #(int("27") + int("4"))
|
||||||
/// ```
|
/// ```
|
||||||
#[func(constructor)]
|
#[func(constructor)]
|
||||||
@ -360,10 +363,19 @@ cast! {
|
|||||||
ToInt,
|
ToInt,
|
||||||
v: i64 => Self(v),
|
v: i64 => Self(v),
|
||||||
v: bool => Self(v as i64),
|
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))?),
|
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> {
|
fn parse_int(mut s: &str) -> Result<i64, ParseIntError> {
|
||||||
let mut sign = 1;
|
let mut sign = 1;
|
||||||
if let Some(rest) = s.strip_prefix('-').or_else(|| s.strip_prefix(repr::MINUS_SIGN)) {
|
if let Some(rest) = s.strip_prefix('-').or_else(|| s.strip_prefix(repr::MINUS_SIGN)) {
|
||||||
|
@ -15,6 +15,7 @@ mod cast;
|
|||||||
mod content;
|
mod content;
|
||||||
mod context;
|
mod context;
|
||||||
mod datetime;
|
mod datetime;
|
||||||
|
mod decimal;
|
||||||
mod dict;
|
mod dict;
|
||||||
mod duration;
|
mod duration;
|
||||||
mod element;
|
mod element;
|
||||||
@ -43,6 +44,7 @@ pub use self::cast::*;
|
|||||||
pub use self::content::*;
|
pub use self::content::*;
|
||||||
pub use self::context::*;
|
pub use self::context::*;
|
||||||
pub use self::datetime::*;
|
pub use self::datetime::*;
|
||||||
|
pub use self::decimal::*;
|
||||||
pub use self::dict::*;
|
pub use self::dict::*;
|
||||||
pub use self::duration::*;
|
pub use self::duration::*;
|
||||||
pub use self::element::*;
|
pub use self::element::*;
|
||||||
@ -105,6 +107,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) {
|
|||||||
global.define_type::<Regex>();
|
global.define_type::<Regex>();
|
||||||
global.define_type::<Selector>();
|
global.define_type::<Selector>();
|
||||||
global.define_type::<Datetime>();
|
global.define_type::<Datetime>();
|
||||||
|
global.define_type::<Decimal>();
|
||||||
global.define_type::<Duration>();
|
global.define_type::<Duration>();
|
||||||
global.define_type::<Version>();
|
global.define_type::<Version>();
|
||||||
global.define_type::<Plugin>();
|
global.define_type::<Plugin>();
|
||||||
|
@ -11,8 +11,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, dict, func, repr, scope, ty, Array, Bytes, Context, Dict, Func, IntoValue,
|
cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func,
|
||||||
Label, Repr, Type, Value, Version,
|
IntoValue, Label, Repr, Type, Value, Version,
|
||||||
};
|
};
|
||||||
use crate::layout::Alignment;
|
use crate::layout::Alignment;
|
||||||
use crate::syntax::{Span, Spanned};
|
use crate::syntax::{Span, Spanned};
|
||||||
@ -775,6 +775,7 @@ cast! {
|
|||||||
ToStr,
|
ToStr,
|
||||||
v: i64 => Self::Int(v),
|
v: i64 => Self::Int(v),
|
||||||
v: f64 => Self::Str(repr::display_float(v).into()),
|
v: f64 => Self::Str(repr::display_float(v).into()),
|
||||||
|
v: Decimal => Self::Str(format_str!("{}", v)),
|
||||||
v: Version => Self::Str(format_str!("{}", v)),
|
v: Version => Self::Str(format_str!("{}", v)),
|
||||||
v: Bytes => Self::Str(
|
v: Bytes => Self::Str(
|
||||||
std::str::from_utf8(&v)
|
std::str::from_utf8(&v)
|
||||||
|
@ -12,9 +12,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|||||||
use crate::diag::{HintedStrResult, HintedString, StrResult};
|
use crate::diag::{HintedStrResult, HintedString, StrResult};
|
||||||
use crate::eval::ops;
|
use crate::eval::ops;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Dict,
|
fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal,
|
||||||
Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType,
|
Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement,
|
||||||
NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type, Version,
|
NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type,
|
||||||
|
Version,
|
||||||
};
|
};
|
||||||
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
|
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
|
||||||
use crate::symbols::Symbol;
|
use crate::symbols::Symbol;
|
||||||
@ -65,6 +66,8 @@ pub enum Value {
|
|||||||
Label(Label),
|
Label(Label),
|
||||||
/// A datetime
|
/// A datetime
|
||||||
Datetime(Datetime),
|
Datetime(Datetime),
|
||||||
|
/// A decimal value: `decimal("123.4500")`
|
||||||
|
Decimal(Decimal),
|
||||||
/// A duration
|
/// A duration
|
||||||
Duration(Duration),
|
Duration(Duration),
|
||||||
/// A content value: `[*Hi* there]`.
|
/// A content value: `[*Hi* there]`.
|
||||||
@ -136,6 +139,7 @@ impl Value {
|
|||||||
Self::Bytes(_) => Type::of::<Bytes>(),
|
Self::Bytes(_) => Type::of::<Bytes>(),
|
||||||
Self::Label(_) => Type::of::<Label>(),
|
Self::Label(_) => Type::of::<Label>(),
|
||||||
Self::Datetime(_) => Type::of::<Datetime>(),
|
Self::Datetime(_) => Type::of::<Datetime>(),
|
||||||
|
Self::Decimal(_) => Type::of::<Decimal>(),
|
||||||
Self::Duration(_) => Type::of::<Duration>(),
|
Self::Duration(_) => Type::of::<Duration>(),
|
||||||
Self::Content(_) => Type::of::<Content>(),
|
Self::Content(_) => Type::of::<Content>(),
|
||||||
Self::Styles(_) => Type::of::<Styles>(),
|
Self::Styles(_) => Type::of::<Styles>(),
|
||||||
@ -204,6 +208,7 @@ impl Value {
|
|||||||
Self::None => Content::empty(),
|
Self::None => Content::empty(),
|
||||||
Self::Int(v) => TextElem::packed(repr::format_int_with_base(v, 10)),
|
Self::Int(v) => TextElem::packed(repr::format_int_with_base(v, 10)),
|
||||||
Self::Float(v) => TextElem::packed(repr::display_float(v)),
|
Self::Float(v) => TextElem::packed(repr::display_float(v)),
|
||||||
|
Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
|
||||||
Self::Str(v) => TextElem::packed(v),
|
Self::Str(v) => TextElem::packed(v),
|
||||||
Self::Version(v) => TextElem::packed(eco_format!("{v}")),
|
Self::Version(v) => TextElem::packed(eco_format!("{v}")),
|
||||||
Self::Symbol(v) => TextElem::packed(v.get()),
|
Self::Symbol(v) => TextElem::packed(v.get()),
|
||||||
@ -248,6 +253,7 @@ impl Debug for Value {
|
|||||||
Self::Bytes(v) => Debug::fmt(v, f),
|
Self::Bytes(v) => Debug::fmt(v, f),
|
||||||
Self::Label(v) => Debug::fmt(v, f),
|
Self::Label(v) => Debug::fmt(v, f),
|
||||||
Self::Datetime(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::Duration(v) => Debug::fmt(v, f),
|
||||||
Self::Content(v) => Debug::fmt(v, f),
|
Self::Content(v) => Debug::fmt(v, f),
|
||||||
Self::Styles(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::Bytes(v) => v.repr(),
|
||||||
Self::Label(v) => v.repr(),
|
Self::Label(v) => v.repr(),
|
||||||
Self::Datetime(v) => v.repr(),
|
Self::Datetime(v) => v.repr(),
|
||||||
|
Self::Decimal(v) => v.repr(),
|
||||||
Self::Duration(v) => v.repr(),
|
Self::Duration(v) => v.repr(),
|
||||||
Self::Content(v) => v.repr(),
|
Self::Content(v) => v.repr(),
|
||||||
Self::Styles(v) => v.repr(),
|
Self::Styles(v) => v.repr(),
|
||||||
@ -337,6 +344,7 @@ impl Hash for Value {
|
|||||||
Self::Content(v) => v.hash(state),
|
Self::Content(v) => v.hash(state),
|
||||||
Self::Styles(v) => v.hash(state),
|
Self::Styles(v) => v.hash(state),
|
||||||
Self::Datetime(v) => v.hash(state),
|
Self::Datetime(v) => v.hash(state),
|
||||||
|
Self::Decimal(v) => v.hash(state),
|
||||||
Self::Duration(v) => v.hash(state),
|
Self::Duration(v) => v.hash(state),
|
||||||
Self::Array(v) => v.hash(state),
|
Self::Array(v) => v.hash(state),
|
||||||
Self::Dict(v) => v.hash(state),
|
Self::Dict(v) => v.hash(state),
|
||||||
@ -645,6 +653,7 @@ primitive! {
|
|||||||
primitive! { Bytes: "bytes", Bytes }
|
primitive! { Bytes: "bytes", Bytes }
|
||||||
primitive! { Label: "label", Label }
|
primitive! { Label: "label", Label }
|
||||||
primitive! { Datetime: "datetime", Datetime }
|
primitive! { Datetime: "datetime", Datetime }
|
||||||
|
primitive! { Decimal: "decimal", Decimal }
|
||||||
primitive! { Duration: "duration", Duration }
|
primitive! { Duration: "duration", Duration }
|
||||||
primitive! { Content: "content",
|
primitive! { Content: "content",
|
||||||
Content,
|
Content,
|
||||||
|
BIN
tests/ref/decimal-display-round.png
Normal file
BIN
tests/ref/decimal-display-round.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 446 B |
BIN
tests/ref/decimal-display.png
Normal file
BIN
tests/ref/decimal-display.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
@ -1,6 +1,21 @@
|
|||||||
--- calc-round ---
|
--- calc-round ---
|
||||||
#test(calc.round(calc.e, digits: 2), 2.72)
|
#test(calc.round(calc.e, digits: 2), 2.72)
|
||||||
#test(calc.round(calc.pi, digits: 2), 3.14)
|
#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 ---
|
--- calc-abs ---
|
||||||
// Test the `abs` function.
|
// Test the `abs` function.
|
||||||
@ -11,9 +26,11 @@
|
|||||||
#test(calc.abs(-3.14), 3.14)
|
#test(calc.abs(-3.14), 3.14)
|
||||||
#test(calc.abs(50%), 50%)
|
#test(calc.abs(50%), 50%)
|
||||||
#test(calc.abs(-25%), 25%)
|
#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 ---
|
--- calc-abs-bad-type ---
|
||||||
// Error: 11-22 expected integer, float, length, angle, ratio, or fraction, found string
|
// Error: 11-22 expected integer, float, length, angle, ratio, fraction, or decimal, found string
|
||||||
#calc.abs("no number")
|
#calc.abs("no number")
|
||||||
|
|
||||||
--- calc-even-and-odd ---
|
--- calc-even-and-odd ---
|
||||||
@ -30,6 +47,13 @@
|
|||||||
#test(calc.rem(5, -3), 2)
|
#test(calc.rem(5, -3), 2)
|
||||||
#test(calc.rem(22.5, 10), 2.5)
|
#test(calc.rem(22.5, 10), 2.5)
|
||||||
#test(calc.rem(9, 4.5), 0)
|
#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 ---
|
--- calc-rem-divisor-zero-1 ---
|
||||||
// Error: 14-15 divisor must not be zero
|
// Error: 14-15 divisor must not be zero
|
||||||
@ -39,6 +63,10 @@
|
|||||||
// Error: 16-19 divisor must not be zero
|
// Error: 16-19 divisor must not be zero
|
||||||
#calc.rem(3.0, 0.0)
|
#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 ---
|
--- calc-div-euclid ---
|
||||||
// Test the `div-euclid` function.
|
// Test the `div-euclid` function.
|
||||||
#test(calc.div-euclid(7, 3), 2)
|
#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(-7, -3), 3)
|
#test(calc.div-euclid(-7, -3), 3)
|
||||||
#test(calc.div-euclid(2.5, 2), 1)
|
#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 ---
|
--- calc-div-euclid-divisor-zero-1 ---
|
||||||
// Error: 21-22 divisor must not be zero
|
// Error: 21-22 divisor must not be zero
|
||||||
@ -55,6 +88,10 @@
|
|||||||
// Error: 23-26 divisor must not be zero
|
// Error: 23-26 divisor must not be zero
|
||||||
#calc.div-euclid(3.0, 0.0)
|
#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 ---
|
--- calc-rem-euclid ---
|
||||||
// Test the `rem-euclid` function.
|
// Test the `rem-euclid` function.
|
||||||
#test(calc.rem-euclid(7, 3), 1)
|
#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(-7, -3), 2)
|
#test(calc.rem-euclid(-7, -3), 2)
|
||||||
#test(calc.rem-euclid(2.5, 2), 0.5)
|
#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 ---
|
--- calc-rem-euclid-divisor-zero-1 ---
|
||||||
// Error: 21-22 divisor must not be zero
|
// Error: 21-22 divisor must not be zero
|
||||||
@ -71,13 +113,19 @@
|
|||||||
// Error: 23-26 divisor must not be zero
|
// Error: 23-26 divisor must not be zero
|
||||||
#calc.rem-euclid(3.0, 0.0)
|
#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 ---
|
--- calc-quo ---
|
||||||
// Test the `quo` function.
|
// Test the `quo` function.
|
||||||
#test(calc.quo(1, 1), 1)
|
#test(calc.quo(1, 1), 1)
|
||||||
#test(calc.quo(5, 3), 1)
|
#test(calc.quo(5, 3), 1)
|
||||||
#test(calc.quo(5, -3), -1)
|
#test(calc.quo(5, -3), -1)
|
||||||
#test(calc.quo(22.5, 10), 2)
|
#test(calc.quo(22.5, 10), 2.0)
|
||||||
#test(calc.quo(9, 4.5), 2)
|
#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 ---
|
--- calc-quo-divisor-zero-1 ---
|
||||||
// Error: 14-15 divisor must not be zero
|
// Error: 14-15 divisor must not be zero
|
||||||
@ -87,17 +135,24 @@
|
|||||||
// Error: 16-19 divisor must not be zero
|
// Error: 16-19 divisor must not be zero
|
||||||
#calc.quo(3.0, 0.0)
|
#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 ---
|
--- calc-min-and-max ---
|
||||||
// Test the `min` and `max` functions.
|
// Test the `min` and `max` functions.
|
||||||
#test(calc.min(2, -4), -4)
|
#test(calc.min(2, -4), -4)
|
||||||
#test(calc.min(3.5, 1e2, -0.1, 3), -0.1)
|
#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(-3, 11), 11)
|
||||||
|
#test(calc.max(decimal("3"), 45), 45)
|
||||||
#test(calc.min("hi"), "hi")
|
#test(calc.min("hi"), "hi")
|
||||||
|
|
||||||
--- calc-pow-log-exp-ln ---
|
--- calc-pow-log-exp-ln ---
|
||||||
// Test the `pow`, `log`, `exp`, and `ln` functions.
|
// Test the `pow`, `log`, `exp`, and `ln` functions.
|
||||||
#test(calc.pow(10, 0), 1)
|
#test(calc.pow(10, 0), 1)
|
||||||
#test(calc.pow(2, 4), 16)
|
#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.exp(2), calc.pow(calc.e, 2))
|
||||||
#test(calc.ln(10), calc.log(10, base: calc.e))
|
#test(calc.ln(10), calc.log(10, base: calc.e))
|
||||||
|
|
||||||
@ -156,6 +211,10 @@
|
|||||||
// Error: 2-25 the result is too large
|
// Error: 2-25 the result is too large
|
||||||
#calc.pow(2, 2147483647)
|
#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 ---
|
--- calc-pow-bad-exponent ---
|
||||||
// Error: 14-36 exponent may not be infinite, subnormal, or NaN
|
// Error: 14-36 exponent may not be infinite, subnormal, or NaN
|
||||||
#calc.pow(2, calc.pow(2.0, 10000.0))
|
#calc.pow(2, calc.pow(2.0, 10000.0))
|
||||||
@ -248,6 +307,30 @@
|
|||||||
// Error: 2-41 the result is too large
|
// Error: 2-41 the result is too large
|
||||||
#calc.lcm(15486487489457, 4874879896543)
|
#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 ---
|
--- calc-min-nothing ---
|
||||||
// Error: 2-12 expected at least one value
|
// Error: 2-12 expected at least one value
|
||||||
#calc.min()
|
#calc.min()
|
||||||
@ -259,3 +342,8 @@
|
|||||||
--- calc-max-uncomparable ---
|
--- calc-max-uncomparable ---
|
||||||
// Error: 16-19 cannot compare 1pt with 1em
|
// Error: 16-19 cannot compare 1pt with 1em
|
||||||
#calc.max(1em, 1pt)
|
#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)
|
||||||
|
79
tests/suite/foundations/decimal.typ
Normal file
79
tests/suite/foundations/decimal.typ
Normal file
@ -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"))
|
@ -6,10 +6,14 @@
|
|||||||
#test(float("3.1415"), 3.1415)
|
#test(float("3.1415"), 3.1415)
|
||||||
#test(float("-7654.321"), -7654.321)
|
#test(float("-7654.321"), -7654.321)
|
||||||
#test(float("\u{2212}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)
|
#test(type(float(10)), float)
|
||||||
|
|
||||||
--- float-constructor-bad-type ---
|
--- 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(float)
|
||||||
|
|
||||||
--- float-constructor-bad-value ---
|
--- float-constructor-bad-value ---
|
||||||
@ -55,20 +59,20 @@
|
|||||||
|
|
||||||
--- float-repr ---
|
--- float-repr ---
|
||||||
// Test the `repr` function with floats.
|
// Test the `repr` function with floats.
|
||||||
#repr(12.0) \
|
#test(repr(12.0), "12.0")
|
||||||
#repr(3.14) \
|
#test(repr(3.14), "3.14")
|
||||||
#repr(1234567890.0) \
|
#test(repr(1234567890.0), "1234567890.0")
|
||||||
#repr(0123456789.0) \
|
#test(repr(0123456789.0), "123456789.0")
|
||||||
#repr(0.0) \
|
#test(repr(0.0), "0.0")
|
||||||
#repr(-0.0) \
|
#test(repr(-0.0), "-0.0")
|
||||||
#repr(-1.0) \
|
#test(repr(-1.0), "-1.0")
|
||||||
#repr(-9876543210.0) \
|
#test(repr(-9876543210.0), "-9876543210.0")
|
||||||
#repr(-0987654321.0) \
|
#test(repr(-0987654321.0), "-987654321.0")
|
||||||
#repr(-3.14) \
|
#test(repr(-3.14), "-3.14")
|
||||||
#repr(4.0 - 8.0) \
|
#test(repr(4.0 - 8.0), "-4.0")
|
||||||
#repr(float.inf) \
|
#test(repr(float.inf), "float.inf")
|
||||||
#repr(-float.inf) \
|
#test(repr(-float.inf), "-float.inf")
|
||||||
#repr(float.nan)
|
#test(repr(float.nan), "float.nan")
|
||||||
|
|
||||||
--- float-display ---
|
--- float-display ---
|
||||||
// Test floats.
|
// Test floats.
|
||||||
|
@ -21,15 +21,34 @@
|
|||||||
#test(int("-834"), -834)
|
#test(int("-834"), -834)
|
||||||
#test(int("\u{2212}79"), -79)
|
#test(int("\u{2212}79"), -79)
|
||||||
#test(int(10 / 3), 3)
|
#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 ---
|
--- 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(10pt)
|
||||||
|
|
||||||
--- int-constructor-bad-value ---
|
--- int-constructor-bad-value ---
|
||||||
// Error: 6-12 invalid integer: nope
|
// Error: 6-12 invalid integer: nope
|
||||||
#int("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 ---
|
--- int-signum ---
|
||||||
// Test int `signum()`
|
// Test int `signum()`
|
||||||
#test(int(0).signum(), 0)
|
#test(int(0).signum(), 0)
|
||||||
@ -58,15 +77,15 @@
|
|||||||
|
|
||||||
--- int-repr ---
|
--- int-repr ---
|
||||||
// Test the `repr` function with integers.
|
// Test the `repr` function with integers.
|
||||||
#repr(12) \
|
#test(repr(12), "12")
|
||||||
#repr(1234567890) \
|
#test(repr(1234567890), "1234567890")
|
||||||
#repr(0123456789) \
|
#test(repr(0123456789), "123456789")
|
||||||
#repr(0) \
|
#test(repr(0), "0")
|
||||||
#repr(-0) \
|
#test(repr(-0), "0")
|
||||||
#repr(-1) \
|
#test(repr(-1), "-1")
|
||||||
#repr(-9876543210) \
|
#test(repr(-9876543210), "-9876543210")
|
||||||
#repr(-0987654321) \
|
#test(repr(-0987654321), "-987654321")
|
||||||
#repr(4 - 8)
|
#test(repr(4 - 8), "-4")
|
||||||
|
|
||||||
--- int-display ---
|
--- int-display ---
|
||||||
// Test integers.
|
// Test integers.
|
||||||
|
@ -23,6 +23,24 @@
|
|||||||
#test(str(-3.14), "−3.14")
|
#test(str(-3.14), "−3.14")
|
||||||
#test(str(4.0 - 8.0), "−4")
|
#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 ---
|
--- str-from-int ---
|
||||||
// Test the `str` function with integers.
|
// Test the `str` function with integers.
|
||||||
#test(str(12), "12")
|
#test(str(12), "12")
|
||||||
@ -36,7 +54,7 @@
|
|||||||
#test(str(4 - 8), "−4")
|
#test(str(4 - 8), "−4")
|
||||||
|
|
||||||
--- str-constructor-bad-type ---
|
--- 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([])
|
||||||
|
|
||||||
--- str-constructor-bad-base ---
|
--- str-constructor-bad-base ---
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
// Test math operators.
|
// Test math operators.
|
||||||
|
|
||||||
// Test plus and minus.
|
// 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 plus.
|
||||||
test(+v, v)
|
test(+v, v)
|
||||||
|
|
||||||
@ -60,6 +60,7 @@
|
|||||||
// Mathematical identities.
|
// Mathematical identities.
|
||||||
#let nums = (
|
#let nums = (
|
||||||
1, 3.14,
|
1, 3.14,
|
||||||
|
decimal("12.45"),
|
||||||
12pt, 3em, 12pt + 3em,
|
12pt, 3em, 12pt + 3em,
|
||||||
45deg,
|
45deg,
|
||||||
90%,
|
90%,
|
||||||
@ -76,12 +77,12 @@
|
|||||||
test(v - v, 0 * v)
|
test(v - v, 0 * v)
|
||||||
test(v + v, 2 * v)
|
test(v + v, 2 * v)
|
||||||
|
|
||||||
// Integer addition does not give a float.
|
// Integer or decimal addition does not give a float.
|
||||||
if type(v) != int {
|
if type(v) not in (int, decimal) {
|
||||||
test(v + v, 2.0 * v)
|
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)
|
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 ---
|
--- ops-multiply-inf-with-length ---
|
||||||
// Test that multiplying infinite numbers by certain units does not crash.
|
// Test that multiplying infinite numbers by certain units does not crash.
|
||||||
#(float("inf") * 1pt)
|
#(float("inf") * 1pt)
|
||||||
@ -164,6 +205,8 @@
|
|||||||
#test((:) == (a: 1), false)
|
#test((:) == (a: 1), false)
|
||||||
#test((a: 2 - 1.0, b: 2) == (b: 2, a: 1), true)
|
#test((a: 2 - 1.0, b: 2) == (b: 2, a: 1), true)
|
||||||
#test("a" != "a", false)
|
#test("a" != "a", false)
|
||||||
|
#test(decimal("1.234") == decimal("1.23400000000"), true)
|
||||||
|
#test(235 == decimal("235.0"), true)
|
||||||
|
|
||||||
// Functions compare by identity.
|
// Functions compare by identity.
|
||||||
#test(test == test, true)
|
#test(test == test, true)
|
||||||
@ -202,6 +245,10 @@
|
|||||||
#test(() >= (), true)
|
#test(() >= (), true)
|
||||||
#test(() <= (1,), true)
|
#test(() <= (1,), true)
|
||||||
#test((1,) <= (), false)
|
#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 ---
|
--- ops-in ---
|
||||||
// Test `in` operator.
|
// Test `in` operator.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user