1pub const POSITIVE: u16 = 0x0000;
11pub const NEGATIVE: u16 = 0x4000;
12pub const NAN: u16 = 0xc000;
13pub const INFINITY: u16 = 0xd000;
14pub const NEGATIVE_INFINITY: u16 = 0xf000;
15
16const CLASS_NEGATIVE_INFINITY: u8 = 1;
17const CLASS_NEGATIVE: u8 = 2;
18const CLASS_ZERO: u8 = 3;
19const CLASS_POSITIVE: u8 = 4;
20const CLASS_INFINITY: u8 = 5;
21const CLASS_NAN: u8 = 6;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Unpacked {
26 pub sign: u16,
28 pub weight: i16,
30 pub dscale: u16,
32 pub digits: Vec<i16>,
34}
35
36#[must_use]
38pub fn encode(sign: u16, weight: i16, dscale: u16, digits: &[i16]) -> Vec<u8> {
39 let lead = digits.iter().take_while(|&&digit| digit == 0).count();
40 let digits = &digits[lead..];
41 let tail = digits.iter().rev().take_while(|&&digit| digit == 0).count();
42 let digits = &digits[..digits.len() - tail];
43 #[expect(clippy::cast_possible_truncation, reason = "a weight is far below 2^15 digits")]
44 let weight = weight.wrapping_sub(lead as i16);
45 let class = match sign {
46 NAN => CLASS_NAN,
47 INFINITY => CLASS_INFINITY,
48 NEGATIVE_INFINITY => CLASS_NEGATIVE_INFINITY,
49 _ if digits.is_empty() => CLASS_ZERO,
50 NEGATIVE => CLASS_NEGATIVE,
51 _ => CLASS_POSITIVE,
52 };
53 let mut bytes = Vec::with_capacity(5 + 2 * digits.len() + 2);
54 bytes.push(class);
55 let words = std::iter::once(weight as u16 ^ 0x8000).chain(digits.iter().map(|&d| d as u16));
56 match class {
57 CLASS_POSITIVE => words.for_each(|word| bytes.extend_from_slice(&word.to_be_bytes())),
58 CLASS_NEGATIVE => {
59 let mut words = words;
61 let weight = words.next().unwrap_or_default();
62 bytes.extend_from_slice(&(!weight).to_be_bytes());
63 for digit in words {
64 bytes.extend_from_slice(&(0xfffe - digit).to_be_bytes());
65 }
66 bytes.extend_from_slice(&0xffffu16.to_be_bytes());
67 }
68 _ => {}
69 }
70 let dscale =
71 if matches!(class, CLASS_POSITIVE | CLASS_NEGATIVE | CLASS_ZERO) { dscale } else { 0 };
72 bytes.extend_from_slice(&dscale.to_be_bytes());
73 bytes
74}
75
76#[must_use]
78pub fn decode(bytes: &[u8]) -> Unpacked {
79 let special = |sign| Unpacked { sign, weight: 0, dscale: 0, digits: Vec::new() };
80 let Some((&class, rest)) = bytes.split_first() else {
81 return special(NAN);
82 };
83 let dscale = match rest.len().checked_sub(2) {
84 Some(at) => u16::from_be_bytes([rest[at], rest[at + 1]]),
85 None => return special(NAN),
86 };
87 let body = &rest[..rest.len() - 2];
88 let words = || body.as_chunks::<2>().0.iter().map(|&pair| u16::from_be_bytes(pair));
89 match class {
90 CLASS_ZERO => Unpacked { sign: POSITIVE, weight: 0, dscale, digits: Vec::new() },
91 CLASS_POSITIVE => {
92 let mut words = words();
93 let weight = (words.next().unwrap_or(0x8000) ^ 0x8000) as i16;
94 let digits = words.map(|word| word as i16).collect();
95 Unpacked { sign: POSITIVE, weight, dscale, digits }
96 }
97 CLASS_NEGATIVE => {
98 let mut words = words();
99 let weight = (!words.next().unwrap_or(0x7fff) ^ 0x8000) as i16;
100 let digits =
101 words.take_while(|&word| word != 0xffff).map(|word| (0xfffe - word) as i16);
102 Unpacked { sign: NEGATIVE, weight, dscale, digits: digits.collect() }
103 }
104 CLASS_INFINITY => special(INFINITY),
105 CLASS_NEGATIVE_INFINITY => special(NEGATIVE_INFINITY),
106 _ => special(NAN),
107 }
108}
109
110#[must_use]
112pub fn key(bytes: &[u8]) -> &[u8] {
113 &bytes[..bytes.len().saturating_sub(2)]
114}
115
116#[must_use]
118pub fn to_text(bytes: &[u8]) -> String {
119 let value = decode(bytes);
120 match value.sign {
121 NAN => return "NaN".to_owned(),
122 INFINITY => return "Infinity".to_owned(),
123 NEGATIVE_INFINITY => return "-Infinity".to_owned(),
124 _ => {}
125 }
126 let mut out = String::new();
127 if value.sign == NEGATIVE {
128 out.push('-');
129 }
130 let digit = |weight: i32| -> i32 {
131 let at = i32::from(value.weight) - weight;
132 usize::try_from(at).ok().and_then(|at| value.digits.get(at)).map_or(0, |&d| i32::from(d))
133 };
134 if value.weight < 0 {
136 out.push('0');
137 } else {
138 for weight in (0..=i32::from(value.weight)).rev() {
139 let d = digit(weight);
140 if weight == i32::from(value.weight) {
141 out.push_str(&d.to_string());
142 } else {
143 out.push_str(&format!("{d:04}"));
144 }
145 }
146 }
147 if value.dscale > 0 {
148 out.push('.');
149 let mut written = 0;
150 let mut weight = -1;
151 while written < usize::from(value.dscale) {
152 let group = format!("{:04}", digit(weight));
153 let take = (usize::from(value.dscale) - written).min(4);
154 out.push_str(&group[..take]);
155 written += take;
156 weight -= 1;
157 }
158 }
159 out
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn bytes(sign: u16, weight: i16, dscale: u16, digits: &[i16]) -> Vec<u8> {
167 encode(sign, weight, dscale, digits)
168 }
169
170 #[test]
171 fn the_bytes_order_as_the_numbers_do() {
172 let values = [
173 bytes(NEGATIVE_INFINITY, 0, 0, &[]),
174 bytes(NEGATIVE, 1, 0, &[1, 0]),
175 bytes(NEGATIVE, 0, 1, &[1, 5000]),
176 bytes(NEGATIVE, 0, 0, &[1]),
177 bytes(NEGATIVE, -1, 4, &[1]),
178 bytes(POSITIVE, 0, 2, &[]),
179 bytes(POSITIVE, -1, 4, &[1]),
180 bytes(POSITIVE, 0, 0, &[1]),
181 bytes(POSITIVE, 0, 1, &[1, 5000]),
182 bytes(POSITIVE, 1, 0, &[1, 0]),
183 bytes(INFINITY, 0, 0, &[]),
184 bytes(NAN, 0, 0, &[]),
185 ];
186 for pair in values.windows(2) {
187 assert!(key(&pair[0]) < key(&pair[1]), "{} < {}", to_text(&pair[0]), to_text(&pair[1]));
188 }
189 assert_eq!(key(&bytes(POSITIVE, 0, 1, &[1])), key(&bytes(POSITIVE, 0, 2, &[1, 0])));
190 }
191
192 #[test]
193 fn a_value_reads_back_and_prints_with_its_scale() {
194 for (sign, weight, dscale, digits, text) in [
195 (POSITIVE, 0, 20, &[0, 3333, 3333, 3333, 3333, 3333][..], "0.33333333333333333333"),
196 (NEGATIVE, 1, 2, &[12, 3456, 7800][..], "-123456.78"),
197 (POSITIVE, 0, 3, &[][..], "0.000"),
198 (POSITIVE, -2, 6, &[33][..], "0.000000"),
199 (POSITIVE, -2, 8, &[33][..], "0.00000033"),
200 (NAN, 0, 0, &[][..], "NaN"),
201 (NEGATIVE_INFINITY, 0, 0, &[][..], "-Infinity"),
202 ] {
203 let packed = bytes(sign, weight, dscale, digits);
204 assert_eq!(to_text(&packed), text);
205 let back = decode(&packed);
206 assert_eq!(encode(back.sign, back.weight, back.dscale, &back.digits), packed);
207 }
208 }
209}