Skip to main content

typst_library/foundations/
decimal.rs

1use std::fmt::{self, Display, Formatter};
2use std::hash::{Hash, Hasher};
3use std::ops::Neg;
4use std::str::FromStr;
5
6use ecow::{EcoString, eco_format};
7use rust_decimal::MathematicalOps;
8use typst_syntax::{Span, Spanned, ast};
9
10use crate::World;
11use crate::diag::{At, SourceResult, warning};
12use crate::engine::Engine;
13use crate::foundations::{Repr, Str, cast, func, repr, scope, ty};
14
15/// A fixed-point decimal number type.
16///
17/// This type should be used for precise arithmetic operations on numbers
18/// represented in base 10. A typical use case is representing currency.
19///
20/// = Example <example>
21/// ```example
22/// Decimal: #(decimal("0.1") + decimal("0.2")) \
23/// Float: #(0.1 + 0.2)
24/// ```
25///
26/// = Construction and casts <construction-and-casts>
27/// To create a decimal number, use the `{decimal(string)}` constructor, such as
28/// in `{decimal("3.141592653")}` _(note the double quotes)._ This constructor
29/// preserves all given fractional digits, provided they are representable as
30/// per the limits specified below (otherwise, an error is raised).
31///
32/// You can also convert any @int[integer] to a decimal with the
33/// `{decimal(int)}` constructor, e.g. `{decimal(59)}`. However, note that
34/// constructing a decimal from a @float[floating-point number], while
35/// supported, *is an imprecise conversion and therefore discouraged.* A warning
36/// will be raised if Typst detects that there was an accidental `float` to
37/// `decimal` cast through its constructor, e.g. if writing `{decimal(3.14)}`
38/// (note the lack of double quotes, indicating this is an accidental `float`
39/// cast and therefore imprecise). It is recommended to use strings for constant
40/// decimal values instead (e.g. `{decimal("3.14")}`).
41///
42/// The precision of a `float` to `decimal` cast can be slightly improved by
43/// rounding the result to 15 digits with @calc.round, but there are still no
44/// precision guarantees for that kind of conversion.
45///
46/// = Operations <operations>
47/// Basic arithmetic operations are supported on two decimals and on pairs of
48/// decimals and integers.
49///
50/// Built-in operations between `float` and `decimal` are not supported in order
51/// to guard against accidental loss of precision. They will raise an error
52/// instead.
53///
54/// Certain `calc` functions, such as trigonometric functions and power between
55/// two real numbers, are also only supported for `float` (although raising
56/// `decimal` to integer exponents is supported). You can opt into potentially
57/// imprecise operations with the `{float(decimal)}` constructor, which casts
58/// the `decimal` number into a `float`, allowing for operations without
59/// precision guarantees.
60///
61/// = Displaying decimals <displaying-decimals>
62/// To display a decimal, simply insert the value into the document. To only
63/// display a certain number of digits, @calc.round[round] the decimal first.
64/// Localized formatting of decimals and other numbers is not yet supported, but
65/// planned for the future.
66///
67/// You can convert decimals to strings using the @str constructor. This way,
68/// you can post-process the displayed representation, e.g. to replace the
69/// period with a comma (as a stand-in for proper built-in localization to
70/// languages that use the comma).
71///
72/// = Precision and limits <precision-and-limits>
73/// A `decimal` number has a limit of 28 to 29 significant base-10 digits. This
74/// includes the sum of digits before and after the decimal point. As such,
75/// numbers with more fractional digits have a smaller range. The maximum and
76/// minimum `decimal` numbers have a value of `{79228162514264337593543950335}`
77/// and `{-79228162514264337593543950335}` respectively. In contrast with
78/// @float, this type does not support infinity or NaN, so overflowing or
79/// underflowing operations will raise an error.
80///
81/// Typical operations between `decimal` numbers, such as addition,
82/// multiplication, and @calc.pow[power] to an integer, will be highly precise
83/// due to their fixed-point representation. Note, however, that multiplication
84/// and division may not preserve all digits in some edge cases: while they are
85/// considered precise, digits past the limits specified above are rounded off
86/// and lost, so some loss of precision beyond the maximum representable digits
87/// is possible. Note that this behavior can be observed not only when dividing,
88/// but also when multiplying by numbers between 0 and 1, as both operations can
89/// push a number's fractional digits beyond the limits described above, leading
90/// to rounding. When those two operations do not surpass the digit limits, they
91/// are fully precise.
92#[ty(scope, cast)]
93#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
94pub struct Decimal(rust_decimal::Decimal);
95
96impl Decimal {
97    pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO);
98    pub const ONE: Self = Self(rust_decimal::Decimal::ONE);
99    pub const MIN: Self = Self(rust_decimal::Decimal::MIN);
100    pub const MAX: Self = Self(rust_decimal::Decimal::MAX);
101
102    /// Whether this decimal value is zero.
103    pub const fn is_zero(self) -> bool {
104        self.0.is_zero()
105    }
106
107    /// Whether this decimal value is negative.
108    pub const fn is_negative(self) -> bool {
109        self.0.is_sign_negative()
110    }
111
112    /// Whether this decimal has fractional part equal to zero (is an integer).
113    pub fn is_integer(self) -> bool {
114        self.0.is_integer()
115    }
116
117    /// Computes the absolute value of this decimal.
118    pub fn abs(self) -> Self {
119        Self(self.0.abs())
120    }
121
122    /// Computes the largest integer less than or equal to this decimal.
123    ///
124    /// A decimal is returned as this may not be within `i64`'s range of
125    /// values.
126    pub fn floor(self) -> Self {
127        Self(self.0.floor())
128    }
129
130    /// Computes the smallest integer greater than or equal to this decimal.
131    ///
132    /// A decimal is returned as this may not be within `i64`'s range of
133    /// values.
134    pub fn ceil(self) -> Self {
135        Self(self.0.ceil())
136    }
137
138    /// Returns the integer part of this decimal.
139    pub fn trunc(self) -> Self {
140        Self(self.0.trunc())
141    }
142
143    /// Returns the fractional part of this decimal (with the integer part set
144    /// to zero).
145    pub fn fract(self) -> Self {
146        Self(self.0.fract())
147    }
148
149    /// Rounds this decimal up to the specified amount of digits with the
150    /// traditional rounding rules, using the "midpoint away from zero"
151    /// strategy (6.5 -> 7, -6.5 -> -7).
152    ///
153    /// If given a negative amount of digits, rounds to integer digits instead
154    /// with the same rounding strategy. For example, rounding to -3 digits
155    /// will turn 34567.89 into 35000.00 and -34567.89 into -35000.00.
156    ///
157    /// Note that this can return `None` when using negative digits where the
158    /// rounded number would overflow the available range for decimals.
159    pub fn round(self, digits: i32) -> Option<Self> {
160        // Positive digits can be handled by just rounding with rust_decimal.
161        if let Ok(positive_digits) = u32::try_from(digits) {
162            return Some(Self(self.0.round_dp_with_strategy(
163                positive_digits,
164                rust_decimal::RoundingStrategy::MidpointAwayFromZero,
165            )));
166        }
167
168        // We received negative digits, so we round to integer digits.
169        let mut num = self.0;
170        let old_scale = num.scale();
171        let digits = -digits as u32;
172
173        let (Ok(_), Some(ten_to_digits)) = (
174            // Same as dividing by 10^digits.
175            num.set_scale(old_scale + digits),
176            rust_decimal::Decimal::TEN.checked_powi(digits as i64),
177        ) else {
178            // Scaling more than any possible amount of integer digits.
179            let mut zero = rust_decimal::Decimal::ZERO;
180            zero.set_sign_negative(self.is_negative());
181            return Some(Self(zero));
182        };
183
184        // Round to this integer digit.
185        num = num.round_dp_with_strategy(
186            0,
187            rust_decimal::RoundingStrategy::MidpointAwayFromZero,
188        );
189
190        // Multiply by 10^digits again, which can overflow and fail.
191        num.checked_mul(ten_to_digits).map(Self)
192    }
193
194    /// Attempts to add two decimals.
195    ///
196    /// Returns `None` on overflow or underflow.
197    pub fn checked_add(self, other: Self) -> Option<Self> {
198        self.0.checked_add(other.0).map(Self)
199    }
200
201    /// Attempts to subtract a decimal from another.
202    ///
203    /// Returns `None` on overflow or underflow.
204    pub fn checked_sub(self, other: Self) -> Option<Self> {
205        self.0.checked_sub(other.0).map(Self)
206    }
207
208    /// Attempts to multiply two decimals.
209    ///
210    /// Returns `None` on overflow or underflow.
211    pub fn checked_mul(self, other: Self) -> Option<Self> {
212        self.0.checked_mul(other.0).map(Self)
213    }
214
215    /// Attempts to divide two decimals.
216    ///
217    /// Returns `None` if `other` is zero, as well as on overflow or underflow.
218    pub fn checked_div(self, other: Self) -> Option<Self> {
219        self.0.checked_div(other.0).map(Self)
220    }
221
222    /// Attempts to obtain the quotient of Euclidean division between two
223    /// decimals. Implemented similarly to [`f64::div_euclid`].
224    ///
225    /// The returned quotient is truncated and adjusted if the remainder was
226    /// negative.
227    ///
228    /// Returns `None` if `other` is zero, as well as on overflow or underflow.
229    pub fn checked_div_euclid(self, other: Self) -> Option<Self> {
230        let q = self.0.checked_div(other.0)?.trunc();
231        if self
232            .0
233            .checked_rem(other.0)
234            .as_ref()
235            .is_some_and(rust_decimal::Decimal::is_sign_negative)
236        {
237            return if other.0.is_sign_positive() {
238                q.checked_sub(rust_decimal::Decimal::ONE).map(Self)
239            } else {
240                q.checked_add(rust_decimal::Decimal::ONE).map(Self)
241            };
242        }
243        Some(Self(q))
244    }
245
246    /// Attempts to obtain the remainder of Euclidean division between two
247    /// decimals. Implemented similarly to [`f64::rem_euclid`].
248    ///
249    /// The returned decimal `r` is non-negative within the range
250    /// `0.0 <= r < other.abs()`.
251    ///
252    /// Returns `None` if `other` is zero, as well as on overflow or underflow.
253    pub fn checked_rem_euclid(self, other: Self) -> Option<Self> {
254        let r = self.0.checked_rem(other.0)?;
255        Some(Self(if r.is_sign_negative() { r.checked_add(other.0.abs())? } else { r }))
256    }
257
258    /// Attempts to calculate the remainder of the division of two decimals.
259    ///
260    /// Returns `None` if `other` is zero, as well as on overflow or underflow.
261    pub fn checked_rem(self, other: Self) -> Option<Self> {
262        self.0.checked_rem(other.0).map(Self)
263    }
264
265    /// Attempts to take one decimal to the power of an integer.
266    ///
267    /// Returns `None` for invalid operands, as well as on overflow or
268    /// underflow.
269    pub fn checked_powi(self, other: i64) -> Option<Self> {
270        self.0.checked_powi(other).map(Self)
271    }
272}
273
274#[scope]
275impl Decimal {
276    /// Converts a value to a `decimal`.
277    ///
278    /// It is recommended to use a string to construct the decimal number, or an
279    /// @int[integer] (if desired). The string must contain a number in the
280    /// format `{"3.14159"}` (or `{"-3.141519"}` for negative numbers). The
281    /// fractional digits are fully preserved; if that's not possible due to the
282    /// limit of significant digits (around 28 to 29) having been reached, an
283    /// error is raised as the given decimal number wouldn't be representable.
284    ///
285    /// While this constructor can be used with @float[floating-point numbers]
286    /// to cast them to `decimal`, doing so is *discouraged* as *this cast is
287    /// inherently imprecise.* It is easy to accidentally perform this cast by
288    /// writing `{decimal(1.234)}` (note the lack of double quotes), which is
289    /// why Typst will emit a warning in that case. Please write
290    /// `{decimal("1.234")}` instead for that particular case (initialization of
291    /// a constant decimal). Also note that floats that are NaN or infinite
292    /// cannot be cast to decimals and will raise an error.
293    ///
294    /// ```example
295    /// #decimal("1.222222222222222")
296    /// ```
297    #[func(constructor)]
298    pub fn construct(
299        engine: &mut Engine,
300        /// The value that should be converted to a decimal.
301        value: Spanned<ToDecimal>,
302    ) -> SourceResult<Decimal> {
303        match value.v {
304            ToDecimal::Str(str) => Self::from_str(&str.replace(repr::MINUS_SIGN, "-"))
305                .map_err(|_| eco_format!("invalid decimal: {str}"))
306                .at(value.span),
307            ToDecimal::Int(int) => Ok(Self::from(int)),
308            ToDecimal::Float(float) => {
309                warn_on_float_literal(engine, value.span);
310                Self::try_from(float)
311                    .map_err(|_| {
312                        eco_format!(
313                            "float is not a valid decimal: {}",
314                            repr::format_float(float, None, true, "")
315                        )
316                    })
317                    .at(value.span)
318            }
319            ToDecimal::Decimal(decimal) => Ok(decimal),
320        }
321    }
322}
323
324/// Emits a warning when a decimal is constructed from a float literal.
325fn warn_on_float_literal(engine: &mut Engine, span: Span) -> Option<()> {
326    let id = span.id()?;
327    let source = engine.world.source(id).ok()?;
328    let node = source.find(span)?;
329    if node.is::<ast::Float>() {
330        engine.sink.warn(warning!(
331            span,
332            "creating a decimal using imprecise float literal";
333            hint: "use a string in the decimal constructor to avoid loss \
334                   of precision: `decimal({})`",
335            node.leaf_text().repr();
336        ));
337    }
338    Some(())
339}
340
341impl FromStr for Decimal {
342    type Err = rust_decimal::Error;
343
344    fn from_str(s: &str) -> Result<Self, Self::Err> {
345        rust_decimal::Decimal::from_str_exact(s).map(Self)
346    }
347}
348
349impl From<i64> for Decimal {
350    fn from(value: i64) -> Self {
351        Self(rust_decimal::Decimal::from(value))
352    }
353}
354
355impl TryFrom<f64> for Decimal {
356    type Error = ();
357
358    /// Attempts to convert a Decimal to a float.
359    ///
360    /// This can fail if the float is infinite or NaN, or otherwise cannot be
361    /// represented by a decimal number.
362    fn try_from(value: f64) -> Result<Self, Self::Error> {
363        rust_decimal::Decimal::from_f64_retain(value).map(Self).ok_or(())
364    }
365}
366
367impl TryFrom<Decimal> for f64 {
368    type Error = rust_decimal::Error;
369
370    /// Attempts to convert a Decimal to a float.
371    ///
372    /// This should in principle be infallible according to the implementation,
373    /// but we mirror the decimal implementation's API either way.
374    fn try_from(value: Decimal) -> Result<Self, Self::Error> {
375        value.0.try_into()
376    }
377}
378
379impl TryFrom<Decimal> for i64 {
380    type Error = rust_decimal::Error;
381
382    /// Attempts to convert a Decimal to an integer.
383    ///
384    /// Returns an error if the decimal has a fractional part, or if there
385    /// would be overflow or underflow.
386    fn try_from(value: Decimal) -> Result<Self, Self::Error> {
387        value.0.try_into()
388    }
389}
390
391impl Display for Decimal {
392    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
393        if self.0.is_sign_negative() {
394            f.write_str(repr::MINUS_SIGN)?;
395        }
396        self.0.abs().fmt(f)
397    }
398}
399
400impl Repr for Decimal {
401    fn repr(&self) -> EcoString {
402        eco_format!("decimal({})", eco_format!("{}", self.0).repr())
403    }
404}
405
406impl Neg for Decimal {
407    type Output = Self;
408
409    fn neg(self) -> Self {
410        Self(-self.0)
411    }
412}
413
414impl Hash for Decimal {
415    fn hash<H: Hasher>(&self, state: &mut H) {
416        // `rust_decimal`'s Hash implementation normalizes decimals before
417        // hashing them. This means decimals with different scales but
418        // equivalent value not only compare equal but also hash equally. Here,
419        // we hash all bytes explicitly to ensure the scale is also considered.
420        // This means that 123.314 == 123.31400, but 123.314.hash() !=
421        // 123.31400.hash().
422        //
423        // Note that this implies that equal decimals can have different hashes,
424        // which might generate problems with certain data structures, such as
425        // HashSet and HashMap.
426        self.0.serialize().hash(state);
427    }
428}
429
430/// A value that can be cast to a decimal.
431pub enum ToDecimal {
432    /// A decimal to be converted to itself.
433    Decimal(Decimal),
434    /// A string with the decimal's representation.
435    Str(EcoString),
436    /// An integer to be converted to the equivalent decimal.
437    Int(i64),
438    /// A float to be converted to the equivalent decimal.
439    Float(f64),
440}
441
442cast! {
443    ToDecimal,
444    v: Decimal => Self::Decimal(v),
445    v: i64 => Self::Int(v),
446    v: bool => Self::Int(v as i64),
447    v: f64 => Self::Float(v),
448    v: Str => Self::Str(EcoString::from(v)),
449}
450
451#[cfg(test)]
452mod tests {
453    use std::str::FromStr;
454
455    use typst_utils::hash128;
456
457    use super::Decimal;
458
459    #[test]
460    fn test_decimals_with_equal_scales_hash_identically() {
461        let a = Decimal::from_str("3.14").unwrap();
462        let b = Decimal::from_str("3.14").unwrap();
463        assert_eq!(a, b);
464        assert_eq!(hash128(&a), hash128(&b));
465    }
466
467    #[test]
468    fn test_decimals_with_different_scales_hash_differently() {
469        let a = Decimal::from_str("3.140").unwrap();
470        let b = Decimal::from_str("3.14000").unwrap();
471        assert_eq!(a, b);
472        assert_ne!(hash128(&a), hash128(&b));
473    }
474
475    #[track_caller]
476    fn test_round(value: &str, digits: i32, expected: &str) {
477        assert_eq!(
478            Decimal::from_str(value).unwrap().round(digits),
479            Some(Decimal::from_str(expected).unwrap()),
480        );
481    }
482
483    #[test]
484    fn test_decimal_positive_round() {
485        test_round("312.55553", 0, "313.00000");
486        test_round("312.55553", 3, "312.556");
487        test_round("312.5555300000", 3, "312.556");
488        test_round("-312.55553", 3, "-312.556");
489        test_round("312.55553", 28, "312.55553");
490        test_round("312.55553", 2341, "312.55553");
491        test_round("-312.55553", 2341, "-312.55553");
492    }
493
494    #[test]
495    fn test_decimal_negative_round() {
496        test_round("4596.55553", -1, "4600");
497        test_round("4596.555530000000", -1, "4600");
498        test_round("-4596.55553", -3, "-5000");
499        test_round("4596.55553", -28, "0");
500        test_round("-4596.55553", -2341, "0");
501        assert_eq!(Decimal::MAX.round(-1), None);
502    }
503}