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 {}