From b0da87f755afaf73f21b0a1ef76d744c264d1c78 Mon Sep 17 00:00:00 2001 From: NicklasXYZ Date: Sat, 28 Jan 2023 20:55:47 +0100 Subject: [PATCH] work on float_list module --- src/gleam_community/maths/float.gleam | 1 + src/gleam_community/maths/float_list.gleam | 257 +++++++++++++++--- ...leam_community_maths_float_list_test.gleam | 250 +++++++++++++++++ 3 files changed, 477 insertions(+), 31 deletions(-) diff --git a/src/gleam_community/maths/float.gleam b/src/gleam_community/maths/float.gleam index 0df101c..cfc81ff 100644 --- a/src/gleam_community/maths/float.gleam +++ b/src/gleam_community/maths/float.gleam @@ -1108,6 +1108,7 @@ if javascript { "../../maths.mjs" "exponential" } +// TODO: Update description here below ///
/// /// Spot a typo? Open an issue! diff --git a/src/gleam_community/maths/float_list.gleam b/src/gleam_community/maths/float_list.gleam index cf10672..4f49abf 100644 --- a/src/gleam_community/maths/float_list.gleam +++ b/src/gleam_community/maths/float_list.gleam @@ -33,8 +33,9 @@ //// * [`cumulative_sum`](#cumulative_sum) //// * [`cumulative_product`](#cumulative_product) //// * **Ranges and intervals** +//// * [`arrange`](#arrange) //// * [`linear_space`](#linear_space) -//// * [`logarithm_space`](#logarithm_space) +//// * [`logarithmic_space`](#logarithmic_space) //// * [`geometric_space`](#geometric_space) //// * **Misc. mathematical functions** //// * [`maximum`](#maximum) @@ -127,7 +128,7 @@ pub fn norm(arr: List(Float), p: Float) -> Float { /// \left( \sum_{i=1}^n \left|x_i - x_j \right|^{p} \right)^{\frac{1}{p}} /// \\] /// -/// 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, j$$. +/// 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, j$$. /// /// The Minkowski distance is a generalization of both the Euclidean distance ($$p=2$$) and the Manhattan distance ($$p = 1$$). /// @@ -135,10 +136,28 @@ pub fn norm(arr: List(Float), p: Float) -> Float { /// Example: /// /// import gleeunit/should +/// import gleam_community/maths/float as floatx /// import gleam_community/maths/float_list /// /// pub fn example () { -/// +/// assert Ok(tol) = floatx.power(-10.0, -6.0) +/// +/// // Empty lists returns 0.0 +/// float_list.minkowski_distance([], [], 1.0) +/// |> should.equal(Ok(0.0)) +/// +/// // Differing lengths returns error +/// float_list.minkowski_distance([], [1.0], 1.0) +/// |> should.be_error() +/// +/// // Test order < 1 +/// float_list.minkowski_distance([0.0, 0.0], [0.0, 0.0], -1.0) +/// |> should.be_error() +/// +/// assert Ok(result) = float_list.minkowski_distance([0.0, 0.0], [1.0, 2.0], 1.0) +/// result +/// |> floatx.is_close(3.0, 0.0, tol) +/// |> should.be_true() /// } /// /// @@ -184,7 +203,7 @@ pub fn minkowski_distance( /// Calculcate the Euclidean distance between two lists (representing vectors): /// /// \\[ -/// \left( \sum_{i=1}^n \left|x_i - x_j \right|^{p} \right)^{\frac{1}{p}} +/// \left( \sum_{i=1}^n \left|x_i - x_j \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, j$$. @@ -193,10 +212,24 @@ pub fn minkowski_distance( /// Example: /// /// import gleeunit/should +/// import gleam_community/maths/float as floatx /// import gleam_community/maths/float_list /// /// pub fn example () { -/// +/// assert Ok(tol) = floatx.power(-10.0, -6.0) +/// +/// // Empty lists returns 0.0 +/// float_list.euclidean_distance([], [], 1.0) +/// |> should.equal(Ok(0.0)) +/// +/// // Differing lengths returns error +/// float_list.euclidean_distance([], [1.0], 1.0) +/// |> should.be_error() +/// +/// assert Ok(result) = float_list.euclidean_distance([0.0, 0.0], [1.0, 2.0]) +/// result +/// |> floatx.is_close(2.23606797749979, 0.0, tol) +/// |> should.be_true() /// } /// /// @@ -219,14 +252,36 @@ pub fn euclidean_distance( /// ///
/// +/// Calculcate the Euclidean distance between two lists (representing vectors): +/// +/// \\[ +/// \sum_{i=1}^n \left|x_i - x_j \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, j$$. +/// ///
/// Example: /// /// import gleeunit/should +/// import gleam_community/maths/float as floatx /// import gleam_community/maths/float_list /// /// pub fn example () { -/// +/// assert Ok(tol) = floatx.power(-10.0, -6.0) +/// +/// // Empty lists returns 0.0 +/// float_list.manhatten_distance([], [], 1.0) +/// |> should.equal(Ok(0.0)) +/// +/// // Differing lengths returns error +/// float_list.manhatten_distance([], [1.0], 1.0) +/// |> should.be_error() +/// +/// assert Ok(result) = float_list.manhatten_distance([0.0, 0.0], [1.0, 2.0]) +/// result +/// |> floatx.is_close(3.0, 0.0, tol) +/// |> should.be_true() /// } ///
/// @@ -249,18 +304,27 @@ pub fn manhatten_distance( /// /// /// -/// Return evenly spaced numbers over a specified interval. -/// Returns num evenly spaced samples, calculated over the interval [start, stop]. -/// The endpoint of the interval can optionally be excluded. +/// Generate a linearly spaced list of points over a specified interval. The endpoint of the interval can optionally be included/excluded. /// ///
/// Example: /// /// import gleeunit/should +/// import gleam_community/maths/float as floatx /// import gleam_community/maths/float_list /// /// pub fn example () { +/// assert Ok(tol) = floatx.power(-10.0, -6.0) +/// assert Ok(linspace) = float_list.linear_space(10.0, 50.0, 5, True) +/// assert Ok(result) = +/// float_list.all_close(linspace, [10.0, 20.0, 30.0, 40.0, 50.0], 0.0, tol) +/// result +/// |> list.all(fn(x) { x == True }) +/// |> should.be_true() /// +/// // A negative number of points (-5) does not work +/// float_list.linear_space(10.0, 50.0, -5, True) +/// |> should.be_error() /// } ///
/// @@ -274,9 +338,39 @@ pub fn linear_space( start: Float, stop: Float, num: Int, - endpoint: option.Option(Bool), -) -> List(Float) { - todo + endpoint: Bool, +) -> Result(List(Float), String) { + let direction: Float = case start <=. stop { + True -> 1.0 + False -> -1.0 + } + case num > 0 { + True -> + case endpoint { + True -> { + let increment: Float = + float.absolute_value(start -. stop) /. intx.to_float(num - 1) + list.range(0, num - 1) + |> list.map(fn(i: Int) -> Float { + start +. intx.to_float(i) *. increment *. direction + }) + |> Ok + } + False -> { + let increment: Float = + float.absolute_value(start -. stop) /. intx.to_float(num) + list.range(0, num - 1) + |> list.map(fn(i: Int) -> Float { + start +. intx.to_float(i) *. increment *. direction + }) + |> Ok + } + } + + False -> + "Invalid input: num < 0. Valid input is num > 0." + |> Error + } } ///
@@ -285,17 +379,27 @@ pub fn linear_space( /// ///
/// -/// Return numbers spaced evenly on a logarrithmic scale. -/// In linear space, the sequence starts at base ** start (base to the power of start) and ends with base ** stop. +/// Generate a logarithmically spaced list of points over a specified interval. The endpoint of the interval can optionally be included/excluded. /// ///
/// Example: /// /// import gleeunit/should +/// import gleam_community/maths/float as floatx /// import gleam_community/maths/float_list /// /// pub fn example () { +/// assert Ok(tol) = floatx.power(-10.0, -6.0) +/// assert Ok(logspace) = float_list.logarithmic_space(1.0, 3.0, 3, True) +/// assert Ok(result) = +/// float_list.all_close(logspace, [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 +/// float_list.logarithmic_space(1.0, 3.0, -3, False) +/// |> should.be_error() /// } ///
/// @@ -305,14 +409,27 @@ pub fn linear_space( /// /// /// -pub fn logarithm_space( +pub fn logarithmic_space( start: Float, stop: Float, num: Int, - endpoint: option.Option(Bool), - base: Int, -) -> List(Float) { - todo + endpoint: Bool, + base: Float, +) -> Result(List(Float), String) { + case num > 0 { + True -> { + assert Ok(linspace) = linear_space(start, stop, num, endpoint) + linspace + |> list.map(fn(i: Float) -> Float { + assert Ok(result) = floatx.power(base, i) + result + }) + |> Ok + } + False -> + "Invalid input: num < 0. Valid input is num > 0." + |> Error + } } ///
@@ -321,8 +438,75 @@ pub fn logarithm_space( /// ///
/// -/// Return numbers spaced evenly on a log scale (a geometric progression). -/// This is similar to logspace, but with endpoints specified directly. Each output sample is a constant multiple of the previous. +/// The function returns a list of numbers spaced evenly on a log scale (a geometric progression). Each output 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 gleeunit/should +/// import gleam_community/maths/float as floatx +/// import gleam_community/maths/float_list +/// +/// pub fn example () { +/// assert Ok(tol) = floatx.power(-10.0, -6.0) +/// assert Ok(logspace) = float_list.geometric_space(10.0, 1000.0, 3, True) +/// assert Ok(result) = +/// float_list.all_close(logspace, [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) +/// float_list.geometric_space(0.0, 1000.0, 3, False) +/// |> should.be_error() +/// +/// float_list.geometric_space(-1000.0, 0.0, 3, False) +/// |> should.be_error() +/// +/// // A negative number of points (-3) does not work +/// float_list.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(List(Float), String) { + case start == 0.0 || stop == 0.0 { + True -> + "" + |> Error + False -> + case num > 0 { + True -> { + assert Ok(log_start) = floatx.logarithm_10(start) + assert Ok(log_stop) = floatx.logarithm_10(stop) + logarithmic_space(log_start, log_stop, num, endpoint, 10.0) + } + False -> + "Invalid input: num < 0. Valid input is num > 0." + |> Error + } + } +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +/// The function returns a list with evenly spaced values within a given interval based on a start, stop value and a given increment (step-length) between consecutive values. /// ///
/// Example: @@ -341,12 +525,11 @@ pub fn logarithm_space( /// /// /// -pub fn geometric_space( +pub fn arrange( start: Float, stop: Float, - num: Int, - endpoint: option.Option(Bool), -) -> List(Float) { + step: Float, +) -> Result(List(Float), String) { todo } @@ -455,10 +638,11 @@ pub fn product(arr: List(Float)) -> Float { /// Calculcate the cumulative sum of the elements in a list: /// /// \\[ -/// \sum_{i=1}^n x_i +/// v_j = \sum_{i=1}^j x_i, \forall j \leq n /// \\] /// /// In the formula, $$n$$ is the length of the list and $$x_i$$ is the value in the input list indexed by $$i$$. +/// Furthermore, $$v_j$$ is the $$j$$th element in the cumulative sum. /// ///
/// Example: @@ -485,7 +669,12 @@ pub fn product(arr: List(Float)) -> Float { /// /// pub fn cumulative_sum(arr: List(Float)) -> List(Float) { - todo + case arr { + [] -> [] + _ -> + arr + |> list.scan(0.0, fn(acc: Float, a: Float) -> Float { a +. acc }) + } } ///
@@ -497,10 +686,11 @@ pub fn cumulative_sum(arr: List(Float)) -> List(Float) { /// Calculcate the cumulative product of the elements in a list: /// /// \\[ -/// \prod_{i=1}^n x_i +/// v_j = \prod_{i=1}^j x_i, \forall j \leq n /// \\] /// /// In the formula, $$n$$ is the length of the list and $$x_i$$ is the value in the input list indexed by $$i$$. +/// Furthermore, $$v_j$$ is the $$j$$th element in the cumulative product. /// ///
/// Example: @@ -511,8 +701,8 @@ pub fn cumulative_sum(arr: List(Float)) -> List(Float) { /// pub fn example () { /// // An empty list returns an error /// [] -/// |> float_list.sum() -/// |> should.equal(0.) +/// |> float_list.cumulative_product() +/// |> should.equal([]) /// /// // Valid input returns a result /// [1.0, 2.0, 3.0] @@ -528,7 +718,12 @@ pub fn cumulative_sum(arr: List(Float)) -> List(Float) { ///
/// pub fn cumumlative_product(arr: List(Float)) -> List(Float) { - todo + case arr { + [] -> [] + _ -> + arr + |> list.scan(1.0, fn(acc: Float, a: Float) -> Float { a *. acc }) + } } ///
diff --git a/test/gleam/gleam_community_maths_float_list_test.gleam b/test/gleam/gleam_community_maths_float_list_test.gleam index 717f1c3..599578d 100644 --- a/test/gleam/gleam_community_maths_float_list_test.gleam +++ b/test/gleam/gleam_community_maths_float_list_test.gleam @@ -121,10 +121,236 @@ pub fn float_list_minkowski_test() { |> floatx.is_close(1.0717734625362931, 0.0, tol) |> should.be_true() + // Euclidean distance (p = 2) assert Ok(result) = float_list.minkowski_distance([0.0, 0.0], [1.0, 2.0], 2.0) result |> floatx.is_close(2.23606797749979, 0.0, tol) |> should.be_true() + + // Manhatten distance (p = 1) + assert Ok(result) = float_list.minkowski_distance([0.0, 0.0], [1.0, 2.0], 1.0) + result + |> floatx.is_close(3.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_list_euclidean_test() { + assert Ok(tol) = floatx.power(-10.0, -6.0) + + // Empty lists returns 0.0 + float_list.euclidean_distance([], []) + |> should.equal(Ok(0.0)) + + // Differing lenghths returns error + float_list.euclidean_distance([], [1.0]) + |> should.be_error() + + // Euclidean distance (p = 2) + assert Ok(result) = float_list.euclidean_distance([0.0, 0.0], [1.0, 2.0]) + result + |> floatx.is_close(2.23606797749979, 0.0, tol) + |> should.be_true() +} + +pub fn float_list_manhatten_test() { + assert Ok(tol) = floatx.power(-10.0, -6.0) + + // Empty lists returns 0.0 + float_list.manhatten_distance([], []) + |> should.equal(Ok(0.0)) + + // Differing lenghths returns error + float_list.manhatten_distance([], [1.0]) + |> should.be_error() + + // Manhatten distance (p = 1) + assert Ok(result) = float_list.manhatten_distance([0.0, 0.0], [1.0, 2.0]) + result + |> floatx.is_close(3.0, 0.0, tol) + |> should.be_true() +} + +pub fn float_list_linear_space_test() { + assert Ok(tol) = floatx.power(-10.0, -6.0) + + // Check that the function agrees, at some arbitrary input + // points, with known function values + // ---> With endpoint included + assert Ok(linspace) = float_list.linear_space(10.0, 50.0, 5, True) + assert Ok(result) = + float_list.all_close(linspace, [10.0, 20.0, 30.0, 40.0, 50.0], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + assert Ok(linspace) = float_list.linear_space(10.0, 20.0, 5, True) + assert Ok(result) = + float_list.all_close(linspace, [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 + assert Ok(linspace) = float_list.linear_space(10.0, 50.0, 5, False) + assert Ok(result) = + float_list.all_close(linspace, [10.0, 18.0, 26.0, 34.0, 42.0], 0.0, tol) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + assert Ok(linspace) = float_list.linear_space(10.0, 20.0, 5, False) + assert Ok(result) = + float_list.all_close(linspace, [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 + assert Ok(linspace) = float_list.linear_space(10.0, -50.0, 5, False) + assert Ok(result) = + float_list.all_close(linspace, [10.0, -2.0, -14.0, -26.0, -38.0], 0.0, tol) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + assert Ok(linspace) = float_list.linear_space(10.0, -20.0, 5, True) + assert Ok(result) = + float_list.all_close(linspace, [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 + assert Ok(linspace) = float_list.linear_space(-10.0, 50.0, 5, False) + assert Ok(result) = + float_list.all_close(linspace, [-10.0, 2.0, 14.0, 26.0, 38.0], 0.0, tol) + + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + assert Ok(linspace) = float_list.linear_space(-10.0, 20.0, 5, True) + assert Ok(result) = + float_list.all_close(linspace, [-10.0, -2.5, 5.0, 12.5, 20.0], 0.0, tol) + + // A negative number of points does not work (-5) + float_list.linear_space(10.0, 50.0, -5, True) + |> should.be_error() +} + +pub fn float_list_logarithmic_space_test() { + assert Ok(tol) = floatx.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 + assert Ok(logspace) = float_list.logarithmic_space(1.0, 3.0, 3, True, 10.0) + assert Ok(result) = + float_list.all_close(logspace, [10.0, 100.0, 1000.0], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, stop, negative base + assert Ok(logspace) = float_list.logarithmic_space(1.0, 3.0, 3, True, -10.0) + assert Ok(result) = + float_list.all_close(logspace, [-10.0, 100.0, -1000.0], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, negative stop, base + assert Ok(logspace) = float_list.logarithmic_space(1.0, -3.0, 3, True, -10.0) + assert Ok(result) = + float_list.all_close(logspace, [-10.0, -0.1, -0.001], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, base, negative stop + assert Ok(logspace) = float_list.logarithmic_space(1.0, -3.0, 3, True, 10.0) + assert Ok(result) = + float_list.all_close(logspace, [10.0, 0.1, 0.001], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive stop, base, negative start + assert Ok(logspace) = float_list.logarithmic_space(-1.0, 3.0, 3, True, 10.0) + assert Ok(result) = + float_list.all_close(logspace, [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 + assert Ok(logspace) = float_list.logarithmic_space(1.0, 3.0, 3, False, 10.0) + assert Ok(result) = + float_list.all_close(logspace, [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) + float_list.logarithmic_space(1.0, 3.0, -3, True, 10.0) + |> should.be_error() +} + +pub fn float_list_geometric_space_test() { + assert Ok(tol) = floatx.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 + assert Ok(logspace) = float_list.geometric_space(10.0, 1000.0, 3, True) + assert Ok(result) = + float_list.all_close(logspace, [10.0, 100.0, 1000.0], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive start, negative stop + assert Ok(logspace) = float_list.geometric_space(10.0, 0.001, 3, True) + assert Ok(result) = + float_list.all_close(logspace, [10.0, 0.1, 0.001], 0.0, tol) + result + |> list.all(fn(x) { x == True }) + |> should.be_true() + + // - Positive stop, negative start + assert Ok(logspace) = float_list.geometric_space(0.1, 1000.0, 3, True) + assert Ok(result) = + float_list.all_close(logspace, [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 + assert Ok(logspace) = float_list.geometric_space(10.0, 1000.0, 3, False) + assert Ok(result) = + float_list.all_close(logspace, [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) + float_list.geometric_space(0.0, 1000.0, 3, False) + |> should.be_error() + + float_list.geometric_space(-1000.0, 0.0, 3, False) + |> should.be_error() + + // A negative number of points does not work + float_list.geometric_space(-1000.0, 0.0, -3, False) + |> should.be_error() } pub fn float_list_maximum_test() { @@ -186,3 +412,27 @@ pub fn float_list_extrema_test() { |> float_list.extrema() |> should.equal(Ok(#(1.0, 4.0))) } + +pub fn float_list_cumulative_sum_test() { + // An empty lists returns an empty list + [] + |> float_list.cumulative_sum() + |> should.equal([]) + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> float_list.cumulative_sum() + |> should.equal([1.0, 3.0, 6.0]) +} + +pub fn float_list_cumulative_product_test() { + // An empty lists returns an empty list + [] + |> float_list.cumumlative_product() + |> should.equal([]) + + // Valid input returns a result + [1.0, 2.0, 3.0] + |> float_list.cumumlative_product() + |> should.equal([1.0, 2.0, 6.0]) +}