1use crate::error::RillError;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct ArmStats {
17 pub pulls: u64,
19 pub total_reward: f64,
21 pub mean_reward: f64,
24}
25
26impl ArmStats {
27 pub const fn empty() -> Self {
29 Self {
30 pulls: 0,
31 total_reward: 0.0,
32 mean_reward: 0.0,
33 }
34 }
35
36 pub fn new(pulls: u64, total_reward: f64) -> Result<Self, RillError> {
40 if !total_reward.is_finite() {
41 return Err(RillError::NonFiniteValue {
42 field: "total_reward",
43 value: total_reward,
44 });
45 }
46 let mean_reward = if pulls > 0 {
47 total_reward / pulls as f64
48 } else {
49 0.0
50 };
51 Ok(Self {
52 pulls,
53 total_reward,
54 mean_reward,
55 })
56 }
57}
58
59impl Default for ArmStats {
60 fn default() -> Self {
61 Self::empty()
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn empty_stats() {
71 let s = ArmStats::empty();
72 assert_eq!(s.pulls, 0);
73 assert!((s.total_reward - 0.0).abs() < 1e-12);
74 assert!((s.mean_reward - 0.0).abs() < 1e-12);
75 }
76
77 #[test]
78 fn default_is_empty() {
79 assert_eq!(ArmStats::default(), ArmStats::empty());
80 }
81
82 #[test]
83 fn new_with_pulls() {
84 let s = ArmStats::new(10, 5.0).unwrap();
85 assert_eq!(s.pulls, 10);
86 assert!((s.total_reward - 5.0).abs() < 1e-12);
87 assert!((s.mean_reward - 0.5).abs() < 1e-12);
88 }
89
90 #[test]
91 fn new_with_zero_pulls() {
92 let s = ArmStats::new(0, 0.0).unwrap();
93 assert_eq!(s.pulls, 0);
94 assert!((s.mean_reward - 0.0).abs() < 1e-12);
95 }
96
97 #[test]
98 fn new_rejects_non_finite() {
99 assert!(ArmStats::new(5, f64::NAN).is_err());
100 assert!(ArmStats::new(5, f64::INFINITY).is_err());
101 }
102
103 #[test]
104 fn mean_reward_for_various_pulls() {
105 let s = ArmStats::new(3, 9.0).unwrap();
106 assert!((s.mean_reward - 3.0).abs() < 1e-12);
107
108 let s = ArmStats::new(100, 250.0).unwrap();
109 assert!((s.mean_reward - 2.5).abs() < 1e-12);
110 }
111
112 #[cfg(feature = "serde")]
113 #[test]
114 fn serde_roundtrip() {
115 let s = ArmStats::new(42, 123.5).unwrap();
116 let json = serde_json::to_string(&s).unwrap();
117 let restored: ArmStats = serde_json::from_str(&json).unwrap();
118 assert_eq!(restored.pulls, 42);
119 assert!((restored.total_reward - 123.5).abs() < 1e-12);
120 assert!((restored.mean_reward - (123.5 / 42.0)).abs() < 1e-12);
121 }
122}