1use 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
82pub 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}
98pub 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}
108pub 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}
119pub 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}
132pub 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}
142pub 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}
152pub 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}
164pub 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}
173pub 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}
182pub 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}
195pub 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}
205pub 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}
215pub 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}
228pub 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}
238pub 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}
248pub 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}
257pub 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}
264pub 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}
274pub 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}
283pub 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}
297pub 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}
306pub 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}
319pub 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}
329pub 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}
339pub 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}
349pub 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}
359pub 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}
369pub 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}
376pub 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}
392pub 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}
402pub 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 let k = ((1.0 - p).ln() / (1.0 - pr).max(1e-12).ln()).ceil() as i64 - 1;
408 StrykeValue::integer(k.max(0))
409}
410pub 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}
419pub 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}
437pub 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}
448pub 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}
457pub 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}
466pub 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}
476pub 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}
486pub 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}
495pub 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 #[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 assert_eq!(
528 http_status_ok(&[StrykeValue::string("ignored".into())]).to_int(),
529 200
530 );
531 }
532
533 #[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 #[test]
546 fn dbeta_uniform_special_case() {
547 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 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 #[test]
580 fn dexp_at_zero_equals_rate() {
581 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 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 #[test]
602 fn qnorm_median_is_mean() {
603 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 #[test]
625 fn qunif_linear_in_p() {
626 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 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 #[test]
647 fn qlogis_median_returns_location() {
648 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 #[test]
660 fn dlogis_at_location_is_quarter_over_scale() {
661 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 #[test]
673 fn dpois_k_zero_equals_exp_neg_lambda() {
674 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 #[test]
688 fn qgeom_min_zero() {
689 let r = qgeom(&[StrykeValue::float(0.0), StrykeValue::float(0.5)]);
691 assert!(r.to_int() >= 0);
692 }
693
694 #[test]
697 fn qbinom_median_balanced() {
698 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 #[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 #[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}