cpop_protocol/rfc/
fixed_point.rs1use serde::{Deserialize, Serialize};
23use std::ops::{Add, Sub};
24
25macro_rules! fixed_point {
26 (
27 $(#[$meta:meta])*
28 $name:ident($inner:ty), scale=$scale:expr, min=$min:expr, max=$max:expr
29 ) => {
30 $(#[$meta])*
31 #[derive(
32 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
33 Serialize, Deserialize,
34 )]
35 #[serde(transparent)]
36 pub struct $name(pub $inner);
37
38 impl $name {
39 pub const MAX: $name = $name($max as $inner);
40 pub const MIN: $name = $name($min as $inner);
41
42 #[inline]
43 pub const fn new(value: $inner) -> Self {
44 $name(value)
45 }
46
47 pub fn from_float(value: f64) -> Self {
48 let scaled = (value * ($scale as f64)).round() as i32;
49 let clamped = scaled.clamp($min, $max) as $inner;
50 $name(clamped)
51 }
52
53 #[inline]
54 pub const fn raw(&self) -> $inner {
55 self.0
56 }
57
58 #[inline]
59 pub fn to_float(&self) -> f64 {
60 self.0 as f64 / $scale as f64
61 }
62 }
63
64 impl From<f64> for $name {
65 fn from(value: f64) -> Self {
66 $name::from_float(value)
67 }
68 }
69
70 impl From<$name> for f64 {
71 fn from(value: $name) -> Self {
72 value.to_float()
73 }
74 }
75 };
76}
77
78fixed_point! {
79 Millibits(u16), scale=1000, min=0, max=1000
82}
83
84fixed_point! {
85 RhoMillibits(i16), scale=1000, min=-1000, max=1000
88}
89
90fixed_point! {
91 Centibits(u16), scale=10000, min=0, max=10000
94}
95
96fixed_point! {
97 Decibits(u16), scale=10, min=0, max=640
100}
101
102fixed_point! {
103 SlopeDecibits(i8), scale=10, min=-100, max=100
106}
107
108fixed_point! {
109 DeciWpm(u16), scale=10, min=0, max=5000
112}
113
114#[derive(
117 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
118)]
119#[serde(transparent)]
120pub struct Microdollars(pub u64);
121
122impl Microdollars {
123 #[inline]
125 pub const fn new(value: u64) -> Self {
126 Microdollars(value)
127 }
128
129 pub fn from_dollars(value: f64) -> Self {
131 let scaled = (value * 1_000_000.0).round() as i64;
132 Microdollars(scaled.max(0) as u64)
133 }
134
135 #[inline]
137 pub const fn raw(&self) -> u64 {
138 self.0
139 }
140
141 #[inline]
143 pub fn to_dollars(&self) -> f64 {
144 self.0 as f64 / 1_000_000.0
145 }
146}
147
148impl Add for Millibits {
149 type Output = Self;
150 fn add(self, other: Self) -> Self {
151 Millibits((self.0 + other.0).min(1000))
152 }
153}
154
155impl Sub for Millibits {
156 type Output = Self;
157 fn sub(self, other: Self) -> Self {
158 Millibits(self.0.saturating_sub(other.0))
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_millibits_roundtrip() {
168 let values = [0.0, 0.5, 0.95, 1.0, 0.001, 0.999];
169 for v in values {
170 let mb = Millibits::from_float(v);
171 let back: f64 = mb.into();
172 assert!(
173 (back - v).abs() < 0.001,
174 "value {} roundtripped to {}",
175 v,
176 back
177 );
178 }
179 }
180
181 #[test]
182 fn test_millibits_clamping() {
183 assert_eq!(Millibits::from_float(-0.5).raw(), 0);
184 assert_eq!(Millibits::from_float(1.5).raw(), 1000);
185 }
186
187 #[test]
188 fn test_rho_millibits_signed() {
189 let rho = RhoMillibits::from_float(-0.75);
190 assert_eq!(rho.raw(), -750);
191 assert!((rho.to_float() - (-0.75)).abs() < 0.001);
192 }
193
194 #[test]
195 fn test_centibits_precision() {
196 let epsilon = Centibits::from_float(0.0005);
197 assert_eq!(epsilon.raw(), 5);
198 assert!((epsilon.to_float() - 0.0005).abs() < 0.0001);
199 }
200
201 #[test]
202 fn test_decibits_entropy() {
203 let entropy = Decibits::from_float(3.2);
204 assert_eq!(entropy.raw(), 32);
205 assert!((entropy.to_float() - 3.2).abs() < 0.1);
206 }
207
208 #[test]
209 fn test_slope_decibits_negative() {
210 let slope = SlopeDecibits::from_float(-1.2);
211 assert_eq!(slope.raw(), -12);
212 assert!((slope.to_float() - (-1.2)).abs() < 0.1);
213 }
214
215 #[test]
216 fn test_deci_wpm() {
217 let wpm = DeciWpm::from_float(45.5);
218 assert_eq!(wpm.raw(), 455);
219 assert!((wpm.to_float() - 45.5).abs() < 0.1);
220 }
221
222 #[test]
223 fn test_microdollars() {
224 let cost = Microdollars::from_dollars(0.05);
225 assert_eq!(cost.raw(), 50000);
226 assert!((cost.to_dollars() - 0.05).abs() < 0.000001);
227 }
228
229 #[test]
230 fn test_millibits_serde() {
231 let mb = Millibits::from_float(0.75);
232 let json = serde_json::to_string(&mb).unwrap();
233 assert_eq!(json, "750");
234 let decoded: Millibits = serde_json::from_str(&json).unwrap();
235 assert_eq!(decoded, mb);
236 }
237
238 #[test]
239 fn test_millibits_arithmetic() {
240 let a = Millibits::new(300);
241 let b = Millibits::new(400);
242 assert_eq!((a + b).raw(), 700);
243 assert_eq!((b - a).raw(), 100);
244 assert_eq!((a - b).raw(), 0); }
246}