Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `bigdecimal` support ([#486])
- `PartialEq` and `PartialOrd` implementations for primitive integers; minor breaking change for type inference ([#491])

### Changed

- `to_base_be` and `core::fmt` trait implementations are available without the "alloc" feature ([#488])

### Fixed

- Check limb overflow in shift ops ([#476])

[#476]: https://github.com/recmo/uint/pull/476
[#483]: https://github.com/recmo/uint/pull/483
[#486]: https://github.com/recmo/uint/pull/486
[#488]: https://github.com/recmo/uint/pull/488
[#491]: https://github.com/recmo/uint/pull/491

## [1.15.0] - 2025-05-22
Expand Down
158 changes: 137 additions & 21 deletions src/base_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ impl<const BITS: usize, const LIMBS: usize> Uint<BITS, LIMBS> {
/// Panics if the base is less than 2.
#[inline]
pub fn to_base_le(&self, base: u64) -> impl Iterator<Item = u64> {
assert!(base > 1);
SpigotLittle {
base,
limbs: self.limbs,
}
SpigotLittle::new(self.limbs, base)
}

/// Returns an iterator over the base `base` digits of the number in
Expand All @@ -68,24 +64,19 @@ impl<const BITS: usize, const LIMBS: usize> Uint<BITS, LIMBS> {
///
/// Panics if the base is less than 2.
#[inline]
#[cfg(feature = "alloc")] // OPT: Find an allocation free method. Maybe extract from the top?
pub fn to_base_be(&self, base: u64) -> impl Iterator<Item = u64> {
struct OwnedVecIterator {
vec: alloc::vec::Vec<u64>,
}

impl Iterator for OwnedVecIterator {
type Item = u64;

#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.vec.pop()
}
// Use `to_base_le` if we can heap-allocate it to reverse the order,
// as it only performs one division per iteration instead of two.
#[cfg(feature = "alloc")]
{
self.to_base_le(base)
.collect::<alloc::vec::Vec<_>>()
.into_iter()
.rev()
}

assert!(base > 1);
OwnedVecIterator {
vec: self.to_base_le(base).collect(),
#[cfg(not(feature = "alloc"))]
{
SpigotBig::new(*self, base)
}
}

Expand Down Expand Up @@ -196,6 +187,15 @@ struct SpigotLittle<const LIMBS: usize> {
limbs: [u64; LIMBS],
}

impl<const LIMBS: usize> SpigotLittle<LIMBS> {
#[inline]
#[track_caller]
fn new(limbs: [u64; LIMBS], base: u64) -> Self {
assert!(base > 1);
Self { base, limbs }
}
}

impl<const LIMBS: usize> Iterator for SpigotLittle<LIMBS> {
type Item = u64;

Expand All @@ -220,6 +220,90 @@ impl<const LIMBS: usize> Iterator for SpigotLittle<LIMBS> {
}
}

/// Implementation of `to_base_be` when `alloc` feature is disabled.
///
/// This is generally slower than simply reversing the result of `to_base_le`
/// as it performs two divisions per iteration instead of one.
#[cfg(not(feature = "alloc"))]
struct SpigotBig<const LIMBS: usize, const BITS: usize> {
base: u64,
n: Uint<BITS, LIMBS>,
power: Uint<BITS, LIMBS>,
done: bool,
}

#[cfg(not(feature = "alloc"))]
impl<const LIMBS: usize, const BITS: usize> SpigotBig<LIMBS, BITS> {
#[inline]
#[track_caller]
fn new(n: Uint<BITS, LIMBS>, base: u64) -> Self {
assert!(base > 1);

Self {
n,
base,
power: Self::highest_power(n, base),
done: false,
}
}

/// Returns the largest power of `base` that fits in `n`.
#[inline]
fn highest_power(n: Uint<BITS, LIMBS>, base: u64) -> Uint<BITS, LIMBS> {
let mut power = Uint::ONE;
if base.is_power_of_two() {
loop {
match power.checked_shl(base.trailing_zeros() as _) {
Some(p) if p < n => power = p,
_ => break,
}
}
} else if let Ok(base) = Uint::try_from(base) {
loop {
match power.checked_mul(base) {
Some(p) if p < n => power = p,
_ => break,
}
}
}
power
}
}

#[cfg(not(feature = "alloc"))]
impl<const LIMBS: usize, const BITS: usize> Iterator for SpigotBig<LIMBS, BITS> {
type Item = u64;

#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}

let digit;
if self.power == 1 {
digit = self.n;
self.done = true;
} else if self.base.is_power_of_two() {
digit = self.n >> self.power.trailing_zeros();
self.n &= self.power - Uint::ONE;

self.power >>= self.base.trailing_zeros();
} else {
(digit, self.n) = self.n.div_rem(self.power);
self.power /= Uint::from(self.base);
}

match u64::try_from(digit) {
Ok(digit) => Some(digit),
Err(e) => debug_unreachable!("digit {digit}: {e}"),
}
}
}

#[cfg(not(feature = "alloc"))]
impl<const LIMBS: usize, const BITS: usize> core::iter::FusedIterator for SpigotBig<LIMBS, BITS> {}

#[cfg(test)]
#[allow(clippy::unreadable_literal)]
#[allow(clippy::zero_prefixed_literal)]
Expand Down Expand Up @@ -331,4 +415,36 @@ mod tests {
Err(BaseConvertError::Overflow)
);
}

#[test]
fn test_roundtrip() {
fn test<const BITS: usize, const LIMBS: usize>(n: Uint<BITS, LIMBS>, base: u64) {
assert_eq!(
n.to_base_be(base).collect::<Vec<_>>(),
n.to_base_le(base)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>(),
);

let digits = n.to_base_le(base);
let n2 = Uint::<BITS, LIMBS>::from_base_le(base, digits).unwrap();
assert_eq!(n, n2);

let digits = n.to_base_be(base);
let n2 = Uint::<BITS, LIMBS>::from_base_be(base, digits).unwrap();
assert_eq!(n, n2);
}

let single = |x: u64| x..=x;
for base in [2..=129, single(1 << 31), single(1 << 32), single(1 << 33)]
.into_iter()
.flatten()
{
test(Uint::<64, 1>::from(123456789), base);
test(Uint::<128, 2>::from(123456789), base);
test(N, base);
}
}
}
98 changes: 39 additions & 59 deletions src/fmt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#![allow(clippy::missing_inline_in_public_items)] // allow format functions
#![cfg(feature = "alloc")]

use crate::Uint;
use core::{
Expand All @@ -9,102 +8,83 @@ use core::{

mod base {
pub(super) trait Base {
/// The base.
const BASE: u64;
/// The prefix for the base.
const PREFIX: &'static str;

/// Highest power of the base that fits in a `u64`.
const MAX: u64;
const MAX: u64 = crate::utils::max_pow_u64(Self::BASE);
/// Number of characters written using `MAX` as the base in
/// `to_base_be`.
///
/// This is `MAX.log(base)`.
// TODO(MSRV-1.67): = `Self::MAX.ilog(Self::BASE)`
const WIDTH: usize;
/// The prefix for the base.
const PREFIX: &'static str;
}

pub(super) struct Binary;
impl Base for Binary {
const MAX: u64 = 1 << 63;
const WIDTH: usize = 63;
const BASE: u64 = 2;
const PREFIX: &'static str = "0b";
const WIDTH: usize = 63;
}

pub(super) struct Octal;
impl Base for Octal {
const MAX: u64 = 1 << 63;
const WIDTH: usize = 21;
const BASE: u64 = 8;
const PREFIX: &'static str = "0o";
const WIDTH: usize = 21;
}

pub(super) struct Decimal;
impl Base for Decimal {
const MAX: u64 = 10_000_000_000_000_000_000;
const WIDTH: usize = 19;
const BASE: u64 = 10;
const PREFIX: &'static str = "";
const WIDTH: usize = 19;
}

pub(super) struct Hexadecimal;
impl Base for Hexadecimal {
const MAX: u64 = 1 << 60;
const WIDTH: usize = 15;
const BASE: u64 = 16;
const PREFIX: &'static str = "0x";
const WIDTH: usize = 15;
}
}
use base::Base;

macro_rules! write_digits {
($self:expr, $f:expr; $base:ty, $base_char:literal) => {
if LIMBS == 0 || $self.is_zero() {
return $f.pad_integral(true, <$base>::PREFIX, "0");
macro_rules! impl_fmt {
($tr:path; $base:ty, $base_char:literal) => {
impl<const BITS: usize, const LIMBS: usize> $tr for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Use `BITS` for all bases since `generic_const_exprs` is not yet stable.
let mut buffer = DisplayBuffer::<BITS>::new();
let mut first = true;
for spigot in self.to_base_be(<$base>::MAX) {
write!(
buffer,
concat!("{:0width$", $base_char, "}"),
spigot,
width = if first { 0 } else { <$base>::WIDTH },
)
.unwrap();
first = false;
}
f.pad_integral(true, <$base>::PREFIX, buffer.as_str())
}
}
// Use `BITS` for all bases since `generic_const_exprs` is not yet stable.
let mut buffer = DisplayBuffer::<BITS>::new();
for (i, spigot) in $self.to_base_be(<$base>::MAX).enumerate() {
write!(
buffer,
concat!("{:0width$", $base_char, "}"),
spigot,
width = if i == 0 { 0 } else { <$base>::WIDTH },
)
.unwrap();
}
return $f.pad_integral(true, <$base>::PREFIX, buffer.as_str());
};
}

impl<const BITS: usize, const LIMBS: usize> fmt::Display for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_digits!(self, f; base::Decimal, "");
}
}

impl<const BITS: usize, const LIMBS: usize> fmt::Debug for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}

impl<const BITS: usize, const LIMBS: usize> fmt::Binary for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_digits!(self, f; base::Binary, "b");
}
}

impl<const BITS: usize, const LIMBS: usize> fmt::Octal for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_digits!(self, f; base::Octal, "o");
}
}

impl<const BITS: usize, const LIMBS: usize> fmt::LowerHex for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_digits!(self, f; base::Hexadecimal, "x");
}
}

impl<const BITS: usize, const LIMBS: usize> fmt::UpperHex for Uint<BITS, LIMBS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_digits!(self, f; base::Hexadecimal, "X");
}
}
impl_fmt!(fmt::Display; base::Decimal, "");
impl_fmt!(fmt::Binary; base::Binary, "b");
impl_fmt!(fmt::Octal; base::Octal, "o");
impl_fmt!(fmt::LowerHex; base::Hexadecimal, "x");
impl_fmt!(fmt::UpperHex; base::Hexadecimal, "X");

struct DisplayBuffer<const SIZE: usize> {
buf: [MaybeUninit<u8>; SIZE],
Expand Down
Loading