From 41720477689dbd09619055de73fd32683caa5d29 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 12:16:00 -0500 Subject: [PATCH 1/4] Port num_digits for boost.decimal --- .../boost/safe_numbers/detail/num_digits.hpp | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 include/boost/safe_numbers/detail/num_digits.hpp diff --git a/include/boost/safe_numbers/detail/num_digits.hpp b/include/boost/safe_numbers/detail/num_digits.hpp new file mode 100644 index 0000000..c35831d --- /dev/null +++ b/include/boost/safe_numbers/detail/num_digits.hpp @@ -0,0 +1,141 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_DETAIL_NUM_DIGITS_HPP +#define BOOST_SAFE_NUMBERS_DETAIL_NUM_DIGITS_HPP + +// https://stackoverflow.com/questions/1489830/efficient-way-to-determine-number-of-digits-in-an-integer +// https://graphics.stanford.edu/~seander/bithacks.html + +#include +#include + +#ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE + +#include +#include +#include +#include + +#endif + +namespace boost::safe_numbers::detail { + +// ============================================================================ +// Power-of-10 lookup tables (generated at compile time) +// ============================================================================ + +template +consteval auto make_powers_of_10() noexcept +{ + constexpr auto N {static_cast(std::numeric_limits::digits10 + 1)}; + + std::array table {}; + table[0] = T{1}; + for (std::size_t i {1}; i < N; ++i) + { + table[i] = static_cast(table[i - 1] * T{10}); + } + + return table; +} + +inline constexpr auto powers_of_10_u32 {make_powers_of_10()}; +inline constexpr auto powers_of_10_u64 {make_powers_of_10()}; +inline constexpr auto powers_of_10_u128 {make_powers_of_10()}; + +// ============================================================================ +// num_digits: counts the number of decimal digits using MSB approximation +// +// Algorithm: log10(x) ~= log2(x) / log2(10) ~= log2(x) / 3.322 +// Use (msb * 1000) / 3322 + 1 as an estimate, then refine with a +// table lookup (at most one comparison in each direction). +// ============================================================================ + +// Overload for types fitting in uint32_t (uint8_t, uint16_t, uint32_t) +template + requires (std::numeric_limits::digits <= 32 && std::is_unsigned_v) +constexpr auto num_digits(const T init_x) noexcept -> int +{ + const auto x {static_cast(init_x)}; + + if (x == 0) + { + return 1; + } + + const auto msb {32 - boost::core::countl_zero(x)}; + + // Approximate log10 + const auto estimated_digits {(msb * 1000) / 3322 + 1}; + + if (estimated_digits < 10 && x >= powers_of_10_u32[static_cast(estimated_digits)]) + { + return estimated_digits + 1; + } + + if (estimated_digits > 1 && x < powers_of_10_u32[static_cast(estimated_digits - 1)]) + { + return estimated_digits - 1; + } + + return estimated_digits; +} + +// Overload for uint64_t +constexpr auto num_digits(const std::uint64_t x) noexcept -> int +{ + if (x <= UINT32_MAX) + { + return num_digits(static_cast(x)); + } + + const auto msb {64 - boost::core::countl_zero(x)}; + + // Approximate log10 + const auto estimated_digits {(msb * 1000) / 3322 + 1}; + + if (estimated_digits < 20 && x >= powers_of_10_u64[static_cast(estimated_digits)]) + { + return estimated_digits + 1; + } + + if (estimated_digits > 1 && x < powers_of_10_u64[static_cast(estimated_digits - 1)]) + { + return estimated_digits - 1; + } + + return estimated_digits; +} + +// Overload for uint128_t +constexpr auto num_digits(const boost::int128::uint128_t& x) noexcept -> int +{ + if (x.high == UINT64_C(0)) + { + return num_digits(x.low); + } + + const auto msb {64 + (64 - boost::int128::detail::countl_zero(x.high))}; + + // Approximate log10 + const auto estimated_digits {(msb * 1000) / 3322 + 1}; + + if (estimated_digits < 39 && x >= powers_of_10_u128[static_cast(estimated_digits)]) + { + return estimated_digits + 1; + } + + // Estimated digits can't be less than 20 (65+ bits) + if (x < powers_of_10_u128[static_cast(estimated_digits - 1)]) + { + return estimated_digits - 1; + } + + return estimated_digits; +} + +} // namespace boost::safe_numbers::detail + +#endif // BOOST_SAFE_NUMBERS_DETAIL_NUM_DIGITS_HPP From fa8f40b5f29f426f5482a2028037a878e412f828 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 12:16:20 -0500 Subject: [PATCH 2/4] Add log10 function --- .../boost/safe_numbers/integer_utilities.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/include/boost/safe_numbers/integer_utilities.hpp b/include/boost/safe_numbers/integer_utilities.hpp index c1fc7c4..0665b1a 100644 --- a/include/boost/safe_numbers/integer_utilities.hpp +++ b/include/boost/safe_numbers/integer_utilities.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace boost::safe_numbers { @@ -120,6 +121,23 @@ consteval auto log2(const detail::verified_type_basis n) noexcept -> int return bit_width(n) - 1; } +// Integer log base 10: floor(log10(n)) == num_digits(n) - 1 +// Uses MSB-based approximation with power-of-10 table lookup (O(1)) +template + requires (!detail::is_verified_type_v) +constexpr auto log10(const T n) noexcept -> int +{ + using underlying = detail::underlying_type_t; + return detail::num_digits(static_cast(n)) - 1; +} + +template +consteval auto log10(const detail::verified_type_basis n) noexcept -> int +{ + using underlying = detail::underlying_type_t; + return detail::num_digits(static_cast(n)) - 1; +} + namespace detail { // Iterative exponentiation by squaring: O(log b) multiplications From 35003070aac557acbdfa57093518534e69f76ee4 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 12:16:29 -0500 Subject: [PATCH 3/4] Add testing of log10 --- test/Jamfile | 1 + test/test_log10.cpp | 284 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 test/test_log10.cpp diff --git a/test/Jamfile b/test/Jamfile index d38f77c..70b55f2 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -136,6 +136,7 @@ run test_is_power_10.cpp ; run test_is_power_2.cpp ; run test_ipow.cpp ; run test_log2.cpp ; +run test_log10.cpp ; # Compile Tests compile compile_tests/compile_test_unsigned_integers.cpp ; diff --git a/test/test_log10.cpp b/test/test_log10.cpp new file mode 100644 index 0000000..32652b0 --- /dev/null +++ b/test/test_log10.cpp @@ -0,0 +1,284 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// Runtime tests: exact powers of 10 +// ============================================================================= + +template +void test_log10_powers_of_10() +{ + using underlying = typename detail::underlying_type_t; + + BOOST_TEST_EQ(log10(T{static_cast(1)}), 0); // 10^0 + BOOST_TEST_EQ(log10(T{static_cast(10)}), 1); // 10^1 + BOOST_TEST_EQ(log10(T{static_cast(100)}), 2); // 10^2 +} + +void test_log10_powers_of_10_u16() +{ + BOOST_TEST_EQ(log10(u16{static_cast(1000)}), 3); // 10^3 + BOOST_TEST_EQ(log10(u16{static_cast(10000)}), 4); // 10^4 +} + +void test_log10_powers_of_10_u32() +{ + BOOST_TEST_EQ(log10(u32{UINT32_C(1000)}), 3); + BOOST_TEST_EQ(log10(u32{UINT32_C(10000)}), 4); + BOOST_TEST_EQ(log10(u32{UINT32_C(100000)}), 5); + BOOST_TEST_EQ(log10(u32{UINT32_C(1000000)}), 6); + BOOST_TEST_EQ(log10(u32{UINT32_C(10000000)}), 7); + BOOST_TEST_EQ(log10(u32{UINT32_C(100000000)}), 8); + BOOST_TEST_EQ(log10(u32{UINT32_C(1000000000)}), 9); +} + +void test_log10_powers_of_10_u64() +{ + BOOST_TEST_EQ(log10(u64{UINT64_C(1000000000000)}), 12); + BOOST_TEST_EQ(log10(u64{UINT64_C(10000000000000000)}), 16); + BOOST_TEST_EQ(log10(u64{UINT64_C(1000000000000000000)}), 18); + BOOST_TEST_EQ(log10(u64{UINT64_C(10000000000000000000)}), 19); +} + +void test_log10_powers_of_10_u128() +{ + using boost::int128::uint128_t; + + BOOST_TEST_EQ(log10(u128{uint128_t{UINT64_C(1000000000000)}}), 12); + + // 10^20 + const auto ten_pow_20 = uint128_t{UINT64_C(10000000000)} * uint128_t{UINT64_C(10000000000)}; + BOOST_TEST_EQ(log10(u128{ten_pow_20}), 20); + + // 10^30 + const auto ten_pow_30 = ten_pow_20 * uint128_t{UINT64_C(10000000000)}; + BOOST_TEST_EQ(log10(u128{ten_pow_30}), 30); +} + +// ============================================================================= +// Runtime tests: non-powers of 10 (floor behavior) +// ============================================================================= + +template +void test_log10_floor() +{ + using underlying = typename detail::underlying_type_t; + + BOOST_TEST_EQ(log10(T{static_cast(2)}), 0); // floor(log10(2)) = 0 + BOOST_TEST_EQ(log10(T{static_cast(5)}), 0); + BOOST_TEST_EQ(log10(T{static_cast(9)}), 0); + BOOST_TEST_EQ(log10(T{static_cast(11)}), 1); // floor(log10(11)) = 1 + BOOST_TEST_EQ(log10(T{static_cast(50)}), 1); + BOOST_TEST_EQ(log10(T{static_cast(99)}), 1); + BOOST_TEST_EQ(log10(T{static_cast(101)}), 2); // floor(log10(101)) = 2 + BOOST_TEST_EQ(log10(T{static_cast(200)}), 2); + BOOST_TEST_EQ(log10(T{static_cast(255)}), 2); +} + +void test_log10_floor_u32() +{ + BOOST_TEST_EQ(log10(u32{UINT32_C(999)}), 2); + BOOST_TEST_EQ(log10(u32{UINT32_C(1001)}), 3); + BOOST_TEST_EQ(log10(u32{UINT32_C(999999999)}), 8); + BOOST_TEST_EQ(log10(u32{UINT32_C(4294967295)}), 9); // floor(log10(2^32 - 1)) +} + +void test_log10_floor_u64() +{ + BOOST_TEST_EQ(log10(u64{UINT64_C(9999999999999999)}), 15); + BOOST_TEST_EQ(log10(u64{UINT64_C(10000000000000001)}), 16); + BOOST_TEST_EQ(log10(u64{UINT64_MAX}), 19); // floor(log10(2^64 - 1)) +} + +void test_log10_floor_u128() +{ + using boost::int128::uint128_t; + + BOOST_TEST_EQ(log10(u128{uint128_t{UINT64_C(9999999999999)}}), 12); + + // 10^20 + 1 + const auto ten_pow_20 = uint128_t{UINT64_C(10000000000)} * uint128_t{UINT64_C(10000000000)}; + BOOST_TEST_EQ(log10(u128{ten_pow_20 + uint128_t{1}}), 20); + + // 10^20 - 1 + BOOST_TEST_EQ(log10(u128{ten_pow_20 - uint128_t{1}}), 19); +} + +// ============================================================================= +// Tests with trailing zeros (exercises remove_trailing_zeros path) +// ============================================================================= + +template +void test_log10_trailing_zeros() +{ + using underlying = typename detail::underlying_type_t; + + // Numbers with trailing zeros where RTZ does the heavy lifting + BOOST_TEST_EQ(log10(T{static_cast(20)}), 1); + BOOST_TEST_EQ(log10(T{static_cast(30)}), 1); + BOOST_TEST_EQ(log10(T{static_cast(50)}), 1); + BOOST_TEST_EQ(log10(T{static_cast(90)}), 1); +} + +void test_log10_trailing_zeros_u32() +{ + BOOST_TEST_EQ(log10(u32{UINT32_C(12300)}), 4); + BOOST_TEST_EQ(log10(u32{UINT32_C(5000000)}), 6); + BOOST_TEST_EQ(log10(u32{UINT32_C(4000000000)}), 9); +} + +void test_log10_trailing_zeros_u64() +{ + BOOST_TEST_EQ(log10(u64{UINT64_C(123456789000)}), 11); + BOOST_TEST_EQ(log10(u64{UINT64_C(5000000000000000000)}), 18); +} + +// ============================================================================= +// Exhaustive u8 tests +// ============================================================================= + +void test_log10_exhaustive_u8() +{ + for (unsigned i {1}; i <= 255; ++i) + { + const auto n = static_cast(i); + auto result = log10(u8{n}); + + // Compute expected floor(log10(i)) + auto expected {0}; + auto tmp {i}; + while (tmp >= 10) + { + tmp /= 10; + ++expected; + } + + BOOST_TEST_EQ(result, expected); + } +} + +// ============================================================================= +// Exhaustive u16 tests +// ============================================================================= + +void test_log10_exhaustive_u16() +{ + for (unsigned i {1}; i <= 65535; ++i) + { + const auto n = static_cast(i); + auto result = log10(u16{n}); + + auto expected {0}; + auto tmp {i}; + while (tmp >= 10) + { + tmp /= 10; + ++expected; + } + + BOOST_TEST_EQ(result, expected); + } +} + +// ============================================================================= +// Constexpr tests +// ============================================================================= + +void test_log10_constexpr() +{ + static_assert(log10(u8{static_cast(1)}) == 0); + static_assert(log10(u8{static_cast(10)}) == 1); + static_assert(log10(u8{static_cast(100)}) == 2); + static_assert(log10(u8{static_cast(255)}) == 2); + + static_assert(log10(u16{static_cast(10000)}) == 4); + static_assert(log10(u16{static_cast(65535)}) == 4); + + static_assert(log10(u32{UINT32_C(1000000000)}) == 9); + static_assert(log10(u32{UINT32_C(4294967295)}) == 9); + + static_assert(log10(u64{UINT64_C(1)}) == 0); + static_assert(log10(u64{UINT64_C(10000000000000000000)}) == 19); +} + +// ============================================================================= +// Verified type tests (consteval) +// ============================================================================= + +void test_log10_verified() +{ + static_assert(log10(verified_u8{u8{static_cast(1)}}) == 0); + static_assert(log10(verified_u8{u8{static_cast(10)}}) == 1); + static_assert(log10(verified_u8{u8{static_cast(100)}}) == 2); + + static_assert(log10(verified_u16{u16{static_cast(10000)}}) == 4); + + static_assert(log10(verified_u32{u32{UINT32_C(1000000000)}}) == 9); + static_assert(log10(verified_u32{u32{UINT32_C(4294967295)}}) == 9); + + static_assert(log10(verified_u64{u64{UINT64_C(10000000000000000000)}}) == 19); + + using boost::int128::uint128_t; + static_assert(log10(verified_u128{u128{uint128_t{UINT64_C(1000000000000)}}}) == 12); +} + +int main() +{ + // Powers of 10 - all types + test_log10_powers_of_10(); + test_log10_powers_of_10(); + test_log10_powers_of_10(); + test_log10_powers_of_10(); + test_log10_powers_of_10(); + test_log10_powers_of_10_u16(); + test_log10_powers_of_10_u32(); + test_log10_powers_of_10_u64(); + test_log10_powers_of_10_u128(); + + // Floor behavior - all types + test_log10_floor(); + test_log10_floor(); + test_log10_floor(); + test_log10_floor(); + test_log10_floor(); + test_log10_floor_u32(); + test_log10_floor_u64(); + test_log10_floor_u128(); + + // Trailing zeros (RTZ path) + test_log10_trailing_zeros(); + test_log10_trailing_zeros(); + test_log10_trailing_zeros(); + test_log10_trailing_zeros(); + test_log10_trailing_zeros(); + test_log10_trailing_zeros_u32(); + test_log10_trailing_zeros_u64(); + + // Exhaustive u8 and u16 + test_log10_exhaustive_u8(); + test_log10_exhaustive_u16(); + + // Constexpr evaluation + test_log10_constexpr(); + + // Verified types (consteval) + test_log10_verified(); + + return boost::report_errors(); +} From 28980d127142754db5f76c95709be9511eedc400 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 12:16:37 -0500 Subject: [PATCH 4/4] Add log 10 to documentation pages --- doc/modules/ROOT/pages/api_reference.adoc | 5 +- doc/modules/ROOT/pages/integer_utilities.adoc | 59 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index 59822d1..3b4e167 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -180,6 +180,9 @@ https://www.boost.org/LICENSE_1_0.txt | xref:integer_utilities.adoc[`log2`] | Returns the floor of the base-2 logarithm + +| xref:integer_utilities.adoc[`log10`] +| Returns the floor of the base-10 logarithm |=== === Arithmetic @@ -231,7 +234,7 @@ This header is not included in the convenience header since it requires external | Contains specializations of `` for library types | `` -| Integer utility functions (`isqrt`, `remove_trailing_zeros`, `is_power_10`, `is_power_2`, `ipow`, `log2`) +| Integer utility functions (`isqrt`, `remove_trailing_zeros`, `is_power_10`, `is_power_2`, `ipow`, `log2`, `log10`) | `` | Stream I/O operators (`operator<<`, `operator>>`) for library types diff --git a/doc/modules/ROOT/pages/integer_utilities.adoc b/doc/modules/ROOT/pages/integer_utilities.adoc index a394c4b..68a4ec2 100644 --- a/doc/modules/ROOT/pages/integer_utilities.adoc +++ b/doc/modules/ROOT/pages/integer_utilities.adoc @@ -306,6 +306,65 @@ static_assert(log2(verified_u32{u32{1048576}}) == 20); // 2^20 static_assert(log2(verified_u64{u64{9223372036854775808ULL}}) == 63); ---- +== log10 + +Returns the integer base-10 logarithm (floor of log~10~) of a value. + +Uses an O(1) algorithm based on the most significant bit position to approximate `log~10~`, refined with a single power-of-10 table lookup. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto log10(const T n) noexcept -> int; +---- + +Computes `floor(log~10~(n))` using `num_digits(n) - 1`, where `num_digits` approximates the digit count via `log~10~(x) {tilde}{equals} log~2~(x) / log~2~(10)` and refines with at most two comparisons against a power-of-10 lookup table. + +==== Parameters + +* `n` -- The value to compute the logarithm of. Must be non-zero. + +==== Return Value + +The floor of the base-10 logarithm of `n`. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +auto r1 = log10(u32{1000}); // r1 == 3 +auto r2 = log10(u32{999}); // r2 == 2 (floor of log10(999)) +auto r3 = log10(u32{1}); // r3 == 0 +auto r4 = log10(u64{10000000000000000000ULL}); // r4 == 19 +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto log10(const verified_type_basis n) noexcept -> int; +---- + +Compile-time only overload for verified types. + +Since `log10` is `consteval` for verified types, the result is guaranteed to be a compile-time constant. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +static_assert(log10(verified_u32{u32{1000000000}}) == 9); +static_assert(log10(verified_u64{u64{10000000000000000000ULL}}) == 19); +---- + == ipow Integer exponentiation using the exponentiation-by-squaring algorithm.