1#![allow(clippy::manual_div_ceil, clippy::while_let_on_iterator)]
12
13extern crate alloc;
14use alloc::string::String;
15use alloc::vec::Vec;
16
17use crate::buffer::{BufferReader, BufferWriter};
18use crate::encode::{CdrDecode, CdrEncode};
19use crate::error::{DecodeError, EncodeError};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Fixed<const P: u32, const S: u32> {
31 digits: Vec<u8>,
34}
35
36impl<const P: u32, const S: u32> Default for Fixed<P, S> {
37 fn default() -> Self {
38 let n = ((P + 1) / 2 + 1) as usize;
39 let mut digits = alloc::vec![0u8; n];
40 let last = digits.len() - 1;
42 digits[last] = 0x0C;
43 Self { digits }
44 }
45}
46
47impl<const P: u32, const S: u32> Fixed<P, S> {
48 pub fn from_bcd_bytes(bytes: Vec<u8>) -> Result<Self, DecodeError> {
53 let expected = ((P + 1) / 2 + 1) as usize;
54 if bytes.len() != expected {
55 return Err(DecodeError::LengthExceeded {
56 announced: bytes.len(),
57 remaining: expected,
58 offset: 0,
59 });
60 }
61 Ok(Self { digits: bytes })
62 }
63
64 #[must_use]
66 pub fn as_bcd_bytes(&self) -> &[u8] {
67 &self.digits
68 }
69
70 pub fn from_str_repr(s: &str) -> Result<Self, DecodeError> {
75 let (sign, rest) = if let Some(stripped) = s.strip_prefix('-') {
76 (false, stripped)
77 } else if let Some(stripped) = s.strip_prefix('+') {
78 (true, stripped)
79 } else {
80 (true, s)
81 };
82 let (int_part, frac_part) = rest.split_once('.').unwrap_or((rest, ""));
83 let total_p = P as usize;
85 let total_s = S as usize;
86 let mut digits_buf = String::with_capacity(total_p);
87 let int_needed = total_p - total_s;
89 if int_part.len() > int_needed {
90 return Err(DecodeError::InvalidString {
91 offset: 0,
92 reason: "fixed: integer part exceeds P-S",
93 });
94 }
95 for _ in int_part.len()..int_needed {
96 digits_buf.push('0');
97 }
98 digits_buf.push_str(int_part);
99 if frac_part.len() > total_s {
101 return Err(DecodeError::InvalidString {
102 offset: 0,
103 reason: "fixed: fractional part exceeds S",
104 });
105 }
106 digits_buf.push_str(frac_part);
107 for _ in frac_part.len()..total_s {
108 digits_buf.push('0');
109 }
110 let mut packed: Vec<u8> = Vec::with_capacity(((P + 1) / 2 + 1) as usize);
112 let mut chars = digits_buf.chars().rev().peekable();
113 let sign_nibble: u8 = if sign { 0x0C } else { 0x0D };
114 let mut current = sign_nibble;
115 let mut have_low = true;
116 while let Some(c) = chars.next() {
117 let d = c.to_digit(10).ok_or(DecodeError::InvalidString {
118 offset: 0,
119 reason: "fixed: non-digit char",
120 })? as u8;
121 if have_low {
122 current |= (d & 0x0F) << 4;
123 packed.push(current);
124 current = 0;
125 have_low = false;
126 } else {
127 current |= d & 0x0F;
128 have_low = true;
129 }
130 }
131 if !have_low {
132 packed.push(current);
133 }
134 packed.reverse();
135 let expected = ((P + 1) / 2 + 1) as usize;
137 while packed.len() < expected {
138 packed.insert(0, 0);
139 }
140 Ok(Self { digits: packed })
141 }
142
143 #[must_use]
145 pub fn to_string_repr(&self) -> String {
146 let mut digits_chars: Vec<char> = Vec::new();
147 let mut sign = '+';
148 for (idx, byte) in self.digits.iter().enumerate() {
149 let high = (byte >> 4) & 0x0F;
150 let low = byte & 0x0F;
151 if idx == self.digits.len() - 1 {
152 digits_chars.push(char::from_digit(u32::from(high), 10).unwrap_or('?'));
153 sign = if low == 0x0D { '-' } else { '+' };
154 } else {
155 digits_chars.push(char::from_digit(u32::from(high), 10).unwrap_or('?'));
156 digits_chars.push(char::from_digit(u32::from(low), 10).unwrap_or('?'));
157 }
158 }
159 while digits_chars.len() > (S as usize + 1) && digits_chars[0] == '0' {
161 digits_chars.remove(0);
162 }
163 let mut out = String::new();
165 if sign == '-' {
166 out.push('-');
167 }
168 if (S as usize) > 0 {
169 let dot_pos = digits_chars.len().saturating_sub(S as usize);
170 for (i, c) in digits_chars.iter().enumerate() {
171 if i == dot_pos {
172 out.push('.');
173 }
174 out.push(*c);
175 }
176 } else {
177 for c in &digits_chars {
178 out.push(*c);
179 }
180 }
181 out
182 }
183}
184
185impl<const P: u32, const S: u32> CdrEncode for Fixed<P, S> {
186 fn encode(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
187 w.write_bytes(&self.digits)
190 }
191}
192
193impl<const P: u32, const S: u32> CdrDecode for Fixed<P, S> {
194 fn decode(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
195 let n = ((P + 1) / 2 + 1) as usize;
196 let bytes = r.read_bytes(n)?;
197 Self::from_bcd_bytes(bytes.to_vec())
198 }
199}
200
201#[cfg(test)]
202#[allow(clippy::expect_used, clippy::unwrap_used)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn fixed_default_is_zero_positive() {
208 let f: Fixed<5, 2> = Fixed::default();
209 assert_eq!(f.to_string_repr(), "0.00");
210 }
211
212 #[test]
213 fn fixed_roundtrip_via_string() {
214 let f: Fixed<5, 2> = Fixed::from_str_repr("123.45").expect("parse");
215 assert_eq!(f.to_string_repr(), "123.45");
216 }
217
218 #[test]
219 fn fixed_roundtrip_negative() {
220 let f: Fixed<6, 3> = Fixed::from_str_repr("-1.500").expect("parse");
221 let s = f.to_string_repr();
222 assert!(s.starts_with('-'));
223 assert!(s.contains("1.500") || s.contains("1.5"));
224 }
225
226 #[test]
227 fn fixed_wire_roundtrip() {
228 use crate::Endianness;
229 let f: Fixed<5, 2> = Fixed::from_str_repr("42.00").expect("parse");
230 let mut writer = BufferWriter::new(Endianness::Little);
231 f.encode(&mut writer).expect("encode");
232 let bytes = writer.into_bytes();
233 let mut reader = BufferReader::new(&bytes, Endianness::Little);
234 let back: Fixed<5, 2> = <Fixed<5, 2> as CdrDecode>::decode(&mut reader).expect("decode");
235 assert_eq!(back, f);
236 }
237
238 #[test]
239 fn fixed_overflow_returns_error() {
240 let res: Result<Fixed<3, 1>, _> = Fixed::from_str_repr("9999.5");
241 assert!(res.is_err());
242 }
243}