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