This repository has been archived on 2026-08-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
donglora/vendor/fixed/src/display.rs
T
swaitsandClaude Opus 4.6 3102fc747a feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:54 -06:00

1194 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright © 2018–2025 Trevor Spiteri
// This library is free software: you can redistribute it and/or
// modify it under the terms of either
//
// * the Apache License, Version 2.0 or
// * the MIT License
//
// at your option.
//
// You should have recieved copies of the Apache License and the MIT
// License along with the library. If not, see
// <https://www.apache.org/licenses/LICENSE-2.0> and
// <https://opensource.org/licenses/MIT>.
use crate::debug_hex;
use crate::debug_hex::IsDebugHex;
use crate::int_helper;
use crate::types::extra::{LeEqU8, LeEqU16, LeEqU32, LeEqU64, LeEqU128, Unsigned};
use crate::{
FixedI8, FixedI16, FixedI32, FixedI64, FixedI128, FixedU8, FixedU16, FixedU32, FixedU64,
FixedU128,
};
use az::{WrappingAs, WrappingCast};
use core::cmp;
use core::cmp::Ordering;
use core::fmt::{
Alignment, Binary, Debug, Display, Formatter, LowerExp, LowerHex, Octal, Result as FmtResult,
UpperExp, UpperHex,
};
use core::ops::{Add, Shl, Shr};
use core::str;
// We need 129 digit bytes: 128 digits, one leading zero.
//
// The leading zero has two purposes:
//
// 1. If there are no integer digits, we still want to start with "0.".
// 2. If rounding causes a carry, we can overflow into this extra zero.
//
// In the end the layout should be:
//
// * data[0..1 + int_digits]: integer digits with 0, 1, or 2 extra zeros
// * data[1 + int_digits..1 + int_digits + frac_digits]: fractional digits
//
// exp is only used for decimal, so its range is from -39 to 38 inclusive.
struct Buffer {
int_digits: usize,
frac_digits: usize,
digits: [u8; 129],
exp: i32,
exp_len: usize,
exp_bytes: [u8; 4],
}
impl Buffer {
fn new(int_digits: u32, frac_digits: u32) -> Buffer {
assert!(int_digits + frac_digits <= 128, "out of bounds");
Buffer {
int_digits: int_digits as usize,
frac_digits: frac_digits as usize,
digits: [0; 129],
exp: 0,
exp_len: 0,
exp_bytes: [0; 4],
}
}
// does not include leading zero
fn int(&mut self) -> &mut [u8] {
let begin = 1;
let end = 1 + self.int_digits;
&mut self.digits[begin..end]
}
fn frac(&mut self) -> &mut [u8] {
let begin = 1 + self.int_digits;
let end = 1 + self.int_digits + self.frac_digits;
&mut self.digits[begin..end]
}
fn int_and_frac(&self) -> &[u8] {
let begin = 1;
let end = 1 + self.int_digits + self.frac_digits;
&self.digits[begin..end]
}
fn find_exp_dec(&mut self, max_frac_digits: Option<usize>, truncation: &mut Truncation) {
match self.int_and_frac().iter().position(|&x| x > 0) {
None => {
self.int_digits = 0;
self.frac_digits = 0;
}
Some(first) => {
// exponent is 0 when first non-zero is at position int_digits - 1
if self.int_digits > first {
let exp = self.int_digits - 1 - first;
self.int_digits -= exp;
self.frac_digits += exp;
self.exp = exp.wrapping_as::<i32>();
} else {
let neg_exp = first - self.int_digits + 1;
self.int_digits += neg_exp;
self.frac_digits -= neg_exp;
self.exp = -neg_exp.wrapping_as::<i32>();
}
// At this point, buf.frac_nbits can be greater than fmt.precision().
if let Some(max) = max_frac_digits {
if self.frac_digits > max {
let frac = self.frac();
update_truncation(truncation, &frac[max..]);
self.frac_digits = max;
}
}
}
}
}
fn inc_exp_after_rounding_up(&mut self, fixed_prec: bool) {
self.exp += 1;
// since this is with exponent, there is at least one integer digit
self.int_digits = self
.int_digits
.checked_sub(1)
.expect("expected at least one int digit for LowerExp/UpperExp");
if !fixed_prec {
self.frac_digits += 1;
}
}
fn finish(
&mut self,
format: Format,
is_neg: bool,
truncation: Truncation,
fmt: &mut Formatter,
) -> FmtResult {
let has_exp = matches!(format, Format::UpExp | Format::LowExp);
let added_ms_digit = self.round_and_trim(format.max_digit(), truncation);
// check to see if rounding increased a digit and we are printing with exponent
if added_ms_digit.0 && has_exp {
self.inc_exp_after_rounding_up(fmt.precision().is_some());
}
self.encode_digits(format == Format::UpHex);
if has_exp {
self.encode_exp(format == Format::UpExp);
}
self.pad_and_print(is_neg, format.prefix(), fmt)
}
// rounds, and then trims trailing zeros from frac.
fn round_and_trim(&mut self, max: u8, truncation: Truncation) -> RoundingAddedMSDigit {
let len = 1 + self.int_digits + self.frac_digits;
// round up if cropped is greater than tie, or if it is tie and current is odd
let is_odd = self.digits[len - 1] & 1 != 0;
let round_up =
truncation == Truncation::GreaterTie || (truncation == Truncation::Tie && is_odd);
if round_up {
let mut incremented_zero_at = None;
for (index, b) in self.digits[0..len].iter_mut().enumerate().rev() {
if *b < max {
if *b == 0 {
incremented_zero_at = Some(index)
}
*b += 1;
break;
}
*b = 0;
// trim
if self.frac_digits > 0 {
self.frac_digits -= 1;
}
}
match incremented_zero_at {
Some(index) if self.digits[0..index].iter().all(|&x| x == 0) => {
RoundingAddedMSDigit(true)
}
_ => RoundingAddedMSDigit(false),
}
} else {
for b in self.digits[len - self.frac_digits..len].iter_mut().rev() {
if *b != 0 {
break;
}
self.frac_digits -= 1;
}
RoundingAddedMSDigit(false)
}
}
fn encode_digits(&mut self, upper: bool) {
for digit in &mut self.digits[..1 + self.int_digits + self.frac_digits] {
if *digit < 10 {
*digit += b'0';
} else {
*digit += if upper { b'A' - 10 } else { b'a' - 10 };
}
}
}
fn encode_exp(&mut self, upper: bool) {
debug_assert!(-39 <= self.exp && self.exp <= 38);
self.exp_len = 1;
self.exp_bytes[0] = if upper { b'E' } else { b'e' };
if self.exp < 0 {
self.exp_len = 2;
self.exp_bytes[1] = b'-';
}
let abs_exp = self.exp.abs().wrapping_as::<u8>();
if abs_exp > 9 {
self.exp_len += 2;
let (e0, e1) = (abs_exp % 10, abs_exp / 10);
self.exp_bytes[self.exp_len - 2] = b'0' + e1;
self.exp_bytes[self.exp_len - 1] = b'0' + e0;
} else {
self.exp_len += 1;
self.exp_bytes[self.exp_len - 1] = b'0' + abs_exp;
}
}
fn pad_and_print(&self, is_neg: bool, maybe_prefix: &str, fmt: &mut Formatter) -> FmtResult {
use core::fmt::Write;
let sign = if is_neg {
"-"
} else if fmt.sign_plus() {
"+"
} else {
""
};
let prefix = if fmt.alternate() { maybe_prefix } else { "" };
// For numbers with a negative exponent:
// * digits[int_digits] is the first non-zero digit
//
// For numbers with no significant integer bits:
// * digits starts with "0" and begin = 0
//
// For numbers with some significant integer bits, data can have:
// * no leading zeros => begin = 0
// * one leading zero => begin = 1
// * two leading zeros => begin = 2
//
// Two leading zeros can happen for decimal only. For example
// with four significant integer bits, we could get anything
// between 8 and 15, so two decimal digits are allocated apart
// from the initial padding zero. This means that for 8, data
// would begin as "008.", and begin = 2.
let abs_begin = if self.exp_bytes[1] == b'-' {
self.int_digits
} else if self.int_digits == 0 || self.digits[0] != b'0' {
0
} else if self.digits[1] != b'0' {
1
} else {
2
};
let end_zeros = fmt.precision().map_or(0, |x| x - self.frac_digits);
let has_frac = self.frac_digits > 0 || end_zeros > 0;
let digits_width = 1 + self.int_digits + self.frac_digits - abs_begin;
let req_width = sign.len()
+ prefix.len()
+ digits_width
+ usize::from(has_frac)
+ end_zeros
+ self.exp_len;
let pad = fmt
.width()
.and_then(|w| w.checked_sub(req_width))
.unwrap_or(0);
let (pad_left, pad_zeros, pad_right) = if fmt.sign_aware_zero_pad() {
(0, pad, 0)
} else {
match fmt.align() {
Some(Alignment::Left) => (0, 0, pad),
Some(Alignment::Center) => (pad / 2, 0, pad - pad / 2),
None | Some(Alignment::Right) => (pad, 0, 0),
}
};
let fill = fmt.fill();
for _ in 0..pad_left {
fmt.write_char(fill)?;
}
fmt.write_str(sign)?;
fmt.write_str(prefix)?;
for _ in 0..pad_zeros {
fmt.write_char('0')?;
}
let int_bytes = &self.digits[abs_begin..1 + self.int_digits];
fmt.write_str(str::from_utf8(int_bytes).unwrap())?;
if has_frac {
fmt.write_char('.')?;
let frac_bytes =
&self.digits[1 + self.int_digits..1 + self.int_digits + self.frac_digits];
fmt.write_str(str::from_utf8(frac_bytes).unwrap())?;
for _ in 0..end_zeros {
fmt.write_char('0')?;
}
}
fmt.write_str(str::from_utf8(&self.exp_bytes[..self.exp_len]).unwrap())?;
for _ in 0..pad_right {
fmt.write_char(fill)?;
}
Ok(())
}
}
fn update_truncation(truncation: &mut Truncation, mut truncated_frac: &[u8]) {
if truncated_frac.is_empty() {
return;
}
let mut leading_zeros = false;
while let [0, tail @ ..] = truncated_frac {
leading_zeros = true;
truncated_frac = tail;
}
if truncated_frac.is_empty() {
// truncation contained at least one zero, and no non-zeros
if *truncation != Truncation::Zero {
*truncation = Truncation::LessTie
}
return;
}
if leading_zeros {
// removed zeros, but there are still non-zero digits
*truncation = Truncation::LessTie;
return;
}
// did not remove any zeros, and we have at least one digit to remove
let first = truncated_frac[0];
truncated_frac = &truncated_frac[1..];
if first < 5 {
*truncation = Truncation::LessTie;
return;
}
if first > 5 || *truncation != Truncation::Zero {
*truncation = Truncation::GreaterTie;
return;
}
// if we only have zeros next, we have a tie
while let [0, tail @ ..] = truncated_frac {
truncated_frac = tail;
}
*truncation = if truncated_frac.is_empty() {
Truncation::Tie
} else {
Truncation::GreaterTie
}
}
#[derive(Clone, Copy, PartialEq)]
enum Format {
Bin,
Oct,
LowHex,
UpHex,
Dec,
LowExp,
UpExp,
}
#[derive(PartialEq)]
enum Truncation {
Zero,
LessTie,
Tie,
GreaterTie,
}
struct RoundingAddedMSDigit(bool);
impl Format {
fn digit_bits(self) -> u32 {
match self {
Format::Bin => 1,
Format::Oct => 3,
Format::LowHex | Format::UpHex => 4,
Format::Dec | Format::LowExp | Format::UpExp => 4,
}
}
fn max_digit(self) -> u8 {
match self {
Format::Bin => 1,
Format::Oct => 7,
Format::LowHex | Format::UpHex => 15,
Format::Dec | Format::LowExp | Format::UpExp => 9,
}
}
fn prefix(self) -> &'static str {
match self {
Format::Bin => "0b",
Format::Oct => "0o",
Format::LowHex | Format::UpHex => "0x",
Format::Dec | Format::LowExp | Format::UpExp => "",
}
}
}
trait FmtHelper
where
Self: Copy + Ord,
Self: Shl<u32, Output = Self> + Shr<u32, Output = Self> + Add<Output = Self>,
Self: WrappingCast<u8> + Mul10 + From<u8>,
{
const ZERO: Self;
const MSB: Self;
const BITS: u32;
type Half: FmtHelper;
fn int_used_nbits(int: Self) -> u32;
fn frac_used_nbits(frac: Self) -> u32;
fn as_half(val: Self) -> Self::Half;
fn div_rem_10(val: Self) -> (Self, u8);
fn wrapping_neg(val: Self) -> Self;
fn write_int_radix2(mut int: Self, format: Format, nbits: u32, buf: &mut Buffer) {
if Self::Half::BITS == Self::BITS / 2 && nbits <= Self::Half::BITS {
return FmtHelper::write_int_radix2(Self::as_half(int), format, nbits, buf);
}
let digit_bits = format.digit_bits();
let mask = format.max_digit();
for b in buf.int().iter_mut().rev() {
debug_assert!(int != Self::ZERO);
*b = int.wrapping_as::<u8>() & mask;
int = int >> digit_bits;
}
debug_assert!(int == Self::ZERO);
}
fn write_frac_radix2(
mut frac: Self,
format: Format,
nbits: u32,
buf: &mut Buffer,
) -> Truncation {
if Self::Half::BITS == Self::BITS / 2 && nbits <= Self::Half::BITS {
return FmtHelper::write_frac_radix2(
Self::as_half(frac >> Self::Half::BITS),
format,
nbits,
buf,
);
}
let digit_bits = format.digit_bits();
let compl_digit_bits = Self::BITS - digit_bits;
for b in &mut *buf.frac() {
debug_assert!(frac != Self::ZERO);
*b = (frac >> compl_digit_bits).wrapping_as::<u8>();
frac = frac << digit_bits;
}
if frac == Self::ZERO {
Truncation::Zero
} else {
match frac.cmp(&Self::MSB) {
Ordering::Less => Truncation::LessTie,
Ordering::Equal => Truncation::Tie,
Ordering::Greater => Truncation::GreaterTie,
}
}
}
// returns the number of significant digits
fn write_int_dec(mut int: Self, nbits: u32, buf: &mut Buffer) -> usize {
if Self::Half::BITS == Self::BITS / 2 && nbits <= Self::Half::BITS {
return FmtHelper::write_int_dec(Self::as_half(int), nbits, buf);
}
let mut sig = 0;
for b in buf.int().iter_mut().rev() {
let (q, r) = Self::div_rem_10(int);
int = q;
*b = r;
if r != 0 || sig != 0 {
sig += 1;
}
}
debug_assert!(int == Self::ZERO);
sig
}
// The truncated bits are compared to the half-way tie, and the comparison
// result is returned.
fn write_frac_dec(
mut frac: Self,
nbits: u32,
frac_format: DecFracFormat,
buf: &mut Buffer,
) -> Truncation {
if Self::Half::BITS == Self::BITS / 2 && nbits <= Self::Half::BITS {
return FmtHelper::write_frac_dec(
Self::as_half(frac >> Self::Half::BITS),
nbits,
frac_format,
buf,
);
}
let mut is_past_point = !frac_format.has_exp || frac_format.int_sig_digits > 0;
let (auto_prec, mut rem_prec) = match frac_format.precision {
Some(prec) => (false, prec),
None => (true, 0),
};
if !auto_prec && frac_format.has_exp && is_past_point {
rem_prec = rem_prec.saturating_sub(frac_format.int_sig_digits - 1);
}
if !auto_prec && is_past_point && buf.frac_digits > rem_prec {
buf.frac_digits = rem_prec;
}
// add_5 is to add rounding when all bits are used
let (mut tie, mut add_5) = if nbits == Self::BITS {
(Self::ZERO, true)
} else {
(Self::MSB >> nbits, false)
};
for (i, b) in buf.frac().iter_mut().enumerate() {
*b = Mul10::mul10_assign(&mut frac);
// Check if very close to zero, to avoid things like 0.19999999 and 0.20000001.
if auto_prec && frac < Self::from(10) || Self::wrapping_neg(frac) < Self::from(10) {
buf.frac_digits = i + 1;
break;
}
if auto_prec {
// tie might overflow in last iteration when i = frac_digits - 1,
// but it has no effect as all it can do is set frac_digits to i + 1
Mul10::mul10_assign(&mut tie);
if add_5 {
tie = tie + Self::from(5);
add_5 = false;
}
if frac < tie || Self::wrapping_neg(frac) < tie {
buf.frac_digits = i + 1;
break;
}
} else if frac_format.has_exp {
debug_assert!(rem_prec > 0);
if is_past_point {
rem_prec -= 1;
if rem_prec == 0 {
buf.frac_digits = i + 1;
break;
}
} else if *b != 0 {
is_past_point = true;
// *b is still before point, so do not decrement rem_prec here.
}
}
}
if frac == Self::ZERO {
Truncation::Zero
} else {
match frac.cmp(&Self::MSB) {
Ordering::Less => Truncation::LessTie,
Ordering::Equal => Truncation::Tie,
Ordering::Greater => Truncation::GreaterTie,
}
}
}
}
macro_rules! impl_format_helper {
($U:ident, $H:ident) => {
impl FmtHelper for $U {
const ZERO: $U = 0;
const MSB: $U = 1 << ($U::BITS - 1);
const BITS: u32 = $U::BITS;
type Half = $H;
fn int_used_nbits(int: $U) -> u32 {
$U::BITS - int.leading_zeros()
}
fn frac_used_nbits(frac: $U) -> u32 {
$U::BITS - frac.trailing_zeros()
}
fn as_half(val: $U) -> Self::Half {
val as Self::Half
}
fn div_rem_10(val: $U) -> ($U, u8) {
(val / 10, (val % 10).wrapping_cast())
}
fn wrapping_neg(val: $U) -> $U {
val.wrapping_neg()
}
}
};
}
impl_format_helper! { u8, u8 }
impl_format_helper! { u16, u8 }
impl_format_helper! { u32, u16 }
impl_format_helper! { u64, u32 }
impl_format_helper! { u128, u64 }
fn fmt<U: FmtHelper>(
(neg, abs): (bool, U),
frac_nbits: u32,
format: Format,
fmt: &mut Formatter,
) -> FmtResult {
let (int, frac) = if frac_nbits == 0 {
(abs, U::ZERO)
} else if frac_nbits == U::BITS {
(U::ZERO, abs)
} else {
(abs >> frac_nbits, abs << (U::BITS - frac_nbits))
};
match format {
Format::Bin | Format::Oct | Format::LowHex | Format::UpHex => {
fmt_radix2((neg, int, frac), format, fmt)
}
Format::Dec | Format::LowExp | Format::UpExp => {
fmt_dec((neg, int, frac), frac_nbits, format, fmt)
}
}
}
#[derive(Clone, Copy, Debug)]
struct DecFracFormat {
int_sig_digits: usize,
has_exp: bool,
precision: Option<usize>,
}
fn fmt_dec<U: FmtHelper>(
(neg, int, frac): (bool, U, U),
frac_nbits: u32,
format: Format,
fmt: &mut Formatter,
) -> FmtResult {
let int_used_nbits = FmtHelper::int_used_nbits(int);
let frac_used_nbits = FmtHelper::frac_used_nbits(frac);
let int_max_len = ceil_log10_2_times(int_used_nbits);
let frac_max_len = if fmt.precision().is_some() {
// for specified precision, we want exact fractions till the very end
frac_used_nbits
} else {
// for auto precision, we don't need more than ceil(log10(2) × frac_nbits)
ceil_log10_2_times(frac_nbits)
};
let mut buf = Buffer::new(int_max_len, frac_max_len);
let int_sig_digits = FmtHelper::write_int_dec(int, int_used_nbits, &mut buf);
let has_exp = matches!(format, Format::UpExp | Format::LowExp);
let frac_format = DecFracFormat {
int_sig_digits,
has_exp,
precision: fmt.precision(),
};
let mut truncation = FmtHelper::write_frac_dec(frac, frac_nbits, frac_format, &mut buf);
if has_exp {
buf.find_exp_dec(fmt.precision(), &mut truncation);
}
buf.finish(format, neg, truncation, fmt)
}
fn fmt_radix2<U: FmtHelper>(
(neg, int, frac): (bool, U, U),
format: Format,
fmt: &mut Formatter,
) -> FmtResult {
let digit_bits = format.digit_bits();
let int_used_nbits = FmtHelper::int_used_nbits(int);
let int_digits = int_used_nbits.div_ceil(digit_bits);
let frac_used_nbits = FmtHelper::frac_used_nbits(frac);
let mut frac_digits = frac_used_nbits.div_ceil(digit_bits);
if let Some(precision) = fmt.precision() {
// frac_digits fits in usize, but precision might wrap to 0 in u32
frac_digits = cmp::min(frac_digits as usize, precision) as u32;
}
let mut buf = Buffer::new(int_digits, frac_digits);
FmtHelper::write_int_radix2(int, format, int_used_nbits, &mut buf);
// for bin, oct, hex, we can simply pass frac_used_bits to write_frac
let frac_rem_cmp_tie = FmtHelper::write_frac_radix2(frac, format, frac_used_nbits, &mut buf);
buf.finish(format, neg, frac_rem_cmp_tie, fmt)
}
macro_rules! impl_fmt {
($Fixed:ident($LeEqU:ident, $Inner:ident)) => {
impl<Frac: $LeEqU> Display for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::Dec, f)
}
}
impl<Frac: Unsigned> Debug for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
if Frac::U32 > $Inner::BITS {
match debug_hex::is_debug_hex(f) {
IsDebugHex::Lower => {
f.write_fmt(format_args!("(0x{:x}", self.to_bits()))?;
}
IsDebugHex::Upper => {
f.write_fmt(format_args!("(0x{:X}", self.to_bits()))?;
}
IsDebugHex::No => {
f.write_fmt(format_args!("({}", self.to_bits()))?;
}
}
return f.write_fmt(format_args!(" >> {})", Frac::U32));
}
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
match debug_hex::is_debug_hex(f) {
IsDebugHex::Lower => fmt(neg_abs, Frac::U32, Format::LowHex, f),
IsDebugHex::Upper => fmt(neg_abs, Frac::U32, Format::UpHex, f),
IsDebugHex::No => fmt(neg_abs, Frac::U32, Format::Dec, f),
}
}
}
impl<Frac: $LeEqU> Binary for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::Bin, f)
}
}
impl<Frac: $LeEqU> Octal for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::Oct, f)
}
}
impl<Frac: $LeEqU> LowerHex for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::LowHex, f)
}
}
impl<Frac: $LeEqU> UpperHex for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::UpHex, f)
}
}
impl<Frac: $LeEqU> LowerExp for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::LowExp, f)
}
}
impl<Frac: $LeEqU> UpperExp for $Fixed<Frac> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let neg_abs = int_helper::$Inner::neg_abs(self.to_bits());
fmt(neg_abs, Self::FRAC_NBITS, Format::UpExp, f)
}
}
};
}
impl_fmt! { FixedU8(LeEqU8, u8) }
impl_fmt! { FixedU16(LeEqU16, u16) }
impl_fmt! { FixedU32(LeEqU32, u32) }
impl_fmt! { FixedU64(LeEqU64, u64) }
impl_fmt! { FixedU128(LeEqU128, u128) }
impl_fmt! { FixedI8(LeEqU8, i8) }
impl_fmt! { FixedI16(LeEqU16, i16) }
impl_fmt! { FixedI32(LeEqU32, i32) }
impl_fmt! { FixedI64(LeEqU64, i64) }
impl_fmt! { FixedI128(LeEqU128, i128) }
// ceil(i × log_10 2), works for input < 112_816
fn ceil_log10_2_times(int_bits: u32) -> u32 {
debug_assert!(int_bits < 112_816);
((u64::from(int_bits) * 0x4D10_4D43 + 0xFFFF_FFFF) >> 32) as u32
}
pub(crate) trait Mul10: Sized {
fn mul10_assign(slf: &mut Self) -> u8;
}
macro_rules! mul10_widen {
($Single:ty, $Double:ty) => {
impl Mul10 for $Single {
#[inline]
fn mul10_assign(x: &mut $Single) -> u8 {
let prod = <$Double>::from(*x) * 10;
*x = prod as $Single;
(prod >> <$Single>::BITS) as u8
}
}
};
}
mul10_widen! { u8, u16 }
mul10_widen! { u16, u32 }
mul10_widen! { u32, u64 }
mul10_widen! { u64, u128 }
impl Mul10 for u128 {
#[inline]
fn mul10_assign(x: &mut u128) -> u8 {
const LO_MASK: u128 = !(!0 << 64);
let hi = (*x >> 64) * 10;
let lo = (*x & LO_MASK) * 10;
// Workaround for https://github.com/rust-lang/rust/issues/63384
// let (wrapped, overflow) = (hi << 64).overflowing_add(lo);
// ((hi >> 64) as u8 + u8::from(overflow), wrapped)
let (hi_lo, hi_hi) = (hi as u64, (hi >> 64) as u64);
let (lo_lo, lo_hi) = (lo as u64, (lo >> 64) as u64);
let (wrapped, overflow) = hi_lo.overflowing_add(lo_hi);
*x = (u128::from(wrapped) << 64) | u128::from(lo_lo);
hi_hi as u8 + u8::from(overflow)
}
}
#[cfg(test)]
mod tests {
use crate::display;
use crate::types::*;
use std::format;
#[cfg(not(feature = "std"))]
use std::string::{String, ToString};
#[test]
fn format() {
let pos = I16F16::from_num(12.3);
assert_eq!(format!("{pos:+}"), "+12.3");
assert_eq!(format!("{pos:.20}"), "12.30000305175781250000");
assert_eq!(format!("{pos:+08}"), "+00012.3");
assert_eq!(format!("{pos:+#08}"), "+00012.3");
assert_eq!(format!("{pos:+08X}"), "+0C.4CCD");
assert_eq!(format!("{pos:+08.1X}"), "+0000C.5");
assert_eq!(format!("{pos:+#08X}"), "+0xC.4CCD");
assert_eq!(format!("{pos:+#08.1X}"), "+0x00C.5");
assert_eq!(format!("{pos:#<8}"), "12.3####");
assert_eq!(format!("{pos:#^8}"), "##12.3##");
assert_eq!(format!("{pos:#^9}"), "##12.3###");
assert_eq!(format!("{pos:#>8}"), "####12.3");
assert_eq!(format!("{pos:#^08}"), "000012.3");
assert_eq!(format!("{pos:^8e}"), " 1.23e1 ");
assert_eq!(format!("{pos:^08E}"), "001.23E1");
assert_eq!(format!("{pos:.20e}"), "1.23000030517578125000e1");
let pos = I16F16::from_bits(9);
assert_eq!(format!("{pos}"), "0.00014");
assert_eq!(format!("{pos:.16}"), "0.0001373291015625");
assert_eq!(format!("{pos:e}"), "1.4e-4");
assert_eq!(format!("{pos:.16e}"), "1.3732910156250000e-4");
assert_eq!(format!("{:.1e}", I16F16::from_num(999)), "1.0e3");
assert_eq!(format!("{:.1e}", I16F16::from_num(1000)), "1.0e3");
assert_eq!(format!("{:.1e}", I16F16::from_num(1001)), "1.0e3");
assert_eq!(format!("{:.1e}", U0F32::lit("0.999")), "1.0e0");
assert_eq!(format!("{:.1e}", I16F16::lit("0.999")), "1.0e0");
assert_eq!(format!("{:.2e}", I16F16::lit("9.099")), "9.10e0");
}
fn trim_frac_zeros(mut x: &str) -> &str {
while x.ends_with('0') {
x = &x[..x.len() - 1];
}
if x.ends_with('.') {
x = &x[..x.len() - 1];
}
x
}
fn up_frac_digits(x: &mut String, frac_digits: usize) {
if let Some(point) = x.find('.') {
if let Some(additional) = frac_digits.checked_sub(x.len() - point - 1) {
x.reserve(additional);
for _ in 0..additional {
x.push('0');
}
}
} else {
x.reserve(frac_digits + 1);
x.push('.');
for _ in 0..frac_digits {
x.push('0');
}
}
}
#[test]
fn hex() {
for i in 0..(1u32 << 7) {
let p = 0x1234_5678_9abc_def0u64 ^ u64::from(i);
let n = -0x1234_5678_9abc_def0i64 ^ i64::from(i);
let f_p = U57F7::from_bits(p);
let f_n = I57F7::from_bits(n);
let mut check_p = format!("{:x}.{:02x}", p >> 7, (p & 0x7f) << 1);
up_frac_digits(&mut check_p, 1000);
let trimmed_p = trim_frac_zeros(&check_p);
let mut check_n = format!("-{:x}.{:02x}", n.abs() >> 7, (n.abs() & 0x7f) << 1);
up_frac_digits(&mut check_n, 1000);
let trimmed_n = trim_frac_zeros(&check_n);
assert_eq!(format!("{f_p:.1000x}"), check_p);
assert_eq!(format!("{f_p:x}"), trimmed_p);
assert_eq!(format!("{f_n:.1000x}"), check_n);
assert_eq!(format!("{f_n:x}"), trimmed_n);
}
}
#[test]
fn debug_hex() {
let v = I16F16::MAX;
assert_eq!(format!("{v:?}"), "32767.99998");
assert_eq!(format!("{v:x?}"), "7fff.ffff");
assert_eq!(format!("{v:X?}"), "7FFF.FFFF");
assert_eq!(format!("{v:010X?}"), "07FFF.FFFF");
}
#[test]
fn dec() {
for i in 0..(1 << 7) {
// use 24 bits of precision to be like f32
let bits = (!0u32 >> 8) ^ i;
let fix = U25F7::from_bits(bits);
let flt = (bits as f32) / 7f32.exp2();
assert_eq!(format!("{fix}"), format!("{flt}"));
assert_eq!(U25F7::from_num(flt), fix);
assert_eq!(fix.to_num::<f32>(), flt);
}
}
#[test]
fn display_frac() {
assert_eq!(
format!("{:X}", I0F128::from_bits(!0)),
"-0.00000000000000000000000000000001"
);
assert_eq!(format!("{:X}", I0F64::from_bits(!0)), "-0.0000000000000001");
assert_eq!(format!("{:X}", I0F32::from_bits(!0)), "-0.00000001");
assert_eq!(format!("{:X}", I0F16::from_bits(!0)), "-0.0001");
assert_eq!(format!("{:X}", I0F8::from_bits(!0)), "-0.01");
assert_eq!(
format!("{:X}", U0F128::from_bits(!0)),
"0.FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
);
assert_eq!(format!("{:X}", U0F64::from_bits(!0)), "0.FFFFFFFFFFFFFFFF");
assert_eq!(format!("{:X}", U0F32::from_bits(!0)), "0.FFFFFFFF");
assert_eq!(format!("{:X}", U0F16::from_bits(!0)), "0.FFFF");
assert_eq!(format!("{:X}", U0F8::from_bits(!0)), "0.FF");
assert_eq!(
format!("{}", I0F128::from_bits(!0)),
"-0.000000000000000000000000000000000000003"
);
assert_eq!(
format!("{}", I0F64::from_bits(!0)),
"-0.00000000000000000005"
);
assert_eq!(format!("{}", I0F32::from_bits(!0)), "-0.0000000002");
assert_eq!(format!("{}", I0F16::from_bits(!0)), "-0.00002");
assert_eq!(format!("{}", I0F8::from_bits(!0)), "-0.004");
assert_eq!(
format!("{}", U0F128::from_bits(!0)),
"0.999999999999999999999999999999999999997"
);
assert_eq!(
format!("{}", U0F64::from_bits(!0)),
"0.99999999999999999995"
);
assert_eq!(format!("{}", U0F32::from_bits(!0)), "0.9999999998");
assert_eq!(format!("{}", U0F16::from_bits(!0)), "0.99998");
assert_eq!(format!("{}", U0F8::from_bits(!0)), "0.996");
// check overflow issues in <u128 as Mul10>::mul10
let no_internal_overflow_bits = 0xe666_6666_6666_6665_ffff_ffff_ffff_ffffu128;
let internal_overflow_bits = 0xe666_6666_6666_6666_ffff_ffff_ffff_ffffu128;
assert_eq!(
format!("{:X}", U0F128::from_bits(no_internal_overflow_bits)),
"0.E666666666666665FFFFFFFFFFFFFFFF"
);
assert_eq!(
format!("{:X}", U0F128::from_bits(internal_overflow_bits)),
"0.E666666666666666FFFFFFFFFFFFFFFF"
);
assert_eq!(
format!("{}", U0F128::from_bits(no_internal_overflow_bits)),
"0.899999999999999999978315956550289911317"
);
assert_eq!(
format!("{}", U0F128::from_bits(internal_overflow_bits)),
"0.900000000000000000032526065174565133017"
);
}
#[test]
fn close_to_round_decimal() {
for i in 0..1000u16 {
// f32 has 24 bits of precision, so we use 1 bit for the
// integer part to have exactly 23 bits for the fraction
let float = f32::from(i + 1000) / 1000.;
let fix = U9F23::from_num(float);
let check = format!("1.{i:03}");
assert_eq!(format!("{fix}"), trim_frac_zeros(&check));
assert_eq!(format!("{fix}"), format!("{float}"));
for prec in 0..10 {
assert_eq!(format!("{fix:.prec$}"), format!("{float:.prec$}"));
}
}
}
#[test]
fn check_ceil_log10_2_times() {
for i in 0..112_816 {
let check = (f64::from(i) * 2f64.log10()).ceil() as u32;
assert_eq!(display::ceil_log10_2_times(i), check);
}
}
#[test]
fn rounding() {
let i = U8F8::from_bits(0xFF80);
assert_eq!(format!("{i}"), "255.5");
assert_eq!(format!("{i:?}"), "255.5");
assert_eq!(format!("{i:.0}"), "256");
assert_eq!(format!("{i:b}"), "11111111.1");
assert_eq!(format!("{i:.0b}"), "100000000");
assert_eq!(format!("{i:o}"), "377.4");
assert_eq!(format!("{i:.0o}"), "400");
assert_eq!(format!("{i:X}"), "FF.8");
assert_eq!(format!("{i:.0X}"), "100");
let i = U8F8::from_bits(0xFE80);
assert_eq!(format!("{i}"), "254.5");
assert_eq!(format!("{i:?}"), "254.5");
assert_eq!(format!("{i:.0}"), "254");
assert_eq!(format!("{i:b}"), "11111110.1");
assert_eq!(format!("{i:.0b}"), "11111110");
assert_eq!(format!("{i:o}"), "376.4");
assert_eq!(format!("{i:.0o}"), "376");
assert_eq!(format!("{i:X}"), "FE.8");
assert_eq!(format!("{i:.0X}"), "FE");
let i = U8F8::from_bits(0xDDDD);
assert_eq!(format!("{i}"), "221.863");
assert_eq!(format!("{i:?}"), "221.863");
assert_eq!(format!("{i:.0}"), "222");
assert_eq!(format!("{i:.1}"), "221.9");
assert_eq!(format!("{i:.2}"), "221.86");
assert_eq!(format!("{i:.3}"), "221.863");
assert_eq!(format!("{i:.4}"), "221.8633");
assert_eq!(format!("{i:.5}"), "221.86328");
assert_eq!(format!("{i:.6}"), "221.863281");
assert_eq!(format!("{i:.7}"), "221.8632812");
assert_eq!(format!("{i:.8}"), "221.86328125");
assert_eq!(format!("{i:.9}"), "221.863281250");
assert_eq!(format!("{i:b}"), "11011101.11011101");
assert_eq!(format!("{i:.0b}"), "11011110");
assert_eq!(format!("{i:.1b}"), "11011110.0");
assert_eq!(format!("{i:.2b}"), "11011101.11");
assert_eq!(format!("{i:.3b}"), "11011101.111");
assert_eq!(format!("{i:.4b}"), "11011101.1110");
assert_eq!(format!("{i:.5b}"), "11011101.11100");
assert_eq!(format!("{i:.6b}"), "11011101.110111");
assert_eq!(format!("{i:.7b}"), "11011101.1101110");
assert_eq!(format!("{i:.8b}"), "11011101.11011101");
assert_eq!(format!("{i:.9b}"), "11011101.110111010");
assert_eq!(format!("{i:o}"), "335.672");
assert_eq!(format!("{i:.0o}"), "336");
assert_eq!(format!("{i:.1o}"), "335.7");
assert_eq!(format!("{i:.2o}"), "335.67");
assert_eq!(format!("{i:.3o}"), "335.672");
assert_eq!(format!("{i:.4o}"), "335.6720");
assert_eq!(format!("{i:X}"), "DD.DD");
assert_eq!(format!("{i:.0X}"), "DE");
assert_eq!(format!("{i:.0X}"), "DE");
assert_eq!(format!("{i:.1X}"), "DD.E");
assert_eq!(format!("{i:.2X}"), "DD.DD");
assert_eq!(format!("{i:.3X}"), "DD.DD0");
}
#[test]
fn compare_frac0_int() {
for u in 0..=255u8 {
let i = u as i8;
let (ifix, ufix) = (I8F0::from_bits(i), U8F0::from_bits(u));
assert_eq!(ifix.to_string(), i.to_string());
assert_eq!(ufix.to_string(), u.to_string());
if i >= 0 {
assert_eq!(format!("{ifix:#X}"), format!("{i:#X}"));
assert_eq!(format!("{ifix:#b}"), format!("{i:#b}"));
} else {
let abs_i = i.wrapping_neg() as u8;
assert_eq!(format!("{ifix:#X}"), format!("-{abs_i:#X}"));
assert_eq!(format!("{ifix:#b}"), format!("-{abs_i:#b}"));
}
assert_eq!(format!("{ufix:#x}"), format!("{u:#x}"));
assert_eq!(format!("{ufix:#o}"), format!("{u:#o}"));
}
}
#[test]
fn compare_frac4_float() {
for u in 0..=255u8 {
// I4F4 and U4F4 are displayed like f32 when the f32
// display precision is the number of fractional digits
// displayed for fixed-point. This verifies correct display
// of the integer part.
let (ifix, ufix) = (I4F4::from_bits(u as i8), U4F4::from_bits(u));
let (iflo, uflo) = (ifix.to_num::<f32>(), ufix.to_num::<f32>());
let (sifix, sufix) = (ifix.to_string(), ufix.to_string());
let pifix = sifix.find('.').map_or(0, |p| sifix.len() - 1 - p);
let pufix = sufix.find('.').map_or(0, |p| sufix.len() - 1 - p);
let (siflo, suflo) = (format!("{iflo:.pifix$}"), format!("{uflo:.pufix$}"));
assert_eq!(sifix, siflo);
assert_eq!(sufix, suflo);
// I28F4 and U28F4 are displayed like f32 when the f32 has
// four bits of precision dedicated to the fractional
// part. For f32, this requires the magnitude’s integer
// part to have 20 significant bits: (1 << 19)..(1 << 20).
let ifixed =
I28F4::from(ifix) + I28F4::from_num(i32::from(ifix.to_bits().signum()) << 19);
let ufixed = U28F4::from(ufix) + U28F4::from_num(1 << 19);
let (ifloat, ufloat) = (ifixed.to_num::<f32>(), ufixed.to_num::<f32>());
let (sifixed, sufixed) = (ifixed.to_string(), ufixed.to_string());
let (sifloat, sufloat) = (ifloat.to_string(), ufloat.to_string());
assert_eq!(sifixed, sifloat);
assert_eq!(sufixed, sufloat);
// The fractional parts of I4F4 and U4F4 are displayed
// like the fractional parts of I28F4 and U28F4
// respectively.
let sifix_frac = sifix.find('.').map(|i| &sifix[i..]);
let sifixed_frac = sifixed.find('.').map(|i| &sifixed[i..]);
assert_eq!(sifix_frac, sifixed_frac);
let sufix_frac = sufix.find('.').map(|i| &sufix[i..]);
let sufixed_frac = sufixed.find('.').map(|i| &sufixed[i..]);
assert_eq!(sufix_frac, sufixed_frac);
}
}
#[test]
fn compare_frac17_float() {
for u in 0..(1 << 17) {
// 24 bits of precision: 17 fractional bits + 7 significant integer bits
let fix = U15F17::from_bits(u) + U15F17::from_num(99);
let fix_pos = I15F17::from_num(fix);
let fix_neg = -fix_pos;
let (flo, flo_neg) = (fix.to_num::<f32>(), fix_neg.to_num::<f32>());
let fix_str = fix.to_string();
let fix_pos_str = fix_pos.to_string();
let fix_neg_str = fix_neg.to_string();
assert_eq!(fix_str, flo.to_string());
assert_eq!(fix_str, fix_pos_str);
assert_eq!(fix_neg_str, flo_neg.to_string());
if u != 0 {
assert_eq!(&fix_neg_str[..1], "-");
assert_eq!(&fix_neg_str[1..], fix_pos_str);
}
let fix_str3 = format!("{fix:.3}");
let fix_pos_str3 = format!("{fix_pos:.3}");
let fix_neg_str3 = format!("{fix_neg:.3}");
assert_eq!(fix_str3, format!("{flo:.3}"));
assert_eq!(fix_str3, fix_pos_str3);
assert_eq!(fix_neg_str3, format!("{flo_neg:.3}"));
if u != 0 {
assert_eq!(&fix_neg_str3[..1], "-");
assert_eq!(&fix_neg_str3[1..], fix_pos_str3);
}
}
}
#[test]
fn exp_small_numbers() {
assert_eq!(format!("{:e}", I0F64::DELTA), "5e-20");
assert_eq!(format!("{:e}", I0F64::DELTA * 2), "1e-19");
assert_eq!(format!("{:e}", I0F64::DELTA * 3), "1.6e-19");
assert_eq!(format!("{:e}", I0F64::DELTA * 4), "2e-19");
}
}