diff options
author | Ethan Thoma <ethanthoma@gmail.com> | 2024-12-21 14:09:12 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-21 22:09:12 +0000 |
commit | 9d76bea763732dee0358feaeae46840cb75093be (patch) | |
tree | 767277049d9bedd56652ebf378cd35db3e98c4a3 | |
parent | 9dee307c7f1fa0641975c98a81da0b6f763c24b5 (diff) | |
download | gleam_stdlib-9d76bea763732dee0358feaeae46840cb75093be.tar.gz gleam_stdlib-9d76bea763732dee0358feaeae46840cb75093be.zip |
Add `sample` function to List module, add `log` and `exp` functions to Float module (#772)
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | src/gleam/float.gleam | 65 | ||||
-rw-r--r-- | src/gleam/list.gleam | 67 | ||||
-rw-r--r-- | src/gleam_stdlib.mjs | 13 | ||||
-rw-r--r-- | test/gleam/float_test.gleam | 78 | ||||
-rw-r--r-- | test/gleam/list_test.gleam | 49 |
6 files changed, 274 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b1cc9..8c926ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## v0.49.0 - 2024-12-19 -- The `list` module gains the `max` function. +- The `float` module gains the `exponential` and `logarithm` function. +- The `list` module gains the `max` and `sample` function. ## v0.48.0 - 2024-12-17 diff --git a/src/gleam/float.gleam b/src/gleam/float.gleam index b313632..34dba05 100644 --- a/src/gleam/float.gleam +++ b/src/gleam/float.gleam @@ -572,3 +572,68 @@ pub fn multiply(a: Float, b: Float) -> Float { pub fn subtract(a: Float, b: Float) -> Float { a -. b } + +/// Returns the natural logarithm (base e) of the given as a `Result`. If the +/// input is less than or equal to 0, returns `Error(Nil)`. +/// +/// ## Examples +/// +/// ```gleam +/// logarithm(1.0) +/// // -> Ok(0.0) +/// ``` +/// +/// ```gleam +/// logarithm(2.718281828459045) // e +/// // -> Ok(1.0) +/// ``` +/// +/// ```gleam +/// logarithm(0.0) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// logarithm(-1.0) +/// // -> Error(Nil) +/// ``` +/// +pub fn logarithm(x: Float) -> Result(Float, Nil) { + // In the following check: + // 1. If x is negative then return an error as the natural logarithm + // of a negative number is undefined (would be a complex number) + // 2. If x is 0 then return an error as the natural logarithm of 0 + // approaches negative infinity + case x <=. 0.0 { + True -> Error(Nil) + False -> Ok(do_log(x)) + } +} + +@external(erlang, "math", "log") +@external(javascript, "../gleam_stdlib.mjs", "log") +fn do_log(x: Float) -> Float + +/// Returns e (Euler's number) raised to the power of the given exponent, as +/// a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// exponential(0.0) +/// // -> Ok(1.0) +/// ``` +/// +/// ```gleam +/// exponential(1.0) +/// // -> Ok(2.718281828459045) +/// ``` +/// +/// ```gleam +/// exponential(-1.0) +/// // -> Ok(0.36787944117144233) +/// ``` +/// +@external(erlang, "math", "exp") +@external(javascript, "../gleam_stdlib.mjs", "exp") +pub fn exponential(x: Float) -> Float diff --git a/src/gleam/list.gleam b/src/gleam/list.gleam index 6c41df0..c4211f8 100644 --- a/src/gleam/list.gleam +++ b/src/gleam/list.gleam @@ -2337,3 +2337,70 @@ pub fn max( } }) } + +/// Take a random sample of k elements from a list using reservoir sampling via +/// Algo L. Returns an empty list if the sample size is less than or equal to 0. +/// +/// Order is not random, only selection is. +/// +/// ## Examples +/// +/// ```gleam +/// reservoir_sample([1, 2, 3, 4, 5], 3) +/// // -> [2, 4, 5] // A random sample of 3 items +/// ``` +/// +pub fn sample(list: List(a), k: Int) -> List(a) { + case k <= 0 { + True -> [] + False -> { + let #(reservoir, list) = split(list, k) + + case length(reservoir) < k { + True -> reservoir + False -> { + let reservoir = + reservoir + |> map2(range(0, k - 1), _, fn(a, b) { #(a, b) }) + |> dict.from_list + + let w = float.exponential(log_random() /. int.to_float(k)) + + sample_loop(list, reservoir, k, k, w) |> dict.values + } + } + } + } +} + +fn sample_loop( + list, + reservoir: Dict(Int, a), + k, + index: Int, + w: Float, +) -> Dict(Int, a) { + let skip = { + let assert Ok(log_result) = float.logarithm(1.0 -. w) + + log_random() /. log_result |> float.floor |> float.round + } + + let index = index + skip + 1 + + case drop(list, skip) { + [] -> reservoir + [elem, ..rest] -> { + let reservoir = int.random(k) |> dict.insert(reservoir, _, elem) + let w = w *. float.exponential(log_random() /. int.to_float(k)) + + sample_loop(rest, reservoir, k, index, w) + } + } +} + +fn log_random() -> Float { + let min_positive = 2.2250738585072014e-308 + let assert Ok(random) = float.logarithm(float.random() +. min_positive) + random +} diff --git a/src/gleam_stdlib.mjs b/src/gleam_stdlib.mjs index 4f804f7..339963b 100644 --- a/src/gleam_stdlib.mjs +++ b/src/gleam_stdlib.mjs @@ -1010,3 +1010,16 @@ export function bit_array_starts_with(bits, prefix) { return true; } + +export function log(x) { + // It is checked in Gleam that: + // - The input is strictly positive (x > 0) + // - This ensures that Math.log will never return NaN or -Infinity + // The function can thus safely pass the input to Math.log + // and a valid finite float will always be produced. + return Math.log(x); +} + +export function exp(x) { + return Math.exp(x); +} diff --git a/test/gleam/float_test.gleam b/test/gleam/float_test.gleam index 7124d80..1c8f96b 100644 --- a/test/gleam/float_test.gleam +++ b/test/gleam/float_test.gleam @@ -528,3 +528,81 @@ pub fn subtract_test() { |> float.subtract(2.0, _) |> should.equal(-1.0) } + +pub fn logarithm_test() { + float.logarithm(1.0) + |> result.unwrap(or: 1.0) + |> float.loosely_equals(with: 0.0, tolerating: 0.001) + |> should.be_true + + float.logarithm(2.718281828459045) + |> result.unwrap(or: 0.0) + |> float.loosely_equals(with: 1.0, tolerating: 0.001) + |> should.be_true + + float.logarithm(10.0) + |> result.unwrap(or: 0.0) + |> float.loosely_equals(with: 2.302585092994046, tolerating: 0.001) + |> should.be_true + + float.logarithm(100.0) + |> result.unwrap(or: 0.0) + |> float.loosely_equals(with: 4.605170185988092, tolerating: 0.001) + |> should.be_true + + float.logarithm(0.5) + |> result.unwrap(or: 0.0) + |> float.loosely_equals(with: -0.6931471805599453, tolerating: 0.001) + |> should.be_true + + float.logarithm(0.1) + |> result.unwrap(or: 0.0) + |> float.loosely_equals(with: -2.3025850929940455, tolerating: 0.001) + |> should.be_true + + float.logarithm(0.0) + |> should.equal(Error(Nil)) + + float.logarithm(-1.0) + |> should.equal(Error(Nil)) + + float.logarithm(-100.0) + |> should.equal(Error(Nil)) + + float.logarithm(-0.1) + |> should.equal(Error(Nil)) +} + +pub fn exponential_test() { + float.exponential(0.0) + |> float.loosely_equals(with: 1.0, tolerating: 0.001) + |> should.be_true + + float.exponential(1.0) + |> float.loosely_equals(with: 2.718281828459045, tolerating: 0.001) + |> should.be_true + + float.exponential(2.0) + |> float.loosely_equals(with: 7.38905609893065, tolerating: 0.001) + |> should.be_true + + float.exponential(-1.0) + |> float.loosely_equals(with: 0.36787944117144233, tolerating: 0.001) + |> should.be_true + + float.exponential(5.0) + |> float.loosely_equals(with: 148.4131591025766, tolerating: 0.001) + |> should.be_true + + float.exponential(-5.0) + |> float.loosely_equals(with: 0.006737946999085467, tolerating: 0.001) + |> should.be_true + + float.exponential(0.000001) + |> float.loosely_equals(with: 1.0000010000005, tolerating: 0.001) + |> should.be_true + + float.exponential(-100.0) + |> float.loosely_equals(with: 3.720075976020836e-44, tolerating: 0.001) + |> should.be_true +} diff --git a/test/gleam/list_test.gleam b/test/gleam/list_test.gleam index 2a67f94..6228396 100644 --- a/test/gleam/list_test.gleam +++ b/test/gleam/list_test.gleam @@ -1299,3 +1299,52 @@ pub fn max_test() { |> list.max(string.compare) |> should.equal(Ok("c")) } + +pub fn sample_test() { + [] + |> list.sample(3) + |> should.equal([]) + + [1, 2, 3] + |> list.sample(0) + |> should.equal([]) + + [1, 2, 3] + |> list.sample(-1) + |> should.equal([]) + + [1, 2] + |> list.sample(5) + |> list.sort(int.compare) + |> should.equal([1, 2]) + + [1] + |> list.sample(1) + |> should.equal([1]) + + let input = list.range(1, 100) + let sample = list.sample(input, 10) + list.length(sample) + |> should.equal(10) + + let repeated = [1, 1, 1, 1, 1] + let sample = list.sample(repeated, 3) + sample + |> list.all(fn(x) { x == 1 }) + |> should.be_true() + + let input = list.range(1, 1000) + let sample = list.sample(input, 100) + sample + |> list.sort(int.compare) + |> list.all(fn(x) { x >= 1 && x <= 1000 }) + |> should.be_true() + + list.length(sample) + |> should.equal(100) + + let min = list.fold(sample, 1000, int.min) + let max = list.fold(sample, 1, int.max) + should.be_true(min >= 1) + should.be_true(max <= 1000) +}
\ No newline at end of file |