mirror of
https://github.com/typst/typst
synced 2025-06-20 04:12:54 +08:00
505 lines
18 KiB
Rust
505 lines
18 KiB
Rust
use std::fmt::{self, Display, Formatter};
|
|
use std::hash::{Hash, Hasher};
|
|
use std::ops::Neg;
|
|
use std::str::FromStr;
|
|
|
|
use ecow::{eco_format, EcoString};
|
|
use rust_decimal::MathematicalOps;
|
|
use typst_syntax::{ast, Span, Spanned};
|
|
|
|
use crate::diag::{warning, At, SourceResult};
|
|
use crate::engine::Engine;
|
|
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str};
|
|
use crate::World;
|
|
|
|
/// A fixed-point decimal number type.
|
|
///
|
|
/// This type should be used for precise arithmetic operations on numbers
|
|
/// represented in base 10. A typical use case is representing currency.
|
|
///
|
|
/// # Example
|
|
/// ```example
|
|
/// Decimal: #(decimal("0.1") + decimal("0.2")) \
|
|
/// Float: #(0.1 + 0.2)
|
|
/// ```
|
|
///
|
|
/// # Construction and casts
|
|
/// To create a decimal number, use the `{decimal(string)}` constructor, such as
|
|
/// in `{decimal("3.141592653")}` **(note the double quotes!)**. This
|
|
/// constructor preserves all given fractional digits, provided they are
|
|
/// representable as per the limits specified below (otherwise, an error is
|
|
/// raised).
|
|
///
|
|
/// You can 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). It is recommended to use strings for
|
|
/// constant decimal values instead (e.g. `{decimal("3.14")}`).
|
|
///
|
|
/// 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.
|
|
///
|
|
/// # Operations
|
|
/// Basic arithmetic operations are supported on two decimals and on pairs of
|
|
/// decimals and integers.
|
|
///
|
|
/// Built-in operations between `float` and `decimal` are not supported in order
|
|
/// to guard against accidental loss of precision. They will raise an error
|
|
/// instead.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// # Displaying decimals
|
|
/// To display a decimal, simply insert the value into the document. To only
|
|
/// display a certain number of digits, [round]($calc.round) the decimal first.
|
|
/// Localized formatting of decimals and other numbers is not yet supported, but
|
|
/// planned for the future.
|
|
///
|
|
/// You can convert decimals to strings using the [`str`] constructor. This way,
|
|
/// you can post-process the displayed representation, e.g. to replace the
|
|
/// period with a comma (as a stand-in for proper built-in localization to
|
|
/// languages that use the comma).
|
|
///
|
|
/// # Precision and 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`], this type does not support infinity or NaN, so overflowing or
|
|
/// underflowing operations will raise an error.
|
|
///
|
|
/// 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 above 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 above, leading
|
|
/// to rounding. When those two operations do not surpass the digit limits, they
|
|
/// are fully precise.
|
|
#[ty(scope, cast)]
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, 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);
|
|
pub const MIN: Self = Self(rust_decimal::Decimal::MIN);
|
|
pub const MAX: Self = Self(rust_decimal::Decimal::MAX);
|
|
|
|
/// 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).
|
|
///
|
|
/// If given a negative amount of digits, rounds to integer digits instead
|
|
/// with the same rounding strategy. For example, rounding to -3 digits
|
|
/// will turn 34567.89 into 35000.00 and -34567.89 into -35000.00.
|
|
///
|
|
/// Note that this can return `None` when using negative digits where the
|
|
/// rounded number would overflow the available range for decimals.
|
|
pub fn round(self, digits: i32) -> Option<Self> {
|
|
// Positive digits can be handled by just rounding with rust_decimal.
|
|
if let Ok(positive_digits) = u32::try_from(digits) {
|
|
return Some(Self(self.0.round_dp_with_strategy(
|
|
positive_digits,
|
|
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
|
|
)));
|
|
}
|
|
|
|
// We received negative digits, so we round to integer digits.
|
|
let mut num = self.0;
|
|
let old_scale = num.scale();
|
|
let digits = -digits as u32;
|
|
|
|
let (Ok(_), Some(ten_to_digits)) = (
|
|
// Same as dividing by 10^digits.
|
|
num.set_scale(old_scale + digits),
|
|
rust_decimal::Decimal::TEN.checked_powi(digits as i64),
|
|
) else {
|
|
// Scaling more than any possible amount of integer digits.
|
|
let mut zero = rust_decimal::Decimal::ZERO;
|
|
zero.set_sign_negative(self.is_negative());
|
|
return Some(Self(zero));
|
|
};
|
|
|
|
// Round to this integer digit.
|
|
num = num.round_dp_with_strategy(
|
|
0,
|
|
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
|
|
);
|
|
|
|
// Multiply by 10^digits again, which can overflow and fail.
|
|
num.checked_mul(ten_to_digits).map(Self)
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// 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 that are NaN or infinite
|
|
/// cannot be cast to decimals and will raise an error.
|
|
///
|
|
/// ```example
|
|
/// #decimal("1.222222222222222")
|
|
/// ```
|
|
#[func(constructor)]
|
|
pub fn construct(
|
|
engine: &mut Engine,
|
|
/// The value that should be converted to a decimal.
|
|
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)
|
|
}
|
|
ToDecimal::Decimal(decimal) => Ok(decimal),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
impl Hash for Decimal {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
// `rust_decimal`'s Hash implementation normalizes decimals before
|
|
// hashing them. This means decimals with different scales but
|
|
// equivalent value not only compare equal but also hash equally. Here,
|
|
// we hash all bytes explicitly to ensure the scale is also considered.
|
|
// This means that 123.314 == 123.31400, but 123.314.hash() !=
|
|
// 123.31400.hash().
|
|
//
|
|
// Note that this implies that equal decimals can have different hashes,
|
|
// which might generate problems with certain data structures, such as
|
|
// HashSet and HashMap.
|
|
self.0.serialize().hash(state);
|
|
}
|
|
}
|
|
|
|
/// A value that can be cast to a decimal.
|
|
pub enum ToDecimal {
|
|
/// A decimal to be converted to itself.
|
|
Decimal(Decimal),
|
|
/// 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: Decimal => Self::Decimal(v),
|
|
v: i64 => Self::Int(v),
|
|
v: bool => Self::Int(v as i64),
|
|
v: f64 => Self::Float(v),
|
|
v: Str => Self::Str(EcoString::from(v)),
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::str::FromStr;
|
|
|
|
use typst_utils::hash128;
|
|
|
|
use super::Decimal;
|
|
|
|
#[test]
|
|
fn test_decimals_with_equal_scales_hash_identically() {
|
|
let a = Decimal::from_str("3.14").unwrap();
|
|
let b = Decimal::from_str("3.14").unwrap();
|
|
assert_eq!(a, b);
|
|
assert_eq!(hash128(&a), hash128(&b));
|
|
}
|
|
|
|
#[test]
|
|
fn test_decimals_with_different_scales_hash_differently() {
|
|
let a = Decimal::from_str("3.140").unwrap();
|
|
let b = Decimal::from_str("3.14000").unwrap();
|
|
assert_eq!(a, b);
|
|
assert_ne!(hash128(&a), hash128(&b));
|
|
}
|
|
|
|
#[track_caller]
|
|
fn test_round(value: &str, digits: i32, expected: &str) {
|
|
assert_eq!(
|
|
Decimal::from_str(value).unwrap().round(digits),
|
|
Some(Decimal::from_str(expected).unwrap()),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decimal_positive_round() {
|
|
test_round("312.55553", 0, "313.00000");
|
|
test_round("312.55553", 3, "312.556");
|
|
test_round("312.5555300000", 3, "312.556");
|
|
test_round("-312.55553", 3, "-312.556");
|
|
test_round("312.55553", 28, "312.55553");
|
|
test_round("312.55553", 2341, "312.55553");
|
|
test_round("-312.55553", 2341, "-312.55553");
|
|
}
|
|
|
|
#[test]
|
|
fn test_decimal_negative_round() {
|
|
test_round("4596.55553", -1, "4600");
|
|
test_round("4596.555530000000", -1, "4600");
|
|
test_round("-4596.55553", -3, "-5000");
|
|
test_round("4596.55553", -28, "0");
|
|
test_round("-4596.55553", -2341, "0");
|
|
assert_eq!(Decimal::MAX.round(-1), None);
|
|
}
|
|
}
|