1use std::collections::BTreeMap;
8
9use bicmath_core::context::ExecContext;
10use bicmath_core::contract::Args;
11use bicmath_core::envelope::Exactness;
12use bicmath_core::error::{EngineError, ErrorCode};
13use bicmath_core::number::{Decimal, Number, NumericMode};
14use bicmath_core::schema::{FieldSchema, NumberKind, ValueSchema};
15use bicmath_core::value::Value;
16use num_bigint::BigInt;
17use num_rational::BigRational;
18use num_traits::{One, Signed, ToPrimitive, Zero};
19
20use crate::mathfn::sqrt;
21
22pub fn all_modes() -> Vec<NumericMode> {
28 vec![
29 NumericMode::Exact,
30 NumericMode::Auto,
31 NumericMode::Scientific,
32 ]
33}
34
35pub fn inferential_modes() -> Vec<NumericMode> {
38 vec![NumericMode::Auto, NumericMode::Scientific]
39}
40
41pub fn any_number_schema() -> ValueSchema {
42 ValueSchema::number(NumberKind::Any)
43}
44
45pub fn float64_schema() -> ValueSchema {
46 ValueSchema::number(NumberKind::Float64)
47}
48
49pub fn integer_schema() -> ValueSchema {
50 ValueSchema::number(NumberKind::Integer)
51}
52
53pub fn text_schema() -> ValueSchema {
54 ValueSchema::text()
55}
56
57pub fn bool_schema() -> ValueSchema {
58 ValueSchema::Bool
59}
60
61pub fn array_schema(items: ValueSchema) -> ValueSchema {
62 ValueSchema::array(items)
63}
64
65pub fn record_schema(fields: Vec<FieldSchema>, allow_extra: bool) -> ValueSchema {
66 ValueSchema::Record {
67 fields,
68 allow_extra,
69 }
70}
71
72pub fn field(name: &str, schema: ValueSchema) -> FieldSchema {
73 FieldSchema::required(name, schema)
74}
75
76pub fn optional_field(name: &str, schema: ValueSchema) -> FieldSchema {
77 FieldSchema::optional(name, schema)
78}
79
80pub fn number_field(name: &str) -> FieldSchema {
81 FieldSchema::required(name, any_number_schema())
82}
83
84pub fn parse_value(raw: serde_json::Value) -> Value {
89 serde_json::from_value(raw).expect("example value must be valid")
90}
91
92pub fn example_args(pairs: &[(&str, serde_json::Value)]) -> BTreeMap<String, Value> {
93 pairs
94 .iter()
95 .map(|(name, raw)| (name.to_string(), parse_value(raw.clone())))
96 .collect()
97}
98
99pub fn record(entries: Vec<(&str, Value)>) -> Value {
104 Value::Record(
105 entries
106 .into_iter()
107 .map(|(key, value)| (key.to_string(), value))
108 .collect(),
109 )
110}
111
112pub fn text(value: impl Into<String>) -> Value {
113 Value::Text(value.into())
114}
115
116pub fn integer_value(value: u64) -> Value {
117 Value::integer(BigInt::from(value))
118}
119
120pub fn bool_value(value: bool) -> Value {
121 Value::Bool(value)
122}
123
124pub fn array_value(values: Vec<Value>) -> Value {
125 Value::Array(values)
126}
127
128pub fn number_value(value: Number) -> Value {
129 Value::Number(value)
130}
131
132pub fn float_value(value: f64) -> Result<Value, EngineError> {
133 Ok(Value::Number(float_number(value)?))
134}
135
136pub fn rational_value(value: BigRational) -> Value {
137 Value::Number(rational_to_number(value))
138}
139
140pub fn rational_to_number(value: BigRational) -> Number {
141 if value.is_integer() {
142 Number::Integer(value.to_integer())
143 } else {
144 Number::Rational(value)
145 }
146}
147
148pub fn float_number(value: f64) -> Result<Number, EngineError> {
149 if !value.is_finite() {
150 return Err(EngineError::domain(
151 "calculation produced a non-finite float64 result",
152 ));
153 }
154 Number::float(value)
155}
156
157pub fn float_array(values: &[f64]) -> Result<Value, EngineError> {
158 values
159 .iter()
160 .map(|value| float_value(*value))
161 .collect::<Result<Vec<_>, _>>()
162 .map(array_value)
163}
164
165pub fn assumptions_value(statements: &[&str]) -> Value {
167 array_value(
168 statements
169 .iter()
170 .map(|statement| text(*statement))
171 .collect(),
172 )
173}
174
175pub fn ensure_finite(number: &Number) -> Result<(), EngineError> {
180 if let Number::Float64(value) = number
181 && !value.get().is_finite()
182 {
183 return Err(EngineError::domain(
184 "non-finite float64 values are not accepted",
185 ));
186 }
187 Ok(())
188}
189
190pub fn number_to_f64(number: &Number) -> Result<f64, EngineError> {
191 ensure_finite(number)?;
192 let value = number.to_f64().ok_or_else(|| {
193 EngineError::domain(format!(
194 "{} value is not representable as float64",
195 number.kind_name()
196 ))
197 })?;
198 if !value.is_finite() {
199 return Err(EngineError::domain("value overflows float64"));
200 }
201 Ok(value)
202}
203
204pub fn scalar_f64(args: &Args, name: &str) -> Result<f64, EngineError> {
205 number_to_f64(args.number(name)?).map_err(|e| e.with_path(name.to_string()))
206}
207
208pub fn optional_f64_param(args: &Args, name: &str) -> Result<Option<f64>, EngineError> {
209 match args.optional_number(name)? {
210 None => Ok(None),
211 Some(number) => number_to_f64(number)
212 .map(Some)
213 .map_err(|e| e.with_path(name.to_string())),
214 }
215}
216
217pub fn confidence_param(args: &Args) -> Result<f64, EngineError> {
218 let value = optional_f64_param(args, "confidence")?.unwrap_or(0.95);
219 if !(0.0..1.0).contains(&value) {
220 return Err(
221 EngineError::domain("confidence must be strictly between 0 and 1")
222 .with_path("confidence".to_string()),
223 );
224 }
225 Ok(value)
226}
227
228pub fn alpha_param(args: &Args) -> Result<f64, EngineError> {
229 let value = optional_f64_param(args, "alpha")?.unwrap_or(0.05);
230 if !(0.0..1.0).contains(&value) {
231 return Err(
232 EngineError::domain("alpha must be strictly between 0 and 1")
233 .with_path("alpha".to_string()),
234 );
235 }
236 Ok(value)
237}
238
239pub fn power_param(args: &Args) -> Result<f64, EngineError> {
240 let value = optional_f64_param(args, "power")?.unwrap_or(0.8);
241 if !(0.0..1.0).contains(&value) {
242 return Err(
243 EngineError::domain("power must be strictly between 0 and 1")
244 .with_path("power".to_string()),
245 );
246 }
247 Ok(value)
248}
249
250pub fn allocation_ratio_param(args: &Args) -> Result<f64, EngineError> {
251 let value = optional_f64_param(args, "allocation_ratio")?.unwrap_or(1.0);
252 if value <= 0.0 {
253 return Err(EngineError::domain("allocation_ratio must be positive")
254 .with_path("allocation_ratio".to_string()));
255 }
256 Ok(value)
257}
258
259pub fn sided_param(args: &Args) -> Result<String, EngineError> {
260 parse_choice(args, "sided", "two", &["two", "one"])
261}
262
263pub fn parse_choice(
264 args: &Args,
265 name: &str,
266 default: &str,
267 variants: &[&str],
268) -> Result<String, EngineError> {
269 match args.optional_text(name)? {
270 None => Ok(default.to_string()),
271 Some(text) => {
272 if variants.contains(&text) {
273 Ok(text.to_string())
274 } else {
275 Err(EngineError::domain(format!(
276 "unknown {name} {text:?}; expected one of {variants:?}"
277 ))
278 .with_path(name.to_string()))
279 }
280 }
281 }
282}
283
284pub fn probability_param(args: &Args, name: &str, default: f64) -> Result<f64, EngineError> {
285 let value = optional_f64_param(args, name)?.unwrap_or(default);
286 if !(0.0..=1.0).contains(&value) {
287 return Err(
288 EngineError::domain(format!("{name} must be in [0, 1]")).with_path(name.to_string())
289 );
290 }
291 Ok(value)
292}
293
294pub fn non_negative_u64(value: &BigInt, name: &str) -> Result<u64, EngineError> {
295 if value.is_negative() {
296 return Err(
297 EngineError::domain(format!("{name} must be a non-negative integer"))
298 .with_path(name.to_string()),
299 );
300 }
301 value.to_u64().ok_or_else(|| {
302 EngineError::domain(format!("{name} is too large for the supported range"))
303 .with_path(name.to_string())
304 })
305}
306
307pub fn insufficient(message: impl Into<String>) -> EngineError {
308 EngineError::new(ErrorCode::InsufficientObservations, message)
309}
310
311pub fn collect_numbers(args: &Args, name: &str) -> Result<Vec<Number>, EngineError> {
316 let items = args.array(name)?;
317 let mut out = Vec::with_capacity(items.len());
318 for (index, item) in items.iter().enumerate() {
319 match item {
320 Value::Number(number) => {
321 ensure_finite(number).map_err(|e| e.with_path(format!("{name}[{index}]")))?;
322 out.push(number.clone());
323 }
324 other => {
325 return Err(EngineError::malformed(format!(
326 "expected a number at {name}[{index}], found {}",
327 other.kind_name()
328 ))
329 .with_path(format!("{name}[{index}]")));
330 }
331 }
332 }
333 Ok(out)
334}
335
336pub fn float_vector(args: &Args, name: &str) -> Result<Vec<f64>, EngineError> {
339 let numbers = collect_numbers(args, name)?;
340 if numbers.is_empty() {
341 return Err(insufficient(format!("{name} must not be empty")));
342 }
343 numbers
344 .iter()
345 .enumerate()
346 .map(|(index, number)| {
347 number_to_f64(number).map_err(|error| error.with_path(format!("{name}[{index}]")))
348 })
349 .collect()
350}
351
352#[derive(Clone, Debug)]
354pub enum Series {
355 Exact(Vec<BigRational>),
356 Float(Vec<f64>),
357}
358
359impl Series {
360 pub fn len(&self) -> usize {
361 match self {
362 Series::Exact(values) => values.len(),
363 Series::Float(values) => values.len(),
364 }
365 }
366
367 pub fn is_empty(&self) -> bool {
368 self.len() == 0
369 }
370
371 pub fn is_float(&self) -> bool {
372 matches!(self, Series::Float(_))
373 }
374
375 pub fn to_f64_vec(&self) -> Result<Vec<f64>, EngineError> {
378 match self {
379 Series::Float(values) => Ok(values.clone()),
380 Series::Exact(values) => values
381 .iter()
382 .map(|value| {
383 value
384 .to_f64()
385 .filter(|v| v.is_finite())
386 .ok_or_else(|| EngineError::domain("value overflows float64"))
387 })
388 .collect(),
389 }
390 }
391}
392
393pub fn classify_series(args: &Args, name: &str, ctx: &ExecContext) -> Result<Series, EngineError> {
398 let values = collect_numbers(args, name)?;
399 let has_float = values.iter().any(Number::is_float);
400 if has_float {
401 if ctx.numeric.mode == NumericMode::Exact {
402 return Err(EngineError::new(
403 ErrorCode::UnsupportedNumericMode,
404 format!("{name} contains float64 values, which require auto or scientific mode"),
405 )
406 .with_path(name.to_string()));
407 }
408 let mut out = Vec::with_capacity(values.len());
409 for (index, value) in values.iter().enumerate() {
410 let float =
411 number_to_f64(value).map_err(|e| e.with_path(format!("{name}[{index}]")))?;
412 out.push(float);
413 }
414 Ok(Series::Float(out))
415 } else {
416 let mut out = Vec::with_capacity(values.len());
417 for (index, value) in values.iter().enumerate() {
418 let rational = value.as_exact_rational().ok_or_else(|| {
419 EngineError::internal("exact value could not be converted to a rational")
420 .with_path(format!("{name}[{index}]"))
421 })?;
422 out.push(rational);
423 }
424 Ok(Series::Exact(out))
425 }
426}
427
428pub fn mean_exact(values: &[BigRational]) -> Result<BigRational, EngineError> {
433 if values.is_empty() {
434 return Err(insufficient("mean requires at least one observation"));
435 }
436 let n = BigInt::from(values.len());
437 let sum: BigRational = values.iter().cloned().sum();
438 Ok(sum / BigRational::from_integer(n))
439}
440
441pub fn variance_exact(values: &[BigRational], ddof: u64) -> Result<BigRational, EngineError> {
442 let n = values.len() as u64;
443 if values.is_empty() {
444 return Err(insufficient("variance requires at least one observation"));
445 }
446 if n <= ddof {
447 return Err(insufficient(format!(
448 "variance with ddof={ddof} requires more than {ddof} observations"
449 )));
450 }
451 let mean = mean_exact(values)?;
452 let mut sum_squares = BigRational::zero();
453 for value in values {
454 let delta = value - &mean;
455 sum_squares += &delta * δ
456 }
457 let denominator = BigRational::from_integer(BigInt::from(n - ddof));
458 Ok(sum_squares / denominator)
459}
460
461pub fn covariance_exact(
462 xs: &[BigRational],
463 ys: &[BigRational],
464 ddof: u64,
465) -> Result<BigRational, EngineError> {
466 let n = xs.len() as u64;
467 if xs.is_empty() {
468 return Err(insufficient("covariance requires at least one observation"));
469 }
470 if n <= ddof {
471 return Err(insufficient(format!(
472 "covariance with ddof={ddof} requires more than {ddof} observations"
473 )));
474 }
475 let mean_x = mean_exact(xs)?;
476 let mean_y = mean_exact(ys)?;
477 let mut sum = BigRational::zero();
478 for (x, y) in xs.iter().zip(ys.iter()) {
479 sum += (x - &mean_x) * (y - &mean_y);
480 }
481 let denominator = BigRational::from_integer(BigInt::from(n - ddof));
482 Ok(sum / denominator)
483}
484
485pub fn rational_sqrt_exact(value: &BigRational) -> Option<BigRational> {
487 if value.is_negative() {
488 return None;
489 }
490 let numer = value.numer().sqrt();
491 let denom = value.denom().sqrt();
492 if &numer * &numer == *value.numer() && &denom * &denom == *value.denom() {
493 Some(BigRational::new(numer, denom))
494 } else {
495 None
496 }
497}
498
499pub fn compensated_sum(values: &[f64]) -> f64 {
505 let mut sum = 0.0f64;
506 let mut compensation = 0.0f64;
507 for &value in values {
508 let next = sum + value;
509 if sum.abs() >= value.abs() {
510 compensation += (sum - next) + value;
511 } else {
512 compensation += (value - next) + sum;
513 }
514 sum = next;
515 }
516 sum + compensation
517}
518
519pub fn mean_f64(values: &[f64]) -> f64 {
520 if values.is_empty() {
521 return f64::NAN;
522 }
523 compensated_sum(values) / values.len() as f64
524}
525
526pub fn variance_f64(values: &[f64], ddof: u64) -> Result<f64, EngineError> {
528 let n = values.len() as u64;
529 if values.is_empty() {
530 return Err(insufficient("variance requires at least one observation"));
531 }
532 if n <= ddof {
533 return Err(insufficient(format!(
534 "variance with ddof={ddof} requires more than {ddof} observations"
535 )));
536 }
537 let mut count = 0u64;
538 let mut mean = 0.0f64;
539 let mut m2 = 0.0f64;
540 for &value in values {
541 count += 1;
542 let delta = value - mean;
543 mean += delta / count as f64;
544 let delta2 = value - mean;
545 m2 += delta * delta2;
546 }
547 Ok(m2 / (n - ddof) as f64)
548}
549
550pub fn covariance_f64(xs: &[f64], ys: &[f64], ddof: u64) -> Result<f64, EngineError> {
552 let n = xs.len() as u64;
553 if xs.is_empty() {
554 return Err(insufficient("covariance requires at least one observation"));
555 }
556 if n <= ddof {
557 return Err(insufficient(format!(
558 "covariance with ddof={ddof} requires more than {ddof} observations"
559 )));
560 }
561 let mean_x = mean_f64(xs);
562 let mean_y = mean_f64(ys);
563 let mut sum = 0.0f64;
564 let mut compensation = 0.0f64;
565 for (x, y) in xs.iter().zip(ys.iter()) {
566 let term = (x - mean_x) * (y - mean_y);
567 let adjusted = term - compensation;
568 let next = sum + adjusted;
569 compensation = (next - sum) - adjusted;
570 sum = next;
571 }
572 Ok(sum / (n - ddof) as f64)
573}
574
575#[derive(Clone, Debug)]
581pub enum Moment {
582 Exact(BigRational),
583 Float(f64),
584}
585
586pub fn mean_moment(series: &Series) -> Result<Moment, EngineError> {
587 match series {
588 Series::Exact(values) => Ok(Moment::Exact(mean_exact(values)?)),
589 Series::Float(values) => {
590 if values.is_empty() {
591 return Err(insufficient("mean requires at least one observation"));
592 }
593 Ok(Moment::Float(mean_f64(values)))
594 }
595 }
596}
597
598pub fn variance_moment(series: &Series, ddof: u64) -> Result<Moment, EngineError> {
599 match series {
600 Series::Exact(values) => Ok(Moment::Exact(variance_exact(values, ddof)?)),
601 Series::Float(values) => Ok(Moment::Float(variance_f64(values, ddof)?)),
602 }
603}
604
605pub fn covariance_moment(xs: &Series, ys: &Series, ddof: u64) -> Result<Moment, EngineError> {
606 match (xs, ys) {
607 (Series::Exact(xs), Series::Exact(ys)) => {
608 Ok(Moment::Exact(covariance_exact(xs, ys, ddof)?))
609 }
610 (Series::Float(xs), Series::Float(ys)) => Ok(Moment::Float(covariance_f64(xs, ys, ddof)?)),
611 _ => Err(EngineError::internal(
612 "mixed exact/float series reached covariance",
613 )),
614 }
615}
616
617pub fn stddev_moment(
621 moment: &Moment,
622 mode: NumericMode,
623) -> Result<(Number, Exactness), EngineError> {
624 match moment {
625 Moment::Exact(value) => {
626 if value.is_negative() {
627 return Err(EngineError::internal("negative exact variance"));
628 }
629 if let Some(root) = rational_sqrt_exact(value) {
630 return Ok((rational_to_number(root), Exactness::Exact));
631 }
632 match mode {
633 NumericMode::Exact => Err(EngineError::new(
634 ErrorCode::UnsupportedNumericMode,
635 "variance is not a perfect square; exact mode cannot represent the \
636 standard deviation; use auto or scientific mode",
637 )),
638 NumericMode::Auto => {
639 let approximate = value.to_f64().ok_or_else(|| {
640 EngineError::domain("variance is not representable as float64")
641 })?;
642 let root = sqrt(approximate);
643 let decimal = Decimal::from_f64_display(root).ok_or_else(|| {
644 EngineError::internal("could not format decimal approximation")
645 })?;
646 Ok((Number::Decimal(decimal), Exactness::Approximate))
647 }
648 NumericMode::Scientific => {
649 let approximate = value.to_f64().ok_or_else(|| {
650 EngineError::domain("variance is not representable as float64")
651 })?;
652 Ok((float_number(sqrt(approximate))?, Exactness::Approximate))
653 }
654 }
655 }
656 Moment::Float(value) => {
657 if value.is_nan() || *value < 0.0 {
658 return Err(EngineError::domain("negative float64 variance"));
659 }
660 Ok((float_number(sqrt(*value))?, Exactness::Approximate))
661 }
662 }
663}
664
665pub fn extreme_number(series: &Series, minimum: bool) -> Result<(Number, Exactness), EngineError> {
670 if series.is_empty() {
671 return Err(insufficient(
672 "minimum and maximum require at least one observation",
673 ));
674 }
675 match series {
676 Series::Exact(values) => {
677 let mut best = values[0].clone();
678 for value in &values[1..] {
679 if (minimum && value < &best) || (!minimum && value > &best) {
680 best = value.clone();
681 }
682 }
683 Ok((rational_to_number(best), Exactness::Exact))
684 }
685 Series::Float(values) => {
686 let mut best = values[0];
687 for &value in &values[1..] {
688 if (minimum && value < best) || (!minimum && value > best) {
689 best = value;
690 }
691 }
692 Ok((float_number(best)?, Exactness::Approximate))
693 }
694 }
695}
696
697pub fn median_number(series: &Series) -> Result<(Number, Exactness), EngineError> {
698 if series.is_empty() {
699 return Err(insufficient("median requires at least one observation"));
700 }
701 match series {
702 Series::Exact(values) => {
703 let mut sorted = values.clone();
704 sorted.sort();
705 let n = sorted.len();
706 let median = if n % 2 == 1 {
707 sorted[n / 2].clone()
708 } else {
709 (sorted[n / 2 - 1].clone() + sorted[n / 2].clone())
710 / BigRational::from_integer(BigInt::from(2))
711 };
712 Ok((rational_to_number(median), Exactness::Exact))
713 }
714 Series::Float(values) => {
715 let mut sorted = values.clone();
716 sorted.sort_by(|a, b| a.total_cmp(b));
717 let n = sorted.len();
718 let median = if n % 2 == 1 {
719 sorted[n / 2]
720 } else {
721 0.5 * (sorted[n / 2 - 1] + sorted[n / 2])
722 };
723 Ok((float_number(median)?, Exactness::Approximate))
724 }
725 }
726}
727
728pub fn quantile_number(
737 series: &Series,
738 q: &Number,
739 method: &str,
740) -> Result<(Number, Exactness), EngineError> {
741 if series.is_empty() {
742 return Err(insufficient("quantile requires at least one observation"));
743 }
744 let use_float = series.is_float() || q.is_float();
745 if use_float {
746 let p = number_to_f64(q)?;
747 if !(0.0..=1.0).contains(&p) {
748 return Err(EngineError::domain("q must be in [0, 1]"));
749 }
750 let mut sorted: Vec<f64> = match series {
751 Series::Float(values) => values.clone(),
752 Series::Exact(values) => values
753 .iter()
754 .map(|value| {
755 value
756 .to_f64()
757 .filter(|v| v.is_finite())
758 .ok_or_else(|| EngineError::domain("value overflows float64"))
759 })
760 .collect::<Result<_, _>>()?,
761 };
762 sorted.sort_by(|a, b| a.total_cmp(b));
763 let n = sorted.len();
764 let h = (n - 1) as f64 * p;
765 let lower = h.floor() as usize;
766 let fraction = h - lower as f64;
767 let upper = if fraction == 0.0 {
768 lower
769 } else {
770 (lower + 1).min(n - 1)
771 };
772 let result = match method {
773 "linear" => sorted[lower] + fraction * (sorted[upper] - sorted[lower]),
774 "lower" => sorted[lower],
775 "higher" => sorted[upper],
776 "midpoint" => 0.5 * (sorted[lower] + sorted[upper]),
777 "nearest" => {
778 let index = h.round_ties_even() as usize;
779 sorted[index.min(n - 1)]
780 }
781 other => {
782 return Err(EngineError::domain(format!(
783 "unknown quantile method {other:?}"
784 )));
785 }
786 };
787 return Ok((float_number(result)?, Exactness::Approximate));
788 }
789 let p = q
790 .as_exact_rational()
791 .ok_or_else(|| EngineError::internal("exact q could not be converted to a rational"))?;
792 if p.is_negative() || p > BigRational::one() {
793 return Err(EngineError::domain("q must be in [0, 1]"));
794 }
795 let values = match series {
796 Series::Exact(values) => values,
797 Series::Float(_) => {
798 return Err(EngineError::internal(
799 "float series reached the exact quantile path",
800 ));
801 }
802 };
803 let mut sorted = values.clone();
804 sorted.sort();
805 let n = sorted.len();
806 let h = BigRational::from_integer(BigInt::from(n - 1)) * &p;
807 let lower = h
808 .floor()
809 .to_integer()
810 .to_usize()
811 .ok_or_else(|| EngineError::internal("quantile index out of range"))?;
812 let fraction = &h - BigRational::from_integer(h.floor().to_integer());
813 let upper = if fraction.is_zero() {
814 lower
815 } else {
816 (lower + 1).min(n - 1)
817 };
818 let result = match method {
819 "linear" => &sorted[lower] + &fraction * (&sorted[upper] - &sorted[lower]),
820 "lower" => sorted[lower].clone(),
821 "higher" => sorted[upper].clone(),
822 "midpoint" => {
823 (&sorted[lower] + &sorted[upper]) / BigRational::from_integer(BigInt::from(2))
824 }
825 "nearest" => {
826 let twice = &fraction * BigRational::from_integer(BigInt::from(2));
827 let index = match twice.cmp(&BigRational::one()) {
828 std::cmp::Ordering::Less => lower,
829 std::cmp::Ordering::Greater => upper,
830 std::cmp::Ordering::Equal => {
831 if lower % 2 == 0 {
832 lower
833 } else {
834 upper
835 }
836 }
837 };
838 sorted[index.min(n - 1)].clone()
839 }
840 other => {
841 return Err(EngineError::domain(format!(
842 "unknown quantile method {other:?}"
843 )));
844 }
845 };
846 Ok((rational_to_number(result), Exactness::Exact))
847}
848
849pub fn mode_numbers(series: &Series) -> Result<(Vec<Number>, Vec<u64>), EngineError> {
851 if series.is_empty() {
852 return Err(insufficient("mode requires at least one observation"));
853 }
854 match series {
855 Series::Exact(values) => {
856 let mut counts: BTreeMap<BigRational, u64> = BTreeMap::new();
857 for value in values {
858 *counts.entry(value.clone()).or_insert(0) += 1;
859 }
860 let max = counts.values().copied().max().unwrap_or(0);
861 let mut modes = Vec::new();
862 let mut mode_counts = Vec::new();
863 for (value, count) in counts {
864 if count == max {
865 modes.push(rational_to_number(value));
866 mode_counts.push(count);
867 }
868 }
869 Ok((modes, mode_counts))
870 }
871 Series::Float(values) => {
872 let mut sorted: Vec<f64> = values
873 .iter()
874 .map(|value| if *value == 0.0 { 0.0 } else { *value })
875 .collect();
876 sorted.sort_by(|a, b| a.total_cmp(b));
877 let mut groups: Vec<(f64, u64)> = Vec::new();
878 for value in sorted {
879 match groups.last_mut() {
880 Some(last) if last.0 == value => last.1 += 1,
881 _ => groups.push((value, 1)),
882 }
883 }
884 let max = groups.iter().map(|group| group.1).max().unwrap_or(0);
885 let mut modes = Vec::new();
886 let mut mode_counts = Vec::new();
887 for (value, count) in groups {
888 if count == max {
889 modes.push(float_number(value)?);
890 mode_counts.push(count);
891 }
892 }
893 Ok((modes, mode_counts))
894 }
895 }
896}