feat: add binomial distribution calc (#1)
Also, renames the project Reviewed-on: #1 Co-authored-by: Felipe Contreras Salinas <felipe@bstr.cl> Co-committed-by: Felipe Contreras Salinas <felipe@bstr.cl>
This commit is contained in:
parent
05fe59302d
commit
5947ac60d5
18 changed files with 1092 additions and 614 deletions
938
Cargo.lock
generated
938
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
22
Cargo.toml
22
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "hypergeometric-calc"
|
||||
version = "0.1.0"
|
||||
name = "distribution-calc"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
authors = ["Felipe Contreras Salinas <felipe@bstr.cl>"]
|
||||
|
||||
|
|
@ -9,18 +9,18 @@ authors = ["Felipe Contreras Salinas <felipe@bstr.cl>"]
|
|||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0.0"
|
||||
fluent-templates = "0.13.0"
|
||||
leptos = { version = "0.8.2", features = ["csr", "tracing"] }
|
||||
leptos-fluent = "0.2.15"
|
||||
leptos_meta = { version = "0.8.2" }
|
||||
leptos_router = { version = "0.8.2" }
|
||||
log = "0.4.26"
|
||||
fluent-templates = "0.13.2"
|
||||
leptos = { version = "0.8.14", features = ["csr", "tracing"] }
|
||||
leptos-fluent = "0.2.20"
|
||||
leptos_meta = { version = "0.8.5" }
|
||||
leptos_router = { version = "0.8.10" }
|
||||
log = "0.4.29"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = { version = "0.3.77", features = ["Document", "Window"] }
|
||||
wasm-bindgen = "0.2.106"
|
||||
wasm-bindgen-test = "0.3.56"
|
||||
web-sys = { version = "0.3.83", features = ["Document", "Window"] }
|
||||
|
||||
|
||||
[profile.release]
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -219,7 +219,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
hypergeometric-calc
|
||||
distribution-calc
|
||||
Copyright (C) 2025 LuckyMeowth
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# hypergeometric-calc
|
||||
Web calculator for Hypergeometric Distribution built using [Leptos].
|
||||
# distribution-calc
|
||||
Web calculator for Probability Distributions built using [Leptos].
|
||||
|
||||
## Dependencies
|
||||
You will need to install the Rust toolchain. We recommend to do using [Rustup]. Once
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
not-found = We couldn't find that page.
|
||||
title = Hypergeometric Distribution Calculator
|
||||
title-home = Calculators
|
||||
title-binom = Binomial Distribution Calculator
|
||||
title-hyper = Hypergeometric Distribution Calculator
|
||||
population = Population Size
|
||||
successes = Successes in Population
|
||||
sample = Sample Size
|
||||
sample-successes = Successes in Sample
|
||||
hyper-description = Hypergeometric distribution measures the probability of getting a given amount of a certain type of elements from a sample on a population. Think on drawing a hand of 7 cards (sample size = 7) from a 52 cards deck (population size = 52) and wanting to know the probability of getting a given number of aces (successes in sample = X, successes in population = 4).
|
||||
success-probability = Success probability
|
||||
trials-number = Number of trials
|
||||
successes-number = Number of successes
|
||||
binom-description = Binomial distribution measures the probability of getting a given amount of successes in a sequence of experiments. For example, if you flip 5 (number of trials = 5) balanced coins (success probability = 0.5), the distribution describes the probability of having a given number of heads (successes number = X).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
not-found = No pudimos encontrar esta página.
|
||||
title = Calculadora Distribución Hipergeométrica
|
||||
title-home = Calculadoras
|
||||
title-binom = Calculadora Distribución Binomial
|
||||
title-hyper = Calculadora Distribución Hipergeométrica
|
||||
population = Tamaño población
|
||||
successes = Éxitos en la población
|
||||
sample = Tamaño de la muestra
|
||||
sample-successes = Éxitos en la muestra
|
||||
hyper-description = La distribución hipergeométrica mide la probabilidad de obtener una cierta cantidad de elementos de cierto tipo en una muestra de una población. Por ejemplo, si tomamos una mano de 7 cartas (tamaño de la muestra = 7) de un mazo de 52 cartas (tamaño población = 52) y queremos saber la probabilidad de tener cierta cantidad de ases (éxitos en la muestra = X, éxitos en la población = 4).
|
||||
success-probability = Probabilidad de éxito
|
||||
trials-number = Número de intentos
|
||||
successes-number = Número de éxitos
|
||||
binom-description = La distribución binomial mide la probabilidad de obtener un cierto número de éxitos en una secuencia de experimentos. Por ejemplo, si lanzas 5 (número de intentos = 5) monedas justas (probabilidad de éxito = 0.5), la distribución describe la probabilidad de obtener un cierto número de monedas (número de éxitos = X).
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ body {
|
|||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
justify-content: space-between;
|
||||
padding: 0.1em 1em;
|
||||
}
|
||||
|
||||
header > select {
|
||||
header select {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
|
|
@ -92,3 +93,10 @@ div.results > span.right {
|
|||
grid-column: 4 / 5;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.calc-description {
|
||||
max-width: 55em;
|
||||
text-align: left;
|
||||
hyphens: auto;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
|
|
|||
420
src/calc.rs
420
src/calc.rs
|
|
@ -2,56 +2,179 @@
|
|||
|
||||
use std::{collections::HashMap, iter::repeat};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct HyperGeometricInput {
|
||||
population_size: u8,
|
||||
successes: u8,
|
||||
sample_size: u8,
|
||||
sample_successes: u8,
|
||||
}
|
||||
|
||||
impl HyperGeometricInput {
|
||||
pub fn new(
|
||||
population_size: u8,
|
||||
successes: u8,
|
||||
sample_size: u8,
|
||||
sample_successes: u8,
|
||||
) -> Option<Self> {
|
||||
if successes > population_size
|
||||
|| sample_size > population_size
|
||||
|| sample_successes > sample_size
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(Self {
|
||||
population_size,
|
||||
successes,
|
||||
sample_size,
|
||||
sample_successes,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of hypergeometric probability calculation.
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
pub struct HyperGeometricProb {
|
||||
/// Probability of getting exactly X successes in the sample.
|
||||
pub exactly: f64,
|
||||
/// Probability of getting strictly less than X successes in the sample.
|
||||
pub less_than: f64,
|
||||
/// Probability of getting less than or exactly X successes in the sample.
|
||||
pub less_or_equal: f64,
|
||||
/// Probability of getting strictly more X successes in the sample.
|
||||
pub greater_than: f64,
|
||||
/// Probability of getting more than or exactly X successes in the sample.
|
||||
pub greater_or_equal: f64,
|
||||
}
|
||||
|
||||
pub fn hyper_geometric(
|
||||
population_size: u8,
|
||||
successes: u8,
|
||||
sample_size: u8,
|
||||
sample_successes: u8,
|
||||
) -> Option<HyperGeometricProb> {
|
||||
if successes > population_size
|
||||
|| sample_size > population_size
|
||||
|| sample_successes > sample_size
|
||||
pub fn hyper_geometric(input: HyperGeometricInput) -> HyperGeometricProb {
|
||||
let exactly = hyper_geometric_exactly(&input);
|
||||
let (less_than, less_or_equal, greater_or_equal, greater_than) = if input.sample_successes
|
||||
< input.sample_size / 2
|
||||
{
|
||||
None
|
||||
let less_than = (0..input.sample_successes)
|
||||
.map(|i| {
|
||||
hyper_geometric_exactly(&HyperGeometricInput {
|
||||
population_size: input.population_size,
|
||||
successes: input.successes,
|
||||
sample_size: input.sample_size,
|
||||
sample_successes: i,
|
||||
})
|
||||
})
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
let less_or_equal = less_than + exactly;
|
||||
let greater_or_equal = (1.0 - less_than).abs();
|
||||
let greater_than = (1.0 - less_or_equal).abs();
|
||||
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||
} else {
|
||||
let exactly =
|
||||
hyper_geometric_exactly(population_size, successes, sample_size, sample_successes);
|
||||
let (less_than, less_or_equal, greater_or_equal, greater_than) =
|
||||
if sample_successes < sample_size / 2 {
|
||||
let less_than = (0..sample_successes)
|
||||
.map(|i| hyper_geometric_exactly(population_size, successes, sample_size, i))
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
let less_or_equal = less_than + exactly;
|
||||
let greater_or_equal = (1.0 - less_than).abs();
|
||||
let greater_than = (1.0 - less_or_equal).abs();
|
||||
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||
} else {
|
||||
let greater_than = (sample_successes + 1..=sample_size)
|
||||
.map(|i| hyper_geometric_exactly(population_size, successes, sample_size, i))
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
let greater_or_equal = greater_than + exactly;
|
||||
let less_or_equal = (1.0 - greater_than).abs();
|
||||
let less_than = (1.0 - greater_or_equal).abs();
|
||||
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||
};
|
||||
Some(HyperGeometricProb {
|
||||
exactly,
|
||||
less_than,
|
||||
less_or_equal,
|
||||
greater_than,
|
||||
greater_or_equal,
|
||||
})
|
||||
let greater_than = (input.sample_successes + 1..=input.sample_size.min(input.successes))
|
||||
.map(|i| {
|
||||
hyper_geometric_exactly(&HyperGeometricInput {
|
||||
population_size: input.population_size,
|
||||
successes: input.successes,
|
||||
sample_size: input.sample_size,
|
||||
sample_successes: i,
|
||||
})
|
||||
})
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
let greater_or_equal = greater_than + exactly;
|
||||
let less_or_equal = (1.0 - greater_than).abs();
|
||||
let less_than = (1.0 - greater_or_equal).abs();
|
||||
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||
};
|
||||
HyperGeometricProb {
|
||||
exactly,
|
||||
less_than,
|
||||
less_or_equal,
|
||||
greater_than,
|
||||
greater_or_equal,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BinomialInput {
|
||||
success_probability: f64,
|
||||
trials_number: u8,
|
||||
successes_number: u8,
|
||||
}
|
||||
|
||||
impl BinomialInput {
|
||||
pub fn new(success_probability: f64, trials_number: u8, successes_number: u8) -> Option<Self> {
|
||||
if success_probability < 0.0
|
||||
|| success_probability > 1.0
|
||||
|| successes_number > trials_number
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(Self {
|
||||
success_probability,
|
||||
trials_number,
|
||||
successes_number,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
pub struct BinomialProb {
|
||||
pub exactly: f64,
|
||||
pub less_than: f64,
|
||||
pub less_or_equal: f64,
|
||||
pub greater_than: f64,
|
||||
pub greater_or_equal: f64,
|
||||
}
|
||||
|
||||
pub fn binomial(input: BinomialInput) -> BinomialProb {
|
||||
let (p_powers, pc_powers) = powers(input.success_probability, input.trials_number);
|
||||
let exactly = binomial_exactly(&input, &p_powers, &pc_powers);
|
||||
let (less_than, less_or_equal, greater_or_equal, greater_than) =
|
||||
if input.successes_number < input.trials_number / 2 {
|
||||
let less_than = (0..input.successes_number)
|
||||
.map(|i| {
|
||||
binomial_exactly(
|
||||
&BinomialInput {
|
||||
success_probability: input.success_probability,
|
||||
trials_number: input.trials_number,
|
||||
successes_number: i,
|
||||
},
|
||||
&p_powers,
|
||||
&pc_powers,
|
||||
)
|
||||
})
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
let less_or_equal = less_than + exactly;
|
||||
let greater_or_equal = (1.0 - less_than).abs();
|
||||
let greater_than = (1.0 - less_or_equal).abs();
|
||||
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||
} else {
|
||||
let greater_than = (input.successes_number + 1..=input.trials_number)
|
||||
.map(|i| {
|
||||
binomial_exactly(
|
||||
&BinomialInput {
|
||||
success_probability: input.success_probability,
|
||||
trials_number: input.trials_number,
|
||||
successes_number: i,
|
||||
},
|
||||
&p_powers,
|
||||
&pc_powers,
|
||||
)
|
||||
})
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
let greater_or_equal = greater_than + exactly;
|
||||
let less_or_equal = (1.0 - greater_than).abs();
|
||||
let less_than = (1.0 - greater_or_equal).abs();
|
||||
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||
};
|
||||
BinomialProb {
|
||||
exactly,
|
||||
less_than,
|
||||
less_or_equal,
|
||||
greater_than,
|
||||
greater_or_equal,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,69 +183,129 @@ pub fn hyper_geometric(
|
|||
///
|
||||
/// The formula is choose(successes, sample_successes) * choose(population_size - successes,
|
||||
/// sample_size - sample_successes) / choose(population_size, sample_size)
|
||||
fn hyper_geometric_exactly(
|
||||
population_size: u8,
|
||||
successes: u8,
|
||||
sample_size: u8,
|
||||
sample_successes: u8,
|
||||
) -> f64 {
|
||||
if population_size == successes {
|
||||
return if sample_successes == sample_size {
|
||||
fn hyper_geometric_exactly(input: &HyperGeometricInput) -> f64 {
|
||||
if input.population_size == input.successes {
|
||||
return if input.sample_successes == input.sample_size {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
if successes == 0 {
|
||||
return if sample_successes == 0 { 1.0 } else { 0.0 };
|
||||
if input.successes == 0 {
|
||||
return if input.sample_successes == 0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
// On top we have: successes!, (population_size - successes)!, sample_size! and
|
||||
// (population_size - sample_size)!
|
||||
let top_factors = (1..=successes)
|
||||
.chain(1..=(population_size - successes))
|
||||
.chain(1..=sample_size)
|
||||
.chain(1..=(population_size - sample_size))
|
||||
let top_factors = (1..=input.successes)
|
||||
.chain(1..=(input.population_size - input.successes))
|
||||
.chain(1..=input.sample_size)
|
||||
.chain(1..=(input.population_size - input.sample_size))
|
||||
.flat_map(|n| factorize(n))
|
||||
.fold(HashMap::<u8, u8>::new(), |mut counts, i| {
|
||||
*counts.entry(i).or_default() += 1;
|
||||
counts
|
||||
});
|
||||
.fold(HashMap::<u8, u8>::new(), group_factors);
|
||||
|
||||
// On bottom we have: sample_successes!, (successes - sample_successes)!
|
||||
// (sample_size - sample_successes)!, (population_size - successes - sample_size + sample_successes)!
|
||||
// and population_size!
|
||||
let bot_factors = (1..=sample_successes)
|
||||
.chain(1..=(successes - sample_successes))
|
||||
.chain(1..=(sample_size - sample_successes))
|
||||
let bot_factors = (1..=input.sample_successes)
|
||||
.chain(1..=(input.successes - input.sample_successes))
|
||||
.chain(1..=(input.sample_size - input.sample_successes))
|
||||
.chain(
|
||||
1..=((population_size as u16 + sample_successes as u16
|
||||
- successes as u16
|
||||
- sample_size as u16) as u8),
|
||||
1..=((input.population_size as u16 + input.sample_successes as u16
|
||||
- input.successes as u16
|
||||
- input.sample_size as u16) as u8),
|
||||
)
|
||||
.chain(1..=population_size)
|
||||
.chain(1..=input.population_size)
|
||||
.flat_map(|n| factorize(n))
|
||||
.fold(HashMap::<u8, u8>::new(), |mut counts, i| {
|
||||
counts.entry(i).and_modify(|count| *count += 1).or_insert(1);
|
||||
counts
|
||||
});
|
||||
.fold(HashMap::<u8, u8>::new(), group_factors);
|
||||
|
||||
let (top_factors, bot_factors) = simplify(top_factors, bot_factors);
|
||||
|
||||
let top_product: f64 = top_factors
|
||||
.into_iter()
|
||||
.flat_map(|(f, count)| repeat(f).take(count as usize))
|
||||
.map(|f| f as f64)
|
||||
.product();
|
||||
let top_product = product(top_factors);
|
||||
let bot_product = product(bot_factors);
|
||||
|
||||
let bot_product: f64 = bot_factors
|
||||
top_product / bot_product
|
||||
}
|
||||
|
||||
/// Computes the probability of getting exactly `successes_number` within `trials_number` given
|
||||
/// that the success probability is `success_probability`.
|
||||
///
|
||||
/// The formula is choose(trials_number, successes_number) * (success_probability)^successes_number
|
||||
/// * (1 - success_probability)^(trials_number - successes_number)
|
||||
fn binomial_exactly(input: &BinomialInput, p_powers: &[f64], pc_powers: &[f64]) -> f64 {
|
||||
if input.success_probability == 0.0 {
|
||||
return if input.successes_number == 0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
if input.success_probability == 1.0 {
|
||||
return if input.successes_number == input.trials_number {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
choose(input.trials_number, input.successes_number)
|
||||
* p_powers[input.successes_number as usize]
|
||||
* pc_powers[(input.trials_number - input.successes_number) as usize]
|
||||
}
|
||||
|
||||
fn choose(n: u8, k: u8) -> f64 {
|
||||
// On top we have: n!
|
||||
let top_factors = (1..=n)
|
||||
.flat_map(|n| factorize(n))
|
||||
.fold(HashMap::<u8, u8>::new(), group_factors);
|
||||
|
||||
// On bottom we have: k!, (n - k)!
|
||||
let bot_factors = (1..=k)
|
||||
.chain(1..=(n - k))
|
||||
.flat_map(|n| factorize(n))
|
||||
.fold(HashMap::<u8, u8>::new(), group_factors);
|
||||
|
||||
let (top_factors, bot_factors) = simplify(top_factors, bot_factors);
|
||||
|
||||
let top_product = product(top_factors);
|
||||
let bot_product = product(bot_factors);
|
||||
|
||||
top_product / bot_product
|
||||
}
|
||||
|
||||
fn powers(p: f64, n: u8) -> (Vec<f64>, Vec<f64>) {
|
||||
let mut p_powers = Vec::with_capacity((n + 1) as usize);
|
||||
let mut pc_powers = Vec::with_capacity((n + 1) as usize);
|
||||
|
||||
let mut p_power = 1.0;
|
||||
let mut pc_power = 1.0;
|
||||
for _ in 0..n + 1 {
|
||||
p_powers.push(p_power);
|
||||
pc_powers.push(pc_power);
|
||||
p_power = p_power * p;
|
||||
pc_power = pc_power * (1.0 - p)
|
||||
}
|
||||
(p_powers, pc_powers)
|
||||
}
|
||||
|
||||
fn group_factors(mut counts: HashMap<u8, u8>, i: u8) -> HashMap<u8, u8> {
|
||||
*counts.entry(i).or_default() += 1;
|
||||
counts
|
||||
}
|
||||
|
||||
fn product(factors: HashMap<u8, u8>) -> f64 {
|
||||
factors
|
||||
.into_iter()
|
||||
.flat_map(|(f, count)| repeat(f).take(count as usize))
|
||||
.map(|f| f as f64)
|
||||
.product();
|
||||
|
||||
top_product / bot_product
|
||||
.product()
|
||||
}
|
||||
|
||||
/// Simplify factors for a fraction.
|
||||
///
|
||||
/// This assumes factors are already prime factors.
|
||||
fn simplify(
|
||||
mut top_factors: HashMap<u8, u8>,
|
||||
mut bot_factors: HashMap<u8, u8>,
|
||||
|
|
@ -158,6 +341,8 @@ fn simplify(
|
|||
const PRIMES: &[u8] = &[2, 3, 5, 7, 11, 13];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Iterator for the prime factors of a number. We obtain this iterator using the `factorize`
|
||||
/// function.
|
||||
struct FactorIter<'a> {
|
||||
/// remainder
|
||||
n: u8,
|
||||
|
|
@ -189,7 +374,7 @@ impl Iterator for FactorIter<'_> {
|
|||
}
|
||||
// If we stopped at a factor, we return it.
|
||||
// If we exhausted the factors, we return the remainder, as it will be a prime (we
|
||||
// checked primes less than sqrt(MAX).
|
||||
// checked primes less than sqrt(u8::MAX).
|
||||
match self.f {
|
||||
Some(f) => Some(f),
|
||||
None => Some(self.n),
|
||||
|
|
@ -207,7 +392,10 @@ fn factorize(n: u8) -> FactorIter<'static> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::calc::hyper_geometric_exactly;
|
||||
use crate::calc::{
|
||||
BinomialInput, BinomialProb, HyperGeometricInput, HyperGeometricProb, binomial,
|
||||
binomial_exactly, hyper_geometric, hyper_geometric_exactly, powers,
|
||||
};
|
||||
|
||||
use super::factorize;
|
||||
|
||||
|
|
@ -228,18 +416,80 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_hypergeometric_exact_all_successes() {
|
||||
assert_eq!(hyper_geometric_exactly(10, 10, 5, 5), 1.0);
|
||||
assert_eq!(hyper_geometric_exactly(10, 10, 5, 4), 0.0);
|
||||
let input = &HyperGeometricInput::new(10, 10, 5, 5).unwrap();
|
||||
assert_eq!(hyper_geometric_exactly(input), 1.0);
|
||||
let input = &HyperGeometricInput::new(10, 10, 5, 4).unwrap();
|
||||
assert_eq!(hyper_geometric_exactly(input), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergeometric_exact_no_successes() {
|
||||
assert_eq!(hyper_geometric_exactly(10, 0, 5, 0), 1.0);
|
||||
assert_eq!(hyper_geometric_exactly(10, 0, 5, 1), 0.0);
|
||||
let input = &HyperGeometricInput::new(10, 0, 5, 0).unwrap();
|
||||
assert_eq!(hyper_geometric_exactly(input), 1.0);
|
||||
let input = &HyperGeometricInput::new(10, 0, 5, 1).unwrap();
|
||||
assert_eq!(hyper_geometric_exactly(input), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergeometric_exact() {
|
||||
assert_eq!(hyper_geometric_exactly(10, 3, 5, 2), 5.0 / 12.0);
|
||||
let input = &HyperGeometricInput::new(10, 3, 5, 2).unwrap();
|
||||
assert_eq!(hyper_geometric_exactly(input), 5.0 / 12.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hypergeometric_aces_poker() {
|
||||
let input = HyperGeometricInput::new(52, 4, 5, 4).unwrap();
|
||||
let exact = 1.846892603195124e-5;
|
||||
assert_eq!(
|
||||
hyper_geometric(input),
|
||||
HyperGeometricProb {
|
||||
exactly: exact,
|
||||
less_than: 1.0 - exact,
|
||||
less_or_equal: 1.0,
|
||||
greater_than: 0.0,
|
||||
greater_or_equal: exact
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binom_exact_all_success() {
|
||||
let (p_powers, pc_powers) = powers(1.0, 5);
|
||||
let input = &BinomialInput::new(1.0, 5, 5).unwrap();
|
||||
assert_eq!(binomial_exactly(input, &p_powers, &pc_powers), 1.0);
|
||||
let input = &BinomialInput::new(1.0, 5, 4).unwrap();
|
||||
assert_eq!(binomial_exactly(input, &p_powers, &pc_powers), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binom_exact_no_success() {
|
||||
let (p_powers, pc_powers) = powers(0.0, 5);
|
||||
let input = &BinomialInput::new(0.0, 5, 0).unwrap();
|
||||
assert_eq!(binomial_exactly(input, &p_powers, &pc_powers), 1.0);
|
||||
let input = &BinomialInput::new(0.0, 5, 1).unwrap();
|
||||
assert_eq!(binomial_exactly(input, &p_powers, &pc_powers), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binomial_exact() {
|
||||
let (p_powers, pc_powers) = powers(0.5, 5);
|
||||
let input = &BinomialInput::new(0.5, 5, 3).unwrap();
|
||||
assert_eq!(binomial_exactly(input, &p_powers, &pc_powers), 10.0 / 32.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binomial() {
|
||||
// 10.0 / 32.0
|
||||
let input = BinomialInput::new(0.5, 5, 3).unwrap();
|
||||
assert_eq!(
|
||||
binomial(input),
|
||||
BinomialProb {
|
||||
exactly: 10.0 / 32.0,
|
||||
less_than: 16.0 / 32.0,
|
||||
less_or_equal: 26.0 / 32.0,
|
||||
greater_than: 6.0 / 32.0,
|
||||
greater_or_equal: 16.0 / 32.0,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,22 +6,22 @@ use leptos::prelude::{
|
|||
use leptos::{IntoView, component, view};
|
||||
use leptos_fluent::move_tr;
|
||||
|
||||
use crate::calc::hyper_geometric;
|
||||
use crate::calc::{BinomialInput, HyperGeometricInput, binomial, hyper_geometric};
|
||||
|
||||
/// A parameterized incrementing button
|
||||
#[component]
|
||||
pub fn Calculator() -> impl IntoView {
|
||||
pub fn HyperCalculator() -> impl IntoView {
|
||||
let (population, set_population) = signal(0u8);
|
||||
let (successes, set_successes) = signal(0u8);
|
||||
let (sample, set_sample) = signal(0u8);
|
||||
let (sample_successes, set_sample_successes) = signal(0u8);
|
||||
let result = move || {
|
||||
hyper_geometric(
|
||||
HyperGeometricInput::new(
|
||||
population.get(),
|
||||
successes.get(),
|
||||
sample.get(),
|
||||
sample_successes.get(),
|
||||
)
|
||||
.map(hyper_geometric)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
view! {
|
||||
|
|
@ -110,6 +110,99 @@ pub fn Calculator() -> impl IntoView {
|
|||
</span>
|
||||
<span class="right">{move || display_rounded(result().greater_or_equal)}</span>
|
||||
</div>
|
||||
<div class="calc-description">{move_tr!("hyper-description")}</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BinomCalculator() -> impl IntoView {
|
||||
let (success_probability, set_success_probability) = signal(0f64);
|
||||
let (trials_number, set_trials_number) = signal(0u8);
|
||||
let (successes_number, set_successes_number) = signal(0u8);
|
||||
let result = move || {
|
||||
BinomialInput::new(
|
||||
success_probability.get(),
|
||||
trials_number.get(),
|
||||
successes_number.get(),
|
||||
)
|
||||
.map(binomial)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
view! {
|
||||
<form>
|
||||
<p>
|
||||
<label for="success_probability">{move_tr!("success-probability")}</label>
|
||||
<input
|
||||
id="success_probability"
|
||||
type="number"
|
||||
min=0
|
||||
max=1
|
||||
prop:step=0.1
|
||||
prop:value=success_probability
|
||||
on:input:target=move |ev| {
|
||||
set_success_probability.set(ev.target().value().parse().unwrap_or_default())
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label for="trials_number">{move_tr!("trials-number")}</label>
|
||||
<input
|
||||
id="trials_number"
|
||||
type="number"
|
||||
min=0
|
||||
prop:value=trials_number
|
||||
on:input:target=move |ev| {
|
||||
set_trials_number.set(ev.target().value().parse().unwrap_or_default())
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label for="successes_number">{move_tr!("successes-number")}</label>
|
||||
<input
|
||||
id="successes_number"
|
||||
type="number"
|
||||
min=0
|
||||
prop:max=trials_number
|
||||
prop:value=successes_number
|
||||
on:input:target=move |ev| {
|
||||
set_successes_number.set(ev.target().value().parse().unwrap_or_default())
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</form>
|
||||
<div class="results">
|
||||
<span class="left">
|
||||
<span>"P(X = "</span>
|
||||
<span>{successes_number}</span>
|
||||
<span>"): "</span>
|
||||
</span>
|
||||
<span class="right">{move || display_rounded(result().exactly)}</span>
|
||||
<span class="left">
|
||||
<span>"P(X < "</span>
|
||||
<span>{successes_number}</span>
|
||||
<span>"): "</span>
|
||||
</span>
|
||||
<span class="right">{move || display_rounded(result().less_than)}</span>
|
||||
<span class="left">
|
||||
<span>"P(X ≤ "</span>
|
||||
<span>{successes_number}</span>
|
||||
<span>"): "</span>
|
||||
</span>
|
||||
<span class="right">{move || display_rounded(result().less_or_equal)}</span>
|
||||
<span class="left">
|
||||
<span>"P(X > "</span>
|
||||
<span>{successes_number}</span>
|
||||
<span>"): "</span>
|
||||
</span>
|
||||
<span class="right">{move || display_rounded(result().greater_than)}</span>
|
||||
<span class="left">
|
||||
<span>"P(X ≥ "</span>
|
||||
<span>{successes_number}</span>
|
||||
<span>"): "</span>
|
||||
</span>
|
||||
<span class="right">{move || display_rounded(result().greater_or_equal)}</span>
|
||||
</div>
|
||||
<div class="calc-description">{move_tr!("binom-description")}</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
18
src/components/common.rs
Normal file
18
src/components/common.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use leptos::html::ElementChild;
|
||||
use leptos::{IntoView, component, view};
|
||||
use leptos_router::nested_router::Outlet;
|
||||
|
||||
use crate::components::localization::{I18n, LanguageSelector};
|
||||
|
||||
#[component]
|
||||
pub fn Common() -> impl IntoView {
|
||||
view! {
|
||||
<I18n>
|
||||
<header>
|
||||
<a href="/">"🏡"</a>
|
||||
<LanguageSelector />
|
||||
</header>
|
||||
<Outlet />
|
||||
</I18n>
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
use fluent_templates::static_loader;
|
||||
use leptos::html::ElementChild;
|
||||
use leptos::prelude::{
|
||||
AddAnyAttr, Children, Get, GlobalAttributes, IntoAttribute, OnAttribute, PropAttribute, Set,
|
||||
AddAnyAttr, Children, Get, GlobalAttributes, OnAttribute, PropAttribute, Set, expect_context,
|
||||
};
|
||||
use leptos::{IntoView, component, view};
|
||||
use leptos_fluent::{Language, expect_i18n, leptos_fluent};
|
||||
use leptos_fluent::{Language, leptos_fluent};
|
||||
|
||||
static_loader! {
|
||||
pub static TRANSLATIONS = {
|
||||
|
|
@ -23,36 +23,39 @@ pub fn I18n(children: Children) -> impl IntoView {
|
|||
translations: [TRANSLATIONS],
|
||||
locales: "./locales",
|
||||
check_translations: "./src/**/*.rs",
|
||||
initial_language_from_localstorage: true,
|
||||
initial_language_from_navigator_to_localstorage: true,
|
||||
set_language_to_localstorage: true,
|
||||
initial_language_from_local_storage: true,
|
||||
initial_language_from_navigator_to_local_storage: true,
|
||||
set_language_to_local_storage: true,
|
||||
sync_html_tag_lang: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Selector for languages
|
||||
#[component]
|
||||
pub fn LanguageSelector() -> impl IntoView {
|
||||
// Use `expect_i18n()` to get the current i18n context:
|
||||
let i18n = expect_i18n();
|
||||
let i18n = expect_context::<leptos_fluent::I18n>();
|
||||
|
||||
view! {
|
||||
<label for="language">"A/文:"</label>
|
||||
<select id="language">
|
||||
{move || {
|
||||
i18n.languages.iter().map(|lang| render_language(lang)).collect::<Vec<_>>()
|
||||
}}
|
||||
</select>
|
||||
<span>
|
||||
<label for="language">"A/文:"</label>
|
||||
<select id="language">
|
||||
{move || {
|
||||
i18n.languages.iter().map(|lang| render_language(lang)).collect::<Vec<_>>()
|
||||
}}
|
||||
</select>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_language(lang: &'static Language) -> impl IntoView {
|
||||
// Passed as atrribute, `Language` is converted to their code,
|
||||
// so `<input id=lang` becomes `<input id=lang.id.to_string()`
|
||||
let i18n = expect_i18n();
|
||||
let i18n = expect_context::<leptos_fluent::I18n>();
|
||||
view! {
|
||||
<option
|
||||
id=lang
|
||||
value=lang
|
||||
prop:selected=lang.is_active()
|
||||
prop:selected=i18n.language.get() == lang
|
||||
on:click=move |_| i18n.language.set(lang)
|
||||
>
|
||||
{lang.name}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod calculator;
|
||||
pub mod common;
|
||||
pub mod localization;
|
||||
|
|
|
|||
47
src/lib.rs
47
src/lib.rs
|
|
@ -1,15 +1,20 @@
|
|||
use leptos::prelude::{AddAnyAttr, IntoAttribute};
|
||||
use leptos::prelude::{AddAnyAttr, Get, expect_context};
|
||||
use leptos::{IntoView, component, view};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::{components::*, path};
|
||||
use leptos_meta::{Html, Meta, Title, provide_meta_context};
|
||||
use leptos_router::components::{ParentRoute, Route, Router, Routes};
|
||||
use leptos_router::path;
|
||||
|
||||
// Modules
|
||||
mod calc;
|
||||
mod components;
|
||||
mod pages;
|
||||
|
||||
// Top-Level pages
|
||||
use crate::components::common::Common;
|
||||
use crate::components::localization::I18n;
|
||||
use crate::pages::binom::Binom;
|
||||
use crate::pages::home::Home;
|
||||
use crate::pages::hyper::Hyper;
|
||||
use crate::pages::not_found::NotFound;
|
||||
|
||||
/// An app router which renders the homepage and handles 404's
|
||||
#[component]
|
||||
|
|
@ -18,19 +23,31 @@ pub fn App() -> impl IntoView {
|
|||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Html attr:lang="en" attr:dir="ltr" attr:data-theme="light" />
|
||||
<I18n>
|
||||
<HtmlAttrs />
|
||||
|
||||
// sets the document title
|
||||
<Title text="Hypergeometric Calculator" />
|
||||
// sets the document title
|
||||
<Title text="🧮" />
|
||||
|
||||
// injects metadata in the <head> of the page
|
||||
<Meta charset="UTF-8" />
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
// injects metadata in the <head> of the page
|
||||
<Meta charset="UTF-8" />
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<Router>
|
||||
<Routes fallback=|| view! { NotFound }>
|
||||
<Route path=path!("/") view=Home />
|
||||
</Routes>
|
||||
</Router>
|
||||
<Router>
|
||||
<Routes fallback=NotFound>
|
||||
<ParentRoute path=path!("/") view=Common>
|
||||
<Route path=path!("") view=Home />
|
||||
<Route path=path!("/hyper") view=Hyper />
|
||||
<Route path=path!("/binom") view=Binom />
|
||||
</ParentRoute>
|
||||
</Routes>
|
||||
</Router>
|
||||
</I18n>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn HtmlAttrs() -> impl IntoView {
|
||||
let i18n = expect_context::<leptos_fluent::I18n>();
|
||||
view! { <Html attr:lang=move || i18n.language.get() attr:dir="ltr" attr:data-theme="light" /> }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use hypergeometric_calc::App;
|
||||
use distribution_calc::App;
|
||||
use leptos::{mount::mount_to_body, view};
|
||||
|
||||
fn main() {
|
||||
|
|
|
|||
17
src/pages/binom.rs
Normal file
17
src/pages/binom.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use leptos::attr::global::ClassAttribute;
|
||||
use leptos::html::ElementChild;
|
||||
use leptos::{IntoView, component, view};
|
||||
use leptos_fluent::move_tr;
|
||||
|
||||
use crate::components::calculator::BinomCalculator;
|
||||
|
||||
///Binom Page
|
||||
#[component]
|
||||
pub fn Binom() -> impl IntoView {
|
||||
view! {
|
||||
<div class="container">
|
||||
<h1 class="title">{move_tr!("title-binom")}</h1>
|
||||
<BinomCalculator />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +1,22 @@
|
|||
use leptos::attr::global::ClassAttribute;
|
||||
use leptos::error::ErrorBoundary;
|
||||
use leptos::html::ElementChild;
|
||||
use leptos::prelude::{CollectView, Get};
|
||||
use leptos::{IntoView, component, view};
|
||||
use leptos_fluent::move_tr;
|
||||
|
||||
use crate::components::calculator::Calculator;
|
||||
use crate::components::localization::{I18n, LanguageSelector};
|
||||
|
||||
/// Default Home Page
|
||||
///Home Page
|
||||
#[component]
|
||||
pub fn Home() -> impl IntoView {
|
||||
view! {
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
view! {
|
||||
<h1>"Uh oh! Something went wrong!"</h1>
|
||||
|
||||
<p>"Errors: "</p>
|
||||
// Render a list of errors as strings - good for development purposes
|
||||
<ul>
|
||||
{move || {
|
||||
errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
|
||||
.collect_view()
|
||||
}}
|
||||
|
||||
</ul>
|
||||
}
|
||||
}>
|
||||
<I18n>
|
||||
<header>
|
||||
<LanguageSelector />
|
||||
</header>
|
||||
<div class="container">
|
||||
<Title />
|
||||
<Calculator />
|
||||
</div>
|
||||
</I18n>
|
||||
</ErrorBoundary>
|
||||
<div class="container">
|
||||
<h1 class="title">{move_tr!("title-home")}</h1>
|
||||
<menu>
|
||||
<li>
|
||||
<a href="/binom">{move_tr!("title-binom")}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/hyper">{move_tr!("title-hyper")}</a>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// Title
|
||||
#[component]
|
||||
pub fn Title() -> impl IntoView {
|
||||
view! {<h1 class="title">{move_tr!("title")}</h1>}
|
||||
}
|
||||
|
|
|
|||
17
src/pages/hyper.rs
Normal file
17
src/pages/hyper.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use leptos::attr::global::ClassAttribute;
|
||||
use leptos::html::ElementChild;
|
||||
use leptos::{IntoView, component, view};
|
||||
use leptos_fluent::move_tr;
|
||||
|
||||
use crate::components::calculator::HyperCalculator;
|
||||
|
||||
///Hyper Page
|
||||
#[component]
|
||||
pub fn Hyper() -> impl IntoView {
|
||||
view! {
|
||||
<div class="container">
|
||||
<h1 class="title">{move_tr!("title-hyper")}</h1>
|
||||
<HyperCalculator />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
pub mod binom;
|
||||
pub mod home;
|
||||
pub mod hyper;
|
||||
pub mod not_found;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue