Skip to main content

ffl/
string.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::fixed::Fixed;
6
7/// Formatting modes for displaying fixed-point values.
8#[derive(Clone, Copy, PartialEq, Eq, Debug)]
9pub enum Mode {
10    /// Decimal format (e.g. `1.23`).
11    Dec,
12    /// Hexadecimal format (e.g. `0x1.3b`).
13    Hex,
14    /// Rational format (e.g. `1+1/2`).
15    DecRational,
16}
17
18/// A stack-allocated, fixed-size string buffer for formatting fixed-point values
19/// without heap allocations.
20pub struct String {
21    buffer: [u8; 128],
22    length: usize,
23}
24
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26enum ZeroMode {
27    NoLeadingZeros,
28    NoTrailingZeros,
29}
30
31impl String {
32    /// Creates an empty string buffer.
33    pub fn empty() -> Self {
34        Self { buffer: [0u8; 128], length: 0 }
35    }
36
37    /// Formats the given fixed-point value in the specified mode and precision.
38    pub fn new<I, const FRAC: usize>(
39        value: Fixed<I, FRAC>,
40        mode: Mode,
41        precision: Option<usize>,
42    ) -> Self
43    where
44        I: Into<i128> + Copy,
45    {
46        let mut s = Self::empty();
47        match mode {
48            Mode::Dec => s.write_dec(value, precision),
49            Mode::Hex => s.write_hex(value),
50            Mode::DecRational => s.write_dec_rational(value),
51        }
52        s
53    }
54
55    /// Returns a string slice referencing the formatted string.
56    pub fn as_str(&self) -> &str {
57        self
58    }
59
60    fn push_byte(&mut self, b: u8) {
61        if self.length < self.buffer.len() {
62            self.buffer[self.length] = b;
63            self.length += 1;
64        }
65    }
66
67    fn write_dec_integer(&mut self, value: u128) {
68        let mut digits = [0u8; 39];
69        let mut len = 0;
70        let mut rem = value;
71        loop {
72            digits[len] = b'0' + (rem % 10) as u8;
73            len += 1;
74            rem /= 10;
75            if rem == 0 {
76                break;
77            }
78        }
79        for i in (0..len).rev() {
80            self.push_byte(digits[i]);
81        }
82    }
83
84    // Formats the fixed-point value in decimal.
85    // Note: To match the C++ ffl library behavior, the fractional part is truncated
86    // rather than rounded to the requested precision.
87    fn write_dec<I, const FRAC: usize>(&mut self, value: Fixed<I, FRAC>, precision: Option<usize>)
88    where
89        I: Into<i128> + Copy,
90    {
91        let raw = value.raw_value();
92        let raw_i: i128 = raw.into();
93        if raw_i < 0 {
94            self.push_byte(b'-');
95        }
96
97        let absolute_value = raw_i.unsigned_abs();
98        let integral_value = if FRAC >= 128 { 0 } else { absolute_value >> FRAC };
99
100        self.write_dec_integer(integral_value);
101
102        let max_digits = precision.unwrap_or(10);
103        if max_digits > 0 {
104            self.push_byte(b'.');
105            let start_pos = self.length;
106            let mut last_nonzero = start_pos;
107
108            let requested_stop = start_pos + max_digits;
109            let max_stop = self.buffer.len() - 1;
110            let stop = if requested_stop < max_stop { requested_stop } else { max_stop };
111
112            let mut remaining_value = absolute_value;
113            let frac_mask = (1u128 << FRAC) - 1;
114
115            let mut pos = start_pos;
116            loop {
117                remaining_value &= frac_mask;
118                remaining_value *= 10;
119                let digit_val = remaining_value >> FRAC;
120                let digit = b'0' + digit_val as u8;
121                if digit != b'0' {
122                    last_nonzero = pos;
123                }
124                if pos < self.buffer.len() {
125                    self.buffer[pos] = digit;
126                }
127                pos += 1;
128                remaining_value &= frac_mask;
129                if (remaining_value == 0 && precision.is_none()) || pos >= stop {
130                    break;
131                }
132            }
133            if precision.is_some() {
134                self.length = stop;
135            } else {
136                self.length = last_nonzero + 1;
137            }
138        }
139    }
140
141    fn write_dec_rational<I, const FRAC: usize>(&mut self, value: Fixed<I, FRAC>)
142    where
143        I: Into<i128> + Copy,
144    {
145        // The library currently supports fixed-point types up to 64 bits (i8..i64).
146        // Ensure that FRAC is at most 64 so that u64 arithmetic is lossless.
147        assert!(FRAC <= 64, "Fixed-point types larger than 64 bits are not supported");
148        let raw = value.raw_value();
149        let raw_i: i128 = raw.into();
150        let sign = if raw_i < 0 { b'-' } else { b'+' };
151        if raw_i < 0 {
152            self.push_byte(b'-');
153        }
154
155        // Since I is at most 64 bits, unsigned_abs() fits losslessly in u64.
156
157        let absolute_value = raw_i.unsigned_abs();
158        let integral_value = if FRAC >= 128 { 0 } else { absolute_value >> FRAC };
159
160        self.write_dec_integer(integral_value);
161        self.push_byte(sign);
162
163        let frac_mask = if FRAC >= 128 { !0u128 } else { (1u128 << FRAC) - 1 };
164        self.write_dec_integer(absolute_value & frac_mask);
165        self.push_byte(b'/');
166
167        self.write_dec_integer(1u128 << FRAC);
168    }
169
170    fn write_hex<I, const FRAC: usize>(&mut self, value: Fixed<I, FRAC>)
171    where
172        I: Into<i128> + Copy,
173    {
174        let bits = core::mem::size_of::<I>() * 8;
175        let raw_value_mask = if bits == 64 { !0u64 } else { (1u64 << bits) - 1 };
176        let raw_i: i128 = value.raw_value().into();
177        let raw_value = (raw_i as u64) & raw_value_mask;
178
179        if FRAC == bits {
180            self.push_byte(b'0');
181        } else {
182            let integral_mask = if bits == 64 {
183                !0u64
184            } else {
185                (!((1u64.wrapping_shl(FRAC as u32)).wrapping_sub(1))) & raw_value_mask
186            };
187            let integral_value = (raw_value & integral_mask) >> FRAC;
188            let integral_hex_digits = (bits - FRAC).div_ceil(4);
189            let integral_shifted = integral_value << ((16 - integral_hex_digits) * 4);
190            self.write_hex_integer(integral_shifted, integral_hex_digits, ZeroMode::NoLeadingZeros);
191        }
192
193        self.push_byte(b'.');
194        if FRAC == 0 {
195            self.push_byte(b'0');
196        } else {
197            let frac_mask = if FRAC >= 64 { !0u64 } else { (1u64 << FRAC) - 1 };
198            let fractional_value = raw_value & frac_mask;
199            let fractional_shifted = fractional_value << (64 - FRAC);
200            let fractional_hex_digits = FRAC.div_ceil(4);
201            self.write_hex_integer(
202                fractional_shifted,
203                fractional_hex_digits,
204                ZeroMode::NoTrailingZeros,
205            );
206        }
207    }
208
209    // Writes `digits` hexadecimal characters of `value` to the buffer, starting
210    // from the most significant 4 bits (bits 60-63) and shifting left.
211    //
212    // Note: The caller must shift `value` left by `(16 - digits) * 4` bits so that the
213    // value's most significant hex digit aligns with bits 60-63.
214    fn write_hex_integer(&mut self, mut value: u64, digits: usize, zero_mode: ZeroMode) {
215        if value == 0 {
216            self.push_byte(b'0');
217            return;
218        }
219
220        let mut had_nonzero = false;
221        let mut last_nonzero = self.length;
222
223        for _ in 0..digits {
224            let digit = value >> 60;
225            if digit == 0 {
226                if zero_mode == ZeroMode::NoLeadingZeros && !had_nonzero {
227                    value <<= 4;
228                    continue;
229                }
230            } else {
231                had_nonzero = true;
232                last_nonzero = self.length;
233            }
234            if digit < 10 {
235                self.push_byte(b'0' + digit as u8);
236            } else {
237                self.push_byte(b'a' + (digit - 10) as u8);
238            }
239            value <<= 4;
240        }
241
242        if zero_mode == ZeroMode::NoTrailingZeros {
243            self.length = last_nonzero + 1;
244        }
245    }
246}
247
248impl core::ops::Deref for String {
249    type Target = str;
250
251    fn deref(&self) -> &Self::Target {
252        core::str::from_utf8(&self.buffer[..self.length]).unwrap()
253    }
254}
255
256impl core::fmt::Display for String {
257    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258        f.write_str(self)
259    }
260}
261
262impl core::fmt::Debug for String {
263    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
264        f.write_str(self)
265    }
266}