diff --git a/src/gleam_community/int.gleam b/src/gleam_community/int.gleam deleted file mode 100644 index 2b7d31b..0000000 --- a/src/gleam_community/int.gleam +++ /dev/null @@ -1,1726 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// A module containing several different kinds of mathematical functions and constants. -//// -//// --- -//// -//// * **Standard mathematical functions** -//// * [`acos`](#acos) -//// * [`acosh`](#acosh) -//// * [`asin`](#asin) -//// * [`asinh`](#asinh) -//// * [`atan`](#atan) -//// * [`atan2`](#atan2) -//// * [`atanh`](#atanh) -//// * [`ceil`](#ceil) -//// * [`cos`](#cos) -//// * [`cosh`](#cosh) -//// * [`exp`](#exp) -//// * [`floor`](#floor) -//// * [`log`](#log) -//// * [`log10`](#log10) -//// * [`log2`](#log2) -//// * [`pow`](#pow) -//// * [`sign`](#sign) -//// * [`sin`](#sin) -//// * [`sinh`](#sinh) -//// * [`tan`](#tan) -//// * [`tanh`](#tanh) -//// * [`to_degrees`](#to_degrees) -//// * [`to_radians`](#to_radians) -//// * **Special mathematical functions** -//// * [`beta`](#beta) -//// * [`erf`](#erf) -//// * [`gamma`](#gamma) -//// * [`gammainc`](#gammainc) -//// * [`round`](#round) -//// * **Combinatorial functions** -//// * [`combination`](#combination) -//// * [`factorial`](#factorial) -//// * [`permutation`](#permutation) -//// * **Mathematical constants** -//// * [`pi`](#pi) -//// * [`tau`](#tau) - -import gleam/list -import gleam/int -import gleam/float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse cosine function: -/// -/// \\[ -/// \forall x \in \[-1, 1\], \\; \cos^{-1}{(x)} = y \in \[0, \pi \] -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\[-1, 1\]$$ as input and returns a -/// numeric value $$y$$ that lies in the range $$\[0, \pi \]$$ (an angle in radians). -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.acos(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// math.acos(1.1) -/// |> should.be_error() -/// -/// math.acos(-1.1) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn acos(x: Float) -> Result(Float, String) { - case x >=. -1.0 && x <=. 1.0 { - True -> - do_acos(x) - |> Ok - False -> - "Invalid input argument: x >= -1 or x <= 1. Valid input is -1. <= x <= 1." - |> Error - } -} - -if erlang { - external fn do_acos(Float) -> Float = - "math" "acos" -} - -if javascript { - external fn do_acos(Float) -> Float = - "../math.mjs" "acos" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse hyperbolic cosine function: -/// -/// \\[ -/// \forall x \in [1, +\infty\), \\; \cosh^{-1}{(x)} = y \in \[0, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\[1, +\infty\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\[0, +\infty\)$$ (an angle in radians). -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.acosh(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// math.acosh(0.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn acosh(x: Float) -> Result(Float, String) { - case x >=. 1.0 { - True -> - do_acosh(x) - |> Ok - False -> - "Invalid input argument: x < 1. Valid input is x >= 1." - |> Error - } -} - -if erlang { - external fn do_acosh(Float) -> Float = - "math" "acosh" -} - -if javascript { - external fn do_acosh(Float) -> Float = - "../math.mjs" "acosh" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse sine function: -/// -/// \\[ -/// \forall x \in \[-1, 1\], \\; \sin^{-1}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\[-1, 1\]$$ as input and returns a numeric -/// value $$y$$ that lies in the range $$\[-\frac{\pi}{2}, \frac{\pi}{2}\]$$ (an angle in radians). -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.asin(0.0) -/// |> should.equal(0.0) -/// -/// math.asin(1.1) -/// |> should.be_error() -/// -/// math.asin(-1.1) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn asin(x: Float) -> Result(Float, String) { - case x >=. -1.0 && x <=. 1.0 { - True -> - do_asin(x) - |> Ok - False -> - "Invalid input argument: x >= -1 or x <= 1. Valid input is -1. <= x <= 1." - |> Error - } -} - -if erlang { - external fn do_asin(Float) -> Float = - "math" "asin" -} - -if javascript { - external fn do_asin(Float) -> Float = - "../math.mjs" "asin" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse hyperbolic sine function: -/// -/// \\[ -/// \forall x \in \(-\infty, \infty\), \\; \sinh^{-1}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, +\infty\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\(-\infty, +\infty\)$$ (an angle in radians). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.asinh(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn asinh(x: Float) -> Float { - do_asinh(x) -} - -if erlang { - external fn do_asinh(Float) -> Float = - "math" "asinh" -} - -if javascript { - external fn do_asinh(Float) -> Float = - "../math.mjs" "asinh" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse tangent function: -/// -/// \\[ -/// \forall x \in \(-\infty, \infty\), \\; \tan^{-1}{(x)} = y \in \[-\frac{\pi}{2}, \frac{\pi}{2}\] -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, +\infty\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\[-\frac{\pi}{2}, \frac{\pi}{2}\]$$ (an angle in radians). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.atan(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn atan(x: Float) -> Float { - do_atan(x) -} - -if erlang { - external fn do_atan(Float) -> Float = - "math" "atan" -} - -if javascript { - external fn do_atan(Float) -> Float = - "../math.mjs" "atan" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse 2-argument tangent function: -/// -/// \\[ -/// \text{atan2}(y, x) = -/// \begin{cases} -/// \tan^{-1}(\frac y x) &\text{if } x > 0, \\\\ -/// \tan^{-1}(\frac y x) + \pi &\text{if } x < 0 \text{ and } y \ge 0, \\\\ -/// \tan^{-1}(\frac y x) - \pi &\text{if } x < 0 \text{ and } y < 0, \\\\ -/// +\frac{\pi}{2} &\text{if } x = 0 \text{ and } y > 0, \\\\ -/// -\frac{\pi}{2} &\text{if } x = 0 \text{ and } y < 0, \\\\ -/// \text{undefined} &\text{if } x = 0 \text{ and } y = 0. -/// \end{cases} -/// \\] -/// -/// The function returns the angle in radians from the x-axis to the line containing the -/// origin $$\(0, 0\)$$ and a point given as input with coordinates $$\(x, y\)$$. The numeric value -/// returned by $$\text{atan2}(y, x)$$ is in the range $$\[-\pi, \pi\]$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.atan2(0.0, 0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn atan2(y: Float, x: Float) -> Float { - do_atan2(y, x) -} - -if erlang { - external fn do_atan2(Float, Float) -> Float = - "math" "atan2" -} - -if javascript { - external fn do_atan2(Float, Float) -> Float = - "../math.mjs" "atan2" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The inverse hyperbolic tangent function: -/// -/// \\[ -/// \forall x \in \(-1, 1\), \\; \tanh^{-1}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-1, 1\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\(-\infty, \infty\)$$ (an angle in radians). -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.atanh(0.0) -/// |> should.equal(Ok(0.0)) -/// -/// math.atanh(1.0) -/// |> should.be_error() -/// -/// math.atanh(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn atanh(x: Float) -> Result(Float, String) { - case x >. -1.0 && x <. 1.0 { - True -> - do_atanh(x) - |> Ok - False -> - "Invalid input argument: x > -1 or x < 1. Valid input is -1. < x < 1." - |> Error - } -} - -if erlang { - external fn do_atanh(Float) -> Float = - "math" "atanh" -} - -if javascript { - external fn do_atanh(Float) -> Float = - "../math.mjs" "atanh" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The ceiling function. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.ceil(0.2) -/// |> should.equal(1.0) -/// -/// math.ceil(0.8) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn ceil(x: Float) -> Float { - do_ceil(x) -} - -if erlang { - external fn do_ceil(Float) -> Float = - "math" "ceil" -} - -if javascript { - external fn do_ceil(Float) -> Float = - "../math.mjs" "ceil" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The cosine function: -/// -/// \\[ -/// \forall x \in \(-\infty, +\infty\), \\; \cos{(x)} = y \in \[-1, 1\] -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, \infty\)$$ (an angle in radians) -/// as input and returns a numeric value $$y$$ that lies in the range $$\[-1, 1\]$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.cos(0.0) -/// |> should.equal(1.0) -/// -/// math.cos(math.pi()) -/// |> should.equal(-1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cos(x: Float) -> Float { - do_cos(x) -} - -if erlang { - external fn do_cos(Float) -> Float = - "math" "cos" -} - -if javascript { - external fn do_cos(Float) -> Float = - "../math.mjs" "cos" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The hyperbolic cosine function: -/// -/// \\[ -/// \forall x \in \(-\infty, \infty\), \\; \cosh{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, \infty\)$$ as input (an angle in radians) -/// and returns a numeric value $$y$$ that lies in the range $$\(-\infty, \infty\)$$. -/// If the input value is too large an overflow error might occur. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.cosh(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cosh(x: Float) -> Float { - do_cosh(x) -} - -if erlang { - external fn do_cosh(Float) -> Float = - "math" "cosh" -} - -if javascript { - external fn do_cosh(Float) -> Float = - "../math.mjs" "cosh" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The exponential function: -/// -/// \\[ -/// \forall x \in \(-\infty, \infty\), \\; e^{(x)} = y \in \(0, +\infty\) -/// \\] -/// -/// If the input value is too large an overflow error might occur. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.exp(0.0) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn exp(x: Float) -> Float { - do_exp(x) -} - -if erlang { - external fn do_exp(Float) -> Float = - "math" "exp" -} - -if javascript { - external fn do_exp(Float) -> Float = - "../math.mjs" "exp" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The floor function. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.floor(0.2) -/// |> should.equal(0.0) -/// -/// math.floor(0.8) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn floor(x: Float) -> Float { - do_floor(x) -} - -if erlang { - external fn do_floor(Float) -> Float = - "math" "floor" -} - -if javascript { - external fn do_floor(Float) -> Float = - "../math.mjs" "floor" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The natural logarithm function: -/// -/// \\[ -/// \forall x \in \(0, \infty\), \\; \log_{e}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(0, \infty\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\(-\infty, \infty\)$$. -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example () { -/// math.log(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// math.log(math.exp(1.0)) -/// |> should.equal(1.0) -/// -/// math.log(-1.0) -/// |> should.be_error() -/// } -///
-/// -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn log(x: Float) -> Result(Float, String) { - case x >. 0.0 { - True -> - do_log(x) - |> Ok - False -> - "Invalid input argument: x <= 0. Valid input is x > 0." - |> Error - } -} - -if erlang { - external fn do_log(Float) -> Float = - "math" "log" -} - -if javascript { - external fn do_log(Float) -> Float = - "../math.mjs" "log" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The The base-10 logarithm function: -/// -/// \\[ -/// \forall x \in \(0, \infty), \\; \log_{10}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(0, \infty\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\(-\infty, \infty\)$$. -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example () { -/// math.log10(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// math.log10(10.0) -/// |> should.equal(Ok(1.0)) -/// -/// math.log10(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn log10(x: Float) -> Result(Float, String) { - case x >. 0.0 { - True -> - do_log10(x) - |> Ok - False -> - "Invalid input argument: x <= 0. Valid input is x > 0." - |> Error - } -} - -if erlang { - external fn do_log10(Float) -> Float = - "math" "log10" -} - -if javascript { - external fn do_log10(Float) -> Float = - "../math.mjs" "log10" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The The base-2 logarithm function: -/// -/// \\[ -/// \forall x \in \(0, \infty), \\; \log_{2}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(0, \infty\)$$ as input and returns -/// a numeric value $$y$$ that lies in the range $$\(-\infty, \infty\)$$. -/// If the input value is outside the domain of the function an error is returned. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example () { -/// math.log2(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// math.log2(2.0) -/// |> should.equal(Ok(1.0)) -/// -/// math.log2(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn log2(x: Float) -> Result(Float, String) { - case x >. 0.0 { - True -> - do_log2(x) - |> Ok - False -> - "Invalid input argument: x <= 0. Valid input is x > 0." - |> Error - } -} - -if erlang { - external fn do_log2(Float) -> Float = - "math" "log2" -} - -if javascript { - external fn do_log2(Float) -> Float = - "../math.mjs" "log2" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The exponentiation function: $$y = x^{a}$$. -/// -/// Note that the function is not defined if: -/// 1. The base is negative ($$x < 0$$) and the exponent is fractional -/// ($$a = \frac{n}{m}$$ is an irrreducible fraction). An error will be returned -/// as an imaginary number will otherwise have to be returned. -/// 2. The base is zero ($$x = 0$$) and the exponent is negative ($$a < 0$$) then the -/// expression is equivalent to the exponent $$y$$ divided by $$0$$ and an -/// error will have to be returned as the expression is otherwise undefined. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.pow(2., -1.) -/// |> should.equal(0.5) -/// -/// math.pow(2., 2.) -/// |> should.equal(4.0) -/// -/// math.pow(-1., 0.5) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn pow(x: Float, y: Float) -> Result(Float, String) { - let fractional: Bool = ceil(y) -. y >. 0.0 - // In the following check: - // 1. If the base (x) is negative and the exponent (y) is fractional - // then return an error as it will otherwise be an imaginary number - // 2. If the base (x) is 0 and the exponent (y) is negative then the - // expression is equivalent to the exponent (y) divided by 0 and an - // error should be returned - case x <. 0.0 && fractional || x == 0.0 && y <. 0.0 { - True -> - "Invalid input argument: x < 0 and y is fractional or x = 0 and y < 0." - |> Error - False -> - do_pow(x, y) - |> Ok - } -} - -if erlang { - external fn do_pow(Float, Float) -> Float = - "math" "pow" -} - -if javascript { - external fn do_pow(Float, Float) -> Float = - "../math.mjs" "pow" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The sign function which returns the sign of the input, indicating -/// whether it is positive, negative, or zero. -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn sign(x: Float) -> Float { - do_sign(x) -} - -if erlang { - fn do_sign(x: Float) -> Float { - case x <. 0.0 { - True -> -1.0 - False -> - case x == 0.0 { - True -> 0.0 - False -> 1.0 - } - } - } -} - -if javascript { - external fn do_sign(Float) -> Float = - "../math.mjs" "sign" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The sine function: -/// -/// \\[ -/// \forall x \in \(-\infty, +\infty\), \\; \sin{(x)} = y \in \[-1, 1\] -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, \infty\)$$ (an angle in radians) -/// as input and returns a numeric value $$y$$ that lies in the range $$\[-1, 1\]$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.sin(0.0) -/// |> should.equal(0.0) -/// -/// math.sin(0.5 *. math.pi()) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn sin(x: Float) -> Float { - do_sin(x) -} - -if erlang { - external fn do_sin(Float) -> Float = - "math" "sin" -} - -if javascript { - external fn do_sin(Float) -> Float = - "../math.mjs" "sin" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The hyperbolic sine function: -/// -/// \\[ -/// \forall x \in \(-\infty, +\infty\), \\; \sinh{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, +\infty\)$$ as input -/// (an angle in radians) and returns a numeric value $$y$$ that lies in the range -/// $$\(-\infty, +\infty\)$$. If the input value is too large an overflow error might -/// occur. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.sinh(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn sinh(x: Float) -> Float { - do_sinh(x) -} - -if erlang { - external fn do_sinh(Float) -> Float = - "math" "sinh" -} - -if javascript { - external fn do_sinh(Float) -> Float = - "../math.mjs" "sinh" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The tangent function: -/// -/// \\[ -/// \forall x \in \(-\infty, +\infty\), \\; \tan{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, +\infty\)$$ as input -/// (an angle in radians) and returns a numeric value $$y$$ that lies in the range -/// $$\(-\infty, +\infty\)$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.tan(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn tan(x: Float) -> Float { - do_tan(x) -} - -if erlang { - external fn do_tan(Float) -> Float = - "math" "tan" -} - -if javascript { - external fn do_tan(Float) -> Float = - "../math.mjs" "tan" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The hyperbolic tangent function: -/// -/// \\[ -/// \forall x \in \(-\infty, \infty\), \\; \tanh{(x)} = y \in \[-1, 1\] -/// \\] -/// -/// The function takes a number $$x$$ in its domain $$\(-\infty, \infty\)$$ as input (an angle in radians) -/// and returns a numeric value $$y$$ that lies in the range $$\[-1, 1\]$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example () { -/// math.tanh(0.0) -/// |> should.equal(0.0) -/// -/// math.tanh(25.0) -/// |> should.equal(1.0) -/// -/// math.tanh(-25.0) -/// |> should.equal(-1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn tanh(x: Float) -> Float { - do_tanh(x) -} - -if erlang { - external fn do_tanh(Float) -> Float = - "math" "tanh" -} - -if javascript { - external fn do_tanh(Float) -> Float = - "../math.mjs" "tanh" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Convert a value in degrees to a value measured in radians. -/// That is, $$1 \text{ radians } = \frac{180}{\pi} \text{ degrees }$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.to_degrees(0.0) -/// |> should.equal(0.0) -/// -/// math.to_degrees(2 *. pi()) -/// |> should.equal(360.) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn to_degrees(x: Float) -> Float { - x *. 180.0 /. pi() -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Convert a value in degrees to a value measured in radians. -/// That is, $$1 \text{ degrees } = \frac{\pi}{180} \text{ radians }$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.sin(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn to_radians(x: Float) -> Float { - x *. pi() /. 180.0 -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The beta function over the real numbers: -/// -/// \\[ -/// \text{B}(x, y) = \frac{\Gamma(x) \cdot \Gamma(y)}{\Gamma(x + y)} -/// \\] -/// -/// The beta function is evaluated through the use of the gamma function. -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn beta(x: Float, y: Float) -> Float { - gamma(x) *. gamma(y) /. gamma(x +. y) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The error function. -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn erf(x: Float) -> Float { - let [a1, a2, a3, a4, a5] = [ - 0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429, - ] - let p = 0.3275911 - - // TODO: Use the implemented sign function - let sign = case x <. 0.0 { - True -> -1.0 - False -> 1.0 - } - let x = float.absolute_value(x) - - // Formula 7.1.26 given in Abramowitz and Stegun. - let t = 1.0 /. { 1.0 +. p *. x } - let y = - 1.0 -. { { { { a5 *. t +. a4 } *. t +. a3 } *. t +. a2 } *. t +. a1 } *. t *. exp( - -1.0 *. x *. x, - ) - sign *. y -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The gamma function over the real numbers. The function is essentially equal to the -/// factorial for any positive integer argument: $$\Gamma(n) = (n - 1)!$$ -/// -/// The implemented gamma function is approximated through Lanczos approximation -/// using the same coefficients used by the GNU Scientific Library. -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn gamma(x: Float) -> Float { - gamma_lanczos(x) -} - -const lanczos_g: Float = 7.0 - -const lanczos_p: List(Float) = [ - 0.99999999999980993, 676.5203681218851, -1259.1392167224028, - 771.32342877765313, -176.61502916214059, 12.507343278686905, - -0.13857109526572012, 0.0000099843695780195716, 0.00000015056327351493116, -] - -fn gamma_lanczos(x: Float) -> Float { - case x <. 0.5 { - True -> pi() /. { sin(pi() *. x) *. gamma_lanczos(1.0 -. x) } - False -> { - let z = x -. 1.0 - let x: Float = - list.index_fold( - lanczos_p, - 0.0, - fn(acc: Float, v: Float, index: Int) { - case index > 0 { - True -> acc +. v /. { z +. int.to_float(index) } - False -> v - } - }, - ) - let t: Float = z +. lanczos_g +. 0.5 - assert Ok(v1) = pow(2.0 *. pi(), 0.5) - assert Ok(v2) = pow(t, z +. 0.5) - v1 *. v2 *. exp(-1.0 *. t) *. x - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The lower incomplete gamma function over the real numbers. -/// -/// The implemented incomplete gamma function is evaluated through a power series -/// expansion. -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn gammainc(a: Float, x: Float) -> Result(Float, String) { - case a >. 0.0 && x >=. 0.0 { - True -> { - assert Ok(v) = pow(x, a) - v *. exp(-1.0 *. x) *. gammainc_sum(a, x, 1.0 /. a, 0.0, 1.0) - |> Ok - } - - False -> - "Invlaid input argument: a <= 0 or x < 0. Valid input is a > 0 and x >= 0." - |> Error - } -} - -fn gammainc_sum(a: Float, x: Float, t: Float, s: Float, n: Float) -> Float { - case t { - 0.0 -> s - _ -> { - let ns: Float = s +. t - let nt: Float = t *. { x /. { a +. n } } - gammainc_sum(a, x, nt, ns, n +. 1.0) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function rounds a floating point number to a specific decimal precision. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.round(0.4444, 2) -/// |> should.equal(0.44) -/// -/// math.round(0.4445, 2) -/// |> should.equal(0.44) -/// -/// math.round(0.4455, 2) -/// |> should.equal(0.45) -/// -/// math.round(0.4555, 2) -/// |> should.equal(0.46) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn round(x: Float, precision: Int) -> Float { - assert Ok(p) = pow(10.0, int.to_float(precision)) - int.to_float(float.round(x *. p)) /. p -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the number of a $$k$$-combinations of $$n$$ elements: -/// -/// \\[ -/// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!} -/// \\] -/// Also known as "$$n$$ choose $$k$$" or the binomial coefficient. -/// -/// The implementation uses the effecient iterative multiplicative formula for the computation. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// // Invalid input gives an error -/// // Error on: n = -1 < 0 -/// math.combination(-1, 1) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// math.combination(4, 0) -/// |> should.equal(Ok(1)) -/// -/// math.combination(4, 4) -/// |> should.equal(Ok(1)) -/// -/// math.combination(4, 2) -/// |> should.equal(Ok(6)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn combination(n: Int, k: Int) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error - False -> - case k < 0 || k > n { - True -> - 0 - |> Ok - False -> - case k == 0 || k == n { - True -> - 1 - |> Ok - False -> { - let min = case k < n - k { - True -> k - False -> n - k - } - list.range(1, min + 1) - |> list.fold( - 1, - fn(acc: Int, x: Int) -> Int { acc * { n + 1 - x } / x }, - ) - |> Ok - } - } - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the total number of combinations of $$n$$ -/// elements, that is $$n!$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// // Invalid input gives an error -/// math.factorial(-1) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// math.factorial(0) -/// |> should.equal(Ok(1)) -/// math.factorial(1) -/// |> should.equal(Ok(1)) -/// math.factorial(2) -/// |> should.equal(Ok(2)) -/// math.factorial(3) -/// |> should.equal(Ok(6)) -/// math.factorial(4) -/// |> should.equal(Ok(24)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn factorial(n) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error - False -> - case n { - 0 -> - 1 - |> Ok - 1 -> - 1 - |> Ok - _ -> - list.range(1, n + 1) - |> list.fold(1, fn(acc: Int, x: Int) { acc * x }) - |> Ok - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the number of $$k$$-permuations (without repetitions) -/// of $$n$$ elements: -/// -/// \\[ -/// P(n, k) = \frac{n!}{(n - k)!} -/// \\] -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// // Invalid input gives an error -/// // Error on: n = -1 < 0 -/// math.permutation(-1, 1) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// math.permutation(4, 0) -/// |> should.equal(Ok(1)) -/// -/// math.permutation(4, 4) -/// |> should.equal(Ok(1)) -/// -/// math.permutation(4, 2) -/// |> should.equal(Ok(12)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn permutation(n: Int, k: Int) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error - False -> - case k < 0 || k > n { - True -> - 0 - |> Ok - False -> - case k == n { - True -> - 1 - |> Ok - False -> { - assert Ok(v1) = factorial(n) - assert Ok(v2) = factorial(n - k) - v1 / v2 - |> Ok - } - } - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The mathematical constant pi: $$\pi \approx 3.1415\dots$$ -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn pi() -> Float { - do_pi() -} - -if erlang { - external fn do_pi() -> Float = - "math" "pi" -} - -if javascript { - external fn do_pi() -> Float = - "../math.mjs" "pi" -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The mathematical constant tau: $$\tau = 2 \cdot \pi \approx 6.283\dots$$ -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn tau() -> Float { - 2.0 *. pi() -} - -/// Returns the absolute difference of the inputs as a positive Int. -/// -/// ## Examples -/// -/// ```gleam -/// > absolute_int_difference(-10, 10) -/// > 20 -/// ``` -/// -/// ```gleam -/// > absolute_int_difference(0, -2) -/// 2 -/// -pub fn absolute_int_difference(a: Int, b: Int) -> Int { - a - b - |> int.absolute_value() -} - -/// Returns the absolute difference of the inputs as a positive Float. -/// -/// ## Examples -/// -/// ```gleam -/// > absolute_float_difference(-10, 10) -/// > 20 -/// ``` -/// -/// ```gleam -/// > absolute_float_difference(0, -2) -/// 2 -/// -pub fn absolute_float_difference(a: Float, b: Float) -> Float { - a -. b - |> float.absolute_value() -} diff --git a/src/gleam_community/float.gleam b/src/gleam_community/maths/float.gleam similarity index 70% rename from src/gleam_community/float.gleam rename to src/gleam_community/maths/float.gleam index ef1ed42..bb4b526 100644 --- a/src/gleam_community/float.gleam +++ b/src/gleam_community/maths/float.gleam @@ -19,11 +19,42 @@ //// .katex { font-size: 1.1em; } //// //// -//// A module containing several different kinds of mathematical functions and constants. +//// A module containing several different kinds of mathematical constants and +//// functions applying to real numbers. //// +//// Function naming has been adopted from C mathematical function. +//// //// --- //// -//// * **Transcendental functions** +//// * **Rounding functions** +//// * [`ceiling`](#ceiling) +//// * [`floor`](#floor) +//// * [`truncate`](#truncate) +//// * [`round`](#round) +//// * **Division functions** +//// * [`gcd`](#gcd) +//// * [`lcm`](#lcm) +//// * **Sign and absolute value functions** +//// * [`abs2`](#abs2) +//// * [`absdiff`](#abs_diff) +//// * [`sign`](#sign) +//// * [`flipsign`](#flipsign) +//// * [`copysign`](#copysign) +//// * **Powers, logs and roots** +//// * [`exp`](#exp) +//// * [`log`](#log) +//// * [`log10`](#log10) +//// * [`log2`](#log2) +//// * [`pow`](#pow) +//// +//// * [`isqrt`](#isqrt) +//// * [`icbrt`](#icbrt) +//// +//// * [`sqrt`](#sqrt) +//// * [`cbrt`](#cbrt) +//// * [`nthrt`](#nthrt) +//// * [`hypot`](#hypot) +//// * **Trigonometric and hyperbolic functions** //// * [`acos`](#acos) //// * [`acosh`](#acosh) //// * [`asin`](#asin) @@ -33,23 +64,16 @@ //// * [`atanh`](#atanh) //// * [`cos`](#cos) //// * [`cosh`](#cosh) -//// * [`exp`](#exp) -//// * [`log`](#log) -//// * [`log10`](#log10) -//// * [`log2`](#log2) -//// * [`pow`](#pow) -//// * [`sqrt`](#sqrt) -//// * [`cbrt`](#cbrt) //// * [`sin`](#sin) //// * [`sinh`](#sinh) //// * [`tan`](#tan) //// * [`tanh`](#tanh) -//// * **Standard mathematical functions** -//// * [`ceil`](#ceil) -//// * [`floor`](#floor) -//// * [`sign`](#sign) -//// * [`round`](#round) -//// * [`absolute_difference`](#absolute_difference) +//// * [`deg2rad`](#deg2rad) +//// * [`rad2deg`](#rad2deg) +//// * **Mathematical functions** +//// * [`min`](#min) +//// * [`max`](#max) +//// * [`minmax`](#minmax) //// * **Special mathematical functions** //// * [`beta`](#beta) //// * [`erf`](#erf) @@ -58,10 +82,17 @@ //// * **Mathematical constants** //// * [`pi`](#pi) //// * [`tau`](#tau) +//// * **Tests** +//// * [`ispow2`](#ispow2) +//// * [`isclose`](#isclose) +//// * [`iseven`](#iseven) +//// * [`isodd`](#isodd) import gleam/list import gleam/int import gleam/float +import gleam/option +import gleam/io ///
/// @@ -83,16 +114,16 @@ import gleam/float /// Example: /// /// import gleeunit/should -/// import gleam_stats/math +/// import gleam_community/maths/float as floatx /// /// pub fn example() { -/// math.acos(1.0) +/// floatx.acos(1.0) /// |> should.equal(Ok(0.0)) /// -/// math.acos(1.1) +/// floatx.acos(1.1) /// |> should.be_error() /// -/// math.acos(-1.1) +/// floatx.acos(-1.1) /// |> should.be_error() /// } /// @@ -144,13 +175,13 @@ if javascript { /// Example: /// /// import gleeunit/should -/// import gleam_stats/math +/// import gleam_community/maths/float as floatx /// /// pub fn example() { -/// math.acosh(1.0) +/// floatx.acosh(1.0) /// |> should.equal(Ok(0.0)) /// -/// math.acosh(0.0) +/// floatx.acosh(0.0) /// |> should.be_error() /// } /// @@ -454,49 +485,6 @@ if javascript { "../math.mjs" "atanh" } -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The ceiling function. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.ceil(0.2) -/// |> should.equal(1.0) -/// -/// math.ceil(0.8) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn ceil(x: Float) -> Float { - do_ceil(x) -} - -if erlang { - external fn do_ceil(Float) -> Float = - "math" "ceil" -} - -if javascript { - external fn do_ceil(Float) -> Float = - "../math.mjs" "ceil" -} - ///
/// /// Spot a typo? Open an issue! @@ -641,49 +629,6 @@ if javascript { "../math.mjs" "exp" } -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The floor function. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.floor(0.2) -/// |> should.equal(0.0) -/// -/// math.floor(0.8) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn floor(x: Float) -> Float { - do_floor(x) -} - -if erlang { - external fn do_floor(Float) -> Float = - "math" "floor" -} - -if javascript { - external fn do_floor(Float) -> Float = - "../math.mjs" "floor" -} - ///
/// /// Spot a typo? Open an issue! @@ -725,7 +670,43 @@ if javascript { /// ///
/// -pub fn log(x: Float) -> Result(Float, String) { +pub fn log(x: Float, base: option.Option) -> Result(Float, String) { + case x >. 0.0 { + True -> { + case base { + option.Some(a) -> { + case a >. 0.0 && a != 1.0 { + True -> { + // Apply the change of base formula + assert Ok(numerator) = log10(x) + assert Ok(denominator) = log10(b) + numerator /. denominator + |> Ok + } + False -> + "Invalid input argument: base <= 0 or base == 1. Valid input is base > 0 and base != 1." + |> Error + } + case + } + _ -> { + "Invalid input argument: x <= 0. Valid input is x > 0." + |> Error + } + } + } + } + // case x >. 0.0 { + // True -> + // do_log(x) + // |> Ok + // False -> + // "Invalid input argument: x <= 0. Valid input is x > 0." + // |> Error + // } +} + +pub fn ln(x: Float) -> Result(Float, String) { case x >. 0.0 { True -> do_log(x) @@ -868,6 +849,67 @@ if javascript { "../math.mjs" "log2" } +// pub fn logb(x: Float, b: Float) -> Result(Float, String) { +// case x >. 0.0 { +// True -> +// case b >. 0.0 && b != 1.0 { +// True -> { +// // Apply the change of base formula +// assert Ok(numerator) = log10(x) +// assert Ok(denominator) = log10(b) +// numerator /. denominator +// |> Ok +// } +// False -> +// "Invalid input argument: b <= 0 or b == 1. Valid input is b > 0 and b != 1." +// |> Error +// } +// False -> +// "Invalid input argument: x <= 0. Valid input is x > 0." +// |> Error +// } +// } + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The The base-2 logarithm function: +/// +/// \\[ +/// \forall x \in \(0, \infty), \\; \log_{2}{(x)} = y \in \(-\infty, +\infty\) +/// \\] +/// +/// The function takes a number $$x$$ in its domain $$\(0, \infty\)$$ as input and returns +/// a numeric value $$y$$ that lies in the range $$\(-\infty, \infty\)$$. +/// If the input value is outside the domain of the function an error is returned. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example () { +/// math.log2(1.0) +/// |> should.equal(Ok(0.0)) +/// +/// math.log2(2.0) +/// |> should.equal(Ok(1.0)) +/// +/// math.log2(-1.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// ///
/// /// Spot a typo? Open an issue! @@ -942,8 +984,29 @@ if javascript { /// ///
/// -/// The sign function which returns the sign of the input, indicating -/// whether it is positive, negative, or zero. +/// The square root function: $$y = \sqrt[2]{x} = x^{\frac{1}{2}}$$. +/// +/// Note that the function is not defined if: +/// 1. The base is negative ($$x < 0$$). An error will be returned +/// as an imaginary number will otherwise have to be returned. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.sqrt(1.0) +/// |> should.equal(1.0) +/// +/// math.sqrt(4.0) +/// |> should.equal(2.0) +/// +/// math.sqrt(-1.0) +/// |> should.be_error() +/// } +///
/// ///
/// @@ -951,26 +1014,124 @@ if javascript { /// ///
/// -pub fn sign(x: Float) -> Float { - do_sign(x) -} - -if erlang { - fn do_sign(x: Float) -> Float { - case x <. 0.0 { - True -> -1.0 - False -> - case x == 0.0 { - True -> 0.0 - False -> 1.0 - } +pub fn sqrt(x: Float) -> Result(Float, String) { + // In the following check: + // 1. If x is negative then return an error as it will otherwise be an + // imaginary number + case x <. 0.0 { + True -> + "Invalid input argument: x < 0." + |> Error + False -> { + assert Ok(result) = pow(x, 1.0 /. 2.0) + result + |> Ok } } } -if javascript { - external fn do_sign(Float) -> Float = - "../math.mjs" "sign" +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The cube root function: $$y = \sqrt[3]{x} = x^{\frac{1}{3}}$$. +/// +/// Note that the function is not defined if: +/// 1. The base is negative ($$x < 0$$). An error will be returned +/// as an imaginary number will otherwise have to be returned. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.cbrt(1.0) +/// |> should.equal(1.0) +/// +/// math.cbrt(27.0) +/// |> should.equal(3.0) +/// +/// math.cbrt(-1.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cbrt(x: Float) -> Result(Float, String) { + // In the following check: + // 1. If x is negative then return an error as it will otherwise be an + // imaginary number + case x <. 0.0 { + True -> + "Invalid input argument: x < 0." + |> Error + False -> { + assert Ok(result) = pow(x, 1.0 /. 3.0) + result + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function to compute the hypotenuse of a right-angled triangle: $$\sqrt[2](x^2 + y^2)$$. +/// +/// The function can also be used to calculate the Euclidean distance in 2 dimensions. +/// +/// Naive (unfused) and corrected (unfused) in [https://arxiv.org/pdf/1904.09481.pdf]("An Improved Algorithm for Hypot(A, B)" by Borges, C. F) +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn hypot(x: Float, y: Float, corrected: option.Option(Bool)) -> Float { + assert Ok(term1) = pow(x, 2.0) + assert Ok(term2) = pow(y, 2.0) + assert Ok(h) = sqrt(term1 +. term2) + case corrected { + option.Some(True) -> { + let ax = float.absolute_value(x) + let ay = float.absolute_value(y) + case ay >. ax { + True -> { + + } + False -> { + + } + } + } + _ -> { + h + } + } } ///
@@ -1189,10 +1350,10 @@ if javascript { /// import gleam_stats/math /// /// pub fn example() { -/// math.to_degrees(0.0) +/// math.rad2deg(0.0) /// |> should.equal(0.0) /// -/// math.to_degrees(2 *. pi()) +/// math.rad2deg(2. *. pi()) /// |> should.equal(360.) /// } /// @@ -1203,7 +1364,7 @@ if javascript { /// ///
/// -pub fn to_degrees(x: Float) -> Float { +pub fn rad2deg(x: Float) -> Float { x *. 180.0 /. pi() } @@ -1223,7 +1384,85 @@ pub fn to_degrees(x: Float) -> Float { /// import gleam_stats/math /// /// pub fn example() { -/// math.sin(0.0) +/// math.deg2rad(360.) +/// |> should.equal(2. *. pi()) +/// } +/// +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn deg2rad(x: Float) -> Float { + x *. pi() /. 180.0 +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The ceiling function that rounds given input $$x \in \mathbb{R}$$ towards $$+\infty$$. +/// ceil(x) returns the nearest integral value of the same type as x that is greater than or equal to x. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.ceil(0.2) +/// |> should.equal(1.0) +/// +/// math.ceil(0.8) +/// |> should.equal(1.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn ceil(x: Float) -> Float { + do_ceil(x) +} + +if erlang { + external fn do_ceil(Float) -> Float = + "math" "ceil" +} + +if javascript { + external fn do_ceil(Float) -> Float = + "../math.mjs" "ceil" +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The floor function that rounds given input $$x \in \mathbb{R}$$ towards $$-\infty$$. +/// floor(x) returns the nearest integral value of the same type as x that is less than or equal to x. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.floor(0.2) +/// |> should.equal(0.0) +/// +/// math.floor(0.8) /// |> should.equal(0.0) /// } ///
@@ -1234,8 +1473,399 @@ pub fn to_degrees(x: Float) -> Float { /// ///
/// -pub fn to_radians(x: Float) -> Float { - x *. pi() /. 180.0 +pub fn floor(x: Float) -> Float { + do_floor(x) +} + +if erlang { + external fn do_floor(Float) -> Float = + "math" "floor" +} + +if javascript { + external fn do_floor(Float) -> Float = + "../math.mjs" "floor" +} + +fn to_int(x: Float) -> Int { + do_to_int(x) +} + +if erlang { + external fn do_to_int(Float) -> Int = + "erlang" "trunc" +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a floating point number to a specific decimal precision. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.round(0.4444, 2) +/// |> should.equal(0.44) +/// +/// math.round(0.4445, 2) +/// |> should.equal(0.44) +/// +/// math.round(0.4455, 2) +/// |> should.equal(0.45) +/// +/// math.round(0.4555, 2) +/// |> should.equal(0.46) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round( + x: Float, + digits: option.Option(Int), + mode: option.Option(String), +) -> Result(Float, String) { + case digits { + option.Some(a) -> + case mode { + option.Some("Nearest") -> { + let positive = x >. 0.0 + // let geq_tie = + // float.absolute_value(x) -. float.absolute_value(truncate(x)) >=. 0.5 + let xabs = float.absolute_value(x) + let geq_tie = xabs -. truncate(xabs) >=. 0.5 + assert Ok(is_even) = int.modulo(to_int(xabs), 2) + io.debug(is_even) + case geq_tie { + True -> + case is_even == 0 { + True -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 0.0 } *. p) /. p + |> Ok + } + False -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 1.0 } *. p) /. p + |> Ok + } + } + False -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 0.0 } *. p) /. p + |> Ok + } + } + } + option.Some("TiesAway") -> { + let positive = x >. 0.0 + let xabs = float.absolute_value(x) + let g_tie = xabs -. truncate(xabs) >=. 0.5 + io.debug(xabs -. truncate(xabs)) + // assert Ok(p) = pow(10.0, int.to_float(a)) + // sign(x) *. truncate({ float.absolute_value(x) +. 1.0 } *. p) /. p + // |> Ok + case g_tie { + True -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 1.0 } *. p) /. p + |> Ok + } + False -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + truncate(x *. p) /. p + |> Ok + } + } + } + // assert Ok(p) = pow(10.0, int.to_float(a)) + // sign(x) *. truncate({ float.absolute_value(x) +. 1.0 } *. p) /. p + // |> Ok + // Round towards positive infinity + option.Some("TiesUp") -> { + let positive = x >. 0.0 + // let geq_tie = + // float.absolute_value(x) -. float.absolute_value(truncate(x)) >=. 0.5 + let xabs = float.absolute_value(x) + let geq_tie = xabs -. truncate(xabs) >=. 0.5 + case geq_tie { + True -> + case positive { + True -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 1.0 } *. p) /. p + |> Ok + } + False -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 0.0 } *. p) /. p + |> Ok + } + } + False -> + case positive { + True -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 0.0 } *. p) /. p + |> Ok + } + False -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + sign(x) *. truncate({ xabs +. 0.0 } *. p) /. p + |> Ok + } + } + } + } + option.Some("ToZero") -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + truncate(x *. p) /. p + |> Ok + } + option.Some("Down") -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + floor(x *. p) /. p + |> Ok + } + option.Some("Up") -> { + assert Ok(p) = pow(10.0, int.to_float(a)) + ceil(x *. p) /. p + |> Ok + } + _ -> + "Invalid Rounding Mode!" + |> Error + } + _ -> + "Invalid!" + |> Error + } + // assert Ok(p) = pow(10.0, int.to_float(digits)) + // int.to_float(float.round(x *. p)) /. p +} + +pub fn truncate(x: Float) -> Float { + do_truncate(x) +} + +if erlang { + external fn do_truncate(Float) -> Float = + "erlang" "trunc" +} + +// pub fn round_to_nearest_ties_away(x: Float, digits: Int) -> Float { +// assert Ok(p) = pow(10.0, int.to_float(digits)) +// int.to_float(float.round(x *. p)) /. p +// } + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.round(0.4444, 2) +/// |> should.equal(0.44) +/// +/// math.round(0.4445, 2) +/// |> should.equal(0.44) +/// +/// math.round(0.4455, 2) +/// |> should.equal(0.45) +/// +/// math.round(0.4555, 2) +/// |> should.equal(0.46) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn trunc(x: Float, precision: Int) -> Float { + assert Ok(p) = pow(10.0, int.to_float(precision)) + int.to_float(float.round(x *. p)) /. p +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The min function. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.min(2.0, 1.5) +/// |> should.equal(1.5) +/// +/// math.min(1.5, 2.0) +/// |> should.equal(1.5) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn min(x: Float, y: Float) -> Float { + case x <. y { + True -> x + False -> y + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The min function. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.min(2.0, 1.5) +/// |> should.equal(1.5) +/// +/// math.min(1.5, 2.0) +/// |> should.equal(1.5) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn max(x: Float, y: Float) -> Float { + case x >. y { + True -> x + False -> y + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The minmax function. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.minmax(2.0, 1.5) +/// |> should.equal(#(1.5, 2.0)) +/// +/// math.minmax(1.5, 2.0) +/// |> should.equal(#(1.5, 2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn minmax(x: Float, y: Float) -> #(Float, Float) { + #(min(x, y), max(x, y)) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The sign function which returns the sign of the input, indicating +/// whether it is positive, negative, or zero. +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn sign(x: Float) -> Float { + do_sign(x) +} + +if erlang { + fn do_sign(x: Float) -> Float { + case x <. 0.0 { + True -> -1.0 + False -> + case x == 0.0 { + True -> 0.0 + False -> 1.0 + } + } + } +} + +if javascript { + external fn do_sign(Float) -> Float = + "../math.mjs" "sign" +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn flipsign(x: Float) -> Float { + -1.0 *. x } ///
@@ -1394,253 +2024,6 @@ fn gammainc_sum(a: Float, x: Float, t: Float, s: Float, n: Float) -> Float { } } -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function rounds a floating point number to a specific decimal precision. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// math.round(0.4444, 2) -/// |> should.equal(0.44) -/// -/// math.round(0.4445, 2) -/// |> should.equal(0.44) -/// -/// math.round(0.4455, 2) -/// |> should.equal(0.45) -/// -/// math.round(0.4555, 2) -/// |> should.equal(0.46) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn round(x: Float, precision: Int) -> Float { - assert Ok(p) = pow(10.0, int.to_float(precision)) - int.to_float(float.round(x *. p)) /. p -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the number of a $$k$$-combinations of $$n$$ elements: -/// -/// \\[ -/// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!} -/// \\] -/// Also known as "$$n$$ choose $$k$$" or the binomial coefficient. -/// -/// The implementation uses the effecient iterative multiplicative formula for the computation. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// // Invalid input gives an error -/// // Error on: n = -1 < 0 -/// math.combination(-1, 1) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// math.combination(4, 0) -/// |> should.equal(Ok(1)) -/// -/// math.combination(4, 4) -/// |> should.equal(Ok(1)) -/// -/// math.combination(4, 2) -/// |> should.equal(Ok(6)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn combination(n: Int, k: Int) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error - False -> - case k < 0 || k > n { - True -> - 0 - |> Ok - False -> - case k == 0 || k == n { - True -> - 1 - |> Ok - False -> { - let min = case k < n - k { - True -> k - False -> n - k - } - list.range(1, min + 1) - |> list.fold( - 1, - fn(acc: Int, x: Int) -> Int { acc * { n + 1 - x } / x }, - ) - |> Ok - } - } - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the total number of combinations of $$n$$ -/// elements, that is $$n!$$. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// // Invalid input gives an error -/// math.factorial(-1) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// math.factorial(0) -/// |> should.equal(Ok(1)) -/// math.factorial(1) -/// |> should.equal(Ok(1)) -/// math.factorial(2) -/// |> should.equal(Ok(2)) -/// math.factorial(3) -/// |> should.equal(Ok(6)) -/// math.factorial(4) -/// |> should.equal(Ok(24)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn factorial(n) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error - False -> - case n { - 0 -> - 1 - |> Ok - 1 -> - 1 - |> Ok - _ -> - list.range(1, n + 1) - |> list.fold(1, fn(acc: Int, x: Int) { acc * x }) - |> Ok - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the number of $$k$$-permuations (without repetitions) -/// of $$n$$ elements: -/// -/// \\[ -/// P(n, k) = \frac{n!}{(n - k)!} -/// \\] -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_stats/math -/// -/// pub fn example() { -/// // Invalid input gives an error -/// // Error on: n = -1 < 0 -/// math.permutation(-1, 1) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// math.permutation(4, 0) -/// |> should.equal(Ok(1)) -/// -/// math.permutation(4, 4) -/// |> should.equal(Ok(1)) -/// -/// math.permutation(4, 2) -/// |> should.equal(Ok(12)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn permutation(n: Int, k: Int) -> Result(Int, String) { - case n < 0 { - True -> - "Invalid input argument: n < 0. Valid input is n > 0." - |> Error - False -> - case k < 0 || k > n { - True -> - 0 - |> Ok - False -> - case k == n { - True -> - 1 - |> Ok - False -> { - assert Ok(v1) = factorial(n) - assert Ok(v2) = factorial(n - k) - v1 / v2 - |> Ok - } - } - } - } -} - ///
/// /// Spot a typo? Open an issue! @@ -1687,52 +2070,20 @@ pub fn tau() -> Float { 2.0 *. pi() } -/// Returns the absolute difference of the inputs as a positive Int. -/// -/// ## Examples -/// -/// ```gleam -/// > absolute_int_difference(-10, 10) -/// > 20 -/// ``` -/// -/// ```gleam -/// > absolute_int_difference(0, -2) -/// 2 -/// -pub fn absolute_int_difference(a: Int, b: Int) -> Int { - a - b - |> int.absolute_value() -} - -/// Returns the absolute difference of the inputs as a positive Float. -/// -/// ## Examples -/// -/// ```gleam -/// > absolute_float_difference(-10, 10) -/// > 20 -/// ``` -/// -/// ```gleam -/// > absolute_float_difference(0, -2) -/// 2 -/// ///
/// /// Spot a typo? Open an issue! /// ///
/// -/// The inverse cosine function: +/// The absolute difference: /// /// \\[ -/// \forall x \in \[-1, 1\], \\; \cos^{-1}{(x)} = y \in \[0, \pi \] +/// \forall x, y \in \mathbb{R}, \\; |x - y| \in \mathbb{R}_{+}. /// \\] /// -/// The function takes a number $$x$$ in its domain $$\[-1, 1\]$$ as input and returns a -/// numeric value $$y$$ that lies in the range $$\[0, \pi \]$$ (an angle in radians). -/// If the input value is outside the domain of the function an error is returned. +/// The function takes two inputs $$x$$ and $$y$$ and returns a positive float +/// value which is the the absolute difference of the inputs. /// ///
/// Example: @@ -1741,14 +2092,11 @@ pub fn absolute_int_difference(a: Int, b: Int) -> Int { /// import gleam_stats/math /// /// pub fn example() { -/// math.acos(1.0) -/// |> should.equal(Ok(0.0)) +/// math.absdiff(-10.0, 10.0) +/// |> should.equal(20.0) /// -/// math.acos(1.1) -/// |> should.be_error() -/// -/// math.acos(-1.1) -/// |> should.be_error() +/// math.absdiff(0.0, -2.0) +/// |> should.equal(2.0) /// } ///
/// @@ -1758,7 +2106,55 @@ pub fn absolute_int_difference(a: Int, b: Int) -> Int { /// ///
/// -pub fn absolute_difference(a: Float, b: Float) -> Float { +pub fn absdiff(a: Float, b: Float) -> Float { a -. b |> float.absolute_value() } + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Determine if a given value $$a$$ is close to or equivalent to a reference value +/// $$b$$ based on supplied relative $$r_{tol}$$ and absolute $$a_{tol}$$ tolerance values. +/// The equivalance of the two given values are then determined based on the equation: +/// +/// \\[ +/// \|a - b\| \leq (a_{tol} + r_{tol} \cdot \|b\|) +/// \\] +/// +/// `True` is returned if statement holds, otherwise `False` is returned. +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// let val: Float = 99. +/// let ref_val: Float = 100. +/// // We set 'atol' and 'rtol' such that the values are equivalent +/// // if 'val' is within 1 percent of 'ref_val' +/- 0.1 +/// let rtol: Float = 0.01 +/// let atol: Float = 0.10 +/// stats.isclose(val, ref_val, rtol, atol) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn isclose(a: Float, b: Float, rtol: Float, atol: Float) -> Bool { + let x: Float = float.absolute_value(a -. b) + let y: Float = atol +. rtol *. float.absolute_value(b) + case x <=. y { + True -> True + False -> False + } +} diff --git a/src/gleam_community/maths/float_list.gleam b/src/gleam_community/maths/float_list.gleam new file mode 100644 index 0000000..77b0089 --- /dev/null +++ b/src/gleam_community/maths/float_list.gleam @@ -0,0 +1,393 @@ +//// +//// +//// +//// +//// +//// +//// A module containing several different kinds of mathematical functions +//// applying to lists of real numbers. +//// +//// Function naming has been adopted from C mathematical function. +//// +//// --- +//// +//// * **Miscellaneous functions** +//// * [`allclose`](#allclose) +//// * [`amax`](#amax) +//// * [`amin`](#amin) +//// * [`argmax`](#argmax) +//// * [`argmin`](#argmin) +//// * [`allclose`](#allclose) +//// * [`trim`](#trim) + +import gleam/list +import gleam/int +import gleam/float +import gleam/pair +import gleam_community/maths/float as floatx + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Determine if a list of values are close to or equivalent to a +/// another list of reference values. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// let val: Float = 99. +/// let ref_val: Float = 100. +/// let xarr: List(Float) = list.repeat(val, 42) +/// let yarr: List(Float) = list.repeat(ref_val, 42) +/// // We set 'atol' and 'rtol' such that the values are equivalent +/// // if 'val' is within 1 percent of 'ref_val' +/- 0.1 +/// let rtol: Float = 0.01 +/// let atol: Float = 0.10 +/// stats.allclose(xarr, yarr, rtol, atol) +/// |> fn(zarr: Result(List(Bool), String)) -> Result(Bool, Nil) { +/// case zarr { +/// Ok(arr) -> +/// arr +/// |> list.all(fn(a: Bool) -> Bool { a }) +/// |> Ok +/// _ -> Nil |> Error +/// } +/// } +/// |> should.equal(Ok(True)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn allclose( + xarr: List(Float), + yarr: List(Float), + rtol: Float, + atol: Float, +) -> Result(List(Bool), String) { + let xlen: Int = list.length(xarr) + let ylen: Int = list.length(yarr) + case xlen == ylen { + False -> + "Invalid input argument: length(xarr) != length(yarr). Valid input is when length(xarr) == length(yarr)." + |> Error + True -> + list.zip(xarr, yarr) + |> list.map(fn(z: #(Float, Float)) -> Bool { + floatx.isclose(pair.first(z), pair.second(z), rtol, atol) + }) + |> Ok + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the indices of the minimum values in a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.argmin() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.argmin() +/// |> should.equal(Ok([4])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn argmin(arr: List(Float)) -> Result(List(Int), String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(min) = + arr + |> amin() + arr + |> list.index_map(fn(index: Int, a: Float) -> Int { + case a -. min { + 0. -> index + _ -> -1 + } + }) + |> list.filter(fn(index: Int) -> Bool { + case index { + -1 -> False + _ -> True + } + }) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the indices of the maximum values in a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.argmax() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.argmax() +/// |> should.equal(Ok([0, 1])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn argmax(arr: List(Float)) -> Result(List(Int), String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(max) = + arr + |> amax() + arr + |> list.index_map(fn(index: Int, a: Float) -> Int { + case a -. max { + 0. -> index + _ -> -1 + } + }) + |> list.filter(fn(index: Int) -> Bool { + case index { + -1 -> False + _ -> True + } + }) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the maximum value of a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.amax() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.amax() +/// |> should.equal(Ok(4.)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn amax(arr: List(Float)) -> Result(Float, String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(val0) = list.at(arr, 0) + arr + |> list.fold( + val0, + fn(acc: Float, a: Float) { + case a >. acc { + True -> a + False -> acc + } + }, + ) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the minimum value of a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.amin() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.amin() +/// |> should.equal(Ok(1.)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn amin(arr: List(Float)) -> Result(Float, String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(val0) = list.at(arr, 0) + arr + |> list.fold( + val0, + fn(acc: Float, a: Float) { + case a <. acc { + True -> a + False -> acc + } + }, + ) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the minimum value of a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.amin() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.amin() +/// |> should.equal(Ok(1.)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn extrema(arr: List(Float)) -> Result(#(Float, Float), String) { + todo + // case arr { + // [] -> + // "Invalid input argument: The list is empty." + // |> Error + // _ -> { + // assert Ok(val0) = list.at(arr, 0) + // arr + // |> list.fold( + // val0, + // fn(acc: Float, a: Float) { + // case a <. acc { + // True -> a + // False -> acc + // } + // }, + // ) + // |> Ok + // } + // } +} diff --git a/src/gleam_community/maths/int.gleam b/src/gleam_community/maths/int.gleam new file mode 100644 index 0000000..3e89f1b --- /dev/null +++ b/src/gleam_community/maths/int.gleam @@ -0,0 +1,455 @@ +//// +//// +//// +//// +//// +//// +//// A module containing several different kinds of mathematical functions +//// applying to integer numbers. +//// +//// Function naming has been adopted from C mathematical function. +//// +//// --- +//// +//// * **Mathematical functions** +//// * [`absdiff`](#abs_diff) +//// * [`flipsign`](#flipsign) +//// * [`max`](#max) +//// * [`min`](#min) +//// * [`minmax`](#minmax) +//// * [`round`](#round) +//// * [`sign`](#sign) +//// * **Combinatorial functions** +//// * [`combination`](#combination) +//// * [`factorial`](#factorial) +//// * [`permutation`](#permutation) +//// * **Tests** +//// * [`ispow2`](#ispow2) +//// * [`iseven`](#iseven) +//// * [`isodd`](#isodd) + +import gleam/list +import gleam/int +import gleam/float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The min function. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.min(2.0, 1.5) +/// |> should.equal(1.5) +/// +/// math.min(1.5, 2.0) +/// |> should.equal(1.5) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn min(x: Int, y: Int) -> Int { + case x < y { + True -> x + False -> y + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The min function. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.min(2.0, 1.5) +/// |> should.equal(1.5) +/// +/// math.min(1.5, 2.0) +/// |> should.equal(1.5) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn max(x: Int, y: Int) -> Int { + case x > y { + True -> x + False -> y + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The minmax function. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.minmax(2.0, 1.5) +/// |> should.equal(#(1.5, 2.0)) +/// +/// math.minmax(1.5, 2.0) +/// |> should.equal(#(1.5, 2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn minmax(x: Int, y: Int) -> #(Int, Int) { + #(min(x, y), max(x, y)) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The sign function which returns the sign of the input, indicating +/// whether it is positive, negative, or zero. +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn sign(x: Int) -> Int { + do_sign(x) +} + +if erlang { + fn do_sign(x: Int) -> Int { + case x < 0 { + True -> -1 + False -> + case x == 0 { + True -> 0 + False -> 1 + } + } + } +} + +if javascript { + external fn do_sign(Int) -> Int = + "../math.mjs" "sign" +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn flipsign(x: Int) -> Int { + -1 * x +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the number of a $$k$$-combinations of $$n$$ elements: +/// +/// \\[ +/// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!} +/// \\] +/// Also known as "$$n$$ choose $$k$$" or the binomial coefficient. +/// +/// The implementation uses the effecient iterative multiplicative formula for the computation. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// // Invalid input gives an error +/// // Error on: n = -1 < 0 +/// math.combination(-1, 1) +/// |> should.be_error() +/// +/// // Valid input returns a result +/// math.combination(4, 0) +/// |> should.equal(Ok(1)) +/// +/// math.combination(4, 4) +/// |> should.equal(Ok(1)) +/// +/// math.combination(4, 2) +/// |> should.equal(Ok(6)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn combination(n: Int, k: Int) -> Result(Int, String) { + case n < 0 { + True -> + "Invalid input argument: n < 0. Valid input is n > 0." + |> Error + False -> + case k < 0 || k > n { + True -> + 0 + |> Ok + False -> + case k == 0 || k == n { + True -> + 1 + |> Ok + False -> { + let min = case k < n - k { + True -> k + False -> n - k + } + list.range(1, min) + |> list.fold( + 1, + fn(acc: Int, x: Int) -> Int { acc * { n + 1 - x } / x }, + ) + |> Ok + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the total number of combinations of $$n$$ +/// elements, that is $$n!$$. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// // Invalid input gives an error +/// math.factorial(-1) +/// |> should.be_error() +/// +/// // Valid input returns a result +/// math.factorial(0) +/// |> should.equal(Ok(1)) +/// math.factorial(1) +/// |> should.equal(Ok(1)) +/// math.factorial(2) +/// |> should.equal(Ok(2)) +/// math.factorial(3) +/// |> should.equal(Ok(6)) +/// math.factorial(4) +/// |> should.equal(Ok(24)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn factorial(n) -> Result(Int, String) { + case n < 0 { + True -> + "Invalid input argument: n < 0. Valid input is n > 0." + |> Error + False -> + case n { + 0 -> + 1 + |> Ok + 1 -> + 1 + |> Ok + _ -> + list.range(1, n) + |> list.fold(1, fn(acc: Int, x: Int) { acc * x }) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the number of $$k$$-permuations (without repetitions) +/// of $$n$$ elements: +/// +/// \\[ +/// P(n, k) = \frac{n!}{(n - k)!} +/// \\] +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// // Invalid input gives an error +/// // Error on: n = -1 < 0 +/// math.permutation(-1, 1) +/// |> should.be_error() +/// +/// // Valid input returns a result +/// math.permutation(4, 0) +/// |> should.equal(Ok(1)) +/// +/// math.permutation(4, 4) +/// |> should.equal(Ok(1)) +/// +/// math.permutation(4, 2) +/// |> should.equal(Ok(12)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn permutation(n: Int, k: Int) -> Result(Int, String) { + case n < 0 { + True -> + "Invalid input argument: n < 0. Valid input is n > 0." + |> Error + False -> + case k < 0 || k > n { + True -> + 0 + |> Ok + False -> + case k == n { + True -> + 1 + |> Ok + False -> { + assert Ok(v1) = factorial(n) + assert Ok(v2) = factorial(n - k) + v1 / v2 + |> Ok + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The absolute difference: +/// +/// \\[ +/// \forall x, y \in \mathbb{Z}, \\; |x - y| \in \mathbb{Z}_{+}. +/// \\] +/// +/// The function takes two inputs $$x$$ and $$y$$ and returns a positive integer +/// value which is the the absolute difference of the inputs. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/math +/// +/// pub fn example() { +/// math.absdiff(-10.0, 10.0) +/// |> should.equal(20.0) +/// +/// math.absdiff(0.0, -2.0) +/// |> should.equal(2.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn absdiff(a: Int, b: Int) -> Int { + a - b + |> int.absolute_value() +} diff --git a/src/gleam_community/maths/int_list.gleam b/src/gleam_community/maths/int_list.gleam new file mode 100644 index 0000000..f8eb5bc --- /dev/null +++ b/src/gleam_community/maths/int_list.gleam @@ -0,0 +1,328 @@ +//// +//// +//// +//// +//// +//// +//// A module containing several different kinds of mathematical functions +//// applying to lists of real numbers. +//// +//// Function naming has been adopted from C mathematical function. +//// +//// --- +//// +//// * **Miscellaneous functions** +//// * [`allclose`](#allclose) +//// * [`amax`](#amax) +//// * [`amin`](#amin) +//// * [`argmax`](#argmax) +//// * [`argmin`](#argmin) +//// * [`allclose`](#allclose) +//// * [`trim`](#trim) + +import gleam/list +import gleam/int +import gleam/float +import gleam/pair +import gleam_community/maths/int as intx + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the indices of the minimum values in a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.argmin() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.argmin() +/// |> should.equal(Ok([4])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn argmin(arr: List(Float)) -> Result(List(Int), String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(min) = + arr + |> amin() + arr + |> list.index_map(fn(index: Int, a: Float) -> Int { + case a -. min { + 0. -> index + _ -> -1 + } + }) + |> list.filter(fn(index: Int) -> Bool { + case index { + -1 -> False + _ -> True + } + }) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the indices of the maximum values in a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.argmax() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.argmax() +/// |> should.equal(Ok([0, 1])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn argmax(arr: List(Float)) -> Result(List(Int), String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(max) = + arr + |> amax() + arr + |> list.index_map(fn(index: Int, a: Float) -> Int { + case a -. max { + 0. -> index + _ -> -1 + } + }) + |> list.filter(fn(index: Int) -> Bool { + case index { + -1 -> False + _ -> True + } + }) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the maximum value of a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.amax() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.amax() +/// |> should.equal(Ok(4.)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn amax(arr: List(Float)) -> Result(Float, String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(val0) = list.at(arr, 0) + arr + |> list.fold( + val0, + fn(acc: Float, a: Float) { + case a >. acc { + True -> a + False -> acc + } + }, + ) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the minimum value of a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.amin() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.amin() +/// |> should.equal(Ok(1.)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn amin(arr: List(Float)) -> Result(Float, String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> { + assert Ok(val0) = list.at(arr, 0) + arr + |> list.fold( + val0, + fn(acc: Float, a: Float) { + case a <. acc { + True -> a + False -> acc + } + }, + ) + |> Ok + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the minimum value of a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.amin() +/// |> should.be_error() +/// +/// // Valid input returns a result +/// [4., 4., 3., 2., 1.] +/// |> stats.amin() +/// |> should.equal(Ok(1.)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn extrema(arr: List(Float)) -> Result(#(Float, Float), String) { + todo + // case arr { + // [] -> + // "Invalid input argument: The list is empty." + // |> Error + // _ -> { + // assert Ok(val0) = list.at(arr, 0) + // arr + // |> list.fold( + // val0, + // fn(acc: Float, a: Float) { + // case a <. acc { + // True -> a + // False -> acc + // } + // }, + // ) + // |> Ok + // } + // } +} diff --git a/src/gleam_community/maths/list.gleam b/src/gleam_community/maths/list.gleam new file mode 100644 index 0000000..ba0d74a --- /dev/null +++ b/src/gleam_community/maths/list.gleam @@ -0,0 +1,87 @@ +//// +//// +//// +//// +//// +//// +//// A module containing several different kinds of mathematical functions +//// applying to lists of real numbers. +//// +//// Function naming has been adopted from C mathematical function. +//// +//// --- +//// +//// * **Miscellaneous functions** +//// * [`trim`](#trim) + +import gleam/list +import gleam/int +import gleam/float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Trim a list to a certain size given min/max indices. The min/max indices +/// are inclusive. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_stats/stats +/// +/// pub fn example () { +/// // An empty lists returns an error +/// [] +/// |> stats.trim(0, 0) +/// |> should.be_error() +/// +/// // Trim the list to only the middle part of list +/// [1., 2., 3., 4., 5., 6.] +/// |> stats.trim(1, 4) +/// |> should.equal(Ok([2., 3., 4., 5.])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn trim(arr: List(a), min: Int, max: Int) -> Result(List(a), String) { + case arr { + [] -> + "Invalid input argument: The list is empty." + |> Error + _ -> + case min >= 0 && max < list.length(arr) { + False -> + "Invalid input argument: min < 0 or max < length(arr). Valid input is min > 0 and max < length(arr)." + |> Error + True -> + arr + |> list.drop(min) + |> list.take(max - min + 1) + |> Ok + } + } +} diff --git a/temp.gleam b/temp.gleam new file mode 100644 index 0000000..aabf936 --- /dev/null +++ b/temp.gleam @@ -0,0 +1,410 @@ +//// Small examples ALSO used in the docs... + +import gleam/int +import gleam/list +import gleam/pair +import gleam_stats/stats +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +pub fn example_sum_test() { + // An empty list returns an error + [] + |> stats.sum() + |> should.equal(0.) + + // Valid input returns a result + [1., 2., 3.] + |> stats.sum() + |> should.equal(6.) +} + +pub fn example_mean_test() { + // An empty list returns an error + [] + |> stats.mean() + |> should.be_error() + + // Valid input returns a result + [1., 2., 3.] + |> stats.mean() + |> should.equal(Ok(2.)) +} + +pub fn example_median_test() { + // An empty list returns an error + [] + |> stats.median() + |> should.be_error() + + // Valid input returns a result + [1., 2., 3.] + |> stats.median() + |> should.equal(Ok(2.)) + + [1., 2., 3., 4.] + |> stats.median() + |> should.equal(Ok(2.5)) +} + +pub fn example_hmean_test() { + // An empty list returns an error + [] + |> stats.hmean() + |> should.be_error() + + // List with negative numbers returns an error + [-1., -3., -6.] + |> stats.hmean() + |> should.be_error() + + // Valid input returns a result + [1., 3., 6.] + |> stats.hmean() + |> should.equal(Ok(2.)) +} + +pub fn example_gmean_test() { + // An empty list returns an error + [] + |> stats.gmean() + |> should.be_error() + // List with negative numbers returns an error + [-1., -3., -6.] + |> stats.gmean() + |> should.be_error() + // Valid input returns a result + [1., 3., 9.] + |> stats.gmean() + |> should.equal(Ok(3.)) +} + +pub fn example_var_test() { + // Degrees of freedom + let ddof: Int = 1 + + // An empty list returns an error + [] + |> stats.var(ddof) + |> should.be_error() + + // Valid input returns a result + [1., 2., 3.] + |> stats.var(ddof) + |> should.equal(Ok(1.)) +} + +pub fn example_std_test() { + // Degrees of freedom + let ddof: Int = 1 + + // An empty list returns an error + [] + |> stats.std(ddof) + |> should.be_error() + + // Valid input returns a result + [1., 2., 3.] + |> stats.std(ddof) + |> should.equal(Ok(1.)) +} + +pub fn example_moment_test() { + // An empty list returns an error + [] + |> stats.moment(0) + |> should.be_error() + + // 0th moment about the mean is 1. per definition + [0., 1., 2., 3., 4.] + |> stats.moment(0) + |> should.equal(Ok(1.)) + + // 1st moment about the mean is 0. per definition + [0., 1., 2., 3., 4.] + |> stats.moment(1) + |> should.equal(Ok(0.)) + + // 2nd moment about the mean + [0., 1., 2., 3., 4.] + |> stats.moment(2) + |> should.equal(Ok(2.)) +} + +pub fn example_skewness_test() { + // An empty list returns an error + [] + |> stats.skewness() + |> should.be_error() + + // No skewness + // -> Zero skewness + [1., 2., 3., 4.] + |> stats.skewness() + |> should.equal(Ok(0.)) + + // Right-skewed distribution + // -> Positive skewness + [1., 1., 1., 2.] + |> stats.skewness() + |> fn(x: Result(Float, String)) -> Bool { + case x { + Ok(x) -> x >. 0. + _ -> False + } + } + |> should.be_true() +} + +pub fn example_kurtosis_test() { + // An empty list returns an error + [] + |> stats.skewness() + |> should.be_error() + + // No tail + // -> Fisher's definition gives kurtosis -3 + [1., 1., 1., 1.] + |> stats.kurtosis() + |> should.equal(Ok(-3.)) + + // Distribution with a tail + // -> Higher kurtosis + [1., 1., 1., 2.] + |> stats.kurtosis() + |> fn(x: Result(Float, String)) -> Bool { + case x { + Ok(x) -> x >. -3. + _ -> False + } + } + |> should.be_true() +} + +pub fn example_zscore_test() { + // An empty list returns an error + [] + // Use degrees of freedom = 1 + |> stats.zscore(1) + |> should.be_error() + + [1., 2., 3.] + // Use degrees of freedom = 1 + |> stats.zscore(1) + |> should.equal(Ok([-1., 0., 1.])) +} + +pub fn example_percentile_test() { + // An empty list returns an error + [] + |> stats.percentile(40) + |> should.be_error() + + // Calculate 40th percentile + [15., 20., 35., 40., 50.] + |> stats.percentile(40) + |> should.equal(Ok(29.)) +} + +pub fn example_iqr_test() { + // An empty list returns an error + [] + |> stats.iqr() + |> should.be_error() + + // Valid input returns a result + [1., 2., 3., 4., 5.] + |> stats.iqr() + |> should.equal(Ok(3.)) +} + +pub fn example_freedman_diaconis_rule_test() { + // An empty list returns an error + [] + |> stats.freedman_diaconis_rule() + |> should.be_error() + + // Calculate histogram bin widths + list.range(0, 1000) + |> list.map(fn(x: Int) -> Float { int.to_float(x) }) + |> stats.freedman_diaconis_rule() + |> should.equal(Ok(10.)) +} + +pub fn example_range_test() { + // Create a range + let range = stats.Range(0., 1.) + // Retrieve min and max values + let stats.Range(min, max) = range + min + |> should.equal(0.) + max + |> should.equal(1.) +} + +pub fn example_bin_test() { + // Create a bin + let bin: stats.Bin = #(stats.Range(0., 1.), 999) + // Retrieve min and max values + let stats.Range(min, max) = pair.first(bin) + min + |> should.equal(0.) + max + |> should.equal(1.) + // Retrieve count + let count = pair.second(bin) + count + |> should.equal(999) +} + +pub fn example_histogram_test() { + // An empty lists returns an error + [] + |> stats.histogram(1.) + |> should.be_error() + // Create the bins of a histogram given a list of values + list.range(0, 100) + |> list.map(fn(x: Int) -> Float { int.to_float(x) }) + // Below 25. is the bin width + // The Freedman-Diaconis’s Rule can be used to determine a decent value + |> stats.histogram(25.) + |> should.equal(Ok([ + #(stats.Range(0., 25.), 25), + #(stats.Range(25., 50.), 25), + #(stats.Range(50., 75.), 25), + #(stats.Range(75., 100.), 25), + ])) +} + +pub fn example_correlation_test() { + // An empty lists returns an error + stats.correlation([], []) + |> should.be_error() + + // Lists with fewer than 2 elements return an error + stats.correlation([1.0], [1.0]) + |> should.be_error() + + // Lists of uneqal length return an error + stats.correlation([1.0, 2.0, 3.0], [1.0, 2.0]) + |> should.be_error() + + // Perfect positive correlation + let xarr0: List(Float) = + list.range(0, 100) + |> list.map(fn(x: Int) -> Float { int.to_float(x) }) + let yarr0: List(Float) = + list.range(0, 100) + |> list.map(fn(x: Int) -> Float { int.to_float(x) }) + stats.correlation(xarr0, yarr0) + |> should.equal(Ok(1.)) + + // Perfect negative correlation + let xarr0: List(Float) = + list.range(0, 100) + |> list.map(fn(x: Int) -> Float { -1. *. int.to_float(x) }) + let yarr0: List(Float) = + list.range(0, 100) + |> list.map(fn(x: Int) -> Float { int.to_float(x) }) + stats.correlation(xarr0, yarr0) + |> should.equal(Ok(-1.)) +} + +pub fn example_trim_test() { + // An empty lists returns an error + [] + |> stats.trim(0, 0) + |> should.be_error() + + // Trim the list to only the middle part of list + [1., 2., 3., 4., 5., 6.] + |> stats.trim(1, 4) + |> should.equal(Ok([2., 3., 4., 5.])) +} + +pub fn example_isclose_test() { + let val: Float = 99. + let ref_val: Float = 100. + // We set 'atol' and 'rtol' such that the values are equivalent + // if 'val' is within 1 percent of 'ref_val' +/- 0.1 + let rtol: Float = 0.01 + let atol: Float = 0.10 + stats.isclose(val, ref_val, rtol, atol) + |> should.be_true() +} + +pub fn example_allclose_test() { + let val: Float = 99. + let ref_val: Float = 100. + let xarr: List(Float) = list.repeat(val, 42) + let yarr: List(Float) = list.repeat(ref_val, 42) + // We set 'atol' and 'rtol' such that the values are equivalent + // if 'val' is within 1 percent of 'ref_val' +/- 0.1 + let rtol: Float = 0.01 + let atol: Float = 0.10 + stats.allclose(xarr, yarr, rtol, atol) + |> fn(zarr: Result(List(Bool), String)) -> Result(Bool, Nil) { + case zarr { + Ok(arr) -> + arr + |> list.all(fn(a: Bool) -> Bool { a }) + |> Ok + _ -> + Nil + |> Error + } + } + |> should.equal(Ok(True)) +} + +pub fn example_amax_test() { + // An empty lists returns an error + [] + |> stats.amax() + |> should.be_error() + + // Valid input returns a result + [4., 4., 3., 2., 1.] + |> stats.amax() + |> should.equal(Ok(4.)) +} + +pub fn example_amin_test() { + // An empty lists returns an error + [] + |> stats.amin() + |> should.be_error() + + // Valid input returns a result + [4., 4., 3., 2., 1.] + |> stats.amin() + |> should.equal(Ok(1.)) +} + +pub fn example_argmax_test() { + // An empty lists returns an error + [] + |> stats.argmax() + |> should.be_error() + + // Valid input returns a result + [4., 4., 3., 2., 1.] + |> stats.argmax() + |> should.equal(Ok([0, 1])) +} + +pub fn example_argmin_test() { + // An empty lists returns an error + [] + |> stats.argmin() + |> should.be_error() + + // Valid input returns a result + [4., 4., 3., 2., 1.] + |> stats.argmin() + |> should.equal(Ok([4])) +} diff --git a/test/eunit_progress.erl b/test/eunit_progress.erl new file mode 100644 index 0000000..faef6b5 --- /dev/null +++ b/test/eunit_progress.erl @@ -0,0 +1,592 @@ +%% eunit_formatters https://github.com/seancribbs/eunit_formatters +%% Changes made to the original code: +%% - Embedded binomial_heap.erl file contents into current file. +%% - ignore warnings for heap implementation to keep complete implementation. +%% - removed "namespaced_dicts" dependant preprocessor directive, +%% as it does not apply for our project, we just assume OTP version >= 17. +%% This is because the previous verison uses rebar, and we won't do that. + +%% Copyright 2014 Sean Cribbs +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. + + +%% @doc A listener/reporter for eunit that prints '.' for each +%% success, 'F' for each failure, and 'E' for each error. It can also +%% optionally summarize the failures at the end. +-compile({nowarn_unused_function, [insert/2, to_list/1, to_list/2, size/1]}). +-module(eunit_progress). +-behaviour(eunit_listener). +-define(NOTEST, true). +-include_lib("eunit/include/eunit.hrl"). + +-define(RED, "\e[0;31m"). +-define(GREEN, "\e[0;32m"). +-define(YELLOW, "\e[0;33m"). +-define(WHITE, "\e[0;37m"). +-define(CYAN, "\e[0;36m"). +-define(RESET, "\e[0m"). + +-record(node,{ + rank = 0 :: non_neg_integer(), + key :: term(), + value :: term(), + children = new() :: binomial_heap() + }). + +-export_type([binomial_heap/0, heap_node/0]). +-type binomial_heap() :: [ heap_node() ]. +-type heap_node() :: #node{}. + +%% eunit_listener callbacks +-export([ + init/1, + handle_begin/3, + handle_end/3, + handle_cancel/3, + terminate/2, + start/0, + start/1 + ]). + +%% -- binomial_heap.erl content start -- + +-record(state, { + status = dict:new() :: euf_dict(), + failures = [] :: [[pos_integer()]], + skips = [] :: [[pos_integer()]], + timings = new() :: binomial_heap(), + colored = true :: boolean(), + profile = false :: boolean() + }). + +-type euf_dict() :: dict:dict(). + +-spec new() -> binomial_heap(). +new() -> + []. + +% Inserts a new pair into the heap (or creates a new heap) +-spec insert(term(), term()) -> binomial_heap(). +insert(Key,Value) -> + insert(Key,Value,[]). + +-spec insert(term(), term(), binomial_heap()) -> binomial_heap(). +insert(Key,Value,Forest) -> + insTree(#node{key=Key,value=Value},Forest). + +% Merges two heaps +-spec merge(binomial_heap(), binomial_heap()) -> binomial_heap(). +merge(TS1,[]) when is_list(TS1) -> TS1; +merge([],TS2) when is_list(TS2) -> TS2; +merge([#node{rank=R1}=T1|TS1]=F1,[#node{rank=R2}=T2|TS2]=F2) -> + if + R1 < R2 -> + [T1 | merge(TS1,F2)]; + R2 < R1 -> + [T2 | merge(F1, TS2)]; + true -> + insTree(link(T1,T2),merge(TS1,TS2)) + end. + +% Deletes the top entry from the heap and returns it +-spec delete(binomial_heap()) -> {{term(), term()}, binomial_heap()}. +delete(TS) -> + {#node{key=Key,value=Value,children=TS1},TS2} = getMin(TS), + {{Key,Value},merge(lists:reverse(TS1),TS2)}. + +% Turns the heap into list in heap order +-spec to_list(binomial_heap()) -> [{term(), term()}]. +to_list([]) -> []; +to_list(List) when is_list(List) -> + to_list([],List). +to_list(Acc, []) -> + lists:reverse(Acc); +to_list(Acc,Forest) -> + {Next, Trees} = delete(Forest), + to_list([Next|Acc], Trees). + +% Take N elements from the top of the heap +-spec take(non_neg_integer(), binomial_heap()) -> [{term(), term()}]. +take(N,Trees) when is_integer(N), is_list(Trees) -> + take(N,Trees,[]). +take(0,_Trees,Acc) -> + lists:reverse(Acc); +take(_N,[],Acc)-> + lists:reverse(Acc); +take(N,Trees,Acc) -> + {Top,T2} = delete(Trees), + take(N-1,T2,[Top|Acc]). + +% Get an estimate of the size based on the binomial property +-spec size(binomial_heap()) -> non_neg_integer(). +size(Forest) -> + erlang:trunc(lists:sum([math:pow(2,R) || #node{rank=R} <- Forest])). + +%% Private API +-spec link(heap_node(), heap_node()) -> heap_node(). +link(#node{rank=R,key=X1,children=C1}=T1,#node{key=X2,children=C2}=T2) -> + case X1 < X2 of + true -> + T1#node{rank=R+1,children=[T2|C1]}; + _ -> + T2#node{rank=R+1,children=[T1|C2]} + end. + +insTree(Tree, []) -> + [Tree]; +insTree(#node{rank=R1}=T1, [#node{rank=R2}=T2|Rest] = TS) -> + case R1 < R2 of + true -> + [T1|TS]; + _ -> + insTree(link(T1,T2),Rest) + end. + +getMin([T]) -> + {T,[]}; +getMin([#node{key=K} = T|TS]) -> + {#node{key=K1} = T1,TS1} = getMin(TS), + case K < K1 of + true -> {T,TS}; + _ -> {T1,[T|TS1]} + end. + +%% -- binomial_heap.erl content end -- + +%% Startup +start() -> + start([]). + +start(Options) -> + eunit_listener:start(?MODULE, Options). + +%%------------------------------------------ +%% eunit_listener callbacks +%%------------------------------------------ +init(Options) -> + #state{colored=proplists:get_bool(colored, Options), + profile=proplists:get_bool(profile, Options)}. + +handle_begin(group, Data, St) -> + GID = proplists:get_value(id, Data), + Dict = St#state.status, + St#state{status=dict:store(GID, orddict:from_list([{type, group}|Data]), Dict)}; +handle_begin(test, Data, St) -> + TID = proplists:get_value(id, Data), + Dict = St#state.status, + St#state{status=dict:store(TID, orddict:from_list([{type, test}|Data]), Dict)}. + +handle_end(group, Data, St) -> + St#state{status=merge_on_end(Data, St#state.status)}; +handle_end(test, Data, St) -> + NewStatus = merge_on_end(Data, St#state.status), + St1 = print_progress(Data, St), + St2 = record_timing(Data, St1), + St2#state{status=NewStatus}. + +handle_cancel(_, Data, #state{status=Status, skips=Skips}=St) -> + Status1 = merge_on_end(Data, Status), + ID = proplists:get_value(id, Data), + St#state{status=Status1, skips=[ID|Skips]}. + +terminate({ok, Data}, St) -> + print_failures(St), + print_pending(St), + print_profile(St), + print_timing(St), + print_results(Data, St); +terminate({error, Reason}, St) -> + io:nl(), io:nl(), + print_colored(io_lib:format("Eunit failed: ~25p~n", [Reason]), ?RED, St), + sync_end(error). + +sync_end(Result) -> + receive + {stop, Reference, ReplyTo} -> + ReplyTo ! {result, Reference, Result}, + ok + end. + +%%------------------------------------------ +%% Print and collect information during run +%%------------------------------------------ +print_progress(Data, St) -> + TID = proplists:get_value(id, Data), + case proplists:get_value(status, Data) of + ok -> + print_progress_success(St), + St; + {skipped, _Reason} -> + print_progress_skipped(St), + St#state{skips=[TID|St#state.skips]}; + {error, Exception} -> + print_progress_failed(Exception, St), + St#state{failures=[TID|St#state.failures]} + end. + +record_timing(Data, State=#state{timings=T, profile=true}) -> + TID = proplists:get_value(id, Data), + case lists:keyfind(time, 1, Data) of + {time, Int} -> + %% It's a min-heap, so we insert negative numbers instead + %% of the actuals and normalize when we report on them. + T1 = insert(-Int, TID, T), + State#state{timings=T1}; + false -> + State + end; +record_timing(_Data, State) -> + State. + +print_progress_success(St) -> + print_colored(".", ?GREEN, St). + +print_progress_skipped(St) -> + print_colored("*", ?YELLOW, St). + +print_progress_failed(_Exc, St) -> + print_colored("F", ?RED, St). + +merge_on_end(Data, Dict) -> + ID = proplists:get_value(id, Data), + dict:update(ID, + fun(Old) -> + orddict:merge(fun merge_data/3, Old, orddict:from_list(Data)) + end, Dict). + +merge_data(_K, undefined, X) -> X; +merge_data(_K, X, undefined) -> X; +merge_data(_K, _, X) -> X. + +%%------------------------------------------ +%% Print information at end of run +%%------------------------------------------ +print_failures(#state{failures=[]}) -> + ok; +print_failures(#state{failures=Fails}=State) -> + io:nl(), + io:fwrite("Failures:~n",[]), + lists:foldr(print_failure_fun(State), 1, Fails), + ok. + +print_failure_fun(#state{status=Status}=State) -> + fun(Key, Count) -> + TestData = dict:fetch(Key, Status), + TestId = format_test_identifier(TestData), + io:fwrite("~n ~p) ~ts~n", [Count, TestId]), + print_failure_reason(proplists:get_value(status, TestData), + proplists:get_value(output, TestData), + State), + io:nl(), + Count + 1 + end. + +print_failure_reason({skipped, Reason}, _Output, State) -> + print_colored(io_lib:format(" ~ts~n", [format_pending_reason(Reason)]), + ?RED, State); +print_failure_reason({error, {_Class, Term, Stack}}, Output, State) when + is_tuple(Term), tuple_size(Term) == 2, is_list(element(2, Term)) -> + print_assertion_failure(Term, Stack, Output, State), + print_failure_output(5, Output, State); +print_failure_reason({error, {error, Error, Stack}}, Output, State) when is_list(Stack) -> + print_colored(indent(5, "Failure: ~p~n", [Error]), ?RED, State), + print_stack(Stack, State), + print_failure_output(5, Output, State); +print_failure_reason({error, Reason}, Output, State) -> + print_colored(indent(5, "Failure: ~p~n", [Reason]), ?RED, State), + print_failure_output(5, Output, State). + +print_stack(Stack, State) -> + print_colored(indent(5, "Stacktrace:~n", []), ?CYAN, State), + print_stackframes(Stack, State). +print_stackframes([{eunit_test, _, _, _} | Stack], State) -> + print_stackframes(Stack, State); +print_stackframes([{eunit_proc, _, _, _} | Stack], State) -> + print_stackframes(Stack, State); +print_stackframes([{Module, Function, _Arity, _Location} | Stack], State) -> + print_colored(indent(7, "~p.~p~n", [Module, Function]), ?CYAN, State), + print_stackframes(Stack, State); +print_stackframes([], _State) -> + ok. + + +print_failure_output(_, <<>>, _) -> ok; +print_failure_output(_, undefined, _) -> ok; +print_failure_output(Indent, Output, State) -> + print_colored(indent(Indent, "Output: ~ts", [Output]), ?CYAN, State). + +print_assertion_failure({Type, Props}, Stack, Output, State) -> + FailureDesc = format_assertion_failure(Type, Props, 5), + {M,F,A,Loc} = lists:last(Stack), + LocationText = io_lib:format(" %% ~ts:~p:in `~ts`", [proplists:get_value(file, Loc), + proplists:get_value(line, Loc), + format_function_name(M,F,A)]), + print_colored(FailureDesc, ?RED, State), + io:nl(), + print_colored(LocationText, ?CYAN, State), + io:nl(), + print_failure_output(5, Output, State), + io:nl(). + +print_pending(#state{skips=[]}) -> + ok; +print_pending(#state{status=Status, skips=Skips}=State) -> + io:nl(), + io:fwrite("Pending:~n", []), + lists:foreach(fun(ID) -> + Info = dict:fetch(ID, Status), + case proplists:get_value(reason, Info) of + undefined -> + ok; + Reason -> + print_pending_reason(Reason, Info, State) + end + end, lists:reverse(Skips)), + io:nl(). + +print_pending_reason(Reason0, Data, State) -> + Text = case proplists:get_value(type, Data) of + group -> + io_lib:format(" ~ts~n", [proplists:get_value(desc, Data)]); + test -> + io_lib:format(" ~ts~n", [format_test_identifier(Data)]) + end, + Reason = io_lib:format(" %% ~ts~n", [format_pending_reason(Reason0)]), + print_colored(Text, ?YELLOW, State), + print_colored(Reason, ?CYAN, State). + +print_profile(#state{timings=T, status=Status, profile=true}=State) -> + TopN = take(10, T), + TopNTime = abs(lists:sum([ Time || {Time, _} <- TopN ])), + TLG = dict:fetch([], Status), + TotalTime = proplists:get_value(time, TLG), + if TotalTime =/= undefined andalso TotalTime > 0 andalso TopN =/= [] -> + TopNPct = (TopNTime / TotalTime) * 100, + io:nl(), io:nl(), + io:fwrite("Top ~p slowest tests (~ts, ~.1f% of total time):", [length(TopN), format_time(TopNTime), TopNPct]), + lists:foreach(print_timing_fun(State), TopN), + io:nl(); + true -> ok + end; +print_profile(#state{profile=false}) -> + ok. + +print_timing(#state{status=Status}) -> + TLG = dict:fetch([], Status), + Time = proplists:get_value(time, TLG), + io:nl(), + io:fwrite("Finished in ~ts~n", [format_time(Time)]), + ok. + +print_results(Data, State) -> + Pass = proplists:get_value(pass, Data, 0), + Fail = proplists:get_value(fail, Data, 0), + Skip = proplists:get_value(skip, Data, 0), + Cancel = proplists:get_value(cancel, Data, 0), + Total = Pass + Fail + Skip + Cancel, + {Color, Result} = if Fail > 0 -> {?RED, error}; + Skip > 0; Cancel > 0 -> {?YELLOW, error}; + Pass =:= 0 -> {?YELLOW, ok}; + true -> {?GREEN, ok} + end, + print_results(Color, Total, Fail, Skip, Cancel, State), + sync_end(Result). + +print_results(Color, 0, _, _, _, State) -> + print_colored(Color, "0 tests\n", State); +print_results(Color, Total, Fail, Skip, Cancel, State) -> + SkipText = format_optional_result(Skip, "skipped"), + CancelText = format_optional_result(Cancel, "cancelled"), + Text = io_lib:format("~p tests, ~p failures~ts~ts~n", [Total, Fail, SkipText, CancelText]), + print_colored(Text, Color, State). + +print_timing_fun(#state{status=Status}=State) -> + fun({Time, Key}) -> + TestData = dict:fetch(Key, Status), + TestId = format_test_identifier(TestData), + io:nl(), + io:fwrite(" ~ts~n", [TestId]), + print_colored([" "|format_time(abs(Time))], ?CYAN, State) + end. + +%%------------------------------------------ +%% Print to the console with the given color +%% if enabled. +%%------------------------------------------ +print_colored(Text, Color, #state{colored=true}) -> + io:fwrite("~s~ts~s", [Color, Text, ?RESET]); +print_colored(Text, _Color, #state{colored=false}) -> + io:fwrite("~ts", [Text]). + +%%------------------------------------------ +%% Generic data formatters +%%------------------------------------------ +format_function_name(M, F, A) -> + io_lib:format("~ts:~ts/~p", [M, F, A]). + +format_optional_result(0, _) -> + []; +format_optional_result(Count, Text) -> + io_lib:format(", ~p ~ts", [Count, Text]). + +format_test_identifier(Data) -> + {Mod, Fun, Arity} = proplists:get_value(source, Data), + Line = case proplists:get_value(line, Data) of + 0 -> ""; + L -> io_lib:format(":~p", [L]) + end, + Desc = case proplists:get_value(desc, Data) of + undefined -> ""; + DescText -> io_lib:format(": ~ts", [DescText]) + end, + io_lib:format("~ts~ts~ts", [format_function_name(Mod, Fun, Arity), Line, Desc]). + +format_time(undefined) -> + "? seconds"; +format_time(Time) -> + io_lib:format("~.3f seconds", [Time / 1000]). + +format_pending_reason({module_not_found, M}) -> + io_lib:format("Module '~ts' missing", [M]); +format_pending_reason({no_such_function, {M,F,A}}) -> + io_lib:format("Function ~ts undefined", [format_function_name(M,F,A)]); +format_pending_reason({exit, Reason}) -> + io_lib:format("Related process exited with reason: ~p", [Reason]); +format_pending_reason(Reason) -> + io_lib:format("Unknown error: ~p", [Reason]). + +%% @doc Formats all the known eunit assertions, you're on your own if +%% you make an assertion yourself. +format_assertion_failure(Type, Props, I) when Type =:= assertion_failed + ; Type =:= assert -> + Keys = proplists:get_keys(Props), + HasEUnitProps = ([expression, value] -- Keys) =:= [], + HasHamcrestProps = ([expected, actual, matcher] -- Keys) =:= [], + if + HasEUnitProps -> + [indent(I, "Failure: ?assert(~ts)~n", [proplists:get_value(expression, Props)]), + indent(I, " expected: true~n", []), + case proplists:get_value(value, Props) of + false -> + indent(I, " got: false", []); + {not_a_boolean, V} -> + indent(I, " got: ~p", [V]) + end]; + HasHamcrestProps -> + [indent(I, "Failure: ?assertThat(~p)~n", [proplists:get_value(matcher, Props)]), + indent(I, " expected: ~p~n", [proplists:get_value(expected, Props)]), + indent(I, " got: ~p", [proplists:get_value(actual, Props)])]; + true -> + [indent(I, "Failure: unknown assert: ~p", [Props])] + end; + +format_assertion_failure(Type, Props, I) when Type =:= assertMatch_failed + ; Type =:= assertMatch -> + Expr = proplists:get_value(expression, Props), + Pattern = proplists:get_value(pattern, Props), + Value = proplists:get_value(value, Props), + [indent(I, "Failure: ?assertMatch(~ts, ~ts)~n", [Pattern, Expr]), + indent(I, " expected: = ~ts~n", [Pattern]), + indent(I, " got: ~p", [Value])]; + +format_assertion_failure(Type, Props, I) when Type =:= assertNotMatch_failed + ; Type =:= assertNotMatch -> + Expr = proplists:get_value(expression, Props), + Pattern = proplists:get_value(pattern, Props), + Value = proplists:get_value(value, Props), + [indent(I, "Failure: ?assertNotMatch(~ts, ~ts)~n", [Pattern, Expr]), + indent(I, " expected not: = ~ts~n", [Pattern]), + indent(I, " got: ~p", [Value])]; + +format_assertion_failure(Type, Props, I) when Type =:= assertEqual_failed + ; Type =:= assertEqual -> + Expr = proplists:get_value(expression, Props), + Expected = proplists:get_value(expected, Props), + Value = proplists:get_value(value, Props), + [indent(I, "Failure: ?assertEqual(~w, ~ts)~n", [Expected, + Expr]), + indent(I, " expected: ~p~n", [Expected]), + indent(I, " got: ~p", [Value])]; + +format_assertion_failure(Type, Props, I) when Type =:= assertNotEqual_failed + ; Type =:= assertNotEqual -> + Expr = proplists:get_value(expression, Props), + Value = proplists:get_value(value, Props), + [indent(I, "Failure: ?assertNotEqual(~p, ~ts)~n", + [Value, Expr]), + indent(I, " expected not: == ~p~n", [Value]), + indent(I, " got: ~p", [Value])]; + +format_assertion_failure(Type, Props, I) when Type =:= assertException_failed + ; Type =:= assertException -> + Expr = proplists:get_value(expression, Props), + Pattern = proplists:get_value(pattern, Props), + {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DATA + [indent(I, "Failure: ?assertException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]), + case proplists:is_defined(unexpected_success, Props) of + true -> + [indent(I, " expected: exception ~ts but nothing was raised~n", [Pattern]), + indent(I, " got: value ~p", [proplists:get_value(unexpected_success, Props)])]; + false -> + Ex = proplists:get_value(unexpected_exception, Props), + [indent(I, " expected: exception ~ts~n", [Pattern]), + indent(I, " got: exception ~p", [Ex])] + end]; + +format_assertion_failure(Type, Props, I) when Type =:= assertNotException_failed + ; Type =:= assertNotException -> + Expr = proplists:get_value(expression, Props), + Pattern = proplists:get_value(pattern, Props), + {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DAT + Ex = proplists:get_value(unexpected_exception, Props), + [indent(I, "Failure: ?assertNotException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]), + indent(I, " expected not: exception ~ts~n", [Pattern]), + indent(I, " got: exception ~p", [Ex])]; + +format_assertion_failure(Type, Props, I) when Type =:= command_failed + ; Type =:= command -> + Cmd = proplists:get_value(command, Props), + Expected = proplists:get_value(expected_status, Props), + Status = proplists:get_value(status, Props), + [indent(I, "Failure: ?cmdStatus(~p, ~p)~n", [Expected, Cmd]), + indent(I, " expected: status ~p~n", [Expected]), + indent(I, " got: status ~p", [Status])]; + +format_assertion_failure(Type, Props, I) when Type =:= assertCmd_failed + ; Type =:= assertCmd -> + Cmd = proplists:get_value(command, Props), + Expected = proplists:get_value(expected_status, Props), + Status = proplists:get_value(status, Props), + [indent(I, "Failure: ?assertCmdStatus(~p, ~p)~n", [Expected, Cmd]), + indent(I, " expected: status ~p~n", [Expected]), + indent(I, " got: status ~p", [Status])]; + +format_assertion_failure(Type, Props, I) when Type =:= assertCmdOutput_failed + ; Type =:= assertCmdOutput -> + Cmd = proplists:get_value(command, Props), + Expected = proplists:get_value(expected_output, Props), + Output = proplists:get_value(output, Props), + [indent(I, "Failure: ?assertCmdOutput(~p, ~p)~n", [Expected, Cmd]), + indent(I, " expected: ~p~n", [Expected]), + indent(I, " got: ~p", [Output])]; + +format_assertion_failure(Type, Props, I) -> + indent(I, "~p", [{Type, Props}]). + +indent(I, Fmt, Args) -> + io_lib:format("~" ++ integer_to_list(I) ++ "s" ++ Fmt, [" "|Args]). + +extract_exception_pattern(Str) -> + ["{", Class, Term|_] = re:split(Str, "[, ]{1,2}", [unicode,{return,list}]), + {Class, Term}. \ No newline at end of file diff --git a/test/gleam/gleam_community_maths_float_test.gleam b/test/gleam/gleam_community_maths_float_test.gleam new file mode 100644 index 0000000..a20323a --- /dev/null +++ b/test/gleam/gleam_community_maths_float_test.gleam @@ -0,0 +1,764 @@ +import gleam_community/maths/float as floatx +import gleeunit +import gleeunit/should +import gleam/result +import gleam/io +import gleam/option + +pub fn main() { + gleeunit.main() +} + +pub fn float_acos_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + assert Ok(result) = floatx.acos(1.0) + result + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + assert Ok(result) = floatx.acos(0.5) + result + |> floatx.isclose(1.047197, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.acos(1.1) + |> should.be_error() + + floatx.acos(-1.1) + |> should.be_error() +} + +pub fn float_acosh_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + assert Ok(result) = floatx.acosh(1.0) + result + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.acosh(0.0) + |> should.be_error() +} + +pub fn float_asin_test() { + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.asin(0.0) + |> should.equal(Ok(0.0)) + + assert Ok(tol) = floatx.pow(-10.0, -6.0) + assert Ok(result) = floatx.asin(0.5) + result + |> floatx.isclose(0.523598, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.asin(1.1) + |> should.be_error() + + floatx.asin(-1.1) + |> should.be_error() +} + +pub fn float_asinh_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.asinh(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.asinh(0.5) + |> floatx.isclose(0.481211, 0.0, tol) + |> should.be_true() +} + +pub fn float_atan_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.atan(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.atan(0.5) + |> floatx.isclose(0.463647, 0.0, tol) + |> should.be_true() +} + +pub fn math_atan2_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.atan2(0.0, 0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.atan2(0.0, 1.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + // Check atan2(y=1.0, x=0.5) + // Should be equal to atan(y / x) for any x > 0 and any y + let result = floatx.atan(1.0 /. 0.5) + floatx.atan2(1.0, 0.5) + |> floatx.isclose(result, 0.0, tol) + |> should.be_true() + + // Check atan2(y=2.0, x=-1.5) + // Should be equal to pi + atan(y / x) for any x < 0 and y >= 0 + let result = floatx.pi() +. floatx.atan(2.0 /. -1.5) + floatx.atan2(2.0, -1.5) + |> floatx.isclose(result, 0.0, tol) + |> should.be_true() + + // Check atan2(y=-2.0, x=-1.5) + // Should be equal to atan(y / x) - pi for any x < 0 and y < 0 + let result = floatx.atan(-2.0 /. -1.5) -. floatx.pi() + floatx.atan2(-2.0, -1.5) + |> floatx.isclose(result, 0.0, tol) + |> should.be_true() + + // Check atan2(y=1.5, x=0.0) + // Should be equal to pi/2 for x = 0 and any y > 0 + let result = floatx.pi() /. 2.0 + floatx.atan2(1.5, 0.0) + |> floatx.isclose(result, 0.0, tol) + |> should.be_true() + + // Check atan2(y=-1.5, x=0.0) + // Should be equal to -pi/2 for x = 0 and any y < 0 + let result = -1.0 *. floatx.pi() /. 2.0 + floatx.atan2(-1.5, 0.0) + |> floatx.isclose(result, 0.0, tol) + |> should.be_true() +} + +pub fn float_atanh_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + assert Ok(result) = floatx.atanh(0.0) + result + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + assert Ok(result) = floatx.atanh(0.5) + result + |> floatx.isclose(0.549306, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.atanh(1.0) + |> should.be_error() + + floatx.atanh(2.0) + |> should.be_error() + + floatx.atanh(1.0) + |> should.be_error() + + floatx.atanh(-2.0) + |> should.be_error() +} + +pub fn float_cos_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.cos(0.0) + |> floatx.isclose(1.0, 0.0, tol) + |> should.be_true() + + floatx.cos(floatx.pi()) + |> floatx.isclose(-1.0, 0.0, tol) + |> should.be_true() + + floatx.cos(0.5) + |> floatx.isclose(0.877582, 0.0, tol) + |> should.be_true() +} + +pub fn float_cosh_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.cosh(0.0) + |> floatx.isclose(1.0, 0.0, tol) + |> should.be_true() + + floatx.cosh(0.5) + |> floatx.isclose(1.127625, 0.0, tol) + |> should.be_true() + // An (overflow) error might occur when given an input + // value that will result in a too large output value + // e.g. floatx.cosh(1000.0) but this is a property of the + // runtime. +} + +pub fn float_exp_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.exp(0.0) + |> floatx.isclose(1.0, 0.0, tol) + |> should.be_true() + + floatx.exp(0.5) + |> floatx.isclose(1.648721, 0.0, tol) + |> should.be_true() + // An (overflow) error might occur when given an input + // value that will result in a too large output value + // e.g. floatx.exp(1000.0) but this is a property of the + // runtime. +} + +pub fn float_log_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.log(1.0) + |> should.equal(Ok(0.0)) + + assert Ok(result) = floatx.log(0.5) + result + |> floatx.isclose(-0.693147, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.log(-1.0) + |> should.be_error() +} + +pub fn floatx_log10_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + assert Ok(result) = floatx.log10(1.0) + result + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + assert Ok(result) = floatx.log10(10.0) + result + |> floatx.isclose(1.0, 0.0, tol) + |> should.be_true() + + assert Ok(result) = floatx.log10(50.0) + result + |> floatx.isclose(1.698970, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.log10(-1.0) + |> should.be_error() +} + +pub fn floatx_log2_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.log2(1.0) + |> should.equal(Ok(0.0)) + + floatx.log2(2.0) + |> should.equal(Ok(1.0)) + + assert Ok(result) = floatx.log2(5.0) + result + |> floatx.isclose(2.321928, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.log2(-1.0) + |> should.be_error() +} + +pub fn floatx_logb_test() { + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.logb(10.0, 10.0) + |> should.equal(Ok(1.0)) + + floatx.logb(10.0, 100.0) + |> should.equal(Ok(0.5)) + + floatx.logb(1.0, 0.25) + |> should.equal(Ok(0.0)) + + // Check that we get an error when the function is evaluated + // outside its domain + floatx.logb(1.0, 1.0) + |> should.be_error() + + floatx.logb(10.0, 1.0) + |> should.be_error() + + floatx.logb(-1.0, 1.0) + |> should.be_error() +} + +pub fn float_pow_test() { + floatx.pow(2.0, 2.0) + |> should.equal(Ok(4.0)) + + floatx.pow(-5.0, 3.0) + |> should.equal(Ok(-125.0)) + + floatx.pow(10.5, 0.0) + |> should.equal(Ok(1.0)) + + floatx.pow(16.0, 0.5) + |> should.equal(Ok(4.0)) + + floatx.pow(2.0, -1.0) + |> should.equal(Ok(0.5)) + + floatx.pow(2.0, -1.0) + |> should.equal(Ok(0.5)) + + // floatx.pow(-1.0, 0.5) is equivalent to float.square_root(-1.0) + // and should return an error as an imaginary number would otherwise + // have to be returned + floatx.pow(-1.0, 0.5) + |> should.be_error() + + // Check another case with a negative base and fractional exponent + floatx.pow(-1.5, 1.5) + |> should.be_error() + + // floatx.pow(0.0, -1.0) is equivalent to 1. /. 0 and is expected + // to be an error + floatx.pow(0.0, -1.0) + |> should.be_error() + + // Check that a negative base and exponent is fine as long as the + // exponent is not fractional + floatx.pow(-2.0, -1.0) + |> should.equal(Ok(-0.5)) +} + +pub fn float_sqrt_test() { + floatx.sqrt(1.0) + |> should.equal(Ok(1.0)) + + floatx.sqrt(9.0) + |> should.equal(Ok(3.0)) + + // An error should be returned as an imaginary number would otherwise + // have to be returned + floatx.sqrt(-1.0) + |> should.be_error() +} + +pub fn float_cbrt_test() { + floatx.cbrt(1.0) + |> should.equal(Ok(1.0)) + + floatx.cbrt(27.0) + |> should.equal(Ok(3.0)) + + // An error should be returned as an imaginary number would otherwise + // have to be returned + floatx.cbrt(-1.0) + |> should.be_error() +} + +pub fn float_hypot_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + + floatx.hypot(0.0, 0.0) + |> should.equal(0.0) + + floatx.hypot(1.0, 0.0) + |> should.equal(1.0) + + floatx.hypot(0.0, 1.0) + |> should.equal(1.0) + + let result = floatx.hypot(11.0, 22.0) + result + |> floatx.isclose(24.596747, 0.0, tol) + |> should.be_true() +} + +pub fn float_sin_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.sin(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.sin(0.5 *. floatx.pi()) + |> floatx.isclose(1.0, 0.0, tol) + |> should.be_true() + + floatx.sin(0.5) + |> floatx.isclose(0.479425, 0.0, tol) + |> should.be_true() +} + +pub fn float_sinh_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.sinh(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.sinh(0.5) + |> floatx.isclose(0.521095, 0.0, tol) + |> should.be_true() + // An (overflow) error might occur when given an input + // value that will result in a too large output value + // e.g. floatx.sinh(1000.0) but this is a property of the + // runtime. +} + +pub fn math_tan_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.tan(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.tan(0.5) + |> floatx.isclose(0.546302, 0.0, tol) + |> should.be_true() +} + +pub fn math_tanh_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + floatx.tanh(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.tanh(25.0) + |> floatx.isclose(1.0, 0.0, tol) + |> should.be_true() + + floatx.tanh(-25.0) + |> floatx.isclose(-1.0, 0.0, tol) + |> should.be_true() + + floatx.tanh(0.5) + |> floatx.isclose(0.462117, 0.0, tol) + |> should.be_true() +} + +pub fn float_rad2deg_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + floatx.rad2deg(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.rad2deg(2.0 *. floatx.pi()) + |> floatx.isclose(360.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_deg2rads_test() { + assert Ok(tol) = floatx.pow(-10.0, -6.0) + floatx.deg2rad(0.0) + |> floatx.isclose(0.0, 0.0, tol) + |> should.be_true() + + floatx.deg2rad(360.0) + |> floatx.isclose(2.0 *. floatx.pi(), 0.0, tol) + |> should.be_true() +} + +pub fn float_ceil_test() { + floatx.ceil(0.1) + |> should.equal(1.0) + + floatx.ceil(0.9) + |> should.equal(1.0) +} + +pub fn float_floor_test() { + floatx.floor(0.1) + |> should.equal(0.0) + + floatx.floor(0.9) + |> should.equal(0.0) +} + +pub fn float_min_test() { + floatx.min(0.75, 0.5) + |> should.equal(0.5) + + floatx.min(0.5, 0.75) + |> should.equal(0.5) + + floatx.min(-0.75, 0.5) + |> should.equal(-0.75) + + floatx.min(-0.75, 0.5) + |> should.equal(-0.75) +} + +pub fn float_max_test() { + floatx.max(0.75, 0.5) + |> should.equal(0.75) + + floatx.max(0.5, 0.75) + |> should.equal(0.75) + + floatx.max(-0.75, 0.5) + |> should.equal(0.5) + + floatx.max(-0.75, 0.5) + |> should.equal(0.5) +} + +pub fn float_minmax_test() { + floatx.minmax(0.75, 0.5) + |> should.equal(#(0.5, 0.75)) + + floatx.minmax(0.5, 0.75) + |> should.equal(#(0.5, 0.75)) + + floatx.minmax(-0.75, 0.5) + |> should.equal(#(-0.75, 0.5)) + + floatx.minmax(-0.75, 0.5) + |> should.equal(#(-0.75, 0.5)) +} + +pub fn float_sign_test() { + floatx.sign(100.0) + |> should.equal(1.0) + + floatx.sign(0.0) + |> should.equal(0.0) + + floatx.sign(-100.0) + |> should.equal(-1.0) +} + +pub fn float_flipsign_test() { + floatx.flipsign(100.0) + |> should.equal(-100.0) + + floatx.flipsign(0.0) + |> should.equal(-0.0) + + floatx.flipsign(-100.0) + |> should.equal(100.0) +} + +pub fn float_beta_test() { + io.debug("TODO: Implement tests for 'floatx.beta'.") +} + +pub fn float_erf_test() { + io.debug("TODO: Implement tests for 'floatx.erf'.") +} + +pub fn float_gamma_test() { + io.debug("TODO: Implement tests for 'floatx.gamma'.") +} + +pub fn math_round_to_nearest_test() { + floatx.round(1.50, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(2.0)) + + floatx.round(1.75, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(2.0)) + + floatx.round(2.00, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(2.0)) + + floatx.round(3.50, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(4.0)) + + floatx.round(4.50, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(4.0)) + + floatx.round(-3.50, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(-4.0)) + + floatx.round(-4.50, option.Some(0), option.Some("Nearest")) + |> should.equal(Ok(-4.0)) +} + +pub fn math_round_up_test() { + floatx.round(0.45, option.Some(0), option.Some("Up")) + |> should.equal(Ok(1.0)) + + floatx.round(0.50, option.Some(0), option.Some("Up")) + |> should.equal(Ok(1.0)) + + floatx.round(0.45, option.Some(1), option.Some("Up")) + |> should.equal(Ok(0.5)) + + floatx.round(0.50, option.Some(1), option.Some("Up")) + |> should.equal(Ok(0.5)) + + floatx.round(0.455, option.Some(2), option.Some("Up")) + |> should.equal(Ok(0.46)) + + floatx.round(0.505, option.Some(2), option.Some("Up")) + |> should.equal(Ok(0.51)) +} + +pub fn math_round_down_test() { + floatx.round(0.45, option.Some(0), option.Some("Down")) + |> should.equal(Ok(0.0)) + + floatx.round(0.50, option.Some(0), option.Some("Down")) + |> should.equal(Ok(0.0)) + + floatx.round(0.45, option.Some(1), option.Some("Down")) + |> should.equal(Ok(0.4)) + + floatx.round(0.50, option.Some(1), option.Some("Down")) + |> should.equal(Ok(0.50)) + + floatx.round(0.4550, option.Some(2), option.Some("Down")) + |> should.equal(Ok(0.45)) + + floatx.round(0.5050, option.Some(2), option.Some("Down")) + |> should.equal(Ok(0.50)) +} + +pub fn math_round_to_zero_test() { + floatx.round(0.50, option.Some(0), option.Some("ToZero")) + |> should.equal(Ok(0.0)) + + floatx.round(0.75, option.Some(0), option.Some("ToZero")) + |> should.equal(Ok(0.0)) + + floatx.round(0.45, option.Some(1), option.Some("ToZero")) + |> should.equal(Ok(0.4)) + + floatx.round(0.57, option.Some(1), option.Some("ToZero")) + |> should.equal(Ok(0.50)) + + floatx.round(0.4575, option.Some(2), option.Some("ToZero")) + |> should.equal(Ok(0.45)) + + floatx.round(0.5075, option.Some(2), option.Some("ToZero")) + |> should.equal(Ok(0.50)) +} + +pub fn math_round_ties_away_test() { + floatx.round(-1.40, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(-1.0)) + + floatx.round(-1.50, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(-2.0)) + + floatx.round(-2.00, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(-2.0)) + + floatx.round(-2.50, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(-3.0)) + + floatx.round(1.40, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(1.0)) + + floatx.round(1.50, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(2.0)) + + floatx.round(2.50, option.Some(0), option.Some("TiesAway")) + |> should.equal(Ok(3.0)) +} + +pub fn math_round_ties_up_test() { + floatx.round(-1.40, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(-1.0)) + + floatx.round(-1.50, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(-1.0)) + + floatx.round(-2.00, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(-2.0)) + + floatx.round(-2.50, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(-2.0)) + + floatx.round(1.40, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(1.0)) + + floatx.round(1.50, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(2.0)) + + floatx.round(2.50, option.Some(0), option.Some("TiesUp")) + |> should.equal(Ok(3.0)) +} + +pub fn float_gammainc_test() { + // Invalid input gives an error + // 1st arg is invalid + floatx.gammainc(-1.0, 1.0) + |> should.be_error() + + // 2nd arg is invalid + floatx.gammainc(1.0, -1.0) + |> should.be_error() + + // Valid input returns a result + floatx.gammainc(1.0, 0.0) + |> result.unwrap(-999.0) + |> floatx.isclose(0.0, 0.0, 0.01) + |> should.be_true() + + floatx.gammainc(1.0, 2.0) + |> result.unwrap(-999.0) + |> floatx.isclose(0.864664716763387308106, 0.0, 0.01) + |> should.be_true() + + floatx.gammainc(2.0, 3.0) + |> result.unwrap(-999.0) + |> floatx.isclose(0.8008517265285442280826, 0.0, 0.01) + |> should.be_true() + + floatx.gammainc(3.0, 4.0) + |> result.unwrap(-999.0) + |> floatx.isclose(1.523793388892911312363, 0.0, 0.01) + |> should.be_true() +} + +pub fn float_absdiff_test() { + floatx.absdiff(0.0, 0.0) + |> should.equal(0.0) + + floatx.absdiff(1.0, 2.0) + |> should.equal(1.0) + + floatx.absdiff(2.0, 1.0) + |> should.equal(1.0) + + floatx.absdiff(-1.0, 0.0) + |> should.equal(1.0) + + floatx.absdiff(0.0, -1.0) + |> should.equal(1.0) + + floatx.absdiff(10.0, 20.0) + |> should.equal(10.0) + + floatx.absdiff(-10.0, -20.0) + |> should.equal(10.0) + + floatx.absdiff(-10.5, 10.5) + |> should.equal(21.0) +} diff --git a/test/gleam/gleam_community_maths_int_test.gleam b/test/gleam/gleam_community_maths_int_test.gleam new file mode 100644 index 0000000..aa937f4 --- /dev/null +++ b/test/gleam/gleam_community_maths_int_test.gleam @@ -0,0 +1,157 @@ +import gleam_community/maths/int as intx +import gleeunit +import gleeunit/should +import gleam/result +import gleam/io + +pub fn int_absdiff_test() { + intx.absdiff(0, 0) + |> should.equal(0) + + intx.absdiff(1, 2) + |> should.equal(1) + + intx.absdiff(2, 1) + |> should.equal(1) + + intx.absdiff(-1, 0) + |> should.equal(1) + + intx.absdiff(0, -1) + |> should.equal(1) + + intx.absdiff(10, 20) + |> should.equal(10) + + intx.absdiff(-10, -20) + |> should.equal(10) + + intx.absdiff(-10, 10) + |> should.equal(20) +} + +pub fn int_factorial_test() { + // Invalid input gives an error + intx.factorial(-1) + |> should.be_error() + + // Valid input returns a result + intx.factorial(0) + |> should.equal(Ok(1)) + + intx.factorial(1) + |> should.equal(Ok(1)) + + intx.factorial(2) + |> should.equal(Ok(2)) + + intx.factorial(3) + |> should.equal(Ok(6)) + + intx.factorial(4) + |> should.equal(Ok(24)) +} + +pub fn int_combination_test() { + // Invalid input gives an error + // Error on: n = -1 < 0 + intx.combination(-1, 1) + |> should.be_error() + + // Valid input returns a result + intx.combination(4, 0) + |> should.equal(Ok(1)) + + intx.combination(4, 4) + |> should.equal(Ok(1)) + + intx.combination(4, 2) + |> should.equal(Ok(6)) + + intx.combination(7, 5) + |> should.equal(Ok(21)) + // NOTE: Tests with the 'combination' function that produce values that + // exceed precision of the JavaScript 'Number' primitive will result in + // errors +} + +pub fn math_permutation_test() { + // Invalid input gives an error + // Error on: n = -1 < 0 + intx.permutation(-1, 1) + |> should.be_error() + + // Valid input returns a result + intx.permutation(4, 0) + |> should.equal(Ok(1)) + + intx.permutation(4, 4) + |> should.equal(Ok(1)) + + intx.permutation(4, 2) + |> should.equal(Ok(12)) +} + +pub fn float_min_test() { + intx.min(75, 50) + |> should.equal(50) + + intx.min(50, 75) + |> should.equal(50) + + intx.min(-75, 50) + |> should.equal(-75) + + intx.min(-75, 50) + |> should.equal(-75) +} + +pub fn float_max_test() { + intx.max(75, 50) + |> should.equal(75) + + intx.max(50, 75) + |> should.equal(75) + + intx.max(-75, 50) + |> should.equal(50) + + intx.max(-75, 50) + |> should.equal(50) +} + +pub fn int_minmax_test() { + intx.minmax(75, 50) + |> should.equal(#(50, 75)) + + intx.minmax(50, 75) + |> should.equal(#(50, 75)) + + intx.minmax(-75, 50) + |> should.equal(#(-75, 50)) + + intx.minmax(-75, 50) + |> should.equal(#(-75, 50)) +} + +pub fn int_sign_test() { + intx.sign(100) + |> should.equal(1) + + intx.sign(0) + |> should.equal(0) + + intx.sign(-100) + |> should.equal(-1) +} + +pub fn int_flipsign_test() { + intx.flipsign(100) + |> should.equal(-100) + + intx.flipsign(0) + |> should.equal(-0) + + intx.flipsign(-100) + |> should.equal(100) +} diff --git a/test/gleam_community_maths_test.gleam b/test/gleam_community_maths_test.gleam new file mode 100644 index 0000000..e9bdd39 --- /dev/null +++ b/test/gleam_community_maths_test.gleam @@ -0,0 +1,9 @@ +if erlang { + pub external fn main() -> Nil = + "gleam_community_maths_test_ffi" "main" +} + +if javascript { + pub external fn main() -> Nil = + "./gleam_community_maths_test_ffi.mjs" "main" +} diff --git a/test/gleam_community_maths_test_ffi.erl b/test/gleam_community_maths_test_ffi.erl new file mode 100644 index 0000000..f727223 --- /dev/null +++ b/test/gleam_community_maths_test_ffi.erl @@ -0,0 +1,40 @@ +-module(gleam_community_maths_test_ffi). + +-export([ + main/0, should_equal/2, should_not_equal/2, should_be_ok/1, + should_be_error/1 +]). + +-include_lib("eunit/include/eunit.hrl"). + +main() -> + Options = [ + no_tty, {report, {eunit_progress, [colored]}} + ], + Files = filelib:wildcard("test/**/*.{erl,gleam}"), + Modules = lists:map(fun filepath_to_module/1, Files), + case eunit:test(Modules, Options) of + ok -> erlang:halt(0); + _ -> erlang:halt(1) + end. + +filepath_to_module(Path0) -> + Path1 = string:replace(Path0, "test/", ""), + Path2 = string:replace(Path1, ".erl", ""), + Path3 = string:replace(Path2, ".gleam", ""), + Path4 = string:replace(Path3, "/", "@", all), + Path5 = list_to_binary(Path4), + binary_to_atom(Path5). + +should_equal(Actual, Expected) -> + ?assertEqual(Expected, Actual), + nil. +should_not_equal(Actual, Expected) -> + ?assertNotEqual(Expected, Actual), + nil. +should_be_ok(A) -> + ?assertMatch({ok, _}, A), + nil. +should_be_error(A) -> + ?assertMatch({error, _}, A), + nil. diff --git a/test/gleam_community_maths_test_ffi.mjs b/test/gleam_community_maths_test_ffi.mjs new file mode 100755 index 0000000..9468767 --- /dev/null +++ b/test/gleam_community_maths_test_ffi.mjs @@ -0,0 +1,35 @@ +import { opendir } from "fs/promises"; + +const dir = "build/dev/javascript/gleam_community_maths/dist/gleam/"; + +export async function main() { + console.log("Running tests..."); + + let passes = 0; + let failures = 0; + + for await (let entry of await opendir(dir)) { + if (!entry.name.endsWith("_test.mjs")) continue; + let module = await import("./gleam/" + entry.name); + + for (let fnName of Object.keys(module)) { + if (!fnName.endsWith("_test")) continue; + try { + module[fnName](); + process.stdout.write(`\u001b[32m.\u001b[0m`); + passes++; + } catch (error) { + let moduleName = "\ngleam/" + entry.name.slice(0, -3); + process.stdout.write(`\n❌ ${moduleName}.${fnName}: ${error}\n`); + failures++; + } + } + } + + console.log(` + +${passes + failures} tests +${passes} passes +${failures} failures`); + process.exit(failures ? 1 : 0); +} diff --git a/test/maths_test.gleam b/test/maths_test.gleam deleted file mode 100644 index 3831e7a..0000000 --- a/test/maths_test.gleam +++ /dev/null @@ -1,12 +0,0 @@ -import gleeunit -import gleeunit/should - -pub fn main() { - gleeunit.main() -} - -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) -}