spinoso_math/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3    clippy::let_underscore_untyped,
4    reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7    clippy::question_mark,
8    reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13    clippy::unnecessary_lazy_evaluations,
14    reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
15)]
16#![cfg_attr(
17    test,
18    allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
19)]
20#![allow(unknown_lints)]
21#![warn(
22    missing_copy_implementations,
23    missing_debug_implementations,
24    missing_docs,
25    rust_2024_compatibility,
26    trivial_casts,
27    trivial_numeric_casts,
28    unused_qualifications,
29    variant_size_differences
30)]
31#![forbid(unsafe_code)]
32// Enable feature callouts in generated documentation:
33// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
34//
35// This approach is borrowed from tokio.
36#![cfg_attr(docsrs, feature(doc_cfg))]
37#![cfg_attr(docsrs, feature(doc_alias))]
38
39//! The Ruby Math module.
40//!
41//! The Math module contains module functions for basic trigonometric and
42//! transcendental functions. See class [`Float`] for a list of constants that
43//! define Ruby's floating point accuracy.
44//!
45//! This crate defines math operations as free functions. These functions differ
46//! from those defined in Rust [`core`] by returning a [`DomainError`] when an
47//! input is outside the domain of the function and results in [`NaN`].
48//!
49//! `spinoso-math` assumes the Ruby VM uses double precision [`f64`] floats.
50//!
51//! # Examples
52//!
53//! Compute the hypotenuse:
54//!
55//! ```
56//! use spinoso_math as math;
57//! assert_eq!(math::hypot(3.0, 4.0), 5.0);
58//! ```
59//!
60//! Compute log with respect to the base 10 and handle domain errors:
61//!
62//! ```
63//! use spinoso_math as math;
64//! assert_eq!(math::log10(1.0), Ok(0.0));
65//! assert_eq!(math::log10(10.0), Ok(1.0));
66//! assert_eq!(math::log10(1e100), Ok(100.0));
67//!
68//! assert_eq!(math::log10(0.0), Ok(f64::NEG_INFINITY));
69//! assert!(math::log10(-0.1).is_err());
70//!
71//! // A NaN return value is distinct from a `DomainError`.
72//! assert!(matches!(math::log10(f64::NAN), Ok(result) if result.is_nan()));
73//! ```
74//!
75//! # Crate features
76//!
77//! All features are enabled by default.
78//!
79//! - **full** - Enables implementations of math functions that do not have
80//!   implementations in Rust [`core`]. Dropping this feature removes the
81//!   [`libm`] dependency.
82//!
83#![cfg_attr(not(feature = "full"), doc = "[`libm`]: https://docs.rs/libm/latest/libm/")]
84//! [`Float`]: https://ruby-doc.org/core-3.1.2/Float.html
85//! [`NaN`]: f64::NAN
86//! [`alloc`]: https://doc.rust-lang.org/alloc/
87
88// Ensure code blocks in `README.md` compile
89#[cfg(doctest)]
90#[doc = include_str!("../README.md")]
91mod readme {}
92
93#[doc(inline)]
94pub use core::f64::consts::E;
95#[doc(inline)]
96pub use core::f64::consts::PI;
97use core::fmt;
98use std::error;
99
100mod math;
101
102pub use math::*;
103
104/// A handle to the `Math` module.
105///
106/// This is a copy zero-sized type with no associated methods. This type exists
107/// so a Ruby VM can attempt to unbox this type and statically dispatch to
108/// functions defined in this crate.
109///
110/// # Examples
111///
112/// ```
113/// # use spinoso_math::Math;
114/// const MATH: Math = Math::new();
115/// ```
116#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
117pub struct Math {
118    _private: (),
119}
120
121impl Math {
122    /// Constructs a new, default `Math`.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// # use spinoso_math::Math;
128    /// const MATH: Math = Math::new();
129    /// ```
130    #[inline]
131    #[must_use]
132    pub const fn new() -> Self {
133        Self { _private: () }
134    }
135}
136
137/// Sum type of all errors possibly returned from `Math` functions.
138///
139/// Math functions in `spinoso-math` return errors in the following conditions:
140///
141/// - The parameters evaluate to a result that is out of range.
142/// - The function is not implemented due to missing compile-time flags.
143#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
144pub enum Error {
145    /// Error that indicates a math function returned a value that was out of
146    /// range.
147    ///
148    /// This error can be used to differentiate between [`NaN`](f64::NAN) inputs
149    /// and what would be `NaN` outputs.
150    ///
151    /// See [`DomainError`].
152    Domain(DomainError),
153    /// Error that indicates a `Math` module function is not implemented.
154    ///
155    /// See [`NotImplementedError`].
156    NotImplemented(NotImplementedError),
157}
158
159impl Error {
160    /// Retrieve the exception message associated with this error.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// # use spinoso_math::{DomainError, Error, NotImplementedError};
166    /// let err = Error::from(DomainError::new());
167    /// assert_eq!(err.message(), "Math::DomainError");
168    ///
169    /// let err = Error::from(NotImplementedError::with_message(
170    ///     "Artichoke was not built with Math::erf support",
171    /// ));
172    /// assert_eq!(
173    ///     err.message(),
174    ///     "Artichoke was not built with Math::erf support"
175    /// );
176    /// ```
177    #[inline]
178    #[must_use]
179    pub const fn message(self) -> &'static str {
180        match self {
181            Self::Domain(err) => err.message(),
182            Self::NotImplemented(err) => err.message(),
183        }
184    }
185}
186
187impl From<DomainError> for Error {
188    #[inline]
189    fn from(err: DomainError) -> Self {
190        Self::Domain(err)
191    }
192}
193
194impl From<NotImplementedError> for Error {
195    #[inline]
196    fn from(err: NotImplementedError) -> Self {
197        Self::NotImplemented(err)
198    }
199}
200
201impl fmt::Display for Error {
202    #[inline]
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        f.write_str("Math error")
205    }
206}
207
208impl error::Error for Error {
209    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
210        match self {
211            Self::Domain(err) => Some(err),
212            Self::NotImplemented(err) => Some(err),
213        }
214    }
215}
216
217/// Error that indicates a math function evaluated to an out of range value.
218///
219/// Domain errors have an associated message.
220///
221/// This error corresponds to the [Ruby `Math::DomainError` Exception class]. It
222/// can be used to differentiate between [`NaN`](f64::NAN) inputs and what would
223/// be `NaN` outputs.
224///
225/// # Examples
226///
227/// ```
228/// # use spinoso_math::DomainError;
229/// let err = DomainError::new();
230/// assert_eq!(err.message(), "Math::DomainError");
231///
232/// let err = DomainError::with_message(r#"Numerical argument is out of domain - "acos""#);
233/// assert_eq!(
234///     err.message(),
235///     r#"Numerical argument is out of domain - "acos""#
236/// );
237/// ```
238///
239/// [Ruby `Math::DomainError` Exception class]: https://ruby-doc.org/core-3.1.2/Math/DomainError.html
240#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
241pub struct DomainError(&'static str);
242
243impl From<&'static str> for DomainError {
244    #[inline]
245    fn from(message: &'static str) -> Self {
246        Self(message)
247    }
248}
249
250impl DomainError {
251    /// Construct a new, default domain error.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// # use spinoso_math::DomainError;
257    /// const ERR: DomainError = DomainError::new();
258    /// assert_eq!(ERR.message(), "Math::DomainError");
259    /// ```
260    #[inline]
261    #[must_use]
262    pub const fn new() -> Self {
263        // ```
264        // [2.6.3] > Math::DomainError.new.message
265        // => "Math::DomainError"
266        // ```
267        Self("Math::DomainError")
268    }
269
270    /// Construct a new, domain error with a message.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # use spinoso_math::DomainError;
276    /// const ERR: DomainError =
277    ///     DomainError::with_message(r#"Numerical argument is out of domain - "acos""#);
278    /// assert_eq!(
279    ///     ERR.message(),
280    ///     r#"Numerical argument is out of domain - "acos""#
281    /// );
282    /// ```
283    #[inline]
284    #[must_use]
285    pub const fn with_message(message: &'static str) -> Self {
286        Self(message)
287    }
288
289    /// Retrieve the exception message associated with this error.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// # use spinoso_math::DomainError;
295    /// let err = DomainError::new();
296    /// assert_eq!(err.message(), "Math::DomainError");
297    ///
298    /// let err = DomainError::with_message(r#"Numerical argument is out of domain - "acos""#);
299    /// assert_eq!(
300    ///     err.message(),
301    ///     r#"Numerical argument is out of domain - "acos""#
302    /// );
303    /// ```
304    #[inline]
305    #[must_use]
306    pub const fn message(self) -> &'static str {
307        self.0
308    }
309}
310
311impl fmt::Display for DomainError {
312    #[inline]
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        f.write_str(self.0)
315    }
316}
317
318impl error::Error for DomainError {}
319
320/// Error that indicates a `Math` module function is not implemented.
321///
322/// Some math functions are not available in the [Rust core library] and require
323/// this crate to be built with extra compile-time features to enable [additional
324/// dependencies].
325///
326/// Not implemented errors have an associated message.
327///
328/// This error corresponds to the [Ruby `NotImplementedError` Exception class].
329///
330/// # Examples
331///
332/// ```
333/// # use spinoso_math::NotImplementedError;
334/// let err = NotImplementedError::new();
335/// assert_eq!(err.message(), "NotImplementedError");
336///
337/// let err = NotImplementedError::with_message("Artichoke was not built with Math::erf support");
338/// assert_eq!(
339///     err.message(),
340///     "Artichoke was not built with Math::erf support"
341/// );
342/// ```
343///
344/// [Rust core library]: https://doc.rust-lang.org/std/primitive.f64.html
345/// [additional dependencies]: https://crates.io/crates/libm
346/// [Ruby `NotImplementedError` Exception class]: https://ruby-doc.org/core-3.1.2/NotImplementedError.html
347#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
348pub struct NotImplementedError(&'static str);
349
350impl NotImplementedError {
351    /// Construct a new, default not implemented error.
352    ///
353    /// # Examples
354    ///
355    /// ```
356    /// # use spinoso_math::NotImplementedError;
357    /// const ERR: NotImplementedError = NotImplementedError::new();
358    /// assert_eq!(ERR.message(), "NotImplementedError");
359    /// ```
360    #[inline]
361    #[must_use]
362    pub const fn new() -> Self {
363        Self("NotImplementedError")
364    }
365
366    /// Construct a new, not implemented error with a message.
367    ///
368    /// # Examples
369    ///
370    /// ```
371    /// # use spinoso_math::NotImplementedError;
372    /// const ERR: NotImplementedError =
373    ///     NotImplementedError::with_message("Artichoke was not built with Math::erf support");
374    /// assert_eq!(
375    ///     ERR.message(),
376    ///     "Artichoke was not built with Math::erf support"
377    /// );
378    /// ```
379    #[inline]
380    #[must_use]
381    pub const fn with_message(message: &'static str) -> Self {
382        Self(message)
383    }
384
385    /// Retrieve the exception message associated with this not implemented
386    /// error.
387    ///
388    /// # Examples
389    ///
390    /// ```
391    /// # use spinoso_math::NotImplementedError;
392    /// let err = NotImplementedError::new();
393    /// assert_eq!(err.message(), "NotImplementedError");
394    ///
395    /// let err = NotImplementedError::with_message("Artichoke was not built with Math::erf support");
396    /// assert_eq!(
397    ///     err.message(),
398    ///     "Artichoke was not built with Math::erf support"
399    /// );
400    /// ```
401    #[inline]
402    #[must_use]
403    pub const fn message(self) -> &'static str {
404        self.0
405    }
406}
407
408impl From<&'static str> for NotImplementedError {
409    #[inline]
410    fn from(message: &'static str) -> Self {
411        Self(message)
412    }
413}
414
415impl fmt::Display for NotImplementedError {
416    #[inline]
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        f.write_str(self.0)
419    }
420}
421
422impl error::Error for NotImplementedError {}