diff --git a/README.md b/README.md index ba95f0d..153dd9d 100644 --- a/README.md +++ b/README.md @@ -11,46 +11,44 @@ The library supports both targets: Erlang and JavaScript. ```gleam import gleam/float -import gleam/iterator -import gleam/option.{Some} -import gleam_community/maths/arithmetics -import gleam_community/maths/combinatorics.{WithoutRepetitions} -import gleam_community/maths/elementary -import gleam_community/maths/piecewise -import gleam_community/maths/predicates +import gleam/yielder +import gleam_community/maths import gleeunit/should pub fn example() { // Evaluate the sine function - let result = elementary.sin(elementary.pi()) + let result = maths.sin(maths.pi()) // Set the relative and absolute tolerance - let assert Ok(absolute_tol) = elementary.power(10.0, -6.0) + let assert Ok(absolute_tol) = float.power(10.0, -6.0) let relative_tol = 0.0 // Check that the value is very close to 0.0 // That is, if 'result' is within +/- 10^(-6) - predicates.is_close(result, 0.0, relative_tol, absolute_tol) + maths.is_close(result, 0.0, relative_tol, absolute_tol) |> should.be_true() // Find the greatest common divisor - arithmetics.gcd(54, 24) + maths.gcd(54, 24) |> should.equal(6) // Find the minimum and maximum of a list - piecewise.extrema([10.0, 3.0, 50.0, 20.0, 3.0], float.compare) + maths.extrema([10.0, 3.0, 50.0, 20.0, 3.0], float.compare) |> should.equal(Ok(#(3.0, 50.0))) // Determine if a number is fractional - predicates.is_fractional(0.3333) + maths.is_fractional(0.3333) |> should.equal(True) // Generate all k = 2 combinations of [1, 2, 3] - let assert Ok(combinations) = - combinatorics.list_combination([1, 2, 3], 2, Some(WithoutRepetitions)) + let assert Ok(combinations) = maths.list_combination([1, 2, 3], 2) combinations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[1, 2], [1, 3], [2, 3]]) + + // Compute the Cosine Similarity between two (orthogonal) vectors + maths.cosine_similarity([#(-1.0, 1.0), #(1.0, 1.0), #(0.0, -1.0)]) + |> should.equal(Ok(0.0)) } ``` diff --git a/gleam.toml b/gleam.toml index df0dc71..0a6ade1 100644 --- a/gleam.toml +++ b/gleam.toml @@ -8,6 +8,7 @@ gleam = ">= 0.32.0" [dependencies] gleam_stdlib = "~> 0.38" +gleam_yielder = ">= 1.1.0 and < 2.0.0" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index adad39a..ea3a357 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,12 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, ] [requirements] gleam_stdlib = { version = "~> 0.38" } +gleam_yielder = { version = ">= 1.1.0 and < 2.0.0" } gleeunit = { version = "~> 1.0" } diff --git a/src/gleam_community/maths.gleam b/src/gleam_community/maths.gleam new file mode 100644 index 0000000..b5a76a4 --- /dev/null +++ b/src/gleam_community/maths.gleam @@ -0,0 +1,5675 @@ +//// +//// +//// +//// +//// +//// +//// --- +//// + +import gleam/bool +import gleam/float +import gleam/int +import gleam/list +import gleam/order +import gleam/result +import gleam/set +import gleam/yielder.{type Yielder, Done, Next} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function calculates the greatest common divisor of two integers +/// \\(x, y \in \mathbb{Z}\\). The greatest common divisor is the largest positive +/// integer that is divisible by both \\(x\\) and \\(y\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.gcd(1, 1) +/// |> should.equal(1) +/// +/// maths.gcd(100, 10) +/// |> should.equal(10) +/// +/// maths.gcd(-36, -17) +/// |> should.equal(1) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn gcd(x: Int, y: Int) -> Int { + let absx = int.absolute_value(x) + let absy = int.absolute_value(y) + do_gcd(absx, absy) +} + +fn do_gcd(x: Int, y: Int) -> Int { + case x == 0 { + True -> y + False -> { + let assert Ok(z) = int.modulo(y, x) + do_gcd(z, x) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// +/// Given two integers, \\(x\\) (dividend) and \\(y\\) (divisor), the Euclidean modulo +/// of \\(x\\) by \\(y\\), denoted as \\(x \mod y\\), is the remainder \\(r\\) of the +/// division of \\(x\\) by \\(y\\), such that: +/// +/// \\[ +/// x = q \cdot y + r \quad \text{and} \quad 0 \leq r < |y|, +/// \\] +/// +/// where \\(q\\) is an integer that represents the quotient of the division. +/// +/// Note that like the Gleam division operator `/` this will return `0` if one of +/// the arguments is `0`. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.int_euclidean_modulo(15, 4) +/// |> should.equal(3) +/// +/// maths.int_euclidean_modulo(-3, -2) +/// |> should.equal(1) +/// +/// maths.int_euclidean_modulo(5, 0) +/// |> should.equal(0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_euclidean_modulo(x: Int, y: Int) -> Int { + case x % y, x, y { + _, 0, _ -> 0 + _, _, 0 -> 0 + md, _, _ if md < 0 -> md + int.absolute_value(y) + md, _, _ -> md + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function calculates the least common multiple of two integers +/// \\(x, y \in \mathbb{Z}\\). The least common multiple is the smallest positive +/// integer that has both \\(x\\) and \\(y\\) as factors. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.lcm(1, 1) +/// |> should.equal(1) +/// +/// maths.lcm(100, 10) +/// |> should.equal(100) +/// +/// maths.lcm(-36, -17) +/// |> should.equal(612) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn lcm(x: Int, y: Int) -> Int { + let absx = int.absolute_value(x) + let absy = int.absolute_value(y) + absx * absy / do_gcd(absx, absy) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns all the positive divisors of an integer, including the +/// number itself. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.divisors(4) +/// |> should.equal([1, 2, 4]) +/// +/// maths.divisors(6) +/// |> should.equal([1, 2, 3, 6]) +/// +/// maths.divisors(13) +/// |> should.equal([1, 13]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn divisors(n: Int) -> List(Int) { + let nabs = float.absolute_value(int.to_float(n)) + let assert Ok(sqrt_result) = float.square_root(nabs) + let max = float.round(sqrt_result) + 1 + find_divisors(n, max, [1, n], 2) + |> list.unique() + |> list.sort(int.compare) +} + +fn find_divisors(n: Int, max: Int, acc: List(Int), i: Int) -> List(Int) { + case i <= max { + True -> { + let acc = case n % i == 0 { + True -> [i, n / i, ..acc] + False -> acc + } + find_divisors(n, max, acc, i + 1) + } + False -> acc + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns all the positive divisors of an integer, excluding the +/// number iteself. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.proper_divisors(4) +/// |> should.equal([1, 2]) +/// +/// maths.proper_divisors(6) +/// |> should.equal([1, 2, 3]) +/// +/// maths.proper_divisors(13) +/// |> should.equal([1]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn proper_divisors(n: Int) -> List(Int) { + let nabs = float.absolute_value(int.to_float(n)) + let assert Ok(sqrt_result) = float.square_root(nabs) + let max = float.round(sqrt_result) + 1 + let divisors = + find_divisors(n, max, [1, n], 2) + |> list.unique() + |> list.sort(int.compare) + + divisors + |> list.take(list.length(divisors) - 1) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted sum of the elements in a list: +/// +/// \\[ +/// \sum_{i=1}^n w_i x_i +/// \\] +/// +/// In the formula, \\(n\\) is the length of the list and \\(x_i \in \mathbb{R}\\) is +/// the value in the input list indexed by \\(i\\), while the \\(w_i \in \mathbb{R}\\) +/// are corresponding positive weights. +/// +///
+/// Example: +/// +/// import gleam/float +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.float_weighted_sum() +/// |> should.equal(Ok(0.0)) +/// +/// [#(1.0, 1.0), #(2.0, 1.0), #(3.0, 1.0)] +/// |> maths.float_weighted_sum() +/// |> should.equal(Ok(6.0)) +/// +/// [#(9.0, 0.5), #(10.0, 0.5), #(10.0, 0.5)] +/// |> maths.float_weighted_sum() +/// |> should.equal(Ok(14.5)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_weighted_sum(arr: List(#(Float, Float))) -> Result(Float, Nil) { + case arr { + [] -> Ok(0.0) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float)) { tuple.1 <. 0.0 }) + case weight_is_negative { + True -> Error(Nil) + False -> { + let weighted_sum = + list.fold(arr, 0.0, fn(acc, a) { a.0 *. a.1 +. acc }) + Ok(weighted_sum) + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted product of the elements in a list: +/// +/// \\[ +/// \prod_{i=1}^n x_i^{w_i} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the list and \\(x_i \in \mathbb{R}\\) is +/// the value in the input list indexed by \\(i\\), while the \\(w_i \in \mathbb{R}\\) +/// are corresponding positive weights. +/// +///
+/// Example: +/// +/// import gleam/float +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.float_weighted_product() +/// |> should.equal(Ok(1.0)) +/// +/// [#(1.0, 1.0), #(2.0, 1.0), #(3.0, 1.0)] +/// |> maths.float_weighted_product() +/// |> should.equal(Ok(6.0)) +/// +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(result) = +/// [#(9.0, 0.5), #(10.0, 0.5), #(10.0, 0.5)] +/// |> maths.float_weighted_product() +/// result +/// |> maths.is_close(30.0, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_weighted_product(arr: List(#(Float, Float))) -> Result(Float, Nil) { + case arr { + [] -> Ok(1.0) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float)) { tuple.1 <. 0.0 }) + case weight_is_negative { + True -> Error(Nil) + False -> { + list.map(arr, fn(a: #(Float, Float)) -> Result(Float, Nil) { + float.power(a.0, a.1) + }) + |> result.all + |> result.map(fn(prods) { + prods + |> list.fold(1.0, fn(acc: Float, a: Float) -> Float { a *. acc }) + }) + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the cumulative sum of the elements in a list: +/// +/// \\[ +/// v_j = \sum_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n +/// \\] +/// +/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative sum of \\(n\\) +/// elements. That is, \\(n\\) is the length of the list and \\(x_i \in \mathbb{R}\\) +/// is the value in the input list indexed by \\(i\\). The value \\(v_j\\) is thus the +/// sum of the \\(1\\) to \\(j\\) first elements in the given list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.float_cumulative_sum() +/// |> should.equal([]) +/// +/// [1.0, 2.0, 3.0] +/// |> maths.float_cumulative_sum() +/// |> should.equal([1.0, 3.0, 6.0]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_cumulative_sum(arr: List(Float)) -> List(Float) { + case arr { + [] -> [] + _ -> list.scan(arr, 0.0, fn(acc, a) { a +. acc }) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the cumulative sum of the elements in a list: +/// +/// \\[ +/// v_j = \sum_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n +/// \\] +/// +/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative sum of \\(n\\) +/// elements. That is, \\(n\\) is the length of the list and \\(x_i \in \mathbb{Z}\\) +/// is the value in the input list indexed by \\(i\\). The value \\(v_j\\) is thus the +/// sum of the \\(1\\) to \\(j\\) first elements in the given list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.int_cumulative_sum() +/// |> should.equal([]) +/// +/// [1, 2, 3] +/// |> maths.int_cumulative_sum() +/// |> should.equal([1, 3, 6]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_cumulative_sum(arr: List(Int)) -> List(Int) { + case arr { + [] -> [] + _ -> list.scan(arr, 0, fn(acc, a) { a + acc }) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the cumulative product of the elements in a list: +/// +/// \\[ +/// v_j = \prod_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n +/// \\] +/// +/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative product of +/// \\(n\\) elements. That is, \\(n\\) is the length of the list and +/// \\(x_i \in \mathbb{R}\\) is the value in the input list indexed by \\(i\\). The +/// value \\(v_j\\) is thus the sum of the \\(1\\) to \\(j\\) first elements in the +/// given list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.float_cumulative_product() +/// |> should.equal([]) +/// +/// [1.0, 2.0, 3.0] +/// |> maths.float_cumulative_product() +/// |> should.equal([1.0, 2.0, 6.0]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_cumulative_product(arr: List(Float)) -> List(Float) { + case arr { + [] -> [] + _ -> list.scan(arr, 1.0, fn(acc, a) { a *. acc }) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the cumulative product of the elements in a list: +/// +/// \\[ +/// v_j = \prod_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n +/// \\] +/// +/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative product of +/// \\(n\\) elements. That is, \\(n\\) is the length of the list and +/// \\(x_i \in \mathbb{Z}\\) is the value in the input list indexed by \\(i\\). The +/// value \\(v_j\\) is thus the product of the \\(1\\) to \\(j\\) first elements in the +/// given list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.int_cumulative_product() +/// |> should.equal([]) +/// +/// [1, 2, 3] +/// |> maths.int_cumulative_product() +/// |> should.equal([1, 2, 6]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_cumulative_product(arr: List(Int)) -> List(Int) { + case arr { + [] -> [] + _ -> list.scan(arr, 1, fn(acc, a) { a * acc }) + } +} + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.degrees_to_radians(360.) +/// |> should.equal(2. *. maths.pi()) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn degrees_to_radians(x: Float) -> Float { + x *. do_pi() /. 180.0 +} + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.radians_to_degrees(0.0) +/// |> should.equal(0.0) +/// +/// maths.radians_to_degrees(2. *. maths.pi()) +/// |> should.equal(360.) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn radians_to_degrees(x: Float) -> Float { + x *. 180.0 /. do_pi() +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Converts polar coordinates \\((r, \theta)\\) to Cartesian coordinates \\((x, y)\\). +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.polar_to_cartesian(1.0, 0.0) +/// |> should.equal(#(1.0, 0.0)) +/// +/// maths.polar_to_cartesian(1.0, float.pi() /. 2.0) +/// |> should.equal(#(0.0, 1.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn polar_to_cartesian(r: Float, theta: Float) -> #(Float, Float) { + // Calculate x and y + let x = r *. cos(theta) + let y = r *. sin(theta) + #(x, y) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Converts Cartesian coordinates \\((x, y)\\) to polar coordinates \\((r, \theta)\\). +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.cartesian_to_polar(1.0, 0.0) +/// |> should.equal((1.0, 0.0)) +/// +/// maths.cartesian_to_polar(0.0, 1.0) +/// |> should.equal((1.0, float.pi() /. 2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cartesian_to_polar(x: Float, y: Float) -> #(Float, Float) { + // Calculate r and theta + let assert Ok(r) = float.power(x *. x +. y *. y, 1.0 /. 2.0) + let theta = atan2(y, x) + #(r, theta) +} + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.acos(1.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.acos(1.1) +/// |> should.be_error() +/// +/// maths.acos(-1.1) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn acos(x: Float) -> Result(Float, Nil) { + case x >=. -1.0 && x <=. 1.0 { + True -> Ok(do_acos(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "acos") +@external(javascript, "../maths.mjs", "acos") +fn do_acos(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.acosh(1.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.acosh(0.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn acosh(x: Float) -> Result(Float, Nil) { + case x >=. 1.0 { + True -> Ok(do_acosh(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "acosh") +@external(javascript, "../maths.mjs", "acosh") +fn do_acosh(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.asin(0.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.asin(1.1) +/// |> should.be_error() +/// +/// maths.asin(-1.1) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn asin(x: Float) -> Result(Float, Nil) { + case x >=. -1.0 && x <=. 1.0 { + True -> Ok(do_asin(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "asin") +@external(javascript, "../maths.mjs", "asin") +fn do_asin(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.asinh(0.0) +/// |> should.equal(0.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn asinh(x: Float) -> Float { + do_asinh(x) +} + +@external(erlang, "math", "asinh") +@external(javascript, "../maths.mjs", "asinh") +fn do_asinh(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.atan(0.0) +/// |> should.equal(0.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn atan(x: Float) -> Float { + do_atan(x) +} + +@external(erlang, "math", "atan") +@external(javascript, "../maths.mjs", "atan") +fn do_atan(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.atan2(0.0, 0.0) +/// |> should.equal(0.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn atan2(y: Float, x: Float) -> Float { + do_atan2(y, x) +} + +@external(erlang, "math", "atan2") +@external(javascript, "../maths.mjs", "atan2") +fn do_atan2(a: Float, b: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.atanh(0.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.atanh(1.0) +/// |> should.be_error() +/// +/// maths.atanh(-1.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn atanh(x: Float) -> Result(Float, Nil) { + case x >. -1.0 && x <. 1.0 { + True -> Ok(do_atanh(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "atanh") +@external(javascript, "../maths.mjs", "atanh") +fn do_atanh(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.cos(0.0) +/// |> should.equal(1.0) +/// +/// maths.cos(maths.pi()) +/// |> should.equal(-1.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cos(x: Float) -> Float { + do_cos(x) +} + +@external(erlang, "math", "cos") +@external(javascript, "../maths.mjs", "cos") +fn do_cos(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.cosh(0.0) +/// |> should.equal(0.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cosh(x: Float) -> Float { + do_cosh(x) +} + +@external(erlang, "math", "cosh") +@external(javascript, "../maths.mjs", "cosh") +fn do_cosh(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.sin(0.0) +/// |> should.equal(0.0) +/// +/// maths.sin(0.5 *. maths.pi()) +/// |> should.equal(1.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn sin(x: Float) -> Float { + do_sin(x) +} + +@external(erlang, "math", "sin") +@external(javascript, "../maths.mjs", "sin") +fn do_sin(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.sinh(0.0) +/// |> should.equal(0.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn sinh(x: Float) -> Float { + do_sinh(x) +} + +@external(erlang, "math", "sinh") +@external(javascript, "../maths.mjs", "sinh") +fn do_sinh(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.tan(0.0) +/// |> should.equal(0.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn tan(x: Float) -> Float { + do_tan(x) +} + +@external(erlang, "math", "tan") +@external(javascript, "../maths.mjs", "tan") +fn do_tan(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example () { +/// maths.tanh(0.0) +/// |> should.equal(0.0) +/// +/// maths.tanh(25.0) +/// |> should.equal(1.0) +/// +/// maths.tanh(-25.0) +/// |> should.equal(-1.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn tanh(x: Float) -> Float { + do_tanh(x) +} + +@external(erlang, "math", "tanh") +@external(javascript, "../maths.mjs", "tanh") +fn do_tanh(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The exponential function: +/// +/// \\[ +/// \forall x \in \(-\infty, \infty\), \\; e^{x} = y \in \(0, +\infty\) +/// \\] +/// +/// where \\(e \approx 2.71828\dots\\) is Eulers' number. +/// +/// Note: If the input value \\(x\\) is too large an overflow error might occur. +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.exponential(0.0) +/// |> should.equal(1.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn exponential(x: Float) -> Float { + do_exponential(x) +} + +@external(erlang, "math", "exp") +@external(javascript, "../maths.mjs", "exponential") +fn do_exponential(a: Float) -> Float + +///
+/// +/// 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_community/maths +/// +/// pub fn example () { +/// maths.natural_logarithm(1.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.natural_logarithm(maths.e()) +/// |> should.equal(Ok(1.0)) +/// +/// maths.natural_logarithm(-1.0) +/// |> should.be_error() +/// } +///
+/// +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn natural_logarithm(x: Float) -> Result(Float, Nil) { + case x >. 0.0 { + True -> Ok(do_natural_logarithm(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "log") +@external(javascript, "../maths.mjs", "logarithm") +fn do_natural_logarithm(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The base \\(b\\) logarithm function (computed through the "change of base" formula): +/// +/// \\[ +/// \forall x \in \(0, \infty\) \textnormal{ and } b > 1, \\; \log_{b}{(x)} = y \in \(-\infty, +\infty\) +/// \\] +/// +/// The function takes a number \\(x\\) in its domain \\(\(0, \infty\)\\) and a base \\(b > 1\\) +/// 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_community/maths +/// +/// pub fn example () { +/// maths.logarithm(1.0, 10.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.logarithm(maths.e(), maths.e()) +/// |> should.equal(Ok(1.0)) +/// +/// maths.logarithm(-1.0, 2.0) +/// |> should.be_error() +/// } +///
+/// +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn logarithm(x: Float, base: Float) -> Result(Float, Nil) { + case x >. 0.0 { + True -> + case base >. 0.0 && base != 1.0 { + False -> Error(Nil) + True -> { + // Apply the "change of base formula" + let assert Ok(numerator) = logarithm_10(x) + let assert Ok(denominator) = logarithm_10(base) + Ok(numerator /. denominator) + } + } + _ -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// 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_community/maths +/// +/// pub fn example () { +/// maths.logarithm_2(1.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.logarithm_2(2.0) +/// |> should.equal(Ok(1.0)) +/// +/// maths.logarithm_2(-1.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn logarithm_2(x: Float) -> Result(Float, Nil) { + case x >. 0.0 { + True -> Ok(do_logarithm_2(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "log2") +@external(javascript, "../maths.mjs", "logarithm_2") +fn do_logarithm_2(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// 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_community/maths +/// +/// pub fn example () { +/// maths.logarithm_10(1.0) +/// |> should.equal(Ok(0.0)) +/// +/// maths.logarithm_10(10.0) +/// |> should.equal(Ok(1.0)) +/// +/// maths.logarithm_10(-1.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn logarithm_10(x: Float) -> Result(Float, Nil) { + case x >. 0.0 { + True -> Ok(do_logarithm_10(x)) + False -> Error(Nil) + } +} + +@external(erlang, "math", "log10") +@external(javascript, "../maths.mjs", "logarithm_10") +fn do_logarithm_10(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The \\(n\\)'th root function: \\(y = \sqrt[n]{x} = x^{\frac{1}{n}}\\). +/// +/// Note that the function is not defined if the input 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_community/maths +/// +/// pub fn example() { +/// maths.nth_root(-1.0, 2) +/// |> should.be_error() +/// +/// maths.nth_root(1.0, 2) +/// |> should.equal(Ok(1.0)) +/// +/// maths.nth_root(27.0, 3) +/// |> should.equal(Ok(3.0)) +/// +/// maths.nth_root(256.0, 4) +/// |> should.equal(Ok(4.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn nth_root(x: Float, n: Int) -> Result(Float, Nil) { + // 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 && n >= 1 { + True -> float.power(x, 1.0 /. int.to_float(n)) + False -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The mathematical constant pi: \\(\pi \approx 3.1415\dots\\) +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn pi() -> Float { + do_pi() +} + +@external(erlang, "math", "pi") +@external(javascript, "../maths.mjs", "pi") +fn do_pi() -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The mathematical (circle) constant tau: \\(\tau = 2 \cdot \pi \approx 6.283\dots\\) +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn tau() -> Float { + 2.0 *. pi() +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The golden ratio: \\(\phi = \frac{1 + \sqrt{5}}{2}\\). +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.golden_ratio() +/// |> should.equal(1.618033988749895) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn golden_ratio() -> Float { + // Calculate the golden ratio: (1 + sqrt(5)) / 2 + let assert Ok(sqrt5) = float.power(5.0, 1.0 /. 2.0) + { 1.0 +. sqrt5 } /. 2.0 +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Euler's number \\(e \approx 2.71828\dots\\). +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// +/// // Test that the constant is approximately equal to 2.7128... +/// maths.e() +/// |> maths.is_close(2.7182818284590452353602, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn e() -> Float { + exponential(1.0) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a float to a specific number of digits (after the decimal place or before +/// if negative). In particular, the input \\(x\\) is rounded to the nearest integer value (at the +/// specified digit) with ties (fractional values of 0.5) being rounded to the nearest even +/// integer. +/// +///
+/// Details +/// +/// The rounding mode rounds \\(12.0654\\) to: +/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) +/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) +/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) +/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) +/// +/// It is also possible to specify a negative number of digits. In that case, the negative +/// number refers to the digits before the decimal point. +/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) +/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) +/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) +/// +///
+/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.round_to_nearest(12.0654, 2) +/// |> should.equal(12.07) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round_to_nearest(x: Float, p: Int) -> Float { + let assert Ok(p) = float.power(10.0, int.to_float(p)) + let xabs = float.absolute_value(x) *. p + let xabs_truncated = truncate_float(xabs) + let remainder = xabs -. xabs_truncated + case remainder { + _ if remainder >. 0.5 -> float_sign(x) *. truncate_float(xabs +. 1.0) /. p + _ if remainder == 0.5 -> { + let assert Ok(is_even) = int.modulo(float.truncate(xabs), 2) + case is_even == 0 { + True -> float_sign(x) *. xabs_truncated /. p + False -> float_sign(x) *. truncate_float(xabs +. 1.0) /. p + } + } + _ -> float_sign(x) *. xabs_truncated /. p + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a float to a specific number of digits (after the decimal place or before +/// if negative). In particular, the input \\(x\\) is rounded to the nearest integer value (at the +/// specified digit) with ties (fractional values of 0.5) being rounded away from zero (C/C++ +/// rounding behaviour). +/// +///
+/// Details +/// +/// The rounding mode rounds \\(12.0654\\) to: +/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) +/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) +/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) +/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) +/// +/// It is also possible to specify a negative number of digits. In that case, the negative +/// number refers to the digits before the decimal point. +/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) +/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) +/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) +/// +///
+/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.round_ties_away(12.0654, 2) +/// |> should.equal(12.07) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round_ties_away(x: Float, p: Int) -> Float { + let assert Ok(p) = float.power(10.0, int.to_float(p)) + let xabs = float.absolute_value(x) *. p + let remainder = xabs -. truncate_float(xabs) + case remainder { + _ if remainder >=. 0.5 -> float_sign(x) *. truncate_float(xabs +. 1.0) /. p + _ -> float_sign(x) *. truncate_float(xabs) /. p + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a float to a specific number of digits (after the decimal place or before +/// if negative). In particular, the input \\(x\\) is rounded to the nearest integer value (at the +/// specified digit) with ties (fractional values of 0.5) being rounded towards \\(+\infty\\) +/// (Java/JavaScript rounding behaviour). +/// +///
+/// Details +/// +/// The rounding mode rounds \\(12.0654\\) to: +/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) +/// - \\(12.1\\) for 1 digits after the decimal point (`digits = 1`) +/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) +/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) +/// +/// It is also possible to specify a negative number of digits. In that case, the negative +/// number refers to the digits before the decimal point. +/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) +/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) +/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) +/// +///
+/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.round_ties_up(12.0654, 2) +/// |> should.equal(12.07) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round_ties_up(x: Float, p: Int) -> Float { + let assert Ok(p) = float.power(10.0, int.to_float(p)) + let xabs = float.absolute_value(x) *. p + let xabs_truncated = truncate_float(xabs) + let remainder = xabs -. xabs_truncated + case remainder { + _ if remainder >=. 0.5 && x >=. 0.0 -> + float_sign(x) *. truncate_float(xabs +. 1.0) /. p + _ -> float_sign(x) *. xabs_truncated /. p + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a float to a specific number of digits (after the decimal place or before +/// if negative). In particular, the input \\(x\\) is rounded to the nearest integer value (at the +/// specified digit) that is less than or equal to the absolute value of the input \\(x\\). This +/// rounding behaviour is similar to behaviour of the Gleam stdlib `truncate` function. +/// +///
+/// Details +/// +/// The rounding mode rounds \\(12.0654\\) to: +/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) +/// - \\(12.0\\) for 1 digit after the decimal point (`digits = 1`) +/// - \\(12.06\\) for 2 digits after the decimal point (`digits = 2`) +/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) +/// +/// It is also possible to specify a negative number of digits. In that case, the negative +/// number refers to the digits before the decimal point. +/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) +/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) +/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) +/// +///
+/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.round_to_zero(12.0654, 2) +/// |> should.equal(12.06) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round_to_zero(x: Float, p: Int) -> Float { + let assert Ok(p) = float.power(10.0, int.to_float(p)) + truncate_float(x *. p) /. p +} + +fn truncate_float(x: Float) -> Float { + do_truncate_float(x) +} + +@external(erlang, "erlang", "trunc") +@external(javascript, "../maths.mjs", "truncate") +fn do_truncate_float(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a float to a specific number of digits (after the decimal place or before +/// if negative). In particular, the input \\(x\\) is rounded to the nearest integer value (at the +/// specified digit) that is less than or equal to the input \\(x\\). This rounding behaviour is +/// similar to behaviour of the Gleam stdlib `floor` function. +/// +///
+/// Details +/// +/// The rounding mode rounds \\(12.0654\\) to: +/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) +/// - \\(12.0\\) for 1 digits after the decimal point (`digits = 1`) +/// - \\(12.06\\) for 2 digits after the decimal point (`digits = 2`) +/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) +/// +/// It is also possible to specify a negative number of digits. In that case, the negative +/// number refers to the digits before the decimal point. +/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) +/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) +/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) +/// +///
+/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.round_down(12.0654, 2) +/// |> should.equal(12.06) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round_down(x: Float, p: Int) -> Float { + let assert Ok(p) = float.power(10.0, int.to_float(p)) + do_floor(x *. p) /. p +} + +@external(erlang, "math", "floor") +@external(javascript, "../maths.mjs", "floor") +fn do_floor(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function rounds a float to a specific number of digits (after the decimal place or before +/// if negative). In particular, the input \\(x\\) is rounded to the nearest integer value (at the +/// specified digit) that is larger than or equal to the input \\(x\\). This rounding behaviour is +/// similar to behaviour of the Gleam stdlib `ceiling` function. +/// +///
+/// Details +/// +/// The rounding mode rounds \\(12.0654\\) to: +/// - \\(13.0\\) for 0 digits after the decimal point (`digits = 0`) +/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) +/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) +/// - \\(12.066\\) for 3 digits after the decimal point (`digits = 3`) +/// +/// It is also possible to specify a negative number of digits. In that case, the negative +/// number refers to the digits before the decimal point. +/// - \\(20.0\\) for 1 digit places before the decimal point (`digit = -1`) +/// - \\(100.0\\) for 2 digits before the decimal point (`digits = -2`) +/// - \\(1000.0\\) for 3 digits before the decimal point (`digits = -3`) +/// +///
+/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.round_up(12.0654, 2) +/// |> should.equal(12.07) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn round_up(x: Float, p: Int) -> Float { + let assert Ok(p) = float.power(10.0, int.to_float(p)) + do_ceiling(x *. p) /. p +} + +@external(erlang, "math", "ceil") +@external(javascript, "../maths.mjs", "ceiling") +fn do_ceiling(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The absolute difference: +/// +/// \\[ +/// \forall x, y \in \mathbb{R}, \\; |x - y| \in \mathbb{R}_{+}. +/// \\] +/// +/// The function takes two inputs \\(x\\) and \\(y\\) and returns a positive float +/// value which is the absolute difference of the inputs. +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.float_absolute_difference(-10.0, 10.0) +/// |> should.equal(20.0) +/// +/// maths.float_absolute_difference(0.0, -2.0) +/// |> should.equal(2.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_absolute_difference(a: Float, b: Float) -> Float { + a -. b + |> float.absolute_value() +} + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.absolute_difference(-10, 10) +/// |> should.equal(20) +/// +/// maths.absolute_difference(0, -2) +/// |> should.equal(2) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_absolute_difference(a: Int, b: Int) -> Int { + a - b + |> int.absolute_value() +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function takes an input \\(x \in \mathbb{R}\\) and returns the sign of +/// the input, indicating whether it is positive (+1.0), negative (-1.0), or +/// zero (0.0). +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_sign(x: Float) -> Float { + do_float_sign(x) +} + +@target(erlang) +fn do_float_sign(x: Float) -> Float { + case x { + _ if x <. 0.0 -> -1.0 + _ if x >. 0.0 -> 1.0 + _ -> 0.0 + } +} + +@target(javascript) +@external(javascript, "../maths.mjs", "sign") +fn do_float_sign(a: Float) -> Float + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function takes an input \\(x \in \mathbb{Z}\\) and returns the sign of +/// the input, indicating whether it is positive (+1), negative (-1), or zero +/// (0). +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_sign(x: Int) -> Int { + do_int_sign(x) +} + +@target(erlang) +fn do_int_sign(x: Int) -> Int { + case x { + _ if x < 0 -> -1 + _ if x > 0 -> 1 + _ -> 0 + } +} + +@target(javascript) +@external(javascript, "../maths.mjs", "sign") +fn do_int_sign(a: Int) -> Int + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function takes two arguments \\(x, y \in \mathbb{R}\\) and returns \\(x\\) +/// such that it has the same sign as \\(y\\). +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_copy_sign(x: Float, y: Float) -> Float { + case float_sign(x) == float_sign(y) { + // x and y have the same sign, just return x + True -> x + // x and y have different signs: + // - x is positive and y is negative, then flip sign of x + // - x is negative and y is positive, then flip sign of x + False -> float_flip_sign(x) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function takes two arguments \\(x, y \in \mathbb{Z}\\) and returns \\(x\\) +/// such that it has the same sign as \\(y\\). +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_copy_sign(x: Int, y: Int) -> Int { + case int_sign(x) == int_sign(y) { + // x and y have the same sign, just return x + True -> x + // x and y have different signs: + // - x is positive and y is negative, then flip sign of x + // - x is negative and y is positive, then flip sign of x + False -> int_flip_sign(x) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function flips the sign of a given input value \\(x \in \mathbb{R}\\). +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn float_flip_sign(x: Float) -> Float { + -1.0 *. x +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function flips the sign of a given input value \\(x \in \mathbb{Z}\\). +/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn int_flip_sign(x: Int) -> Int { + -1 * x +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The minmax function takes two arguments \\(x, y\\) along with a function +/// for comparing \\(x, y\\). The function returns a tuple with the smallest +/// value first and largest second. +/// +///
+/// Example +/// +/// import gleam/float +/// import gleam/int +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.minmax(2.0, 1.5, float.compare) +/// |> should.equal(#(1.5, 2.0)) +/// +/// maths.minmax(1, 2, int.compare) +/// |> should.equal(#(1, 2)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn minmax(x: a, y: a, compare: fn(a, a) -> order.Order) { + case compare(x, y) { + order.Lt -> #(x, y) + order.Eq -> #(x, y) + order.Gt -> #(y, x) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the minimum value of a given list. +/// +///
+/// Example: +/// +/// import gleam/int +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.list_minimum(int.compare) +/// |> should.be_error() +/// +/// [4, 4, 3, 2, 1] +/// |> maths.list_minimum(int.compare) +/// |> should.equal(Ok(1)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+pub fn list_minimum( + arr: List(a), + compare: fn(a, a) -> order.Order, +) -> Result(a, Nil) { + case arr { + [] -> Error(Nil) + [x, ..rest] -> + Ok( + list.fold(rest, x, fn(acc, element) { + case compare(element, acc) { + order.Lt -> element + _ -> acc + } + }), + ) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the maximum value of a given list. +/// +///
+/// Example: +/// +/// import gleam/float +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.list_maximum(float.compare) +/// |> should.be_error() +/// +/// [4.0, 4.0, 3.0, 2.0, 1.0] +/// |> maths.list_maximum(float.compare) +/// |> should.equal(Ok(4.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn list_maximum( + arr: List(a), + compare: fn(a, a) -> order.Order, +) -> Result(a, Nil) { + case arr { + [] -> Error(Nil) + [x, ..rest] -> + Ok( + list.fold(rest, x, fn(acc, element) { + case compare(acc, element) { + order.Lt -> element + _ -> acc + } + }), + ) + } +} + +///
+/// +/// Back to top ↑ +/// +///
+/// +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the indices of the minimum values in a given list. +/// +///
+/// Example: +/// +/// import gleam/float +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.arg_minimum(float.compare) +/// |> should.be_error() +/// +/// [4.0, 4.0, 3.0, 2.0, 1.0] +/// |> maths.arg_minimum(float.compare) +/// |> should.equal(Ok([4])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn arg_minimum( + arr: List(a), + compare: fn(a, a) -> order.Order, +) -> Result(List(Int), Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let assert Ok(min) = list_minimum(arr, compare) + Ok( + list.index_map(arr, fn(element, index) { + case compare(element, min) { + order.Eq -> index + _ -> -1 + } + }) + |> list.filter(fn(index) { + case index { + -1 -> False + _ -> True + } + }), + ) + } + } +} + +///
+/// +/// Back to top ↑ +/// +///
+/// +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns the indices of the maximum values in a given list. +/// +///
+/// Example: +/// +/// import gleam/float +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.arg_maximum(float.compare) +/// |> should.be_error() +/// +/// [4.0, 4.0, 3.0, 2.0, 1.0] +/// |> maths.arg_maximum(float.compare) +/// |> should.equal(Ok([0, 1])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn arg_maximum( + arr: List(a), + compare: fn(a, a) -> order.Order, +) -> Result(List(Int), Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let assert Ok(max) = list_maximum(arr, compare) + Ok( + list.index_map(arr, fn(element, index) { + case compare(element, max) { + order.Eq -> index + _ -> -1 + } + }) + |> list.filter(fn(index) { + case index { + -1 -> False + _ -> True + } + }), + ) + } + } +} + +///
+/// +/// Back to top ↑ +/// +///
+/// +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Returns a tuple consisting of the minimum and maximum values of a given list. +/// +///
+/// Example: +/// +/// import gleam/float +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.extrema(float.compare) +/// |> should.be_error() +/// +/// [4.0, 4.0, 3.0, 2.0, 1.0] +/// |> maths.extrema(float.compare) +/// |> should.equal(Ok(#(1.0, 4.0))) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn extrema( + arr: List(a), + compare: fn(a, a) -> order.Order, +) -> Result(#(a, a), Nil) { + case arr { + [] -> Error(Nil) + [x, ..rest] -> + Ok( + list.fold(rest, #(x, x), fn(acc, element) { + let first = acc.0 + let second = acc.1 + case compare(element, first), compare(second, element) { + order.Lt, order.Lt -> #(element, element) + order.Lt, _ -> #(element, second) + _, order.Lt -> #(first, element) + _, _ -> #(first, second) + } + }), + ) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the number of \\(k\\)-combinations of \\(n\\) elements +/// with repetitions: +/// +/// \\[ +/// C^*(n, k) = \binom{n + k - 1}{k} = \frac{(n + k - 1)!}{k! (n - 1)!} +/// \\] +/// +/// Also known as the "stars and bars" problem in maths. Furthermore, the implementation uses an +/// efficient iterative multiplicative formula for computing the result. +/// +///
+/// Details +/// +/// A \\(k\\)-combination with repetitions is a sequence of \\(k\\) elements selected from +/// \\(n\\) elements where the order of selection does not matter and elements are allowed to +/// repeat. For example, consider selecting 2 elements from a list of 3 elements: `["A", "B", "C"]`. +/// In this case, possible selections are: +/// - `["A", "A"], ["A", "B"], ["A", "C"]` +/// - `["B", "B"], ["B", "C"], ["C", "C"]` +/// +///
+///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.combination_with_repetitions(-1, 1) +/// |> should.be_error() +/// +/// maths.combination_with_repetitions(2, 3) +/// |> should.equal(Ok(4)) +/// +/// maths.combination_with_repetitions(13, 5) +/// |> should.equal(Ok(6188)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn combination_with_repetitions(n: Int, k: Int) -> Result(Int, Nil) { + combination(n + k - 1, k) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the number of \\(k\\)-combinations of \\(n\\) elements +/// without repetitions: +/// +/// \\[ +/// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!} +/// \\] +/// +/// Also known as "\\(n\\) choose \\(k\\)" or the binomial coefficient. +/// +/// +///
+/// Details +/// +/// A \\(k\\)-combination without repetition is a sequence of \\(k\\) elements selected from +/// \\(n\\) elements where the order of selection does not matter and elements are not allowed to +/// repeat. For example, consider selecting 2 elements from a list of 3 elements: +/// `["A", "B", "C"]`. In this case, possible selections are: +/// - `["A", "B"]` +/// - `["A", "C"]` +/// - `["B", "C"]` +/// +///
+///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.combination(-1, 1) +/// |> should.be_error() +/// +/// maths.combination(4, 0) +/// |> should.equal(Ok(1)) +/// +/// maths.combination(4, 4) +/// |> should.equal(Ok(1)) +/// +/// maths.combination(13, 5) +/// |> should.equal(Ok(1287)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn combination(n: Int, k: Int) -> Result(Int, Nil) { + case n, k { + _, _ if n < 0 -> Error(Nil) + _, _ if k < 0 -> Error(Nil) + _, _ if k == 0 || k == n -> Ok(1) + _, _ -> { + let min = case k < n - k { + True -> k + False -> n - k + } + Ok( + list.fold(list.range(1, min), 1, fn(acc, x) { acc * { n + 1 - x } / x }), + ) + } + } +} + +///
+/// +/// 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_community/maths +/// +/// pub fn example() { +/// maths.factorial(-1) +/// |> should.be_error() +/// +/// maths.factorial(0) +/// |> should.equal(Ok(1)) +/// +/// maths.factorial(3) +/// |> should.equal(Ok(6)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn factorial(n: Int) -> Result(Int, Nil) { + case n { + _ if n < 0 -> Error(Nil) + _ -> Ok(do_factorial(n, 1)) + } +} + +fn do_factorial(n: Int, acc: Int) -> Int { + case n { + 0 -> acc + 1 -> acc + _ -> do_factorial(n - 1, acc * n) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the number of \\(k\\)-permutations without +/// repetitions: +/// +/// \\[ +/// P(n, k) = \binom{n}{k} \cdot k! = \frac{n!}{(n - k)!} +/// \\] +/// +/// The implementation uses an efficient iterative multiplicative formula for computing the result. +/// +///
+/// Details +/// +/// A \\(k\\)-permutation without repetitions is a sequence of \\(k\\) elements selected from \ +/// \\(n\\) elements where the order of selection matters and elements are not allowed to repeat. +/// For example, consider selecting 2 elements from a list of 3 elements: `["A", "B", "C"]`. In +/// this case, possible selections are: +/// - `["A", "B"], ["B", "A"]` +/// - `["A", "C"], ["C", "A"]` +/// - `["B", "C"], ["C", "B"]` +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.permutation(-1, 1) +/// |> should.be_error() +/// +/// maths.permutation(4, 0) +/// |> should.equal(Ok(1)) +/// +/// maths.permutation(4, 2) +/// |> should.equal(Ok(12)) +/// +/// maths.permutation(13, 5) +/// |> should.equal(Ok(154_440)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn permutation(n: Int, k: Int) -> Result(Int, Nil) { + case n, k { + _, _ if n < 0 -> Error(Nil) + _, _ if k < 0 -> Error(Nil) + _, _ if k > n -> Ok(0) + _, _ if k == 0 -> Ok(1) + _, _ -> Ok(do_permutation(n, k, 1)) + } +} + +fn do_permutation(n: Int, k: Int, acc: Int) -> Int { + case k { + 0 -> acc + _ -> do_permutation(n - 1, k - 1, acc * n) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A combinatorial function for computing the number of \\(k\\)-permutations with repetitions: +/// +/// \\[ +/// P^*(n, k) = n^k +/// \\] +/// +///
+/// Details +/// +/// A \\(k\\)-permutation with repetitions is a sequence of \\(k\\) elements selected from \\(n\\) +/// elements where the order of selection matters and elements are allowed to repeat. For example, +/// consider selecting 2 elements from a list of 3 elements: `["A", "B", "C"]`. In this case, +/// possible selections are: +/// - `["A", "A"], ["A", "B"], ["A", "C"]` +/// - `["B", "A"], ["B", "B"], ["B", "C"]` +/// - `["C", "A"], ["C", "B"], ["C", "C"]` +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.permutation_with_repetitions(1, -1) +/// |> should.be_error() +/// +/// maths.permutation_with_repetitions(2, 3) +/// |> should.equal(Ok(8)) +/// +/// maths.permutation_with_repetitions(4, 4) +/// |> should.equal(Ok(256)) +/// +/// maths.permutation_with_repetitions(6, 3) +/// |> should.equal(Ok(216)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn permutation_with_repetitions(n: Int, k: Int) -> Result(Int, Nil) { + case n, k { + _, _ if n < 0 -> Error(Nil) + _, _ if k < 0 -> Error(Nil) + _, _ -> { + let n_float = int.to_float(n) + let k_float = int.to_float(k) + // 'n' and 'k' are positive integers, so no errors here... + let assert Ok(result) = float.power(n_float, k_float) + Ok(float.round(result)) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Generates all possible combinations of \\(k\\) elements selected from a given list of size +/// \\(n\\). The function handles the case without repetitions, that is, repeated elements +/// are not treated as distinct. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// // All 2-combinations of [1, 2, 3] without repetition +/// let assert Ok(combinations) = maths.list_combination([1, 2, 3], 2) +/// +/// combinations +/// |> yielder.to_list() +/// |> should.equal([[1, 2], [1, 3], [2, 3]]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn list_combination(arr: List(a), k: Int) -> Result(Yielder(List(a)), Nil) { + case k, list.length(arr) { + _, _ if k < 0 -> Error(Nil) + _, arr_length if k > arr_length -> Error(Nil) + // Special case: When k = n, then the entire list is the only valid combination + _, arr_length if k == arr_length -> { + Ok(yielder.single(arr)) + } + _, _ -> { + Ok(do_list_combination_without_repetitions(yielder.from_list(arr), k, [])) + } + } +} + +fn do_list_combination_without_repetitions( + arr: Yielder(a), + k: Int, + prefix: List(a), +) -> Yielder(List(a)) { + case k { + 0 -> yielder.single(list.reverse(prefix)) + _ -> + case yielder.step(arr) { + yielder.Done -> yielder.empty() + yielder.Next(x, xs) -> { + let with_x = + do_list_combination_without_repetitions(xs, k - 1, [x, ..prefix]) + let without_x = do_list_combination_without_repetitions(xs, k, prefix) + yielder.concat([with_x, without_x]) + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Generates all possible combinations of \\(k\\) elements selected from a given list of size +/// \\(n\\). The function handles the case when the repetition of elements is allowed, that is, +/// repeated elements are treated as distinct. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// // All 2-combinations of [1, 2, 3] with repetition +/// let assert Ok(combinations) = +/// maths.list_combination_with_repetitions([1, 2, 3], 2) +/// +/// combinations +/// |> yielder.to_list() +/// |> should.equal([[1, 1], [1, 2], [1, 3], [2, 2], [2, 3], [3, 3]]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn list_combination_with_repetitions( + arr: List(a), + k: Int, +) -> Result(Yielder(List(a)), Nil) { + case k { + _ if k < 0 -> Error(Nil) + _ -> { + Ok(do_list_combination_with_repetitions(yielder.from_list(arr), k, [])) + } + } +} + +fn do_list_combination_with_repetitions( + arr: Yielder(a), + k: Int, + prefix: List(a), +) -> Yielder(List(a)) { + case k { + 0 -> yielder.single(list.reverse(prefix)) + _ -> + case yielder.step(arr) { + yielder.Done -> yielder.empty() + yielder.Next(x, xs) -> { + let with_x = + do_list_combination_with_repetitions(arr, k - 1, [x, ..prefix]) + let without_x = do_list_combination_with_repetitions(xs, k, prefix) + yielder.concat([with_x, without_x]) + } + } + } +} + +fn remove_first_by_index( + arr: Yielder(#(Int, a)), + index_to_remove: Int, +) -> Yielder(#(Int, a)) { + yielder.flat_map(arr, fn(arg) { + let #(index, element) = arg + case index == index_to_remove { + True -> yielder.empty() + False -> yielder.single(#(index, element)) + } + }) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Generates all possible permutations of \\(k\\) elements selected from a given list of size +/// \\(n\\). The function handles the case without repetitions, that is, repeated elements are +/// not treated as distinct. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// // All 2-permutations of [1, 2] without repetition +/// let assert Ok(permutations) = +/// [1, 2] +/// |> maths.list_permutation(2) +/// permutations +/// |> yielder.to_list() +/// |> should.equal([[1, 2], [2, 1]]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn list_permutation(arr: List(a), k: Int) -> Result(Yielder(List(a)), Nil) { + case k, list.length(arr) { + _, _ if k < 0 -> Error(Nil) + _, arr_length if k > arr_length -> Error(Nil) + _, _ -> { + let indexed_arr = list.index_map(arr, fn(x, i) { #(i, x) }) + Ok(do_list_permutation_without_repetitions( + yielder.from_list(indexed_arr), + k, + )) + } + } +} + +fn do_list_permutation_without_repetitions( + arr: Yielder(#(Int, a)), + k: Int, +) -> Yielder(List(a)) { + case k { + 0 -> yielder.single([]) + _ -> + yielder.flat_map(arr, fn(arg) { + let #(index, element) = arg + let remaining = remove_first_by_index(arr, index) + let permutations = + do_list_permutation_without_repetitions(remaining, k - 1) + yielder.map(permutations, fn(permutation) { [element, ..permutation] }) + }) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Generates all possible permutations of \\(k\\) elements selected from a given list of size +/// \\(n\\). The function handles the case when the repetition of elements is allowed, that is, +/// repeated elements are treated as distinct. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// // All 2-permutations of [1, 2] with repetition +/// let assert Ok(permutations) = +/// [1, 2] +/// |> maths.list_permutation_with_repetitions(2) +/// permutations +/// |> yielder.to_list() +/// |> set.from_list() +/// |> should.equal(set.from_list([[1, 1], [1, 2], [2, 2], [2, 1]])) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn list_permutation_with_repetitions( + arr: List(a), + k: Int, +) -> Result(Yielder(List(a)), Nil) { + case k { + _ if k < 0 -> Error(Nil) + _ -> { + let indexed_arr = list.index_map(arr, fn(x, i) { #(i, x) }) + Ok(do_list_permutation_with_repetitions(indexed_arr, k)) + } + } +} + +fn do_list_permutation_with_repetitions( + arr: List(#(Int, a)), + k: Int, +) -> Yielder(List(a)) { + case k { + 0 -> yielder.single([]) + _ -> + yielder.flat_map(arr |> yielder.from_list, fn(arg) { + let #(_, element) = arg + // Allow the same element (by index) to be reused in future recursive calls + let permutations = do_list_permutation_with_repetitions(arr, k - 1) + // Prepend the current element to each generated permutation + yielder.map(permutations, fn(permutation) { [element, ..permutation] }) + }) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Generate a list containing all combinations of pairs of elements coming from two given sets. +/// +///
+/// Example: +/// +/// import gleam/set +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// set.from_list([]) +/// |> maths.cartesian_product(set.from_list([])) +/// |> should.equal(set.from_list([])) +/// +/// set.from_list([1.0, 10.0]) +/// |> maths.cartesian_product(set.from_list([1.0, 2.0])) +/// |> should.equal( +/// set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]), +/// ) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cartesian_product(xset: set.Set(a), yset: set.Set(b)) -> set.Set(#(a, b)) { + set.fold(xset, set.new(), fn(accumulator0: set.Set(#(a, b)), member0: a) { + set.fold(yset, accumulator0, fn(accumulator1: set.Set(#(a, b)), member1: b) { + set.insert(accumulator1, #(member0, member1)) + }) + }) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the \\(p\\)-norm of a list (representing a vector): +/// +/// \\[ +/// \left( \sum_{i=1}^n \left|x_{i}\right|^{p} \right)^{\frac{1}{p}} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the list and \\(x_i\\) is the value in +/// the input list indexed by \\(i\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// [1.0, 1.0, 1.0] +/// |> maths.norm(1.0) +/// |> should.equal(Ok(3.0)) +/// +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(result) = +/// [1.0, 2.0, 3.0] +/// |> maths.norm(2.0) +/// result +/// |> maths.is_close(3.7416573867739413, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn norm(arr: List(Float), p: Float) -> Result(Float, Nil) { + case arr { + [] -> Ok(0.0) + _ -> { + let aggregate = + list.fold(arr, 0.0, fn(accumulator, element) { + let assert Ok(result) = float.power(float.absolute_value(element), p) + result +. accumulator + }) + float.power(aggregate, 1.0 /. p) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted \\(p\\)-norm of a list (representing a vector): +/// +/// \\[ +/// \left( \sum_{i=1}^n w_{i} \left|x_{i}\right|^{p} \right)^{\frac{1}{p}} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the list and \\(x_i\\) is the value in +/// the input list indexed by \\(i\\), while \\(w_i \in \mathbb{R}_{+}\\) is +/// a corresponding positive weight. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// [#(1.0, 0.5), #(1.0, 0.5), #(1.0, 0.5)] +/// |> maths.norm_with_weights(1.0) +/// |> should.equal(Ok(1.5)) +/// +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(result) = +/// [#(1.0, 0.5), #(2.0, 0.5), #(3.0, 0.5)] +/// |> maths.norm_with_weights(2.0) +/// result +/// |> maths.is_close(2.6457513110645907, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn norm_with_weights( + arr: List(#(Float, Float)), + p: Float, +) -> Result(Float, Nil) { + case arr { + [] -> Ok(0.0) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float)) { tuple.1 <. 0.0 }) + case weight_is_negative { + False -> { + let aggregate = + list.fold(arr, 0.0, fn(accumulator, tuple) { + let assert Ok(result) = + float.power(float.absolute_value(tuple.0), p) + tuple.1 *. result +. accumulator + }) + float.power(aggregate, 1.0 /. p) + } + True -> Error(Nil) + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the Manhattan distance between two lists (representing +/// vectors): +/// +/// \\[ +/// \sum_{i=1}^n \left|x_i - y_i \right| +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.manhattan_distance([#(1.0, 2.0), #(2.0, 3.0)]) +/// |> should.equal(Ok(2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn manhattan_distance(arr: List(#(Float, Float))) -> Result(Float, Nil) { + minkowski_distance(arr, 1.0) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted Manhattan distance between two lists (representing +/// vectors): +/// +/// \\[ +/// \sum_{i=1}^n w_{i} \left|x_i - y_i \right| +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\), while the +/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.manhattan_distance_with_weights([#(1.0, 2.0, 0.5), #(2.0, 3.0, 1.0)]) +/// |> should.equal(Ok(1.5)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn manhattan_distance_with_weights( + arr: List(#(Float, Float, Float)), +) -> Result(Float, Nil) { + minkowski_distance_with_weights(arr, 1.0) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the Minkowski distance between two lists (representing +/// vectors): +/// +/// \\[ +/// \left( \sum_{i=1}^n w_{i} \left|x_i - y_i \right|^{p} \right)^{\frac{1}{p}} +/// \\] +/// +/// In the formula, \\(p >= 1\\) is the order, \\(n\\) is the length of the two lists +/// and \\(x_i, y_i\\) are the values in the respective input lists indexed by \\(i\\). +/// +/// The Minkowski distance is a generalization of the Euclidean distance +/// (\\(p=2\\)) and the Manhattan distance (\\(p = 1\\)). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// +/// let assert Ok(result) = +/// maths.minkowski_distance([#(1.0, 2.0), #(3.0, 4.0), #(5.0, 6.0)], 4.0) +/// result +/// |> maths.is_close(1.3160740129524924, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn minkowski_distance( + arr: List(#(Float, Float)), + p: Float, +) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + case p <. 1.0 { + True -> Error(Nil) + False -> { + let differences = + list.map(arr, fn(tuple: #(Float, Float)) -> Float { + tuple.0 -. tuple.1 + }) + norm(differences, p) + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted Minkowski distance between two lists (representing +/// vectors): +/// +/// \\[ +/// \left( \sum_{i=1}^n w_{i} \left|x_i - y_i \right|^{p} \right)^{\frac{1}{p}} +/// \\] +/// +/// In the formula, \\(p >= 1\\) is the order, \\(n\\) is the length of the two lists +/// and \\(x_i, y_i\\) are the values in the respective input lists indexed by \\(i\\). +/// The \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +/// The Minkowski distance is a generalization of the Euclidean distance +/// (\\(p=2\\)) and the Manhattan distance (\\(p = 1\\)). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// +/// let assert Ok(result) = +/// maths.minkowski_distance_with_weights( +/// [#(1.0, 2.0, 0.5), #(3.0, 4.0, 1.0), #(5.0, 6.0, 1.0)], +/// 4.0, +/// ) +/// result +/// |> maths.is_close(1.2574334296829355, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn minkowski_distance_with_weights( + arr: List(#(Float, Float, Float)), + p: Float, +) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float, Float)) { tuple.2 <. 0.0 }) + + case p <. 1.0, weight_is_negative { + False, False -> { + let differences = + list.map(arr, fn(tuple: #(Float, Float, Float)) -> #(Float, Float) { + #(tuple.0 -. tuple.1, tuple.2) + }) + norm_with_weights(differences, p) + } + _, _ -> Error(Nil) + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the Euclidean distance between two lists (representing +/// vectors): +/// +/// \\[ +/// \left( \sum_{i=1}^n \left|x_i - y_i \right|^{2} \right)^{\frac{1}{2}} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// +/// let assert Ok(result) = maths.euclidean_distance([#(1.0, 2.0), #(3.0, 4.0)]) +/// result +/// |> maths.is_close(1.4142135623730951, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn euclidean_distance(arr: List(#(Float, Float))) -> Result(Float, Nil) { + minkowski_distance(arr, 2.0) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted Euclidean distance between two lists (representing +/// vectors): +/// +/// \\[ +/// \left( \sum_{i=1}^n w_{i} \left|x_i - y_i \right|^{2} \right)^{\frac{1}{2}} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\), while the +/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// +/// let assert Ok(result) = +/// maths.euclidean_distance_with_weights([#(1.0, 2.0, 0.5), #(3.0, 4.0, 1.0)]) +/// result +/// |> maths.is_close(1.224744871391589, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn euclidean_distance_with_weights( + arr: List(#(Float, Float, Float)), +) -> Result(Float, Nil) { + minkowski_distance_with_weights(arr, 2.0) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the Chebyshev distance between two lists (representing vectors): +/// +/// \\[ +/// \text{max}_{i=1}^n \left|x_i - y_i \right| +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.chebyshev_distance([#(-5.0, -1.0), #(-10.0, -12.0), #(-3.0, -3.0)]) +/// |> should.equal(Ok(4.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn chebyshev_distance(arr: List(#(Float, Float))) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + list.map(arr, fn(tuple) { float.absolute_value({ tuple.0 -. tuple.1 }) }) + |> list_maximum(float.compare) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted Chebyshev distance between two lists (representing vectors): +/// +/// \\[ +/// \text{max}_{i=1}^n w_i \left|x_i - y_i \right| +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\), while the +/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.chebyshev_distance_with_weights([ +/// #(-5.0, -1.0, 0.5), +/// #(-10.0, -12.0, 1.0), +/// #(-3.0, -3.0, 1.0), +/// ]) +/// |> should.equal(Ok(2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn chebyshev_distance_with_weights( + arr: List(#(Float, Float, Float)), +) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float, Float)) { tuple.2 <. 0.0 }) + + case weight_is_negative { + True -> Error(Nil) + False -> { + list.map(arr, fn(tuple) { + float.absolute_value({ tuple.0 -. tuple.1 }) *. tuple.2 + }) + |> list_maximum(float.compare) + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the arithmetic mean of the elements in a list: +/// +/// \\[ +/// \bar{x} = \frac{1}{n}\sum_{i=1}^n x_i +/// \\] +/// +/// In the formula, \\(n\\) is the sample size (the length of the list) and \\(x_i\\) +/// is the sample point in the input list indexed by \\(i\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.mean() +/// |> should.be_error() +/// +/// [1.0, 2.0, 3.0] +/// |> maths.mean() +/// |> should.equal(Ok(2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn mean(arr: List(Float)) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> Ok(float.sum(arr) /. int.to_float(list.length(arr))) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the median of the elements in a list. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// [] +/// |> maths.median() +/// |> should.be_error() +/// +/// [1.0, 2.0, 3.0] +/// |> maths.median() +/// |> should.equal(Ok(2.0)) +/// +/// [1.0, 2.0, 3.0, 4.0] +/// |> maths.median() +/// |> should.equal(Ok(2.5)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn median(arr: List(Float)) -> Result(Float, Nil) { + use <- bool.guard(list.is_empty(arr), Error(Nil)) + let length = list.length(arr) + let mid = length / 2 + + case length % 2 == 0 { + True -> do_median(arr, mid, True, 0) + False -> do_median(arr, mid, False, 0) + } +} + +fn do_median( + xs: List(Float), + mid: Int, + mean: Bool, + index: Int, +) -> Result(Float, Nil) { + use <- bool.guard(index > mid, Error(Nil)) + let mid_less_one = mid - 1 + + case xs { + [x, ..] if !mean && index == mid -> Ok(x) + [x, y, ..] if mean && index == mid_less_one -> Ok({ x +. y } /. 2.0) + [_, ..rest] -> do_median(rest, mid, mean, index + 1) + [] -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the sample variance of the elements in a list: +/// +/// \\[ +/// s^{2} = \frac{1}{n - d} \sum_{i=1}^{n}(x_i - \bar{x}) +/// \\] +/// +/// In the formula, \\(n\\) is the sample size (the length of the list) and \\(x_i\\) +/// is the sample point in the input list indexed by \\(i\\). +/// Furthermore, \\(\bar{x}\\) is the sample mean and \\(d\\) is the "Delta +/// Degrees of Freedom". It is typically set to \\(d = 1\\), which gives an unbiased estimate. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// // Degrees of freedom +/// let ddof = 1 +/// +/// [] +/// |> maths.variance(ddof) +/// |> should.be_error() +/// +/// [1.0, 2.0, 3.0] +/// |> maths.variance(ddof) +/// |> should.equal(Ok(1.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn variance(arr: List(Float), ddof: Int) -> Result(Float, Nil) { + case arr, ddof { + [], _ -> Error(Nil) + _, _ if ddof < 0 -> Error(Nil) + _, _ -> { + let assert Ok(mean) = mean(arr) + Ok( + list.map(arr, fn(a: Float) -> Float { + let assert Ok(result) = float.power(a -. mean, 2.0) + result + }) + |> float.sum() + |> fn(a: Float) -> Float { + a /. { int.to_float(list.length(arr)) -. int.to_float(ddof) } + }, + ) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the sample standard deviation of the elements in a list: +/// \\[ +/// s = \left(\frac{1}{n - d} \sum_{i=1}^{n}(x_i - \bar{x})\right)^{\frac{1}{2}} +/// \\] +/// +/// In the formula, \\(n\\) is the sample size (the length of the list) and \\(x_i\\) +/// is the sample point in the input list indexed by \\(i\\). +/// Furthermore, \\(\bar{x}\\) is the sample mean and \\(d\\) is the "Delta +/// Degrees of Freedom", and is typically set to \\(d = 1\\), which gives an unbiased estimate. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// // Degrees of freedom +/// let ddof = 1 +/// +/// [] +/// |> maths.standard_deviation(ddof) +/// |> should.be_error() +/// +/// [1.0, 2.0, 3.0] +/// |> maths.standard_deviation(ddof) +/// |> should.equal(Ok(1.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn standard_deviation(arr: List(Float), ddof: Int) -> Result(Float, Nil) { + case arr, ddof { + [], _ -> Error(Nil) + _, _ if ddof < 0 -> Error(Nil) + _, _ -> { + let assert Ok(variance) = variance(arr, ddof) + float.square_root(variance) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The Jaccard index measures similarity between two sets of elements. Mathematically, the +/// Jaccard index is defined as: +/// +/// \\[ +/// \frac{|X \cap Y|}{|X \cup Y|} \\; \in \\; \left[0, 1\right] +/// \\] +/// +/// where: +/// +/// - \\(X\\) and \\(Y\\) are two sets being compared +/// - \\(|X \cap Y|\\) represents the size of the intersection of the two sets +/// - \\(|X \cup Y|\\) denotes the size of the union of the two sets +/// +/// The value of the Jaccard index ranges from 0 to 1, where 0 indicates that the +/// two sets share no elements and 1 indicates that the sets are identical. The +/// Jaccard index is a special case of the [Tversky index](#tversky_index) (with +/// \\(\alpha=\beta=1\\)). +/// +///
+/// Example: +/// +/// import gleam/set +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let xset = set.from_list(["cat", "dog", "hippo", "monkey"]) +/// let yset = set.from_list(["monkey", "rhino", "ostrich", "salmon"]) +/// maths.jaccard_index(xset, yset) +/// |> should.equal(1.0 /. 7.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn jaccard_index(xset: set.Set(a), yset: set.Set(a)) -> Float { + let assert Ok(result) = tversky_index(xset, yset, 1.0, 1.0) + result +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The Sørensen-Dice coefficient measures the similarity between two sets of elements. +/// Mathematically, the coefficient is defined as: +/// +/// \\[ +/// \frac{2 |X \cap Y|}{|X| + |Y|} \\; \in \\; \left[0, 1\right] +/// \\] +/// +/// where: +/// +/// - \\(X\\) and \\(Y\\) are two sets being compared +/// - \\(|X \cap Y|\\) is the size of the intersection of the two sets (i.e., the +/// number of elements common to both sets) +/// - \\(|X|\\) and \\(|Y|\\) are the sizes of the sets \\(X\\) and \\(Y\\), respectively +/// +/// The coefficient ranges from 0 to 1, where 0 indicates no similarity (the sets +/// share no elements) and 1 indicates perfect similarity (the sets are identical). +/// The higher the coefficient, the greater the similarity between the two sets. +/// The Sørensen-Dice coefficient is a special case of the +/// [Tversky index](#tversky_index) (with \\(\alpha=\beta=0.5\\)). +/// +///
+/// Example: +/// +/// import gleam/set +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let xset = set.from_list(["cat", "dog", "hippo", "monkey"]) +/// let yset = set.from_list(["monkey", "rhino", "ostrich", "salmon", "spider"]) +/// maths.sorensen_dice_coefficient(xset, yset) +/// |> should.equal(2.0 *. 1.0 /. { 4.0 +. 5.0 }) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn sorensen_dice_coefficient(xset: set.Set(a), yset: set.Set(a)) -> Float { + let assert Ok(result) = tversky_index(xset, yset, 0.5, 0.5) + result +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The Tversky index is a generalization of the Jaccard index and Sørensen-Dice +/// coefficient, which adds flexibility in measuring similarity between two sets using two +/// parameters, \\(\alpha\\) and \\(\beta\\). These parameters allow for asymmetric +/// similarity measures between sets. The Tversky index is defined as: +/// +/// \\[ +/// \frac{|X \cap Y|}{|X \cap Y| + \alpha|X - Y| + \beta|Y - X|} +/// \\] +/// +/// where: +/// +/// - \\(X\\) and \\(Y\\) are the sets being compared +/// - \\(|X - Y|\\) and \\(|Y - X|\\) are the sizes of the relative complements of +/// \\(Y\\) in \\(X\\) and \\(X\\) in \\(Y\\), respectively, +/// - \\(\alpha\\) and \\(\beta\\) are parameters that weight the relative importance +/// of the elements unique to \\(X\\) and \\(Y\\) +/// +/// The Tversky index reduces to the Jaccard index when \\(\alpha = \beta = 1\\) and +/// to the Sørensen-Dice coefficient when \\(\alpha = \beta = 0.5\\). In general, the +/// Tversky index can take on any non-negative value, including 0. The index equals +/// 0 when there is no intersection between the two sets, indicating no similarity. +/// The Tversky index does not have a strict upper limit of 1 when \\(\alpha \neq \beta\\). +/// +///
+/// Example: +/// +/// import gleam/set +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let yset = set.from_list(["cat", "dog", "hippo", "monkey"]) +/// let xset = set.from_list(["monkey", "rhino", "ostrich", "salmon"]) +/// // Test Jaccard index (alpha = beta = 1) +/// maths.tversky_index(xset, yset, 1.0, 1.0) +/// |> should.equal(Ok(1.0 /. 7.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn tversky_index( + xset: set.Set(a), + yset: set.Set(a), + alpha: Float, + beta: Float, +) -> Result(Float, Nil) { + case alpha >=. 0.0, beta >=. 0.0 { + True, True -> { + let intersection = + set.intersection(xset, yset) + |> set.size() + |> int.to_float() + let difference1 = + set.difference(xset, yset) + |> set.size() + |> int.to_float() + let difference2 = + set.difference(yset, xset) + |> set.size() + |> int.to_float() + Ok( + intersection + /. { intersection +. alpha *. difference1 +. beta *. difference2 }, + ) + } + _, _ -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The Overlap coefficient, also known as the Szymkiewicz–Simpson coefficient, is +/// a measure of similarity between two sets that focuses on the size of the +/// intersection relative to the smaller of the two sets. It is defined +/// mathematically as: +/// +/// \\[ +/// \frac{|X \cap Y|}{\min(|X|, |Y|)} \\; \in \\; \left[0, 1\right] +/// \\] +/// +/// where: +/// +/// - \\(X\\) and \\(Y\\) are the sets being compared +/// - \\(|X \cap Y|\\) is the size of the intersection of the sets +/// - \\(\min(|X|, |Y|)\\) is the size of the smaller set among \\(X\\) and \\(Y\\) +/// +/// The coefficient ranges from 0 to 1, where 0 indicates no overlap and 1 +/// indicates that the smaller set is a suyset of the larger set. This +/// measure is especially useful in situations where the similarity in terms +/// of the proportion of overlap is more relevant than the difference in sizes +/// between the two sets. +/// +///
+/// Example: +/// +/// import gleam/set +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let set_a = set.from_list(["horse", "dog", "hippo", "monkey", "bird"]) +/// let set_b = set.from_list(["monkey", "bird", "ostrich", "salmon"]) +/// maths.overlap_coefficient(set_a, set_b) +/// |> should.equal(2.0 /. 4.0) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn overlap_coefficient(xset: set.Set(a), yset: set.Set(a)) -> Float { + let intersection = + set.intersection(xset, yset) + |> set.size() + |> int.to_float() + let minsize = + int.min(set.size(xset), set.size(yset)) + |> int.to_float() + intersection /. minsize +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the cosine similarity between two lists (representing +/// vectors): +/// +/// \\[ +/// \frac{\sum_{i=1}^n x_i \cdot y_i} +/// {\left(\sum_{i=1}^n x_i^2\right)^{\frac{1}{2}} +/// \cdot +/// \left(\sum_{i=1}^n y_i^2\right)^{\frac{1}{2}}} +/// \\; \in \\; \left[-1, 1\right] +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i\\), \\(y_i\\) are +/// the values in the respective input lists indexed by \\(i\\). +/// +/// The cosine similarity provides a value between -1 and 1, where 1 means the +/// vectors are in the same direction, -1 means they are in exactly opposite +/// directions, and 0 indicates orthogonality. +/// +///
+/// Example: +/// +/// import gleam/option +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// // Two orthogonal vectors +/// maths.cosine_similarity([#(-1.0, 1.0), #(1.0, 1.0), #(0.0, -1.0)]) +/// |> should.equal(Ok(0.0)) +/// +/// // Two identical (parallel) vectors +/// maths.cosine_similarity([#(1.0, 1.0), #(2.0, 2.0), #(3.0, 3.0)]) +/// |> should.equal(Ok(1.0)) +/// +/// // Two parallel, but oppositely oriented vectors +/// maths.cosine_similarity([#(-1.0, 1.0), #(-2.0, 2.0), #(-3.0, 3.0)]) +/// |> should.equal(Ok(-1.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cosine_similarity(arr: List(#(Float, Float))) -> Result(Float, Nil) { + let numerator = + arr + |> list.fold(0.0, fn(accumulator, tuple) { + accumulator +. tuple.0 *. tuple.1 + }) + + let xarr = arr |> list.map(fn(tuple: #(Float, Float)) { tuple.0 }) + let yarr = arr |> list.map(fn(tuple: #(Float, Float)) { tuple.1 }) + + let assert Ok(xarr_norm) = norm(xarr, 2.0) + let assert Ok(yarr_norm) = norm(yarr, 2.0) + + let denominator = { + xarr_norm *. yarr_norm + } + Ok(numerator /. denominator) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted cosine similarity between two lists (representing +/// vectors): +/// +/// \\[ +/// \frac{\sum_{i=1}^n w_{i} \cdot x_i \cdot y_i} +/// {\left(\sum_{i=1}^n w_{i} \cdot x_i^2\right)^{\frac{1}{2}} +/// \cdot +/// \left(\sum_{i=1}^n w_{i} \cdot y_i^2\right)^{\frac{1}{2}}} +/// \\; \in \\; \left[-1, 1\right] +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists and \\(x_i\\), \\(y_i\\) are +/// the values in the respective input lists indexed by \\(i\\), while the +/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +/// The cosine similarity provides a value between -1 and 1, where 1 means the +/// vectors are in the same direction, -1 means they are in exactly opposite +/// directions, and 0 indicates orthogonality. +/// +///
+/// Example: +/// +/// import gleam/option +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// +/// let assert Ok(result) = +/// maths.cosine_similarity_with_weights([ +/// #(1.0, 1.0, 2.0), +/// #(2.0, 2.0, 3.0), +/// #(3.0, 3.0, 4.0), +/// ]) +/// result +/// |> maths.is_close(1.0, 0.0, tolerance) +/// |> should.be_true() +/// +/// let assert Ok(result) = +/// maths.cosine_similarity_with_weights([ +/// #(-1.0, 1.0, 1.0), +/// #(-2.0, 2.0, 0.5), +/// #(-3.0, 3.0, 0.33), +/// ]) +/// result +/// |> maths.is_close(-1.0, 0.0, tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn cosine_similarity_with_weights( + arr: List(#(Float, Float, Float)), +) -> Result(Float, Nil) { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float, Float)) { tuple.2 <. 0.0 }) + + case weight_is_negative { + False -> { + let numerator = + arr + |> list.fold(0.0, fn(accumulator, tuple) { + accumulator +. tuple.0 *. tuple.1 *. tuple.2 + }) + + let xarr = + arr + |> list.map(fn(tuple: #(Float, Float, Float)) { #(tuple.0, tuple.2) }) + let yarr = + arr + |> list.map(fn(tuple: #(Float, Float, Float)) { #(tuple.1, tuple.2) }) + + let assert Ok(xarr_norm) = norm_with_weights(xarr, 2.0) + let assert Ok(yarr_norm) = norm_with_weights(yarr, 2.0) + let denominator = { + xarr_norm *. yarr_norm + } + Ok(numerator /. denominator) + } + True -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the Canberra distance between two lists: +/// +/// \\[ +/// \sum_{i=1}^n \frac{\left| x_i - y_i \right|} +/// {\left| x_i \right| + \left| y_i \right|} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists, and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.canberra_distance([]) +/// |> should.be_error() +/// +/// maths.canberra_distance([#(1.0, -2.0), #(2.0, -1.0)]) +/// |> should.equal(Ok(2.0)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn canberra_distance(arr: List(#(Float, Float))) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + Ok( + list.fold(arr, 0.0, fn(accumulator, tuple) { + let numerator = float.absolute_value({ tuple.0 -. tuple.1 }) + let denominator = { + float.absolute_value(tuple.0) +. float.absolute_value(tuple.1) + } + accumulator +. numerator /. denominator + }), + ) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted Canberra distance between two lists: +/// +/// \\[ +/// \sum_{i=1}^n w_{i}\frac{\left| x_i - y_i \right|} +/// {\left| x_i \right| + \left| y_i \right|} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists, and \\(x_i, y_i\\) are the +/// values in the respective input lists indexed by \\(i\\), while the +/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.canberra_distance([]) +/// |> should.be_error() +/// +/// maths.canberra_distance_with_weights([#(1.0, -2.0, 0.5), #(2.0, -1.0, 1.0)]) +/// |> should.equal(Ok(1.5)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn canberra_distance_with_weights( + arr: List(#(Float, Float, Float)), +) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float, Float)) { tuple.2 <. 0.0 }) + + case weight_is_negative { + True -> Error(Nil) + False -> { + Ok( + list.fold(arr, 0.0, fn(accumulator, tuple) { + let numerator = float.absolute_value({ tuple.0 -. tuple.1 }) + let denominator = { + float.absolute_value(tuple.0) +. float.absolute_value(tuple.1) + } + accumulator +. tuple.2 *. numerator /. denominator + }), + ) + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the Bray-Curtis distance between two lists: +/// +/// \\[ +/// \frac{\sum_{i=1}^n \left| x_i - y_i \right|} +/// {\sum_{i=1}^n \left| x_i + y_i \right|} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists, and \\(x_i, y_i\\) are the values +/// in the respective input lists indexed by \\(i\\). +/// +/// The Bray-Curtis distance is in the range \\([0, 1]\\) if all entries \\(x_i, y_i\\) are +/// positive. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.canberra_distance([]) +/// |> should.be_error() +/// +/// maths.canberra_distance_with_weights([#(1.0, -2.0, 0.5), #(2.0, -1.0, 1.0)]) +/// |> should.equal(Ok(1.5)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +/// +pub fn braycurtis_distance(arr: List(#(Float, Float))) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let numerator = + list.fold(arr, 0.0, fn(accumulator, tuple) { + accumulator +. float.absolute_value({ tuple.0 -. tuple.1 }) + }) + + let denominator = + list.fold(arr, 0.0, fn(accumulator, tuple) { + accumulator +. float.absolute_value({ tuple.0 +. tuple.1 }) + }) + + Ok({ numerator /. denominator }) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Calculate the weighted Bray-Curtis distance between two lists: +/// +/// \\[ +/// \frac{\sum_{i=1}^n w_{i} \left| x_i - y_i \right|} +/// {\sum_{i=1}^n w_{i}\left| x_i + y_i \right|} +/// \\] +/// +/// In the formula, \\(n\\) is the length of the two lists, and \\(x_i, y_i\\) are the values +/// in the respective input lists indexed by \\(i\\), while the +/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights. +/// +/// The Bray-Curtis distance is in the range \\([0, 1]\\) if all entries \\(x_i, y_i\\) are +/// positive and \\(w_i = 1.0\\;\forall i=1...n\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.braycurtis_distance_with_weights([]) +/// |> should.be_error() +/// +/// maths.braycurtis_distance_with_weights([#(1.0, 3.0, 0.5), #(2.0, 4.0, 1.0)]) +/// |> should.equal(Ok(0.375)) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn braycurtis_distance_with_weights( + arr: List(#(Float, Float, Float)), +) -> Result(Float, Nil) { + case arr { + [] -> Error(Nil) + _ -> { + let weight_is_negative = + list.any(arr, fn(tuple: #(Float, Float, Float)) { tuple.2 <. 0.0 }) + + case weight_is_negative { + True -> Error(Nil) + False -> { + let numerator = + list.fold(arr, 0.0, fn(accumulator, tuple) { + accumulator + +. tuple.2 + *. float.absolute_value({ tuple.0 -. tuple.1 }) + }) + + let denominator = + list.fold(arr, 0.0, fn(accumulator, tuple) { + accumulator + +. tuple.2 + *. float.absolute_value({ tuple.0 +. tuple.1 }) + }) + + Ok({ numerator /. denominator }) + } + } + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Determine if a given value \\(x\\) is close to or equivalent to a reference value +/// \\(y\\) 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: +/// +/// \\[ +/// \|x - y\| \leq (a_{tol} + r_{tol} \cdot \|y\|) +/// \\] +/// +/// `True` is returned if the statement holds, otherwise `False` is returned. +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let value = 99. +/// let reference_value = 100. +/// // We set 'absolute_tolerance' and 'relative_tolerance' such that the values are +/// // equivalent if 'value' is within 1 percent of 'reference_value' +/- 0.1 +/// let relative_tolerance = 0.01 +/// let absolute_tolerance = 0.10 +/// maths.is_close(value, reference_value, relative_tolerance, absolute_tolerance) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_close(x: Float, y: Float, rtol: Float, atol: Float) -> Bool { + let x = float_absolute_difference(x, y) + let y = atol +. rtol *. float.absolute_value(y) + case x <=. y { + True -> True + False -> False + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Determine if each value \\(x_i\\) is close to or equivalent to its corresponding reference value +/// \\(y_i\\), in a list of value pairs \\((x_i, y_i)\\), based on supplied relative \\(r_{tol}\\) +/// and absolute \\(a_{tol}\\) tolerance values. The equivalence of each pair \\((x_i, y_i)\\) is +/// determined by the equation: +/// +/// \\[ +/// \|x_i - y_i\| \leq (a_{tol} + r_{tol} \cdot \|y_i\|), \\; \forall i=1,...,n. +/// \\] +/// +/// A list of `Bool` values is returned, where each entry indicates if the corresponding pair +/// satisfies the condition. If all conditions are satisfied, the list will contain only `True` +/// values. +/// +///
+/// Example: +/// +/// import gleam/list +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let value = 99.0 +/// let reference_value = 100.0 +/// let xarr = list.repeat(value, 42) +/// let yarr = list.repeat(reference_value, 42) +/// let arr = list.zip(xarr, yarr) +/// // We set 'absolute_tolerance' and 'relative_tolerance' such that +/// // the values are equivalent if 'value' is within 1 percent of +/// // 'reference_value' +/- 0.1 +/// let relative_tolerance = 0.01 +/// let absolute_tolerance = 0.1 +/// let assert Ok(result) = +/// maths.all_close(arr, relative_tolerance, absolute_tolerance) +/// result +/// |> list.all(fn(x) { x == True }) +/// |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn all_close( + arr: List(#(Float, Float)), + rtol: Float, + atol: Float, +) -> Result(List(Bool), Nil) { + Ok(list.map(arr, fn(tuple) { is_close(tuple.0, tuple.1, rtol, atol) })) +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Determine if a given value \\(x\\) is fractional, i.e., if it contains a fractional part: +/// +/// \\[ +/// x - \lfloor x \rfloor > 0 +/// \\] +/// +/// `True` is returned if the given value is fractional (i.e., it has a non-zero decimal part), +/// otherwise `False` is returned. +/// +///
+/// Example +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// maths.is_fractional(0.3333) +/// |> should.equal(True) +/// +/// maths.is_fractional(1.0) +/// |> should.equal(False) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_fractional(x: Float) -> Bool { + do_ceiling(x) -. x >. 0.0 +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function that determines if a given integer value \\(x \in \mathbb{Z}\\) is a power of +/// another integer value \\(y \in \mathbb{Z}\\), i.e., the function evaluates whether \\(x\\) can +/// be expressed as \\(y^n\\) for some integer \\(n \geq 0\\), by computing the base-\\(y\\) +/// logarithm of \\(x\\): +/// +/// \\[ +/// n = \log_y(x) +/// \\] +/// +/// If \\(n\\) is an integer (i.e., it has no fractional part), then \\(x\\) is a power of \\(y\\) +/// and `True` is returned. Otherwise `False` is returned. +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// // Check if 4 is a power of 2 (it is) +/// maths.is_power(4, 2) +/// |> should.equal(True) +/// +/// // Check if 5 is a power of 2 (it is not) +/// maths.is_power(5, 2) +/// |> should.equal(False) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_power(x: Int, y: Int) -> Bool { + let assert Ok(value) = logarithm(int.to_float(x), int.to_float(y)) + let truncated = round_to_zero(value, 0) + let remainder = value -. truncated + remainder == 0.0 +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function that tests whether a given integer value \\(n \in \mathbb{Z}\\) is a +/// perfect number. A number is perfect if it is equal to the sum of its proper +/// positive divisors. +/// +///
+/// Details +/// +/// For example: +/// - \\(6\\) is a perfect number since the divisors of 6 are \\(1 + 2 + 3 = 6\\). +/// - \\(28\\) is a perfect number since the divisors of 28 are \\(1 + 2 + 4 + 7 + 14 = 28\\). +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.is_perfect(6) +/// |> should.equal(True) +/// +/// maths.is_perfect(28) +/// |> should.equal(True) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_perfect(n: Int) -> Bool { + int.sum(proper_divisors(n)) == n +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function that tests whether a given integer value \\(x \in \mathbb{Z}\\) is a +/// prime number. A prime number is a natural number greater than 1 that has no +/// positive divisors other than 1 and itself. +/// +/// The function uses the Miller-Rabin primality test to assess if \\(x\\) is prime. +/// It is a probabilistic test, so it can mistakenly identify a composite number +/// as prime. However, the probability of such errors decreases with more testing +/// iterations (the function uses 64 iterations internally, which is typically +/// more than sufficient). The Miller-Rabin test is particularly useful for large +/// numbers. +/// +///
+/// Details +/// +/// Examples of prime numbers: +/// - \\(2\\) is a prime number since it has only two divisors: \\(1\\) and \\(2\\). +/// - \\(7\\) is a prime number since it has only two divisors: \\(1\\) and \\(7\\). +/// - \\(4\\) is not a prime number since it has divisors other than \\(1\\) and itself, such +/// as \\(2\\). +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.is_prime(2) +/// |> should.equal(True) +/// +/// maths.is_prime(4) +/// |> should.equal(False) +/// +/// // Test the 2nd Carmichael number +/// maths.is_prime(1105) +/// |> should.equal(False) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_prime(x: Int) -> Bool { + case x { + x if x < 2 -> { + False + } + x if x == 2 -> { + True + } + _ -> { + miller_rabin_test(x, 64) + } + } +} + +fn miller_rabin_test(n: Int, k: Int) -> Bool { + case n, k { + _, 0 -> True + _, _ -> { + // Generate a random int in the range [2, n] + let random_candidate = 2 + int.random(n - 2) + case powmod_with_check(random_candidate, n - 1, n) == 1 { + True -> miller_rabin_test(n, k - 1) + False -> False + } + } + } +} + +fn powmod_with_check(base: Int, exponent: Int, modulus: Int) -> Int { + case exponent, { exponent % 2 } == 0 { + 0, _ -> 1 + _, True -> { + let x = powmod_with_check(base, exponent / 2, modulus) + case { x * x } % modulus, x != 1 && x != { modulus - 1 } { + 1, True -> 0 + _, _ -> { x * x } % modulus + } + } + _, _ -> { base * powmod_with_check(base, exponent - 1, modulus) } % modulus + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function that tests whether a given real number \\(x \in \mathbb{R}\\) is strictly +/// between two other real numbers, \\(a,b \in \mathbb{R}\\), such that \\(a < x < b\\). +/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.is_between(5.5, 5.0, 6.0) +/// |> should.equal(True) +/// +/// maths.is_between(5.0, 5.0, 6.0) +/// |> should.equal(False) +/// +/// maths.is_between(6.0, 5.0, 6.0) +/// |> should.equal(False) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_between(x: Float, lower: Float, upper: Float) -> Bool { + lower <. x && x <. upper +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function that tests whether a given integer \\(n \in \mathbb{Z}\\) is divisible by another +/// integer \\(d \in \mathbb{Z}\\), such that \\(n \mod d = 0\\). +/// +///
+/// Details +/// +/// For example: +/// - \\(n = 10\\) is divisible by \\(d = 2\\) because \\(10 \mod 2 = 0\\). +/// - \\(n = 7\\) is not divisible by \\(d = 3\\) because \\(7 \mod 3 \neq 0\\). +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.is_divisible(10, 2) +/// |> should.equal(True) +/// +/// maths.is_divisible(7, 3) +/// |> should.equal(False) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_divisible(n: Int, d: Int) -> Bool { + n % d == 0 +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// A function that tests whether a given integer \\(m \in \mathbb{Z}\\) is a multiple of another +/// integer \\(k \in \mathbb{Z}\\), such that \\(m = k \cdot q\\), with \\(q \in \mathbb{Z}\\). +/// +///
+/// Details +/// +/// For example: +/// - \\(m = 15\\) is a multiple of \\(k = 5\\) because \\(15 = 5 \cdot 3\\). +/// - \\(m = 14\\) is not a multiple of \\(k = 5\\) because \\(\frac{14}{5}\\) does not yield an +/// integer quotient. +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// maths.is_multiple(15, 5) +/// |> should.equal(True) +/// +/// maths.is_multiple(14, 5) +/// |> should.equal(False) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn is_multiple(m: Int, k: Int) -> Bool { + m % k == 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 assert [a1, a2, a3, a4, a5] = [ + 0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429, + ] + let p = 0.3275911 + + let sign = float_sign(x) + 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 + *. exponential(-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 = + list.index_fold(lanczos_p, 0.0, fn(acc, v, index) { + case index > 0 { + True -> acc +. v /. { z +. int.to_float(index) } + False -> v + } + }) + let t = z +. lanczos_g +. 0.5 + let assert Ok(v1) = float.power(2.0 *. pi(), 0.5) + let assert Ok(v2) = float.power(t, z +. 0.5) + v1 *. v2 *. exponential(-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 incomplete_gamma(a: Float, x: Float) -> Result(Float, Nil) { + case a >. 0.0 && x >=. 0.0 { + True -> { + let assert Ok(v) = float.power(x, a) + Ok( + v + *. exponential(-1.0 *. x) + *. incomplete_gamma_sum(a, x, 1.0 /. a, 0.0, 1.0), + ) + } + + False -> Error(Nil) + } +} + +fn incomplete_gamma_sum( + a: Float, + x: Float, + t: Float, + s: Float, + n: Float, +) -> Float { + case t { + 0.0 -> s + _ -> { + let ns = s +. t + let nt = t *. { x /. { a +. n } } + incomplete_gamma_sum(a, x, nt, ns, n +. 1.0) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns an iterator generating evenly spaced values within a specified interval +/// `[start, stop)` based on a given increment size. +/// +/// Note that if `increment > 0`, the sequence progresses from `start` towards `stop`, while if +/// `increment < 0`, the sequence progresses from `start` towards `stop` in reverse. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// maths.arange(1.0, 5.0, 1.0) +/// |> yielder.to_list() +/// |> should.equal([1.0, 2.0, 3.0, 4.0]) +/// +/// // No points returned since +/// // start is smaller than stop and the step is positive +/// maths.arange(5.0, 1.0, 1.0) +/// |> yielder.to_list() +/// |> should.equal([]) +/// +/// // Points returned since +/// // start smaller than stop but negative step +/// maths.arange(5.0, 1.0, -1.0) +/// |> yielder.to_list() +/// |> should.equal([5.0, 4.0, 3.0, 2.0]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn arange(start: Float, stop: Float, increment: Float) -> Yielder(Float) { + // Check if the range would be empty due to direction and increment + case + { start >=. stop && increment >. 0.0 } + || { start <=. stop && increment <. 0.0 } + { + True -> yielder.empty() + False -> { + let direction = case start <=. stop { + True -> 1.0 + False -> -1.0 + } + let increment_abs = float.absolute_value(increment) + let distance = float.absolute_value(start -. stop) + let num = float.round(distance /. increment_abs) + + yielder.map(yielder.range(0, num - 1), fn(i) { + start +. int.to_float(i) *. increment_abs *. direction + }) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns an iterator for generating linearly spaced points over a specified +/// interval. The endpoint of the interval can optionally be included/excluded. The number of +/// points and whether the endpoint is included determine the spacing between values. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(linspace) = maths.linear_space(10.0, 20.0, 5, True) +/// let pairs = +/// linspace |> yielder.to_list() |> list.zip([10.0, 12.5, 15.0, 17.5, 20.0]) +/// let assert Ok(result) = maths.all_close(pairs, 0.0, tolerance) +/// result +/// |> list.all(fn(x) { x == True }) +/// |> should.be_true() +/// +/// // A negative number of points (-5) does not work +/// maths.linear_space(10.0, 50.0, -5, True) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn linear_space( + start: Float, + stop: Float, + steps: Int, + endpoint: Bool, +) -> Result(Yielder(Float), Nil) { + let direction = case start <=. stop { + True -> 1.0 + False -> -1.0 + } + + let increment = case endpoint { + True -> { + float.absolute_value(start -. stop) /. int.to_float(steps - 1) + } + False -> { + float.absolute_value(start -. stop) /. int.to_float(steps) + } + } + case steps > 0 { + True -> { + Ok( + yielder.map(yielder.range(0, steps - 1), fn(i) { + start +. int.to_float(i) *. increment *. direction + }), + ) + } + False -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns an iterator for generating logarithmically spaced points over a specified +/// interval. The endpoint of the interval can optionally be included/excluded. The number of +/// points, base, and whether the endpoint is included determine the spacing between values. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, True, 10.0) +/// let pairs = logspace |> yielder.to_list() |> list.zip([10.0, 100.0, 1000.0]) +/// let assert Ok(result) = maths.all_close(pairs, 0.0, tolerance) +/// result +/// |> list.all(fn(x) { x == True }) +/// |> should.be_true() +/// +/// // A negative number of points (-3) does not work +/// maths.logarithmic_space(1.0, 3.0, -3, False, 10.0) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn logarithmic_space( + start: Float, + stop: Float, + steps: Int, + endpoint: Bool, + base: Float, +) -> Result(Yielder(Float), Nil) { + case steps > 0 { + True -> { + let assert Ok(linspace) = linear_space(start, stop, steps, endpoint) + + Ok( + yielder.map(linspace, fn(i) { + let assert Ok(result) = float.power(base, i) + result + }), + ) + } + False -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns an iterator of numbers spaced evenly on a log scale (a geometric +/// progression). Each point in the list is a constant multiple of the previous. The function is +/// similar to the [`logarithmic_space`](#logarithmic_space) function, but with endpoints +/// specified directly. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example () { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(logspace) = maths.geometric_space(10.0, 1000.0, 3, True) +/// let pairs = logspace |> yielder.to_list() |> list.zip([10.0, 100.0, 1000.0]) +/// let assert Ok(result) = maths.all_close(pairs, 0.0, tolerance) +/// result +/// |> list.all(fn(x) { x == True }) +/// |> should.be_true() +/// +/// // Input (start and stop can't be equal to 0.0) +/// maths.geometric_space(0.0, 1000.0, 3, False) +/// |> should.be_error() +/// +/// maths.geometric_space(-1000.0, 0.0, 3, False) +/// |> should.be_error() +/// +/// // A negative number of points (-3) does not work +/// maths.geometric_space(10.0, 1000.0, -3, False) +/// |> should.be_error() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn geometric_space( + start: Float, + stop: Float, + steps: Int, + endpoint: Bool, +) -> Result(Yielder(Float), Nil) { + case start == 0.0 || stop == 0.0 { + True -> Error(Nil) + False -> + case steps > 0 { + True -> { + let assert Ok(log_start) = logarithm_10(start) + let assert Ok(log_stop) = logarithm_10(stop) + logarithmic_space(log_start, log_stop, steps, endpoint, 10.0) + } + False -> Error(Nil) + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns an iterator of exponentially spaced points over a specified interval. The +/// endpoint of the interval can optionally be included/excluded. The number of points and whether +/// the endpoint is included determine the spacing between values. The sequence is generated by +/// computing intermediate values in a logarithmic domain and transforming them into the exponential +/// domain. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(tolerance) = float.power(10.0, -6.0) +/// let assert Ok(exp_space) = maths.exponential_space(1.0, 1000.0, 4, True) +/// let expected = [1.0, 10.0, 100.0, 1000.0] +/// let pairs = exp_space |> yielder.to_list() |> list.zip(expected) +/// let assert Ok(result) = maths.all_close(pairs, 0.0, tolerance) +/// result |> list.all(fn(x) { x == True }) |> should.be_true() +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn exponential_space( + start: Float, + stop: Float, + steps: Int, + endpoint: Bool, +) -> Result(Yielder(Float), Nil) { + case steps > 0 { + True -> { + let assert Ok(log_start) = logarithm_10(start) + let assert Ok(log_stop) = logarithm_10(stop) + let assert Ok(log_space) = + linear_space(log_start, log_stop, steps, endpoint) + Ok( + yielder.map(log_space, fn(log_value) { + let assert Ok(exp_value) = float.power(10.0, log_value) + exp_value + }), + ) + } + False -> Error(Nil) + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// Generates evenly spaced points around a center value. The total span is determined by +/// the radius argument of the function. +/// +///
+/// Example: +/// +/// import gleam/yielder +/// import gleeunit/should +/// import gleam_community/maths +/// +/// pub fn example() { +/// let assert Ok(sym_space) = maths.symmetric_space(0.0, 5.0, 5) +/// sym_space +/// |> yielder.to_list() +/// |> should.equal([-5.0, -2.5, 0.0, 2.5, 5.0]) +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +pub fn symmetric_space( + center: Float, + radius: Float, + steps: Int, +) -> Result(Yielder(Float), Nil) { + case steps > 0 { + False -> Error(Nil) + True -> { + let start = center -. radius + let stop = center +. radius + linear_space(start, stop, steps, True) + } + } +} diff --git a/src/gleam_community/maths/arithmetics.gleam b/src/gleam_community/maths/arithmetics.gleam deleted file mode 100644 index b44f80a..0000000 --- a/src/gleam_community/maths/arithmetics.gleam +++ /dev/null @@ -1,720 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Arithmetics: A module containing a collection of fundamental mathematical functions relating to simple arithmetics (addition, subtraction, multiplication, etc.), but also number theory. -//// -//// * **Division functions** -//// * [`gcd`](#gcd) -//// * [`lcm`](#lcm) -//// * [`divisors`](#divisors) -//// * [`proper_divisors`](#proper_divisors) -//// * [`int_euclidean_modulo`](#int_euclidean_modulo) -//// * **Sums and products** -//// * [`float_sum`](#float_sum) -//// * [`int_sum`](#int_sum) -//// * [`float_product`](#float_product) -//// * [`int_product`](#int_product) -//// * [`float_cumulative_sum`](#float_cumulative_sum) -//// * [`int_cumulative_sum`](#int_cumulative_sum) -//// * [`float_cumulative_product`](#float_cumulative_product) -//// * [`int_cumulative_product`](#int_cumulative_product) -//// - -import gleam/int -import gleam/list -import gleam/option -import gleam/pair -import gleam/result -import gleam_community/maths/conversion -import gleam_community/maths/elementary -import gleam_community/maths/piecewise - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function calculates the greatest common divisor of two integers -/// \\(x, y \in \mathbb{Z}\\). The greatest common divisor is the largest positive -/// integer that is divisible by both \\(x\\) and \\(y\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example() { -/// arithmetics.gcd(1, 1) -/// |> should.equal(1) -/// -/// arithmetics.gcd(100, 10) -/// |> should.equal(10) -/// -/// arithmetics.gcd(-36, -17) -/// |> should.equal(1) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn gcd(x: Int, y: Int) -> Int { - let absx = piecewise.int_absolute_value(x) - let absy = piecewise.int_absolute_value(y) - do_gcd(absx, absy) -} - -fn do_gcd(x: Int, y: Int) -> Int { - case x == 0 { - True -> y - False -> { - let assert Ok(z) = int.modulo(y, x) - do_gcd(z, x) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// -/// Given two integers, \\(x\\) (dividend) and \\(y\\) (divisor), the Euclidean modulo -/// of \\(x\\) by \\(y\\), denoted as \\(x \mod y\\), is the remainder \\(r\\) of the -/// division of \\(x\\) by \\(y\\), such that: -/// -/// \\[ -/// x = q \cdot y + r \quad \text{and} \quad 0 \leq r < |y|, -/// \\] -/// -/// where \\(q\\) is an integer that represents the quotient of the division. -/// -/// The Euclidean modulo function of two numbers, is the remainder operation most -/// commonly utilized in mathematics. This differs from the standard truncating -/// modulo operation frequently employed in programming via the `%` operator. -/// Unlike the `%` operator, which may return negative results depending on the -/// divisor's sign, the Euclidean modulo function is designed to always yield a -/// positive outcome, ensuring consistency with mathematical conventions. -/// -/// Note that like the Gleam division operator `/` this will return `0` if one of -/// the arguments is `0`. -/// -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example() { -/// arithmetics.euclidean_modulo(15, 4) -/// |> should.equal(3) -/// -/// arithmetics.euclidean_modulo(-3, -2) -/// |> should.equal(1) -/// -/// arithmetics.euclidean_modulo(5, 0) -/// |> should.equal(0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_euclidean_modulo(x: Int, y: Int) -> Int { - case x % y, x, y { - _, 0, _ -> 0 - _, _, 0 -> 0 - md, _, _ if md < 0 -> md + int.absolute_value(y) - md, _, _ -> md - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function calculates the least common multiple of two integers -/// \\(x, y \in \mathbb{Z}\\). The least common multiple is the smallest positive -/// integer that has both \\(x\\) and \\(y\\) as factors. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example() { -/// arithmetics.lcm(1, 1) -/// |> should.equal(1) -/// -/// arithmetics.lcm(100, 10) -/// |> should.equal(100) -/// -/// arithmetics.lcm(-36, -17) -/// |> should.equal(612) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn lcm(x: Int, y: Int) -> Int { - let absx = piecewise.int_absolute_value(x) - let absy = piecewise.int_absolute_value(y) - absx * absy / do_gcd(absx, absy) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns all the positive divisors of an integer, including the -/// number itself. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example() { -/// arithmetics.divisors(4) -/// |> should.equal([1, 2, 4]) -/// -/// arithmetics.divisors(6) -/// |> should.equal([1, 2, 3, 6]) -/// -/// arithmetics.proper_divisors(13) -/// |> should.equal([1, 13]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn divisors(n: Int) -> List(Int) { - find_divisors(n) -} - -fn find_divisors(n: Int) -> List(Int) { - let nabs = piecewise.float_absolute_value(conversion.int_to_float(n)) - let assert Ok(sqrt_result) = elementary.square_root(nabs) - let max = conversion.float_to_int(sqrt_result) + 1 - list.range(2, max) - |> list.fold([1, n], fn(acc, i) { - case n % i == 0 { - True -> [i, n / i, ..acc] - False -> acc - } - }) - |> list.unique() - |> list.sort(int.compare) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns all the positive divisors of an integer, excluding the -/// number iteself. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example() { -/// arithmetics.proper_divisors(4) -/// |> should.equal([1, 2]) -/// -/// arithmetics.proper_divisors(6) -/// |> should.equal([1, 2, 3]) -/// -/// arithmetics.proper_divisors(13) -/// |> should.equal([1]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn proper_divisors(n: Int) -> List(Int) { - let divisors = find_divisors(n) - divisors - |> list.take(list.length(divisors) - 1) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) sum of the elements in a list: -/// -/// \\[ -/// \sum_{i=1}^n w_i x_i -/// \\] -/// -/// In the formula, \\(n\\) is the length of the list and \\(x_i \in \mathbb{R}\\) is -/// the value in the input list indexed by \\(i\\), while the \\(w_i \in \mathbb{R}\\) -/// are corresponding weights (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// // An empty list returns an error -/// [] -/// |> arithmetics.float_sum(option.None) -/// |> should.equal(0.0) -/// -/// // Valid input returns a result -/// [1.0, 2.0, 3.0] -/// |> arithmetics.float_sum(option.None) -/// |> should.equal(6.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_sum(arr: List(Float), weights: option.Option(List(Float))) -> Float { - case arr, weights { - [], _ -> 0.0 - _, option.None -> - arr - |> list.fold(0.0, fn(acc, a) { a +. acc }) - _, option.Some(warr) -> { - list.zip(arr, warr) - |> list.fold(0.0, fn(acc, a) { pair.first(a) *. pair.second(a) +. acc }) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the sum of the elements in a list: -/// -/// \\[ -/// \sum_{i=1}^n x_i -/// \\] -/// -/// In the formula, \\(n\\) is the length of the list and \\(x_i \in \mathbb{Z}\\) is -/// the value in the input list indexed by \\(i\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// // An empty list returns 0 -/// [] -/// |> arithmetics.int_sum() -/// |> should.equal(0) -/// -/// // Valid input returns a result -/// [1, 2, 3] -/// |> arithmetics.int_sum() -/// |> should.equal(6) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_sum(arr: List(Int)) -> Int { - case arr { - [] -> 0 - _ -> - arr - |> list.fold(0, fn(acc, a) { a + acc }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) product of the elements in a list: -/// -/// \\[ -/// \prod_{i=1}^n x_i^{w_i} -/// \\] -/// -/// In the formula, \\(n\\) is the length of the list and \\(x_i \in \mathbb{R}\\) is -/// the value in the input list indexed by \\(i\\), while the \\(w_i \in \mathbb{R}\\) -/// are corresponding weights (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// // An empty list returns 1.0 -/// [] -/// |> arithmetics.float_product(option.None) -/// |> should.equal(1.0) -/// -/// // Valid input returns a result -/// [1.0, 2.0, 3.0] -/// |> arithmetics.float_product(option.None) -/// |> should.equal(6.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_product( - arr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - case arr, weights { - [], _ -> - 1.0 - |> Ok - _, option.None -> - arr - |> list.fold(1.0, fn(acc, a) { a *. acc }) - |> Ok - _, option.Some(warr) -> { - list.zip(arr, warr) - |> list.map(fn(a: #(Float, Float)) -> Result(Float, Nil) { - pair.first(a) - |> elementary.power(pair.second(a)) - }) - |> result.all - |> result.map(fn(prods) { - prods - |> list.fold(1.0, fn(acc: Float, a: Float) -> Float { a *. acc }) - }) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the product of the elements in a list: -/// -/// \\[ -/// \prod_{i=1}^n x_i -/// \\] -/// -/// In the formula, \\(n\\) is the length of the list and \\(x_i \in \mathbb{Z}\\) is -/// the value in the input list indexed by \\(i\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// // An empty list returns 1 -/// [] -/// |> arithmetics.int_product() -/// |> should.equal(1) -/// -/// // Valid input returns a result -/// [1, 2, 3] -/// |> arithmetics.int_product() -/// |> should.equal(6) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_product(arr: List(Int)) -> Int { - case arr { - [] -> 1 - _ -> - arr - |> list.fold(1, fn(acc, a) { a * acc }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the cumulative sum of the elements in a list: -/// -/// \\[ -/// v_j = \sum_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n -/// \\] -/// -/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative sum of \\(n\\) -/// elements. That is, \\(n\\) is the length of the list and \\(x_i \in \mathbb{R}\\) -/// is the value in the input list indexed by \\(i\\). The value \\(v_j\\) is thus the -/// sum of the \\(1\\) to \\(j\\) first elements in the given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// [] -/// |> arithmetics.float_cumulative_sum() -/// |> should.equal([]) -/// -/// // Valid input returns a result -/// [1.0, 2.0, 3.0] -/// |> arithmetics.float_cumulative_sum() -/// |> should.equal([1.0, 3.0, 6.0]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_cumulative_sum(arr: List(Float)) -> List(Float) { - case arr { - [] -> [] - _ -> - arr - |> list.scan(0.0, fn(acc, a) { a +. acc }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the cumulative sum of the elements in a list: -/// -/// \\[ -/// v_j = \sum_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n -/// \\] -/// -/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative sum of \\(n\\) -/// elements. That is, \\(n\\) is the length of the list and \\(x_i \in \mathbb{Z}\\) -/// is the value in the input list indexed by \\(i\\). The value \\(v_j\\) is thus the -/// sum of the \\(1\\) to \\(j\\) first elements in the given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// [] -/// |> arithmetics.int_cumulative_sum() -/// |> should.equal([]) -/// -/// // Valid input returns a result -/// [1, 2, 3] -/// |> arithmetics.int_cumulative_sum() -/// |> should.equal([1, 3, 6]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_cumulative_sum(arr: List(Int)) -> List(Int) { - case arr { - [] -> [] - _ -> - arr - |> list.scan(0, fn(acc, a) { a + acc }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the cumulative product of the elements in a list: -/// -/// \\[ -/// v_j = \prod_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n -/// \\] -/// -/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative product of -/// \\(n\\) elements. That is, \\(n\\) is the length of the list and -/// \\(x_i \in \mathbb{R}\\) is the value in the input list indexed by \\(i\\). The -/// value \\(v_j\\) is thus the sum of the \\(1\\) to \\(j\\) first elements in the -/// given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// // An empty list returns an error -/// [] -/// |> arithmetics.float_cumulative_product() -/// |> should.equal([]) -/// -/// // Valid input returns a result -/// [1.0, 2.0, 3.0] -/// |> arithmetics.float_cumulative_product() -/// |> should.equal([1.0, 2.0, 6.0]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_cumulative_product(arr: List(Float)) -> List(Float) { - case arr { - [] -> [] - _ -> - arr - |> list.scan(1.0, fn(acc, a) { a *. acc }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the cumulative product of the elements in a list: -/// -/// \\[ -/// v_j = \prod_{i=1}^j x_i \\;\\; \forall j = 1,\dots, n -/// \\] -/// -/// In the formula, \\(v_j\\) is the \\(j\\)'th element in the cumulative product of -/// \\(n\\) elements. That is, \\(n\\) is the length of the list and -/// \\(x_i \in \mathbb{Z}\\) is the value in the input list indexed by \\(i\\). The -/// value \\(v_j\\) is thus the product of the \\(1\\) to \\(j\\) first elements in the -/// given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/arithmetics -/// -/// pub fn example () { -/// // An empty list returns an error -/// [] -/// |> arithmetics.int_cumulative_product() -/// |> should.equal([]) -/// -/// // Valid input returns a result -/// [1, 2, 3] -/// |> arithmetics.int_cumulative_product() -/// |> should.equal([1, 2, 6]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_cumulative_product(arr: List(Int)) -> List(Int) { - case arr { - [] -> [] - _ -> - arr - |> list.scan(1, fn(acc, a) { a * acc }) - } -} diff --git a/src/gleam_community/maths/combinatorics.gleam b/src/gleam_community/maths/combinatorics.gleam deleted file mode 100644 index a3a4e7e..0000000 --- a/src/gleam_community/maths/combinatorics.gleam +++ /dev/null @@ -1,630 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Combinatorics: A module that offers mathematical functions related to counting, arrangements, -//// and permutations/combinations. -//// -//// * **Combinatorial functions** -//// * [`combination`](#combination) -//// * [`factorial`](#factorial) -//// * [`permutation`](#permutation) -//// * [`list_combination`](#list_combination) -//// * [`list_permutation`](#list_permutation) -//// * [`cartesian_product`](#cartesian_product) -//// - -import gleam/iterator -import gleam/list -import gleam/option -import gleam/set -import gleam_community/maths/conversion -import gleam_community/maths/elementary - -pub type CombinatoricsMode { - WithRepetitions - WithoutRepetitions -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the number of \\(k\\)-combinations of \\(n\\) elements. -/// -/// **Without Repetitions:** -/// -/// \\[ -/// C(n, k) = \binom{n}{k} = \frac{n!}{k! (n-k)!} -/// \\] -/// Also known as "\\(n\\) choose \\(k\\)" or the binomial coefficient. -/// -/// **With Repetitions:** -/// -/// \\[ -/// C^*(n, k) = \binom{n + k - 1}{k} = \frac{(n + k - 1)!}{k! (n - 1)!} -/// \\] -/// Also known as the "stars and bars" problem in combinatorics. -/// -/// The implementation uses an efficient iterative multiplicative formula for computing the result. -/// -///
-/// Details -/// -/// A \\(k\\)-combination is a sequence of \\(k\\) elements selected from \\(n\\) elements where -/// the order of selection does not matter. For example, consider selecting 2 elements from a list -/// of 3 elements: `["A", "B", "C"]`: -/// -/// - For \\(k\\)-combinations (without repetitions), where order does not matter, the possible -/// selections are: -/// - `["A", "B"]` -/// - `["A", "C"]` -/// - `["B", "C"]` -/// -/// - For \\(k\\)-combinations (with repetitions), where order does not matter but elements can -/// repeat, the possible selections are: -/// - `["A", "A"], ["A", "B"], ["A", "C"]` -/// - `["B", "B"], ["B", "C"], ["C", "C"]` -/// -/// - On the contrary, for \\(k\\)-permutations (without repetitions), the order matters, so the -/// possible selections are: -/// - `["A", "B"], ["B", "A"]` -/// - `["A", "C"], ["C", "A"]` -/// - `["B", "C"], ["C", "B"]` -///
-///
-/// Example: -/// -/// import gleam/option -/// import gleeunit/should -/// import gleam_community/maths/combinatorics -/// -/// pub fn example() { -/// // Invalid input gives an error -/// combinatorics.combination(-1, 1, option.None) -/// |> should.be_error() -/// -/// // Valid input: n = 4 and k = 0 -/// combinatorics.combination(4, 0, option.Some(combinatorics.WithoutRepetitions)) -/// |> should.equal(Ok(1)) -/// -/// // Valid input: k = n (n = 4, k = 4) -/// combinatorics.combination(4, 4, option.Some(combinatorics.WithoutRepetitions)) -/// |> should.equal(Ok(1)) -/// -/// // Valid input: combinations with repetition (n = 2, k = 3) -/// combinatorics.combination(2, 3, option.Some(combinatorics.WithRepetitions)) -/// |> should.equal(Ok(4)) -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn combination( - n: Int, - k: Int, - mode: option.Option(CombinatoricsMode), -) -> Result(Int, Nil) { - case n, k { - _, _ if n < 0 -> Error(Nil) - _, _ if k < 0 -> Error(Nil) - _, _ -> { - case mode { - option.Some(WithRepetitions) -> combination_with_repetitions(n, k) - _ -> combination_without_repetitions(n, k) - } - } - } -} - -fn combination_with_repetitions(n: Int, k: Int) -> Result(Int, Nil) { - combination_without_repetitions(n + k - 1, k) -} - -fn combination_without_repetitions(n: Int, k: Int) -> Result(Int, Nil) { - case n, k { - _, _ if k == 0 || k == n -> { - 1 |> Ok - } - _, _ -> { - let min = case k < n - k { - True -> k - False -> n - k - } - list.range(1, min) - |> list.fold(1, fn(acc, x) { 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_community/maths/combinatorics -/// -/// pub fn example() { -/// // Invalid input gives an error -/// combinatorics.factorial(-1) -/// |> should.be_error() -/// -/// // Valid input returns a result (n = 0) -/// combinatorics.factorial(0) -/// |> should.equal(Ok(1)) -/// -/// combinatorics.factorial(3) -/// |> should.equal(Ok(6)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn factorial(n) -> Result(Int, Nil) { - case n { - _ if n < 0 -> Error(Nil) - 0 -> Ok(1) - 1 -> Ok(1) - _ -> - list.range(1, n) - |> list.fold(1, fn(acc, x) { acc * x }) - |> Ok - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A combinatorial function for computing the number of \\(k\\)-permutations. -/// -/// **Without** repetitions: -/// -/// \\[ -/// P(n, k) = \binom{n}{k} \cdot k! = \frac{n!}{(n - k)!} -/// \\] -/// -/// **With** repetitions: -/// -/// \\[ -/// P^*(n, k) = n^k -/// \\] -/// -/// The implementation uses an efficient iterative multiplicative formula for computing the result. -/// -///
-/// Details -/// -/// A \\(k\\)-permutation (without repetitions) is a sequence of \\(k\\) elements selected from \ -/// \\(n\\) elements where the order of selection matters. For example, consider selecting 2 -/// elements from a list of 3 elements: `["A", "B", "C"]`: -/// -/// - For \\(k\\)-permutations (without repetitions), the order matters, so the possible selections -/// are: -/// - `["A", "B"], ["B", "A"]` -/// - `["A", "C"], ["C", "A"]` -/// - `["B", "C"], ["C", "B"]` -/// -/// - For \\(k\\)-permutations (with repetitions), the order also matters, but we have repeated -/// selections: -/// - `["A", "A"], ["A", "B"], ["A", "C"]` -/// - `["B", "A"], ["B", "B"], ["B", "C"]` -/// - `["C", "A"], ["C", "B"], ["C", "C"]` -/// -/// - On the contrary, for \\(k\\)-combinations (without repetitions), where order does not matter, -/// the possible selections are: -/// - `["A", "B"]` -/// - `["A", "C"]` -/// - `["B", "C"]` -///
-/// -///
-/// Example: -/// -/// import gleam/option -/// import gleeunit/should -/// import gleam_community/maths/combinatorics -/// -/// pub fn example() { -/// // Invalid input gives an error -/// combinatorics.permutation(-1, 1, option.None) -/// |> should.be_error() -/// -/// // Valid input returns a result (n = 4, k = 0) -/// combinatorics.permutation(4, 0, option.Some(combinatorics.WithoutRepetitions)) -/// |> should.equal(Ok(1)) -/// -/// // Valid input returns the correct number of permutations (n = 4, k = 2) -/// combinatorics.permutation(4, 2, option.Some(combinatorics.WithoutRepetitions)) -/// |> should.equal(Ok(12)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn permutation( - n: Int, - k: Int, - mode: option.Option(CombinatoricsMode), -) -> Result(Int, Nil) { - case n, k, mode { - _, _, _ if n < 0 -> Error(Nil) - _, _, _ if k < 0 -> Error(Nil) - _, _, option.Some(WithRepetitions) -> permutation_with_repetitions(n, k) - _, _, _ -> permutation_without_repetitions(n, k) - } -} - -fn permutation_without_repetitions(n: Int, k: Int) -> Result(Int, Nil) { - case n, k { - _, _ if k < 0 || k > n -> { - 0 |> Ok - } - _, _ if k == 0 -> { - 1 |> Ok - } - _, _ -> - list.range(0, k - 1) - |> list.fold(1, fn(acc, x) { acc * { n - x } }) - |> Ok - } -} - -fn permutation_with_repetitions(n: Int, k: Int) -> Result(Int, Nil) { - let n_float = conversion.int_to_float(n) - let k_float = conversion.int_to_float(k) - // 'n' ank 'k' are positive integers, so no errors here... - let assert Ok(result) = elementary.power(n_float, k_float) - result - |> conversion.float_to_int() - |> Ok -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Generates all possible combinations of \\(k\\) elements selected from a given list of size -/// \\(n\\). -/// -/// The function can handle cases with and without repetitions -/// (see more details [here](#combination)). Also, note that repeated elements are treated as -/// distinct. -/// -///
-/// Example: -/// -/// import gleam/set -/// import gleam/option -/// import gleam/iterator -/// import gleeunit/should -/// import gleam_community/maths/combinatorics -/// -/// pub fn example () { -/// // Generate all 3-combinations without repetition -/// let assert Ok(result) = -/// combinatorics.list_combination( -/// [1, 2, 3, 4], -/// 3, -/// option.Some(combinatorics.WithoutRepetitions), -/// ) -/// -/// result -/// |> iterator.to_list() -/// |> set.from_list() -/// |> should.equal(set.from_list([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]])) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn list_combination( - arr: List(a), - k: Int, - mode: option.Option(CombinatoricsMode), -) -> Result(iterator.Iterator(List(a)), Nil) { - case k, mode { - _, _ if k < 0 -> Error(Nil) - _, option.Some(WithRepetitions) -> list_combination_with_repetitions(arr, k) - _, _ -> list_combination_without_repetitions(arr, k) - } -} - -fn list_combination_without_repetitions( - arr: List(a), - k: Int, -) -> Result(iterator.Iterator(List(a)), Nil) { - case k, list.length(arr) { - _, arr_length if k > arr_length -> Error(Nil) - // Special case: When k = n, then the entire list is the only valid combination - _, arr_length if k == arr_length -> { - iterator.single(arr) |> Ok - } - _, _ -> { - Ok( - do_list_combination_without_repetitions(iterator.from_list(arr), k, []), - ) - } - } -} - -fn do_list_combination_without_repetitions( - arr: iterator.Iterator(a), - k: Int, - prefix: List(a), -) -> iterator.Iterator(List(a)) { - case k { - 0 -> iterator.single(list.reverse(prefix)) - _ -> - case arr |> iterator.step { - iterator.Done -> iterator.empty() - iterator.Next(x, xs) -> { - let with_x = - do_list_combination_without_repetitions(xs, k - 1, [x, ..prefix]) - let without_x = do_list_combination_without_repetitions(xs, k, prefix) - iterator.concat([with_x, without_x]) - } - } - } -} - -fn list_combination_with_repetitions( - arr: List(a), - k: Int, -) -> Result(iterator.Iterator(List(a)), Nil) { - Ok(do_list_combination_with_repetitions(iterator.from_list(arr), k, [])) -} - -fn do_list_combination_with_repetitions( - arr: iterator.Iterator(a), - k: Int, - prefix: List(a), -) -> iterator.Iterator(List(a)) { - case k { - 0 -> iterator.single(list.reverse(prefix)) - _ -> - case arr |> iterator.step { - iterator.Done -> iterator.empty() - iterator.Next(x, xs) -> { - let with_x = - do_list_combination_with_repetitions(arr, k - 1, [x, ..prefix]) - let without_x = do_list_combination_with_repetitions(xs, k, prefix) - iterator.concat([with_x, without_x]) - } - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Generates all possible permutations of \\(k\\) elements selected from a given list of size -/// \\(n\\). -/// -/// The function can handle cases with and without repetitions -/// (see more details [here](#permutation)). Also, note that repeated elements are treated as -/// distinct. -/// -///
-/// Example: -/// -/// import gleam/set -/// import gleam/option -/// import gleam/iterator -/// import gleeunit/should -/// import gleam_community/maths/combinatorics -/// -/// pub fn example () { -/// // Generate all 3-permutations without repetition -/// let assert Ok(result) = -/// combinatorics.list_permutation( -/// [1, 2, 3], -/// 3, -/// option.Some(combinatorics.WithoutRepetitions), -/// ) -/// -/// result -/// |> iterator.to_list() -/// |> set.from_list() -/// |> should.equal( -/// set.from_list([ -/// [1, 2, 3], -/// [2, 1, 3], -/// [3, 1, 2], -/// [1, 3, 2], -/// [2, 3, 1], -/// [3, 2, 1], -/// ]), -/// ) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -/// -pub fn list_permutation( - arr: List(a), - k: Int, - mode: option.Option(CombinatoricsMode), -) -> Result(iterator.Iterator(List(a)), Nil) { - case k, mode { - _, _ if k < 0 -> Error(Nil) - _, option.Some(WithRepetitions) -> - Ok(list_permutation_with_repetitions(arr, k)) - _, _ -> list_permutation_without_repetitions(arr, k) - } -} - -fn remove_first_by_index( - arr: iterator.Iterator(#(Int, a)), - index_to_remove: Int, -) -> iterator.Iterator(#(Int, a)) { - iterator.flat_map(arr, fn(arg) { - let #(index, element) = arg - case index == index_to_remove { - True -> iterator.empty() - False -> iterator.single(#(index, element)) - } - }) -} - -fn list_permutation_without_repetitions( - arr: List(a), - k: Int, -) -> Result(iterator.Iterator(List(a)), Nil) { - case k, list.length(arr) { - _, arr_length if k > arr_length -> Error(Nil) - _, _ -> { - let indexed_arr = list.index_map(arr, fn(x, i) { #(i, x) }) - Ok(do_list_permutation_without_repetitions( - iterator.from_list(indexed_arr), - k, - )) - } - } -} - -fn do_list_permutation_without_repetitions( - arr: iterator.Iterator(#(Int, a)), - k: Int, -) -> iterator.Iterator(List(a)) { - case k { - 0 -> iterator.single([]) - _ -> - iterator.flat_map(arr, fn(arg) { - let #(index, element) = arg - let remaining = remove_first_by_index(arr, index) - let permutations = - do_list_permutation_without_repetitions(remaining, k - 1) - iterator.map(permutations, fn(permutation) { [element, ..permutation] }) - }) - } -} - -fn list_permutation_with_repetitions( - arr: List(a), - k: Int, -) -> iterator.Iterator(List(a)) { - let indexed_arr = list.index_map(arr, fn(x, i) { #(i, x) }) - do_list_permutation_with_repetitions(indexed_arr, k) -} - -fn do_list_permutation_with_repetitions( - arr: List(#(Int, a)), - k: Int, -) -> iterator.Iterator(List(a)) { - case k { - 0 -> iterator.single([]) - _ -> - iterator.flat_map(arr |> iterator.from_list, fn(arg) { - let #(_, element) = arg - // Allow the same element (by index) to be reused in future recursive calls - let permutations = do_list_permutation_with_repetitions(arr, k - 1) - // Prepend the current element to each generated permutation - iterator.map(permutations, fn(permutation) { [element, ..permutation] }) - }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Generate a list containing all combinations of pairs of elements coming from two given lists. -/// -///
-/// Example: -/// -/// import gleam/set -/// import gleeunit/should -/// import gleam_community/maths/combinatorics -/// -/// pub fn example () { -/// // Cartesian product of two empty sets -/// set.from_list([]) -/// |> combinatorics.cartesian_product(set.from_list([])) -/// |> should.equal(set.from_list([])) -/// -/// // Cartesian product of two sets with numeric values -/// set.from_list([1.0, 10.0]) -/// |> combinatorics.cartesian_product(set.from_list([1.0, 2.0])) -/// |> should.equal( -/// set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]), -/// ) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cartesian_product(xset: set.Set(a), yset: set.Set(b)) -> set.Set(#(a, b)) { - xset - |> set.fold(set.new(), fn(accumulator0: set.Set(#(a, b)), member0: a) { - set.fold(yset, accumulator0, fn(accumulator1: set.Set(#(a, b)), member1: b) { - set.insert(accumulator1, #(member0, member1)) - }) - }) -} diff --git a/src/gleam_community/maths/conversion.gleam b/src/gleam_community/maths/conversion.gleam deleted file mode 100644 index 1355938..0000000 --- a/src/gleam_community/maths/conversion.gleam +++ /dev/null @@ -1,185 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Conversion: A module containing various functions for converting between types and quantities. -//// -//// * **Misc. functions** -//// * [`float_to_int`](#float_to_int) -//// * [`int_to_float`](#int_to_float) -//// * [`degrees_to_radians`](#degrees_to_radians) -//// * [`radians_to_degrees`](#radians_to_degrees) -//// - -import gleam/int - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that produces a number of type `Float` from an `Int`. -/// -/// Note: The function is equivalent to the `int.to_float` function in the Gleam stdlib. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/conversion -/// -/// pub fn example() { -/// conversion.int_to_float(-1) -/// |> should.equal(-1.0) -/// -/// conversion.int_to_float(1) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_to_float(x: Int) -> Float { - int.to_float(x) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns the integral part of a given floating point value. -/// That is, everything after the decimal point of a given floating point value is discarded -/// and only the integer value before the decimal point is returned. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/conversion -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// conversion.float_to_int(12.0654) -/// |> should.equal(12) -/// -/// // Note: Making the following function call is equivalent -/// // but instead of returning a value of type 'Int' a value -/// // of type 'Float' is returned. -/// piecewise.round(12.0654, option.Some(0), option.Some(piecewise.RoundToZero)) -/// |> should.equal(Ok(12.0)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_to_int(x: Float) -> Int { - do_to_int(x) -} - -@external(erlang, "erlang", "trunc") -@external(javascript, "../../maths.mjs", "truncate") -fn do_to_int(a: Float) -> Int - -///
-/// -/// 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_community/maths/conversion -/// import gleam_community/maths/elementary -/// -/// pub fn example() { -/// conversion.degrees_to_radians(360.) -/// |> should.equal(2. *. elementary.pi()) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn degrees_to_radians(x: Float) -> Float { - x *. do_pi() /. 180.0 -} - -///
-/// -/// 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_community/maths/conversion -/// import gleam_community/maths/elementary -/// -/// pub fn example() { -/// conversion.radians_to_degrees(0.0) -/// |> should.equal(0.0) -/// -/// conversion.radians_to_degrees(2. *. elementary.pi()) -/// |> should.equal(360.) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn radians_to_degrees(x: Float) -> Float { - x *. 180.0 /. do_pi() -} - -@external(erlang, "math", "pi") -@external(javascript, "../../maths.mjs", "pi") -fn do_pi() -> Float diff --git a/src/gleam_community/maths/elementary.gleam b/src/gleam_community/maths/elementary.gleam deleted file mode 100644 index 491cab7..0000000 --- a/src/gleam_community/maths/elementary.gleam +++ /dev/null @@ -1,1192 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Elementary: A module containing a comprehensive set of foundational mathematical functions and constants. -//// -//// * **Trigonometric and hyperbolic functions** -//// * [`acos`](#acos) -//// * [`acosh`](#acosh) -//// * [`asin`](#asin) -//// * [`asinh`](#asinh) -//// * [`atan`](#atan) -//// * [`atan2`](#atan2) -//// * [`atanh`](#atanh) -//// * [`cos`](#cos) -//// * [`cosh`](#cosh) -//// * [`sin`](#sin) -//// * [`sinh`](#sinh) -//// * [`tan`](#tan) -//// * [`tanh`](#tanh) -//// * **Powers, logs and roots** -//// * [`exponential`](#exponential) -//// * [`natural_logarithm`](#natural_logarithm) -//// * [`logarithm`](#logarithm) -//// * [`logarithm_2`](#logarithm_2) -//// * [`logarithm_10`](#logarithm_10) -//// * [`power`](#power) -//// * [`square_root`](#square_root) -//// * [`cube_root`](#cube_root) -//// * [`nth_root`](#nth_root) -//// * **Mathematical constants** -//// * [`pi`](#pi) -//// * [`tau`](#tau) -//// * [`e`](#e) -//// - -import gleam/int -import gleam/option - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.acos(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.acos(1.1) -/// |> should.be_error() -/// -/// elementary.acos(-1.1) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn acos(x: Float) -> Result(Float, Nil) { - case x >=. -1.0 && x <=. 1.0 { - True -> Ok(do_acos(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "acos") -@external(javascript, "../../maths.mjs", "acos") -fn do_acos(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.acosh(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.acosh(0.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn acosh(x: Float) -> Result(Float, Nil) { - case x >=. 1.0 { - True -> Ok(do_acosh(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "acosh") -@external(javascript, "../../maths.mjs", "acosh") -fn do_acosh(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.asin(0.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.asin(1.1) -/// |> should.be_error() -/// -/// elementary.asin(-1.1) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn asin(x: Float) -> Result(Float, Nil) { - case x >=. -1.0 && x <=. 1.0 { - True -> Ok(do_asin(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "asin") -@external(javascript, "../../maths.mjs", "asin") -fn do_asin(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.asinh(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn asinh(x: Float) -> Float { - do_asinh(x) -} - -@external(erlang, "math", "asinh") -@external(javascript, "../../maths.mjs", "asinh") -fn do_asinh(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.atan(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn atan(x: Float) -> Float { - do_atan(x) -} - -@external(erlang, "math", "atan") -@external(javascript, "../../maths.mjs", "atan") -fn do_atan(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.atan2(0.0, 0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn atan2(y: Float, x: Float) -> Float { - do_atan2(y, x) -} - -@external(erlang, "math", "atan2") -@external(javascript, "../../maths.mjs", "atan2") -fn do_atan2(a: Float, b: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.atanh(0.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.atanh(1.0) -/// |> should.be_error() -/// -/// elementary.atanh(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn atanh(x: Float) -> Result(Float, Nil) { - case x >. -1.0 && x <. 1.0 { - True -> Ok(do_atanh(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "atanh") -@external(javascript, "../../maths.mjs", "atanh") -fn do_atanh(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.cos(0.0) -/// |> should.equal(1.0) -/// -/// elementary.cos(elementary.pi()) -/// |> should.equal(-1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cos(x: Float) -> Float { - do_cos(x) -} - -@external(erlang, "math", "cos") -@external(javascript, "../../maths.mjs", "cos") -fn do_cos(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.cosh(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cosh(x: Float) -> Float { - do_cosh(x) -} - -@external(erlang, "math", "cosh") -@external(javascript, "../../maths.mjs", "cosh") -fn do_cosh(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.sin(0.0) -/// |> should.equal(0.0) -/// -/// elementary.sin(0.5 *. elementary.pi()) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn sin(x: Float) -> Float { - do_sin(x) -} - -@external(erlang, "math", "sin") -@external(javascript, "../../maths.mjs", "sin") -fn do_sin(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.sinh(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn sinh(x: Float) -> Float { - do_sinh(x) -} - -@external(erlang, "math", "sinh") -@external(javascript, "../../maths.mjs", "sinh") -fn do_sinh(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.tan(0.0) -/// |> should.equal(0.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn tan(x: Float) -> Float { - do_tan(x) -} - -@external(erlang, "math", "tan") -@external(javascript, "../../maths.mjs", "tan") -fn do_tan(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example () { -/// elementary.tanh(0.0) -/// |> should.equal(0.0) -/// -/// elementary.tanh(25.0) -/// |> should.equal(1.0) -/// -/// elementary.tanh(-25.0) -/// |> should.equal(-1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn tanh(x: Float) -> Float { - do_tanh(x) -} - -@external(erlang, "math", "tanh") -@external(javascript, "../../maths.mjs", "tanh") -fn do_tanh(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The exponential function: -/// -/// \\[ -/// \forall x \in \(-\infty, \infty\), \\; e^{(x)} = y \in \(0, +\infty\) -/// \\] -/// -/// \\(e \approx 2.71828\dots\\) is Eulers' number. -/// -/// Note: If the input value \\(x\\) is too large an overflow error might occur. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam_community/maths/elementary -/// -/// pub fn example() { -/// elementary.exponential(0.0) -/// |> should.equal(1.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn exponential(x: Float) -> Float { - do_exponential(x) -} - -@external(erlang, "math", "exp") -@external(javascript, "../../maths.mjs", "exponential") -fn do_exponential(a: Float) -> Float - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example () { -/// elementary.natural_logarithm(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.natural_logarithm(elementary.e()) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.natural_logarithm(-1.0) -/// |> should.be_error() -/// } -///
-/// -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn natural_logarithm(x: Float) -> Result(Float, Nil) { - case x >. 0.0 { - True -> Ok(do_natural_logarithm(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "log") -@external(javascript, "../../maths.mjs", "logarithm") -fn do_natural_logarithm(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The base \\(b\\) logarithm function (computed through the "change of base" formula): -/// -/// \\[ -/// \forall x \in \(0, \infty\) \textnormal{ and } b > 1, \\; \log_{b}{(x)} = y \in \(-\infty, +\infty\) -/// \\] -/// -/// The function takes a number \\(x\\) in its domain \\(\(0, \infty\)\\) and a base \\(b > 1\\) -/// 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/option -/// import gleam_community/maths/elementary -/// -/// pub fn example () { -/// elementary.logarithm(1.0, option.Some(10.0)) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.logarithm(elementary.e(), option.Some(elementary.e())) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.logarithm(-1.0, option.Some(2.0)) -/// |> should.be_error() -/// } -///
-/// -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn logarithm(x: Float, base: option.Option(Float)) -> Result(Float, Nil) { - case x >. 0.0, base { - True, option.Some(a) -> - case a >. 0.0 && a != 1.0 { - False -> Error(Nil) - True -> { - // Apply the "change of base formula" - let assert Ok(numerator) = logarithm_10(x) - let assert Ok(denominator) = logarithm_10(a) - Ok(numerator /. denominator) - } - } - _, _ -> Error(Nil) - } -} - -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example () { -/// elementary.logarithm_2(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.logarithm_2(2.0) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.logarithm_2(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn logarithm_2(x: Float) -> Result(Float, Nil) { - case x >. 0.0 { - True -> Ok(do_logarithm_2(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "log2") -@external(javascript, "../../maths.mjs", "logarithm_2") -fn do_logarithm_2(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example () { -/// elementary.logarithm_10(1.0) -/// |> should.equal(Ok(0.0)) -/// -/// elementary.logarithm_10(10.0) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.logarithm_10(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn logarithm_10(x: Float) -> Result(Float, Nil) { - case x >. 0.0 { - True -> Ok(do_logarithm_10(x)) - False -> Error(Nil) - } -} - -@external(erlang, "math", "log10") -@external(javascript, "../../maths.mjs", "logarithm_10") -fn do_logarithm_10(a: Float) -> Float - -///
-/// -/// Back to top ↑ -/// -///
-/// -///
-/// -/// 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.power(2., -1.) -/// |> should.equal(Ok(0.5)) -/// -/// elementary.power(2., 2.) -/// |> should.equal(Ok(4.0)) -/// -/// elementary.power(-1., 0.5) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn power(x: Float, y: Float) -> Result(Float, Nil) { - let fractional = do_ceiling(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 -> Error(Nil) - False -> - do_power(x, y) - |> Ok - } -} - -@external(erlang, "math", "pow") -@external(javascript, "../../maths.mjs", "power") -fn do_power(a: Float, b: Float) -> Float - -@external(erlang, "math", "ceil") -@external(javascript, "../../maths.mjs", "ceiling") -fn do_ceiling(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The square root function: \\(y = \sqrt[2]{x} = x^{\frac{1}{2}}\\). -/// -/// Note that the function is not defined if: -/// 1. The input 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.square_root(1.0) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.square_root(4.0) -/// |> should.equal(Ok(2.0)) -/// -/// elementary.square_root(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn square_root(x: Float) -> Result(Float, Nil) { - // 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 -> Error(Nil) - False -> power(x, 1.0 /. 2.0) - } -} - -///
-/// -/// 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 input 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.cube_root(1.0) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.cube_root(27.0) -/// |> should.equal(Ok(3.0)) -/// -/// elementary.cube_root(-1.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cube_root(x: Float) -> Result(Float, Nil) { - // 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 -> Error(Nil) - False -> power(x, 1.0 /. 3.0) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The \\(n\\)'th root function: \\(y = \sqrt[n]{x} = x^{\frac{1}{n}}\\). -/// -/// Note that the function is not defined if: -/// 1. The input 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_community/maths/elementary -/// -/// pub fn example() { -/// elementary.nth_root(1.0, 2) -/// |> should.equal(Ok(1.0)) -/// -/// elementary.nth_root(27.0, 3) -/// |> should.equal(Ok(3.0)) -/// -/// elementary.nth_root(256.0, 4) -/// |> should.equal(Ok(4.0)) -/// -/// elementary.nth_root(-1.0, 2) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn nth_root(x: Float, n: Int) -> Result(Float, Nil) { - // 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 && n >= 1 { - True -> power(x, 1.0 /. int.to_float(n)) - False -> Error(Nil) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The mathematical constant pi: \\(\pi \approx 3.1415\dots\\) -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn pi() -> Float { - do_pi() -} - -@external(erlang, "math", "pi") -@external(javascript, "../../maths.mjs", "pi") -fn do_pi() -> Float - -///
-/// -/// 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() -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Euler's number \\(e \approx 2.71828\dots\\). -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam_community/maths/elementary -/// -/// pub fn example() { -/// // Test that the constant is approximately equal to 2.7128... -/// elementary.e() -/// |> elementary.is_close(2.7128, 0.0, 0.000001) -/// |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn e() -> Float { - exponential(1.0) -} diff --git a/src/gleam_community/maths/metrics.gleam b/src/gleam_community/maths/metrics.gleam deleted file mode 100644 index 6c8959c..0000000 --- a/src/gleam_community/maths/metrics.gleam +++ /dev/null @@ -1,1265 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Metrics: A module offering functions for calculating distances and other -//// types of metrics. -//// -//// Disclaimer: In this module, the terms "distance" and "metric" are used in -//// a broad and practical sense. That is, they are used to denote any difference -//// or discrepancy between two inputs. Consequently, they may not align with their -//// precise mathematical definitions (in particular, some "distance" functions in -//// this module do not satisfy the triangle inequality). -//// -//// * **Distance measures** -//// * [`norm`](#norm) -//// * [`manhattan_distance`](#manhattan_distance) -//// * [`euclidean_distance`](#euclidean_distance) -//// * [`chebyshev_distance`](#chebyshev_distance) -//// * [`minkowski_distance`](#minkowski_distance) -//// * [`cosine_similarity`](#cosine_similarity) -//// * [`canberra_distance`](#canberra_distance) -//// * [`braycurtis_distance`](#braycurtis_distance) -//// * **Set & string similarity measures** -//// * [`jaccard_index`](#jaccard_index) -//// * [`sorensen_dice_coefficient`](#sorensen_dice_coefficient) -//// * [`tversky_index`](#tversky_index) -//// * [`overlap_coefficient`](#overlap_coefficient) -//// * **Basic statistical measures** -//// * [`mean`](#mean) -//// * [`median`](#median) -//// * [`variance`](#variance) -//// * [`standard_deviation`](#standard_deviation) -//// - -import gleam/bool -import gleam/float -import gleam/int -import gleam/list -import gleam/option -import gleam/pair -import gleam/result -import gleam/set -import gleam_community/maths/arithmetics -import gleam_community/maths/conversion -import gleam_community/maths/elementary -import gleam_community/maths/piecewise - -/// Utility function that checks all lists have the expected length and contents -/// The function is primarily used by all distance measures taking 'List(Float)' -/// as input -fn validate_lists( - xarr: List(Float), - yarr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Bool, Nil) { - case xarr, yarr { - [], _ -> Error(Nil) - _, [] -> Error(Nil) - _, _ -> { - let xarr_length = list.length(xarr) - let yarr_length = list.length(yarr) - case xarr_length == yarr_length, weights { - False, _ -> Error(Nil) - True, option.None -> { - True - |> Ok - } - True, option.Some(warr) -> { - let warr_length = list.length(warr) - case xarr_length == warr_length { - True -> { - validate_weights(warr) - } - False -> Error(Nil) - } - } - } - } - } -} - -fn validate_weights(warr: List(Float)) -> Result(Bool, Nil) { - // Check that all the given weights are positive - let assert Ok(minimum) = piecewise.list_minimum(warr, float.compare) - case minimum >=. 0.0 { - False -> Error(Nil) - True -> Ok(True) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) \\(p\\)-norm of a list (representing a vector): -/// -/// \\[ -/// \left( \sum_{i=1}^n w_{i} \left|x_{i}\right|^{p} \right)^{\frac{1}{p}} -/// \\] -/// -/// In the formula, \\(n\\) is the length of the list and \\(x_i\\) is the value in -/// the input list indexed by \\(i\\), while \\(w_i \in \mathbb{R}_{+}\\) is -/// a corresponding positive weight (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/elementary -/// import gleam_community/maths/metrics -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// let assert Ok(tol) = elementary.power(-10.0, -6.0) -/// -/// let assert Ok(result) = -/// [1.0, 1.0, 1.0] -/// |> metrics.norm(1.0, option.None) -/// result -/// |> predicates.is_close(3.0, 0.0, tol) -/// |> should.be_true() -/// -/// let assert Ok(result) = -/// [1.0, 1.0, 1.0] -/// |> metrics.norm(-1.0, option.None) -/// result -/// |> predicates.is_close(0.3333333333333333, 0.0, tol) -/// |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn norm( - arr: List(Float), - p: Float, - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - case arr, weights { - [], _ -> - 0.0 - |> Ok - _, option.None -> { - let aggregate = - arr - |> list.fold(0.0, fn(accumulator, element) { - let assert Ok(result) = - piecewise.float_absolute_value(element) - |> elementary.power(p) - result +. accumulator - }) - let assert Ok(result) = elementary.power(aggregate, 1.0 /. p) - result - |> Ok - } - _, option.Some(warr) -> { - let arr_length = list.length(arr) - let warr_length = list.length(warr) - case arr_length == warr_length { - True -> { - case validate_weights(warr) { - Ok(_) -> { - let tuples = list.zip(arr, warr) - let aggregate = - tuples - |> list.fold(0.0, fn(accumulator, tuple) { - let first_element = pair.first(tuple) - let second_element = pair.second(tuple) - let assert Ok(result) = - elementary.power( - piecewise.float_absolute_value(first_element), - p, - ) - second_element *. result +. accumulator - }) - let assert Ok(result) = elementary.power(aggregate, 1.0 /. p) - result - |> Ok - } - Error(msg) -> - msg - |> Error - } - } - False -> Error(Nil) - } - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) Manhattan distance between two lists (representing -/// vectors): -/// -/// \\[ -/// \sum_{i=1}^n w_{i} \left|x_i - y_i \right| -/// \\] -/// -/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the -/// values in the respective input lists indexed by \\(i\\), while the -/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights -/// (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/elementary -/// import gleam_community/maths/metrics -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// let assert Ok(tol) = elementary.power(-10.0, -6.0) -/// -/// // Empty lists returns an error -/// metrics.manhattan_distance([], [], option.None) -/// |> should.be_error() -/// -/// // Differing lengths returns error -/// metrics.manhattan_distance([], [1.0], option.None) -/// |> should.be_error() -/// -/// let assert Ok(result) = -/// metrics.manhattan_distance([0.0, 0.0], [1.0, 2.0], option.None) -/// result -/// |> predicates.is_close(3.0, 0.0, tol) -/// |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn manhattan_distance( - xarr: List(Float), - yarr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - minkowski_distance(xarr, yarr, 1.0, weights) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) Minkowski distance between two lists (representing -/// vectors): -/// -/// \\[ -/// \left( \sum_{i=1}^n w_{i} \left|x_i - y_i \right|^{p} \right)^{\frac{1}{p}} -/// \\] -/// -/// In the formula, \\(p >= 1\\) is the order, \\(n\\) is the length of the two lists -/// and \\(x_i, y_i\\) are the values in the respective input lists indexed by \\(i\\). -/// The \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights -/// (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -/// The Minkowski distance is a generalization of both the Euclidean distance -/// (\\(p=2\\)) and the Manhattan distance (\\(p = 1\\)). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/elementary -/// import gleam_community/maths/metrics -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// let assert Ok(tol) = elementary.power(-10.0, -6.0) -/// -/// // Empty lists returns an error -/// metrics.minkowski_distance([], [], 1.0, option.None) -/// |> should.be_error() -/// -/// // Differing lengths returns error -/// metrics.minkowski_distance([], [1.0], 1.0, option.None) -/// |> should.be_error() -/// -/// // Test order < 1 -/// metrics.minkowski_distance([0.0, 0.0], [0.0, 0.0], -1.0, option.None) -/// |> should.be_error() -/// -/// let assert Ok(result) = -/// metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 1.0, option.None) -/// result -/// |> predicates.is_close(3.0, 0.0, tol) -/// |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn minkowski_distance( - xarr: List(Float), - yarr: List(Float), - p: Float, - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - use _ <- result.try(validate_lists(xarr, yarr, weights)) - case p <. 1.0 { - True -> Error(Nil) - False -> { - let differences: List(Float) = - list.zip(xarr, yarr) - |> list.map(fn(tuple: #(Float, Float)) -> Float { - pair.first(tuple) -. pair.second(tuple) - }) - norm(differences, p, weights) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) Euclidean distance between two lists (representing -/// vectors): -/// -/// \\[ -/// \left( \sum_{i=1}^n w_{i} \left|x_i - y_i \right|^{2} \right)^{\frac{1}{2}} -/// \\] -/// -/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the -/// values in the respective input lists indexed by \\(i\\), while the -/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights -/// (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/elementary -/// import gleam_community/maths/metrics -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// let assert Ok(tol) = elementary.power(-10.0, -6.0) -/// -/// // Empty lists returns an error -/// metrics.euclidean_distance([], [], option.None) -/// |> should.be_error() -/// -/// // Differing lengths returns an error -/// metrics.euclidean_distance([], [1.0], option.None) -/// |> should.be_error() -/// -/// let assert Ok(result) = -/// metrics.euclidean_distance([0.0, 0.0], [1.0, 2.0], option.None) -/// result -/// |> predicates.is_close(2.23606797749979, 0.0, tol) -/// |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn euclidean_distance( - xarr: List(Float), - yarr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - minkowski_distance(xarr, yarr, 2.0, weights) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the Chebyshev distance between two lists (representing vectors): -/// -/// \\[ -/// \text{max}_{i=1}^n \left|x_i - y_i \right| -/// \\] -/// -/// In the formula, \\(n\\) is the length of the two lists and \\(x_i, y_i\\) are the -/// values in the respective input lists indexed by \\(i\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/elementary -/// import gleam_community/maths/metrics -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// // Empty lists returns an error -/// metrics.chebyshev_distance([], []) -/// |> should.be_error() -/// -/// // Differing lengths returns error -/// metrics.chebyshev_distance([], [1.0]) -/// |> should.be_error() -/// -/// metrics.chebyshev_distance([-5.0, -10.0, -3.0], [-1.0, -12.0, -3.0]) -/// |> should.equal(Ok(4.0)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn chebyshev_distance( - xarr: List(Float), - yarr: List(Float), -) -> Result(Float, Nil) { - case validate_lists(xarr, yarr, option.None) { - Error(msg) -> - msg - |> Error - Ok(_) -> { - list.zip(xarr, yarr) - |> list.map(fn(tuple) { - { pair.first(tuple) -. pair.second(tuple) } - |> piecewise.float_absolute_value() - }) - |> piecewise.list_maximum(float.compare) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the arithmetic mean of the elements in a list: -/// -/// \\[ -/// \bar{x} = \frac{1}{n}\sum_{i=1}^n x_i -/// \\] -/// -/// In the formula, \\(n\\) is the sample size (the length of the list) and \\(x_i\\) -/// is the sample point in the input list indexed by \\(i\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// -/// pub fn example () { -/// // An empty list returns an error -/// [] -/// |> metrics.mean() -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [1., 2., 3.] -/// |> metrics.mean() -/// |> should.equal(Ok(2.)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn mean(arr: List(Float)) -> Result(Float, Nil) { - case arr { - [] -> Error(Nil) - _ -> - arr - |> arithmetics.float_sum(option.None) - |> fn(a) { a /. conversion.int_to_float(list.length(arr)) } - |> Ok - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the median of the elements in a list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// -/// pub fn example () { -/// // An empty list returns an error -/// [] -/// |> metrics.median() -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [1., 2., 3.] -/// |> metrics.median() -/// |> should.equal(Ok(2.)) -/// -/// [1., 2., 3., 4.] -/// |> metrics.median() -/// |> should.equal(Ok(2.5)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn median(arr: List(Float)) -> Result(Float, Nil) { - use <- bool.guard(list.is_empty(arr), Error(Nil)) - let length = list.length(arr) - let mid = length / 2 - - case length % 2 == 0 { - True -> do_median(arr, mid, True, 0) - False -> do_median(arr, mid, False, 0) - } -} - -fn do_median( - xs: List(Float), - mid: Int, - mean: Bool, - index: Int, -) -> Result(Float, Nil) { - use <- bool.guard(index > mid, Error(Nil)) - let mid_less_one = mid - 1 - - case xs { - [x, ..] if !mean && index == mid -> Ok(x) - [x, y, ..] if mean && index == mid_less_one -> Ok({ x +. y } /. 2.0) - [_, ..rest] -> do_median(rest, mid, mean, index + 1) - [] -> Error(Nil) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the sample variance of the elements in a list: -/// -/// \\[ -/// s^{2} = \frac{1}{n - d} \sum_{i=1}^{n}(x_i - \bar{x}) -/// \\] -/// -/// In the formula, \\(n\\) is the sample size (the length of the list) and \\(x_i\\) -/// is the sample point in the input list indexed by \\(i\\). -/// Furthermore, \\(\bar{x}\\) is the sample mean and \\(d\\) is the "Delta -/// Degrees of Freedom", and is by default set to \\(d = 0\\), which gives a biased -/// estimate of the sample variance. Setting \\(d = 1\\) gives an unbiased estimate. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// -/// pub fn example () { -/// // Degrees of freedom -/// let ddof = 1 -/// -/// // An empty list returns an error -/// [] -/// |> metrics.variance(ddof) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [1., 2., 3.] -/// |> metrics.variance(ddof) -/// |> should.equal(Ok(1.)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn variance(arr: List(Float), ddof: Int) -> Result(Float, Nil) { - case arr, ddof { - [], _ -> Error(Nil) - _, _ if ddof < 0 -> Error(Nil) - _, _ -> { - let assert Ok(mean) = mean(arr) - arr - |> list.map(fn(a: Float) -> Float { - let assert Ok(result) = elementary.power(a -. mean, 2.0) - result - }) - |> arithmetics.float_sum(option.None) - |> fn(a: Float) -> Float { - a - /. { - conversion.int_to_float(list.length(arr)) - -. conversion.int_to_float(ddof) - } - } - |> Ok - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the sample standard deviation of the elements in a list: -/// \\[ -/// s = \left(\frac{1}{n - d} \sum_{i=1}^{n}(x_i - \bar{x})\right)^{\frac{1}{2}} -/// \\] -/// -/// In the formula, \\(n\\) is the sample size (the length of the list) and \\(x_i\\) -/// is the sample point in the input list indexed by \\(i\\). -/// Furthermore, \\(\bar{x}\\) is the sample mean and \\(d\\) is the "Delta -/// Degrees of Freedom", and is by default set to \\(d = 0\\), which gives a biased -/// estimate of the sample standard deviation. Setting \\(d = 1\\) gives an unbiased -/// estimate. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// -/// pub fn example () { -/// // Degrees of freedom -/// let ddof = 1 -/// -/// // An empty list returns an error -/// [] -/// |> metrics.standard_deviationddof) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [1., 2., 3.] -/// |> metrics.standard_deviation(ddof) -/// |> should.equal(Ok(1.)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn standard_deviation(arr: List(Float), ddof: Int) -> Result(Float, Nil) { - case arr, ddof { - [], _ -> Error(Nil) - _, _ if ddof < 0 -> Error(Nil) - _, _ -> { - let assert Ok(variance) = variance(arr, ddof) - // The computed variance will always be positive - // So an error should never be returned - let assert Ok(stdev) = elementary.square_root(variance) - stdev - |> Ok - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The Jaccard index measures similarity between two sets of elements. -/// Mathematically, the Jaccard index is defined as: -/// -/// \\[ -/// \frac{|X \cap Y|}{|X \cup Y|} \\; \in \\; \left[0, 1\right] -/// \\] -/// -/// where: -/// -/// - \\(X\\) and \\(Y\\) are two sets being compared, -/// - \\(|X \cap Y|\\) represents the size of the intersection of the two sets -/// - \\(|X \cup Y|\\) denotes the size of the union of the two sets -/// -/// The value of the Jaccard index ranges from 0 to 1, where 0 indicates that the -/// two sets share no elements and 1 indicates that the sets are identical. The -/// Jaccard index is a special case of the [Tversky index](#tversky_index) (with -/// \\(\alpha=\beta=1\\)). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// import gleam/set -/// -/// pub fn example () { -/// let xset = set.from_list(["cat", "dog", "hippo", "monkey"]) -/// let yset = -/// set.from_list(["monkey", "rhino", "ostrich", "salmon"]) -/// metrics.jaccard_index(xset, yset) -/// |> should.equal(1.0 /. 7.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn jaccard_index(xset: set.Set(a), yset: set.Set(a)) -> Float { - let assert Ok(result) = tversky_index(xset, yset, 1.0, 1.0) - result -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The Sørensen-Dice coefficient measures the similarity between two sets of -/// elements. Mathematically, the coefficient is defined as: -/// -/// \\[ -/// \frac{2 |X \cap Y|}{|X| + |Y|} \\; \in \\; \left[0, 1\right] -/// \\] -/// -/// where: -/// - \\(X\\) and \\(Y\\) are two sets being compared -/// - \\(|X \cap Y|\\) is the size of the intersection of the two sets (i.e., the -/// number of elements common to both sets) -/// - \\(|X|\\) and \\(|Y|\\) are the sizes of the sets \\(X\\) and \\(Y\\), respectively -/// -/// The coefficient ranges from 0 to 1, where 0 indicates no similarity (the sets -/// share no elements) and 1 indicates perfect similarity (the sets are identical). -/// The higher the coefficient, the greater the similarity between the two sets. -/// The Sørensen-Dice coefficient is a special case of the -/// [Tversky index](#tversky_index) (with \\(\alpha=\beta=0.5\\)). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// import gleam/set -/// -/// pub fn example () { -/// let xset = set.from_list(["cat", "dog", "hippo", "monkey"]) -/// let yset = -/// set.from_list(["monkey", "rhino", "ostrich", "salmon", "spider"]) -/// metrics.sorensen_dice_coefficient(xset, yset) -/// |> should.equal(2.0 *. 1.0 /. { 4.0 +. 5.0 }) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn sorensen_dice_coefficient(xset: set.Set(a), yset: set.Set(a)) -> Float { - let assert Ok(result) = tversky_index(xset, yset, 0.5, 0.5) - result -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The Tversky index is a generalization of the Jaccard index and Sørensen-Dice -/// coefficient, which adds flexibility through two parameters, \\(\alpha\\) and -/// \\(\beta\\), allowing for asymmetric similarity measures between sets. The -/// Tversky index is defined as: -/// -/// \\[ -/// \frac{|X \cap Y|}{|X \cap Y| + \alpha|X - Y| + \beta|Y - X|} -/// \\] -/// -/// where: -/// -/// - \\(X\\) and \\(Y\\) are the sets being compared -/// - \\(|X - Y|\\) and \\(|Y - X|\\) are the sizes of the relative complements of -/// \\(Y\\) in \\(X\\) and \\(X\\) in \\(Y\\), respectively, -/// - \\(\alpha\\) and \\(\beta\\) are parameters that weigh the relative importance -/// of the elements unique to \\(X\\) and \\(Y\\) -/// -/// The Tversky index reduces to the Jaccard index when \\(\alpha = \beta = 1\\) and -/// to the Sørensen-Dice coefficient when \\(\alpha = \beta = 0.5\\). In general, the -/// Tversky index can take on any non-negative value, including 0. The index equals -/// 0 when there is no intersection between the two sets, indicating no similarity. -/// However, unlike similarity measures bounded strictly between 0 and 1, the -/// Tversky index does not have a strict upper limit of 1 when \\(\alpha \neq \beta\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// import gleam/set -/// -/// pub fn example () { -/// let yset = set.from_list(["cat", "dog", "hippo", "monkey"]) -/// let xset = -/// set.from_list(["monkey", "rhino", "ostrich", "salmon"]) -/// // Test Jaccard index (alpha = beta = 1) -/// metrics.tversky_index(xset, yset, 1.0, 1.0) -/// |> should.equal(1.0 /. 7.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn tversky_index( - xset: set.Set(a), - yset: set.Set(a), - alpha: Float, - beta: Float, -) -> Result(Float, Nil) { - case alpha >=. 0.0, beta >=. 0.0 { - True, True -> { - let intersection = - set.intersection(xset, yset) - |> set.size() - |> conversion.int_to_float() - let difference1 = - set.difference(xset, yset) - |> set.size() - |> conversion.int_to_float() - let difference2 = - set.difference(yset, xset) - |> set.size() - |> conversion.int_to_float() - intersection - /. { intersection +. alpha *. difference1 +. beta *. difference2 } - |> Ok - } - _, _ -> Error(Nil) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The Overlap coefficient, also known as the Szymkiewicz–Simpson coefficient, is -/// a measure of similarity between two sets that focuses on the size of the -/// intersection relative to the smaller of the two sets. It is defined -/// mathematically as: -/// -/// \\[ -/// \frac{|X \cap Y|}{\min(|X|, |Y|)} \\; \in \\; \left[0, 1\right] -/// \\] -/// -/// where: -/// -/// - \\(X\\) and \\(Y\\) are the sets being compared -/// - \\(|X \cap Y|\\) is the size of the intersection of the sets -/// - \\(\min(|X|, |Y|)\\) is the size of the smaller set among \\(X\\) and \\(Y\\) -/// -/// The coefficient ranges from 0 to 1, where 0 indicates no overlap and 1 -/// indicates that the smaller set is a suyset of the larger set. This -/// measure is especially useful in situations where the similarity in terms -/// of the proportion of overlap is more relevant than the difference in sizes -/// between the two sets. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/metrics -/// import gleam/set -/// -/// pub fn example () { -/// let set_a = -/// set.from_list(["horse", "dog", "hippo", "monkey", "bird"]) -/// let set_b = -/// set.from_list(["monkey", "bird", "ostrich", "salmon"]) -/// metrics.overlap_coefficient(set_a, set_b) -/// |> should.equal(2.0 /. 4.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn overlap_coefficient(xset: set.Set(a), yset: set.Set(a)) -> Float { - let intersection = - set.intersection(xset, yset) - |> set.size() - |> conversion.int_to_float() - let minsize = - piecewise.minimum(set.size(xset), set.size(yset), int.compare) - |> conversion.int_to_float() - intersection /. minsize -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) cosine similarity between two lists (representing -/// vectors): -/// -/// \\[ -/// \frac{\sum_{i=1}^n w_{i} \cdot x_i \cdot y_i} -/// {\left(\sum_{i=1}^n w_{i} \cdot x_i^2\right)^{\frac{1}{2}} -/// \cdot -/// \left(\sum_{i=1}^n w_{i} \cdot y_i^2\right)^{\frac{1}{2}}} -/// \\; \in \\; \left[-1, 1\right] -/// \\] -/// -/// In the formula, \\(n\\) is the length of the two lists and \\(x_i\\), \\(y_i\\) are -/// the values in the respective input lists indexed by \\(i\\), while the -/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights -/// (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -/// The cosine similarity provides a value between -1 and 1, where 1 means the -/// vectors are in the same direction, -1 means they are in exactly opposite -/// directions, and 0 indicates orthogonality. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/metrics -/// -/// pub fn example() { -/// // Two orthogonal vectors -/// metrics.cosine_similarity([-1.0, 1.0, 0.0], [1.0, 1.0, -1.0], option.None) -/// |> should.equal(Ok(0.0)) -/// -/// // Two identical (parallel) vectors -/// metrics.cosine_similarity([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], option.None) -/// |> should.equal(Ok(1.0)) -/// -/// // Two parallel, but oppositely oriented vectors -/// metrics.cosine_similarity([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0], option.None) -/// |> should.equal(Ok(-1.0)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn cosine_similarity( - xarr: List(Float), - yarr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - case validate_lists(xarr, yarr, weights) { - Error(msg) -> - msg - |> Error - Ok(_) -> { - let zipped_arr = list.zip(xarr, yarr) - - let numerator_elements = - zipped_arr - |> list.map(fn(tuple) { pair.first(tuple) *. pair.second(tuple) }) - - case weights { - option.None -> { - let numerator = - numerator_elements - |> arithmetics.float_sum(option.None) - - let assert Ok(xarr_norm) = norm(xarr, 2.0, option.None) - let assert Ok(yarr_norm) = norm(yarr, 2.0, option.None) - let denominator = { - xarr_norm *. yarr_norm - } - numerator /. denominator - |> Ok - } - _ -> { - let numerator = - numerator_elements - |> arithmetics.float_sum(weights) - - let assert Ok(xarr_norm) = norm(xarr, 2.0, weights) - let assert Ok(yarr_norm) = norm(yarr, 2.0, weights) - let denominator = { - xarr_norm *. yarr_norm - } - numerator /. denominator - |> Ok - } - } - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) Canberra distance between two lists: -/// -/// \\[ -/// \sum_{i=1}^n w_{i}\frac{\left| x_i - y_i \right|} -/// {\left| x_i \right| + \left| y_i \right|} -/// \\] -/// -/// In the formula, \\(n\\) is the length of the two lists, and \\(x_i, y_i\\) are the -/// values in the respective input lists indexed by \\(i\\), while the -/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights -/// (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/metrics -/// -/// pub fn example() { -/// // Empty lists returns an error -/// metrics.canberra_distance([], [], option.None) -/// |> should.be_error() -/// -/// // Different sized lists returns an error -/// metrics.canberra_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) -/// |> should.be_error() -/// -/// // Valid inputs -/// metrics.canberra_distance([1.0, 2.0], [-2.0, -1.0], option.None) -/// |> should.equal(Ok(2.0)) -/// -/// metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([1.0, 0.5])) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn canberra_distance( - xarr: List(Float), - yarr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - case validate_lists(xarr, yarr, weights) { - Error(msg) -> - msg - |> Error - Ok(_) -> { - let arr = - list.zip(xarr, yarr) - |> list.map(canberra_distance_helper) - - case weights { - option.None -> { - arr - |> arithmetics.float_sum(option.None) - |> Ok - } - _ -> { - arr - |> arithmetics.float_sum(weights) - |> Ok - } - } - } - } -} - -fn canberra_distance_helper(tuple: #(Float, Float)) -> Float { - let numerator = - piecewise.float_absolute_value({ pair.first(tuple) -. pair.second(tuple) }) - let denominator = { - piecewise.float_absolute_value(pair.first(tuple)) - +. piecewise.float_absolute_value(pair.second(tuple)) - } - numerator /. denominator -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Calculate the (weighted) Bray-Curtis distance between two lists: -/// -/// \\[ -/// \frac{\sum_{i=1}^n w_{i} \left| x_i - y_i \right|} -/// {\sum_{i=1}^n w_{i}\left| x_i + y_i \right|} -/// \\] -/// -/// In the formula, \\(n\\) is the length of the two lists, and \\(x_i, y_i\\) are the values -/// in the respective input lists indexed by \\(i\\), while the -/// \\(w_i \in \mathbb{R}_{+}\\) are corresponding positive weights -/// (\\(w_i = 1.0\\;\forall i=1...n\\) by default). -/// -/// The Bray-Curtis distance is in the range \\([0, 1]\\) if all entries \\(x_i, y_i\\) are -/// positive. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/metrics -/// -/// pub fn example() { -/// // Empty lists returns an error -/// metrics.braycurtis_distance([], [], option.None) -/// |> should.be_error() -/// -/// // Different sized lists returns an error -/// metrics.braycurtis_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) -/// |> should.be_error() -/// -/// // Valid inputs -/// metrics.braycurtis_distance([1.0, 0.0], [0.0, 2.0], option.None) -/// |> should.equal(Ok(1.0)) -/// -/// metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([0.5, 1.0])) -/// |> should.equal(Ok(0.375)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -/// -pub fn braycurtis_distance( - xarr: List(Float), - yarr: List(Float), - weights: option.Option(List(Float)), -) -> Result(Float, Nil) { - case validate_lists(xarr, yarr, weights) { - Error(msg) -> - msg - |> Error - Ok(_) -> { - let zipped_arr = list.zip(xarr, yarr) - let numerator_elements = - zipped_arr - |> list.map(fn(tuple) { - piecewise.float_absolute_value({ - pair.first(tuple) -. pair.second(tuple) - }) - }) - let denominator_elements = - zipped_arr - |> list.map(fn(tuple) { - piecewise.float_absolute_value({ - pair.first(tuple) +. pair.second(tuple) - }) - }) - - case weights { - option.None -> { - let numerator = - numerator_elements - |> arithmetics.float_sum(option.None) - let denominator = - denominator_elements - |> arithmetics.float_sum(option.None) - { numerator /. denominator } - |> Ok - } - _ -> { - let numerator = - numerator_elements - |> arithmetics.float_sum(weights) - let denominator = - denominator_elements - |> arithmetics.float_sum(weights) - { numerator /. denominator } - |> Ok - } - } - } - } -} diff --git a/src/gleam_community/maths/piecewise.gleam b/src/gleam_community/maths/piecewise.gleam deleted file mode 100644 index cebdc38..0000000 --- a/src/gleam_community/maths/piecewise.gleam +++ /dev/null @@ -1,1215 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Piecewise: A module containing functions that have different definitions depending on conditions or intervals of their domain. -//// -//// * **Rounding functions** -//// * [`ceiling`](#ceiling) -//// * [`floor`](#floor) -//// * [`truncate`](#truncate) -//// * [`round`](#round) -//// * **Sign and absolute value functions** -//// * [`float_absolute_value`](#float_absolute_value) -//// * [`int_absolute_value`](#int_absolute_value) -//// * [`float_absolute_difference`](#float_absolute_difference) -//// * [`int_absolute_difference`](#int_absolute_difference) -//// * [`float_sign`](#float_sign) -//// * [`int_sign`](#int_sign) -//// * [`float_copy_sign`](#float_copy_sign) -//// * [`int_copy_sign`](#float_copy_sign) -//// * [`float_flip_sign`](#float_flip_sign) -//// * [`int_flip_sign`](#int_flip_sign) -//// * **Misc. mathematical functions** -//// * [`minimum`](#minimum) -//// * [`maximum`](#maximum) -//// * [`minmax`](#minmax) -//// * [`list_minimum`](#list_minimum) -//// * [`list_maximum`](#list_maximum) -//// * [`extrema`](#extrema) -//// * [`arg_minimum`](#arg_minimum) -//// * [`arg_maximum`](#arg_maximum) -//// - -import gleam/int -import gleam/list -import gleam/option -import gleam/order -import gleam/pair -import gleam_community/maths/conversion -import gleam_community/maths/elementary - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The ceiling function rounds a given input value \\(x \in \mathbb{R}\\) to the nearest integer -/// value (at the specified digit) that is larger than or equal to the input \\(x\\). -/// -/// Note: The ceiling function is used as an alias for the rounding function [`round`](#round) -/// with rounding mode `RoundUp`. -/// -///
-/// Details -/// -/// For example, \\(12.0654\\) is rounded to: -/// - \\(13.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) -/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.066\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative number refers to the digits before the decimal point. -/// For example, \\(12.0654\\) is rounded to: -/// - \\(20.0\\) for 1 digit places before the decimal point (`digit = -1`) -/// - \\(100.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(1000.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -///
-/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.ceiling(12.0654, option.Some(1)) -/// |> should.equal(12.1) -/// -/// piecewise.ceiling(12.0654, option.Some(2)) -/// |> should.equal(12.07) -/// -/// piecewise.ceiling(12.0654, option.Some(3)) -/// |> should.equal(12.066) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn ceiling(x: Float, digits: option.Option(Int)) -> Float { - round(x, digits, option.Some(RoundUp)) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The floor function rounds input \\(x \in \mathbb{R}\\) to the nearest integer value (at the -/// specified digit) that is less than or equal to the input \\(x\\). -/// -/// Note: The floor function is used as an alias for the rounding function [`round`](#round) -/// with rounding mode `RoundDown`. -/// -///
-/// Details -/// -/// For example, \\(12.0654\\) is rounded to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.0\\) for 1 digits after the decimal point (`digits = 1`) -/// - \\(12.06\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -///
-/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.floor(12.0654, option.Some(1)) -/// |> should.equal(12.0) -/// -/// piecewise.floor(12.0654, option.Some(2)) -/// |> should.equal(12.06) -/// -/// piecewise.floor(12.0654, option.Some(3)) -/// |> should.equal(12.065) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn floor(x: Float, digits: option.Option(Int)) -> Float { - round(x, digits, option.Some(RoundDown)) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The truncate function rounds a given input \\(x \in \mathbb{R}\\) to the nearest integer -/// value (at the specified digit) that is less than or equal to the absolute value of the -/// input \\(x\\). -/// -/// Note: The truncate function is used as an alias for the rounding function [`round`](#round) -/// with rounding mode `RoundToZero`. -/// -///
-/// Details -/// -/// For example, \\(12.0654\\) is rounded to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.0\\) for 1 digits after the decimal point (`digits = 1`) -/// - \\(12.06\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -///
-/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.truncate(12.0654, option.Some(1)) -/// |> should.equal(12.0) -/// -/// piecewise.truncate(12.0654, option.Some(2)) -/// |> should.equal(12.06) -/// -/// piecewise.truncate(12.0654, option.Some(3)) -/// |> should.equal(12.065) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn truncate(x: Float, digits: option.Option(Int)) -> Float { - round(x, digits, option.Some(RoundToZero)) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function rounds a float to a specific number of digits (after the decimal place or before -/// if negative) using a specified rounding mode. -/// -/// Valid rounding modes include: -/// - `RoundNearest` (default): The input \\(x\\) is rounded to the nearest integer value (at the -/// specified digit) with ties (fractional values of 0.5) being rounded to the nearest even -/// integer. -/// - `RoundTiesAway`: The input \\(x\\) is rounded to the nearest integer value (at the -/// specified digit) with ties (fractional values of 0.5) being rounded away from zero (C/C++ -/// rounding behavior). -/// - `RoundTiesUp`: The input \\(x\\) is rounded to the nearest integer value (at the specified -/// digit) with ties (fractional values of 0.5) being rounded towards \\(+\infty\\) -/// (Java/JavaScript rounding behaviour). -/// - `RoundToZero`: The input \\(x\\) is rounded to the nearest integer value (at the specified -/// digit) that is less than or equal to the absolute value of the input \\(x\\). An alias for -/// this rounding mode is [`truncate`](#truncate). -/// - `RoundDown`: The input \\(x\\) is rounded to the nearest integer value (at the specified -/// digit) that is less than or equal to the input \\(x\\). An alias for this rounding mode is -/// [`floor`](#floor). -/// - `RoundUp`: The input \\(x\\) is rounded to the nearest integer value (at the specified -/// digit) that is larger than or equal to the input \\(x\\). An alias for this rounding mode -/// is [`ceiling`](#ceiling). -/// -///
-/// Details -/// -/// The `RoundNearest` rounding mode, rounds \\(12.0654\\) to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) -/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -/// The `RoundTiesAway` rounding mode, rounds \\(12.0654\\) to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) -/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -/// The `RoundTiesUp` rounding mode, rounds \\(12.0654\\) to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.1\\) for 1 digits after the decimal point (`digits = 1`) -/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -/// The `RoundToZero` rounding mode, rounds \\(12.0654\\) to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.0\\) for 1 digit after the decimal point (`digits = 1`) -/// - \\(12.06\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -/// The `RoundDown` rounding mode, rounds \\(12.0654\\) to: -/// - \\(12.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.0\\) for 1 digits after the decimal point (`digits = 1`) -/// - \\(12.06\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.065\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(10.0\\) for 1 digit before the decimal point (`digits = -1`) -/// - \\(0.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(0.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -/// The `RoundUp` rounding mode, rounds \\(12.0654\\) to: -/// - \\(13.0\\) for 0 digits after the decimal point (`digits = 0`) -/// - \\(12.1\\) for 1 digit after the decimal point (`digits = 1`) -/// - \\(12.07\\) for 2 digits after the decimal point (`digits = 2`) -/// - \\(12.066\\) for 3 digits after the decimal point (`digits = 3`) -/// -/// It is also possible to specify a negative number of digits. In that case, the negative -/// number refers to the digits before the decimal point. -/// - \\(20.0\\) for 1 digit places before the decimal point (`digit = -1`) -/// - \\(100.0\\) for 2 digits before the decimal point (`digits = -2`) -/// - \\(1000.0\\) for 3 digits before the decimal point (`digits = -3`) -/// -///
-/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/option -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// // The default number of digits is 0 if None is provided -/// piecewise.round(12.0654, option.None, option.Some(piecewise.RoundNearest)) -/// |> should.equal(12.0) -/// -/// // The default rounding mode is "RoundNearest" if None is provided -/// piecewise.round(12.0654, option.None, option.None) -/// |> should.equal(12.0) -/// -/// // Try different rounding modes -/// piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundNearest)) -/// |> should.equal(12.07) -/// -/// piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundTiesAway)) -/// |> should.equal(12.07) -/// -/// piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundTiesUp)) -/// |> should.equal(12.07) -/// -/// piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundToZero)) -/// |> should.equal(12.06) -/// -/// piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundDown)) -/// |> should.equal(12.06) -/// -/// piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundUp)) -/// |> should.equal(12.07) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn round( - x: Float, - digits: option.Option(Int), - mode: option.Option(RoundingMode), -) -> Float { - case digits { - option.Some(a) -> { - let assert Ok(p) = elementary.power(10.0, conversion.int_to_float(a)) - // Round the given input x using at the specified digit - do_round(p, x, mode) - } - // Round the given input x using at the default digit - option.None -> do_round(1.0, x, mode) - } -} - -pub type RoundingMode { - RoundNearest - RoundTiesAway - RoundTiesUp - RoundToZero - RoundDown - RoundUp -} - -fn do_round(p: Float, x: Float, mode: option.Option(RoundingMode)) -> Float { - case mode { - // Determine the rounding mode - option.Some(RoundNearest) -> round_to_nearest(p, x) - option.Some(RoundTiesAway) -> round_ties_away(p, x) - option.Some(RoundTiesUp) -> round_ties_up(p, x) - option.Some(RoundToZero) -> round_to_zero(p, x) - option.Some(RoundDown) -> round_down(p, x) - option.Some(RoundUp) -> round_up(p, x) - // Otherwise, use the default rounding mode - option.None -> round_to_nearest(p, x) - } -} - -fn round_to_nearest(p: Float, x: Float) -> Float { - let xabs = float_absolute_value(x) *. p - let xabs_truncated = truncate_float(xabs) - let remainder = xabs -. xabs_truncated - case remainder { - _ if remainder >. 0.5 -> float_sign(x) *. truncate_float(xabs +. 1.0) /. p - _ if remainder == 0.5 -> { - let assert Ok(is_even) = int.modulo(conversion.float_to_int(xabs), 2) - case is_even == 0 { - True -> float_sign(x) *. xabs_truncated /. p - False -> float_sign(x) *. truncate_float(xabs +. 1.0) /. p - } - } - _ -> float_sign(x) *. xabs_truncated /. p - } -} - -fn round_ties_away(p: Float, x: Float) -> Float { - let xabs = float_absolute_value(x) *. p - let remainder = xabs -. truncate_float(xabs) - case remainder { - _ if remainder >=. 0.5 -> float_sign(x) *. truncate_float(xabs +. 1.0) /. p - _ -> float_sign(x) *. truncate_float(xabs) /. p - } -} - -fn round_ties_up(p: Float, x: Float) -> Float { - let xabs = float_absolute_value(x) *. p - let xabs_truncated = truncate_float(xabs) - let remainder = xabs -. xabs_truncated - case remainder { - _ if remainder >=. 0.5 && x >=. 0.0 -> - float_sign(x) *. truncate_float(xabs +. 1.0) /. p - _ -> float_sign(x) *. xabs_truncated /. p - } -} - -// Rounding mode: ToZero / Truncate -fn round_to_zero(p: Float, x: Float) -> Float { - truncate_float(x *. p) /. p -} - -fn truncate_float(x: Float) -> Float { - do_truncate_float(x) -} - -@external(erlang, "erlang", "trunc") -@external(javascript, "../../maths.mjs", "truncate") -fn do_truncate_float(a: Float) -> Float - -// Rounding mode: Down / Floor -fn round_down(p: Float, x: Float) -> Float { - do_floor(x *. p) /. p -} - -@external(erlang, "math", "floor") -@external(javascript, "../../maths.mjs", "floor") -fn do_floor(a: Float) -> Float - -// Rounding mode: Up / Ceiling -fn round_up(p: Float, x: Float) -> Float { - do_ceiling(x *. p) /. p -} - -@external(erlang, "math", "ceil") -@external(javascript, "../../maths.mjs", "ceiling") -fn do_ceiling(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The absolute value: -/// -/// \\[ -/// \forall x \in \mathbb{R}, \\; |x| \in \mathbb{R}_{+}. -/// \\] -/// -/// The function takes an input \\(x\\) and returns a positive float value. -/// -/// -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_absolute_value(x: Float) -> Float { - case x >. 0.0 { - True -> x - False -> -1.0 *. x - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The absolute value: -/// -/// \\[ -/// \forall x \in \mathbb{Z}, \\; |x| \in \mathbb{Z}_{+}. -/// \\] -/// -/// The function takes an input \\(x\\) and returns a positive integer value. -/// -/// -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_absolute_value(x: Int) -> Int { - case x > 0 { - True -> x - False -> -1 * x - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The absolute difference: -/// -/// \\[ -/// \forall x, y \in \mathbb{R}, \\; |x - y| \in \mathbb{R}_{+}. -/// \\] -/// -/// The function takes two inputs \\(x\\) and \\(y\\) and returns a positive float -/// value which is the the absolute difference of the inputs. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.float_absolute_difference(-10.0, 10.0) -/// |> should.equal(20.0) -/// -/// piecewise.float_absolute_difference(0.0, -2.0) -/// |> should.equal(2.0) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_absolute_difference(a: Float, b: Float) -> Float { - a -. b - |> float_absolute_value() -} - -///
-/// -/// 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_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.absolute_difference(-10, 10) -/// |> should.equal(20) -/// -/// piecewise.absolute_difference(0, -2) -/// |> should.equal(2) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_absolute_difference(a: Int, b: Int) -> Int { - a - b - |> int_absolute_value() -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function takes an input \\(x \in \mathbb{R}\\) and returns the sign of -/// the input, indicating whether it is positive (+1.0), negative (-1.0), or -/// zero (0.0). -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_sign(x: Float) -> Float { - do_float_sign(x) -} - -@target(erlang) -fn do_float_sign(x: Float) -> Float { - case x <. 0.0 { - True -> -1.0 - False -> - case x == 0.0 { - True -> 0.0 - False -> 1.0 - } - } -} - -@target(javascript) -@external(javascript, "../../maths.mjs", "sign") -fn do_float_sign(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function takes an input \\(x \in \mathbb{Z}\\) and returns the sign of -/// the input, indicating whether it is positive (+1), negative (-1), or zero -/// (0). -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_sign(x: Int) -> Int { - do_int_sign(x) -} - -@target(erlang) -fn do_int_sign(x: Int) -> Int { - case x < 0 { - True -> -1 - False -> - case x == 0 { - True -> 0 - False -> 1 - } - } -} - -@target(javascript) -@external(javascript, "../../maths.mjs", "sign") -fn do_int_sign(a: Int) -> Int - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function takes two arguments \\(x, y \in \mathbb{R}\\) and returns \\(x\\) -/// such that it has the same sign as \\(y\\). -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_copy_sign(x: Float, y: Float) -> Float { - case float_sign(x) == float_sign(y) { - // x and y have the same sign, just return x - True -> x - // x and y have different signs: - // - x is positive and y is negative, then flip sign of x - // - x is negative and y is positive, then flip sign of x - False -> float_flip_sign(x) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function takes two arguments \\(x, y \in \mathbb{Z}\\) and returns \\(x\\) -/// such that it has the same sign as \\(y\\). -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_copy_sign(x: Int, y: Int) -> Int { - case int_sign(x) == int_sign(y) { - // x and y have the same sign, just return x - True -> x - // x and y have different signs: - // - x is positive and y is negative, then flip sign of x - // - x is negative and y is positive, then flip sign of x - False -> int_flip_sign(x) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function flips the sign of a given input value \\(x \in \mathbb{R}\\). -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn float_flip_sign(x: Float) -> Float { - -1.0 *. x -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function flips the sign of a given input value \\(x \in \mathbb{Z}\\). -/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn int_flip_sign(x: Int) -> Int { - -1 * x -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The minimum function takes two arguments \\(x, y\\) along with a function -/// for comparing \\(x, y\\). The function returns the smallest of the two given -/// values. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.minimum(2.0, 1.5, float.compare) -/// |> should.equal(1.5) -/// -/// piecewise.minimum(1.5, 2.0, float.compare) -/// |> should.equal(1.5) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn minimum(x: a, y: a, compare: fn(a, a) -> order.Order) { - case compare(x, y) { - order.Lt -> x - order.Eq -> x - order.Gt -> y - } -} - -///
-/// -/// Back to top ↑ -/// -///
-/// -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The maximum function takes two arguments \\(x, y\\) along with a function -/// for comparing \\(x, y\\). The function returns the largest of the two given -/// values. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.maximum(2.0, 1.5, float.compare) -/// |> should.equal(1.5) -/// -/// piecewise.maximum(1.5, 2.0, float.compare) -/// |> should.equal(1.5) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn maximum(x: a, y: a, compare: fn(a, a) -> order.Order) { - case compare(x, y) { - order.Lt -> y - order.Eq -> y - order.Gt -> x - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The minmax function takes two arguments \\(x, y\\) along with a function -/// for comparing \\(x, y\\). The function returns a tuple with the smallest -/// value first and largest second. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example() { -/// piecewise.minmax(2.0, 1.5, float.compare) -/// |> should.equal(#(1.5, 2.0)) -/// -/// piecewise.minmax(1.5, 2.0, float.compare) -/// |> should.equal(#(1.5, 2.0)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn minmax(x: a, y: a, compare: fn(a, a) -> order.Order) { - #(minimum(x, y, compare), maximum(x, y, compare)) -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Returns the minimum value of a given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/int -/// import gleam_community/maths/piecewise -/// -/// pub fn example () { -/// // An empty lists returns an error -/// [] -/// |> piecewise.list_minimum(int.compare) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [4, 4, 3, 2, 1] -/// |> piecewise.list_minimum(int.compare) -/// |> should.equal(Ok(1)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-pub fn list_minimum( - arr: List(a), - compare: fn(a, a) -> order.Order, -) -> Result(a, Nil) { - case arr { - [] -> Error(Nil) - [x, ..rest] -> - Ok( - list.fold(rest, x, fn(acc, element) { - case compare(element, acc) { - order.Lt -> element - _ -> acc - } - }), - ) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Returns the maximum value of a given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example () { -/// // An empty lists returns an error -/// [] -/// |> piecewise.list_maximum(float.compare) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [4.0, 4.0, 3.0, 2.0, 1.0] -/// |> piecewise.list_maximum(float.compare) -/// |> should.equal(Ok(4.0)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn list_maximum( - arr: List(a), - compare: fn(a, a) -> order.Order, -) -> Result(a, Nil) { - case arr { - [] -> Error(Nil) - [x, ..rest] -> - Ok( - list.fold(rest, x, fn(acc, element) { - case compare(acc, element) { - order.Lt -> element - _ -> acc - } - }), - ) - } -} - -///
-/// -/// Back to top ↑ -/// -///
-/// -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Returns the indices of the minimum values in a given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example () { -/// // An empty lists returns an error -/// [] -/// |> piecewise.arg_minimum(float.compare) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [4.0, 4.0, 3.0, 2.0, 1.0] -/// |> piecewise.arg_minimum(float.compare) -/// |> should.equal(Ok([4])) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn arg_minimum( - arr: List(a), - compare: fn(a, a) -> order.Order, -) -> Result(List(Int), Nil) { - case arr { - [] -> Error(Nil) - _ -> { - let assert Ok(min) = - arr - |> list_minimum(compare) - arr - |> list.index_map(fn(element, index) { - case compare(element, min) { - order.Eq -> index - _ -> -1 - } - }) - |> list.filter(fn(index) { - case index { - -1 -> False - _ -> True - } - }) - |> Ok - } - } -} - -///
-/// -/// Back to top ↑ -/// -///
-/// -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Returns the indices of the maximum values in a given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example () { -/// // An empty lists returns an error -/// [] -/// |> piecewise.arg_maximum(float.compare) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [4.0, 4.0, 3.0, 2.0, 1.0] -/// |> piecewise.arg_maximum(float.compare) -/// |> should.equal(Ok([0, 1])) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn arg_maximum( - arr: List(a), - compare: fn(a, a) -> order.Order, -) -> Result(List(Int), Nil) { - case arr { - [] -> Error(Nil) - _ -> { - let assert Ok(max) = - arr - |> list_maximum(compare) - arr - |> list.index_map(fn(element, index) { - case compare(element, max) { - order.Eq -> index - _ -> -1 - } - }) - |> list.filter(fn(index) { - case index { - -1 -> False - _ -> True - } - }) - |> Ok - } - } -} - -///
-/// -/// Back to top ↑ -/// -///
-/// -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Returns a tuple consisting of the minimum and maximum values of a given list. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam/float -/// import gleam_community/maths/piecewise -/// -/// pub fn example () { -/// // An empty lists returns an error -/// [] -/// |> piecewise.extrema(float.compare) -/// |> should.be_error() -/// -/// // Valid input returns a result -/// [4.0, 4.0, 3.0, 2.0, 1.0] -/// |> piecewise.extrema(float.compare) -/// |> should.equal(Ok(#(1.0, 4.0))) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn extrema( - arr: List(a), - compare: fn(a, a) -> order.Order, -) -> Result(#(a, a), Nil) { - case arr { - [] -> Error(Nil) - [x, ..rest] -> - Ok( - list.fold(rest, #(x, x), fn(acc, element) { - let first = pair.first(acc) - let second = pair.second(acc) - case compare(element, first), compare(second, element) { - order.Lt, order.Lt -> #(element, element) - order.Lt, _ -> #(element, second) - _, order.Lt -> #(first, element) - _, _ -> #(first, second) - } - }), - ) - } -} diff --git a/src/gleam_community/maths/predicates.gleam b/src/gleam_community/maths/predicates.gleam deleted file mode 100644 index 73e5743..0000000 --- a/src/gleam_community/maths/predicates.gleam +++ /dev/null @@ -1,590 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Predicates: A module containing functions for testing various mathematical -//// properties of numbers. -//// -//// * **Tests** -//// * [`is_close`](#is_close) -//// * [`list_all_close`](#all_close) -//// * [`is_fractional`](#is_fractional) -//// * [`is_between`](#is_between) -//// * [`is_power`](#is_power) -//// * [`is_perfect`](#is_perfect) -//// * [`is_even`](#is_even) -//// * [`is_odd`](#is_odd) -//// * [`is_divisible`](#is_divisible) -//// * [`is_multiple`](#is_multiple) -//// * [`is_prime`](#is_prime) -//// - -import gleam/int -import gleam/list -import gleam/option -import gleam/pair -import gleam_community/maths/arithmetics -import gleam_community/maths/elementary -import gleam_community/maths/piecewise - -///
-/// -/// 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_community/maths/predicates -/// -/// pub fn example () { -/// let val = 99. -/// let ref_val = 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 = 0.01 -/// let atol = 0.10 -/// floatx.is_close(val, ref_val, rtol, atol) -/// |> should.be_true() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_close(a: Float, b: Float, rtol: Float, atol: Float) -> Bool { - let x = float_absolute_difference(a, b) - let y = atol +. rtol *. float_absolute_value(b) - case x <=. y { - True -> True - False -> False - } -} - -fn float_absolute_value(x: Float) -> Float { - case x >. 0.0 { - True -> x - False -> -1.0 *. x - } -} - -fn float_absolute_difference(a: Float, b: Float) -> Float { - a -. b - |> float_absolute_value() -} - -///
-/// -/// 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/list -/// import gleam_community/maths/predicates -/// -/// pub fn example () { -/// let val = 99. -/// let ref_val = 100. -/// let xarr = list.repeat(val, 42) -/// let yarr = 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 = 0.01 -/// let atol = 0.10 -/// predicates.all_close(xarr, yarr, rtol, atol) -/// |> fn(zarr: Result(List(Bool), Nil)) -> Result(Bool, Nil) { -/// case zarr { -/// Ok(arr) -> -/// arr -/// |> list.all(fn(a) { a }) -/// |> Ok -/// _ -> Nil |> Error -/// } -/// } -/// |> should.equal(Ok(True)) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn all_close( - xarr: List(Float), - yarr: List(Float), - rtol: Float, - atol: Float, -) -> Result(List(Bool), Nil) { - let xlen = list.length(xarr) - let ylen = list.length(yarr) - case xlen == ylen { - False -> Error(Nil) - True -> - list.zip(xarr, yarr) - |> list.map(fn(z) { is_close(pair.first(z), pair.second(z), rtol, atol) }) - |> Ok - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// Determine if a given value is fractional. -/// -/// `True` is returned if the given value is fractional, otherwise `False` is -/// returned. -/// -///
-/// Example -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example () { -/// predicates.is_fractional(0.3333) -/// |> should.equal(True) -/// -/// predicates.is_fractional(1.0) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_fractional(x: Float) -> Bool { - do_ceiling(x) -. x >. 0.0 -} - -@external(erlang, "math", "ceil") -@external(javascript, "../../maths.mjs", "ceiling") -fn do_ceiling(a: Float) -> Float - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer value \\(x \in \mathbb{Z}\\) is a -/// power of another integer value \\(y \in \mathbb{Z}\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// // Check if 4 is a power of 2 (it is) -/// predicates.is_power(4, 2) -/// |> should.equal(True) -/// -/// // Check if 5 is a power of 2 (it is not) -/// predicates.is_power(5, 2) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_power(x: Int, y: Int) -> Bool { - let assert Ok(value) = - elementary.logarithm(int.to_float(x), option.Some(int.to_float(y))) - let truncated = piecewise.truncate(value, option.Some(0)) - let remainder = value -. truncated - remainder == 0.0 -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer value \\(n \in \mathbb{Z}\\) is a -/// perfect number. A number is perfect if it is equal to the sum of its proper -/// positive divisors. -/// -///
-/// Details -/// -/// For example: -/// - \\(6\\) is a perfect number since the divisors of 6 are \\(1 + 2 + 3 = 6\\). -/// - \\(28\\) is a perfect number since the divisors of 28 are \\(1 + 2 + 4 + 7 + 14 = 28\\). -/// -///
-/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_perfect(6) -/// |> should.equal(True) -/// -/// predicates.is_perfect(28) -/// |> should.equal(True) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_perfect(n: Int) -> Bool { - do_sum(arithmetics.proper_divisors(n)) == n -} - -fn do_sum(arr: List(Int)) -> Int { - case arr { - [] -> 0 - _ -> - arr - |> list.fold(0, fn(acc, a) { a + acc }) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer value \\(x \in \mathbb{Z}\\) is even. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_even(-3) -/// |> should.equal(False) -/// -/// predicates.is_even(-4) -/// |> should.equal(True) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_even(x: Int) -> Bool { - x % 2 == 0 -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer value \\(x \in \mathbb{Z}\\) is odd. -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_odd(-3) -/// |> should.equal(True) -/// -/// predicates.is_odd(-4) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_odd(x: Int) -> Bool { - x % 2 != 0 -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer value \\(x \in \mathbb{Z}\\) is a -/// prime number. A prime number is a natural number greater than 1 that has no -/// positive divisors other than 1 and itself. -/// -/// The function uses the Miller-Rabin primality test to assess if \\(x\\) is prime. -/// It is a probabilistic test, so it can mistakenly identify a composite number -/// as prime. However, the probability of such errors decreases with more testing -/// iterations (the function uses 64 iterations internally, which is typically -/// more than sufficient). The Miller-Rabin test is particularly useful for large -/// numbers. -/// -///
-/// Details -/// -/// Examples of prime numbers: -/// - \\(2\\) is a prime number since it has only two divisors: \\(1\\) and \\(2\\). -/// - \\(7\\) is a prime number since it has only two divisors: \\(1\\) and \\(7\\). -/// - \\(4\\) is not a prime number since it has divisors other than \\(1\\) and itself, such -/// as \\(2\\). -/// -///
-/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_prime(2) -/// |> should.equal(True) -/// -/// predicates.is_prime(4) -/// |> should.equal(False) -/// -/// // Test the 2nd Carmichael number -/// predicates.is_prime(1105) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_prime(x: Int) -> Bool { - case x { - x if x < 2 -> { - False - } - x if x == 2 -> { - True - } - _ -> { - miller_rabin_test(x, 64) - } - } -} - -fn miller_rabin_test(n: Int, k: Int) -> Bool { - case n, k { - _, 0 -> True - _, _ -> { - // Generate a random int in the range [2, n] - let random_candidate = 2 + int.random(n - 2) - case powmod_with_check(random_candidate, n - 1, n) == 1 { - True -> miller_rabin_test(n, k - 1) - False -> False - } - } - } -} - -fn powmod_with_check(base: Int, exponent: Int, modulus: Int) -> Int { - case exponent, { exponent % 2 } == 0 { - 0, _ -> 1 - _, True -> { - let x = powmod_with_check(base, exponent / 2, modulus) - case { x * x } % modulus, x != 1 && x != { modulus - 1 } { - 1, True -> 0 - _, _ -> { x * x } % modulus - } - } - _, _ -> { base * powmod_with_check(base, exponent - 1, modulus) } % modulus - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given real number \\(x \in \mathbb{R}\\) is strictly -/// between two other real numbers, \\(a,b \in \mathbb{R}\\), such that \\(a < x < b\\). -/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_between(5.5, 5.0, 6.0) -/// |> should.equal(True) -/// -/// predicates.is_between(5.0, 5.0, 6.0) -/// |> should.equal(False) -/// -/// predicates.is_between(6.0, 5.0, 6.0) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_between(x: Float, lower: Float, upper: Float) -> Bool { - lower <. x && x <. upper -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer \\(n \in \mathbb{Z}\\) is divisible by another -/// integer \\(d \in \mathbb{Z}\\), such that \\(n \mod d = 0\\). -/// -///
-/// Details -/// -/// For example: -/// - \\(n = 10\\) is divisible by \\(d = 2\\) because \\(10 \mod 2 = 0\\). -/// - \\(n = 7\\) is not divisible by \\(d = 3\\) because \\(7 \mod 3 \neq 0\\). -/// -///
-/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_divisible(10, 2) -/// |> should.equal(True) -/// -/// predicates.is_divisible(7, 3) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_divisible(n: Int, d: Int) -> Bool { - n % d == 0 -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// A function that tests whether a given integer \\(m \in \mathbb{Z}\\) is a multiple of another -/// integer \\(k \in \mathbb{Z}\\), such that \\(m = k \times q\\), with \\(q \in \mathbb{Z}\\). -/// -///
-/// Details -/// -/// For example: -/// - \\(m = 15\\) is a multiple of \\(k = 5\\) because \\(15 = 5 \times 3\\). -/// - \\(m = 14\\) is not a multiple of \\(k = 5\\) because \\(14 \div 5\\) does not yield an -/// integer quotient. -/// -///
-/// -///
-/// Example: -/// -/// import gleeunit/should -/// import gleam_community/maths/predicates -/// -/// pub fn example() { -/// predicates.is_multiple(15, 5) -/// |> should.equal(True) -/// -/// predicates.is_multiple(14, 5) -/// |> should.equal(False) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn is_multiple(m: Int, k: Int) -> Bool { - m % k == 0 -} diff --git a/src/gleam_community/maths/sequences.gleam b/src/gleam_community/maths/sequences.gleam deleted file mode 100644 index c801184..0000000 --- a/src/gleam_community/maths/sequences.gleam +++ /dev/null @@ -1,328 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Sequences: A module containing functions for generating various types of -//// sequences, ranges and intervals. -//// -//// * **Ranges and intervals** -//// * [`arange`](#arange) -//// * [`linear_space`](#linear_space) -//// * [`logarithmic_space`](#logarithmic_space) -//// * [`geometric_space`](#geometric_space) -//// - -import gleam/iterator -import gleam_community/maths/conversion -import gleam_community/maths/elementary -import gleam_community/maths/piecewise - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns an iterator generating evenly spaced values within a given interval. -/// based on a start value but excludes the stop value. The spacing between values is determined -/// by the step size provided. The function supports both positive and negative step values. -/// -///
-/// Example: -/// -/// import gleam/iterator -/// import gleeunit/should -/// import gleam_community/maths/sequences -/// -/// pub fn example () { -/// sequences.arange(1.0, 5.0, 1.0) -/// |> iterator.to_list() -/// |> should.equal([1.0, 2.0, 3.0, 4.0]) -/// -/// // No points returned since -/// // start is smaller than stop and the step is positive -/// sequences.arange(5.0, 1.0, 1.0) -/// |> iterator.to_list() -/// |> should.equal([]) -/// -/// // Points returned since -/// // start smaller than stop but negative step -/// sequences.arange(5.0, 1.0, -1.0) -/// |> iterator.to_list() -/// |> should.equal([5.0, 4.0, 3.0, 2.0]) -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn arange( - start: Float, - stop: Float, - step: Float, -) -> iterator.Iterator(Float) { - case start >=. stop && step >. 0.0 || start <=. stop && step <. 0.0 { - True -> iterator.empty() - False -> { - let direction = case start <=. stop { - True -> { - 1.0 - } - False -> { - -1.0 - } - } - let step_abs = piecewise.float_absolute_value(step) - let num = - piecewise.float_absolute_value(start -. stop) /. step_abs - |> conversion.float_to_int() - - iterator.range(0, num - 1) - |> iterator.map(fn(i) { - start +. conversion.int_to_float(i) *. step_abs *. direction - }) - } - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns an iterator for generating linearly spaced points over a specified -/// interval. The endpoint of the interval can optionally be included/excluded. The number of -/// points and whether the endpoint is included determine the spacing between values. -/// -///
-/// Example: -/// -/// import gleam/iterator -/// import gleeunit/should -/// import gleam_community/maths/elementary -/// import gleam_community/maths/sequences -/// import gleam_community/maths/predicates -/// -/// pub fn example () { -/// let assert Ok(tol) = elementary.power(10.0, -6.0) -/// let assert Ok(linspace) = sequences.linear_space(10.0, 20.0, 5, True) -/// let assert Ok(result) = -/// predicates.all_close( -/// linspace |> iterator.to_list(), -/// [10.0, 12.5, 15.0, 17.5, 20.0], -/// 0.0, -/// tol, -/// ) -/// -/// result -/// |> list.all(fn(x) { x == True }) -/// |> should.be_true() -/// -/// // A negative number of points (-5) does not work -/// sequences.linear_space(10.0, 50.0, -5, True) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn linear_space( - start: Float, - stop: Float, - num: Int, - endpoint: Bool, -) -> Result(iterator.Iterator(Float), Nil) { - let direction = case start <=. stop { - True -> 1.0 - False -> -1.0 - } - - let increment = case endpoint { - True -> { - piecewise.float_absolute_value(start -. stop) - /. conversion.int_to_float(num - 1) - } - False -> { - piecewise.float_absolute_value(start -. stop) - /. conversion.int_to_float(num) - } - } - case num > 0 { - True -> { - iterator.range(0, num - 1) - |> iterator.map(fn(i) { - start +. conversion.int_to_float(i) *. increment *. direction - }) - |> Ok - } - False -> Error(Nil) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns an iterator of logarithmically spaced points over a specified interval. -/// The endpoint of the interval can optionally be included/excluded. The number of points, base, -/// and whether the endpoint is included determine the spacing between values. -/// -///
-/// Example: -/// -/// import gleam/iterator -/// import gleeunit/should -/// import gleam_community/maths/elementary -/// import gleam_community/maths/sequences -/// import gleam_community/maths/predicates -/// -/// pub fn example () { -/// let assert Ok(tol) = elementary.power(10.0, -6.0) -/// let assert Ok(logspace) = sequences.logarithmic_space(1.0, 3.0, 3, True, 10.0) -/// let assert Ok(result) = -/// predicates.all_close( -/// logspace |> iterator.to_list(), -/// [10.0, 100.0, 1000.0], -/// 0.0, -/// tol, -/// ) -/// result -/// |> list.all(fn(x) { x == True }) -/// |> should.be_true() -/// -/// // A negative number of points (-3) does not work -/// sequences.logarithmic_space(1.0, 3.0, -3, False, 10.0) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn logarithmic_space( - start: Float, - stop: Float, - num: Int, - endpoint: Bool, - base: Float, -) -> Result(iterator.Iterator(Float), Nil) { - case num > 0 { - True -> { - let assert Ok(linspace) = linear_space(start, stop, num, endpoint) - linspace - |> iterator.map(fn(i) { - let assert Ok(result) = elementary.power(base, i) - result - }) - |> Ok - } - False -> Error(Nil) - } -} - -///
-/// -/// Spot a typo? Open an issue! -/// -///
-/// -/// The function returns an iterator of numbers spaced evenly on a log scale (a geometric -/// progression). Each point in the list is a constant multiple of the previous. The function is -/// similar to the [`logarithmic_space`](#logarithmic_space) function, but with endpoints -/// specified directly. -/// -///
-/// Example: -/// -/// import gleam/iterator -/// import gleeunit/should -/// import gleam_community/maths/elementary -/// import gleam_community/maths/sequences -/// import gleam_community/maths/predicates -/// -/// pub fn example () { -/// let assert Ok(tol) = elementary.power(10.0, -6.0) -/// let assert Ok(logspace) = sequences.geometric_space(10.0, 1000.0, 3, True) -/// let assert Ok(result) = -/// predicates.all_close( -/// logspace |> iterator.to_list(), -/// [10.0, 100.0, 1000.0], -/// 0.0, -/// tol, -/// ) -/// result -/// |> list.all(fn(x) { x == True }) -/// |> should.be_true() -/// -/// // Input (start and stop can't be equal to 0.0) -/// sequences.geometric_space(0.0, 1000.0, 3, False) -/// |> should.be_error() -/// -/// sequences.geometric_space(-1000.0, 0.0, 3, False) -/// |> should.be_error() -/// -/// // A negative number of points (-3) does not work -/// sequences.geometric_space(10.0, 1000.0, -3, False) -/// |> should.be_error() -/// } -///
-/// -///
-/// -/// Back to top ↑ -/// -///
-/// -pub fn geometric_space( - start: Float, - stop: Float, - num: Int, - endpoint: Bool, -) -> Result(iterator.Iterator(Float), Nil) { - case start == 0.0 || stop == 0.0 { - True -> Error(Nil) - False -> - case num > 0 { - True -> { - let assert Ok(log_start) = elementary.logarithm_10(start) - let assert Ok(log_stop) = elementary.logarithm_10(stop) - logarithmic_space(log_start, log_stop, num, endpoint, 10.0) - } - False -> Error(Nil) - } - } -} diff --git a/src/gleam_community/maths/special.gleam b/src/gleam_community/maths/special.gleam deleted file mode 100644 index 6422709..0000000 --- a/src/gleam_community/maths/special.gleam +++ /dev/null @@ -1,195 +0,0 @@ -//// -//// -//// -//// -//// -//// -//// --- -//// -//// Special: A module containing special mathematical functions. -//// -//// * **Special mathematical functions** -//// * [`beta`](#beta) -//// * [`erf`](#erf) -//// * [`gamma`](#gamma) -//// * [`incomplete_gamma`](#incomplete_gamma) -//// - -import gleam/list -import gleam_community/maths/conversion -import gleam_community/maths/elementary -import gleam_community/maths/piecewise - -///
-/// -/// 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 assert [a1, a2, a3, a4, a5] = [ - 0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429, - ] - let p = 0.3275911 - - let sign = piecewise.float_sign(x) - let x = piecewise.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 - *. elementary.exponential(-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 -> - elementary.pi() - /. { elementary.sin(elementary.pi() *. x) *. gamma_lanczos(1.0 -. x) } - False -> { - let z = x -. 1.0 - let x = - list.index_fold(lanczos_p, 0.0, fn(acc, v, index) { - case index > 0 { - True -> acc +. v /. { z +. conversion.int_to_float(index) } - False -> v - } - }) - let t = z +. lanczos_g +. 0.5 - let assert Ok(v1) = elementary.power(2.0 *. elementary.pi(), 0.5) - let assert Ok(v2) = elementary.power(t, z +. 0.5) - v1 *. v2 *. elementary.exponential(-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 incomplete_gamma(a: Float, x: Float) -> Result(Float, Nil) { - case a >. 0.0 && x >=. 0.0 { - True -> { - let assert Ok(v) = elementary.power(x, a) - v - *. elementary.exponential(-1.0 *. x) - *. incomplete_gamma_sum(a, x, 1.0 /. a, 0.0, 1.0) - |> Ok - } - - False -> Error(Nil) - } -} - -fn incomplete_gamma_sum( - a: Float, - x: Float, - t: Float, - s: Float, - n: Float, -) -> Float { - case t { - 0.0 -> s - _ -> { - let ns = s +. t - let nt = t *. { x /. { a +. n } } - incomplete_gamma_sum(a, x, nt, ns, n +. 1.0) - } - } -} diff --git a/test/gleam_community/arithmetics_test.gleam b/test/gleam_community/arithmetics_test.gleam new file mode 100644 index 0000000..6cbc9ec --- /dev/null +++ b/test/gleam_community/arithmetics_test.gleam @@ -0,0 +1,203 @@ +import gleam/float +import gleam_community/maths +import gleeunit/should + +pub fn int_gcd_test() { + maths.gcd(1, 1) + |> should.equal(1) + + maths.gcd(100, 10) + |> should.equal(10) + + maths.gcd(10, 100) + |> should.equal(10) + + maths.gcd(100, -10) + |> should.equal(10) + + maths.gcd(-36, -17) + |> should.equal(1) + + maths.gcd(-30, -42) + |> should.equal(6) +} + +pub fn int_euclidean_modulo_test() { + // Base Case: Positive x, Positive y + // Note that the truncated, floored, and euclidean + // definitions should agree for this base case + maths.int_euclidean_modulo(15, 4) + |> should.equal(3) + + // Case: Positive x, Negative y + maths.int_euclidean_modulo(15, -4) + |> should.equal(3) + + // Case: Negative x, Positive y + maths.int_euclidean_modulo(-15, 4) + |> should.equal(1) + + // Case: Negative x, Negative y + maths.int_euclidean_modulo(-15, -4) + |> should.equal(1) + + // Case: Positive x, Zero y + maths.int_euclidean_modulo(5, 0) + |> should.equal(0) + + // Case: Zero x, Negative y + maths.int_euclidean_modulo(0, 5) + |> should.equal(0) +} + +pub fn int_lcm_test() { + maths.lcm(1, 1) + |> should.equal(1) + + maths.lcm(100, 10) + |> should.equal(100) + + maths.lcm(10, 100) + |> should.equal(100) + + maths.lcm(100, -10) + |> should.equal(100) + + maths.lcm(-36, -17) + |> should.equal(612) + + maths.lcm(-30, -42) + |> should.equal(210) +} + +pub fn int_proper_divisors_test() { + maths.proper_divisors(2) + |> should.equal([1]) + + maths.proper_divisors(6) + |> should.equal([1, 2, 3]) + + maths.proper_divisors(13) + |> should.equal([1]) + + maths.proper_divisors(18) + |> should.equal([1, 2, 3, 6, 9]) +} + +pub fn int_divisors_test() { + maths.divisors(2) + |> should.equal([1, 2]) + + maths.divisors(6) + |> should.equal([1, 2, 3, 6]) + + maths.divisors(13) + |> should.equal([1, 13]) + + maths.divisors(18) + |> should.equal([1, 2, 3, 6, 9, 18]) +} + +pub fn float_list_cumulative_sum_test() { + // An empty lists returns an empty list + [] + |> maths.float_cumulative_sum() + |> should.equal([]) + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> maths.float_cumulative_sum() + |> should.equal([1.0, 3.0, 6.0]) + + [-2.0, 4.0, 6.0] + |> maths.float_cumulative_sum() + |> should.equal([-2.0, 2.0, 8.0]) +} + +pub fn int_list_cumulative_sum_test() { + // An empty lists returns an empty list + [] + |> maths.int_cumulative_sum() + |> should.equal([]) + + // Valid input returns a result + [1, 2, 3] + |> maths.int_cumulative_sum() + |> should.equal([1, 3, 6]) + + [-2, 4, 6] + |> maths.int_cumulative_sum() + |> should.equal([-2, 2, 8]) +} + +pub fn float_list_cumulative_product_test() { + // An empty lists returns an empty list + [] + |> maths.float_cumulative_product() + |> should.equal([]) + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> maths.float_cumulative_product() + |> should.equal([1.0, 2.0, 6.0]) + + [-2.0, 4.0, 6.0] + |> maths.float_cumulative_product() + |> should.equal([-2.0, -8.0, -48.0]) +} + +pub fn int_list_cumulative_product_test() { + // An empty lists returns an empty list + [] + |> maths.int_cumulative_product() + |> should.equal([]) + + // Valid input returns a result + [1, 2, 3] + |> maths.int_cumulative_product() + |> should.equal([1, 2, 6]) + + [-2, 4, 6] + |> maths.int_cumulative_product() + |> should.equal([-2, -8, -48]) +} + +pub fn float_weighted_product_test() { + [] + |> maths.float_weighted_product() + |> should.equal(Ok(1.0)) + + [#(1.0, 0.0), #(2.0, 0.0), #(3.0, 0.0)] + |> maths.float_weighted_product() + |> should.equal(Ok(1.0)) + + [#(1.0, 1.0), #(2.0, 1.0), #(3.0, 1.0)] + |> maths.float_weighted_product() + |> should.equal(Ok(6.0)) + + let assert Ok(tolerance) = float.power(10.0, -6.0) + let assert Ok(result) = + [#(9.0, 0.5), #(10.0, 0.5), #(10.0, 0.5)] + |> maths.float_weighted_product() + result + |> maths.is_close(30.0, 0.0, tolerance) + |> should.be_true() +} + +pub fn float_weighted_sum_test() { + [] + |> maths.float_weighted_sum() + |> should.equal(Ok(0.0)) + + [#(1.0, 0.0), #(2.0, 0.0), #(3.0, 0.0)] + |> maths.float_weighted_sum() + |> should.equal(Ok(0.0)) + + [#(1.0, 1.0), #(2.0, 1.0), #(3.0, 1.0)] + |> maths.float_weighted_sum() + |> should.equal(Ok(6.0)) + + [#(9.0, 0.5), #(10.0, 0.5), #(10.0, 0.5)] + |> maths.float_weighted_sum() + |> should.equal(Ok(14.5)) +} diff --git a/test/gleam_community/maths/combinatorics_test.gleam b/test/gleam_community/combinatorics_test.gleam similarity index 51% rename from test/gleam_community/maths/combinatorics_test.gleam rename to test/gleam_community/combinatorics_test.gleam index 729f450..dc9122a 100644 --- a/test/gleam_community/maths/combinatorics_test.gleam +++ b/test/gleam_community/combinatorics_test.gleam @@ -1,132 +1,139 @@ -import gleam/iterator import gleam/list -import gleam/option import gleam/set -import gleam_community/maths/combinatorics +import gleam/yielder +import gleam_community/maths import gleeunit/should pub fn int_factorial_test() { // Invalid input gives an error (factorial of negative number) - combinatorics.factorial(-1) + maths.factorial(-1) |> should.be_error() // Valid inputs for factorial of small numbers - combinatorics.factorial(0) + maths.factorial(0) |> should.equal(Ok(1)) - combinatorics.factorial(1) + maths.factorial(1) |> should.equal(Ok(1)) - combinatorics.factorial(2) + maths.factorial(2) |> should.equal(Ok(2)) - combinatorics.factorial(3) + maths.factorial(3) |> should.equal(Ok(6)) - combinatorics.factorial(4) + maths.factorial(4) |> should.equal(Ok(24)) } -pub fn int_combination_test() { +pub fn int_combination_with_repetitions_test() { // Invalid input: k < 0 should return an error - combinatorics.combination(1, -1, option.None) + maths.combination_with_repetitions(1, -1) |> should.be_error() // Invalid input: n < 0 should return an error - combinatorics.combination(-1, 1, option.None) + maths.combination_with_repetitions(-1, 1) |> should.be_error() - // Valid input: k > n without repetition gives 0 combinations - combinatorics.combination(2, 3, option.Some(combinatorics.WithoutRepetitions)) - |> should.equal(Ok(0)) - // Valid input: k > n with repetition allowed - combinatorics.combination(2, 3, option.Some(combinatorics.WithRepetitions)) + maths.combination_with_repetitions(2, 3) |> should.equal(Ok(4)) - // Valid input: zero combinations (k=0) should always yield 1 combination - combinatorics.combination(4, 0, option.Some(combinatorics.WithoutRepetitions)) - |> should.equal(Ok(1)) - - combinatorics.combination(4, 0, option.Some(combinatorics.WithRepetitions)) - |> should.equal(Ok(1)) - - // Valid input: k = n without repetition gives 1 combination - combinatorics.combination(4, 4, option.Some(combinatorics.WithoutRepetitions)) + maths.combination_with_repetitions(4, 0) |> should.equal(Ok(1)) // Valid input: k = n with repetition allows more combinations - combinatorics.combination(4, 4, option.Some(combinatorics.WithRepetitions)) + maths.combination_with_repetitions(4, 4) |> should.equal(Ok(35)) - // Valid input: k < n without and with repetition - combinatorics.combination(4, 2, option.Some(combinatorics.WithoutRepetitions)) - |> should.equal(Ok(6)) - - combinatorics.combination(4, 2, option.Some(combinatorics.WithRepetitions)) + maths.combination_with_repetitions(4, 2) |> should.equal(Ok(10)) - // Valid input with larger values of n and k - combinatorics.combination(7, 5, option.Some(combinatorics.WithoutRepetitions)) - |> should.equal(Ok(21)) - - combinatorics.combination(7, 5, option.Some(combinatorics.WithRepetitions)) + maths.combination_with_repetitions(7, 5) |> should.equal(Ok(462)) // 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() { +pub fn int_combination_without_repetitions_test() { + // Valid input: k > n without repetition gives 0 combinations + maths.combination(2, 3) + |> should.equal(Ok(0)) + + // Valid input: k = n without repetition gives 1 combination + maths.combination(4, 4) + |> should.equal(Ok(1)) + + // Valid input: zero combinations (k=0) should always yield 1 combination + maths.combination(4, 0) + |> should.equal(Ok(1)) + + // Valid input: k < n without and with repetition + maths.combination(4, 2) + |> should.equal(Ok(6)) + + // Valid input with larger values of n and k + maths.combination(7, 5) + |> should.equal(Ok(21)) +} + +pub fn math_permutation_with_repetitions_test() { // Invalid input: k < 0 should return an error - combinatorics.permutation(1, -1, option.None) + maths.permutation_with_repetitions(1, -1) + |> should.be_error() + + // Valid input: k > n with repetition allowed gives non-zero permutations + maths.permutation_with_repetitions(2, 3) + |> should.equal(Ok(8)) + + maths.permutation_with_repetitions(4, 0) + |> should.equal(Ok(1)) + + // Valid input: k = n permutations with repetition + maths.permutation_with_repetitions(4, 4) + |> should.equal(Ok(256)) + + maths.permutation_with_repetitions(4, 2) + |> should.equal(Ok(16)) + + maths.permutation_with_repetitions(6, 2) + |> should.equal(Ok(36)) + + maths.permutation_with_repetitions(6, 3) + |> should.equal(Ok(216)) +} + +pub fn math_permutation_without_repetitions_test() { + // Invalid input: k < 0 should return an error + maths.permutation(1, -1) |> should.be_error() // Invalid input: n < 0 should return an error - combinatorics.permutation(-1, 1, option.None) + maths.permutation(-1, 1) |> should.be_error() // Valid input: k > n without repetition gives 0 permutations - combinatorics.permutation(2, 3, option.Some(combinatorics.WithoutRepetitions)) + maths.permutation(2, 3) |> should.equal(Ok(0)) - // Valid input: k > n with repetition allowed gives non-zero permutations - combinatorics.permutation(2, 3, option.Some(combinatorics.WithRepetitions)) - |> should.equal(Ok(8)) - // Valid input: k = 0 should always yield 1 permutation - combinatorics.permutation(4, 0, option.Some(combinatorics.WithoutRepetitions)) - |> should.equal(Ok(1)) - - combinatorics.permutation(4, 0, option.Some(combinatorics.WithRepetitions)) + maths.permutation(4, 0) |> should.equal(Ok(1)) // Valid input: k = n permutations without repetition - combinatorics.permutation(4, 4, option.Some(combinatorics.WithoutRepetitions)) + maths.permutation(4, 4) |> should.equal(Ok(24)) - // Valid input: k = n permutations with repetition - combinatorics.permutation(4, 4, option.Some(combinatorics.WithRepetitions)) - |> should.equal(Ok(256)) - // Valid input: k < n permutations without and with repetition - combinatorics.permutation(4, 2, option.Some(combinatorics.WithoutRepetitions)) + maths.permutation(4, 2) |> should.equal(Ok(12)) - combinatorics.permutation(4, 2, option.Some(combinatorics.WithRepetitions)) - |> should.equal(Ok(16)) - // Valid input with larger values of n and k - combinatorics.permutation(6, 2, option.Some(combinatorics.WithoutRepetitions)) + maths.permutation(6, 2) |> should.equal(Ok(30)) - combinatorics.permutation(6, 2, option.Some(combinatorics.WithRepetitions)) - |> should.equal(Ok(36)) - - combinatorics.permutation(6, 3, option.Some(combinatorics.WithoutRepetitions)) + maths.permutation(6, 3) |> should.equal(Ok(120)) - - combinatorics.permutation(6, 3, option.Some(combinatorics.WithRepetitions)) - |> should.equal(Ok(216)) } pub fn list_cartesian_product_test() { @@ -135,7 +142,7 @@ pub fn list_cartesian_product_test() { let yset = set.from_list([]) let expected_result = set.from_list([]) xset - |> combinatorics.cartesian_product(yset) + |> maths.cartesian_product(yset) |> should.equal(expected_result) // Cartesian product of two sets with the same elements @@ -154,7 +161,7 @@ pub fn list_cartesian_product_test() { #(3, 3), ]) xset - |> combinatorics.cartesian_product(yset) + |> maths.cartesian_product(yset) |> should.equal(expected_result) // Cartesian product with floating-point numbers @@ -163,7 +170,7 @@ pub fn list_cartesian_product_test() { let expected_result = set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]) xset - |> combinatorics.cartesian_product(yset) + |> maths.cartesian_product(yset) |> should.equal(expected_result) // Cartesian product of sets with different sizes @@ -179,7 +186,7 @@ pub fn list_cartesian_product_test() { #(100.0, 2.0), ]) xset - |> combinatorics.cartesian_product(yset) + |> maths.cartesian_product(yset) |> should.equal(expected_result) // Cartesian product with different types (strings) @@ -195,195 +202,216 @@ pub fn list_cartesian_product_test() { #("z", "x"), ]) xset - |> combinatorics.cartesian_product(yset) + |> maths.cartesian_product(yset) |> should.equal(expected_result) } -pub fn list_permutation_test() { +pub fn cartesian_product_mixed_types_test() { + // Cartesian product of two empty sets + set.from_list([]) + |> maths.cartesian_product(set.from_list([])) + |> should.equal(set.from_list([])) + + // Cartesian product of two sets with numeric values + set.from_list([1.0, 10.0]) + |> maths.cartesian_product(set.from_list([1.0, 2.0])) + |> should.equal( + set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]), + ) + + // Cartesian product of two sets with different types + set.from_list(["1", "10"]) + |> maths.cartesian_product(set.from_list([1.0, 2.0])) + |> should.equal( + set.from_list([#("1", 1.0), #("1", 2.0), #("10", 1.0), #("10", 2.0)]), + ) +} + +pub fn list_permutation_with_repetitions_test() { // Invalid input: k < 0 should return an error for an empty list [] - |> combinatorics.list_permutation(-1, option.None) - |> should.be_error() - - // Invalid input: k > n should return an error without repetition - [1, 2] - |> combinatorics.list_permutation( - 3, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_permutation_with_repetitions(-1) |> should.be_error() // Valid input: An empty list returns a single empty permutation let assert Ok(permutations) = [] - |> combinatorics.list_permutation(0, option.None) + |> maths.list_permutation_with_repetitions(0) permutations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[]]) - // Singleton list returns a single permutation regardless of repetition settings let assert Ok(permutations) = ["a"] - |> combinatorics.list_permutation( - 1, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_permutation_with_repetitions(1) permutations - |> iterator.to_list() - |> should.equal([["a"]]) - - let assert Ok(permutations) = - ["a"] - |> combinatorics.list_permutation( - 1, - option.Some(combinatorics.WithRepetitions), - ) - permutations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([["a"]]) // 4-permutations of a single element repeats it 4 times let assert Ok(permutations) = ["a"] - |> combinatorics.list_permutation( - 4, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_permutation_with_repetitions(4) permutations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([["a", "a", "a", "a"]]) - // 2-permutations of [1, 2] without repetition - let assert Ok(permutations) = - [1, 2] - |> combinatorics.list_permutation( - 2, - option.Some(combinatorics.WithoutRepetitions), - ) - permutations - |> iterator.to_list() - |> set.from_list() - |> should.equal(set.from_list([[1, 2], [2, 1]])) - // 2-permutations of [1, 2] with repetition let assert Ok(permutations) = [1, 2] - |> combinatorics.list_permutation( - 2, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_permutation_with_repetitions(2) permutations - |> iterator.to_list() + |> yielder.to_list() |> set.from_list() |> should.equal(set.from_list([[1, 1], [1, 2], [2, 2], [2, 1]])) - // 3-permutations of [1, 2, 3] without repetition - let assert Ok(permutations) = - [1, 2, 3] - |> combinatorics.list_permutation( - 3, - option.Some(combinatorics.WithoutRepetitions), - ) - permutations - |> iterator.to_list() - |> set.from_list() - |> should.equal( - set.from_list([ - [1, 2, 3], - [2, 1, 3], - [3, 1, 2], - [1, 3, 2], - [2, 3, 1], - [3, 2, 1], - ]), - ) - // 3-permutations of [1, 2, 3] with repetition let assert Ok(permutations) = [1, 2, 3] - |> combinatorics.list_permutation( - 3, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_permutation_with_repetitions(3) permutations - |> iterator.to_list() - |> set.from_list() - |> should.equal( - set.from_list([ - [1, 1, 1], - [1, 1, 2], - [1, 1, 3], - [1, 2, 1], - [1, 2, 2], - [1, 2, 3], - [1, 3, 1], - [1, 3, 2], - [1, 3, 3], - [2, 1, 1], - [2, 1, 2], - [2, 1, 3], - [2, 2, 1], - [2, 2, 2], - [2, 2, 3], - [2, 3, 1], - [2, 3, 2], - [2, 3, 3], - [3, 1, 1], - [3, 1, 2], - [3, 1, 3], - [3, 2, 1], - [3, 2, 2], - [3, 2, 3], - [3, 3, 1], - [3, 3, 2], - [3, 3, 3], - ]), - ) - - // Repeated elements are treated as distinct in permutations without repetition - let assert Ok(permutations) = - [1.0, 1.0] - |> combinatorics.list_permutation( - 2, - option.Some(combinatorics.WithoutRepetitions), - ) - permutations - |> iterator.to_list() - |> should.equal([[1.0, 1.0], [1.0, 1.0]]) + |> yielder.to_list() + |> should.equal([ + [1, 1, 1], + [1, 1, 2], + [1, 1, 3], + [1, 2, 1], + [1, 2, 2], + [1, 2, 3], + [1, 3, 1], + [1, 3, 2], + [1, 3, 3], + [2, 1, 1], + [2, 1, 2], + [2, 1, 3], + [2, 2, 1], + [2, 2, 2], + [2, 2, 3], + [2, 3, 1], + [2, 3, 2], + [2, 3, 3], + [3, 1, 1], + [3, 1, 2], + [3, 1, 3], + [3, 2, 1], + [3, 2, 2], + [3, 2, 3], + [3, 3, 1], + [3, 3, 2], + [3, 3, 3], + ]) // Repeated elements allow more possibilities when repetition is allowed let assert Ok(permutations) = [1.0, 1.0] - |> combinatorics.list_permutation( - 2, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_permutation_with_repetitions(2) permutations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]]) + let assert Ok(permutations) = + ["a", "b", "c", "d", "e"] + |> maths.list_permutation_with_repetitions(5) + permutations + |> yielder.to_list() + |> list.length() + |> should.equal(3125) +} + +pub fn list_permutation_without_repetitions_test() { + // Invalid input: k > n should return an error without repetition + [1, 2] + |> maths.list_permutation(3) + |> should.be_error() + + // Singleton list returns a single permutation regardless of repetition settings + let assert Ok(permutations) = + ["a"] + |> maths.list_permutation(1) + permutations + |> yielder.to_list() + |> should.equal([["a"]]) + + // 2-permutations of [1, 2] without repetition + let assert Ok(permutations) = + [1, 2] + |> maths.list_permutation(2) + permutations + |> yielder.to_list() + |> should.equal([[1, 2], [2, 1]]) + + // 2-permutations without repetition + let assert Ok(result) = + [1, 2, 3] + |> maths.list_permutation(2) + result + |> yielder.to_list() + |> should.equal([[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]) + + // 3-permutations of [1, 2, 3] without repetition + let assert Ok(permutations) = + [1, 2, 3] + |> maths.list_permutation(3) + permutations + |> yielder.to_list() + |> should.equal([ + [1, 2, 3], + [1, 3, 2], + [2, 1, 3], + [2, 3, 1], + [3, 1, 2], + [3, 2, 1], + ]) + + // 3-permutations of [1, 2, 3, 4] without repetition + let assert Ok(permutations) = + [1, 2, 3, 4] + |> maths.list_permutation(3) + permutations + |> yielder.to_list() + |> should.equal([ + [1, 2, 3], + [1, 2, 4], + [1, 3, 2], + [1, 3, 4], + [1, 4, 2], + [1, 4, 3], + [2, 1, 3], + [2, 1, 4], + [2, 3, 1], + [2, 3, 4], + [2, 4, 1], + [2, 4, 3], + [3, 1, 2], + [3, 1, 4], + [3, 2, 1], + [3, 2, 4], + [3, 4, 1], + [3, 4, 2], + [4, 1, 2], + [4, 1, 3], + [4, 2, 1], + [4, 2, 3], + [4, 3, 1], + [4, 3, 2], + ]) + + // Repeated elements are treated as distinct in permutations without repetition + let assert Ok(permutations) = + [1.0, 1.0] + |> maths.list_permutation(2) + permutations + |> yielder.to_list() + |> should.equal([[1.0, 1.0], [1.0, 1.0]]) + // Large inputs: Ensure the correct number of permutations is generated let assert Ok(permutations) = ["a", "b", "c", "d", "e"] - |> combinatorics.list_permutation( - 5, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_permutation(5) permutations - |> iterator.to_list() + |> yielder.to_list() |> list.length() |> should.equal(120) - - let assert Ok(permutations) = - ["a", "b", "c", "d", "e"] - |> combinatorics.list_permutation( - 5, - option.Some(combinatorics.WithRepetitions), - ) - permutations - |> iterator.to_list() - |> list.length() - |> should.equal(3125) } pub fn permutation_alignment_test() { @@ -392,20 +420,12 @@ pub fn permutation_alignment_test() { let arr = ["a", "b", "c", "d", "e", "f"] let length = list.length(arr) - let assert Ok(number_of_permutations) = - combinatorics.permutation( - length, - length, - option.Some(combinatorics.WithoutRepetitions), - ) + let assert Ok(number_of_permutations) = maths.permutation(length, length) let assert Ok(permutations) = arr - |> combinatorics.list_permutation( - length, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_permutation(length) permutations - |> iterator.to_list() + |> yielder.to_list() |> list.length() |> should.equal(number_of_permutations) @@ -414,230 +434,196 @@ pub fn permutation_alignment_test() { let length = list.length(arr) let assert Ok(number_of_permutations) = - combinatorics.permutation( - length, - length, - option.Some(combinatorics.WithRepetitions), - ) + maths.permutation_with_repetitions(length, length) let assert Ok(permutations) = arr - |> combinatorics.list_permutation( - length, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_permutation_with_repetitions(length) permutations - |> iterator.to_list() + |> yielder.to_list() |> list.length() |> should.equal(number_of_permutations) } -pub fn list_combination_test() { +pub fn list_combination_with_repetitions_test() { // Invalid input: k < 0 should return an error for an empty list [] - |> combinatorics.list_combination(-1, option.None) - |> should.be_error() - - // Invalid input: k > n should return an error without repetition - [1, 2] - |> combinatorics.list_combination( - 3, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination_with_repetitions(-1) |> should.be_error() // Valid input: k > n with repetition allowed let assert Ok(combinations) = [1, 2] - |> combinatorics.list_combination( - 3, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_combination_with_repetitions(3) combinations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[1, 1, 1], [1, 1, 2], [1, 2, 2], [2, 2, 2]]) + let assert Ok(combinations) = + [] + |> maths.list_combination_with_repetitions(0) + combinations + |> yielder.to_list() + |> should.equal([[]]) + + let assert Ok(combinations) = + [1, 2] + |> maths.list_combination_with_repetitions(1) + combinations + |> yielder.to_list() + |> should.equal([[1], [2]]) + + let assert Ok(combinations) = + [1, 2] + |> maths.list_combination_with_repetitions(2) + combinations + |> yielder.to_list() + |> should.equal([[1, 1], [1, 2], [2, 2]]) + + let assert Ok(combinations) = + [1, 2, 3, 4] + |> maths.list_combination_with_repetitions(2) + combinations + |> yielder.to_list() + |> should.equal([ + [1, 1], + [1, 2], + [1, 3], + [1, 4], + [2, 2], + [2, 3], + [2, 4], + [3, 3], + [3, 4], + [4, 4], + ]) + + // 3-combination of [1, 2, 3] with repetition + let assert Ok(combinations) = + [1, 2, 3] + |> maths.list_combination_with_repetitions(3) + combinations + |> yielder.to_list() + |> should.equal([ + [1, 1, 1], + [1, 1, 2], + [1, 1, 3], + [1, 2, 2], + [1, 2, 3], + [1, 3, 3], + [2, 2, 2], + [2, 2, 3], + [2, 3, 3], + [3, 3, 3], + ]) + + // 3-permutations of [1, 2, 3, 4] with repetition + let assert Ok(permutations) = + maths.list_combination_with_repetitions([1, 2, 3, 4], 3) + + permutations + |> yielder.to_list() + |> should.equal([ + [1, 1, 1], + [1, 1, 2], + [1, 1, 3], + [1, 1, 4], + [1, 2, 2], + [1, 2, 3], + [1, 2, 4], + [1, 3, 3], + [1, 3, 4], + [1, 4, 4], + [2, 2, 2], + [2, 2, 3], + [2, 2, 4], + [2, 3, 3], + [2, 3, 4], + [2, 4, 4], + [3, 3, 3], + [3, 3, 4], + [3, 4, 4], + [4, 4, 4], + ]) + + // Repetition creates more possibilities even with identical elements + let assert Ok(combinations) = + [1.0, 1.0] + |> maths.list_combination_with_repetitions(2) + combinations + |> yielder.to_list() + |> should.equal([[1.0, 1.0], [1.0, 1.0], [1.0, 1.0]]) + + let assert Ok(combinations) = + ["a", "b", "c", "d", "e"] + |> maths.list_combination_with_repetitions(5) + combinations + |> yielder.to_list() + |> list.length() + |> should.equal(126) +} + +pub fn list_combination_without_repetitions_test() { + // Invalid input: k > n should return an error without repetition + [1, 2] + |> maths.list_combination(3) + |> should.be_error() + // Valid input: Empty list should return a single empty combination let assert Ok(combinations) = [] - |> combinatorics.list_combination( - 0, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(0) combinations - |> iterator.to_list() - |> should.equal([[]]) - - let assert Ok(combinations) = - [] - |> combinatorics.list_combination( - 0, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[]]) // 1-combination of [1, 2] without and with repetition let assert Ok(combinations) = [1, 2] - |> combinatorics.list_combination( - 1, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(1) combinations - |> iterator.to_list() - |> should.equal([[1], [2]]) - - let assert Ok(combinations) = - [1, 2] - |> combinatorics.list_combination( - 1, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[1], [2]]) // 2-combination of [1, 2] without and with repetition let assert Ok(combinations) = [1, 2] - |> combinatorics.list_combination( - 2, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(2) combinations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[1, 2]]) - let assert Ok(combinations) = - [1, 2] - |> combinatorics.list_combination( - 2, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() - |> should.equal([[1, 1], [1, 2], [2, 2]]) - // 2-combination of [1, 2, 3, 4] without and with repetition let assert Ok(combinations) = [1, 2, 3, 4] - |> combinatorics.list_combination( - 2, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(2) combinations - |> iterator.to_list() - |> set.from_list() - |> should.equal( - set.from_list([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]), - ) - - let assert Ok(combinations) = - [1, 2, 3, 4] - |> combinatorics.list_combination( - 2, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() - |> set.from_list() - |> should.equal( - set.from_list([ - [1, 1], - [1, 2], - [1, 3], - [1, 4], - [2, 2], - [2, 3], - [2, 4], - [3, 3], - [3, 4], - [4, 4], - ]), - ) + |> yielder.to_list() + |> should.equal([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]) // 3-combination of [1, 2, 3, 4] without repetition let assert Ok(combinations) = [1, 2, 3, 4] - |> combinatorics.list_combination( - 3, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(3) combinations - |> iterator.to_list() - |> set.from_list() - |> should.equal(set.from_list([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]])) - - // 3-combination of [1, 2, 3] with repetition - let assert Ok(combinations) = - [1, 2, 3] - |> combinatorics.list_combination( - 3, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() - |> set.from_list() - |> should.equal( - set.from_list([ - [1, 1, 1], - [1, 1, 2], - [1, 1, 3], - [1, 2, 2], - [1, 2, 3], - [1, 3, 3], - [2, 2, 2], - [2, 2, 3], - [2, 3, 3], - [3, 3, 3], - ]), - ) + |> yielder.to_list() + |> should.equal([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]) // Combinations treat repeated elements as distinct in certain scenarios let assert Ok(combinations) = [1.0, 1.0] - |> combinatorics.list_combination( - 2, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(2) combinations - |> iterator.to_list() + |> yielder.to_list() |> should.equal([[1.0, 1.0]]) - // Repetition creates more possibilities even with identical elements - let assert Ok(combinations) = - [1.0, 1.0] - |> combinatorics.list_combination( - 2, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() - |> should.equal([[1.0, 1.0], [1.0, 1.0], [1.0, 1.0]]) - // Large input: Ensure correct number of combinations is generated let assert Ok(combinations) = ["a", "b", "c", "d", "e"] - |> combinatorics.list_combination( - 5, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(5) combinations - |> iterator.to_list() + |> yielder.to_list() |> list.length() |> should.equal(1) - - let assert Ok(combinations) = - ["a", "b", "c", "d", "e"] - |> combinatorics.list_combination( - 5, - option.Some(combinatorics.WithRepetitions), - ) - combinations - |> iterator.to_list() - |> list.length() - |> should.equal(126) } pub fn combination_alignment_test() { @@ -646,20 +632,12 @@ pub fn combination_alignment_test() { let arr = ["a", "b", "c", "d", "e", "f"] let length = list.length(arr) - let assert Ok(number_of_combinations) = - combinatorics.combination( - length, - length, - option.Some(combinatorics.WithoutRepetitions), - ) + let assert Ok(number_of_combinations) = maths.combination(length, length) let assert Ok(combinations) = arr - |> combinatorics.list_combination( - length, - option.Some(combinatorics.WithoutRepetitions), - ) + |> maths.list_combination(length) combinations - |> iterator.to_list() + |> yielder.to_list() |> list.length() |> should.equal(number_of_combinations) @@ -668,40 +646,12 @@ pub fn combination_alignment_test() { let length = list.length(arr) let assert Ok(number_of_combinations) = - combinatorics.combination( - length, - length, - option.Some(combinatorics.WithRepetitions), - ) + maths.combination_with_repetitions(length, length) let assert Ok(combinations) = arr - |> combinatorics.list_combination( - length, - option.Some(combinatorics.WithRepetitions), - ) + |> maths.list_combination_with_repetitions(length) combinations - |> iterator.to_list() + |> yielder.to_list() |> list.length() |> should.equal(number_of_combinations) } - -pub fn example_test() { - // Cartesian product of two empty sets - set.from_list([]) - |> combinatorics.cartesian_product(set.from_list([])) - |> should.equal(set.from_list([])) - - // Cartesian product of two sets with numeric values - set.from_list([1.0, 10.0]) - |> combinatorics.cartesian_product(set.from_list([1.0, 2.0])) - |> should.equal( - set.from_list([#(1.0, 1.0), #(1.0, 2.0), #(10.0, 1.0), #(10.0, 2.0)]), - ) - - // Cartesian product of two sets with different types - set.from_list(["1", "10"]) - |> combinatorics.cartesian_product(set.from_list([1.0, 2.0])) - |> should.equal( - set.from_list([#("1", 1.0), #("1", 2.0), #("10", 1.0), #("10", 2.0)]), - ) -} diff --git a/test/gleam_community/conversion_test.gleam b/test/gleam_community/conversion_test.gleam new file mode 100644 index 0000000..081089c --- /dev/null +++ b/test/gleam_community/conversion_test.gleam @@ -0,0 +1,113 @@ +import gleam/float +import gleam_community/maths +import gleeunit/should + +pub fn float_to_degree_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + maths.radians_to_degrees(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.radians_to_degrees(2.0 *. maths.pi()) + |> maths.is_close(360.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_to_radian_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + maths.degrees_to_radians(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.degrees_to_radians(360.0) + |> maths.is_close(2.0 *. maths.pi(), 0.0, tol) + |> should.be_true() +} + +pub fn float_cartesian_to_polar_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Test: Cartesian (1, 0) -> Polar (1, 0) + let #(r, theta) = maths.cartesian_to_polar(1.0, 0.0) + r + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + theta + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + // Test: Cartesian (0, 1) -> Polar (1, pi/2) + let #(r, theta) = maths.cartesian_to_polar(0.0, 1.0) + r + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + theta + |> maths.is_close(maths.pi() /. 2.0, 0.0, tol) + |> should.be_true() + + // Test: Cartesian (-1, 0) -> Polar (1, pi) + let #(r, theta) = maths.cartesian_to_polar(-1.0, 0.0) + r + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + theta + |> maths.is_close(maths.pi(), 0.0, tol) + |> should.be_true() + + // Test: Cartesian (0, -1) -> Polar (1, -pi/2) + let #(r, theta) = maths.cartesian_to_polar(0.0, -1.0) + r + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + theta + |> maths.is_close(-1.0 *. maths.pi() /. 2.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_polar_to_cartesian_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Test: Polar (1, 0) -> Cartesian (1, 0) + let #(x, y) = maths.polar_to_cartesian(1.0, 0.0) + x + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + y + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + // Test: Polar (1, pi/2) -> Cartesian (0, 1) + let #(x, y) = maths.polar_to_cartesian(1.0, maths.pi() /. 2.0) + x + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + y + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + // Test: Polar (1, pi) -> Cartesian (-1, 0) + let #(x, y) = maths.polar_to_cartesian(1.0, maths.pi()) + x + |> maths.is_close(-1.0, 0.0, tol) + |> should.be_true() + + y + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + // Test: Polar (1, -pi/2) -> Cartesian (0, -1) + let #(x, y) = maths.polar_to_cartesian(1.0, -1.0 *. maths.pi() /. 2.0) + x + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + y + |> maths.is_close(-1.0, 0.0, tol) + |> should.be_true() +} diff --git a/test/gleam_community/elementary_test.gleam b/test/gleam_community/elementary_test.gleam new file mode 100644 index 0000000..ed833f2 --- /dev/null +++ b/test/gleam_community/elementary_test.gleam @@ -0,0 +1,419 @@ +import gleam/float +import gleam_community/maths +import gleeunit/should + +pub fn float_acos_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = maths.acos(1.0) + result + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = maths.acos(0.5) + result + |> maths.is_close(1.047197, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.acos(1.1) + |> should.be_error() + + maths.acos(-1.1) + |> should.be_error() +} + +pub fn float_acosh_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = maths.acosh(1.0) + result + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.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 + maths.asin(0.0) + |> should.equal(Ok(0.0)) + + let assert Ok(tol) = float.power(10.0, -6.0) + let assert Ok(result) = maths.asin(0.5) + result + |> maths.is_close(0.523598, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.asin(1.1) + |> should.be_error() + + maths.asin(-1.1) + |> should.be_error() +} + +pub fn float_asinh_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.asinh(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.asinh(0.5) + |> maths.is_close(0.481211, 0.0, tol) + |> should.be_true() +} + +pub fn float_atan_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.atan(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.atan(0.5) + |> maths.is_close(0.463647, 0.0, tol) + |> should.be_true() +} + +pub fn math_atan2_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.atan2(0.0, 0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.atan2(0.0, 1.0) + |> maths.is_close(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 = maths.atan(1.0 /. 0.5) + maths.atan2(1.0, 0.5) + |> maths.is_close(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 = maths.pi() +. maths.atan(2.0 /. -1.5) + maths.atan2(2.0, -1.5) + |> maths.is_close(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 = maths.atan(-2.0 /. -1.5) -. maths.pi() + maths.atan2(-2.0, -1.5) + |> maths.is_close(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 = maths.pi() /. 2.0 + maths.atan2(1.5, 0.0) + |> maths.is_close(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 *. maths.pi() /. 2.0 + maths.atan2(-1.5, 0.0) + |> maths.is_close(result, 0.0, tol) + |> should.be_true() +} + +pub fn float_atanh_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = maths.atanh(0.0) + result + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = maths.atanh(0.5) + result + |> maths.is_close(0.549306, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.atanh(1.0) + |> should.be_error() + + maths.atanh(2.0) + |> should.be_error() + + maths.atanh(1.0) + |> should.be_error() + + maths.atanh(-2.0) + |> should.be_error() +} + +pub fn float_cos_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.cos(0.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.cos(maths.pi()) + |> maths.is_close(-1.0, 0.0, tol) + |> should.be_true() + + maths.cos(0.5) + |> maths.is_close(0.877582, 0.0, tol) + |> should.be_true() +} + +pub fn float_cosh_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.cosh(0.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.cosh(0.5) + |> maths.is_close(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. maths.cosh(1000.0) but this is a property of the + // runtime. +} + +pub fn float_sin_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.sin(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.sin(0.5 *. maths.pi()) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.sin(0.5) + |> maths.is_close(0.479425, 0.0, tol) + |> should.be_true() +} + +pub fn float_sinh_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.sinh(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.sinh(0.5) + |> maths.is_close(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. maths.sinh(1000.0) but this is a property of the + // runtime. +} + +pub fn math_tan_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.tan(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.tan(0.5) + |> maths.is_close(0.546302, 0.0, tol) + |> should.be_true() +} + +pub fn math_tanh_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.tanh(0.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.tanh(25.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.tanh(-25.0) + |> maths.is_close(-1.0, 0.0, tol) + |> should.be_true() + + maths.tanh(0.5) + |> maths.is_close(0.462117, 0.0, tol) + |> should.be_true() +} + +pub fn float_exponential_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.exponential(0.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.exponential(0.5) + |> maths.is_close(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. maths.exponential(1000.0) but this is a property of the + // runtime. +} + +pub fn float_natural_logarithm_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.natural_logarithm(1.0) + |> should.equal(Ok(0.0)) + + let assert Ok(result) = maths.natural_logarithm(0.5) + result + |> maths.is_close(-0.693147, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.natural_logarithm(-1.0) + |> should.be_error() +} + +pub fn float_logarithm_test() { + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.logarithm(10.0, 10.0) + |> should.equal(Ok(1.0)) + + maths.logarithm(10.0, 100.0) + |> should.equal(Ok(0.5)) + + maths.logarithm(1.0, 0.25) + |> should.equal(Ok(0.0)) + + // Check that we get an error when the function is evaluated + // outside its domain + maths.logarithm(1.0, 1.0) + |> should.be_error() + + maths.logarithm(10.0, 1.0) + |> should.be_error() + + maths.logarithm(-1.0, 1.0) + |> should.be_error() + + maths.logarithm(1.0, 10.0) + |> should.equal(Ok(0.0)) + + maths.logarithm(maths.e(), maths.e()) + |> should.equal(Ok(1.0)) + + maths.logarithm(-1.0, 2.0) + |> should.be_error() +} + +pub fn float_logarithm_2_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + maths.logarithm_2(1.0) + |> should.equal(Ok(0.0)) + + maths.logarithm_2(2.0) + |> should.equal(Ok(1.0)) + + let assert Ok(result) = maths.logarithm_2(5.0) + result + |> maths.is_close(2.321928, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.logarithm_2(-1.0) + |> should.be_error() +} + +pub fn float_logarithm_10_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = maths.logarithm_10(1.0) + result + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = maths.logarithm_10(10.0) + result + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = maths.logarithm_10(50.0) + result + |> maths.is_close(1.69897, 0.0, tol) + |> should.be_true() + + // Check that we get an error when the function is evaluated + // outside its domain + maths.logarithm_10(-1.0) + |> should.be_error() +} + +pub fn float_nth_root_test() { + maths.nth_root(9.0, 2) + |> should.equal(Ok(3.0)) + + maths.nth_root(27.0, 3) + |> should.equal(Ok(3.0)) + + maths.nth_root(1.0, 4) + |> should.equal(Ok(1.0)) + + maths.nth_root(256.0, 4) + |> should.equal(Ok(4.0)) + + // An error should be returned as an imaginary number would otherwise + // have to be returned + maths.nth_root(-1.0, 4) + |> should.be_error() +} + +pub fn float_constants_test() { + let assert Ok(tolerance) = float.power(10.0, -12.0) + + // Test that the constant is approximately equal to 2.7128... + maths.e() + |> maths.is_close(2.7182818284590452353602, 0.0, tolerance) + |> should.be_true() + + // Test that the constant is approximately equal to 2.7128... + maths.pi() + |> maths.is_close(3.14159265359, 0.0, tolerance) + |> should.be_true() + + // Test that the constant is approximately equal to 1.6180... + maths.golden_ratio() + |> maths.is_close(1.618033988749895, 0.0, tolerance) + |> should.be_true() +} diff --git a/test/gleam_community/maths/arithmetics_test.gleam b/test/gleam_community/maths/arithmetics_test.gleam deleted file mode 100644 index 3643561..0000000 --- a/test/gleam_community/maths/arithmetics_test.gleam +++ /dev/null @@ -1,227 +0,0 @@ -import gleam/option -import gleam_community/maths/arithmetics -import gleeunit/should - -pub fn int_gcd_test() { - arithmetics.gcd(1, 1) - |> should.equal(1) - - arithmetics.gcd(100, 10) - |> should.equal(10) - - arithmetics.gcd(10, 100) - |> should.equal(10) - - arithmetics.gcd(100, -10) - |> should.equal(10) - - arithmetics.gcd(-36, -17) - |> should.equal(1) - - arithmetics.gcd(-30, -42) - |> should.equal(6) -} - -pub fn int_euclidean_modulo_test() { - // Base Case: Positive x, Positive y - // Note that the truncated, floored, and euclidean - // definitions should agree for this base case - arithmetics.int_euclidean_modulo(15, 4) - |> should.equal(3) - - // Case: Positive x, Negative y - arithmetics.int_euclidean_modulo(15, -4) - |> should.equal(3) - - // Case: Negative x, Positive y - arithmetics.int_euclidean_modulo(-15, 4) - |> should.equal(1) - - // Case: Negative x, Negative y - arithmetics.int_euclidean_modulo(-15, -4) - |> should.equal(1) - - // Case: Positive x, Zero y - arithmetics.int_euclidean_modulo(5, 0) - |> should.equal(0) - - // Case: Zero x, Negative y - arithmetics.int_euclidean_modulo(0, 5) - |> should.equal(0) -} - -pub fn int_lcm_test() { - arithmetics.lcm(1, 1) - |> should.equal(1) - - arithmetics.lcm(100, 10) - |> should.equal(100) - - arithmetics.lcm(10, 100) - |> should.equal(100) - - arithmetics.lcm(100, -10) - |> should.equal(100) - - arithmetics.lcm(-36, -17) - |> should.equal(612) - - arithmetics.lcm(-30, -42) - |> should.equal(210) -} - -pub fn int_proper_divisors_test() { - arithmetics.proper_divisors(2) - |> should.equal([1]) - - arithmetics.proper_divisors(6) - |> should.equal([1, 2, 3]) - - arithmetics.proper_divisors(13) - |> should.equal([1]) - - arithmetics.proper_divisors(18) - |> should.equal([1, 2, 3, 6, 9]) -} - -pub fn int_divisors_test() { - arithmetics.divisors(2) - |> should.equal([1, 2]) - - arithmetics.divisors(6) - |> should.equal([1, 2, 3, 6]) - - arithmetics.divisors(13) - |> should.equal([1, 13]) - - arithmetics.divisors(18) - |> should.equal([1, 2, 3, 6, 9, 18]) -} - -pub fn float_list_sum_test() { - // An empty list returns 0 - [] - |> arithmetics.float_sum(option.None) - |> should.equal(0.0) - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> arithmetics.float_sum(option.None) - |> should.equal(6.0) - - [-2.0, 4.0, 6.0] - |> arithmetics.float_sum(option.None) - |> should.equal(8.0) -} - -pub fn int_list_sum_test() { - // An empty list returns 0 - [] - |> arithmetics.int_sum() - |> should.equal(0) - - // Valid input returns a result - [1, 2, 3] - |> arithmetics.int_sum() - |> should.equal(6) - - [-2, 4, 6] - |> arithmetics.int_sum() - |> should.equal(8) -} - -pub fn float_list_product_test() { - // An empty list returns 0 - [] - |> arithmetics.float_product(option.None) - |> should.equal(Ok(1.0)) - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> arithmetics.float_product(option.None) - |> should.equal(Ok(6.0)) - - [-2.0, 4.0, 6.0] - |> arithmetics.float_product(option.None) - |> should.equal(Ok(-48.0)) -} - -pub fn int_list_product_test() { - // An empty list returns 0 - [] - |> arithmetics.int_product() - |> should.equal(1) - - // Valid input returns a result - [1, 2, 3] - |> arithmetics.int_product() - |> should.equal(6) - - [-2, 4, 6] - |> arithmetics.int_product() - |> should.equal(-48) -} - -pub fn float_list_cumulative_sum_test() { - // An empty lists returns an empty list - [] - |> arithmetics.float_cumulative_sum() - |> should.equal([]) - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> arithmetics.float_cumulative_sum() - |> should.equal([1.0, 3.0, 6.0]) - - [-2.0, 4.0, 6.0] - |> arithmetics.float_cumulative_sum() - |> should.equal([-2.0, 2.0, 8.0]) -} - -pub fn int_list_cumulative_sum_test() { - // An empty lists returns an empty list - [] - |> arithmetics.int_cumulative_sum() - |> should.equal([]) - - // Valid input returns a result - [1, 2, 3] - |> arithmetics.int_cumulative_sum() - |> should.equal([1, 3, 6]) - - [-2, 4, 6] - |> arithmetics.int_cumulative_sum() - |> should.equal([-2, 2, 8]) -} - -pub fn float_list_cumulative_product_test() { - // An empty lists returns an empty list - [] - |> arithmetics.float_cumulative_product() - |> should.equal([]) - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> arithmetics.float_cumulative_product() - |> should.equal([1.0, 2.0, 6.0]) - - [-2.0, 4.0, 6.0] - |> arithmetics.float_cumulative_product() - |> should.equal([-2.0, -8.0, -48.0]) -} - -pub fn int_list_cumulative_product_test() { - // An empty lists returns an empty list - [] - |> arithmetics.int_cumulative_product() - |> should.equal([]) - - // Valid input returns a result - [1, 2, 3] - |> arithmetics.int_cumulative_product() - |> should.equal([1, 2, 6]) - - [-2, 4, 6] - |> arithmetics.int_cumulative_product() - |> should.equal([-2, -8, -48]) -} diff --git a/test/gleam_community/maths/conversion_test.gleam b/test/gleam_community/maths/conversion_test.gleam deleted file mode 100644 index 0a55a10..0000000 --- a/test/gleam_community/maths/conversion_test.gleam +++ /dev/null @@ -1,39 +0,0 @@ -import gleam_community/maths/conversion -import gleam_community/maths/elementary -import gleam_community/maths/predicates -import gleeunit/should - -pub fn float_to_degree_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - conversion.radians_to_degrees(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - conversion.radians_to_degrees(2.0 *. elementary.pi()) - |> predicates.is_close(360.0, 0.0, tol) - |> should.be_true() -} - -pub fn float_to_radian_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - conversion.degrees_to_radians(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - conversion.degrees_to_radians(360.0) - |> predicates.is_close(2.0 *. elementary.pi(), 0.0, tol) - |> should.be_true() -} - -pub fn float_to_int_test() { - conversion.float_to_int(12.0654) - |> should.equal(12) -} - -pub fn int_to_float_test() { - conversion.int_to_float(-1) - |> should.equal(-1.0) - - conversion.int_to_float(1) - |> should.equal(1.0) -} diff --git a/test/gleam_community/maths/elementary_test.gleam b/test/gleam_community/maths/elementary_test.gleam deleted file mode 100644 index a547f87..0000000 --- a/test/gleam_community/maths/elementary_test.gleam +++ /dev/null @@ -1,486 +0,0 @@ -import gleam/option -import gleam_community/maths/elementary -import gleam_community/maths/predicates -import gleeunit/should - -pub fn float_acos_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - let assert Ok(result) = elementary.acos(1.0) - result - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = elementary.acos(0.5) - result - |> predicates.is_close(1.047197, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.acos(1.1) - |> should.be_error() - - elementary.acos(-1.1) - |> should.be_error() -} - -pub fn float_acosh_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - let assert Ok(result) = elementary.acosh(1.0) - result - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.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 - elementary.asin(0.0) - |> should.equal(Ok(0.0)) - - let assert Ok(tol) = elementary.power(10.0, -6.0) - let assert Ok(result) = elementary.asin(0.5) - result - |> predicates.is_close(0.523598, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.asin(1.1) - |> should.be_error() - - elementary.asin(-1.1) - |> should.be_error() -} - -pub fn float_asinh_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.asinh(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.asinh(0.5) - |> predicates.is_close(0.481211, 0.0, tol) - |> should.be_true() -} - -pub fn float_atan_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.atan(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.atan(0.5) - |> predicates.is_close(0.463647, 0.0, tol) - |> should.be_true() -} - -pub fn math_atan2_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.atan2(0.0, 0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.atan2(0.0, 1.0) - |> predicates.is_close(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 = elementary.atan(1.0 /. 0.5) - elementary.atan2(1.0, 0.5) - |> predicates.is_close(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 = elementary.pi() +. elementary.atan(2.0 /. -1.5) - elementary.atan2(2.0, -1.5) - |> predicates.is_close(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 = elementary.atan(-2.0 /. -1.5) -. elementary.pi() - elementary.atan2(-2.0, -1.5) - |> predicates.is_close(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 = elementary.pi() /. 2.0 - elementary.atan2(1.5, 0.0) - |> predicates.is_close(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 *. elementary.pi() /. 2.0 - elementary.atan2(-1.5, 0.0) - |> predicates.is_close(result, 0.0, tol) - |> should.be_true() -} - -pub fn float_atanh_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - let assert Ok(result) = elementary.atanh(0.0) - result - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = elementary.atanh(0.5) - result - |> predicates.is_close(0.549306, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.atanh(1.0) - |> should.be_error() - - elementary.atanh(2.0) - |> should.be_error() - - elementary.atanh(1.0) - |> should.be_error() - - elementary.atanh(-2.0) - |> should.be_error() -} - -pub fn float_cos_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.cos(0.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - elementary.cos(elementary.pi()) - |> predicates.is_close(-1.0, 0.0, tol) - |> should.be_true() - - elementary.cos(0.5) - |> predicates.is_close(0.877582, 0.0, tol) - |> should.be_true() -} - -pub fn float_cosh_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.cosh(0.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - elementary.cosh(0.5) - |> predicates.is_close(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. elementary.cosh(1000.0) but this is a property of the - // runtime. -} - -pub fn float_sin_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.sin(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.sin(0.5 *. elementary.pi()) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - elementary.sin(0.5) - |> predicates.is_close(0.479425, 0.0, tol) - |> should.be_true() -} - -pub fn float_sinh_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.sinh(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.sinh(0.5) - |> predicates.is_close(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. elementary.sinh(1000.0) but this is a property of the - // runtime. -} - -pub fn math_tan_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.tan(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.tan(0.5) - |> predicates.is_close(0.546302, 0.0, tol) - |> should.be_true() -} - -pub fn math_tanh_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.tanh(0.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - elementary.tanh(25.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - elementary.tanh(-25.0) - |> predicates.is_close(-1.0, 0.0, tol) - |> should.be_true() - - elementary.tanh(0.5) - |> predicates.is_close(0.462117, 0.0, tol) - |> should.be_true() -} - -pub fn float_exponential_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.exponential(0.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - elementary.exponential(0.5) - |> predicates.is_close(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. elementary.exponential(1000.0) but this is a property of the - // runtime. -} - -pub fn float_natural_logarithm_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.natural_logarithm(1.0) - |> should.equal(Ok(0.0)) - - let assert Ok(result) = elementary.natural_logarithm(0.5) - result - |> predicates.is_close(-0.693147, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.natural_logarithm(-1.0) - |> should.be_error() -} - -pub fn float_logarithm_test() { - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.logarithm(10.0, option.Some(10.0)) - |> should.equal(Ok(1.0)) - - elementary.logarithm(10.0, option.Some(100.0)) - |> should.equal(Ok(0.5)) - - elementary.logarithm(1.0, option.Some(0.25)) - |> should.equal(Ok(0.0)) - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.logarithm(1.0, option.Some(1.0)) - |> should.be_error() - - elementary.logarithm(10.0, option.Some(1.0)) - |> should.be_error() - - elementary.logarithm(-1.0, option.Some(1.0)) - |> should.be_error() - - elementary.logarithm(1.0, option.Some(10.0)) - |> should.equal(Ok(0.0)) - - elementary.logarithm(elementary.e(), option.Some(elementary.e())) - |> should.equal(Ok(1.0)) - - elementary.logarithm(-1.0, option.Some(2.0)) - |> should.be_error() -} - -pub fn float_logarithm_2_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - elementary.logarithm_2(1.0) - |> should.equal(Ok(0.0)) - - elementary.logarithm_2(2.0) - |> should.equal(Ok(1.0)) - - let assert Ok(result) = elementary.logarithm_2(5.0) - result - |> predicates.is_close(2.321928, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.logarithm_2(-1.0) - |> should.be_error() -} - -pub fn float_logarithm_10_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - let assert Ok(result) = elementary.logarithm_10(1.0) - result - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = elementary.logarithm_10(10.0) - result - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = elementary.logarithm_10(50.0) - result - |> predicates.is_close(1.69897, 0.0, tol) - |> should.be_true() - - // Check that we get an error when the function is evaluated - // outside its domain - elementary.logarithm_10(-1.0) - |> should.be_error() -} - -pub fn float_power_test() { - elementary.power(2.0, 2.0) - |> should.equal(Ok(4.0)) - - elementary.power(-5.0, 3.0) - |> should.equal(Ok(-125.0)) - - elementary.power(10.5, 0.0) - |> should.equal(Ok(1.0)) - - elementary.power(16.0, 0.5) - |> should.equal(Ok(4.0)) - - elementary.power(2.0, -1.0) - |> should.equal(Ok(0.5)) - - elementary.power(2.0, -1.0) - |> should.equal(Ok(0.5)) - - // When y = 0, the result should universally be 1, regardless of the value of x - elementary.power(10.0, 0.0) - |> should.equal(Ok(1.0)) - elementary.power(-10.0, 0.0) - |> should.equal(Ok(1.0)) - - elementary.power(2.0, -1.0) - |> should.equal(Ok(0.5)) - - // elementary.power(-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 - elementary.power(-1.0, 0.5) - |> should.be_error() - - // Check another case with a negative base and fractional exponent - elementary.power(-1.5, 1.5) - |> should.be_error() - - // elementary.power(0.0, -1.0) is equivalent to 1. /. 0 and is expected - // to be an error - elementary.power(0.0, -1.0) - |> should.be_error() - - // Check that a negative base and exponent is fine as long as the - // exponent is not fractional - elementary.power(-2.0, -1.0) - |> should.equal(Ok(-0.5)) -} - -pub fn float_square_root_test() { - elementary.square_root(1.0) - |> should.equal(Ok(1.0)) - - elementary.square_root(9.0) - |> should.equal(Ok(3.0)) - - // An error should be returned as an imaginary number would otherwise - // have to be returned - elementary.square_root(-1.0) - |> should.be_error() -} - -pub fn float_cube_root_test() { - elementary.cube_root(1.0) - |> should.equal(Ok(1.0)) - - elementary.cube_root(27.0) - |> should.equal(Ok(3.0)) - - // An error should be returned as an imaginary number would otherwise - // have to be returned - elementary.cube_root(-1.0) - |> should.be_error() -} - -pub fn float_nth_root_test() { - elementary.nth_root(9.0, 2) - |> should.equal(Ok(3.0)) - - elementary.nth_root(27.0, 3) - |> should.equal(Ok(3.0)) - - elementary.nth_root(1.0, 4) - |> should.equal(Ok(1.0)) - - elementary.nth_root(256.0, 4) - |> should.equal(Ok(4.0)) - - // An error should be returned as an imaginary number would otherwise - // have to be returned - elementary.nth_root(-1.0, 4) - |> should.be_error() -} - -pub fn float_constants_test() { - elementary.e() - |> predicates.is_close(2.71828, 0.0, 0.00001) - |> should.be_true() - - elementary.pi() - |> predicates.is_close(3.14159, 0.0, 0.00001) - |> should.be_true() -} diff --git a/test/gleam_community/maths/metrics_test.gleam b/test/gleam_community/maths/metrics_test.gleam deleted file mode 100644 index 4debea5..0000000 --- a/test/gleam_community/maths/metrics_test.gleam +++ /dev/null @@ -1,629 +0,0 @@ -import gleam/option -import gleam/set -import gleam_community/maths/elementary -import gleam_community/maths/metrics -import gleam_community/maths/predicates -import gleeunit/should - -pub fn float_list_norm_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // An empty lists returns 0.0 - [] - |> metrics.norm(1.0, option.None) - |> should.equal(Ok(0.0)) - - // Check that the function agrees, at some arbitrary input - // points, with known function values - let assert Ok(result) = - [1.0, 1.0, 1.0] - |> metrics.norm(1.0, option.None) - result - |> predicates.is_close(3.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - [1.0, 1.0, 1.0] - |> metrics.norm(-1.0, option.None) - result - |> predicates.is_close(0.3333333333333333, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - [-1.0, -1.0, -1.0] - |> metrics.norm(-1.0, option.None) - result - |> predicates.is_close(0.3333333333333333, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - [-1.0, -1.0, -1.0] - |> metrics.norm(1.0, option.None) - result - |> predicates.is_close(3.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - [-1.0, -2.0, -3.0] - |> metrics.norm(-10.0, option.None) - result - |> predicates.is_close(0.9999007044905545, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - [-1.0, -2.0, -3.0] - |> metrics.norm(-100.0, option.None) - result - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - [-1.0, -2.0, -3.0] - |> metrics.norm(2.0, option.None) - result - |> predicates.is_close(3.7416573867739413, 0.0, tol) - |> should.be_true() -} - -pub fn float_list_manhattan_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Empty lists returns an error - metrics.manhattan_distance([], [], option.None) - |> should.be_error() - - // Differing lengths returns error - metrics.manhattan_distance([], [1.0], option.None) - |> should.be_error() - - // Try with valid input (same as Minkowski distance with p = 1) - let assert Ok(result) = - metrics.manhattan_distance([0.0, 0.0], [1.0, 2.0], option.None) - result - |> predicates.is_close(3.0, 0.0, tol) - |> should.be_true() - - metrics.manhattan_distance([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], option.None) - |> should.equal(Ok(9.0)) - - metrics.manhattan_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([1.0, 1.0, 1.0]), - ) - |> should.equal(Ok(9.0)) - - metrics.manhattan_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([1.0, 2.0, 3.0]), - ) - |> should.equal(Ok(18.0)) - - // Try invalid input with weights (different sized lists returns an error) - metrics.manhattan_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([7.0, 8.0]), - ) - |> should.be_error() - - // Try invalid input with weights that are negative - metrics.manhattan_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([-7.0, -8.0, -9.0]), - ) - |> should.be_error() -} - -pub fn float_list_minkowski_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Empty lists returns an error - metrics.minkowski_distance([], [], 1.0, option.None) - |> should.be_error() - - // Differing lengths returns error - metrics.minkowski_distance([], [1.0], 1.0, option.None) - |> should.be_error() - - // Test order < 1 - metrics.minkowski_distance([0.0, 0.0], [0.0, 0.0], -1.0, option.None) - |> should.be_error() - - // Check that the function agrees, at some arbitrary input - // points, with known function values - let assert Ok(result) = - metrics.minkowski_distance([1.0, 1.0], [1.0, 1.0], 1.0, option.None) - result - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 10.0, option.None) - result - |> predicates.is_close(1.0717734625362931, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 100.0, option.None) - result - |> predicates.is_close(1.0069555500567189, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 10.0, option.None) - result - |> predicates.is_close(1.0717734625362931, 0.0, tol) - |> should.be_true() - - // Euclidean distance (p = 2) - let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 2.0, option.None) - result - |> predicates.is_close(2.23606797749979, 0.0, tol) - |> should.be_true() - - // Manhattan distance (p = 1) - let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 1.0, option.None) - result - |> predicates.is_close(3.0, 0.0, tol) - |> should.be_true() - - // Try different valid input - let assert Ok(result) = - metrics.minkowski_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - 4.0, - option.None, - ) - result - |> predicates.is_close(3.9482220388574776, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.minkowski_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - 4.0, - option.Some([1.0, 1.0, 1.0]), - ) - result - |> predicates.is_close(3.9482220388574776, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.minkowski_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - 4.0, - option.Some([1.0, 2.0, 3.0]), - ) - result - |> predicates.is_close(4.6952537402198615, 0.0, tol) - |> should.be_true() - - // Try invalid input with weights (different sized lists returns an error) - metrics.minkowski_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - 2.0, - option.Some([7.0, 8.0]), - ) - |> should.be_error() - - // Try invalid input with weights that are negative - metrics.minkowski_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - 2.0, - option.Some([-7.0, -8.0, -9.0]), - ) - |> should.be_error() -} - -pub fn float_list_euclidean_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Empty lists returns an error - metrics.euclidean_distance([], [], option.None) - |> should.be_error() - - // Differing lengths returns error - metrics.euclidean_distance([], [1.0], option.None) - |> should.be_error() - - // Try with valid input (same as Minkowski distance with p = 2) - let assert Ok(result) = - metrics.euclidean_distance([0.0, 0.0], [1.0, 2.0], option.None) - result - |> predicates.is_close(2.23606797749979, 0.0, tol) - |> should.be_true() - - // Try different valid input - let assert Ok(result) = - metrics.euclidean_distance([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], option.None) - result - |> predicates.is_close(5.196152422706632, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.euclidean_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([1.0, 1.0, 1.0]), - ) - result - |> predicates.is_close(5.196152422706632, 0.0, tol) - |> should.be_true() - - let assert Ok(result) = - metrics.euclidean_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([1.0, 2.0, 3.0]), - ) - result - |> predicates.is_close(7.3484692283495345, 0.0, tol) - |> should.be_true() - - // Try invalid input with weights (different sized lists returns an error) - metrics.euclidean_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([7.0, 8.0]), - ) - |> should.be_error() - - // Try invalid input with weights that are negative - metrics.euclidean_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([-7.0, -8.0, -9.0]), - ) - |> should.be_error() -} - -pub fn mean_test() { - // An empty list returns an error - [] - |> metrics.mean() - |> should.be_error() - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> metrics.mean() - |> should.equal(Ok(2.0)) -} - -pub fn median_test() { - // An empty list returns an error - [] - |> metrics.median() - |> should.be_error() - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> metrics.median() - |> should.equal(Ok(2.0)) - - [1.0, 2.0, 3.0, 4.0] - |> metrics.median() - |> should.equal(Ok(2.5)) -} - -pub fn variance_test() { - // Degrees of freedom - let ddof = 1 - - // An empty list returns an error - [] - |> metrics.variance(ddof) - |> should.be_error() - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> metrics.variance(ddof) - |> should.equal(Ok(1.0)) -} - -pub fn standard_deviation_test() { - // Degrees of freedom - let ddof = 1 - - // An empty list returns an error - [] - |> metrics.standard_deviation(ddof) - |> should.be_error() - - // Valid input returns a result - [1.0, 2.0, 3.0] - |> metrics.standard_deviation(ddof) - |> should.equal(Ok(1.0)) -} - -pub fn jaccard_index_test() { - metrics.jaccard_index(set.from_list([]), set.from_list([])) - |> should.equal(0.0) - - let set_a = set.from_list([0, 1, 2, 5, 6, 8, 9]) - let set_b = set.from_list([0, 2, 3, 4, 5, 7, 9]) - metrics.jaccard_index(set_a, set_b) - |> should.equal(4.0 /. 10.0) - - let set_c = set.from_list([0, 1, 2, 3, 4, 5]) - let set_d = set.from_list([6, 7, 8, 9, 10]) - metrics.jaccard_index(set_c, set_d) - |> should.equal(0.0 /. 11.0) - - let set_e = set.from_list(["cat", "dog", "hippo", "monkey"]) - let set_f = set.from_list(["monkey", "rhino", "ostrich", "salmon"]) - metrics.jaccard_index(set_e, set_f) - |> should.equal(1.0 /. 7.0) -} - -pub fn sorensen_dice_coefficient_test() { - metrics.sorensen_dice_coefficient(set.from_list([]), set.from_list([])) - |> should.equal(0.0) - - let set_a = set.from_list([0, 1, 2, 5, 6, 8, 9]) - let set_b = set.from_list([0, 2, 3, 4, 5, 7, 9]) - metrics.sorensen_dice_coefficient(set_a, set_b) - |> should.equal(2.0 *. 4.0 /. { 7.0 +. 7.0 }) - - let set_c = set.from_list([0, 1, 2, 3, 4, 5]) - let set_d = set.from_list([6, 7, 8, 9, 10]) - metrics.sorensen_dice_coefficient(set_c, set_d) - |> should.equal(2.0 *. 0.0 /. { 6.0 +. 5.0 }) - - let set_e = set.from_list(["cat", "dog", "hippo", "monkey"]) - let set_f = set.from_list(["monkey", "rhino", "ostrich", "salmon", "spider"]) - metrics.sorensen_dice_coefficient(set_e, set_f) - |> should.equal(2.0 *. 1.0 /. { 4.0 +. 5.0 }) -} - -pub fn overlap_coefficient_test() { - metrics.overlap_coefficient(set.from_list([]), set.from_list([])) - |> should.equal(0.0) - - let set_a = set.from_list([0, 1, 2, 5, 6, 8, 9]) - let set_b = set.from_list([0, 2, 3, 4, 5, 7, 9]) - metrics.overlap_coefficient(set_a, set_b) - |> should.equal(4.0 /. 7.0) - - let set_c = set.from_list([0, 1, 2, 3, 4, 5]) - let set_d = set.from_list([6, 7, 8, 9, 10]) - metrics.overlap_coefficient(set_c, set_d) - |> should.equal(0.0 /. 5.0) - - let set_e = set.from_list(["horse", "dog", "hippo", "monkey", "bird"]) - let set_f = set.from_list(["monkey", "bird", "ostrich", "salmon"]) - metrics.overlap_coefficient(set_e, set_f) - |> should.equal(2.0 /. 4.0) -} - -pub fn cosine_similarity_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Empty lists returns an error - metrics.cosine_similarity([], [], option.None) - |> should.be_error() - - // One empty list returns an error - metrics.cosine_similarity([1.0, 2.0, 3.0], [], option.None) - |> should.be_error() - - // One empty list returns an error - metrics.cosine_similarity([], [1.0, 2.0, 3.0], option.None) - |> should.be_error() - - // Different sized lists returns an error - metrics.cosine_similarity([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) - |> should.be_error() - - // Two orthogonal vectors (represented by lists) - metrics.cosine_similarity([-1.0, 1.0, 0.0], [1.0, 1.0, -1.0], option.None) - |> should.equal(Ok(0.0)) - - // Two identical (parallel) vectors (represented by lists) - metrics.cosine_similarity([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], option.None) - |> should.equal(Ok(1.0)) - - // Two parallel, but oppositely oriented vectors (represented by lists) - metrics.cosine_similarity([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0], option.None) - |> should.equal(Ok(-1.0)) - - // Try with arbitrary valid input - let assert Ok(result) = - metrics.cosine_similarity([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], option.None) - result - |> predicates.is_close(0.9746318461970762, 0.0, tol) - |> should.be_true() - - // Try valid input with weights - let assert Ok(result) = - metrics.cosine_similarity( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([1.0, 1.0, 1.0]), - ) - result - |> predicates.is_close(0.9746318461970762, 0.0, tol) - |> should.be_true() - - // Try with different weights - let assert Ok(result) = - metrics.cosine_similarity( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([1.0, 2.0, 3.0]), - ) - result - |> predicates.is_close(0.9855274566525745, 0.0, tol) - |> should.be_true() - - // Try invalid input with weights (different sized lists returns an error) - metrics.cosine_similarity( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([7.0, 8.0]), - ) - |> should.be_error() - - // Try invalid input with weights that are negative - metrics.cosine_similarity( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([-7.0, -8.0, -9.0]), - ) - |> should.be_error() -} - -pub fn chebyshev_distance_test() { - // Empty lists returns an error - metrics.chebyshev_distance([], []) - |> should.be_error() - - // One empty list returns an error - metrics.chebyshev_distance([1.0, 2.0, 3.0], []) - |> should.be_error() - - // One empty list returns an error - metrics.chebyshev_distance([], [1.0, 2.0, 3.0]) - |> should.be_error() - - // Different sized lists returns an error - metrics.chebyshev_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0]) - |> should.be_error() - - // Try different types of valid input - metrics.chebyshev_distance([1.0, 0.0], [0.0, 2.0]) - |> should.equal(Ok(2.0)) - - metrics.chebyshev_distance([1.0, 0.0], [2.0, 0.0]) - |> should.equal(Ok(1.0)) - - metrics.chebyshev_distance([1.0, 0.0], [-2.0, 0.0]) - |> should.equal(Ok(3.0)) - - metrics.chebyshev_distance([-5.0, -10.0, -3.0], [-1.0, -12.0, -3.0]) - |> should.equal(Ok(4.0)) - - metrics.chebyshev_distance([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]) - |> should.equal(Ok(0.0)) -} - -pub fn canberra_distance_test() { - // Empty lists returns an error - metrics.canberra_distance([], [], option.None) - |> should.be_error() - - // One empty list returns an error - metrics.canberra_distance([1.0, 2.0, 3.0], [], option.None) - |> should.be_error() - - // One empty list returns an error - metrics.canberra_distance([], [1.0, 2.0, 3.0], option.None) - |> should.be_error() - - // Different sized lists returns an error - metrics.canberra_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) - |> should.be_error() - - // Try different types of valid input - metrics.canberra_distance([0.0, 0.0], [0.0, 0.0], option.None) - |> should.equal(Ok(0.0)) - - metrics.canberra_distance([1.0, 2.0], [-2.0, -1.0], option.None) - |> should.equal(Ok(2.0)) - - metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.None) - |> should.equal(Ok(2.0)) - - metrics.canberra_distance([1.0, 0.0], [2.0, 0.0], option.None) - |> should.equal(Ok(1.0 /. 3.0)) - - metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([1.0, 1.0])) - |> should.equal(Ok(2.0)) - - metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([1.0, 0.5])) - |> should.equal(Ok(1.5)) - - metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([0.5, 0.5])) - |> should.equal(Ok(1.0)) - - // Different sized lists (weights) returns an error - metrics.canberra_distance( - [1.0, 2.0, 3.0], - [1.0, 2.0, 3.0], - option.Some([1.0]), - ) - |> should.be_error() - - // Try invalid input with weights that are negative - metrics.canberra_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([-7.0, -8.0, -9.0]), - ) - |> should.be_error() -} - -pub fn braycurtis_distance_test() { - // Empty lists returns an error - metrics.braycurtis_distance([], [], option.None) - |> should.be_error() - - // One empty list returns an error - metrics.braycurtis_distance([1.0, 2.0, 3.0], [], option.None) - |> should.be_error() - - // One empty list returns an error - metrics.braycurtis_distance([], [1.0, 2.0, 3.0], option.None) - |> should.be_error() - - // Different sized lists returns an error - metrics.braycurtis_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) - |> should.be_error() - - // Try different types of valid input - metrics.braycurtis_distance([0.0, 0.0], [0.0, 0.0], option.None) - |> should.equal(Ok(0.0)) - - metrics.braycurtis_distance([1.0, 2.0], [-2.0, -1.0], option.None) - |> should.equal(Ok(3.0)) - - metrics.braycurtis_distance([1.0, 0.0], [0.0, 2.0], option.None) - |> should.equal(Ok(1.0)) - - metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.None) - |> should.equal(Ok(0.4)) - - metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([1.0, 1.0])) - |> should.equal(Ok(0.4)) - - metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([0.5, 1.0])) - |> should.equal(Ok(0.375)) - - metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([0.25, 0.25])) - |> should.equal(Ok(0.4)) - - // Different sized lists (weights) returns an error - metrics.braycurtis_distance( - [1.0, 2.0, 3.0], - [1.0, 2.0, 3.0], - option.Some([1.0]), - ) - |> should.be_error() - - // Try invalid input with weights that are negative - metrics.braycurtis_distance( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - option.Some([-7.0, -8.0, -9.0]), - ) - |> should.be_error() -} diff --git a/test/gleam_community/maths/piecewise_test.gleam b/test/gleam_community/maths/piecewise_test.gleam deleted file mode 100644 index 81ab38e..0000000 --- a/test/gleam_community/maths/piecewise_test.gleam +++ /dev/null @@ -1,753 +0,0 @@ -import gleam/float -import gleam/int -import gleam/option -import gleam_community/maths/piecewise -import gleeunit/should - -pub fn float_ceiling_test() { - // Round 3. digit AFTER decimal point - piecewise.ceiling(12.0654, option.Some(3)) - |> should.equal(12.066) - - // Round 2. digit AFTER decimal point - piecewise.ceiling(12.0654, option.Some(2)) - |> should.equal(12.07) - - // Round 1. digit AFTER decimal point - piecewise.ceiling(12.0654, option.Some(1)) - |> should.equal(12.1) - - // Round 0. digit BEFORE decimal point - piecewise.ceiling(12.0654, option.Some(0)) - |> should.equal(13.0) - - // Round 1. digit BEFORE decimal point - piecewise.ceiling(12.0654, option.Some(-1)) - |> should.equal(20.0) - - // Round 2. digit BEFORE decimal point - piecewise.ceiling(12.0654, option.Some(-2)) - |> should.equal(100.0) - - // Round 3. digit BEFORE decimal point - piecewise.ceiling(12.0654, option.Some(-3)) - |> should.equal(1000.0) -} - -pub fn float_floor_test() { - // Round 3. digit AFTER decimal point - piecewise.floor(12.0654, option.Some(3)) - |> should.equal(12.065) - - // Round 2. digit AFTER decimal point - piecewise.floor(12.0654, option.Some(2)) - |> should.equal(12.06) - - // Round 1. digit AFTER decimal point - piecewise.floor(12.0654, option.Some(1)) - |> should.equal(12.0) - - // Round 0. digit BEFORE decimal point - piecewise.floor(12.0654, option.Some(0)) - |> should.equal(12.0) - - // Round 1. digit BEFORE decimal point - piecewise.floor(12.0654, option.Some(-1)) - |> should.equal(10.0) - - // Round 2. digit BEFORE decimal point - piecewise.floor(12.0654, option.Some(-2)) - |> should.equal(0.0) - - // Round 2. digit BEFORE decimal point - piecewise.floor(12.0654, option.Some(-3)) - |> should.equal(0.0) -} - -pub fn float_truncate_test() { - // Round 3. digit AFTER decimal point - piecewise.truncate(12.0654, option.Some(3)) - |> should.equal(12.065) - - // Round 2. digit AFTER decimal point - piecewise.truncate(12.0654, option.Some(2)) - |> should.equal(12.06) - - // Round 1. digit AFTER decimal point - piecewise.truncate(12.0654, option.Some(1)) - |> should.equal(12.0) - - // Round 0. digit BEFORE decimal point - piecewise.truncate(12.0654, option.Some(0)) - |> should.equal(12.0) - - // Round 1. digit BEFORE decimal point - piecewise.truncate(12.0654, option.Some(-1)) - |> should.equal(10.0) - - // Round 2. digit BEFORE decimal point - piecewise.truncate(12.0654, option.Some(-2)) - |> should.equal(0.0) - - // Round 2. digit BEFORE decimal point - piecewise.truncate(12.0654, option.Some(-3)) - |> should.equal(0.0) -} - -pub fn math_round_to_nearest_test() { - // Try with positive values - piecewise.round(1.5, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(2.0) - - piecewise.round(1.75, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(2.0) - - piecewise.round(2.0, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(2.0) - - piecewise.round(3.5, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(4.0) - - piecewise.round(4.5, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(4.0) - - // Try with negative values - piecewise.round(-3.5, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(-4.0) - - piecewise.round(-4.5, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(-4.0) - - // Round 3. digit AFTER decimal point - piecewise.round(12.0654, option.Some(3), option.Some(piecewise.RoundNearest)) - |> should.equal(12.065) - - // Round 2. digit AFTER decimal point - piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundNearest)) - |> should.equal(12.07) - - // Round 1. digit AFTER decimal point - piecewise.round(12.0654, option.Some(1), option.Some(piecewise.RoundNearest)) - |> should.equal(12.1) - - // Round 0. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(0), option.Some(piecewise.RoundNearest)) - |> should.equal(12.0) - - // Round 1. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(-1), option.Some(piecewise.RoundNearest)) - |> should.equal(10.0) - - // Round 2. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(-2), option.Some(piecewise.RoundNearest)) - |> should.equal(0.0) - - // Round 3. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(-3), option.Some(piecewise.RoundNearest)) - |> should.equal(0.0) -} - -pub fn math_round_up_test() { - // Note: Rounding mode "RoundUp" is an alias for the ceiling function - // Try with positive values - piecewise.round(0.45, option.Some(0), option.Some(piecewise.RoundUp)) - |> should.equal(1.0) - - piecewise.round(0.5, option.Some(0), option.Some(piecewise.RoundUp)) - |> should.equal(1.0) - - piecewise.round(0.45, option.Some(1), option.Some(piecewise.RoundUp)) - |> should.equal(0.5) - - piecewise.round(0.5, option.Some(1), option.Some(piecewise.RoundUp)) - |> should.equal(0.5) - - piecewise.round(0.455, option.Some(2), option.Some(piecewise.RoundUp)) - |> should.equal(0.46) - - piecewise.round(0.505, option.Some(2), option.Some(piecewise.RoundUp)) - |> should.equal(0.51) - - // Try with negative values - piecewise.round(-0.45, option.Some(0), option.Some(piecewise.RoundUp)) - |> should.equal(-0.0) - - piecewise.round(-0.5, option.Some(0), option.Some(piecewise.RoundUp)) - |> should.equal(-0.0) - - piecewise.round(-0.45, option.Some(1), option.Some(piecewise.RoundUp)) - |> should.equal(-0.4) - - piecewise.round(-0.5, option.Some(1), option.Some(piecewise.RoundUp)) - |> should.equal(-0.5) - - piecewise.round(-0.455, option.Some(2), option.Some(piecewise.RoundUp)) - |> should.equal(-0.45) - - piecewise.round(-0.505, option.Some(2), option.Some(piecewise.RoundUp)) - |> should.equal(-0.5) -} - -pub fn math_round_down_test() { - // Note: Rounding mode "RoundDown" is an alias for the floor function - // Try with positive values - piecewise.round(0.45, option.Some(0), option.Some(piecewise.RoundDown)) - |> should.equal(0.0) - - piecewise.round(0.5, option.Some(0), option.Some(piecewise.RoundDown)) - |> should.equal(0.0) - - piecewise.round(0.45, option.Some(1), option.Some(piecewise.RoundDown)) - |> should.equal(0.4) - - piecewise.round(0.5, option.Some(1), option.Some(piecewise.RoundDown)) - |> should.equal(0.5) - - piecewise.round(0.455, option.Some(2), option.Some(piecewise.RoundDown)) - |> should.equal(0.45) - - piecewise.round(0.505, option.Some(2), option.Some(piecewise.RoundDown)) - |> should.equal(0.5) - - // Try with negative values - piecewise.round(-0.45, option.Some(0), option.Some(piecewise.RoundDown)) - |> should.equal(-1.0) - - piecewise.round(-0.5, option.Some(0), option.Some(piecewise.RoundDown)) - |> should.equal(-1.0) - - piecewise.round(-0.45, option.Some(1), option.Some(piecewise.RoundDown)) - |> should.equal(-0.5) - - piecewise.round(-0.5, option.Some(1), option.Some(piecewise.RoundDown)) - |> should.equal(-0.5) - - piecewise.round(-0.455, option.Some(2), option.Some(piecewise.RoundDown)) - |> should.equal(-0.46) - - piecewise.round(-0.505, option.Some(2), option.Some(piecewise.RoundDown)) - |> should.equal(-0.51) -} - -pub fn math_round_to_zero_test() { - // Note: Rounding mode "RoundToZero" is an alias for the truncate function - // Try with positive values - piecewise.round(0.5, option.Some(0), option.Some(piecewise.RoundToZero)) - |> should.equal(0.0) - - piecewise.round(0.75, option.Some(0), option.Some(piecewise.RoundToZero)) - |> should.equal(0.0) - - piecewise.round(0.45, option.Some(1), option.Some(piecewise.RoundToZero)) - |> should.equal(0.4) - - piecewise.round(0.57, option.Some(1), option.Some(piecewise.RoundToZero)) - |> should.equal(0.5) - - piecewise.round(0.4575, option.Some(2), option.Some(piecewise.RoundToZero)) - |> should.equal(0.45) - - piecewise.round(0.5075, option.Some(2), option.Some(piecewise.RoundToZero)) - |> should.equal(0.5) - - // Try with negative values - piecewise.round(-0.5, option.Some(0), option.Some(piecewise.RoundToZero)) - |> should.equal(0.0) - - piecewise.round(-0.75, option.Some(0), option.Some(piecewise.RoundToZero)) - |> should.equal(0.0) - - piecewise.round(-0.45, option.Some(1), option.Some(piecewise.RoundToZero)) - |> should.equal(-0.4) - - piecewise.round(-0.57, option.Some(1), option.Some(piecewise.RoundToZero)) - |> should.equal(-0.5) - - piecewise.round(-0.4575, option.Some(2), option.Some(piecewise.RoundToZero)) - |> should.equal(-0.45) - - piecewise.round(-0.5075, option.Some(2), option.Some(piecewise.RoundToZero)) - |> should.equal(-0.5) -} - -pub fn math_round_ties_away_test() { - // Try with positive values - piecewise.round(1.4, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(1.0) - - piecewise.round(1.5, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(2.0) - - piecewise.round(2.5, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(3.0) - - // Try with negative values - piecewise.round(-1.4, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(-1.0) - - piecewise.round(-1.5, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(-2.0) - - piecewise.round(-2.0, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(-2.0) - - piecewise.round(-2.5, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(-3.0) - - // Round 3. digit AFTER decimal point - piecewise.round(12.0654, option.Some(3), option.Some(piecewise.RoundTiesAway)) - |> should.equal(12.065) - - // Round 2. digit AFTER decimal point - piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundTiesAway)) - |> should.equal(12.07) - - // Round 1. digit AFTER decimal point - piecewise.round(12.0654, option.Some(1), option.Some(piecewise.RoundTiesAway)) - |> should.equal(12.1) - - // Round 0. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(0), option.Some(piecewise.RoundTiesAway)) - |> should.equal(12.0) - - // Round 1. digit BEFORE decimal point - piecewise.round( - 12.0654, - option.Some(-1), - option.Some(piecewise.RoundTiesAway), - ) - |> should.equal(10.0) - - // Round 2. digit BEFORE decimal point - piecewise.round( - 12.0654, - option.Some(-2), - option.Some(piecewise.RoundTiesAway), - ) - |> should.equal(0.0) - - // Round 2. digit BEFORE decimal point - piecewise.round( - 12.0654, - option.Some(-3), - option.Some(piecewise.RoundTiesAway), - ) - |> should.equal(0.0) -} - -pub fn math_round_ties_up_test() { - // Try with positive values - piecewise.round(1.4, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(1.0) - - piecewise.round(1.5, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(2.0) - - piecewise.round(2.5, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(3.0) - - // Try with negative values - piecewise.round(-1.4, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(-1.0) - - piecewise.round(-1.5, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(-1.0) - - piecewise.round(-2.0, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(-2.0) - - piecewise.round(-2.5, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(-2.0) - - // Round 3. digit AFTER decimal point - piecewise.round(12.0654, option.Some(3), option.Some(piecewise.RoundTiesUp)) - |> should.equal(12.065) - - // Round 2. digit AFTER decimal point - piecewise.round(12.0654, option.Some(2), option.Some(piecewise.RoundTiesUp)) - |> should.equal(12.07) - - // Round 1. digit AFTER decimal point - piecewise.round(12.0654, option.Some(1), option.Some(piecewise.RoundTiesUp)) - |> should.equal(12.1) - - // Round 0. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(0), option.Some(piecewise.RoundTiesUp)) - |> should.equal(12.0) - - // Round 1. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(-1), option.Some(piecewise.RoundTiesUp)) - |> should.equal(10.0) - - // Round 2. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(-2), option.Some(piecewise.RoundTiesUp)) - |> should.equal(0.0) - - // Round 2. digit BEFORE decimal point - piecewise.round(12.0654, option.Some(-3), option.Some(piecewise.RoundTiesUp)) - |> should.equal(0.0) -} - -pub fn math_round_edge_cases_test() { - // The default number of digits is 0 if None is provided - piecewise.round(12.0654, option.None, option.Some(piecewise.RoundNearest)) - |> should.equal(12.0) - - // The default rounding mode is piecewise.RoundNearest if None is provided - piecewise.round(12.0654, option.None, option.None) - |> should.equal(12.0) -} - -pub fn float_absolute_value_test() { - piecewise.float_absolute_value(20.0) - |> should.equal(20.0) - - piecewise.float_absolute_value(-20.0) - |> should.equal(20.0) -} - -pub fn int_absolute_value_test() { - piecewise.int_absolute_value(20) - |> should.equal(20) - - piecewise.int_absolute_value(-20) - |> should.equal(20) -} - -pub fn float_absolute_difference_test() { - piecewise.float_absolute_difference(20.0, 15.0) - |> should.equal(5.0) - - piecewise.float_absolute_difference(-20.0, -15.0) - |> should.equal(5.0) - - piecewise.float_absolute_difference(20.0, -15.0) - |> should.equal(35.0) - - piecewise.float_absolute_difference(-20.0, 15.0) - |> should.equal(35.0) - - piecewise.float_absolute_difference(0.0, 0.0) - |> should.equal(0.0) - - piecewise.float_absolute_difference(1.0, 2.0) - |> should.equal(1.0) - - piecewise.float_absolute_difference(2.0, 1.0) - |> should.equal(1.0) - - piecewise.float_absolute_difference(-1.0, 0.0) - |> should.equal(1.0) - - piecewise.float_absolute_difference(0.0, -1.0) - |> should.equal(1.0) - - piecewise.float_absolute_difference(10.0, 20.0) - |> should.equal(10.0) - - piecewise.float_absolute_difference(-10.0, -20.0) - |> should.equal(10.0) - - piecewise.float_absolute_difference(-10.5, 10.5) - |> should.equal(21.0) -} - -pub fn int_absolute_difference_test() { - piecewise.int_absolute_difference(20, 15) - |> should.equal(5) - - piecewise.int_absolute_difference(-20, -15) - |> should.equal(5) - - piecewise.int_absolute_difference(20, -15) - |> should.equal(35) - - piecewise.int_absolute_difference(-20, 15) - |> should.equal(35) -} - -pub fn float_sign_test() { - piecewise.float_sign(100.0) - |> should.equal(1.0) - - piecewise.float_sign(0.0) - |> should.equal(0.0) - - piecewise.float_sign(-100.0) - |> should.equal(-1.0) -} - -pub fn float_flip_sign_test() { - piecewise.float_flip_sign(100.0) - |> should.equal(-100.0) - - piecewise.float_flip_sign(0.0) - |> should.equal(-0.0) - - piecewise.float_flip_sign(-100.0) - |> should.equal(100.0) -} - -pub fn float_copy_sign_test() { - piecewise.float_copy_sign(100.0, 10.0) - |> should.equal(100.0) - - piecewise.float_copy_sign(-100.0, 10.0) - |> should.equal(100.0) - - piecewise.float_copy_sign(100.0, -10.0) - |> should.equal(-100.0) - - piecewise.float_copy_sign(-100.0, -10.0) - |> should.equal(-100.0) -} - -pub fn int_sign_test() { - piecewise.int_sign(100) - |> should.equal(1) - - piecewise.int_sign(0) - |> should.equal(0) - - piecewise.int_sign(-100) - |> should.equal(-1) -} - -pub fn int_flip_sign_test() { - piecewise.int_flip_sign(100) - |> should.equal(-100) - - piecewise.int_flip_sign(0) - |> should.equal(-0) - - piecewise.int_flip_sign(-100) - |> should.equal(100) -} - -pub fn int_copy_sign_test() { - piecewise.int_copy_sign(100, 10) - |> should.equal(100) - - piecewise.int_copy_sign(-100, 10) - |> should.equal(100) - - piecewise.int_copy_sign(100, -10) - |> should.equal(-100) - - piecewise.int_copy_sign(-100, -10) - |> should.equal(-100) -} - -pub fn float_minimum_test() { - piecewise.minimum(0.75, 0.5, float.compare) - |> should.equal(0.5) - - piecewise.minimum(0.5, 0.75, float.compare) - |> should.equal(0.5) - - piecewise.minimum(-0.75, 0.5, float.compare) - |> should.equal(-0.75) - - piecewise.minimum(-0.75, 0.5, float.compare) - |> should.equal(-0.75) -} - -pub fn int_minimum_test() { - piecewise.minimum(75, 50, int.compare) - |> should.equal(50) - - piecewise.minimum(50, 75, int.compare) - |> should.equal(50) - - piecewise.minimum(-75, 50, int.compare) - |> should.equal(-75) - - piecewise.minimum(-75, 50, int.compare) - |> should.equal(-75) -} - -pub fn float_maximum_test() { - piecewise.maximum(0.75, 0.5, float.compare) - |> should.equal(0.75) - - piecewise.maximum(0.5, 0.75, float.compare) - |> should.equal(0.75) - - piecewise.maximum(-0.75, 0.5, float.compare) - |> should.equal(0.5) - - piecewise.maximum(-0.75, 0.5, float.compare) - |> should.equal(0.5) -} - -pub fn int_maximum_test() { - piecewise.maximum(75, 50, int.compare) - |> should.equal(75) - - piecewise.maximum(50, 75, int.compare) - |> should.equal(75) - - piecewise.maximum(-75, 50, int.compare) - |> should.equal(50) - - piecewise.maximum(-75, 50, int.compare) - |> should.equal(50) -} - -pub fn float_minmax_test() { - piecewise.minmax(0.75, 0.5, float.compare) - |> should.equal(#(0.5, 0.75)) - - piecewise.minmax(0.5, 0.75, float.compare) - |> should.equal(#(0.5, 0.75)) - - piecewise.minmax(-0.75, 0.5, float.compare) - |> should.equal(#(-0.75, 0.5)) - - piecewise.minmax(-0.75, 0.5, float.compare) - |> should.equal(#(-0.75, 0.5)) -} - -pub fn int_minmax_test() { - piecewise.minmax(75, 50, int.compare) - |> should.equal(#(50, 75)) - - piecewise.minmax(50, 75, int.compare) - |> should.equal(#(50, 75)) - - piecewise.minmax(-75, 50, int.compare) - |> should.equal(#(-75, 50)) - - piecewise.minmax(-75, 50, int.compare) - |> should.equal(#(-75, 50)) -} - -pub fn float_list_minimum_test() { - // An empty lists returns an error - [] - |> piecewise.list_minimum(float.compare) - |> should.be_error() - - // Valid input returns a result - [4.5, 4.5, 3.5, 2.5, 1.5] - |> piecewise.list_minimum(float.compare) - |> should.equal(Ok(1.5)) -} - -pub fn int_list_minimum_test() { - // An empty lists returns an error - [] - |> piecewise.list_minimum(int.compare) - |> should.be_error() - - // Valid input returns a result - [4, 4, 3, 2, 1] - |> piecewise.list_minimum(int.compare) - |> should.equal(Ok(1)) -} - -pub fn float_list_maximum_test() { - // An empty lists returns an error - [] - |> piecewise.list_maximum(float.compare) - |> should.be_error() - - // Valid input returns a result - [4.5, 4.5, 3.5, 2.5, 1.5] - |> piecewise.list_maximum(float.compare) - |> should.equal(Ok(4.5)) -} - -pub fn int_list_maximum_test() { - // An empty lists returns an error - [] - |> piecewise.list_maximum(int.compare) - |> should.be_error() - - // Valid input returns a result - [4, 4, 3, 2, 1] - |> piecewise.list_maximum(int.compare) - |> should.equal(Ok(4)) -} - -pub fn float_list_arg_maximum_test() { - // An empty lists returns an error - [] - |> piecewise.arg_maximum(float.compare) - |> should.be_error() - - // Valid input returns a result - [4.5, 4.5, 3.5, 2.5, 1.5] - |> piecewise.arg_maximum(float.compare) - |> should.equal(Ok([0, 1])) -} - -pub fn int_list_arg_maximum_test() { - // An empty lists returns an error - [] - |> piecewise.arg_maximum(int.compare) - |> should.be_error() - - // Valid input returns a result - [4, 4, 3, 2, 1] - |> piecewise.arg_maximum(int.compare) - |> should.equal(Ok([0, 1])) -} - -pub fn float_list_arg_minimum_test() { - // An empty lists returns an error - [] - |> piecewise.arg_minimum(float.compare) - |> should.be_error() - - // Valid input returns a result - [4.5, 4.5, 3.5, 2.5, 1.5] - |> piecewise.arg_minimum(float.compare) - |> should.equal(Ok([4])) -} - -pub fn int_list_arg_minimum_test() { - // An empty lists returns an error - [] - |> piecewise.arg_minimum(int.compare) - |> should.be_error() - - // Valid input returns a result - [4, 4, 3, 2, 1] - |> piecewise.arg_minimum(int.compare) - |> should.equal(Ok([4])) -} - -pub fn float_list_extrema_test() { - // An empty lists returns an error - [] - |> piecewise.extrema(float.compare) - |> should.be_error() - - // Valid input returns a result - [4.0, 4.0, 3.0, 2.0, 1.0] - |> piecewise.extrema(float.compare) - |> should.equal(Ok(#(1.0, 4.0))) - - // Valid input returns a result - [1.0, 4.0, 2.0, 5.0, 0.0] - |> piecewise.extrema(float.compare) - |> should.equal(Ok(#(0.0, 5.0))) -} - -pub fn int_list_extrema_test() { - // An empty lists returns an error - [] - |> piecewise.extrema(int.compare) - |> should.be_error() - - // Valid input returns a result - [4, 4, 3, 2, 1] - |> piecewise.extrema(int.compare) - |> should.equal(Ok(#(1, 4))) - - // Valid input returns a result - [1, 4, 2, 5, 0] - |> piecewise.extrema(int.compare) - |> should.equal(Ok(#(0, 5))) -} diff --git a/test/gleam_community/maths/predicates_test.gleam b/test/gleam_community/maths/predicates_test.gleam deleted file mode 100644 index e544eb5..0000000 --- a/test/gleam_community/maths/predicates_test.gleam +++ /dev/null @@ -1,203 +0,0 @@ -import gleam/list -import gleam_community/maths/predicates -import gleeunit/should - -pub fn float_is_close_test() { - let val = 99.0 - let ref_val = 100.0 - // We set 'atol' and 'rtol' such that the values are equivalent - // if 'val' is within 1 percent of 'ref_val' +/- 0.1 - let rtol = 0.01 - let atol = 0.1 - predicates.is_close(val, ref_val, rtol, atol) - |> should.be_true() -} - -pub fn float_list_all_close_test() { - let val = 99.0 - let ref_val = 100.0 - let xarr = list.repeat(val, 42) - let yarr = 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 = 0.01 - let atol = 0.1 - predicates.all_close(xarr, yarr, rtol, atol) - |> fn(zarr: Result(List(Bool), Nil)) -> Result(Bool, Nil) { - case zarr { - Ok(arr) -> - arr - |> list.all(fn(a) { a }) - |> Ok - _ -> - Nil - |> Error - } - } - |> should.equal(Ok(True)) -} - -pub fn float_is_fractional_test() { - predicates.is_fractional(1.5) - |> should.equal(True) - - predicates.is_fractional(0.5) - |> should.equal(True) - - predicates.is_fractional(0.3333) - |> should.equal(True) - - predicates.is_fractional(0.9999) - |> should.equal(True) - - predicates.is_fractional(1.0) - |> should.equal(False) - - predicates.is_fractional(999.0) - |> should.equal(False) -} - -pub fn int_is_power_test() { - predicates.is_power(10, 10) - |> should.equal(True) - - predicates.is_power(11, 10) - |> should.equal(False) - - predicates.is_power(4, 2) - |> should.equal(True) - - predicates.is_power(5, 2) - |> should.equal(False) - - predicates.is_power(27, 3) - |> should.equal(True) - - predicates.is_power(28, 3) - |> should.equal(False) -} - -pub fn int_is_even_test() { - predicates.is_even(0) - |> should.equal(True) - - predicates.is_even(2) - |> should.equal(True) - - predicates.is_even(12) - |> should.equal(True) - - predicates.is_even(5) - |> should.equal(False) - - predicates.is_even(-3) - |> should.equal(False) - - predicates.is_even(-4) - |> should.equal(True) -} - -pub fn int_is_odd_test() { - predicates.is_odd(0) - |> should.equal(False) - - predicates.is_odd(3) - |> should.equal(True) - - predicates.is_odd(13) - |> should.equal(True) - - predicates.is_odd(4) - |> should.equal(False) - - predicates.is_odd(-3) - |> should.equal(True) - - predicates.is_odd(-4) - |> should.equal(False) -} - -pub fn int_is_perfect_test() { - predicates.is_perfect(6) - |> should.equal(True) - - predicates.is_perfect(28) - |> should.equal(True) - - predicates.is_perfect(496) - |> should.equal(True) - - predicates.is_perfect(1) - |> should.equal(False) - - predicates.is_perfect(3) - |> should.equal(False) - - predicates.is_perfect(13) - |> should.equal(False) -} - -pub fn int_is_prime_test() { - // Test a negative integer, i.e., not a natural number - predicates.is_prime(-7) - |> should.equal(False) - - predicates.is_prime(1) - |> should.equal(False) - - predicates.is_prime(2) - |> should.equal(True) - - predicates.is_prime(3) - |> should.equal(True) - - predicates.is_prime(5) - |> should.equal(True) - - predicates.is_prime(7) - |> should.equal(True) - - predicates.is_prime(11) - |> should.equal(True) - - predicates.is_prime(42) - |> should.equal(False) - - predicates.is_prime(7919) - |> should.equal(True) - - // Test 1st Carmichael number - predicates.is_prime(561) - |> should.equal(False) - - // Test 2nd Carmichael number - predicates.is_prime(1105) - |> should.equal(False) -} - -pub fn is_between_test() { - predicates.is_between(5.5, 5.0, 6.0) - |> should.equal(True) - - predicates.is_between(5.0, 5.0, 6.0) - |> should.equal(False) - - predicates.is_between(6.0, 5.0, 6.0) - |> should.equal(False) -} - -pub fn is_divisible_test() { - predicates.is_divisible(10, 2) - |> should.equal(True) - - predicates.is_divisible(7, 3) - |> should.equal(False) -} - -pub fn is_multiple_test() { - predicates.is_multiple(15, 5) - |> should.equal(True) - - predicates.is_multiple(14, 5) - |> should.equal(False) -} diff --git a/test/gleam_community/maths/sequences_test.gleam b/test/gleam_community/maths/sequences_test.gleam deleted file mode 100644 index 1571d4b..0000000 --- a/test/gleam_community/maths/sequences_test.gleam +++ /dev/null @@ -1,328 +0,0 @@ -import gleam/iterator -import gleam/list -import gleam_community/maths/elementary -import gleam_community/maths/predicates -import gleam_community/maths/sequences -import gleeunit/should - -pub fn float_list_linear_space_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Check that the function agrees, at some arbitrary input - // points, with known function values - // ---> With endpoint included - let assert Ok(linspace) = sequences.linear_space(10.0, 50.0, 5, True) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [10.0, 20.0, 30.0, 40.0, 50.0], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - let assert Ok(linspace) = sequences.linear_space(10.0, 20.0, 5, True) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [10.0, 12.5, 15.0, 17.5, 20.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // Try with negative stop - // ----> Without endpoint included - let assert Ok(linspace) = sequences.linear_space(10.0, 50.0, 5, False) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [10.0, 18.0, 26.0, 34.0, 42.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - let assert Ok(linspace) = sequences.linear_space(10.0, 20.0, 5, False) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [10.0, 12.0, 14.0, 16.0, 18.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // Try with negative stop - let assert Ok(linspace) = sequences.linear_space(10.0, -50.0, 5, False) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [10.0, -2.0, -14.0, -26.0, -38.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - let assert Ok(linspace) = sequences.linear_space(10.0, -20.0, 5, True) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [10.0, 2.5, -5.0, -12.5, -20.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // Try with negative start - let assert Ok(linspace) = sequences.linear_space(-10.0, 50.0, 5, False) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [-10.0, 2.0, 14.0, 26.0, 38.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - let assert Ok(linspace) = sequences.linear_space(-10.0, 20.0, 5, True) - let assert Ok(result) = - predicates.all_close( - linspace |> iterator.to_list(), - [-10.0, -2.5, 5.0, 12.5, 20.0], - 0.0, - tol, - ) - - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // A negative number of points does not work (-5) - sequences.linear_space(10.0, 50.0, -5, True) - |> should.be_error() -} - -pub fn float_list_logarithmic_space_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - // ---> With endpoint included - // - Positive start, stop, base - let assert Ok(logspace) = sequences.logarithmic_space(1.0, 3.0, 3, True, 10.0) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [10.0, 100.0, 1000.0], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive start, stop, negative base - let assert Ok(logspace) = - sequences.logarithmic_space(1.0, 3.0, 3, True, -10.0) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [-10.0, 100.0, -1000.0], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive start, negative stop, base - let assert Ok(logspace) = - sequences.logarithmic_space(1.0, -3.0, 3, True, -10.0) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [-10.0, -0.1, -0.001], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive start, base, negative stop - let assert Ok(logspace) = - sequences.logarithmic_space(1.0, -3.0, 3, True, 10.0) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [10.0, 0.1, 0.001], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive stop, base, negative start - let assert Ok(logspace) = - sequences.logarithmic_space(-1.0, 3.0, 3, True, 10.0) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [0.1, 10.0, 1000.0], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // ----> Without endpoint included - // - Positive start, stop, base - let assert Ok(logspace) = - sequences.logarithmic_space(1.0, 3.0, 3, False, 10.0) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [10.0, 46.41588834, 215.443469], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // A negative number of points does not work (-3) - sequences.logarithmic_space(1.0, 3.0, -3, True, 10.0) - |> should.be_error() -} - -pub fn float_list_geometric_space_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - // Check that the function agrees, at some arbitrary input - // points, with known function values - // ---> With endpoint included - // - Positive start, stop - let assert Ok(logspace) = sequences.geometric_space(10.0, 1000.0, 3, True) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [10.0, 100.0, 1000.0], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive start, negative stop - let assert Ok(logspace) = sequences.geometric_space(10.0, 0.001, 3, True) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [10.0, 0.1, 0.001], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // - Positive stop, negative start - let assert Ok(logspace) = sequences.geometric_space(0.1, 1000.0, 3, True) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [0.1, 10.0, 1000.0], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // ----> Without endpoint included - // - Positive start, stop - let assert Ok(logspace) = sequences.geometric_space(10.0, 1000.0, 3, False) - let assert Ok(result) = - predicates.all_close( - logspace |> iterator.to_list(), - [10.0, 46.41588834, 215.443469], - 0.0, - tol, - ) - result - |> list.all(fn(x) { x == True }) - |> should.be_true() - - // Test invalid input (start and stop can't be equal to 0.0) - sequences.geometric_space(0.0, 1000.0, 3, False) - |> should.be_error() - - sequences.geometric_space(-1000.0, 0.0, 3, False) - |> should.be_error() - - // A negative number of points does not work - sequences.geometric_space(-1000.0, 0.0, -3, False) - |> should.be_error() -} - -pub fn float_list_arange_test() { - // Positive start, stop, step - sequences.arange(1.0, 5.0, 1.0) - |> iterator.to_list() - |> should.equal([1.0, 2.0, 3.0, 4.0]) - - sequences.arange(1.0, 5.0, 0.5) - |> iterator.to_list() - |> should.equal([1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5]) - - sequences.arange(1.0, 2.0, 0.25) - |> iterator.to_list() - |> should.equal([1.0, 1.25, 1.5, 1.75]) - - // Reverse (switch start/stop largest/smallest value) - sequences.arange(5.0, 1.0, 1.0) - |> iterator.to_list() - |> should.equal([]) - - // Reverse negative step - sequences.arange(5.0, 1.0, -1.0) - |> iterator.to_list() - |> should.equal([5.0, 4.0, 3.0, 2.0]) - - // Positive start, negative stop, step - sequences.arange(5.0, -1.0, -1.0) - |> iterator.to_list() - |> should.equal([5.0, 4.0, 3.0, 2.0, 1.0, 0.0]) - - // Negative start, stop, step - sequences.arange(-5.0, -1.0, -1.0) - |> iterator.to_list() - |> should.equal([]) - - // Negative start, stop, positive step - sequences.arange(-5.0, -1.0, 1.0) - |> iterator.to_list() - |> should.equal([-5.0, -4.0, -3.0, -2.0]) -} diff --git a/test/gleam_community/maths/special_test.gleam b/test/gleam_community/maths/special_test.gleam deleted file mode 100644 index 158c8f0..0000000 --- a/test/gleam_community/maths/special_test.gleam +++ /dev/null @@ -1,114 +0,0 @@ -import gleam/result -import gleam_community/maths/elementary -import gleam_community/maths/predicates -import gleam_community/maths/special -import gleeunit/should - -pub fn float_beta_function_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Valid input returns a result - special.beta(-0.5, 0.5) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - special.beta(0.5, 0.5) - |> predicates.is_close(3.1415926535897927, 0.0, tol) - |> should.be_true() - - special.beta(0.5, -0.5) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - special.beta(5.0, 5.0) - |> predicates.is_close(0.0015873015873015873, 0.0, tol) - |> should.be_true() -} - -pub fn float_error_function_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Valid input returns a result - special.erf(-0.5) - |> predicates.is_close(-0.5204998778130465, 0.0, tol) - |> should.be_true() - - special.erf(0.5) - |> predicates.is_close(0.5204998778130465, 0.0, tol) - |> should.be_true() - - special.erf(1.0) - |> predicates.is_close(0.8427007929497148, 0.0, tol) - |> should.be_true() - - special.erf(2.0) - |> predicates.is_close(0.9953222650189527, 0.0, tol) - |> should.be_true() - - special.erf(10.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() -} - -pub fn float_gamma_function_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Valid input returns a result - special.gamma(-0.5) - |> predicates.is_close(-3.5449077018110318, 0.0, tol) - |> should.be_true() - - special.gamma(0.5) - |> predicates.is_close(1.7724538509055159, 0.0, tol) - |> should.be_true() - - special.gamma(1.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - special.gamma(2.0) - |> predicates.is_close(1.0, 0.0, tol) - |> should.be_true() - - special.gamma(3.0) - |> predicates.is_close(2.0, 0.0, tol) - |> should.be_true() - - special.gamma(10.0) - |> predicates.is_close(362_880.0, 0.0, tol) - |> should.be_true() -} - -pub fn float_incomplete_gamma_function_test() { - let assert Ok(tol) = elementary.power(10.0, -6.0) - - // Invalid input gives an error - // 1st arg is invalid - special.incomplete_gamma(-1.0, 1.0) - |> should.be_error() - - // 2nd arg is invalid - special.incomplete_gamma(1.0, -1.0) - |> should.be_error() - - // Valid input returns a result - special.incomplete_gamma(1.0, 0.0) - |> result.unwrap(-999.0) - |> predicates.is_close(0.0, 0.0, tol) - |> should.be_true() - - special.incomplete_gamma(1.0, 2.0) - |> result.unwrap(-999.0) - |> predicates.is_close(0.864664716763387308106, 0.0, tol) - |> should.be_true() - - special.incomplete_gamma(2.0, 3.0) - |> result.unwrap(-999.0) - |> predicates.is_close(0.8008517265285442280826, 0.0, tol) - |> should.be_true() - - special.incomplete_gamma(3.0, 4.0) - |> result.unwrap(-999.0) - |> predicates.is_close(1.523793388892911312363, 0.0, tol) - |> should.be_true() -} diff --git a/test/gleam_community/metrics_test.gleam b/test/gleam_community/metrics_test.gleam new file mode 100644 index 0000000..f799692 --- /dev/null +++ b/test/gleam_community/metrics_test.gleam @@ -0,0 +1,549 @@ +import gleam/float +import gleam/set +import gleam_community/maths +import gleeunit/should + +pub fn float_list_norm_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // An empty lists returns 0.0 + [] + |> maths.norm(1.0) + |> should.equal(Ok(0.0)) + + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = + [1.0, 1.0, 1.0] + |> maths.norm(1.0) + result + |> maths.is_close(3.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [1.0, 1.0, 1.0] + |> maths.norm(-1.0) + result + |> maths.is_close(0.3333333333333333, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [-1.0, -1.0, -1.0] + |> maths.norm(-1.0) + result + |> maths.is_close(0.3333333333333333, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [-1.0, -1.0, -1.0] + |> maths.norm(1.0) + result + |> maths.is_close(3.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [-1.0, -2.0, -3.0] + |> maths.norm(-10.0) + result + |> maths.is_close(0.9999007044905545, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [-1.0, -2.0, -3.0] + |> maths.norm(-100.0) + result + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [-1.0, -2.0, -3.0] + |> maths.norm(2.0) + result + |> maths.is_close(3.7416573867739413, 0.0, tol) + |> should.be_true() +} + +pub fn float_list_norm_with_weights_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // An empty lists returns 0.0 + [] + |> maths.norm_with_weights(1.0) + |> should.equal(Ok(0.0)) + + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = + [#(1.0, 1.0), #(1.0, 1.0), #(1.0, 1.0)] + |> maths.norm_with_weights(1.0) + result + |> maths.is_close(3.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [#(1.0, 0.5), #(1.0, 0.5), #(1.0, 0.5)] + |> maths.norm_with_weights(1.0) + result + |> maths.is_close(1.5, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + [#(1.0, 0.5), #(2.0, 0.5), #(3.0, 0.5)] + |> maths.norm_with_weights(2.0) + result + |> maths.is_close(2.6457513110645907, 0.0, tol) + |> should.be_true() +} + +pub fn float_list_manhattan_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Try with valid input (same as Minkowski distance with p = 1) + let assert Ok(result) = maths.manhattan_distance([#(0.0, 1.0), #(0.0, 2.0)]) + result + |> maths.is_close(3.0, 0.0, tol) + |> should.be_true() + + maths.manhattan_distance([#(1.0, 4.0), #(2.0, 5.0), #(3.0, 6.0)]) + |> should.equal(Ok(9.0)) + + maths.manhattan_distance([#(1.0, 2.0), #(2.0, 3.0)]) + |> should.equal(Ok(2.0)) + + maths.manhattan_distance_with_weights([#(1.0, 2.0, 0.5), #(2.0, 3.0, 1.0)]) + |> should.equal(Ok(1.5)) + + maths.manhattan_distance_with_weights([ + #(1.0, 4.0, 1.0), + #(2.0, 5.0, 1.0), + #(3.0, 6.0, 1.0), + ]) + |> should.equal(Ok(9.0)) + + maths.manhattan_distance_with_weights([ + #(1.0, 4.0, 1.0), + #(2.0, 5.0, 2.0), + #(3.0, 6.0, 3.0), + ]) + |> should.equal(Ok(18.0)) + + maths.manhattan_distance_with_weights([ + #(1.0, 4.0, -7.0), + #(2.0, 5.0, -8.0), + #(3.0, 6.0, -9.0), + ]) + |> should.be_error() +} + +pub fn float_list_minkowski_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Test order < 1 + maths.minkowski_distance([#(0.0, 0.0), #(0.0, 0.0)], -1.0) + |> should.be_error() + + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(result) = + maths.minkowski_distance([#(1.0, 1.0), #(1.0, 1.0)], 1.0) + result + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.minkowski_distance([#(0.0, 1.0), #(0.0, 1.0)], 10.0) + result + |> maths.is_close(1.0717734625362931, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.minkowski_distance([#(0.0, 1.0), #(0.0, 1.0)], 100.0) + result + |> maths.is_close(1.0069555500567189, 0.0, tol) + |> should.be_true() + + // Euclidean distance (p = 2) + let assert Ok(result) = + maths.minkowski_distance([#(0.0, 1.0), #(0.0, 2.0)], 2.0) + result + |> maths.is_close(2.23606797749979, 0.0, tol) + |> should.be_true() + + // Manhattan distance (p = 1) + let assert Ok(result) = + maths.minkowski_distance([#(0.0, 1.0), #(0.0, 2.0)], 1.0) + result + |> maths.is_close(3.0, 0.0, tol) + |> should.be_true() + + // Try different valid input + let assert Ok(result) = + maths.minkowski_distance([#(1.0, 4.0), #(2.0, 5.0), #(3.0, 6.0)], 4.0) + result + |> maths.is_close(3.9482220388574776, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.minkowski_distance_with_weights( + [#(1.0, 4.0, 1.0), #(2.0, 5.0, 1.0), #(3.0, 6.0, 1.0)], + 4.0, + ) + result + |> maths.is_close(3.9482220388574776, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.minkowski_distance_with_weights( + [#(1.0, 4.0, 1.0), #(2.0, 5.0, 2.0), #(3.0, 6.0, 3.0)], + 4.0, + ) + result + |> maths.is_close(4.6952537402198615, 0.0, tol) + |> should.be_true() + + // Try invalid input with weights that are negative + maths.minkowski_distance_with_weights( + [#(1.0, 4.0, -7.0), #(2.0, 5.0, -8.0), #(3.0, 6.0, -9.0)], + 2.0, + ) + |> should.be_error() +} + +pub fn float_list_euclidean_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Empty lists returns an error + maths.euclidean_distance([]) + |> should.be_error() + + // Try with valid input (same as Minkowski distance with p = 2) + let assert Ok(result) = maths.euclidean_distance([#(0.0, 1.0), #(0.0, 2.0)]) + result + |> maths.is_close(2.23606797749979, 0.0, tol) + |> should.be_true() + + // Try different valid input + let assert Ok(result) = + maths.euclidean_distance([#(1.0, 4.0), #(2.0, 5.0), #(3.0, 6.0)]) + result + |> maths.is_close(5.196152422706632, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.euclidean_distance_with_weights([ + #(1.0, 4.0, 1.0), + #(2.0, 5.0, 1.0), + #(3.0, 6.0, 1.0), + ]) + result + |> maths.is_close(5.196152422706632, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.euclidean_distance_with_weights([ + #(1.0, 4.0, 1.0), + #(2.0, 5.0, 2.0), + #(3.0, 6.0, 3.0), + ]) + result + |> maths.is_close(7.3484692283495345, 0.0, tol) + |> should.be_true() + + // Try invalid input with weights that are negative + maths.euclidean_distance_with_weights([ + #(1.0, 4.0, -7.0), + #(2.0, 5.0, -8.0), + #(3.0, 6.0, -9.0), + ]) + |> should.be_error() +} + +pub fn mean_test() { + // An empty list returns an error + [] + |> maths.mean() + |> should.be_error() + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> maths.mean() + |> should.equal(Ok(2.0)) +} + +pub fn median_test() { + // An empty list returns an error + [] + |> maths.median() + |> should.be_error() + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> maths.median() + |> should.equal(Ok(2.0)) + + [1.0, 2.0, 3.0, 4.0] + |> maths.median() + |> should.equal(Ok(2.5)) +} + +pub fn variance_test() { + // Degrees of freedom + let ddof = 1 + + // An empty list returns an error + [] + |> maths.variance(ddof) + |> should.be_error() + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> maths.variance(ddof) + |> should.equal(Ok(1.0)) +} + +pub fn standard_deviation_test() { + // Degrees of freedom + let ddof = 1 + + // An empty list returns an error + [] + |> maths.standard_deviation(ddof) + |> should.be_error() + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> maths.standard_deviation(ddof) + |> should.equal(Ok(1.0)) +} + +pub fn jaccard_index_test() { + maths.jaccard_index(set.from_list([]), set.from_list([])) + |> should.equal(0.0) + + let set_a = set.from_list([0, 1, 2, 5, 6, 8, 9]) + let set_b = set.from_list([0, 2, 3, 4, 5, 7, 9]) + maths.jaccard_index(set_a, set_b) + |> should.equal(4.0 /. 10.0) + + let set_c = set.from_list([0, 1, 2, 3, 4, 5]) + let set_d = set.from_list([6, 7, 8, 9, 10]) + maths.jaccard_index(set_c, set_d) + |> should.equal(0.0 /. 11.0) + + let set_e = set.from_list(["cat", "dog", "hippo", "monkey"]) + let set_f = set.from_list(["monkey", "rhino", "ostrich", "salmon"]) + maths.jaccard_index(set_e, set_f) + |> should.equal(1.0 /. 7.0) +} + +pub fn sorensen_dice_coefficient_test() { + maths.sorensen_dice_coefficient(set.from_list([]), set.from_list([])) + |> should.equal(0.0) + + let set_a = set.from_list([0, 1, 2, 5, 6, 8, 9]) + let set_b = set.from_list([0, 2, 3, 4, 5, 7, 9]) + maths.sorensen_dice_coefficient(set_a, set_b) + |> should.equal(2.0 *. 4.0 /. { 7.0 +. 7.0 }) + + let set_c = set.from_list([0, 1, 2, 3, 4, 5]) + let set_d = set.from_list([6, 7, 8, 9, 10]) + maths.sorensen_dice_coefficient(set_c, set_d) + |> should.equal(2.0 *. 0.0 /. { 6.0 +. 5.0 }) + + let set_e = set.from_list(["cat", "dog", "hippo", "monkey"]) + let set_f = set.from_list(["monkey", "rhino", "ostrich", "salmon", "spider"]) + maths.sorensen_dice_coefficient(set_e, set_f) + |> should.equal(2.0 *. 1.0 /. { 4.0 +. 5.0 }) +} + +pub fn overlap_coefficient_test() { + maths.overlap_coefficient(set.from_list([]), set.from_list([])) + |> should.equal(0.0) + + let set_a = set.from_list([0, 1, 2, 5, 6, 8, 9]) + let set_b = set.from_list([0, 2, 3, 4, 5, 7, 9]) + maths.overlap_coefficient(set_a, set_b) + |> should.equal(4.0 /. 7.0) + + let set_c = set.from_list([0, 1, 2, 3, 4, 5]) + let set_d = set.from_list([6, 7, 8, 9, 10]) + maths.overlap_coefficient(set_c, set_d) + |> should.equal(0.0 /. 5.0) + + let set_e = set.from_list(["horse", "dog", "hippo", "monkey", "bird"]) + let set_f = set.from_list(["monkey", "bird", "ostrich", "salmon"]) + maths.overlap_coefficient(set_e, set_f) + |> should.equal(2.0 /. 4.0) +} + +pub fn cosine_similarity_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Two orthogonal vectors (represented by lists) + maths.cosine_similarity([#(-1.0, 1.0), #(1.0, 1.0), #(0.0, -1.0)]) + |> should.equal(Ok(0.0)) + + // Two identical (parallel) vectors (represented by lists) + maths.cosine_similarity([#(1.0, 1.0), #(2.0, 2.0), #(3.0, 3.0)]) + |> should.equal(Ok(1.0)) + + // Two parallel, but oppositely oriented vectors (represented by lists) + maths.cosine_similarity([#(-1.0, 1.0), #(-2.0, 2.0), #(-3.0, 3.0)]) + |> should.equal(Ok(-1.0)) + + // Try with arbitrary valid input + let assert Ok(result) = + maths.cosine_similarity([#(1.0, 4.0), #(2.0, 5.0), #(3.0, 6.0)]) + result + |> maths.is_close(0.9746318461970762, 0.0, tol) + |> should.be_true() + + // Try valid input with weights + let assert Ok(result) = + maths.cosine_similarity_with_weights([ + #(1.0, 4.0, 1.0), + #(2.0, 5.0, 1.0), + #(3.0, 6.0, 1.0), + ]) + result + |> maths.is_close(0.9746318461970762, 0.0, tol) + |> should.be_true() + + // Try with different weights + let assert Ok(result) = + maths.cosine_similarity_with_weights([ + #(1.0, 4.0, 1.0), + #(2.0, 5.0, 2.0), + #(3.0, 6.0, 3.0), + ]) + result + |> maths.is_close(0.9855274566525745, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.cosine_similarity_with_weights([ + #(1.0, 1.0, 2.0), + #(2.0, 2.0, 3.0), + #(3.0, 3.0, 4.0), + ]) + result + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + let assert Ok(result) = + maths.cosine_similarity_with_weights([ + #(-1.0, 1.0, 1.0), + #(-2.0, 2.0, 0.5), + #(-3.0, 3.0, 0.33), + ]) + result + |> maths.is_close(-1.0, 0.0, tol) + |> should.be_true() + + // Try invalid input with weights that are negative + maths.cosine_similarity_with_weights([ + #(1.0, 4.0, -7.0), + #(2.0, 5.0, -8.0), + #(3.0, 6.0, -9.0), + ]) + |> should.be_error() +} + +pub fn chebyshev_distance_test() { + // Empty lists returns an error + maths.chebyshev_distance([]) + |> should.be_error() + + // Try different types of valid input + maths.chebyshev_distance([#(1.0, 0.0), #(0.0, 2.0)]) + |> should.equal(Ok(2.0)) + + maths.chebyshev_distance([#(1.0, 2.0), #(0.0, 0.0)]) + |> should.equal(Ok(1.0)) + + maths.chebyshev_distance([#(1.0, -2.0), #(0.0, 0.0)]) + |> should.equal(Ok(3.0)) + + maths.chebyshev_distance([#(-5.0, -1.0), #(-10.0, -12.0), #(-3.0, -3.0)]) + |> should.equal(Ok(4.0)) + + maths.chebyshev_distance([#(1.0, 1.0), #(2.0, 2.0), #(3.0, 3.0)]) + |> should.equal(Ok(0.0)) + + maths.chebyshev_distance_with_weights([ + #(-5.0, -1.0, 0.5), + #(-10.0, -12.0, 1.0), + #(-3.0, -3.0, 1.0), + ]) + |> should.equal(Ok(2.0)) +} + +pub fn canberra_distance_test() { + // Empty lists returns an error + maths.canberra_distance([]) + |> should.be_error() + + // Try different types of valid input + maths.canberra_distance([#(0.0, 0.0), #(0.0, 0.0)]) + |> should.equal(Ok(0.0)) + + maths.canberra_distance([#(1.0, -2.0), #(2.0, -1.0)]) + |> should.equal(Ok(2.0)) + + maths.canberra_distance([#(1.0, 0.0), #(0.0, 2.0)]) + |> should.equal(Ok(2.0)) + + maths.canberra_distance([#(1.0, 2.0), #(0.0, 0.0)]) + |> should.equal(Ok(1.0 /. 3.0)) + + maths.canberra_distance_with_weights([#(1.0, 0.0, 1.0), #(0.0, 2.0, 1.0)]) + |> should.equal(Ok(2.0)) + + maths.canberra_distance_with_weights([#(1.0, 0.0, 1.0), #(0.0, 2.0, 0.5)]) + |> should.equal(Ok(1.5)) + + maths.canberra_distance_with_weights([#(1.0, 0.0, 0.5), #(0.0, 2.0, 0.5)]) + |> should.equal(Ok(1.0)) + + // Try invalid input with weights that are negative + maths.canberra_distance_with_weights([ + #(1.0, 4.0, -7.0), + #(2.0, 5.0, -8.0), + #(3.0, 6.0, -9.0), + ]) + |> should.be_error() +} + +pub fn braycurtis_distance_test() { + // Empty lists returns an error + maths.braycurtis_distance([]) + |> should.be_error() + + // Try different types of valid input + maths.braycurtis_distance([#(0.0, 0.0), #(0.0, 0.0)]) + |> should.equal(Ok(0.0)) + maths.braycurtis_distance([#(1.0, -2.0), #(2.0, -1.0)]) + |> should.equal(Ok(3.0)) + + maths.braycurtis_distance([#(1.0, 0.0), #(0.0, 2.0)]) + |> should.equal(Ok(1.0)) + + maths.braycurtis_distance([#(1.0, 3.0), #(2.0, 4.0)]) + |> should.equal(Ok(0.4)) + + maths.braycurtis_distance_with_weights([#(1.0, 3.0, 1.0), #(2.0, 4.0, 1.0)]) + |> should.equal(Ok(0.4)) + + maths.braycurtis_distance_with_weights([#(1.0, 3.0, 0.5), #(2.0, 4.0, 1.0)]) + |> should.equal(Ok(0.375)) + + maths.braycurtis_distance_with_weights([#(1.0, 3.0, 0.25), #(2.0, 4.0, 0.25)]) + |> should.equal(Ok(0.4)) + // Try invalid input with weights that are negative + maths.braycurtis_distance_with_weights([ + #(1.0, 4.0, -7.0), + #(2.0, 5.0, -8.0), + #(3.0, 6.0, -9.0), + ]) + |> should.be_error() +} diff --git a/test/gleam_community/piecewise_test.gleam b/test/gleam_community/piecewise_test.gleam new file mode 100644 index 0000000..f74a84f --- /dev/null +++ b/test/gleam_community/piecewise_test.gleam @@ -0,0 +1,656 @@ +import gleam/float +import gleam/int +import gleam_community/maths +import gleeunit/should + +pub fn float_ceiling_test() { + // Round 3. digit AFTER decimal point + maths.round_up(12.0654, 3) + |> should.equal(12.066) + + // Round 2. digit AFTER decimal point + maths.round_up(12.0654, 2) + |> should.equal(12.07) + + // Round 1. digit AFTER decimal point + maths.round_up(12.0654, 1) + |> should.equal(12.1) + + // Round 0. digit BEFORE decimal point + maths.round_up(12.0654, 0) + |> should.equal(13.0) + + // Round 1. digit BEFORE decimal point + maths.round_up(12.0654, -1) + |> should.equal(20.0) + + // Round 2. digit BEFORE decimal point + maths.round_up(12.0654, -2) + |> should.equal(100.0) + + // Round 3. digit BEFORE decimal point + maths.round_up(12.0654, -3) + |> should.equal(1000.0) +} + +pub fn float_floor_test() { + // Round 3. digit AFTER decimal point + maths.round_down(12.0654, 3) + |> should.equal(12.065) + + // Round 2. digit AFTER decimal point + maths.round_down(12.0654, 2) + |> should.equal(12.06) + + // Round 1. digit AFTER decimal point + maths.round_down(12.0654, 1) + |> should.equal(12.0) + + // Round 0. digit BEFORE decimal point + maths.round_down(12.0654, 0) + |> should.equal(12.0) + + // Round 1. digit BEFORE decimal point + maths.round_down(12.0654, -1) + |> should.equal(10.0) + + // Round 2. digit BEFORE decimal point + maths.round_down(12.0654, -2) + |> should.equal(0.0) + + // Round 2. digit BEFORE decimal point + maths.round_down(12.0654, -3) + |> should.equal(0.0) +} + +pub fn float_truncate_test() { + // Round 3. digit AFTER decimal point + maths.round_to_zero(12.0654, 3) + |> should.equal(12.065) + + // Round 2. digit AFTER decimal point + maths.round_to_zero(12.0654, 2) + |> should.equal(12.06) + + // Round 1. digit AFTER decimal point + maths.round_to_zero(12.0654, 1) + |> should.equal(12.0) + + // Round 0. digit BEFORE decimal point + maths.round_to_zero(12.0654, 0) + |> should.equal(12.0) + + // Round 1. digit BEFORE decimal point + maths.round_to_zero(12.0654, -1) + |> should.equal(10.0) + + // Round 2. digit BEFORE decimal point + maths.round_to_zero(12.0654, -2) + |> should.equal(0.0) + + // Round 2. digit BEFORE decimal point + maths.round_to_zero(12.0654, -3) + |> should.equal(0.0) +} + +pub fn math_round_to_nearest_test() { + // Try with positive values + maths.round_to_nearest(1.5, 0) + |> should.equal(2.0) + + maths.round_to_nearest(1.75, 0) + |> should.equal(2.0) + + maths.round_to_nearest(2.0, 0) + |> should.equal(2.0) + + maths.round_to_nearest(3.5, 0) + |> should.equal(4.0) + + maths.round_to_nearest(4.5, 0) + |> should.equal(4.0) + + // Try with negative values + maths.round_to_nearest(-3.5, 0) + |> should.equal(-4.0) + + maths.round_to_nearest(-4.5, 0) + |> should.equal(-4.0) + + // // Round 3. digit AFTER decimal point + maths.round_to_nearest(12.0654, 3) + |> should.equal(12.065) + + // Round 2. digit AFTER decimal point + maths.round_to_nearest(12.0654, 2) + |> should.equal(12.07) + + // Round 1. digit AFTER decimal point + maths.round_to_nearest(12.0654, 1) + |> should.equal(12.1) + + // Round 0. digit BEFORE decimal point + maths.round_to_nearest(12.0654, 0) + |> should.equal(12.0) + + // Round 1. digit BEFORE decimal point + maths.round_to_nearest(12.0654, -1) + |> should.equal(10.0) + + // Round 2. digit BEFORE decimal point + maths.round_to_nearest(12.0654, -2) + |> should.equal(0.0) + + // Round 3. digit BEFORE decimal point + maths.round_to_nearest(12.0654, -3) + |> should.equal(0.0) +} + +pub fn math_round_up_test() { + // Try with positive values + maths.round_up(0.45, 0) + |> should.equal(1.0) + + maths.round_up(0.5, 0) + |> should.equal(1.0) + + maths.round_up(0.45, 1) + |> should.equal(0.5) + + maths.round_up(0.5, 1) + |> should.equal(0.5) + + maths.round_up(0.455, 2) + |> should.equal(0.46) + + maths.round_up(0.505, 2) + |> should.equal(0.51) + + // Try with negative values + maths.round_up(-0.45, 0) + |> should.equal(-0.0) + + maths.round_up(-0.5, 0) + |> should.equal(-0.0) + + maths.round_up(-0.45, 1) + |> should.equal(-0.4) + + maths.round_up(-0.5, 1) + |> should.equal(-0.5) + + maths.round_up(-0.455, 2) + |> should.equal(-0.45) + + maths.round_up(-0.505, 2) + |> should.equal(-0.5) +} + +pub fn math_round_down_test() { + // Try with positive values + maths.round_down(0.45, 0) + |> should.equal(0.0) + + maths.round_down(0.5, 0) + |> should.equal(0.0) + + maths.round_down(0.45, 1) + |> should.equal(0.4) + + maths.round_down(0.5, 1) + |> should.equal(0.5) + + maths.round_down(0.455, 2) + |> should.equal(0.45) + + maths.round_down(0.505, 2) + |> should.equal(0.5) + + // Try with negative values + maths.round_down(-0.45, 0) + |> should.equal(-1.0) + + maths.round_down(-0.5, 0) + |> should.equal(-1.0) + + maths.round_down(-0.45, 1) + |> should.equal(-0.5) + + maths.round_down(-0.5, 1) + |> should.equal(-0.5) + + maths.round_down(-0.455, 2) + |> should.equal(-0.46) + + maths.round_down(-0.505, 2) + |> should.equal(-0.51) +} + +pub fn math_round_to_zero_test() { + // Note: Rounding mode "RoundToZero" is an alias for the truncate function + // Try with positive values + maths.round_to_zero(0.5, 0) + |> should.equal(0.0) + + maths.round_to_zero(0.75, 0) + |> should.equal(0.0) + + maths.round_to_zero(0.45, 1) + |> should.equal(0.4) + + maths.round_to_zero(0.57, 1) + |> should.equal(0.5) + + maths.round_to_zero(0.4575, 2) + |> should.equal(0.45) + + maths.round_to_zero(0.5075, 2) + |> should.equal(0.5) + + // Try with negative values + maths.round_to_zero(-0.5, 0) + |> should.equal(0.0) + + maths.round_to_zero(-0.75, 0) + |> should.equal(0.0) + + maths.round_to_zero(-0.45, 1) + |> should.equal(-0.4) + + maths.round_to_zero(-0.57, 1) + |> should.equal(-0.5) + + maths.round_to_zero(-0.4575, 2) + |> should.equal(-0.45) + + maths.round_to_zero(-0.5075, 2) + |> should.equal(-0.5) +} + +pub fn math_round_ties_away_test() { + // Try with positive values + maths.round_ties_away(1.4, 0) + |> should.equal(1.0) + + maths.round_ties_away(1.5, 0) + |> should.equal(2.0) + + maths.round_ties_away(2.5, 0) + |> should.equal(3.0) + + // Try with negative values + maths.round_ties_away(-1.4, 0) + |> should.equal(-1.0) + + maths.round_ties_away(-1.5, 0) + |> should.equal(-2.0) + + maths.round_ties_away(-2.0, 0) + |> should.equal(-2.0) + + maths.round_ties_away(-2.5, 0) + |> should.equal(-3.0) + + // Round 3. digit AFTER decimal point + maths.round_ties_away(12.0654, 3) + |> should.equal(12.065) + + // Round 2. digit AFTER decimal point + maths.round_ties_away(12.0654, 2) + |> should.equal(12.07) + + // Round 1. digit AFTER decimal point + maths.round_ties_away(12.0654, 1) + |> should.equal(12.1) + + // Round 0. digit BEFORE decimal point + maths.round_ties_away(12.0654, 0) + |> should.equal(12.0) + + // Round 1. digit BEFORE decimal point + maths.round_ties_away(12.0654, -1) + |> should.equal(10.0) + + // Round 2. digit BEFORE decimal point + maths.round_ties_away(12.0654, -2) + |> should.equal(0.0) + + // Round 2. digit BEFORE decimal point + maths.round_ties_away(12.0654, -3) + |> should.equal(0.0) +} + +pub fn math_round_ties_up_test() { + // Try with positive values + maths.round_ties_up(1.4, 0) + |> should.equal(1.0) + + maths.round_ties_up(1.5, 0) + |> should.equal(2.0) + + maths.round_ties_up(2.5, 0) + |> should.equal(3.0) + + // Try with negative values + maths.round_ties_up(-1.4, 0) + |> should.equal(-1.0) + + maths.round_ties_up(-1.5, 0) + |> should.equal(-1.0) + + maths.round_ties_up(-2.0, 0) + |> should.equal(-2.0) + + maths.round_ties_up(-2.5, 0) + |> should.equal(-2.0) + + // Round 3. digit AFTER decimal point + maths.round_ties_up(12.0654, 3) + |> should.equal(12.065) + + // Round 2. digit AFTER decimal point + maths.round_ties_up(12.0654, 2) + |> should.equal(12.07) + + // Round 1. digit AFTER decimal point + maths.round_ties_up(12.0654, 1) + |> should.equal(12.1) + + // Round 0. digit BEFORE decimal point + maths.round_ties_up(12.0654, 0) + |> should.equal(12.0) + + // Round 1. digit BEFORE decimal point + maths.round_ties_up(12.0654, -1) + |> should.equal(10.0) + + // Round 2. digit BEFORE decimal point + maths.round_ties_up(12.0654, -2) + |> should.equal(0.0) + + // Round 2. digit BEFORE decimal point + maths.round_ties_up(12.0654, -3) + |> should.equal(0.0) +} + +pub fn float_absolute_difference_test() { + maths.float_absolute_difference(20.0, 15.0) + |> should.equal(5.0) + + maths.float_absolute_difference(-20.0, -15.0) + |> should.equal(5.0) + + maths.float_absolute_difference(20.0, -15.0) + |> should.equal(35.0) + + maths.float_absolute_difference(-20.0, 15.0) + |> should.equal(35.0) + + maths.float_absolute_difference(0.0, 0.0) + |> should.equal(0.0) + + maths.float_absolute_difference(1.0, 2.0) + |> should.equal(1.0) + + maths.float_absolute_difference(2.0, 1.0) + |> should.equal(1.0) + + maths.float_absolute_difference(-1.0, 0.0) + |> should.equal(1.0) + + maths.float_absolute_difference(0.0, -1.0) + |> should.equal(1.0) + + maths.float_absolute_difference(10.0, 20.0) + |> should.equal(10.0) + + maths.float_absolute_difference(-10.0, -20.0) + |> should.equal(10.0) + + maths.float_absolute_difference(-10.5, 10.5) + |> should.equal(21.0) +} + +pub fn int_absolute_difference_test() { + maths.int_absolute_difference(20, 15) + |> should.equal(5) + + maths.int_absolute_difference(-20, -15) + |> should.equal(5) + + maths.int_absolute_difference(20, -15) + |> should.equal(35) + + maths.int_absolute_difference(-20, 15) + |> should.equal(35) +} + +pub fn float_sign_test() { + maths.float_sign(100.0) + |> should.equal(1.0) + + maths.float_sign(0.0) + |> should.equal(0.0) + + maths.float_sign(-100.0) + |> should.equal(-1.0) +} + +pub fn float_flip_sign_test() { + maths.float_flip_sign(100.0) + |> should.equal(-100.0) + + maths.float_flip_sign(0.0) + |> should.equal(-0.0) + + maths.float_flip_sign(-100.0) + |> should.equal(100.0) +} + +pub fn float_copy_sign_test() { + maths.float_copy_sign(100.0, 10.0) + |> should.equal(100.0) + + maths.float_copy_sign(-100.0, 10.0) + |> should.equal(100.0) + + maths.float_copy_sign(100.0, -10.0) + |> should.equal(-100.0) + + maths.float_copy_sign(-100.0, -10.0) + |> should.equal(-100.0) +} + +pub fn int_sign_test() { + maths.int_sign(100) + |> should.equal(1) + + maths.int_sign(0) + |> should.equal(0) + + maths.int_sign(-100) + |> should.equal(-1) +} + +pub fn int_flip_sign_test() { + maths.int_flip_sign(100) + |> should.equal(-100) + + maths.int_flip_sign(0) + |> should.equal(-0) + + maths.int_flip_sign(-100) + |> should.equal(100) +} + +pub fn int_copy_sign_test() { + maths.int_copy_sign(100, 10) + |> should.equal(100) + + maths.int_copy_sign(-100, 10) + |> should.equal(100) + + maths.int_copy_sign(100, -10) + |> should.equal(-100) + + maths.int_copy_sign(-100, -10) + |> should.equal(-100) +} + +pub fn float_minmax_test() { + maths.minmax(0.75, 0.5, float.compare) + |> should.equal(#(0.5, 0.75)) + + maths.minmax(0.5, 0.75, float.compare) + |> should.equal(#(0.5, 0.75)) + + maths.minmax(-0.75, 0.5, float.compare) + |> should.equal(#(-0.75, 0.5)) + + maths.minmax(-0.75, 0.5, float.compare) + |> should.equal(#(-0.75, 0.5)) +} + +pub fn int_minmax_test() { + maths.minmax(75, 50, int.compare) + |> should.equal(#(50, 75)) + + maths.minmax(50, 75, int.compare) + |> should.equal(#(50, 75)) + + maths.minmax(-75, 50, int.compare) + |> should.equal(#(-75, 50)) + + maths.minmax(-75, 50, int.compare) + |> should.equal(#(-75, 50)) +} + +pub fn float_list_minimum_test() { + // An empty lists returns an error + [] + |> maths.list_minimum(float.compare) + |> should.be_error() + + // Valid input returns a result + [4.5, 4.5, 3.5, 2.5, 1.5] + |> maths.list_minimum(float.compare) + |> should.equal(Ok(1.5)) +} + +pub fn int_list_minimum_test() { + // An empty lists returns an error + [] + |> maths.list_minimum(int.compare) + |> should.be_error() + + // Valid input returns a result + [4, 4, 3, 2, 1] + |> maths.list_minimum(int.compare) + |> should.equal(Ok(1)) +} + +pub fn float_list_maximum_test() { + // An empty lists returns an error + [] + |> maths.list_maximum(float.compare) + |> should.be_error() + + // Valid input returns a result + [4.5, 4.5, 3.5, 2.5, 1.5] + |> maths.list_maximum(float.compare) + |> should.equal(Ok(4.5)) +} + +pub fn int_list_maximum_test() { + // An empty lists returns an error + [] + |> maths.list_maximum(int.compare) + |> should.be_error() + + // Valid input returns a result + [4, 4, 3, 2, 1] + |> maths.list_maximum(int.compare) + |> should.equal(Ok(4)) +} + +pub fn float_list_arg_maximum_test() { + // An empty lists returns an error + [] + |> maths.arg_maximum(float.compare) + |> should.be_error() + + // Valid input returns a result + [4.5, 4.5, 3.5, 2.5, 1.5] + |> maths.arg_maximum(float.compare) + |> should.equal(Ok([0, 1])) +} + +pub fn int_list_arg_maximum_test() { + // An empty lists returns an error + [] + |> maths.arg_maximum(int.compare) + |> should.be_error() + + // Valid input returns a result + [4, 4, 3, 2, 1] + |> maths.arg_maximum(int.compare) + |> should.equal(Ok([0, 1])) +} + +pub fn float_list_arg_minimum_test() { + // An empty lists returns an error + [] + |> maths.arg_minimum(float.compare) + |> should.be_error() + + // Valid input returns a result + [4.5, 4.5, 3.5, 2.5, 1.5] + |> maths.arg_minimum(float.compare) + |> should.equal(Ok([4])) +} + +pub fn int_list_arg_minimum_test() { + // An empty lists returns an error + [] + |> maths.arg_minimum(int.compare) + |> should.be_error() + + // Valid input returns a result + [4, 4, 3, 2, 1] + |> maths.arg_minimum(int.compare) + |> should.equal(Ok([4])) +} + +pub fn float_list_extrema_test() { + // An empty lists returns an error + [] + |> maths.extrema(float.compare) + |> should.be_error() + + // Valid input returns a result + [4.0, 4.0, 3.0, 2.0, 1.0] + |> maths.extrema(float.compare) + |> should.equal(Ok(#(1.0, 4.0))) + + // Valid input returns a result + [1.0, 4.0, 2.0, 5.0, 0.0] + |> maths.extrema(float.compare) + |> should.equal(Ok(#(0.0, 5.0))) +} + +pub fn int_list_extrema_test() { + // An empty lists returns an error + [] + |> maths.extrema(int.compare) + |> should.be_error() + + // Valid input returns a result + [4, 4, 3, 2, 1] + |> maths.extrema(int.compare) + |> should.equal(Ok(#(1, 4))) + + // Valid input returns a result + [1, 4, 2, 5, 0] + |> maths.extrema(int.compare) + |> should.equal(Ok(#(0, 5))) +} diff --git a/test/gleam_community/sequences_test.gleam b/test/gleam_community/sequences_test.gleam new file mode 100644 index 0000000..ef8b68f --- /dev/null +++ b/test/gleam_community/sequences_test.gleam @@ -0,0 +1,390 @@ +import gleam/float +import gleam/list +import gleam/yielder +import gleam_community/maths +import gleeunit/should + +pub fn float_list_linear_space_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Check that the function agrees, at some arbitrary input + // points, with known function values + // ---> With endpoint included + let assert Ok(linspace) = maths.linear_space(10.0, 50.0, 5, True) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([10.0, 20.0, 30.0, 40.0, 50.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + let assert Ok(linspace) = maths.linear_space(10.0, 20.0, 5, True) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([10.0, 12.5, 15.0, 17.5, 20.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // Try with negative stop + // ----> Without endpoint included + let assert Ok(linspace) = maths.linear_space(10.0, 50.0, 5, False) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([10.0, 18.0, 26.0, 34.0, 42.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + let assert Ok(linspace) = maths.linear_space(10.0, 20.0, 5, False) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([10.0, 12.0, 14.0, 16.0, 18.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // Try with negative stop + let assert Ok(linspace) = maths.linear_space(10.0, -50.0, 5, False) + let assert Ok(result) = + maths.all_close( + linspace + |> yielder.to_list() + |> list.zip([10.0, -2.0, -14.0, -26.0, -38.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + let assert Ok(linspace) = maths.linear_space(10.0, -20.0, 5, True) + let assert Ok(result) = + maths.all_close( + linspace + |> yielder.to_list() + |> list.zip([10.0, 2.5, -5.0, -12.5, -20.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // Try with negative start + let assert Ok(linspace) = maths.linear_space(-10.0, 50.0, 5, False) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([-10.0, 2.0, 14.0, 26.0, 38.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + let assert Ok(linspace) = maths.linear_space(-10.0, 20.0, 5, True) + let assert Ok(result) = + maths.all_close( + linspace |> yielder.to_list() |> list.zip([-10.0, -2.5, 5.0, 12.5, 20.0]), + 0.0, + tol, + ) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // A negative number of points does not work (-5) + maths.linear_space(10.0, 50.0, -5, True) + |> should.be_error() +} + +pub fn float_list_logarithmic_space_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + // ---> With endpoint included + // - Positive start, stop, base + let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, True, 10.0) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([10.0, 100.0, 1000.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, stop, negative base + let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, True, -10.0) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([-10.0, 100.0, -1000.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, negative stop, base + let assert Ok(logspace) = maths.logarithmic_space(1.0, -3.0, 3, True, -10.0) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([-10.0, -0.1, -0.001]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, base, negative stop + let assert Ok(logspace) = maths.logarithmic_space(1.0, -3.0, 3, True, 10.0) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([10.0, 0.1, 0.001]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive stop, base, negative start + let assert Ok(logspace) = maths.logarithmic_space(-1.0, 3.0, 3, True, 10.0) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([0.1, 10.0, 1000.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // ----> Without endpoint included + // - Positive start, stop, base + let assert Ok(logspace) = maths.logarithmic_space(1.0, 3.0, 3, False, 10.0) + let assert Ok(result) = + maths.all_close( + logspace + |> yielder.to_list() + |> list.zip([10.0, 46.41588834, 215.443469]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // A negative number of points does not work (-3) + maths.logarithmic_space(1.0, 3.0, -3, True, 10.0) + |> should.be_error() +} + +pub fn float_list_geometric_space_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + // Check that the function agrees, at some arbitrary input + // points, with known function values + // ---> With endpoint included + // - Positive start, stop + let assert Ok(logspace) = maths.geometric_space(10.0, 1000.0, 3, True) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([10.0, 100.0, 1000.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, negative stop + let assert Ok(logspace) = maths.geometric_space(10.0, 0.001, 3, True) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([10.0, 0.1, 0.001]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive stop, negative start + let assert Ok(logspace) = maths.geometric_space(0.1, 1000.0, 3, True) + let assert Ok(result) = + maths.all_close( + logspace |> yielder.to_list() |> list.zip([0.1, 10.0, 1000.0]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // ----> Without endpoint included + // - Positive start, stop + let assert Ok(logspace) = maths.geometric_space(10.0, 1000.0, 3, False) + let assert Ok(result) = + maths.all_close( + logspace + |> yielder.to_list() + |> list.zip([10.0, 46.41588834, 215.443469]), + 0.0, + tol, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // Test invalid input (start and stop can't be equal to 0.0) + maths.geometric_space(0.0, 1000.0, 3, False) + |> should.be_error() + + maths.geometric_space(-1000.0, 0.0, 3, False) + |> should.be_error() + + // A negative number of points does not work + maths.geometric_space(-1000.0, 0.0, -3, False) + |> should.be_error() +} + +pub fn float_list_arange_test() { + // Positive start, stop, step + maths.arange(1.0, 5.0, 1.0) + |> yielder.to_list() + |> should.equal([1.0, 2.0, 3.0, 4.0]) + + maths.arange(1.0, 5.0, 0.5) + |> yielder.to_list() + |> should.equal([1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5]) + + maths.arange(1.0, 2.0, 0.25) + |> yielder.to_list() + |> should.equal([1.0, 1.25, 1.5, 1.75]) + + // Reverse (switch start/stop largest/smallest value) + maths.arange(5.0, 1.0, 1.0) + |> yielder.to_list() + |> should.equal([]) + + // Reverse negative step + maths.arange(5.0, 1.0, -1.0) + |> yielder.to_list() + |> should.equal([5.0, 4.0, 3.0, 2.0]) + + // Positive start, negative stop, step + maths.arange(5.0, -1.0, -1.0) + |> yielder.to_list() + |> should.equal([5.0, 4.0, 3.0, 2.0, 1.0, 0.0]) + + // Negative start, stop, step + maths.arange(-5.0, -1.0, -1.0) + |> yielder.to_list() + |> should.equal([]) + + // Negative start, stop, positive step + maths.arange(-5.0, -1.0, 1.0) + |> yielder.to_list() + |> should.equal([-5.0, -4.0, -3.0, -2.0]) +} + +pub fn float_list_exponential_space_test() { + let assert Ok(tolerance) = float.power(10.0, -6.0) + + // Check that the function agrees, at some arbitrary input + // points, with known function values + // ---> With endpoint included + let assert Ok(exp_space) = maths.exponential_space(1.0, 1000.0, 4, True) + let assert Ok(result) = + maths.all_close( + exp_space |> yielder.to_list() |> list.zip([1.0, 10.0, 100.0, 1000.0]), + 0.0, + tolerance, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // ---> Without endpoint included + let assert Ok(exp_space) = maths.exponential_space(1.0, 1000.0, 4, False) + let assert Ok(result) = + maths.all_close( + exp_space + |> yielder.to_list() + |> list.zip([ + 1.0, 5.623413251903491, 31.622776601683793, 177.82794100389228, + ]), + 0.0, + tolerance, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // A negative number of points does not work (-3) + maths.exponential_space(1.0, 1000.0, -3, True) + |> should.be_error() +} + +pub fn float_list_symmetric_space_test() { + let assert Ok(tolerance) = float.power(10.0, -6.0) + + // Check that the function agrees, at some arbitrary input + // points, with known function values + let assert Ok(sym_space) = maths.symmetric_space(0.0, 5.0, 5) + sym_space + |> yielder.to_list() + |> should.equal([-5.0, -2.5, 0.0, 2.5, 5.0]) + + let assert Ok(sym_space) = maths.symmetric_space(0.0, 5.0, 5) + sym_space + |> yielder.to_list() + |> should.equal([-5.0, -2.5, 0.0, 2.5, 5.0]) + + // Negative center + let assert Ok(sym_space) = maths.symmetric_space(-10.0, 5.0, 5) + sym_space + |> yielder.to_list() + |> should.equal([-15.0, -12.5, -10.0, -7.5, -5.0]) + + // Uneven number of points + let assert Ok(sym_space) = maths.symmetric_space(0.0, 2.0, 4) + let assert Ok(result) = + maths.all_close( + sym_space + |> yielder.to_list() + |> list.zip([-2.0, -0.6666666666666667, 0.6666666666666665, 2.0]), + 0.0, + tolerance, + ) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // A negative number of points does not work (-5) + maths.symmetric_space(0.0, 5.0, -5) + |> should.be_error() +} diff --git a/test/gleam_community/special_test.gleam b/test/gleam_community/special_test.gleam new file mode 100644 index 0000000..ab07eb9 --- /dev/null +++ b/test/gleam_community/special_test.gleam @@ -0,0 +1,113 @@ +import gleam/float +import gleam/result +import gleam_community/maths +import gleeunit/should + +pub fn float_beta_function_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Valid input returns a result + maths.beta(-0.5, 0.5) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.beta(0.5, 0.5) + |> maths.is_close(3.1415926535897927, 0.0, tol) + |> should.be_true() + + maths.beta(0.5, -0.5) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.beta(5.0, 5.0) + |> maths.is_close(0.0015873015873015873, 0.0, tol) + |> should.be_true() +} + +pub fn float_error_function_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Valid input returns a result + maths.erf(-0.5) + |> maths.is_close(-0.5204998778130465, 0.0, tol) + |> should.be_true() + + maths.erf(0.5) + |> maths.is_close(0.5204998778130465, 0.0, tol) + |> should.be_true() + + maths.erf(1.0) + |> maths.is_close(0.8427007929497148, 0.0, tol) + |> should.be_true() + + maths.erf(2.0) + |> maths.is_close(0.9953222650189527, 0.0, tol) + |> should.be_true() + + maths.erf(10.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_gamma_function_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Valid input returns a result + maths.gamma(-0.5) + |> maths.is_close(-3.5449077018110318, 0.0, tol) + |> should.be_true() + + maths.gamma(0.5) + |> maths.is_close(1.7724538509055159, 0.0, tol) + |> should.be_true() + + maths.gamma(1.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.gamma(2.0) + |> maths.is_close(1.0, 0.0, tol) + |> should.be_true() + + maths.gamma(3.0) + |> maths.is_close(2.0, 0.0, tol) + |> should.be_true() + + maths.gamma(10.0) + |> maths.is_close(362_880.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_incomplete_gamma_function_test() { + let assert Ok(tol) = float.power(10.0, -6.0) + + // Invalid input gives an error + // 1st arg is invalid + maths.incomplete_gamma(-1.0, 1.0) + |> should.be_error() + + // 2nd arg is invalid + maths.incomplete_gamma(1.0, -1.0) + |> should.be_error() + + // Valid input returns a result + maths.incomplete_gamma(1.0, 0.0) + |> result.unwrap(-999.0) + |> maths.is_close(0.0, 0.0, tol) + |> should.be_true() + + maths.incomplete_gamma(1.0, 2.0) + |> result.unwrap(-999.0) + |> maths.is_close(0.864664716763387308106, 0.0, tol) + |> should.be_true() + + maths.incomplete_gamma(2.0, 3.0) + |> result.unwrap(-999.0) + |> maths.is_close(0.8008517265285442280826, 0.0, tol) + |> should.be_true() + + maths.incomplete_gamma(3.0, 4.0) + |> result.unwrap(-999.0) + |> maths.is_close(1.523793388892911312363, 0.0, tol) + |> should.be_true() +}