aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEthan Thoma <ethanthoma@gmail.com>2024-12-21 14:09:12 -0800
committerGitHub <noreply@github.com>2024-12-21 22:09:12 +0000
commit9d76bea763732dee0358feaeae46840cb75093be (patch)
tree767277049d9bedd56652ebf378cd35db3e98c4a3
parent9dee307c7f1fa0641975c98a81da0b6f763c24b5 (diff)
downloadgleam_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.md3
-rw-r--r--src/gleam/float.gleam65
-rw-r--r--src/gleam/list.gleam67
-rw-r--r--src/gleam_stdlib.mjs13
-rw-r--r--test/gleam/float_test.gleam78
-rw-r--r--test/gleam/list_test.gleam49
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