#![cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
use crate::transforms::TransformError;
#[derive(Debug, Clone, PartialEq)]
enum Field {
Num(f64),
Str(&'static str),
Null,
}
#[derive(Debug, Clone, PartialEq)]
struct Refusal {
code: &'static str,
message: String,
fields: Vec<(&'static str, Field)>,
}
impl Refusal {
fn out_of_range(
parameter: &'static str,
min: f64,
max: Option<f64>,
got: f64,
message: String,
) -> Self {
Refusal {
code: "parameter_out_of_range",
message,
fields: vec![
("parameter", Field::Str(parameter)),
("min", Field::Num(min)),
("max", max.map_or(Field::Null, Field::Num)),
("got", Field::Num(got)),
],
}
}
}
impl From<TransformError> for Refusal {
fn from(e: TransformError) -> Self {
let code = match e {
TransformError::NonPositiveData => "non_positive_data",
TransformError::NonFiniteData => "value_not_finite",
TransformError::InsufficientData => "insufficient_data",
TransformError::InvalidTransform => "invalid_transform",
TransformError::InvalidInverse => "invalid_inverse",
TransformError::InvalidLambdaRange => "invalid_lambda_range",
};
Refusal {
code,
message: e.to_string(),
fields: Vec::new(),
}
}
}
impl From<Refusal> for JsValue {
fn from(refusal: Refusal) -> JsValue {
let err = js_sys::Error::new(&refusal.message);
let _ = js_sys::Reflect::set(&err, &"code".into(), &refusal.code.into());
for (key, value) in refusal.fields {
let value = match value {
Field::Num(n) => JsValue::from_f64(n),
Field::Str(s) => s.into(),
Field::Null => JsValue::NULL,
};
let _ = js_sys::Reflect::set(&err, &key.into(), &value);
}
err.into()
}
}
#[wasm_bindgen]
pub fn mean(data: &[f64]) -> f64 {
crate::stats::mean(data).unwrap_or(f64::NAN)
}
#[wasm_bindgen]
pub fn std_dev(data: &[f64]) -> f64 {
crate::stats::std_dev(data).unwrap_or(f64::NAN)
}
#[wasm_bindgen]
pub fn variance(data: &[f64]) -> f64 {
crate::stats::variance(data).unwrap_or(f64::NAN)
}
#[wasm_bindgen]
pub fn normal_cdf(x: f64) -> f64 {
crate::special::standard_normal_cdf(x)
}
#[wasm_bindgen]
pub fn normal_sf(x: f64) -> f64 {
crate::special::standard_normal_sf(x)
}
#[wasm_bindgen]
pub fn box_cox(data: &[f64], lambda: f64) -> Result<Vec<f64>, JsValue> {
crate::transforms::box_cox(data, lambda).map_err(|e| Refusal::from(e).into())
}
#[derive(serde::Serialize, tsify::Tsify)]
struct LambdaEstimateDto {
lambda: f64,
at_bound: bool,
}
#[wasm_bindgen(unchecked_return_type = "LambdaEstimateDto")]
pub fn estimate_lambda(data: &[f64], lambda_min: f64, lambda_max: f64) -> Result<JsValue, JsValue> {
let est =
crate::transforms::estimate_lambda(data, lambda_min, lambda_max).map_err(Refusal::from)?;
serde_wasm_bindgen::to_value(&LambdaEstimateDto {
lambda: est.lambda,
at_bound: est.at_bound,
})
.map_err(|e| {
Refusal {
code: "malformed_input",
message: e.to_string(),
fields: vec![("parameter", Field::Str("result"))],
}
.into()
})
}
#[wasm_bindgen]
pub fn rfft(data: &[f64]) -> Vec<f64> {
crate::fourier::rfft(data)
.into_iter()
.flat_map(|z| [z.re, z.im])
.collect()
}
fn check_probability(p: f64) -> Result<(), Refusal> {
if p.is_finite() && p > 0.0 && p < 1.0 {
Ok(())
} else {
Err(Refusal::out_of_range(
"p",
0.0,
Some(1.0),
p,
format!("p must be strictly between 0 and 1, got {p}"),
))
}
}
fn check_df(name: &'static str, df: f64) -> Result<(), Refusal> {
if df.is_finite() && df > 0.0 {
Ok(())
} else {
Err(Refusal::out_of_range(
name,
0.0,
None,
df,
format!("{name} must be a finite number > 0, got {df}"),
))
}
}
fn check_finite(name: &'static str, x: f64) -> Result<(), Refusal> {
if x.is_nan() {
Err(Refusal {
code: "value_not_finite",
message: format!("{name} must be a number, got NaN"),
fields: vec![("parameter", Field::Str(name))],
})
} else {
Ok(())
}
}
#[wasm_bindgen]
pub fn inverse_normal_cdf(p: f64) -> Result<f64, JsValue> {
check_probability(p)?;
Ok(crate::special::inverse_normal_cdf(p))
}
#[wasm_bindgen]
pub fn t_distribution_cdf(t: f64, df: f64) -> Result<f64, JsValue> {
check_finite("t", t)?;
check_df("df", df)?;
Ok(crate::special::t_distribution_cdf(t, df))
}
#[wasm_bindgen]
pub fn t_distribution_quantile(p: f64, df: f64) -> Result<f64, JsValue> {
check_probability(p)?;
check_df("df", df)?;
Ok(crate::special::t_distribution_quantile(p, df))
}
#[wasm_bindgen]
pub fn f_distribution_cdf(x: f64, df1: f64, df2: f64) -> Result<f64, JsValue> {
check_finite("x", x)?;
check_df("df1", df1)?;
check_df("df2", df2)?;
Ok(crate::special::f_distribution_cdf(x, df1, df2))
}
#[wasm_bindgen]
pub fn f_distribution_quantile(p: f64, df1: f64, df2: f64) -> Result<f64, JsValue> {
check_probability(p)?;
check_df("df1", df1)?;
check_df("df2", df2)?;
Ok(crate::special::f_distribution_quantile(p, df1, df2))
}
#[wasm_bindgen]
pub fn chi_squared_cdf(x: f64, k: f64) -> Result<f64, JsValue> {
check_finite("x", x)?;
check_df("k", k)?;
Ok(crate::special::chi_squared_cdf(x, k))
}
#[wasm_bindgen]
pub fn chi_squared_quantile(p: f64, k: f64) -> Result<f64, JsValue> {
check_probability(p)?;
check_df("k", k)?;
Ok(crate::special::chi_squared_quantile(p, k))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_probability_outside_0_1_names_the_range() {
let r = check_probability(1.5).expect_err("outside");
assert_eq!(r.code, "parameter_out_of_range");
assert_eq!(
r.fields,
vec![
("parameter", Field::Str("p")),
("min", Field::Num(0.0)),
("max", Field::Num(1.0)),
("got", Field::Num(1.5)),
]
);
assert!(check_probability(0.5).is_ok());
}
#[test]
fn degrees_of_freedom_are_open_above() {
let r = check_df("df2", 0.0).expect_err("zero");
assert_eq!(r.fields[0], ("parameter", Field::Str("df2")));
assert_eq!(r.fields[2], ("max", Field::Null));
}
#[test]
fn nan_is_refused_as_not_finite() {
assert_eq!(
check_finite("x", f64::NAN).expect_err("NaN").code,
"value_not_finite"
);
assert!(check_finite("x", f64::INFINITY).is_ok());
}
#[test]
fn transform_errors_keep_their_reason() {
let r = Refusal::from(TransformError::NonPositiveData);
assert_eq!(r.code, "non_positive_data");
assert_eq!(r.message, TransformError::NonPositiveData.to_string());
}
}