diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7507077 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: dtolnay diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6b7c03d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + schedule: [cron: "40 1 * * *"] + +permissions: + contents: read + +env: + RUSTFLAGS: -Dwarnings + +jobs: + pre_ci: + uses: dtolnay/.github/.github/workflows/pre_ci.yml@master + + test: + name: Rust ${{matrix.rust}} + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: [nightly, beta, stable, 1.86.0] + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.rust}} + - name: Enable type layout randomization + run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV + if: matrix.rust == 'nightly' + - run: cargo build + - run: cargo test + - run: cargo test --release + - run: cargo build --no-default-features + - run: cargo test --tests --no-default-features + - run: cargo test --tests --no-default-features --release + - run: cargo build --tests --features no-panic --release + if: matrix.rust == 'nightly' + - run: cargo bench --no-run + if: matrix.rust == 'nightly' + - uses: actions/upload-artifact@v6 + if: matrix.rust == 'nightly' && always() + with: + name: Cargo.lock + path: Cargo.lock + continue-on-error: true + + msrv: + name: Rust 1.68.0 + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@1.68 + - run: cargo build + - run: cargo build --no-default-features + + doc: + name: Documentation + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RUSTDOCFLAGS: -Dwarnings + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/install@cargo-docs-rs + - run: cargo docs-rs + + miri: + name: Miri + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@miri + - run: cargo miri setup + - run: cargo miri test + env: + MIRIFLAGS: -Zmiri-strict-provenance + + clippy: + name: Clippy + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@clippy + - run: cargo clippy --tests --benches -- -Dclippy::all -Dclippy::pedantic + + fuzz: + name: Fuzz + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/install@cargo-fuzz + - run: cargo fuzz check + + outdated: + name: Outdated + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/install@cargo-outdated + - run: cargo outdated --workspace --exit-code 1 + - run: cargo outdated --manifest-path fuzz/Cargo.toml --exit-code 1 diff --git a/.gitignore b/.gitignore index a9d37c5..e9e2199 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -target -Cargo.lock +/target/ +/Cargo.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 34fd8de..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -sudo: false -language: rust - -matrix: - include: - - rust: stable - env: - - FEATURES="i128" - - rust: beta - env: - - FEATURES="i128" - - rust: nightly - env: - - FEATURES="i128" - - BUILD_BENCH="true" - - rust: 1.0.0 - script: cargo build - - rust: 1.20.0 - script: cargo test - -script: - - cargo build --features "$FEATURES" - - cargo test --features "$FEATURES" - - cargo test --features "$FEATURES" --release - - cargo build --no-default-features --features "$FEATURES" - - cargo test --tests --no-default-features --features "$FEATURES" - - cargo test --tests --no-default-features --features "$FEATURES" --release - - if [ "$BUILD_BENCH" == "true" ]; then cargo bench --no-run --features "$FEATURES"; fi diff --git a/Cargo.toml b/Cargo.toml index 03fe09b..d8bd99a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,33 @@ [package] name = "itoa" -version = "0.4.5" # remember to update html_root_url +version = "1.0.18" authors = ["David Tolnay "] +categories = ["value-formatting", "no-std", "no-std::no-alloc"] +description = "Fast integer primitive to string conversion" +documentation = "https://docs.rs/itoa" +edition = "2021" +exclude = ["*.png"] +keywords = ["integer"] license = "MIT OR Apache-2.0" -description = "Fast functions for printing integer primitives to an io::Write" repository = "https://github.com/dtolnay/itoa" -documentation = "https://github.com/dtolnay/itoa" -categories = ["value-formatting"] -readme = "README.md" -exclude = ["performance.png"] +rust-version = "1.68" -[features] -default = ["std"] -i128 = [] -std = [] +[dependencies] +no-panic = { version = "0.1", optional = true } -[badges] -travis-ci = { repository = "dtolnay/itoa" } +[target.'cfg(not(miri))'.dev-dependencies] +criterion = { version = "0.8", default-features = false } + +[[bench]] +name = "bench" +harness = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = [ + "--generate-link-to-definition", + "--generate-macro-expansion", + "--extern-html-root-url=core=https://doc.rust-lang.org", + "--extern-html-root-url=alloc=https://doc.rust-lang.org", + "--extern-html-root-url=std=https://doc.rust-lang.org", +] diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 16fe87b..1b5ec8b 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -174,28 +174,3 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/README.md b/README.md index d36f4fd..c539202 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,51 @@ itoa ==== -[![Build Status](https://api.travis-ci.org/dtolnay/itoa.svg?branch=master)](https://travis-ci.org/dtolnay/itoa) -[![Latest Version](https://img.shields.io/crates/v/itoa.svg)](https://crates.io/crates/itoa) -[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/itoa) +[github](https://github.com/dtolnay/itoa) +[crates.io](https://crates.io/crates/itoa) +[docs.rs](https://docs.rs/itoa) +[build status](https://github.com/dtolnay/itoa/actions?query=branch%3Amaster) -This crate provides fast functions for printing integer primitives to an -[`io::Write`] or a [`fmt::Write`]. The implementation comes straight from -[libcore] but avoids the performance penalty of going through -[`fmt::Formatter`]. +This crate provides a fast conversion of integer primitives to decimal strings. +The implementation comes straight from [libcore] but avoids the performance +penalty of going through [`core::fmt::Formatter`]. -See also [`dtoa`] for printing floating point primitives. +See also [`zmij`] for printing floating point primitives. -*Version requirement: rustc 1.0+* - -[`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html -[`fmt::Write`]: https://doc.rust-lang.org/core/fmt/trait.Write.html -[libcore]: https://github.com/rust-lang/rust/blob/b8214dc6c6fc20d0a660fb5700dca9ebf51ebe89/src/libcore/fmt/num.rs#L201-L254 -[`fmt::Formatter`]: https://doc.rust-lang.org/std/fmt/struct.Formatter.html -[`dtoa`]: https://github.com/dtolnay/dtoa +[libcore]: https://github.com/rust-lang/rust/blob/1.92.0/library/core/src/fmt/num.rs#L190-L253 +[`core::fmt::Formatter`]: https://doc.rust-lang.org/std/fmt/struct.Formatter.html +[`zmij`]: https://github.com/dtolnay/zmij ```toml [dependencies] -itoa = "0.4" +itoa = "1.0" ```
-## Performance (lower is better) - -![performance](https://raw.githubusercontent.com/dtolnay/itoa/master/performance.png) - -
- -## Examples +## Example ```rust -use std::{fmt, io}; - -fn demo_itoa_write() -> io::Result<()> { - // Write to a vector or other io::Write. - let mut buf = Vec::new(); - itoa::write(&mut buf, 128u64)?; - println!("{:?}", buf); - - // Write to a stack buffer. - let mut bytes = [0u8; 20]; - let n = itoa::write(&mut bytes[..], 128u64)?; - println!("{:?}", &bytes[..n]); - - Ok(()) -} - -fn demo_itoa_fmt() -> fmt::Result { - // Write to a string. - let mut s = String::new(); - itoa::fmt(&mut s, 128u64)?; - println!("{}", s); - - Ok(()) +fn main() { + let mut buffer = itoa::Buffer::new(); + let printed = buffer.format(128u64); + assert_eq!(printed, "128"); } ``` -The function signatures are: +
-```rust -fn write(writer: W, value: V) -> io::Result; +## Performance -fn fmt(writer: W, value: V) -> fmt::Result; -``` +The [itoa-benchmark] compares this library and other Rust integer formatting +implementations across a range of integer sizes. The vertical axis in this chart +shows nanoseconds taken by a single execution of +`itoa::Buffer::new().format(value)` so a lower result indicates a faster +library. -where `itoa::Integer` is implemented for i8, u8, i16, u16, i32, u32, i64, u64, -i128, u128, isize and usize. 128-bit integer support requires rustc 1.26+ and -the `i128` feature of this crate enabled. +[itoa-benchmark]: https://github.com/dtolnay/itoa-benchmark -The `write` function is only available when the `std` feature is enabled -(default is enabled). The return value gives the number of bytes written. +![performance](https://raw.githubusercontent.com/dtolnay/itoa/master/itoa-benchmark.png)
diff --git a/benches/bench.rs b/benches/bench.rs index c3b5500..bfd3192 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,83 +1,41 @@ -#![cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] -#![feature(test)] -#![allow(non_snake_case)] - -extern crate itoa; -extern crate test; - -macro_rules! benches { - ( - $( - $(#[$attr:meta])* - $name:ident($value:expr) - ),* - ) => { - mod bench_itoa_write { - use test::{Bencher, black_box}; - $( - $(#[$attr])* - #[bench] - fn $name(b: &mut Bencher) { - use itoa; - - let mut buf = Vec::with_capacity(40); - - b.iter(|| { - buf.clear(); - itoa::write(&mut buf, black_box($value)).unwrap() - }); - } - )* - } - - mod bench_itoa_fmt { - use test::{Bencher, black_box}; - $( - $(#[$attr])* - #[bench] - fn $name(b: &mut Bencher) { - use itoa; - - let mut buf = String::with_capacity(40); - - b.iter(|| { - buf.clear(); - itoa::fmt(&mut buf, black_box($value)).unwrap() - }); - } - )* - } - - mod bench_std_fmt { - use test::{Bencher, black_box}; - $( - $(#[$attr])* - #[bench] - fn $name(b: &mut Bencher) { - use std::io::Write; - - let mut buf = Vec::with_capacity(40); - - b.iter(|| { - buf.clear(); - write!(&mut buf, "{}", black_box($value)).unwrap() - }); - } - )* - } - } +use criterion::{criterion_group, criterion_main, Criterion}; +use std::fmt::Display; +use std::hint; +use std::io::Write; + +fn do_bench(c: &mut Criterion, group_name: &str, int: impl itoa::Integer + Display) { + let mut group = c.benchmark_group(group_name); + group.bench_function("itoa", |b| { + let mut buf = itoa::Buffer::new(); + b.iter(move || { + let int = hint::black_box(int); + let formatted = buf.format(int); + hint::black_box(formatted); + }); + }); + group.bench_function("std::fmt", |b| { + let mut buf = Vec::with_capacity(20); + b.iter(|| { + buf.clear(); + let int = hint::black_box(int); + write!(&mut buf, "{int}").unwrap(); + hint::black_box(buf.as_slice()); + }); + }); + group.finish(); } -benches!{ - bench_u64_0(0u64), - bench_u64_half(::max_value() as u64), - bench_u64_max(::max_value()), +fn bench(c: &mut Criterion) { + do_bench(c, "u64[0]", 0u64); + do_bench(c, "u64[half]", u64::from(u32::MAX)); + do_bench(c, "u64[max]", u64::MAX); - bench_i16_0(0i16), - bench_i16_min(::min_value()), + do_bench(c, "i16[0]", 0i16); + do_bench(c, "i16[min]", i16::MIN); - #[cfg(feature = "i128")] - bench_u128_0(0u128), - #[cfg(feature = "i128")] - bench_u128_max(::max_value()) + do_bench(c, "u128[0]", 0u128); + do_bench(c, "u128[max]", u128::MAX); } + +criterion_group!(benches, bench); +criterion_main!(benches); diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..4bc31dc --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,5 @@ +/artifacts/ +/corpus/ +/coverage/ +/target/ +/Cargo.lock diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..2b5e899 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "itoa-fuzz" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2021" +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } +itoa = { path = ".." } +libfuzzer-sys = "0.4" + +[[bin]] +name = "fuzz_itoa" +path = "fuzz_targets/fuzz_itoa.rs" +test = false +doc = false + +[workspace] diff --git a/fuzz/fuzz_targets/fuzz_itoa.rs b/fuzz/fuzz_targets/fuzz_itoa.rs new file mode 100644 index 0000000..2342153 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_itoa.rs @@ -0,0 +1,51 @@ +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use std::mem; + +#[derive(Arbitrary, Debug)] +enum IntegerInput { + I8(i8), + U8(u8), + I16(i16), + U16(u16), + I32(i32), + U32(u32), + I64(i64), + U64(u64), + I128(i128), + U128(u128), + Isize(isize), + Usize(usize), +} + +macro_rules! test_itoa { + ($val:expr) => { + match $val { + val => { + let mut buffer = itoa::Buffer::new(); + let string = buffer.format(val); + assert!(string.len() <= mem::size_of::()); + assert_eq!(val, string.parse().unwrap()); + } + } + }; +} + +fuzz_target!(|input: IntegerInput| { + match input { + IntegerInput::I8(val) => test_itoa!(val), + IntegerInput::U8(val) => test_itoa!(val), + IntegerInput::I16(val) => test_itoa!(val), + IntegerInput::U16(val) => test_itoa!(val), + IntegerInput::I32(val) => test_itoa!(val), + IntegerInput::U32(val) => test_itoa!(val), + IntegerInput::I64(val) => test_itoa!(val), + IntegerInput::U64(val) => test_itoa!(val), + IntegerInput::I128(val) => test_itoa!(val), + IntegerInput::U128(val) => test_itoa!(val), + IntegerInput::Isize(val) => test_itoa!(val), + IntegerInput::Usize(val) => test_itoa!(val), + } +}); diff --git a/itoa-benchmark.png b/itoa-benchmark.png new file mode 100644 index 0000000..47a6144 Binary files /dev/null and b/itoa-benchmark.png differ diff --git a/performance.png b/performance.png index 1e23b71..cd2f848 100644 Binary files a/performance.png and b/performance.png differ diff --git a/src/lib.rs b/src/lib.rs index 1b0f1aa..74cd9b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,90 +1,66 @@ -//! This crate provides fast functions for printing integer primitives to an -//! [`io::Write`] or a [`fmt::Write`]. The implementation comes straight from -//! [libcore] but avoids the performance penalty of going through -//! [`fmt::Formatter`]. +//! [![github]](https://github.com/dtolnay/itoa) [![crates-io]](https://crates.io/crates/itoa) [![docs-rs]](https://docs.rs/itoa) //! -//! See also [`dtoa`] for printing floating point primitives. -//! -//! [`io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html -//! [`fmt::Write`]: https://doc.rust-lang.org/core/fmt/trait.Write.html -//! [libcore]: https://github.com/rust-lang/rust/blob/b8214dc6c6fc20d0a660fb5700dca9ebf51ebe89/src/libcore/fmt/num.rs#L201-L254 -//! [`fmt::Formatter`]: https://doc.rust-lang.org/std/fmt/struct.Formatter.html -//! [`dtoa`]: https://github.com/dtolnay/dtoa +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs //! //!
//! -//! # Performance (lower is better) +//! This crate provides a fast conversion of integer primitives to decimal +//! strings. The implementation comes straight from [libcore] but avoids the +//! performance penalty of going through [`core::fmt::Formatter`]. //! -//! ![performance](https://raw.githubusercontent.com/dtolnay/itoa/master/performance.png) +//! See also [`zmij`] for printing floating point primitives. //! -//!
+//! [libcore]: https://github.com/rust-lang/rust/blob/1.92.0/library/core/src/fmt/num.rs#L190-L253 +//! [`zmij`]: https://github.com/dtolnay/zmij //! -//! # Examples +//! # Example //! -//! ```edition2018 -//! use std::{fmt, io}; +//! ``` +//! fn main() { +//! let mut buffer = itoa::Buffer::new(); +//! let printed = buffer.format(128u64); +//! assert_eq!(printed, "128"); +//! } +//! ``` //! -//! fn demo_itoa_write() -> io::Result<()> { -//! // Write to a vector or other io::Write. -//! let mut buf = Vec::new(); -//! itoa::write(&mut buf, 128u64)?; -//! println!("{:?}", buf); +//! # Performance //! -//! // Write to a stack buffer. -//! let mut bytes = [0u8; 20]; -//! let n = itoa::write(&mut bytes[..], 128u64)?; -//! println!("{:?}", &bytes[..n]); +//! The [itoa-benchmark] compares this library and other Rust integer formatting +//! implementations across a range of integer sizes. The vertical axis in this +//! chart shows nanoseconds taken by a single execution of +//! `itoa::Buffer::new().format(value)` so a lower result indicates a faster +//! library. //! -//! Ok(()) -//! } +//! [itoa-benchmark]: https://github.com/dtolnay/itoa-benchmark //! -//! fn demo_itoa_fmt() -> fmt::Result { -//! // Write to a string. -//! let mut s = String::new(); -//! itoa::fmt(&mut s, 128u64)?; -//! println!("{}", s); -//! -//! Ok(()) -//! } -//! ``` - -#![doc(html_root_url = "https://docs.rs/itoa/0.4.5")] -#![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(feature = "cargo-clippy", allow(renamed_and_removed_lints))] -#![cfg_attr( - feature = "cargo-clippy", - allow(const_static_lifetime, transmute_ptr_to_ptr), +//! ![performance](https://raw.githubusercontent.com/dtolnay/itoa/master/itoa-benchmark.png) + +#![doc(html_root_url = "https://docs.rs/itoa/1.0.18")] +#![no_std] +#![allow( + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::expl_impl_clone_on_copy, + clippy::identity_op, + clippy::items_after_statements, + clippy::must_use_candidate, + clippy::needless_doctest_main, + clippy::unreadable_literal )] -#[cfg(feature = "i128")] -mod udiv128; - -#[cfg(feature = "std")] -use std::{fmt, io, mem, ptr, slice, str}; +mod u128_ext; -#[cfg(not(feature = "std"))] -use core::{fmt, mem, ptr, slice, str}; +use core::hint; +use core::mem::{self, MaybeUninit}; +use core::str; +#[cfg(feature = "no-panic")] +use no_panic::no_panic; -/// Write integer to an `io::Write`. -#[cfg(feature = "std")] -#[inline] -pub fn write(mut wr: W, value: V) -> io::Result { - let mut buf = Buffer::new(); - let s = buf.format(value); - match wr.write_all(s.as_bytes()) { - Ok(()) => Ok(s.len()), - Err(e) => Err(e), - } -} - -/// Write integer to an `fmt::Write`. -#[inline] -pub fn fmt(mut wr: W, value: V) -> fmt::Result { - let mut buf = Buffer::new(); - wr.write_str(buf.format(value)) -} - -/// A safe API for formatting integers to text. +/// A correctly sized stack allocation for the formatted integer to be written +/// into. /// /// # Example /// @@ -93,9 +69,8 @@ pub fn fmt(mut wr: W, value: V) -> fmt::Result { /// let printed = buffer.format(1234); /// assert_eq!(printed, "1234"); /// ``` -#[derive(Copy)] pub struct Buffer { - bytes: [u8; I128_MAX_LEN], + bytes: [MaybeUninit; i128::MAX_STR_LEN], } impl Default for Buffer { @@ -105,6 +80,9 @@ impl Default for Buffer { } } +impl Copy for Buffer {} + +#[allow(clippy::non_canonical_clone_impl)] impl Clone for Buffer { #[inline] fn clone(&self) -> Self { @@ -116,230 +94,373 @@ impl Buffer { /// This is a cheap operation; you don't need to worry about reusing buffers /// for efficiency. #[inline] - #[allow(deprecated)] + #[cfg_attr(feature = "no-panic", no_panic)] pub fn new() -> Buffer { - Buffer { - bytes: unsafe { mem::uninitialized() }, - } + let bytes = [MaybeUninit::::uninit(); i128::MAX_STR_LEN]; + Buffer { bytes } } - /// Print an integer into this buffer and return a reference to its string representation - /// within the buffer. + /// Print an integer into this buffer and return a reference to its string + /// representation within the buffer. + #[cfg_attr(feature = "no-panic", no_panic)] pub fn format(&mut self, i: I) -> &str { - i.write(self) + let buf_ptr = self.bytes.as_mut_ptr().cast::(); + let string = i.write(unsafe { &mut *buf_ptr }); + if string.len() > I::MAX_STR_LEN { + unsafe { hint::unreachable_unchecked() }; + } + string } } -// Seal to prevent downstream implementations of the Integer trait. -mod private { - pub trait Sealed {} -} - -/// An integer that can be formatted by `itoa::write` and `itoa::fmt`. +/// An integer that can be written into an [`itoa::Buffer`][Buffer]. /// /// This trait is sealed and cannot be implemented for types outside of itoa. pub trait Integer: private::Sealed { - // Not public API. - #[doc(hidden)] - fn write(self, buf: &mut Buffer) -> &str; + /// The maximum length of string that formatting an integer of this type can + /// produce on the current target platform. + const MAX_STR_LEN: usize; } -trait IntegerPrivate { - fn write_to(self, buf: &mut B) -> &[u8]; +// Seal to prevent downstream implementations of the Integer trait. +mod private { + #[doc(hidden)] + pub trait Sealed: Copy { + #[doc(hidden)] + type Buffer: 'static; + fn write(self, buf: &mut Self::Buffer) -> &str; + } } -const DEC_DIGITS_LUT: &'static [u8] = b"\ - 0001020304050607080910111213141516171819\ - 2021222324252627282930313233343536373839\ - 4041424344454647484950515253545556575859\ - 6061626364656667686970717273747576777879\ - 8081828384858687888990919293949596979899"; - -// Adaptation of the original implementation at -// https://github.com/rust-lang/rust/blob/b8214dc6c6fc20d0a660fb5700dca9ebf51ebe89/src/libcore/fmt/num.rs#L188-L266 -macro_rules! impl_IntegerCommon { - ($max_len:expr, $t:ident) => { - impl Integer for $t { +macro_rules! impl_Integer { + ($Signed:ident, $Unsigned:ident) => { + const _: () = { + assert!($Signed::MIN < 0, "need signed"); + assert!($Unsigned::MIN == 0, "need unsigned"); + assert!($Signed::BITS == $Unsigned::BITS, "need counterparts"); + }; + + impl Integer for $Unsigned { + const MAX_STR_LEN: usize = $Unsigned::MAX.ilog10() as usize + 1; + } + + impl private::Sealed for $Unsigned { + type Buffer = [MaybeUninit; Self::MAX_STR_LEN]; + #[inline] - fn write(self, buf: &mut Buffer) -> &str { - unsafe { - debug_assert!($max_len <= I128_MAX_LEN); - let buf = mem::transmute::<&mut [u8; I128_MAX_LEN], &mut [u8; $max_len]>( - &mut buf.bytes, - ); - let bytes = self.write_to(buf); - str::from_utf8_unchecked(bytes) - } + #[cfg_attr(feature = "no-panic", no_panic)] + fn write(self, buf: &mut Self::Buffer) -> &str { + let offset = Unsigned::fmt(self, buf); + // SAFETY: Starting from `offset`, all elements of the slice have been set. + unsafe { slice_buffer_to_str(buf, offset) } } } - impl private::Sealed for $t {} + impl Integer for $Signed { + const MAX_STR_LEN: usize = $Signed::MAX.ilog10() as usize + 2; + } + + impl private::Sealed for $Signed { + type Buffer = [MaybeUninit; Self::MAX_STR_LEN]; + + #[inline] + #[cfg_attr(feature = "no-panic", no_panic)] + fn write(self, buf: &mut Self::Buffer) -> &str { + let mut offset = Self::MAX_STR_LEN - $Unsigned::MAX_STR_LEN; + offset += Unsigned::fmt( + self.unsigned_abs(), + (&mut buf[offset..]).try_into().unwrap(), + ); + if self < 0 { + offset -= 1; + buf[offset].write(b'-'); + } + // SAFETY: Starting from `offset`, all elements of the slice have been set. + unsafe { slice_buffer_to_str(buf, offset) } + } + } }; } -macro_rules! impl_Integer { - ($($max_len:expr => $t:ident),* as $conv_fn:ident) => {$( - impl_IntegerCommon!($max_len, $t); +impl_Integer!(i8, u8); +impl_Integer!(i16, u16); +impl_Integer!(i32, u32); +impl_Integer!(i64, u64); +impl_Integer!(i128, u128); + +macro_rules! impl_Integer_size { + ($t:ty as $primitive:ident #[cfg(target_pointer_width = $width:literal)]) => { + #[cfg(target_pointer_width = $width)] + impl Integer for $t { + const MAX_STR_LEN: usize = <$primitive as Integer>::MAX_STR_LEN; + } + + #[cfg(target_pointer_width = $width)] + impl private::Sealed for $t { + type Buffer = <$primitive as private::Sealed>::Buffer; - impl IntegerPrivate<[u8; $max_len]> for $t { - #[allow(unused_comparisons)] #[inline] - fn write_to(self, buf: &mut [u8; $max_len]) -> &[u8] { - let is_nonnegative = self >= 0; - let mut n = if is_nonnegative { - self as $conv_fn - } else { - // convert the negative num to positive by summing 1 to it's 2 complement - (!(self as $conv_fn)).wrapping_add(1) - }; - let mut curr = buf.len() as isize; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - - unsafe { - // need at least 16 bits for the 4-characters-at-a-time to work. - if mem::size_of::<$t>() >= 2 { - // eagerly decode 4 characters at a time - while n >= 10000 { - let rem = (n % 10000) as isize; - n /= 10000; - - let d1 = (rem / 100) << 1; - let d2 = (rem % 100) << 1; - curr -= 4; - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - ptr::copy_nonoverlapping(lut_ptr.offset(d2), buf_ptr.offset(curr + 2), 2); - } - } + #[cfg_attr(feature = "no-panic", no_panic)] + fn write(self, buf: &mut Self::Buffer) -> &str { + (self as $primitive).write(buf) + } + } + }; +} - // if we reach here numbers are <= 9999, so at most 4 chars long - let mut n = n as isize; // possibly reduce 64bit math +impl_Integer_size!(isize as i16 #[cfg(target_pointer_width = "16")]); +impl_Integer_size!(usize as u16 #[cfg(target_pointer_width = "16")]); +impl_Integer_size!(isize as i32 #[cfg(target_pointer_width = "32")]); +impl_Integer_size!(usize as u32 #[cfg(target_pointer_width = "32")]); +impl_Integer_size!(isize as i64 #[cfg(target_pointer_width = "64")]); +impl_Integer_size!(usize as u64 #[cfg(target_pointer_width = "64")]); + +#[repr(C, align(2))] +struct DecimalPairs([u8; 200]); + +// The string of all two-digit numbers in range 00..99 is used as a lookup table. +static DECIMAL_PAIRS: DecimalPairs = DecimalPairs( + *b"0001020304050607080910111213141516171819\ + 2021222324252627282930313233343536373839\ + 4041424344454647484950515253545556575859\ + 6061626364656667686970717273747576777879\ + 8081828384858687888990919293949596979899", +); + +// Returns {value / 100, value % 100} correct for values of up to 4 digits. +fn divmod100(value: u32) -> (u32, u32) { + debug_assert!(value < 10_000); + const EXP: u32 = 19; // 19 is faster or equal to 12 even for 3 digits. + const SIG: u32 = (1 << EXP) / 100 + 1; + let div = (value * SIG) >> EXP; // value / 100 + (div, value - div * 100) +} - // decode 2 more chars, if > 2 chars - if n >= 100 { - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } +/// This function converts a slice of ascii characters into a `&str` starting +/// from `offset`. +/// +/// # Safety +/// +/// `buf` content starting from `offset` index MUST BE initialized and MUST BE +/// ascii characters. +#[cfg_attr(feature = "no-panic", no_panic)] +unsafe fn slice_buffer_to_str(buf: &[MaybeUninit], offset: usize) -> &str { + // SAFETY: `offset` is always included between 0 and `buf`'s length. + let written = unsafe { buf.get_unchecked(offset..) }; + // SAFETY: (`assume_init_ref`) All buf content since offset is set. + // SAFETY: (`from_utf8_unchecked`) Writes use ASCII from the lookup table exclusively. + unsafe { str::from_utf8_unchecked(&*(written as *const [MaybeUninit] as *const [u8])) } +} + +trait Unsigned: Integer { + fn fmt(self, buf: &mut Self::Buffer) -> usize; +} - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } else { - let d1 = n << 1; - curr -= 2; - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); +macro_rules! impl_Unsigned { + ($Unsigned:ident) => { + impl Unsigned for $Unsigned { + #[cfg_attr(feature = "no-panic", no_panic)] + fn fmt(self, buf: &mut Self::Buffer) -> usize { + // Count the number of bytes in buf that are not initialized. + let mut offset = buf.len(); + // Consume the least-significant decimals from a working copy. + let mut remain = self; + + // Format per four digits from the lookup table. + // Four digits need a 16-bit $Unsigned or wider. + while mem::size_of::() > 1 + && remain + > 999 + .try_into() + .expect("branch is not hit for types that cannot fit 999 (u8)") + { + offset -= 4; + + // pull two pairs + let scale: Self = 1_00_00 + .try_into() + .expect("branch is not hit for types that cannot fit 1E4 (u8)"); + let quad = remain % scale; + remain /= scale; + let (pair1, pair2) = divmod100(quad as u32); + unsafe { + buf[offset + 0] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 0)); + buf[offset + 1] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 1)); + buf[offset + 2] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 0)); + buf[offset + 3] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 1)); } + } - if !is_nonnegative { - curr -= 1; - *buf_ptr.offset(curr) = b'-'; + // Format per two digits from the lookup table. + if remain > 9 { + offset -= 2; + + let (last, pair) = divmod100(remain as u32); + remain = last as Self; + unsafe { + buf[offset + 0] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair as usize * 2 + 0)); + buf[offset + 1] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair as usize * 2 + 1)); } } - let len = buf.len() - curr as usize; - unsafe { slice::from_raw_parts(buf_ptr.offset(curr), len) } + // Format the last remaining digit, if any. + if remain != 0 || self == 0 { + offset -= 1; + + // Either the compiler sees that remain < 10, or it prevents + // a boundary check up next. + let last = remain as u8 & 15; + buf[offset].write(b'0' + last); + // not used: remain = 0; + } + + offset } } - )*}; + }; } -const I8_MAX_LEN: usize = 4; -const U8_MAX_LEN: usize = 3; -const I16_MAX_LEN: usize = 6; -const U16_MAX_LEN: usize = 5; -const I32_MAX_LEN: usize = 11; -const U32_MAX_LEN: usize = 10; -const I64_MAX_LEN: usize = 20; -const U64_MAX_LEN: usize = 20; - -impl_Integer!( - I8_MAX_LEN => i8, - U8_MAX_LEN => u8, - I16_MAX_LEN => i16, - U16_MAX_LEN => u16, - I32_MAX_LEN => i32, - U32_MAX_LEN => u32 - as u32); - -impl_Integer!(I64_MAX_LEN => i64, U64_MAX_LEN => u64 as u64); - -#[cfg(target_pointer_width = "16")] -impl_Integer!(I16_MAX_LEN => isize, U16_MAX_LEN => usize as u16); - -#[cfg(target_pointer_width = "32")] -impl_Integer!(I32_MAX_LEN => isize, U32_MAX_LEN => usize as u32); - -#[cfg(target_pointer_width = "64")] -impl_Integer!(I64_MAX_LEN => isize, U64_MAX_LEN => usize as u64); - -#[cfg(all(feature = "i128"))] -macro_rules! impl_Integer128 { - ($($max_len:expr => $t:ident),*) => {$( - impl_IntegerCommon!($max_len, $t); - - impl IntegerPrivate<[u8; $max_len]> for $t { - #[allow(unused_comparisons)] - #[inline] - fn write_to(self, buf: &mut [u8; $max_len]) -> &[u8] { - let is_nonnegative = self >= 0; - let n = if is_nonnegative { - self as u128 - } else { - // convert the negative num to positive by summing 1 to it's 2 complement - (!(self as u128)).wrapping_add(1) - }; - let mut curr = buf.len() as isize; - let buf_ptr = buf.as_mut_ptr(); - - unsafe { - // Divide by 10^19 which is the highest power less than 2^64. - let (n, rem) = udiv128::udivmod_1e19(n); - let buf1 = buf_ptr.offset(curr - U64_MAX_LEN as isize) as *mut [u8; U64_MAX_LEN]; - curr -= rem.write_to(&mut *buf1).len() as isize; - - if n != 0 { - // Memset the base10 leading zeros of rem. - let target = buf.len() as isize - 19; - ptr::write_bytes(buf_ptr.offset(target), b'0', (curr - target) as usize); - curr = target; - - // Divide by 10^19 again. - let (n, rem) = udiv128::udivmod_1e19(n); - let buf2 = buf_ptr.offset(curr - U64_MAX_LEN as isize) as *mut [u8; U64_MAX_LEN]; - curr -= rem.write_to(&mut *buf2).len() as isize; - - if n != 0 { - // Memset the leading zeros. - let target = buf.len() as isize - 38; - ptr::write_bytes(buf_ptr.offset(target), b'0', (curr - target) as usize); - curr = target; - - // There is at most one digit left - // because u128::max / 10^19 / 10^19 is 3. - curr -= 1; - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } +impl_Unsigned!(u8); +impl_Unsigned!(u16); +impl_Unsigned!(u32); +impl_Unsigned!(u64); + +impl Unsigned for u128 { + #[cfg_attr(feature = "no-panic", no_panic)] + fn fmt(self, buf: &mut Self::Buffer) -> usize { + // Optimize common-case zero, which would also need special treatment due to + // its "leading" zero. + if self == 0 { + let offset = buf.len() - 1; + buf[offset].write(b'0'); + return offset; + } + // Take the 16 least-significant decimals. + let (quot_1e16, mod_1e16) = div_rem_1e16(self); + let (mut remain, mut offset) = if quot_1e16 == 0 { + (mod_1e16, u128::MAX_STR_LEN) + } else { + // Write digits at buf[23..39]. + enc_16lsd::<{ u128::MAX_STR_LEN - 16 }>(buf, mod_1e16); + + // Take another 16 decimals. + let (quot2, mod2) = div_rem_1e16(quot_1e16); + if quot2 == 0 { + (mod2, u128::MAX_STR_LEN - 16) + } else { + // Write digits at buf[7..23]. + enc_16lsd::<{ u128::MAX_STR_LEN - 32 }>(buf, mod2); + // Quot2 has at most 7 decimals remaining after two 1e16 divisions. + (quot2 as u64, u128::MAX_STR_LEN - 32) + } + }; + + // Format per four digits from the lookup table. + while remain > 999 { + offset -= 4; + + // pull two pairs + let quad = remain % 1_00_00; + remain /= 1_00_00; + let (pair1, pair2) = divmod100(quad as u32); + unsafe { + buf[offset + 0].write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 0)); + buf[offset + 1].write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 1)); + buf[offset + 2].write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 0)); + buf[offset + 3].write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 1)); + } + } - if !is_nonnegative { - curr -= 1; - *buf_ptr.offset(curr) = b'-'; - } + // Format per two digits from the lookup table. + if remain > 9 { + offset -= 2; - let len = buf.len() - curr as usize; - slice::from_raw_parts(buf_ptr.offset(curr), len) - } + let (last, pair) = divmod100(remain as u32); + remain = last as u64; + unsafe { + buf[offset + 0].write(*DECIMAL_PAIRS.0.get_unchecked(pair as usize * 2 + 0)); + buf[offset + 1].write(*DECIMAL_PAIRS.0.get_unchecked(pair as usize * 2 + 1)); } } - )*}; + + // Format the last remaining digit, if any. + if remain != 0 { + offset -= 1; + + // Either the compiler sees that remain < 10, or it prevents + // a boundary check up next. + let last = remain as u8 & 15; + buf[offset].write(b'0' + last); + // not used: remain = 0; + } + offset + } +} + +// Encodes the 16 least-significant decimals of n into `buf[OFFSET..OFFSET + 16]`. +#[cfg_attr(feature = "no-panic", no_panic)] +fn enc_16lsd(buf: &mut [MaybeUninit], n: u64) { + // Consume the least-significant decimals from a working copy. + let mut remain = n; + + // Format per four digits from the lookup table. + for quad_index in (1..4).rev() { + // pull two pairs + let quad = remain % 1_00_00; + remain /= 1_00_00; + let (pair1, pair2) = divmod100(quad as u32); + unsafe { + buf[quad_index * 4 + OFFSET + 0] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 0)); + buf[quad_index * 4 + OFFSET + 1] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 1)); + buf[quad_index * 4 + OFFSET + 2] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 0)); + buf[quad_index * 4 + OFFSET + 3] + .write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 1)); + } + } + + // final two pairs + let (pair1, pair2) = divmod100(remain as u32); + unsafe { + buf[OFFSET + 0].write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 0)); + buf[OFFSET + 1].write(*DECIMAL_PAIRS.0.get_unchecked(pair1 as usize * 2 + 1)); + buf[OFFSET + 2].write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 0)); + buf[OFFSET + 3].write(*DECIMAL_PAIRS.0.get_unchecked(pair2 as usize * 2 + 1)); + } } -#[cfg(all(feature = "i128"))] -const U128_MAX_LEN: usize = 39; -const I128_MAX_LEN: usize = 40; +// Euclidean division plus remainder with constant 1E16 basically consumes 16 +// decimals from n. +// +// The integer division algorithm is based on the following paper: +// +// T. Granlund and P. Montgomery, “Division by Invariant Integers Using Multiplication” +// in Proc. of the SIGPLAN94 Conference on Programming Language Design and +// Implementation, 1994, pp. 61–72 +// +#[cfg_attr(feature = "no-panic", no_panic)] +fn div_rem_1e16(n: u128) -> (u128, u64) { + const D: u128 = 1_0000_0000_0000_0000; + // The check inlines well with the caller flow. + if n < D { + return (0, n as u64); + } -#[cfg(all(feature = "i128"))] -impl_Integer128!(I128_MAX_LEN => i128, U128_MAX_LEN => u128); + // These constant values are computed with the CHOOSE_MULTIPLIER procedure + // from the Granlund & Montgomery paper, using N=128, prec=128 and d=1E16. + const M_HIGH: u128 = 76624777043294442917917351357515459181; + const SH_POST: u8 = 51; + + // n.widening_mul(M_HIGH).1 >> SH_POST + let quot = u128_ext::mulhi(n, M_HIGH) >> SH_POST; + let rem = n - quot * D; + (quot, rem as u64) +} diff --git a/src/u128_ext.rs b/src/u128_ext.rs new file mode 100644 index 0000000..866a733 --- /dev/null +++ b/src/u128_ext.rs @@ -0,0 +1,22 @@ +#[cfg(feature = "no-panic")] +use no_panic::no_panic; + +/// Multiply unsigned 128 bit integers, return upper 128 bits of the result +#[inline] +#[cfg_attr(feature = "no-panic", no_panic)] +pub(crate) fn mulhi(x: u128, y: u128) -> u128 { + let x_lo = x as u64; + let x_hi = (x >> 64) as u64; + let y_lo = y as u64; + let y_hi = (y >> 64) as u64; + + // handle possibility of overflow + let carry = (u128::from(x_lo) * u128::from(y_lo)) >> 64; + let m = u128::from(x_lo) * u128::from(y_hi) + carry; + let high1 = m >> 64; + + let m_lo = m as u64; + let high2 = (u128::from(x_hi) * u128::from(y_lo) + u128::from(m_lo)) >> 64; + + u128::from(x_hi) * u128::from(y_hi) + high1 + high2 +} diff --git a/src/udiv128.rs b/src/udiv128.rs deleted file mode 100644 index adbdce2..0000000 --- a/src/udiv128.rs +++ /dev/null @@ -1,61 +0,0 @@ -// The code in this file is based on Rust's compiler-builtins crate. The Rust -// compiler automatically links programs against this crate for target-specific -// runtime support. We have copied the implementation of `__udivmodti4()` which -// is an intrinsic implementing division with remainder for architectures -// without 128-bit integers. This implementation works around some poor codegen -// by LLVM (https://github.com/rust-lang/rust/issues/44545) and allows for -// inlining which does not happen with the intrinsic. -// -// The compiler-builtins crate carries the following license, which is available -// in full at: -// https://github.com/rust-lang-nursery/compiler-builtins/blob/master/LICENSE.TXT -// -// --- -// -// Copyright 2009-2016 compiler-builtins Developers -// -// The compiler-builtins crate is dual licensed under both the University of -// Illinois "BSD-Like" license and the MIT license. As a user of this code you -// may choose to use it under either license. As a contributor, you agree to -// allow your code to be used under both. - -#[inline] -pub fn udivmod_1e19(n: u128) -> (u128, u64) { - let d = 10_000_000_000_000_000_000_u64; // 10^19 - - let high = (n >> 64) as u64; - if high == 0 { - let low = n as u64; - return ((low / d) as u128, low % d); - } - - let sr = 65 - high.leading_zeros(); - - // 2 <= sr <= 65 - let mut q: u128 = n << (128 - sr); - let mut r: u128 = n >> sr; - let mut carry: u64 = 0; - - // Don't use a range because they may generate references to memcpy in unoptimized code - // - // Loop invariants: r < d; carry is 0 or 1 - let mut i = 0; - while i < sr { - i += 1; - - // r:q = ((r:q) << 1) | carry - r = (r << 1) | (q >> 127); - q = (q << 1) | carry as u128; - - // carry = 0 - // if r >= d { - // r -= d; - // carry = 1; - // } - let s = (d as u128).wrapping_sub(r).wrapping_sub(1) as i128 >> 127; - carry = (s & 1) as u64; - r -= (d as u128) & s as u128; - } - - ((q << 1) | carry as u128, r as u64) -} diff --git a/tests/test.rs b/tests/test.rs index dd8d494..c4f0617 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,50 +1,45 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(cast_lossless, string_lit_as_bytes) -)] #![allow(non_snake_case)] -extern crate itoa; - macro_rules! test { - ( - $( - $(#[$attr:meta])* - $name:ident($value:expr, $expected:expr) - ),* - ) => { + ($($name:ident($value:expr, $expected:expr))*) => { $( - $(#[$attr])* #[test] fn $name() { - #[cfg(feature = "std")] - { - let mut buf = [b'\0'; 40]; - let len = itoa::write(&mut buf[..], $value).unwrap(); - assert_eq!(&buf[0..len], $expected.as_bytes()); - } - - let mut s = String::new(); - itoa::fmt(&mut s, $value).unwrap(); + let mut buffer = itoa::Buffer::new(); + let s = buffer.format($value); assert_eq!(s, $expected); } )* } } -test!{ - test_u64_0(0u64, "0"), - test_u64_half(::max_value() as u64, "4294967295"), - test_u64_max(::max_value(), "18446744073709551615"), - test_i64_min(::min_value(), "-9223372036854775808"), +test! { + test_u64_0(0u64, "0") + test_u64_half(u64::from(u32::MAX), "4294967295") + test_u64_max(u64::MAX, "18446744073709551615") + test_i64_min(i64::MIN, "-9223372036854775808") + + test_i16_0(0i16, "0") + test_i16_min(i16::MIN, "-32768") + + test_u128_0(0u128, "0") + test_u128_max(u128::MAX, "340282366920938463463374607431768211455") + test_i128_min(i128::MIN, "-170141183460469231731687303715884105728") + test_i128_max(i128::MAX, "170141183460469231731687303715884105727") +} - test_i16_0(0i16, "0"), - test_i16_min(::min_value(), "-32768"), +#[test] +fn test_max_str_len() { + use itoa::Integer as _; - #[cfg(feature = "i128")] - test_u128_0(0u128, "0"), - #[cfg(feature = "i128")] - test_u128_max(::max_value(), "340282366920938463463374607431768211455"), - #[cfg(feature = "i128")] - test_i128_min(::min_value(), "-170141183460469231731687303715884105728") + assert_eq!(i8::MAX_STR_LEN, 4); + assert_eq!(u8::MAX_STR_LEN, 3); + assert_eq!(i16::MAX_STR_LEN, 6); + assert_eq!(u16::MAX_STR_LEN, 5); + assert_eq!(i32::MAX_STR_LEN, 11); + assert_eq!(u32::MAX_STR_LEN, 10); + assert_eq!(i64::MAX_STR_LEN, 20); + assert_eq!(u64::MAX_STR_LEN, 20); + assert_eq!(i128::MAX_STR_LEN, 40); + assert_eq!(u128::MAX_STR_LEN, 39); }