1#![allow(clippy::manual_div_ceil, clippy::while_let_on_iterator)]
19
20extern crate alloc;
21use alloc::string::String;
22use alloc::vec::Vec;
23
24use crate::buffer::{BufferReader, BufferWriter};
25use crate::encode::{CdrDecode, CdrEncode};
26use crate::error::{DecodeError, EncodeError};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Fixed<const P: u32, const S: u32> {
38 digits: Vec<u8>,
41}
42
43impl<const P: u32, const S: u32> Default for Fixed<P, S> {
44 fn default() -> Self {
45 let n = ((P + 2) / 2) as usize;
46 let mut digits = alloc::vec![0u8; n];
47 let last = digits.len() - 1;
49 digits[last] = 0x0C;
50 Self { digits }
51 }
52}
53
54impl<const P: u32, const S: u32> Fixed<P, S> {
55 pub fn from_bcd_bytes(bytes: Vec<u8>) -> Result<Self, DecodeError> {
60 let expected = ((P + 2) / 2) as usize;
61 if bytes.len() != expected {
62 return Err(DecodeError::LengthExceeded {
63 announced: bytes.len(),
64 remaining: expected,
65 offset: 0,
66 });
67 }
68 Ok(Self { digits: bytes })
69 }
70
71 #[must_use]
73 pub fn as_bcd_bytes(&self) -> &[u8] {
74 &self.digits
75 }
76
77 pub fn from_str_repr(s: &str) -> Result<Self, DecodeError> {
82 let (sign, rest) = if let Some(stripped) = s.strip_prefix('-') {
83 (false, stripped)
84 } else if let Some(stripped) = s.strip_prefix('+') {
85 (true, stripped)
86 } else {
87 (true, s)
88 };
89 let (int_part, frac_part) = rest.split_once('.').unwrap_or((rest, ""));
90 let total_p = P as usize;
92 let total_s = S as usize;
93 let mut digits_buf = String::with_capacity(total_p);
94 let int_needed = total_p - total_s;
96 if int_part.len() > int_needed {
97 return Err(DecodeError::InvalidString {
98 offset: 0,
99 reason: "fixed: integer part exceeds P-S",
100 });
101 }
102 for _ in int_part.len()..int_needed {
103 digits_buf.push('0');
104 }
105 digits_buf.push_str(int_part);
106 if frac_part.len() > total_s {
108 return Err(DecodeError::InvalidString {
109 offset: 0,
110 reason: "fixed: fractional part exceeds S",
111 });
112 }
113 digits_buf.push_str(frac_part);
114 for _ in frac_part.len()..total_s {
115 digits_buf.push('0');
116 }
117 let mut nibbles: Vec<u8> = Vec::with_capacity(P as usize + 2);
125 if (P + 1) % 2 == 1 {
126 nibbles.push(0);
127 }
128 for c in digits_buf.chars() {
129 let d = c.to_digit(10).ok_or(DecodeError::InvalidString {
130 offset: 0,
131 reason: "fixed: non-digit char",
132 })? as u8;
133 nibbles.push(d & 0x0F);
134 }
135 nibbles.push(if sign { 0x0C } else { 0x0D });
136 let mut packed: Vec<u8> = Vec::with_capacity(nibbles.len() / 2);
138 for pair in nibbles.chunks_exact(2) {
139 packed.push((pair[0] << 4) | pair[1]);
140 }
141 Ok(Self { digits: packed })
142 }
143
144 #[must_use]
146 pub fn to_string_repr(&self) -> String {
147 let mut digits_chars: Vec<char> = Vec::new();
148 let mut sign = '+';
149 for (idx, byte) in self.digits.iter().enumerate() {
150 let high = (byte >> 4) & 0x0F;
151 let low = byte & 0x0F;
152 if idx == self.digits.len() - 1 {
153 digits_chars.push(char::from_digit(u32::from(high), 10).unwrap_or('?'));
154 sign = if low == 0x0D { '-' } else { '+' };
155 } else {
156 digits_chars.push(char::from_digit(u32::from(high), 10).unwrap_or('?'));
157 digits_chars.push(char::from_digit(u32::from(low), 10).unwrap_or('?'));
158 }
159 }
160 while digits_chars.len() > (S as usize + 1) && digits_chars[0] == '0' {
162 digits_chars.remove(0);
163 }
164 let mut out = String::new();
166 if sign == '-' {
167 out.push('-');
168 }
169 if (S as usize) > 0 {
170 let dot_pos = digits_chars.len().saturating_sub(S as usize);
171 for (i, c) in digits_chars.iter().enumerate() {
172 if i == dot_pos {
173 out.push('.');
174 }
175 out.push(*c);
176 }
177 } else {
178 for c in &digits_chars {
179 out.push(*c);
180 }
181 }
182 out
183 }
184}
185
186impl<const P: u32, const S: u32> CdrEncode for Fixed<P, S> {
187 fn encode(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
188 w.write_bytes(&self.digits)
191 }
192}
193
194impl<const P: u32, const S: u32> CdrDecode for Fixed<P, S> {
195 fn decode(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
196 let n = ((P + 2) / 2) as usize;
197 let bytes = r.read_bytes(n)?;
198 Self::from_bcd_bytes(bytes.to_vec())
199 }
200}
201
202#[cfg(test)]
203#[allow(clippy::expect_used, clippy::unwrap_used)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn fixed_default_is_zero_positive() {
209 let f: Fixed<5, 2> = Fixed::default();
210 assert_eq!(f.to_string_repr(), "0.00");
211 }
212
213 #[test]
214 fn fixed_roundtrip_via_string() {
215 let f: Fixed<5, 2> = Fixed::from_str_repr("123.45").expect("parse");
216 assert_eq!(f.to_string_repr(), "123.45");
217 }
218
219 #[test]
220 fn fixed_roundtrip_negative() {
221 let f: Fixed<6, 3> = Fixed::from_str_repr("-1.500").expect("parse");
222 let s = f.to_string_repr();
223 assert!(s.starts_with('-'));
224 assert!(s.contains("1.500") || s.contains("1.5"));
225 }
226
227 #[test]
228 fn fixed_wire_roundtrip() {
229 use crate::Endianness;
230 let f: Fixed<5, 2> = Fixed::from_str_repr("42.00").expect("parse");
231 let mut writer = BufferWriter::new(Endianness::Little);
232 f.encode(&mut writer).expect("encode");
233 let bytes = writer.into_bytes();
234 let mut reader = BufferReader::new(&bytes, Endianness::Little);
235 let back: Fixed<5, 2> = <Fixed<5, 2> as CdrDecode>::decode(&mut reader).expect("decode");
236 assert_eq!(back, f);
237 }
238
239 #[test]
240 fn fixed_overflow_returns_error() {
241 let res: Result<Fixed<3, 1>, _> = Fixed::from_str_repr("9999.5");
242 assert!(res.is_err());
243 }
244
245 #[test]
250 fn fixed_bcd_bytes_odd_p_match_orb() {
251 let f: Fixed<5, 2> = Fixed::from_str_repr("123.45").expect("parse");
252 assert_eq!(f.as_bcd_bytes(), &[0x12, 0x34, 0x5C]);
253 assert_eq!(f.to_string_repr(), "123.45");
254 }
255
256 #[test]
260 fn fixed_even_p_keeps_msd_no_corruption() {
261 let f: Fixed<4, 0> = Fixed::from_str_repr("1234").expect("parse");
262 assert_eq!(f.as_bcd_bytes(), &[0x01, 0x23, 0x4C]);
263 assert_eq!(f.to_string_repr(), "1234");
264 }
265
266 #[test]
269 fn fixed_negative_pad_to_p_match_orb() {
270 let f: Fixed<6, 2> = Fixed::from_str_repr("-1.50").expect("parse");
271 assert_eq!(f.as_bcd_bytes(), &[0x00, 0x00, 0x15, 0x0D]);
272 assert_eq!(f.to_string_repr(), "-1.50");
273 }
274
275 #[test]
278 fn fixed_even_p_full_digits_roundtrip() {
279 let f: Fixed<6, 3> = Fixed::from_str_repr("123.456").expect("parse");
280 assert_eq!(f.as_bcd_bytes(), &[0x01, 0x23, 0x45, 0x6C]);
281 assert_eq!(f.to_string_repr(), "123.456");
282 }
283
284 #[test]
288 fn fixed_exhaustive_roundtrip_4_0() {
289 for n in 0u32..=9999 {
290 let s = alloc::format!("{n}");
291 let f: Fixed<4, 0> = Fixed::from_str_repr(&s).expect("parse");
292 assert_eq!(
294 f.to_string_repr().parse::<u32>().expect("reparse"),
295 n,
296 "roundtrip lost value for {n}"
297 );
298 }
299 }
300}