kaccy_ai/
utils.rs

1//! Utility functions and helpers for common AI operations
2//!
3//! This module provides convenient helper functions that simplify
4//! common patterns when using the kaccy-ai crate.
5
6use crate::ai_evaluator::{FraudCheckRequest, VerificationRequest};
7use crate::error::{AiError, Result};
8
9/// Builder for creating `VerificationRequest` with sensible defaults
10pub 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    /// Create a new verification request builder
20    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    /// Set the commitment description
31    #[must_use]
32    pub fn description(mut self, desc: impl Into<String>) -> Self {
33        self.description = Some(desc.into());
34        self
35    }
36
37    /// Set the deadline
38    #[must_use]
39    pub fn deadline(mut self, deadline: impl Into<String>) -> Self {
40        self.deadline = deadline.into();
41        self
42    }
43
44    /// Set the evidence description
45    #[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    /// Build the verification request
52    #[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
64/// Builder for creating `FraudCheckRequest` with sensible defaults
65pub 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    /// Create a new fraud check request builder
75    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    /// Set commitments made
86    #[must_use]
87    pub fn commitments_made(mut self, count: i32) -> Self {
88        self.commitments_made = count;
89        self
90    }
91
92    /// Set commitments fulfilled
93    #[must_use]
94    pub fn commitments_fulfilled(mut self, count: i32) -> Self {
95        self.commitments_fulfilled = count;
96        self
97    }
98
99    /// Set average quality score
100    #[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    /// Build the fraud check request
107    #[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
119/// Validate a URL format
120pub 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
134/// Validate a confidence score is within valid range (0-100)
135pub 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
144/// Validate a quality score is within valid range (0-100)
145pub 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/// Calculate success rate from counts
155#[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/// Format a duration in human-readable form
164#[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/// Format a cost amount in USD with appropriate precision
177#[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/// Calculate average from a slice of f64 values
189#[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/// Calculate median from a slice of f64 values
198#[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/// Calculate standard deviation from a slice of f64 values
216#[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
235/// Clamp a value between min and max
236pub 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/// Normalize a score from one range to another
247#[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/// Check if a score represents a passing grade (>= 70%)
254#[must_use]
255pub fn is_passing_score(score: f64) -> bool {
256    score >= 70.0
257}
258
259/// Check if a score represents excellence (>= 90%)
260#[must_use]
261pub fn is_excellent_score(score: f64) -> bool {
262    score >= 90.0
263}
264
265/// Convert a confidence percentage to a risk level description
266#[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
281/// Retry a function with exponential backoff
282///
283/// # Example
284/// ```no_run
285/// use kaccy_ai::utils::retry_with_exponential_backoff;
286///
287/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
288/// let result = retry_with_exponential_backoff(
289///     3,
290///     std::time::Duration::from_millis(100),
291///     || async {
292///         // Your async operation here
293///         Ok::<_, std::io::Error>(42)
294///     }
295/// ).await?;
296/// # Ok(())
297/// # }
298/// ```
299pub 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; // Exponential backoff
321            }
322        }
323    }
324}
325
326// Additional statistical utilities
327
328/// Calculate percentile from a slice of f64 values
329///
330/// # Arguments
331/// * `values` - Slice of values to calculate percentile from
332/// * `percentile` - Percentile to calculate (0-100)
333///
334/// # Returns
335/// The value at the given percentile, or 0.0 if values is empty
336#[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/// Calculate weighted average from slices of values and weights
351///
352/// # Arguments
353/// * `values` - Slice of values
354/// * `weights` - Slice of weights (must be same length as values)
355///
356/// # Returns
357/// Weighted average, or 0.0 if inputs are empty or lengths don't match
358#[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/// Calculate variance from a slice of f64 values
378#[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/// Calculate coefficient of variation (CV) - std dev as percentage of mean
396/// Useful for comparing variability across datasets with different scales
397#[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// Formatting utilities
413
414/// Format token count in human-readable form
415#[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/// Format file size in human-readable form
427#[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/// Format percentage with appropriate precision
445#[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
456// Validation utilities
457
458/// Validate token count is within reasonable range
459pub 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
475/// Validate temperature parameter for LLM requests
476pub 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
485/// Validate model name is not empty
486pub 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// Score aggregation utilities
496
497/// Aggregate multiple scores using different strategies
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum AggregationStrategy {
500    /// Take the average of all scores
501    Average,
502    /// Take the median of all scores
503    Median,
504    /// Take the minimum score (most conservative)
505    Minimum,
506    /// Take the maximum score (most optimistic)
507    Maximum,
508    /// Take the weighted average (requires weights)
509    Weighted,
510}
511
512/// Aggregate scores using the specified strategy
513pub 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/// Combine quality and originality scores into a final score
538/// Uses weighted average: quality (60%) + originality (40%)
539#[must_use]
540pub fn combine_quality_originality(quality: f64, originality: f64) -> f64 {
541    quality * 0.6 + originality * 0.4
542}
543
544/// Calculate consensus score from multiple evaluations
545/// Returns average score and confidence based on agreement
546#[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    // Lower standard deviation means higher confidence
556    // Map std dev to confidence: 0 std dev = 100% confidence, high std dev = low confidence
557    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// Comparison utilities
571
572/// Compare two scores and return the difference as a percentage
573#[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/// Determine if two scores are significantly different (> 10% difference)
582#[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/// Get score grade letter (A, B, C, D, F)
589#[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/// Get score tier description
605#[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    // Tests for new statistical utilities
761
762    #[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        // Mismatched lengths
779        assert!((calculate_weighted_average(&values, &[0.5, 0.5])).abs() < 1e-10);
780
781        // Empty inputs
782        assert!((calculate_weighted_average(&[], &[])).abs() < 1e-10);
783
784        // Zero total weight
785        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        // Empty
804        assert!((calculate_coefficient_of_variation(&[])).abs() < 1e-10);
805
806        // Zero mean
807        assert!((calculate_coefficient_of_variation(&[0.0, 0.0, 0.0])).abs() < 1e-10);
808    }
809
810    // Tests for formatting utilities
811
812    #[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    // Tests for validation utilities
835
836    #[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    // Tests for aggregation utilities
860
861    #[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        // 70 * 0.2 + 80 * 0.3 + 90 * 0.5 = 14 + 24 + 45 = 83.0
880        assert!(
881            (aggregate_scores(&scores, AggregationStrategy::Weighted, Some(&weights)) - 83.0).abs()
882                < 1e-10
883        );
884
885        // Empty scores
886        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); // 80 * 0.6 + 90 * 0.4
895    }
896
897    #[test]
898    fn test_calculate_consensus() {
899        // High agreement (low std dev) -> high confidence
900        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        // Low agreement (high std dev) -> low confidence
906        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        // Empty
912        let (avg3, confidence3) = calculate_consensus(&[]);
913        assert!((avg3).abs() < 1e-10);
914        assert!((confidence3).abs() < 1e-10);
915    }
916
917    // Tests for comparison utilities
918
919    #[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)); // 25% difference
929        assert!(scores_significantly_different(100.0, 89.0)); // 12.4% difference
930        assert!(!scores_significantly_different(100.0, 95.0)); // 5.3% difference
931    }
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}