1use crate::ai_evaluator::{FraudCheckRequest, VerificationRequest};
7use crate::error::{AiError, Result};
8
9pub struct VerificationRequestBuilder {
11 title: String,
12 description: Option<String>,
13 deadline: String,
14 evidence_url: String,
15 evidence_description: Option<String>,
16}
17
18impl VerificationRequestBuilder {
19 pub fn new(title: impl Into<String>, evidence_url: impl Into<String>) -> Self {
21 Self {
22 title: title.into(),
23 description: None,
24 deadline: String::new(),
25 evidence_url: evidence_url.into(),
26 evidence_description: None,
27 }
28 }
29
30 #[must_use]
32 pub fn description(mut self, desc: impl Into<String>) -> Self {
33 self.description = Some(desc.into());
34 self
35 }
36
37 #[must_use]
39 pub fn deadline(mut self, deadline: impl Into<String>) -> Self {
40 self.deadline = deadline.into();
41 self
42 }
43
44 #[must_use]
46 pub fn evidence_description(mut self, desc: impl Into<String>) -> Self {
47 self.evidence_description = Some(desc.into());
48 self
49 }
50
51 #[must_use]
53 pub fn build(self) -> VerificationRequest {
54 VerificationRequest {
55 commitment_title: self.title,
56 commitment_description: self.description,
57 deadline: self.deadline,
58 evidence_url: self.evidence_url,
59 evidence_description: self.evidence_description,
60 }
61 }
62}
63
64pub struct FraudCheckRequestBuilder {
66 content_type: String,
67 content: String,
68 commitments_made: i32,
69 commitments_fulfilled: i32,
70 avg_quality_score: Option<f64>,
71}
72
73impl FraudCheckRequestBuilder {
74 pub fn new(content_type: impl Into<String>, content: impl Into<String>) -> Self {
76 Self {
77 content_type: content_type.into(),
78 content: content.into(),
79 commitments_made: 0,
80 commitments_fulfilled: 0,
81 avg_quality_score: None,
82 }
83 }
84
85 #[must_use]
87 pub fn commitments_made(mut self, count: i32) -> Self {
88 self.commitments_made = count;
89 self
90 }
91
92 #[must_use]
94 pub fn commitments_fulfilled(mut self, count: i32) -> Self {
95 self.commitments_fulfilled = count;
96 self
97 }
98
99 #[must_use]
101 pub fn avg_quality_score(mut self, score: f64) -> Self {
102 self.avg_quality_score = Some(score);
103 self
104 }
105
106 #[must_use]
108 pub fn build(self) -> FraudCheckRequest {
109 FraudCheckRequest {
110 content_type: self.content_type,
111 content: self.content,
112 commitments_made: self.commitments_made,
113 commitments_fulfilled: self.commitments_fulfilled,
114 avg_quality_score: self.avg_quality_score,
115 }
116 }
117}
118
119pub fn validate_url(url: &str) -> Result<()> {
121 if url.is_empty() {
122 return Err(AiError::InvalidInput("URL cannot be empty".to_string()));
123 }
124
125 if !url.starts_with("http://") && !url.starts_with("https://") {
126 return Err(AiError::InvalidInput(
127 "URL must start with http:// or https://".to_string(),
128 ));
129 }
130
131 Ok(())
132}
133
134pub fn validate_confidence(score: f64) -> Result<()> {
136 if !(0.0..=100.0).contains(&score) {
137 return Err(AiError::InvalidInput(format!(
138 "Confidence score must be between 0 and 100, got {score}"
139 )));
140 }
141 Ok(())
142}
143
144pub fn validate_quality_score(score: f64) -> Result<()> {
146 if !(0.0..=100.0).contains(&score) {
147 return Err(AiError::InvalidInput(format!(
148 "Quality score must be between 0 and 100, got {score}"
149 )));
150 }
151 Ok(())
152}
153
154#[must_use]
156pub fn calculate_success_rate(successes: usize, total: usize) -> f64 {
157 if total == 0 {
158 return 0.0;
159 }
160 successes as f64 / total as f64
161}
162
163#[must_use]
165pub fn format_duration(duration: std::time::Duration) -> String {
166 let secs = duration.as_secs();
167 if secs < 60 {
168 format!("{secs}s")
169 } else if secs < 3600 {
170 format!("{}m {}s", secs / 60, secs % 60)
171 } else {
172 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
173 }
174}
175
176#[must_use]
178pub fn format_cost(cost: f64) -> String {
179 if cost < 0.01 {
180 format!("${cost:.6}")
181 } else if cost < 1.0 {
182 format!("${cost:.4}")
183 } else {
184 format!("${cost:.2}")
185 }
186}
187
188#[must_use]
190pub fn calculate_average(values: &[f64]) -> f64 {
191 if values.is_empty() {
192 return 0.0;
193 }
194 values.iter().sum::<f64>() / values.len() as f64
195}
196
197#[must_use]
199pub fn calculate_median(values: &[f64]) -> f64 {
200 if values.is_empty() {
201 return 0.0;
202 }
203
204 let mut sorted = values.to_vec();
205 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
206
207 let mid = sorted.len() / 2;
208 if sorted.len() % 2 == 0 {
209 f64::midpoint(sorted[mid - 1], sorted[mid])
210 } else {
211 sorted[mid]
212 }
213}
214
215#[must_use]
217pub fn calculate_std_dev(values: &[f64]) -> f64 {
218 if values.is_empty() {
219 return 0.0;
220 }
221
222 let avg = calculate_average(values);
223 let variance = values
224 .iter()
225 .map(|v| {
226 let diff = v - avg;
227 diff * diff
228 })
229 .sum::<f64>()
230 / values.len() as f64;
231
232 variance.sqrt()
233}
234
235pub fn clamp<T: PartialOrd>(value: T, min: T, max: T) -> T {
237 if value < min {
238 min
239 } else if value > max {
240 max
241 } else {
242 value
243 }
244}
245
246#[must_use]
248pub fn normalize_score(score: f64, from_min: f64, from_max: f64, to_min: f64, to_max: f64) -> f64 {
249 let normalized = (score - from_min) / (from_max - from_min);
250 to_min + normalized * (to_max - to_min)
251}
252
253#[must_use]
255pub fn is_passing_score(score: f64) -> bool {
256 score >= 70.0
257}
258
259#[must_use]
261pub fn is_excellent_score(score: f64) -> bool {
262 score >= 90.0
263}
264
265#[must_use]
267pub fn confidence_to_risk_level(confidence: f64) -> &'static str {
268 if confidence >= 90.0 {
269 "Very Low Risk"
270 } else if confidence >= 75.0 {
271 "Low Risk"
272 } else if confidence >= 60.0 {
273 "Medium Risk"
274 } else if confidence >= 40.0 {
275 "High Risk"
276 } else {
277 "Very High Risk"
278 }
279}
280
281pub async fn retry_with_exponential_backoff<F, Fut, T, E>(
300 max_retries: u32,
301 initial_delay: std::time::Duration,
302 mut f: F,
303) -> std::result::Result<T, E>
304where
305 F: FnMut() -> Fut,
306 Fut: std::future::Future<Output = std::result::Result<T, E>>,
307{
308 let mut delay = initial_delay;
309 let mut attempts = 0;
310
311 loop {
312 match f().await {
313 Ok(result) => return Ok(result),
314 Err(e) => {
315 attempts += 1;
316 if attempts >= max_retries {
317 return Err(e);
318 }
319 tokio::time::sleep(delay).await;
320 delay *= 2; }
322 }
323 }
324}
325
326#[must_use]
337pub fn calculate_percentile(values: &[f64], percentile: f64) -> f64 {
338 if values.is_empty() {
339 return 0.0;
340 }
341
342 let clamped_percentile = clamp(percentile, 0.0, 100.0);
343 let mut sorted = values.to_vec();
344 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
345
346 let index = (clamped_percentile / 100.0 * (sorted.len() - 1) as f64).round() as usize;
347 sorted[index.min(sorted.len() - 1)]
348}
349
350#[must_use]
359pub fn calculate_weighted_average(values: &[f64], weights: &[f64]) -> f64 {
360 if values.is_empty() || weights.is_empty() || values.len() != weights.len() {
361 return 0.0;
362 }
363
364 let total_weight: f64 = weights.iter().sum();
365 if total_weight == 0.0 {
366 return 0.0;
367 }
368
369 values
370 .iter()
371 .zip(weights.iter())
372 .map(|(v, w)| v * w)
373 .sum::<f64>()
374 / total_weight
375}
376
377#[must_use]
379pub fn calculate_variance(values: &[f64]) -> f64 {
380 if values.is_empty() {
381 return 0.0;
382 }
383
384 let avg = calculate_average(values);
385 values
386 .iter()
387 .map(|v| {
388 let diff = v - avg;
389 diff * diff
390 })
391 .sum::<f64>()
392 / values.len() as f64
393}
394
395#[must_use]
398pub fn calculate_coefficient_of_variation(values: &[f64]) -> f64 {
399 if values.is_empty() {
400 return 0.0;
401 }
402
403 let avg = calculate_average(values);
404 if avg == 0.0 {
405 return 0.0;
406 }
407
408 let std_dev = calculate_std_dev(values);
409 (std_dev / avg) * 100.0
410}
411
412#[must_use]
416pub fn format_tokens(count: usize) -> String {
417 if count < 1000 {
418 format!("{count} tokens")
419 } else if count < 1_000_000 {
420 format!("{:.1}K tokens", count as f64 / 1000.0)
421 } else {
422 format!("{:.1}M tokens", count as f64 / 1_000_000.0)
423 }
424}
425
426#[must_use]
428pub fn format_file_size(bytes: u64) -> String {
429 const KB: u64 = 1024;
430 const MB: u64 = KB * 1024;
431 const GB: u64 = MB * 1024;
432
433 if bytes < KB {
434 format!("{bytes} B")
435 } else if bytes < MB {
436 format!("{:.2} KB", bytes as f64 / KB as f64)
437 } else if bytes < GB {
438 format!("{:.2} MB", bytes as f64 / MB as f64)
439 } else {
440 format!("{:.2} GB", bytes as f64 / GB as f64)
441 }
442}
443
444#[must_use]
446pub fn format_percentage(value: f64) -> String {
447 if value < 1.0 {
448 format!("{value:.2}%")
449 } else if value < 10.0 {
450 format!("{value:.1}%")
451 } else {
452 format!("{value:.0}%")
453 }
454}
455
456pub fn validate_token_count(count: usize, max_tokens: usize) -> Result<()> {
460 if count == 0 {
461 return Err(AiError::InvalidInput(
462 "Token count cannot be zero".to_string(),
463 ));
464 }
465
466 if count > max_tokens {
467 return Err(AiError::InvalidInput(format!(
468 "Token count {count} exceeds maximum of {max_tokens}"
469 )));
470 }
471
472 Ok(())
473}
474
475pub fn validate_temperature(temperature: f64) -> Result<()> {
477 if !(0.0..=2.0).contains(&temperature) {
478 return Err(AiError::InvalidInput(format!(
479 "Temperature must be between 0.0 and 2.0, got {temperature}"
480 )));
481 }
482 Ok(())
483}
484
485pub fn validate_model_name(model: &str) -> Result<()> {
487 if model.trim().is_empty() {
488 return Err(AiError::InvalidInput(
489 "Model name cannot be empty".to_string(),
490 ));
491 }
492 Ok(())
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum AggregationStrategy {
500 Average,
502 Median,
504 Minimum,
506 Maximum,
508 Weighted,
510}
511
512pub fn aggregate_scores(
514 scores: &[f64],
515 strategy: AggregationStrategy,
516 weights: Option<&[f64]>,
517) -> f64 {
518 if scores.is_empty() {
519 return 0.0;
520 }
521
522 match strategy {
523 AggregationStrategy::Average => calculate_average(scores),
524 AggregationStrategy::Median => calculate_median(scores),
525 AggregationStrategy::Minimum => scores.iter().copied().fold(f64::INFINITY, f64::min),
526 AggregationStrategy::Maximum => scores.iter().copied().fold(f64::NEG_INFINITY, f64::max),
527 AggregationStrategy::Weighted => {
528 if let Some(w) = weights {
529 calculate_weighted_average(scores, w)
530 } else {
531 calculate_average(scores)
532 }
533 }
534 }
535}
536
537#[must_use]
540pub fn combine_quality_originality(quality: f64, originality: f64) -> f64 {
541 quality * 0.6 + originality * 0.4
542}
543
544#[must_use]
547pub fn calculate_consensus(scores: &[f64]) -> (f64, f64) {
548 if scores.is_empty() {
549 return (0.0, 0.0);
550 }
551
552 let avg = calculate_average(scores);
553 let std_dev = calculate_std_dev(scores);
554
555 let confidence = if std_dev < 5.0 {
558 100.0
559 } else if std_dev < 10.0 {
560 90.0 - (std_dev - 5.0) * 4.0
561 } else if std_dev < 20.0 {
562 70.0 - (std_dev - 10.0) * 2.0
563 } else {
564 clamp(50.0 - (std_dev - 20.0), 0.0, 50.0)
565 };
566
567 (avg, confidence)
568}
569
570#[must_use]
574pub fn score_difference_percent(score1: f64, score2: f64) -> f64 {
575 if score2 == 0.0 {
576 return 0.0;
577 }
578 ((score1 - score2) / score2) * 100.0
579}
580
581#[must_use]
583pub fn scores_significantly_different(score1: f64, score2: f64) -> bool {
584 let diff_percent = score_difference_percent(score1, score2).abs();
585 diff_percent > 10.0
586}
587
588#[must_use]
590pub fn score_to_grade(score: f64) -> char {
591 if score >= 90.0 {
592 'A'
593 } else if score >= 80.0 {
594 'B'
595 } else if score >= 70.0 {
596 'C'
597 } else if score >= 60.0 {
598 'D'
599 } else {
600 'F'
601 }
602}
603
604#[must_use]
606pub fn score_to_tier(score: f64) -> &'static str {
607 if score >= 95.0 {
608 "Exceptional"
609 } else if score >= 85.0 {
610 "Excellent"
611 } else if score >= 75.0 {
612 "Good"
613 } else if score >= 65.0 {
614 "Fair"
615 } else if score >= 50.0 {
616 "Poor"
617 } else {
618 "Very Poor"
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn test_verification_request_builder() {
628 let request = VerificationRequestBuilder::new("Test Commitment", "https://example.com")
629 .description("Test description")
630 .deadline("2024-12-31")
631 .evidence_description("Evidence desc")
632 .build();
633
634 assert_eq!(request.commitment_title, "Test Commitment");
635 assert_eq!(
636 request.commitment_description,
637 Some("Test description".to_string())
638 );
639 assert_eq!(request.deadline, "2024-12-31");
640 assert_eq!(request.evidence_url, "https://example.com");
641 }
642
643 #[test]
644 fn test_fraud_check_request_builder() {
645 let request = FraudCheckRequestBuilder::new("Test Type", "Test Content")
646 .commitments_made(10)
647 .commitments_fulfilled(8)
648 .avg_quality_score(85.0)
649 .build();
650
651 assert_eq!(request.content_type, "Test Type");
652 assert_eq!(request.commitments_made, 10);
653 assert_eq!(request.commitments_fulfilled, 8);
654 assert_eq!(request.avg_quality_score, Some(85.0));
655 }
656
657 #[test]
658 fn test_validate_url() {
659 assert!(validate_url("https://example.com").is_ok());
660 assert!(validate_url("http://example.com").is_ok());
661 assert!(validate_url("").is_err());
662 assert!(validate_url("example.com").is_err());
663 }
664
665 #[test]
666 fn test_validate_confidence() {
667 assert!(validate_confidence(50.0).is_ok());
668 assert!(validate_confidence(0.0).is_ok());
669 assert!(validate_confidence(100.0).is_ok());
670 assert!(validate_confidence(-1.0).is_err());
671 assert!(validate_confidence(101.0).is_err());
672 }
673
674 #[test]
675 fn test_calculate_success_rate() {
676 assert!((calculate_success_rate(7, 10) - 0.7).abs() < 1e-10);
677 assert!((calculate_success_rate(10, 10) - 1.0).abs() < 1e-10);
678 assert!((calculate_success_rate(0, 10)).abs() < 1e-10);
679 assert!((calculate_success_rate(0, 0)).abs() < 1e-10);
680 }
681
682 #[test]
683 fn test_format_duration() {
684 assert_eq!(format_duration(std::time::Duration::from_secs(30)), "30s");
685 assert_eq!(
686 format_duration(std::time::Duration::from_secs(90)),
687 "1m 30s"
688 );
689 assert_eq!(
690 format_duration(std::time::Duration::from_secs(3661)),
691 "1h 1m"
692 );
693 }
694
695 #[test]
696 fn test_format_cost() {
697 assert_eq!(format_cost(0.001), "$0.001000");
698 assert_eq!(format_cost(0.05), "$0.0500");
699 assert_eq!(format_cost(1.50), "$1.50");
700 }
701
702 #[test]
703 fn test_calculate_average() {
704 assert!((calculate_average(&[1.0, 2.0, 3.0]) - 2.0).abs() < 1e-10);
705 assert!((calculate_average(&[])).abs() < 1e-10);
706 assert!((calculate_average(&[5.0]) - 5.0).abs() < 1e-10);
707 }
708
709 #[test]
710 fn test_calculate_median() {
711 assert!((calculate_median(&[1.0, 2.0, 3.0]) - 2.0).abs() < 1e-10);
712 assert!((calculate_median(&[1.0, 2.0, 3.0, 4.0]) - 2.5).abs() < 1e-10);
713 assert!((calculate_median(&[])).abs() < 1e-10);
714 }
715
716 #[test]
717 fn test_calculate_std_dev() {
718 let values = vec![2.0, 4.0, 6.0, 8.0];
719 let std_dev = calculate_std_dev(&values);
720 assert!((std_dev - 2.236).abs() < 0.01);
721 }
722
723 #[test]
724 fn test_clamp() {
725 assert_eq!(clamp(5, 0, 10), 5);
726 assert_eq!(clamp(-5, 0, 10), 0);
727 assert_eq!(clamp(15, 0, 10), 10);
728 }
729
730 #[test]
731 fn test_normalize_score() {
732 assert!((normalize_score(50.0, 0.0, 100.0, 0.0, 1.0) - 0.5).abs() < 1e-10);
733 assert!((normalize_score(0.0, 0.0, 100.0, 0.0, 1.0)).abs() < 1e-10);
734 assert!((normalize_score(100.0, 0.0, 100.0, 0.0, 1.0) - 1.0).abs() < 1e-10);
735 }
736
737 #[test]
738 fn test_is_passing_score() {
739 assert!(is_passing_score(70.0));
740 assert!(is_passing_score(85.0));
741 assert!(!is_passing_score(69.9));
742 }
743
744 #[test]
745 fn test_is_excellent_score() {
746 assert!(is_excellent_score(90.0));
747 assert!(is_excellent_score(95.0));
748 assert!(!is_excellent_score(89.9));
749 }
750
751 #[test]
752 fn test_confidence_to_risk_level() {
753 assert_eq!(confidence_to_risk_level(95.0), "Very Low Risk");
754 assert_eq!(confidence_to_risk_level(80.0), "Low Risk");
755 assert_eq!(confidence_to_risk_level(65.0), "Medium Risk");
756 assert_eq!(confidence_to_risk_level(50.0), "High Risk");
757 assert_eq!(confidence_to_risk_level(30.0), "Very High Risk");
758 }
759
760 #[test]
763 fn test_calculate_percentile() {
764 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
765 assert!((calculate_percentile(&values, 0.0) - 1.0).abs() < 1e-10);
766 assert!((calculate_percentile(&values, 50.0) - 3.0).abs() < 1e-10);
767 assert!((calculate_percentile(&values, 100.0) - 5.0).abs() < 1e-10);
768 assert!((calculate_percentile(&[], 50.0)).abs() < 1e-10);
769 }
770
771 #[test]
772 fn test_calculate_weighted_average() {
773 let values = vec![80.0, 90.0, 70.0];
774 let weights = vec![0.5, 0.3, 0.2];
775 let weighted_avg = calculate_weighted_average(&values, &weights);
776 assert!((weighted_avg - 81.0).abs() < 1e-10);
777
778 assert!((calculate_weighted_average(&values, &[0.5, 0.5])).abs() < 1e-10);
780
781 assert!((calculate_weighted_average(&[], &[])).abs() < 1e-10);
783
784 assert!((calculate_weighted_average(&values, &[0.0, 0.0, 0.0])).abs() < 1e-10);
786 }
787
788 #[test]
789 fn test_calculate_variance() {
790 let values = vec![2.0, 4.0, 6.0, 8.0];
791 let variance = calculate_variance(&values);
792 assert!((variance - 5.0).abs() < 1e-10);
793
794 assert!((calculate_variance(&[])).abs() < 1e-10);
795 }
796
797 #[test]
798 fn test_calculate_coefficient_of_variation() {
799 let values = vec![10.0, 12.0, 14.0, 16.0];
800 let cv = calculate_coefficient_of_variation(&values);
801 assert!(cv > 0.0 && cv < 100.0);
802
803 assert!((calculate_coefficient_of_variation(&[])).abs() < 1e-10);
805
806 assert!((calculate_coefficient_of_variation(&[0.0, 0.0, 0.0])).abs() < 1e-10);
808 }
809
810 #[test]
813 fn test_format_tokens() {
814 assert_eq!(format_tokens(500), "500 tokens");
815 assert_eq!(format_tokens(1500), "1.5K tokens");
816 assert_eq!(format_tokens(1_500_000), "1.5M tokens");
817 }
818
819 #[test]
820 fn test_format_file_size() {
821 assert_eq!(format_file_size(500), "500 B");
822 assert_eq!(format_file_size(1536), "1.50 KB");
823 assert_eq!(format_file_size(1_572_864), "1.50 MB");
824 assert_eq!(format_file_size(1_610_612_736), "1.50 GB");
825 }
826
827 #[test]
828 fn test_format_percentage() {
829 assert_eq!(format_percentage(0.5), "0.50%");
830 assert_eq!(format_percentage(5.5), "5.5%");
831 assert_eq!(format_percentage(55.5), "56%");
832 }
833
834 #[test]
837 fn test_validate_token_count() {
838 assert!(validate_token_count(100, 1000).is_ok());
839 assert!(validate_token_count(0, 1000).is_err());
840 assert!(validate_token_count(1001, 1000).is_err());
841 }
842
843 #[test]
844 fn test_validate_temperature() {
845 assert!(validate_temperature(0.7).is_ok());
846 assert!(validate_temperature(0.0).is_ok());
847 assert!(validate_temperature(2.0).is_ok());
848 assert!(validate_temperature(-0.1).is_err());
849 assert!(validate_temperature(2.1).is_err());
850 }
851
852 #[test]
853 fn test_validate_model_name() {
854 assert!(validate_model_name("gpt-4").is_ok());
855 assert!(validate_model_name("").is_err());
856 assert!(validate_model_name(" ").is_err());
857 }
858
859 #[test]
862 fn test_aggregate_scores() {
863 let scores = vec![70.0, 80.0, 90.0];
864
865 assert!(
866 (aggregate_scores(&scores, AggregationStrategy::Average, None) - 80.0).abs() < 1e-10
867 );
868 assert!(
869 (aggregate_scores(&scores, AggregationStrategy::Median, None) - 80.0).abs() < 1e-10
870 );
871 assert!(
872 (aggregate_scores(&scores, AggregationStrategy::Minimum, None) - 70.0).abs() < 1e-10
873 );
874 assert!(
875 (aggregate_scores(&scores, AggregationStrategy::Maximum, None) - 90.0).abs() < 1e-10
876 );
877
878 let weights = vec![0.2, 0.3, 0.5];
879 assert!(
881 (aggregate_scores(&scores, AggregationStrategy::Weighted, Some(&weights)) - 83.0).abs()
882 < 1e-10
883 );
884
885 assert!((aggregate_scores(&[], AggregationStrategy::Average, None)).abs() < 1e-10);
887 }
888
889 #[test]
890 fn test_combine_quality_originality() {
891 let quality = 80.0;
892 let originality = 90.0;
893 let combined = combine_quality_originality(quality, originality);
894 assert!((combined - 84.0).abs() < 1e-10); }
896
897 #[test]
898 fn test_calculate_consensus() {
899 let scores = vec![85.0, 86.0, 84.0, 85.5];
901 let (avg, confidence) = calculate_consensus(&scores);
902 assert!((avg - 85.125).abs() < 0.1);
903 assert!(confidence > 90.0);
904
905 let scores2 = vec![50.0, 90.0, 60.0, 80.0];
907 let (avg2, confidence2) = calculate_consensus(&scores2);
908 assert!((avg2 - 70.0).abs() < 1e-10);
909 assert!(confidence2 < 80.0);
910
911 let (avg3, confidence3) = calculate_consensus(&[]);
913 assert!((avg3).abs() < 1e-10);
914 assert!((confidence3).abs() < 1e-10);
915 }
916
917 #[test]
920 fn test_score_difference_percent() {
921 assert!((score_difference_percent(110.0, 100.0) - 10.0).abs() < 1e-10);
922 assert!((score_difference_percent(90.0, 100.0) + 10.0).abs() < 1e-10);
923 assert!((score_difference_percent(100.0, 0.0)).abs() < 1e-10);
924 }
925
926 #[test]
927 fn test_scores_significantly_different() {
928 assert!(scores_significantly_different(100.0, 80.0)); assert!(scores_significantly_different(100.0, 89.0)); assert!(!scores_significantly_different(100.0, 95.0)); }
932
933 #[test]
934 fn test_score_to_grade() {
935 assert_eq!(score_to_grade(95.0), 'A');
936 assert_eq!(score_to_grade(85.0), 'B');
937 assert_eq!(score_to_grade(75.0), 'C');
938 assert_eq!(score_to_grade(65.0), 'D');
939 assert_eq!(score_to_grade(50.0), 'F');
940 }
941
942 #[test]
943 fn test_score_to_tier() {
944 assert_eq!(score_to_tier(96.0), "Exceptional");
945 assert_eq!(score_to_tier(87.0), "Excellent");
946 assert_eq!(score_to_tier(77.0), "Good");
947 assert_eq!(score_to_tier(67.0), "Fair");
948 assert_eq!(score_to_tier(52.0), "Poor");
949 assert_eq!(score_to_tier(40.0), "Very Poor");
950 }
951}