use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
use parking_lot::Mutex;
static BERNOULLI_CACHE: Mutex<Vec<Ratio<BigInt>>> = Mutex::new(Vec::new());
#[must_use]
pub(crate) fn bernoulli(n: usize) -> Ratio<BigInt> {
let mut cache = BERNOULLI_CACHE.lock();
while cache.len() <= n {
let m = cache.len();
if m == 0 {
cache.push(Ratio::one()); continue;
}
if m == 1 {
cache.push(Ratio::new(BigInt::from(-1), BigInt::from(2)));
continue;
}
if m >= 3 && m % 2 == 1 {
cache.push(Ratio::zero());
continue;
}
let mut sum = Ratio::zero();
let mut binom: Ratio<BigInt> = Ratio::one(); for k in 0..m {
sum += &binom * &cache[k];
binom = binom * Ratio::from_integer(BigInt::from(m + 1 - k))
/ Ratio::from_integer(BigInt::from(k + 1));
}
let result = -sum / Ratio::from_integer(BigInt::from(m + 1));
tracing::trace!(index = m, value = %result, "bernoulli: computed");
cache.push(result);
}
cache[n].clone()
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::ToPrimitive;
#[test]
fn b0_is_one() {
let b0 = bernoulli(0);
assert_eq!(b0, Ratio::one(), "B_0 = 1");
}
#[test]
fn b1_is_neg_half() {
let b1 = bernoulli(1);
let expected = Ratio::new(BigInt::from(-1), BigInt::from(2));
assert_eq!(b1, expected, "B_1 = -1/2");
}
#[test]
fn b2_is_one_sixth() {
let b2 = bernoulli(2);
let expected = Ratio::new(BigInt::from(1), BigInt::from(6));
assert_eq!(b2, expected, "B_2 = 1/6");
}
#[test]
fn b4_is_neg_one_thirtieth() {
let b4 = bernoulli(4);
let expected = Ratio::new(BigInt::from(-1), BigInt::from(30));
assert_eq!(b4, expected, "B_4 = -1/30");
}
#[test]
fn b6_is_one_forty_second() {
let b6 = bernoulli(6);
let expected = Ratio::new(BigInt::from(1), BigInt::from(42));
assert_eq!(b6, expected, "B_6 = 1/42");
}
#[test]
fn b8_is_neg_one_thirtieth() {
let b8 = bernoulli(8);
let expected = Ratio::new(BigInt::from(-1), BigInt::from(30));
assert_eq!(b8, expected, "B_8 = -1/30");
}
#[test]
fn b10() {
let b10 = bernoulli(10);
let expected = Ratio::new(BigInt::from(5), BigInt::from(66));
assert_eq!(b10, expected, "B_10 = 5/66");
}
#[test]
fn b12() {
let b12 = bernoulli(12);
let expected = Ratio::new(BigInt::from(-691), BigInt::from(2730));
assert_eq!(b12, expected, "B_12 = -691/2730");
}
#[test]
fn odd_bernoulli_numbers_are_zero() {
for n in [3, 5, 7, 9, 11, 13, 15, 17, 19, 21] {
let bn = bernoulli(n);
assert!(bn.is_zero(), "B_{n} should be 0, got {bn}");
}
}
#[test]
fn b14() {
let b14 = bernoulli(14);
let expected = Ratio::new(BigInt::from(7), BigInt::from(6));
assert_eq!(b14, expected, "B_14 = 7/6");
}
#[test]
fn b16() {
let b16 = bernoulli(16);
let expected = Ratio::new(BigInt::from(-3617), BigInt::from(510));
assert_eq!(b16, expected, "B_16 = -3617/510");
}
#[test]
fn b18() {
let b18 = bernoulli(18);
let expected = Ratio::new(BigInt::from(43867), BigInt::from(798));
assert_eq!(b18, expected, "B_18 = 43867/798");
}
#[test]
fn b20() {
let b20 = bernoulli(20);
let expected = Ratio::new(BigInt::from(-174611), BigInt::from(330));
assert_eq!(b20, expected, "B_20 = -174611/330");
}
#[test]
fn cache_is_reused() {
let first = bernoulli(30);
let second = bernoulli(30);
assert_eq!(first, second, "cache should return identical values");
}
#[test]
fn large_index_computable() {
let b50 = bernoulli(50);
assert!(!b50.is_zero(), "B_50 should be nonzero");
assert_eq!(
b50.denom(),
&BigInt::from(66),
"B_50 denominator should be 66"
);
}
#[test]
fn alternating_signs_for_even() {
for k in 1..=15 {
let b = bernoulli(2 * k);
let f = b.to_f64().unwrap();
if k % 2 == 1 {
assert!(f > 0.0, "B_{} should be positive, got {}", 2 * k, f);
} else {
assert!(f < 0.0, "B_{} should be negative, got {}", 2 * k, f);
}
}
}
}