Skip to main content

stryke/
builtins_const.rs

1//! Constants & distribution helpers.
2//! HTTP status codes return their numeric value. HTTP method names
3//! return their canonical uppercase form. Distribution PDF/quantile/
4//! sampler functions use the `statrs` crate.
5
6use crate::value::StrykeValue;
7use statrs::distribution::{
8    Beta, Cauchy, ChiSquared, ContinuousCDF, Discrete, Exp, FisherSnedecor, Gamma, LogNormal,
9    Normal, StudentsT, Uniform, Weibull,
10};
11
12fn arg_f64(args: &[StrykeValue], idx: usize) -> Option<f64> {
13    args.get(idx).map(|v| v.to_number())
14}
15
16macro_rules! http_status {
17    ($name:ident, $code:expr) => {
18        pub fn $name(_args: &[StrykeValue]) -> StrykeValue {
19            StrykeValue::integer($code)
20        }
21    };
22}
23
24http_status!(http_status_continue, 100);
25http_status!(http_status_switching_protocols, 101);
26http_status!(http_status_ok, 200);
27http_status!(http_status_created, 201);
28http_status!(http_status_accepted, 202);
29http_status!(http_status_no_content, 204);
30http_status!(http_status_partial_content, 206);
31http_status!(http_status_multiple_choices, 300);
32http_status!(http_status_moved_permanently, 301);
33http_status!(http_status_found, 302);
34http_status!(http_status_see_other, 303);
35http_status!(http_status_not_modified, 304);
36http_status!(http_status_temporary_redirect, 307);
37http_status!(http_status_permanent_redirect, 308);
38http_status!(http_status_bad_request, 400);
39http_status!(http_status_unauthorized, 401);
40http_status!(http_status_payment_required, 402);
41http_status!(http_status_forbidden, 403);
42http_status!(http_status_not_found, 404);
43http_status!(http_status_method_not_allowed, 405);
44http_status!(http_status_not_acceptable, 406);
45http_status!(http_status_conflict, 409);
46http_status!(http_status_gone, 410);
47http_status!(http_status_length_required, 411);
48http_status!(http_status_precondition_failed, 412);
49http_status!(http_status_payload_too_large, 413);
50http_status!(http_status_uri_too_long, 414);
51http_status!(http_status_unsupported_media_type, 415);
52http_status!(http_status_range_not_satisfiable, 416);
53http_status!(http_status_expectation_failed, 417);
54http_status!(http_status_im_a_teapot, 418);
55http_status!(http_status_unprocessable_entity, 422);
56http_status!(http_status_too_many_requests, 429);
57http_status!(http_status_internal_server_error, 500);
58http_status!(http_status_not_implemented, 501);
59http_status!(http_status_bad_gateway, 502);
60http_status!(http_status_service_unavailable, 503);
61http_status!(http_status_gateway_timeout, 504);
62http_status!(http_status_http_version_not_supported, 505);
63
64macro_rules! http_method {
65    ($name:ident, $verb:expr) => {
66        pub fn $name(_args: &[StrykeValue]) -> StrykeValue {
67            StrykeValue::string($verb.to_string())
68        }
69    };
70}
71
72http_method!(http_method_get, "GET");
73http_method!(http_method_post, "POST");
74http_method!(http_method_put, "PUT");
75http_method!(http_method_delete, "DELETE");
76http_method!(http_method_patch, "PATCH");
77http_method!(http_method_head, "HEAD");
78http_method!(http_method_options, "OPTIONS");
79http_method!(http_method_trace, "TRACE");
80http_method!(http_method_connect, "CONNECT");
81
82// ══════════════════════════════════════════════════════════════════════
83// Distribution PDF / quantile functions (statrs backed)
84// ══════════════════════════════════════════════════════════════════════
85/// `dbeta` — see implementation.
86pub fn dbeta(args: &[StrykeValue]) -> StrykeValue {
87    let x = arg_f64(args, 0).unwrap_or(0.0);
88    let a = arg_f64(args, 1).unwrap_or(1.0);
89    let b = arg_f64(args, 2).unwrap_or(1.0);
90    match Beta::new(a, b) {
91        Ok(d) => {
92            use statrs::distribution::Continuous;
93            StrykeValue::float(d.pdf(x))
94        }
95        Err(_) => StrykeValue::UNDEF,
96    }
97}
98/// `qbeta` — see implementation.
99pub fn qbeta(args: &[StrykeValue]) -> StrykeValue {
100    let p = arg_f64(args, 0).unwrap_or(0.5);
101    let a = arg_f64(args, 1).unwrap_or(1.0);
102    let b = arg_f64(args, 2).unwrap_or(1.0);
103    match Beta::new(a, b) {
104        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
105        Err(_) => StrykeValue::UNDEF,
106    }
107}
108/// `rbeta` — see implementation.
109pub fn rbeta(args: &[StrykeValue]) -> StrykeValue {
110    use rand::Rng;
111    let mut rng = rand::thread_rng();
112    let p: f64 = rng.gen();
113    qbeta(&[
114        StrykeValue::float(p),
115        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
116        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
117    ])
118}
119/// `dcauchy` — see implementation.
120pub fn dcauchy(args: &[StrykeValue]) -> StrykeValue {
121    let x = arg_f64(args, 0).unwrap_or(0.0);
122    let loc = arg_f64(args, 1).unwrap_or(0.0);
123    let scale = arg_f64(args, 2).unwrap_or(1.0);
124    match Cauchy::new(loc, scale) {
125        Ok(d) => {
126            use statrs::distribution::Continuous;
127            StrykeValue::float(d.pdf(x))
128        }
129        Err(_) => StrykeValue::UNDEF,
130    }
131}
132/// `qcauchy` — see implementation.
133pub fn qcauchy(args: &[StrykeValue]) -> StrykeValue {
134    let p = arg_f64(args, 0).unwrap_or(0.5);
135    let loc = arg_f64(args, 1).unwrap_or(0.0);
136    let scale = arg_f64(args, 2).unwrap_or(1.0);
137    match Cauchy::new(loc, scale) {
138        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
139        Err(_) => StrykeValue::UNDEF,
140    }
141}
142/// `rcauchy` — see implementation.
143pub fn rcauchy(args: &[StrykeValue]) -> StrykeValue {
144    use rand::Rng;
145    let p: f64 = rand::thread_rng().gen();
146    qcauchy(&[
147        StrykeValue::float(p),
148        args.first().cloned().unwrap_or(StrykeValue::float(0.0)),
149        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
150    ])
151}
152/// `dexp` — see implementation.
153pub fn dexp(args: &[StrykeValue]) -> StrykeValue {
154    let x = arg_f64(args, 0).unwrap_or(0.0);
155    let rate = arg_f64(args, 1).unwrap_or(1.0);
156    match Exp::new(rate) {
157        Ok(d) => {
158            use statrs::distribution::Continuous;
159            StrykeValue::float(d.pdf(x))
160        }
161        Err(_) => StrykeValue::UNDEF,
162    }
163}
164/// `qexp` — see implementation.
165pub fn qexp(args: &[StrykeValue]) -> StrykeValue {
166    let p = arg_f64(args, 0).unwrap_or(0.5);
167    let rate = arg_f64(args, 1).unwrap_or(1.0);
168    match Exp::new(rate) {
169        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
170        Err(_) => StrykeValue::UNDEF,
171    }
172}
173/// `rexp` — see implementation.
174pub fn rexp(args: &[StrykeValue]) -> StrykeValue {
175    use rand::Rng;
176    let p: f64 = rand::thread_rng().gen();
177    qexp(&[
178        StrykeValue::float(p),
179        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
180    ])
181}
182/// `dgamma` — see implementation.
183pub fn dgamma(args: &[StrykeValue]) -> StrykeValue {
184    let x = arg_f64(args, 0).unwrap_or(0.0);
185    let shape = arg_f64(args, 1).unwrap_or(1.0);
186    let rate = arg_f64(args, 2).unwrap_or(1.0);
187    match Gamma::new(shape, rate) {
188        Ok(d) => {
189            use statrs::distribution::Continuous;
190            StrykeValue::float(d.pdf(x))
191        }
192        Err(_) => StrykeValue::UNDEF,
193    }
194}
195/// `qgamma` — see implementation.
196pub fn qgamma(args: &[StrykeValue]) -> StrykeValue {
197    let p = arg_f64(args, 0).unwrap_or(0.5);
198    let shape = arg_f64(args, 1).unwrap_or(1.0);
199    let rate = arg_f64(args, 2).unwrap_or(1.0);
200    match Gamma::new(shape, rate) {
201        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
202        Err(_) => StrykeValue::UNDEF,
203    }
204}
205/// `rgamma` — see implementation.
206pub fn rgamma(args: &[StrykeValue]) -> StrykeValue {
207    use rand::Rng;
208    let p: f64 = rand::thread_rng().gen();
209    qgamma(&[
210        StrykeValue::float(p),
211        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
212        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
213    ])
214}
215/// `dlnorm` — see implementation.
216pub fn dlnorm(args: &[StrykeValue]) -> StrykeValue {
217    let x = arg_f64(args, 0).unwrap_or(1.0);
218    let mu = arg_f64(args, 1).unwrap_or(0.0);
219    let sigma = arg_f64(args, 2).unwrap_or(1.0);
220    match LogNormal::new(mu, sigma) {
221        Ok(d) => {
222            use statrs::distribution::Continuous;
223            StrykeValue::float(d.pdf(x))
224        }
225        Err(_) => StrykeValue::UNDEF,
226    }
227}
228/// `qlnorm` — see implementation.
229pub fn qlnorm(args: &[StrykeValue]) -> StrykeValue {
230    let p = arg_f64(args, 0).unwrap_or(0.5);
231    let mu = arg_f64(args, 1).unwrap_or(0.0);
232    let sigma = arg_f64(args, 2).unwrap_or(1.0);
233    match LogNormal::new(mu, sigma) {
234        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
235        Err(_) => StrykeValue::UNDEF,
236    }
237}
238/// `rlnorm` — see implementation.
239pub fn rlnorm(args: &[StrykeValue]) -> StrykeValue {
240    use rand::Rng;
241    let p: f64 = rand::thread_rng().gen();
242    qlnorm(&[
243        StrykeValue::float(p),
244        args.first().cloned().unwrap_or(StrykeValue::float(0.0)),
245        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
246    ])
247}
248/// `dlogis` — see implementation.
249pub fn dlogis(args: &[StrykeValue]) -> StrykeValue {
250    let x = arg_f64(args, 0).unwrap_or(0.0);
251    let loc = arg_f64(args, 1).unwrap_or(0.0);
252    let scale = arg_f64(args, 2).unwrap_or(1.0).max(1e-12);
253    let z = (x - loc) / scale;
254    let pdf = (-z).exp() / (scale * (1.0 + (-z).exp()).powi(2));
255    StrykeValue::float(pdf)
256}
257/// `qlogis` — see implementation.
258pub fn qlogis(args: &[StrykeValue]) -> StrykeValue {
259    let p = arg_f64(args, 0).unwrap_or(0.5).clamp(1e-12, 1.0 - 1e-12);
260    let loc = arg_f64(args, 1).unwrap_or(0.0);
261    let scale = arg_f64(args, 2).unwrap_or(1.0);
262    StrykeValue::float(loc + scale * (p / (1.0 - p)).ln())
263}
264/// `rlogis` — see implementation.
265pub fn rlogis(args: &[StrykeValue]) -> StrykeValue {
266    use rand::Rng;
267    let p: f64 = rand::thread_rng().gen();
268    qlogis(&[
269        StrykeValue::float(p),
270        args.first().cloned().unwrap_or(StrykeValue::float(0.0)),
271        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
272    ])
273}
274/// `dpois` — see implementation.
275pub fn dpois(args: &[StrykeValue]) -> StrykeValue {
276    let k = arg_f64(args, 0).unwrap_or(0.0).max(0.0).round() as u64;
277    let lambda = arg_f64(args, 1).unwrap_or(1.0);
278    match statrs::distribution::Poisson::new(lambda) {
279        Ok(d) => StrykeValue::float(d.pmf(k)),
280        Err(_) => StrykeValue::UNDEF,
281    }
282}
283/// `qpois` — see implementation.
284pub fn qpois(args: &[StrykeValue]) -> StrykeValue {
285    let p = arg_f64(args, 0).unwrap_or(0.5);
286    let lambda = arg_f64(args, 1).unwrap_or(1.0);
287    let Ok(d) = statrs::distribution::Poisson::new(lambda) else {
288        return StrykeValue::UNDEF;
289    };
290    use statrs::distribution::DiscreteCDF;
291    let mut k: u64 = 0;
292    while d.cdf(k) < p && k < 1_000_000 {
293        k += 1;
294    }
295    StrykeValue::integer(k as i64)
296}
297/// `rpois` — see implementation.
298pub fn rpois(args: &[StrykeValue]) -> StrykeValue {
299    use rand::Rng;
300    let p: f64 = rand::thread_rng().gen();
301    qpois(&[
302        StrykeValue::float(p),
303        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
304    ])
305}
306/// `dweibull` — see implementation.
307pub fn dweibull(args: &[StrykeValue]) -> StrykeValue {
308    let x = arg_f64(args, 0).unwrap_or(0.0);
309    let shape = arg_f64(args, 1).unwrap_or(1.0);
310    let scale = arg_f64(args, 2).unwrap_or(1.0);
311    match Weibull::new(shape, scale) {
312        Ok(d) => {
313            use statrs::distribution::Continuous;
314            StrykeValue::float(d.pdf(x))
315        }
316        Err(_) => StrykeValue::UNDEF,
317    }
318}
319/// `qweibull` — see implementation.
320pub fn qweibull(args: &[StrykeValue]) -> StrykeValue {
321    let p = arg_f64(args, 0).unwrap_or(0.5);
322    let shape = arg_f64(args, 1).unwrap_or(1.0);
323    let scale = arg_f64(args, 2).unwrap_or(1.0);
324    match Weibull::new(shape, scale) {
325        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
326        Err(_) => StrykeValue::UNDEF,
327    }
328}
329/// `rweibull` — see implementation.
330pub fn rweibull(args: &[StrykeValue]) -> StrykeValue {
331    use rand::Rng;
332    let p: f64 = rand::thread_rng().gen();
333    qweibull(&[
334        StrykeValue::float(p),
335        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
336        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
337    ])
338}
339/// `qnorm` — see implementation.
340pub fn qnorm(args: &[StrykeValue]) -> StrykeValue {
341    let p = arg_f64(args, 0).unwrap_or(0.5);
342    let mu = arg_f64(args, 1).unwrap_or(0.0);
343    let sigma = arg_f64(args, 2).unwrap_or(1.0);
344    match Normal::new(mu, sigma) {
345        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
346        Err(_) => StrykeValue::UNDEF,
347    }
348}
349/// `rnorm` — see implementation.
350pub fn rnorm(args: &[StrykeValue]) -> StrykeValue {
351    use rand::Rng;
352    let p: f64 = rand::thread_rng().gen();
353    qnorm(&[
354        StrykeValue::float(p),
355        args.first().cloned().unwrap_or(StrykeValue::float(0.0)),
356        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
357    ])
358}
359/// `qunif` — see implementation.
360pub fn qunif(args: &[StrykeValue]) -> StrykeValue {
361    let p = arg_f64(args, 0).unwrap_or(0.5);
362    let lo = arg_f64(args, 1).unwrap_or(0.0);
363    let hi = arg_f64(args, 2).unwrap_or(1.0);
364    match Uniform::new(lo, hi) {
365        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
366        Err(_) => StrykeValue::UNDEF,
367    }
368}
369/// `runif` — see implementation.
370pub fn runif(args: &[StrykeValue]) -> StrykeValue {
371    use rand::Rng;
372    let lo = arg_f64(args, 0).unwrap_or(0.0);
373    let hi = arg_f64(args, 1).unwrap_or(1.0);
374    StrykeValue::float(rand::thread_rng().gen_range(lo..hi))
375}
376/// `qbinom` — see implementation.
377pub fn qbinom(args: &[StrykeValue]) -> StrykeValue {
378    let p = arg_f64(args, 0).unwrap_or(0.5);
379    let n = arg_f64(args, 1).unwrap_or(10.0).max(0.0).round() as u64;
380    let pr = arg_f64(args, 2).unwrap_or(0.5);
381    let Ok(d) = statrs::distribution::Binomial::new(pr, n) else {
382        return StrykeValue::UNDEF;
383    };
384    use statrs::distribution::DiscreteCDF;
385    for k in 0..=n {
386        if d.cdf(k) >= p {
387            return StrykeValue::integer(k as i64);
388        }
389    }
390    StrykeValue::integer(n as i64)
391}
392/// `rbinom` — see implementation.
393pub fn rbinom(args: &[StrykeValue]) -> StrykeValue {
394    use rand::Rng;
395    let p: f64 = rand::thread_rng().gen();
396    qbinom(&[
397        StrykeValue::float(p),
398        args.first().cloned().unwrap_or(StrykeValue::float(10.0)),
399        args.get(1).cloned().unwrap_or(StrykeValue::float(0.5)),
400    ])
401}
402/// `qgeom` — see implementation.
403pub fn qgeom(args: &[StrykeValue]) -> StrykeValue {
404    let p = arg_f64(args, 0).unwrap_or(0.5);
405    let pr = arg_f64(args, 1).unwrap_or(0.5).clamp(1e-12, 1.0);
406    // Inverse CDF for geometric: k = ceil(ln(1-p) / ln(1-pr)) - 1
407    let k = ((1.0 - p).ln() / (1.0 - pr).max(1e-12).ln()).ceil() as i64 - 1;
408    StrykeValue::integer(k.max(0))
409}
410/// `rgeom` — see implementation.
411pub fn rgeom(args: &[StrykeValue]) -> StrykeValue {
412    use rand::Rng;
413    let p: f64 = rand::thread_rng().gen();
414    qgeom(&[
415        StrykeValue::float(p),
416        args.first().cloned().unwrap_or(StrykeValue::float(0.5)),
417    ])
418}
419/// `qhyper` — see implementation.
420pub fn qhyper(args: &[StrykeValue]) -> StrykeValue {
421    let p = arg_f64(args, 0).unwrap_or(0.5);
422    let pop = arg_f64(args, 1).unwrap_or(10.0).max(1.0).round() as u64;
423    let succ = arg_f64(args, 2).unwrap_or(5.0).max(0.0).round() as u64;
424    let draws = arg_f64(args, 3).unwrap_or(3.0).max(0.0).round() as u64;
425    let Ok(d) = statrs::distribution::Hypergeometric::new(pop, succ, draws) else {
426        return StrykeValue::UNDEF;
427    };
428    use statrs::distribution::DiscreteCDF;
429    let max = draws.min(succ);
430    for k in 0..=max {
431        if d.cdf(k) >= p {
432            return StrykeValue::integer(k as i64);
433        }
434    }
435    StrykeValue::integer(max as i64)
436}
437/// `rhyper` — see implementation.
438pub fn rhyper(args: &[StrykeValue]) -> StrykeValue {
439    use rand::Rng;
440    let p: f64 = rand::thread_rng().gen();
441    qhyper(&[
442        StrykeValue::float(p),
443        args.first().cloned().unwrap_or(StrykeValue::float(10.0)),
444        args.get(1).cloned().unwrap_or(StrykeValue::float(5.0)),
445        args.get(2).cloned().unwrap_or(StrykeValue::float(3.0)),
446    ])
447}
448/// `qchisq` — see implementation.
449pub fn qchisq(args: &[StrykeValue]) -> StrykeValue {
450    let p = arg_f64(args, 0).unwrap_or(0.5);
451    let df = arg_f64(args, 1).unwrap_or(1.0);
452    match ChiSquared::new(df) {
453        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
454        Err(_) => StrykeValue::UNDEF,
455    }
456}
457/// `rchisq` — see implementation.
458pub fn rchisq(args: &[StrykeValue]) -> StrykeValue {
459    use rand::Rng;
460    let p: f64 = rand::thread_rng().gen();
461    qchisq(&[
462        StrykeValue::float(p),
463        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
464    ])
465}
466/// `qf` — see implementation.
467pub fn qf(args: &[StrykeValue]) -> StrykeValue {
468    let p = arg_f64(args, 0).unwrap_or(0.5);
469    let df1 = arg_f64(args, 1).unwrap_or(1.0);
470    let df2 = arg_f64(args, 2).unwrap_or(1.0);
471    match FisherSnedecor::new(df1, df2) {
472        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
473        Err(_) => StrykeValue::UNDEF,
474    }
475}
476/// `rf` — see implementation.
477pub fn rf(args: &[StrykeValue]) -> StrykeValue {
478    use rand::Rng;
479    let p: f64 = rand::thread_rng().gen();
480    qf(&[
481        StrykeValue::float(p),
482        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
483        args.get(1).cloned().unwrap_or(StrykeValue::float(1.0)),
484    ])
485}
486/// `qt` — see implementation.
487pub fn qt(args: &[StrykeValue]) -> StrykeValue {
488    let p = arg_f64(args, 0).unwrap_or(0.5);
489    let df = arg_f64(args, 1).unwrap_or(1.0);
490    match StudentsT::new(0.0, 1.0, df) {
491        Ok(d) => StrykeValue::float(d.inverse_cdf(p)),
492        Err(_) => StrykeValue::UNDEF,
493    }
494}
495/// `rt` — see implementation.
496pub fn rt(args: &[StrykeValue]) -> StrykeValue {
497    use rand::Rng;
498    let p: f64 = rand::thread_rng().gen();
499    qt(&[
500        StrykeValue::float(p),
501        args.first().cloned().unwrap_or(StrykeValue::float(1.0)),
502    ])
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    fn approx(a: f64, b: f64, eps: f64) -> bool {
510        (a - b).abs() < eps
511    }
512
513    // ─── http_status_* ───────────────────────────────────────────────────
514
515    #[test]
516    fn http_status_macro_pins_canonical_codes() {
517        assert_eq!(http_status_ok(&[]).to_int(), 200);
518        assert_eq!(http_status_not_found(&[]).to_int(), 404);
519        assert_eq!(http_status_internal_server_error(&[]).to_int(), 500);
520        assert_eq!(http_status_im_a_teapot(&[]).to_int(), 418);
521        assert_eq!(http_status_too_many_requests(&[]).to_int(), 429);
522    }
523
524    #[test]
525    fn http_status_ignores_args() {
526        // _args is unused — passing junk must not crash and must return code.
527        assert_eq!(
528            http_status_ok(&[StrykeValue::string("ignored".into())]).to_int(),
529            200
530        );
531    }
532
533    // ─── http_method_* ───────────────────────────────────────────────────
534
535    #[test]
536    fn http_method_macro_emits_uppercase_verb() {
537        assert_eq!(http_method_get(&[]).to_string(), "GET");
538        assert_eq!(http_method_post(&[]).to_string(), "POST");
539        assert_eq!(http_method_delete(&[]).to_string(), "DELETE");
540        assert_eq!(http_method_options(&[]).to_string(), "OPTIONS");
541    }
542
543    // ─── dbeta / qbeta ───────────────────────────────────────────────────
544
545    #[test]
546    fn dbeta_uniform_special_case() {
547        // Beta(1,1) is Uniform(0,1) — pdf is 1.0 everywhere in (0,1).
548        let r = dbeta(&[
549            StrykeValue::float(0.5),
550            StrykeValue::float(1.0),
551            StrykeValue::float(1.0),
552        ]);
553        assert!(approx(r.to_number(), 1.0, 1e-9));
554    }
555
556    #[test]
557    fn dbeta_invalid_params_returns_undef() {
558        // Beta requires a,b > 0. Zero is invalid.
559        let r = dbeta(&[
560            StrykeValue::float(0.5),
561            StrykeValue::float(0.0),
562            StrykeValue::float(1.0),
563        ]);
564        assert!(r.is_undef());
565    }
566
567    #[test]
568    fn qbeta_median_of_uniform_is_half() {
569        let r = qbeta(&[
570            StrykeValue::float(0.5),
571            StrykeValue::float(1.0),
572            StrykeValue::float(1.0),
573        ]);
574        assert!(approx(r.to_number(), 0.5, 1e-9));
575    }
576
577    // ─── dexp / qexp ─────────────────────────────────────────────────────
578
579    #[test]
580    fn dexp_at_zero_equals_rate() {
581        // Exponential PDF at x=0: f(0) = lambda.
582        let r = dexp(&[StrykeValue::float(0.0), StrykeValue::float(2.0)]);
583        assert!(approx(r.to_number(), 2.0, 1e-9));
584    }
585
586    #[test]
587    fn qexp_quartile_relation() {
588        // Exp(1) quantile: F^{-1}(p) = -ln(1-p). p=0.5 → ln(2).
589        let r = qexp(&[StrykeValue::float(0.5), StrykeValue::float(1.0)]);
590        assert!(approx(r.to_number(), 2f64.ln(), 1e-9));
591    }
592
593    #[test]
594    fn dexp_negative_rate_returns_undef() {
595        let r = dexp(&[StrykeValue::float(1.0), StrykeValue::float(-1.0)]);
596        assert!(r.is_undef());
597    }
598
599    // ─── qnorm ───────────────────────────────────────────────────────────
600
601    #[test]
602    fn qnorm_median_is_mean() {
603        // qnorm(0.5, mu, sigma) = mu for any sigma > 0.
604        let r = qnorm(&[
605            StrykeValue::float(0.5),
606            StrykeValue::float(7.0),
607            StrykeValue::float(3.0),
608        ]);
609        assert!(approx(r.to_number(), 7.0, 1e-9));
610    }
611
612    #[test]
613    fn qnorm_invalid_sigma_returns_undef() {
614        let r = qnorm(&[
615            StrykeValue::float(0.5),
616            StrykeValue::float(0.0),
617            StrykeValue::float(-1.0),
618        ]);
619        assert!(r.is_undef());
620    }
621
622    // ─── qunif / runif ───────────────────────────────────────────────────
623
624    #[test]
625    fn qunif_linear_in_p() {
626        // U[2,10] median = 6.
627        let r = qunif(&[
628            StrykeValue::float(0.5),
629            StrykeValue::float(2.0),
630            StrykeValue::float(10.0),
631        ]);
632        assert!(approx(r.to_number(), 6.0, 1e-9));
633    }
634
635    #[test]
636    fn runif_within_range() {
637        // 1000 samples must all fall in [lo, hi).
638        for _ in 0..1000 {
639            let r = runif(&[StrykeValue::float(-5.0), StrykeValue::float(5.0)]).to_number();
640            assert!((-5.0..5.0).contains(&r), "out of range: {r}");
641        }
642    }
643
644    // ─── qlogis ─────────────────────────────────────────────────────────
645
646    #[test]
647    fn qlogis_median_returns_location() {
648        // Logistic median = loc.
649        let r = qlogis(&[
650            StrykeValue::float(0.5),
651            StrykeValue::float(4.0),
652            StrykeValue::float(2.0),
653        ]);
654        assert!(approx(r.to_number(), 4.0, 1e-6));
655    }
656
657    // ─── dlogis ──────────────────────────────────────────────────────────
658
659    #[test]
660    fn dlogis_at_location_is_quarter_over_scale() {
661        // f(loc) = 1/(4*scale) for logistic distribution.
662        let r = dlogis(&[
663            StrykeValue::float(0.0),
664            StrykeValue::float(0.0),
665            StrykeValue::float(1.0),
666        ]);
667        assert!(approx(r.to_number(), 0.25, 1e-9));
668    }
669
670    // ─── dpois ───────────────────────────────────────────────────────────
671
672    #[test]
673    fn dpois_k_zero_equals_exp_neg_lambda() {
674        // P(X=0) = e^{-lambda}.
675        let r = dpois(&[StrykeValue::float(0.0), StrykeValue::float(2.5)]);
676        assert!(approx(r.to_number(), (-2.5f64).exp(), 1e-9));
677    }
678
679    #[test]
680    fn dpois_invalid_lambda_returns_undef() {
681        let r = dpois(&[StrykeValue::float(0.0), StrykeValue::float(-1.0)]);
682        assert!(r.is_undef());
683    }
684
685    // ─── qgeom ───────────────────────────────────────────────────────────
686
687    #[test]
688    fn qgeom_min_zero() {
689        // Quantile clamped to >= 0.
690        let r = qgeom(&[StrykeValue::float(0.0), StrykeValue::float(0.5)]);
691        assert!(r.to_int() >= 0);
692    }
693
694    // ─── qbinom ──────────────────────────────────────────────────────────
695
696    #[test]
697    fn qbinom_median_balanced() {
698        // Binomial(n=10, p=0.5) median ≈ 5.
699        let r = qbinom(&[
700            StrykeValue::float(0.5),
701            StrykeValue::float(10.0),
702            StrykeValue::float(0.5),
703        ]);
704        let k = r.to_int();
705        assert!((4..=5).contains(&k), "expected 4 or 5, got {k}");
706    }
707
708    // ─── qchisq ──────────────────────────────────────────────────────────
709
710    #[test]
711    fn qchisq_invalid_df_returns_undef() {
712        let r = qchisq(&[StrykeValue::float(0.5), StrykeValue::float(0.0)]);
713        assert!(r.is_undef());
714    }
715
716    // ─── arg_f64 ────────────────────────────────────────────────────────
717
718    #[test]
719    fn arg_f64_missing_index_returns_none() {
720        assert!(arg_f64(&[], 0).is_none());
721        assert!(arg_f64(&[StrykeValue::float(1.0)], 5).is_none());
722    }
723}