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:
Felipe 2025-12-08 15:54:09 -03:00 committed by Ludwig
parent 05fe59302d
commit 5947ac60d5
Signed by: Ludwig
GPG key ID: 441A26F83D31FAFF
18 changed files with 1092 additions and 614 deletions

938
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "hypergeometric-calc" name = "distribution-calc"
version = "0.1.0" version = "0.1.1"
edition = "2024" edition = "2024"
authors = ["Felipe Contreras Salinas <felipe@bstr.cl>"] authors = ["Felipe Contreras Salinas <felipe@bstr.cl>"]
@ -9,18 +9,18 @@ authors = ["Felipe Contreras Salinas <felipe@bstr.cl>"]
[dependencies] [dependencies]
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
console_log = "1.0.0" console_log = "1.0.0"
fluent-templates = "0.13.0" fluent-templates = "0.13.2"
leptos = { version = "0.8.2", features = ["csr", "tracing"] } leptos = { version = "0.8.14", features = ["csr", "tracing"] }
leptos-fluent = "0.2.15" leptos-fluent = "0.2.20"
leptos_meta = { version = "0.8.2" } leptos_meta = { version = "0.8.5" }
leptos_router = { version = "0.8.2" } leptos_router = { version = "0.8.10" }
log = "0.4.26" log = "0.4.29"
[dev-dependencies] [dev-dependencies]
wasm-bindgen = "0.2.100" wasm-bindgen = "0.2.106"
wasm-bindgen-test = "0.3.50" wasm-bindgen-test = "0.3.56"
web-sys = { version = "0.3.77", features = ["Document", "Window"] } web-sys = { version = "0.3.83", features = ["Document", "Window"] }
[profile.release] [profile.release]

View file

@ -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. 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 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. 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.

View file

@ -1,5 +1,5 @@
# hypergeometric-calc # distribution-calc
Web calculator for Hypergeometric Distribution built using [Leptos]. Web calculator for Probability Distributions built using [Leptos].
## Dependencies ## Dependencies
You will need to install the Rust toolchain. We recommend to do using [Rustup]. Once You will need to install the Rust toolchain. We recommend to do using [Rustup]. Once

View file

@ -1,6 +1,13 @@
not-found = We couldn't find that page. 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 population = Population Size
successes = Successes in Population successes = Successes in Population
sample = Sample Size sample = Sample Size
sample-successes = Successes in Sample 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).

View file

@ -1,6 +1,13 @@
not-found = No pudimos encontrar esta página. 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 population = Tamaño población
successes = Éxitos en la población successes = Éxitos en la población
sample = Tamaño de la muestra sample = Tamaño de la muestra
sample-successes = Éxitos en 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).

View file

@ -24,10 +24,11 @@ body {
header { header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: end; justify-content: space-between;
padding: 0.1em 1em;
} }
header > select { header select {
margin: 0 1em; margin: 0 1em;
} }
@ -92,3 +93,10 @@ div.results > span.right {
grid-column: 4 / 5; grid-column: 4 / 5;
text-align: left; text-align: left;
} }
div.calc-description {
max-width: 55em;
text-align: left;
hyphens: auto;
margin-top: 2em;
}

View file

@ -2,33 +2,147 @@
use std::{collections::HashMap, iter::repeat}; use std::{collections::HashMap, iter::repeat};
#[derive(Default)] #[derive(Debug)]
pub struct HyperGeometricProb { pub struct HyperGeometricInput {
pub exactly: f64, population_size: u8,
pub less_than: f64, successes: u8,
pub less_or_equal: f64, sample_size: u8,
pub greater_than: f64, sample_successes: u8,
pub greater_or_equal: f64,
} }
pub fn hyper_geometric( impl HyperGeometricInput {
pub fn new(
population_size: u8, population_size: u8,
successes: u8, successes: u8,
sample_size: u8, sample_size: u8,
sample_successes: u8, sample_successes: u8,
) -> Option<HyperGeometricProb> { ) -> Option<Self> {
if successes > population_size if successes > population_size
|| sample_size > population_size || sample_size > population_size
|| sample_successes > sample_size || sample_successes > sample_size
{ {
None None
} else { } else {
let exactly = Some(Self {
hyper_geometric_exactly(population_size, successes, sample_size, sample_successes); 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(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
{
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 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) = let (less_than, less_or_equal, greater_or_equal, greater_than) =
if sample_successes < sample_size / 2 { if input.successes_number < input.trials_number / 2 {
let less_than = (0..sample_successes) let less_than = (0..input.successes_number)
.map(|i| hyper_geometric_exactly(population_size, successes, sample_size, i)) .map(|i| {
binomial_exactly(
&BinomialInput {
success_probability: input.success_probability,
trials_number: input.trials_number,
successes_number: i,
},
&p_powers,
&pc_powers,
)
})
.sum::<f64>() .sum::<f64>()
.abs(); .abs();
let less_or_equal = less_than + exactly; let less_or_equal = less_than + exactly;
@ -36,8 +150,18 @@ pub fn hyper_geometric(
let greater_than = (1.0 - less_or_equal).abs(); let greater_than = (1.0 - less_or_equal).abs();
(less_than, less_or_equal, greater_or_equal, greater_than) (less_than, less_or_equal, greater_or_equal, greater_than)
} else { } else {
let greater_than = (sample_successes + 1..=sample_size) let greater_than = (input.successes_number + 1..=input.trials_number)
.map(|i| hyper_geometric_exactly(population_size, successes, sample_size, i)) .map(|i| {
binomial_exactly(
&BinomialInput {
success_probability: input.success_probability,
trials_number: input.trials_number,
successes_number: i,
},
&p_powers,
&pc_powers,
)
})
.sum::<f64>() .sum::<f64>()
.abs(); .abs();
let greater_or_equal = greater_than + exactly; let greater_or_equal = greater_than + exactly;
@ -45,13 +169,12 @@ pub fn hyper_geometric(
let less_than = (1.0 - greater_or_equal).abs(); let less_than = (1.0 - greater_or_equal).abs();
(less_than, less_or_equal, greater_or_equal, greater_than) (less_than, less_or_equal, greater_or_equal, greater_than)
}; };
Some(HyperGeometricProb { BinomialProb {
exactly, exactly,
less_than, less_than,
less_or_equal, less_or_equal,
greater_than, greater_than,
greater_or_equal, greater_or_equal,
})
} }
} }
@ -60,69 +183,129 @@ pub fn hyper_geometric(
/// ///
/// The formula is choose(successes, sample_successes) * choose(population_size - successes, /// The formula is choose(successes, sample_successes) * choose(population_size - successes,
/// sample_size - sample_successes) / choose(population_size, sample_size) /// sample_size - sample_successes) / choose(population_size, sample_size)
fn hyper_geometric_exactly( fn hyper_geometric_exactly(input: &HyperGeometricInput) -> f64 {
population_size: u8, if input.population_size == input.successes {
successes: u8, return if input.sample_successes == input.sample_size {
sample_size: u8,
sample_successes: u8,
) -> f64 {
if population_size == successes {
return if sample_successes == sample_size {
1.0 1.0
} else { } else {
0.0 0.0
}; };
} }
if successes == 0 { if input.successes == 0 {
return if sample_successes == 0 { 1.0 } else { 0.0 }; return if input.sample_successes == 0 {
1.0
} else {
0.0
};
} }
// On top we have: successes!, (population_size - successes)!, sample_size! and // On top we have: successes!, (population_size - successes)!, sample_size! and
// (population_size - sample_size)! // (population_size - sample_size)!
let top_factors = (1..=successes) let top_factors = (1..=input.successes)
.chain(1..=(population_size - successes)) .chain(1..=(input.population_size - input.successes))
.chain(1..=sample_size) .chain(1..=input.sample_size)
.chain(1..=(population_size - sample_size)) .chain(1..=(input.population_size - input.sample_size))
.flat_map(|n| factorize(n)) .flat_map(|n| factorize(n))
.fold(HashMap::<u8, u8>::new(), |mut counts, i| { .fold(HashMap::<u8, u8>::new(), group_factors);
*counts.entry(i).or_default() += 1;
counts
});
// On bottom we have: sample_successes!, (successes - sample_successes)! // On bottom we have: sample_successes!, (successes - sample_successes)!
// (sample_size - sample_successes)!, (population_size - successes - sample_size + sample_successes)! // (sample_size - sample_successes)!, (population_size - successes - sample_size + sample_successes)!
// and population_size! // and population_size!
let bot_factors = (1..=sample_successes) let bot_factors = (1..=input.sample_successes)
.chain(1..=(successes - sample_successes)) .chain(1..=(input.successes - input.sample_successes))
.chain(1..=(sample_size - sample_successes)) .chain(1..=(input.sample_size - input.sample_successes))
.chain( .chain(
1..=((population_size as u16 + sample_successes as u16 1..=((input.population_size as u16 + input.sample_successes as u16
- successes as u16 - input.successes as u16
- sample_size as u16) as u8), - input.sample_size as u16) as u8),
) )
.chain(1..=population_size) .chain(1..=input.population_size)
.flat_map(|n| factorize(n)) .flat_map(|n| factorize(n))
.fold(HashMap::<u8, u8>::new(), |mut counts, i| { .fold(HashMap::<u8, u8>::new(), group_factors);
counts.entry(i).and_modify(|count| *count += 1).or_insert(1);
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
}
/// 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 counts
}); }
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 bot_product: f64 = bot_factors fn product(factors: HashMap<u8, u8>) -> f64 {
factors
.into_iter() .into_iter()
.flat_map(|(f, count)| repeat(f).take(count as usize)) .flat_map(|(f, count)| repeat(f).take(count as usize))
.map(|f| f as f64) .map(|f| f as f64)
.product(); .product()
top_product / bot_product
} }
/// Simplify factors for a fraction.
///
/// This assumes factors are already prime factors.
fn simplify( fn simplify(
mut top_factors: HashMap<u8, u8>, mut top_factors: HashMap<u8, u8>,
mut bot_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]; const PRIMES: &[u8] = &[2, 3, 5, 7, 11, 13];
#[derive(Debug)] #[derive(Debug)]
/// Iterator for the prime factors of a number. We obtain this iterator using the `factorize`
/// function.
struct FactorIter<'a> { struct FactorIter<'a> {
/// remainder /// remainder
n: u8, n: u8,
@ -189,7 +374,7 @@ impl Iterator for FactorIter<'_> {
} }
// If we stopped at a factor, we return it. // 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 // 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 { match self.f {
Some(f) => Some(f), Some(f) => Some(f),
None => Some(self.n), None => Some(self.n),
@ -207,7 +392,10 @@ fn factorize(n: u8) -> FactorIter<'static> {
#[cfg(test)] #[cfg(test)]
mod 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; use super::factorize;
@ -228,18 +416,80 @@ mod test {
#[test] #[test]
fn test_hypergeometric_exact_all_successes() { fn test_hypergeometric_exact_all_successes() {
assert_eq!(hyper_geometric_exactly(10, 10, 5, 5), 1.0); let input = &HyperGeometricInput::new(10, 10, 5, 5).unwrap();
assert_eq!(hyper_geometric_exactly(10, 10, 5, 4), 0.0); 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] #[test]
fn test_hypergeometric_exact_no_successes() { fn test_hypergeometric_exact_no_successes() {
assert_eq!(hyper_geometric_exactly(10, 0, 5, 0), 1.0); let input = &HyperGeometricInput::new(10, 0, 5, 0).unwrap();
assert_eq!(hyper_geometric_exactly(10, 0, 5, 1), 0.0); 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] #[test]
fn test_hypergeometric_exact() { 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,
}
);
} }
} }

View file

@ -6,22 +6,22 @@ use leptos::prelude::{
use leptos::{IntoView, component, view}; use leptos::{IntoView, component, view};
use leptos_fluent::move_tr; use leptos_fluent::move_tr;
use crate::calc::hyper_geometric; use crate::calc::{BinomialInput, HyperGeometricInput, binomial, hyper_geometric};
/// A parameterized incrementing button
#[component] #[component]
pub fn Calculator() -> impl IntoView { pub fn HyperCalculator() -> impl IntoView {
let (population, set_population) = signal(0u8); let (population, set_population) = signal(0u8);
let (successes, set_successes) = signal(0u8); let (successes, set_successes) = signal(0u8);
let (sample, set_sample) = signal(0u8); let (sample, set_sample) = signal(0u8);
let (sample_successes, set_sample_successes) = signal(0u8); let (sample_successes, set_sample_successes) = signal(0u8);
let result = move || { let result = move || {
hyper_geometric( HyperGeometricInput::new(
population.get(), population.get(),
successes.get(), successes.get(),
sample.get(), sample.get(),
sample_successes.get(), sample_successes.get(),
) )
.map(hyper_geometric)
.unwrap_or_default() .unwrap_or_default()
}; };
view! { view! {
@ -110,6 +110,99 @@ pub fn Calculator() -> impl IntoView {
</span> </span>
<span class="right">{move || display_rounded(result().greater_or_equal)}</span> <span class="right">{move || display_rounded(result().greater_or_equal)}</span>
</div> </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
View 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>
}
}

View file

@ -3,10 +3,10 @@
use fluent_templates::static_loader; use fluent_templates::static_loader;
use leptos::html::ElementChild; use leptos::html::ElementChild;
use leptos::prelude::{ 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::{IntoView, component, view};
use leptos_fluent::{Language, expect_i18n, leptos_fluent}; use leptos_fluent::{Language, leptos_fluent};
static_loader! { static_loader! {
pub static TRANSLATIONS = { pub static TRANSLATIONS = {
@ -23,36 +23,39 @@ pub fn I18n(children: Children) -> impl IntoView {
translations: [TRANSLATIONS], translations: [TRANSLATIONS],
locales: "./locales", locales: "./locales",
check_translations: "./src/**/*.rs", check_translations: "./src/**/*.rs",
initial_language_from_localstorage: true, initial_language_from_local_storage: true,
initial_language_from_navigator_to_localstorage: true, initial_language_from_navigator_to_local_storage: true,
set_language_to_localstorage: true, set_language_to_local_storage: true,
sync_html_tag_lang: true, sync_html_tag_lang: true,
} }
} }
/// Selector for languages
#[component] #[component]
pub fn LanguageSelector() -> impl IntoView { pub fn LanguageSelector() -> impl IntoView {
// Use `expect_i18n()` to get the current i18n context: let i18n = expect_context::<leptos_fluent::I18n>();
let i18n = expect_i18n();
view! { view! {
<span>
<label for="language">"A/文:"</label> <label for="language">"A/文:"</label>
<select id="language"> <select id="language">
{move || { {move || {
i18n.languages.iter().map(|lang| render_language(lang)).collect::<Vec<_>>() i18n.languages.iter().map(|lang| render_language(lang)).collect::<Vec<_>>()
}} }}
</select> </select>
</span>
} }
} }
fn render_language(lang: &'static Language) -> impl IntoView { fn render_language(lang: &'static Language) -> impl IntoView {
// Passed as atrribute, `Language` is converted to their code, // Passed as atrribute, `Language` is converted to their code,
// so `<input id=lang` becomes `<input id=lang.id.to_string()` // so `<input id=lang` becomes `<input id=lang.id.to_string()`
let i18n = expect_i18n(); let i18n = expect_context::<leptos_fluent::I18n>();
view! { view! {
<option <option
id=lang id=lang
value=lang value=lang
prop:selected=lang.is_active() prop:selected=i18n.language.get() == lang
on:click=move |_| i18n.language.set(lang) on:click=move |_| i18n.language.set(lang)
> >
{lang.name} {lang.name}

View file

@ -1,2 +1,3 @@
pub mod calculator; pub mod calculator;
pub mod common;
pub mod localization; pub mod localization;

View file

@ -1,15 +1,20 @@
use leptos::prelude::{AddAnyAttr, IntoAttribute}; use leptos::prelude::{AddAnyAttr, Get, expect_context};
use leptos::{IntoView, component, view}; use leptos::{IntoView, component, view};
use leptos_meta::*; use leptos_meta::{Html, Meta, Title, provide_meta_context};
use leptos_router::{components::*, path}; use leptos_router::components::{ParentRoute, Route, Router, Routes};
use leptos_router::path;
// Modules // Modules
mod calc; mod calc;
mod components; mod components;
mod pages; 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::home::Home;
use crate::pages::hyper::Hyper;
use crate::pages::not_found::NotFound;
/// An app router which renders the homepage and handles 404's /// An app router which renders the homepage and handles 404's
#[component] #[component]
@ -18,19 +23,31 @@ pub fn App() -> impl IntoView {
provide_meta_context(); provide_meta_context();
view! { view! {
<Html attr:lang="en" attr:dir="ltr" attr:data-theme="light" /> <I18n>
<HtmlAttrs />
// sets the document title // sets the document title
<Title text="Hypergeometric Calculator" /> <Title text="🧮" />
// injects metadata in the <head> of the page // injects metadata in the <head> of the page
<Meta charset="UTF-8" /> <Meta charset="UTF-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1.0" /> <Meta name="viewport" content="width=device-width, initial-scale=1.0" />
<Router> <Router>
<Routes fallback=|| view! { NotFound }> <Routes fallback=NotFound>
<Route path=path!("/") view=Home /> <ParentRoute path=path!("/") view=Common>
<Route path=path!("") view=Home />
<Route path=path!("/hyper") view=Hyper />
<Route path=path!("/binom") view=Binom />
</ParentRoute>
</Routes> </Routes>
</Router> </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" /> }
}

View file

@ -1,4 +1,4 @@
use hypergeometric_calc::App; use distribution_calc::App;
use leptos::{mount::mount_to_body, view}; use leptos::{mount::mount_to_body, view};
fn main() { fn main() {

17
src/pages/binom.rs Normal file
View 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>
}
}

View file

@ -1,50 +1,22 @@
use leptos::attr::global::ClassAttribute; use leptos::attr::global::ClassAttribute;
use leptos::error::ErrorBoundary;
use leptos::html::ElementChild; use leptos::html::ElementChild;
use leptos::prelude::{CollectView, Get};
use leptos::{IntoView, component, view}; use leptos::{IntoView, component, view};
use leptos_fluent::move_tr; use leptos_fluent::move_tr;
use crate::components::calculator::Calculator; ///Home Page
use crate::components::localization::{I18n, LanguageSelector};
/// Default Home Page
#[component] #[component]
pub fn Home() -> impl IntoView { pub fn Home() -> impl IntoView {
view! { 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"> <div class="container">
<Title /> <h1 class="title">{move_tr!("title-home")}</h1>
<Calculator /> <menu>
<li>
<a href="/binom">{move_tr!("title-binom")}</a>
</li>
<li>
<a href="/hyper">{move_tr!("title-hyper")}</a>
</li>
</menu>
</div> </div>
</I18n>
</ErrorBoundary>
} }
} }
/// Title
#[component]
pub fn Title() -> impl IntoView {
view! {<h1 class="title">{move_tr!("title")}</h1>}
}

17
src/pages/hyper.rs Normal file
View 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>
}
}

View file

@ -1,2 +1,4 @@
pub mod binom;
pub mod home; pub mod home;
pub mod hyper;
pub mod not_found; pub mod not_found;