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