Skip to main content

typst_library/foundations/
float.rs

1use std::num::ParseFloatError;
2
3use ecow::{EcoString, eco_format};
4use typst_utils::Scalar;
5
6use crate::diag::{StrResult, bail};
7use crate::foundations::{
8    Bytes, Decimal, Endianness, Repr, Str, cast, func, repr, scope, ty,
9};
10use crate::layout::Ratio;
11
12/// A floating-point number.
13///
14/// A limited-precision representation of a real number. Typst uses 64 bits to
15/// store floats. Wherever a float is expected, you can also pass an
16/// @int[integer].
17///
18/// You can convert a value to a float with this type's constructor.
19///
20/// NaN and positive infinity are available as `{float.nan}` and `{float.inf}`
21/// respectively.
22///
23/// = Example <example>
24/// ```example
25/// #3.14 \
26/// #1e4 \
27/// #(10 / 4)
28/// ```
29#[ty(scope, cast, name = "float")]
30type f64;
31
32#[scope(ext)]
33impl f64 {
34    /// Positive infinity.
35    const INF: f64 = f64::INFINITY;
36
37    /// A NaN value, as defined by the
38    /// [IEEE 754 standard](https://en.wikipedia.org/wiki/IEEE_754).
39    const NAN: f64 = f64::NAN;
40
41    /// Converts a value to a float.
42    ///
43    /// - Booleans are converted to `0.0` or `1.0`.
44    /// - Integers are converted to the closest 64-bit float. For integers with
45    ///   absolute value less than `{calc.pow(2, 53)}`, this conversion is
46    ///   exact.
47    /// - Ratios are divided by 100%.
48    /// - Strings are parsed in base 10 to the closest 64-bit float. Exponential
49    ///   notation is supported.
50    ///
51    /// ```example
52    /// #float(false) \
53    /// #float(true) \
54    /// #float(4) \
55    /// #float(40%) \
56    /// #float("2.7") \
57    /// #float("1e5")
58    /// ```
59    #[func(constructor)]
60    pub fn construct(
61        /// The value that should be converted to a float.
62        value: ToFloat,
63    ) -> f64 {
64        value.0
65    }
66
67    /// Checks if a float is not a number.
68    ///
69    /// In IEEE 754, more than one bit pattern represents a NaN. This function
70    /// returns `true` if the float is any of those bit patterns.
71    ///
72    /// ```example
73    /// #float.is-nan(0) \
74    /// #float.is-nan(1) \
75    /// #float.is-nan(float.nan)
76    /// ```
77    #[func]
78    pub fn is_nan(self) -> bool {
79        f64::is_nan(self)
80    }
81
82    /// Checks if a float is infinite.
83    ///
84    /// Floats can represent positive infinity and negative infinity. This
85    /// function returns `{true}` if the float is an infinity.
86    ///
87    /// ```example
88    /// #float.is-infinite(0) \
89    /// #float.is-infinite(1) \
90    /// #float.is-infinite(float.inf)
91    /// ```
92    #[func]
93    pub fn is_infinite(self) -> bool {
94        f64::is_infinite(self)
95    }
96
97    /// Calculates the sign of a floating point number.
98    ///
99    /// - If the number is positive (including `{+0.0}`), returns `{1.0}`.
100    /// - If the number is negative (including `{-0.0}`), returns `{-1.0}`.
101    /// - If the number is NaN, returns `{float.nan}`.
102    ///
103    /// ```example
104    /// #(5.0).signum() \
105    /// #(-5.0).signum() \
106    /// #(0.0).signum() \
107    /// #float.nan.signum()
108    /// ```
109    #[func]
110    pub fn signum(self) -> f64 {
111        f64::signum(self)
112    }
113
114    /// Interprets bytes as a float.
115    ///
116    /// ```example
117    /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
118    /// #float.from-bytes(bytes((63, 240, 0, 0, 0, 0, 0, 0)), endian: "big")
119    /// ```
120    #[func]
121    pub fn from_bytes(
122        /// The bytes that should be converted to a float.
123        ///
124        /// Must have a length of either 4 or 8. The bytes are then interpreted
125        /// in #link("https://en.wikipedia.org/wiki/IEEE_754")[IEEE 754]'s
126        /// binary32 (single-precision) or binary64 (double-precision) format
127        /// depending on the length of the bytes.
128        bytes: Bytes,
129        /// The endianness of the conversion.
130        #[named]
131        #[default(Endianness::Little)]
132        endian: Endianness,
133    ) -> StrResult<f64> {
134        // Convert slice to an array of length 4 or 8.
135        if let Ok(buffer) = <[u8; 8]>::try_from(bytes.as_ref()) {
136            return Ok(match endian {
137                Endianness::Little => f64::from_le_bytes(buffer),
138                Endianness::Big => f64::from_be_bytes(buffer),
139            });
140        };
141        if let Ok(buffer) = <[u8; 4]>::try_from(bytes.as_ref()) {
142            return Ok(match endian {
143                Endianness::Little => f32::from_le_bytes(buffer),
144                Endianness::Big => f32::from_be_bytes(buffer),
145            } as f64);
146        };
147
148        bail!("bytes must have a length of 4 or 8");
149    }
150
151    /// Converts a float to bytes.
152    ///
153    /// ```example
154    /// #array(1.0.to-bytes(endian: "big")) \
155    /// #array(1.0.to-bytes())
156    /// ```
157    #[func]
158    pub fn to_bytes(
159        self,
160        /// The endianness of the conversion.
161        #[named]
162        #[default(Endianness::Little)]
163        endian: Endianness,
164        /// The size of the resulting bytes.
165        ///
166        /// This must be either 4 or 8. The call will return the representation
167        /// of this float in either
168        /// #link("https://en.wikipedia.org/wiki/IEEE_754")[IEEE 754]'s binary32
169        /// (single-precision) or binary64 (double-precision) format depending
170        /// on the provided size.
171        #[named]
172        #[default(8)]
173        size: u32,
174    ) -> StrResult<Bytes> {
175        Ok(match size {
176            8 => Bytes::new(match endian {
177                Endianness::Little => self.to_le_bytes(),
178                Endianness::Big => self.to_be_bytes(),
179            }),
180            4 => Bytes::new(match endian {
181                Endianness::Little => (self as f32).to_le_bytes(),
182                Endianness::Big => (self as f32).to_be_bytes(),
183            }),
184            _ => bail!("size must be either 4 or 8"),
185        })
186    }
187}
188
189impl Repr for f64 {
190    fn repr(&self) -> EcoString {
191        repr::format_float(*self, None, true, "")
192    }
193}
194
195/// A value that can be cast to a float.
196pub struct ToFloat(f64);
197
198cast! {
199    ToFloat,
200    v: f64 => Self(v),
201    v: bool => Self(v as i64 as f64),
202    v: i64 => Self(v as f64),
203    v: Decimal => Self(f64::try_from(v).map_err(|_| eco_format!("invalid float: {v}"))?),
204    v: Ratio => Self(v.get()),
205    v: Str => Self(
206        parse_float(v.clone().into())
207            .map_err(|_| eco_format!("invalid float: {v}"))?
208    ),
209}
210
211fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> {
212    s.replace(repr::MINUS_SIGN, "-").parse()
213}
214
215/// A floating-point number that must be positive (strictly larger than zero).
216#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
217pub struct PositiveF64(f64);
218
219impl PositiveF64 {
220    /// Wrap a float if it is positive.
221    pub fn new(value: f64) -> Option<Self> {
222        (value > 0.0).then_some(Self(value))
223    }
224
225    /// Get the underlying value.
226    pub fn get(self) -> f64 {
227        self.0
228    }
229}
230
231cast! {
232    PositiveF64,
233    self => self.get().into_value(),
234    v: f64 => Self::new(v).ok_or("number must be positive")?,
235}
236
237cast! {
238    Scalar,
239    self => self.get().into_value(),
240    v: f64 => Self::new(v),
241}
242
243cast! {
244    f32,
245    self => (self as f64).into_value(),
246}