diff --git a/test/Jamfile b/test/Jamfile index a5460db..11871fd 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -151,6 +151,25 @@ run test_signed_bounded_conversions.cpp ; run test_signed_bounded_unary.cpp ; compile-fail compile_fail_bounded_int_mixed_ops.cpp ; +# Signed free function tests +run test_signed_charconv.cpp ; +run test_signed_fmt_format.cpp ; +run test_signed_std_format.cpp ; +run test_signed_streaming.cpp ; +run test_signed_bounded_charconv.cpp ; +run test_signed_bounded_fmt_format.cpp ; +run test_signed_bounded_std_format.cpp ; +run test_signed_limits.cpp ; +run test_signed_gcd.cpp ; +run test_signed_lcm.cpp ; +run test_signed_midpoint.cpp ; +run test_signed_abs_diff.cpp ; +run test_signed_to_from_be.cpp ; +run test_signed_to_from_le.cpp ; +run test_signed_to_from_be_bytes.cpp ; +run test_signed_to_from_le_bytes.cpp ; +run test_signed_to_from_ne_bytes.cpp ; + # Byte conversion tests run test_to_from_be.cpp ; run test_to_from_le.cpp ; diff --git a/test/test_signed_abs_diff.cpp b/test/test_signed_abs_diff.cpp new file mode 100644 index 0000000..329eddc --- /dev/null +++ b/test/test_signed_abs_diff.cpp @@ -0,0 +1,142 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// abs_diff(a, a) == 0 +// ============================================================================= + +template +void test_abs_diff_equal() +{ + using underlying = typename detail::underlying_type_t; + + BOOST_TEST_EQ(abs_diff(T{static_cast(0)}, T{static_cast(0)}), T{static_cast(0)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(1)}, T{static_cast(1)}), T{static_cast(0)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(-1)}, T{static_cast(-1)}), T{static_cast(0)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(42)}, T{static_cast(42)}), T{static_cast(0)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(-42)}, T{static_cast(-42)}), T{static_cast(0)}); +} + +// ============================================================================= +// Same-sign differences (no subtraction overflow possible) +// ============================================================================= + +template +void test_abs_diff_same_sign() +{ + using underlying = typename detail::underlying_type_t; + + // Positive - positive + BOOST_TEST_EQ(abs_diff(T{static_cast(10)}, T{static_cast(3)}), T{static_cast(7)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(3)}, T{static_cast(10)}), T{static_cast(7)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(50)}, T{static_cast(25)}), T{static_cast(25)}); + + // Negative - negative + BOOST_TEST_EQ(abs_diff(T{static_cast(-10)}, T{static_cast(-3)}), T{static_cast(7)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(-3)}, T{static_cast(-10)}), T{static_cast(7)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(-1)}, T{static_cast(-2)}), T{static_cast(1)}); +} + +// ============================================================================= +// Small cross-sign differences (safe for i8: result fits in type) +// ============================================================================= + +template +void test_abs_diff_cross_sign_small() +{ + using underlying = typename detail::underlying_type_t; + + // These are safe: the subtraction result fits in the type + BOOST_TEST_EQ(abs_diff(T{static_cast(5)}, T{static_cast(-5)}), T{static_cast(10)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(-5)}, T{static_cast(5)}), T{static_cast(10)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(1)}, T{static_cast(-1)}), T{static_cast(2)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(0)}, T{static_cast(-42)}), T{static_cast(42)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(-42)}, T{static_cast(0)}), T{static_cast(42)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(0)}, T{static_cast(10)}), T{static_cast(10)}); + BOOST_TEST_EQ(abs_diff(T{static_cast(0)}, T{static_cast(-10)}), T{static_cast(10)}); +} + +// ============================================================================= +// Type-specific tests with wider ranges +// ============================================================================= + +void test_abs_diff_i16() +{ + // Same-sign large values + BOOST_TEST_EQ(abs_diff(i16{static_cast(30000)}, i16{static_cast(25000)}), + i16{static_cast(5000)}); + BOOST_TEST_EQ(abs_diff(i16{static_cast(-30000)}, i16{static_cast(-25000)}), + i16{static_cast(5000)}); + // Cross-sign but result fits in i16 + BOOST_TEST_EQ(abs_diff(i16{static_cast(-1000)}, i16{static_cast(1000)}), + i16{static_cast(2000)}); +} + +void test_abs_diff_i32() +{ + BOOST_TEST_EQ(abs_diff(i32{static_cast(-100)}, i32{static_cast(200)}), + i32{static_cast(300)}); + BOOST_TEST_EQ(abs_diff(i32{static_cast(1000000)}, i32{static_cast(-1000000)}), + i32{static_cast(2000000)}); +} + +void test_abs_diff_i64() +{ + BOOST_TEST_EQ(abs_diff(i64{static_cast(-1000000000000LL)}, i64{static_cast(1000000000000LL)}), + i64{static_cast(2000000000000LL)}); + BOOST_TEST_EQ(abs_diff(i64{static_cast(-1LL)}, i64{static_cast(1LL)}), + i64{static_cast(2)}); +} + +// ============================================================================= +// Constexpr tests +// ============================================================================= + +void test_abs_diff_constexpr() +{ + static_assert(abs_diff(i8{static_cast(10)}, i8{static_cast(3)}) == i8{static_cast(7)}); + static_assert(abs_diff(i8{static_cast(-3)}, i8{static_cast(-10)}) == i8{static_cast(7)}); + static_assert(abs_diff(i8{static_cast(5)}, i8{static_cast(-5)}) == i8{static_cast(10)}); + static_assert(abs_diff(i8{static_cast(0)}, i8{static_cast(0)}) == i8{static_cast(0)}); + static_assert(abs_diff(i16{static_cast(1000)}, i16{static_cast(-1000)}) == i16{static_cast(2000)}); + static_assert(abs_diff(i32{static_cast(100)}, i32{static_cast(-200)}) == i32{static_cast(300)}); +} + +int main() +{ + test_abs_diff_equal(); + test_abs_diff_equal(); + test_abs_diff_equal(); + test_abs_diff_equal(); + + test_abs_diff_same_sign(); + test_abs_diff_same_sign(); + test_abs_diff_same_sign(); + test_abs_diff_same_sign(); + + test_abs_diff_cross_sign_small(); + test_abs_diff_cross_sign_small(); + test_abs_diff_cross_sign_small(); + test_abs_diff_cross_sign_small(); + + test_abs_diff_i16(); + test_abs_diff_i32(); + test_abs_diff_i64(); + + test_abs_diff_constexpr(); + + return boost::report_errors(); +} diff --git a/test/test_signed_bounded_charconv.cpp b/test/test_signed_bounded_charconv.cpp new file mode 100644 index 0000000..fa1b772 --- /dev/null +++ b/test/test_signed_bounded_charconv.cpp @@ -0,0 +1,426 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic ignored "-Wundef" +# pragma clang diagnostic ignored "-Wconversion" +# pragma clang diagnostic ignored "-Wsign-conversion" +# pragma clang diagnostic ignored "-Wfloat-equal" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Woverflow" + +# if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wdeprecated-copy" +# endif + +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wundef" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Woverflow" + +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4389) +# pragma warning(disable : 4127) +# pragma warning(disable : 4305) +# pragma warning(disable : 4309) +#endif + +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_COMPARE +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_CONVERSION + +#include + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +// Ignore [[nodiscard]] on the test that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include +#include +#include + +#endif + +using namespace boost::safe_numbers; +using boost::charconv::to_chars; +using boost::charconv::from_chars; + +static std::mt19937_64 rng {42}; + +// bounded_int has no default constructor, so use a copy of value as placeholder +template +void test(T value, int base = 10) +{ + char buffer[256]; + const auto r_to {to_chars(buffer, buffer + sizeof(buffer), value, base)}; + BOOST_TEST(r_to.ec == std::errc{}); + + auto return_val {value}; + const auto r_from {from_chars(buffer, r_to.ptr, return_val, base)}; + BOOST_TEST(r_from.ec == std::errc{}); + + BOOST_TEST_EQ(value, return_val); +} + +// ----------------------------------------------- +// Roundtrip tests for i8-backed bounded_int +// ----------------------------------------------- + +void test_i8_range_roundtrip() +{ + using type = bounded_int<-100, 100>; + + // Boundary values + test(type{i8{-100}}); + test(type{i8{100}}); + + // Negative mid-range + test(type{i8{-42}}); + test(type{i8{-1}}); + + // Zero and positive mid-range + test(type{i8{0}}); + test(type{i8{1}}); + test(type{i8{50}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-100, 100}; + for (int i {}; i < 256; ++i) + { + test(type{i8{dist(rng)}}); + } +} + +void test_i8_negative_only_roundtrip() +{ + using type = bounded_int<-120, -10>; + + // Boundary values + test(type{i8{-120}}); + test(type{i8{-10}}); + + // Mid-range + test(type{i8{-50}}); + test(type{i8{-75}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-120, -10}; + for (int i {}; i < 256; ++i) + { + test(type{i8{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Roundtrip tests for i16-backed bounded_int +// ----------------------------------------------- + +void test_i16_range_roundtrip() +{ + using type = bounded_int<-1000, 1000>; + + // Boundary values + test(type{i16{-1000}}); + test(type{i16{1000}}); + + // Negative mid-range + test(type{i16{-42}}); + test(type{i16{-1}}); + + // Zero and positive mid-range + test(type{i16{0}}); + test(type{i16{500}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-1000, 1000}; + for (int i {}; i < 1024; ++i) + { + test(type{i16{dist(rng)}}); + } +} + +void test_i16_negative_min_roundtrip() +{ + using type = bounded_int<-30000, 30000>; + + // Boundary values + test(type{i16{-30000}}); + test(type{i16{30000}}); + + // Mid-range + test(type{i16{-15000}}); + test(type{i16{0}}); + test(type{i16{15000}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-30000, 30000}; + for (int i {}; i < 1024; ++i) + { + test(type{i16{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Roundtrip tests for i32-backed bounded_int +// ----------------------------------------------- + +void test_i32_range_roundtrip() +{ + using type = bounded_int<-100000, 100000>; + + // Boundary values + test(type{i32{-100000}}); + test(type{i32{100000}}); + + // Negative mid-range + test(type{i32{-42}}); + test(type{i32{-1}}); + + // Zero and positive mid-range + test(type{i32{0}}); + test(type{i32{50000}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-100000, 100000}; + for (int i {}; i < 1024; ++i) + { + test(type{i32{dist(rng)}}); + } +} + +void test_i32_wide_range_roundtrip() +{ + using type = bounded_int<-2000000000, 2000000000>; + + // Boundary values + test(type{i32{-2000000000}}); + test(type{i32{2000000000}}); + + // Mid-range + test(type{i32{-1000000000}}); + test(type{i32{0}}); + test(type{i32{1000000000}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-2000000000, 2000000000}; + for (int i {}; i < 1024; ++i) + { + test(type{i32{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Roundtrip tests for i64-backed bounded_int +// ----------------------------------------------- + +void test_i64_range_roundtrip() +{ + using type = bounded_int<-3000000000LL, 3000000000LL>; + + // Boundary values + test(type{i64{-3000000000LL}}); + test(type{i64{3000000000LL}}); + + // Negative mid-range + test(type{i64{-42}}); + test(type{i64{-1}}); + + // Zero and positive mid-range + test(type{i64{0}}); + test(type{i64{1500000000LL}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-3000000000LL, 3000000000LL}; + for (int i {}; i < 1024; ++i) + { + test(type{i64{dist(rng)}}); + } +} + +void test_i64_wide_range_roundtrip() +{ + using type = bounded_int<-4000000000000LL, 4000000000000LL>; + + // Boundary values + test(type{i64{-4000000000000LL}}); + test(type{i64{4000000000000LL}}); + + // Mid-range + test(type{i64{-2000000000000LL}}); + test(type{i64{0}}); + test(type{i64{2000000000000LL}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {-4000000000000LL, 4000000000000LL}; + for (int i {}; i < 1024; ++i) + { + test(type{i64{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Multi-base roundtrip tests +// ----------------------------------------------- + +void test_multiple_bases() +{ + using type8 = bounded_int<-100, 100>; + + for (int base {2}; base <= 36; ++base) + { + test(type8{i8{-100}}, base); + test(type8{i8{-42}}, base); + test(type8{i8{0}}, base); + test(type8{i8{42}}, base); + test(type8{i8{100}}, base); + } + + using type16 = bounded_int<-1000, 1000>; + + for (int base {2}; base <= 36; ++base) + { + test(type16{i16{-1000}}, base); + test(type16{i16{0}}, base); + test(type16{i16{1000}}, base); + } + + using type32 = bounded_int<-100000, 100000>; + + for (int base {2}; base <= 36; ++base) + { + test(type32{i32{-100000}}, base); + test(type32{i32{0}}, base); + test(type32{i32{100000}}, base); + } + + using type64 = bounded_int<-3000000000LL, 3000000000LL>; + + for (int base {2}; base <= 36; ++base) + { + test(type64{i64{-3000000000LL}}, base); + test(type64{i64{0}}, base); + test(type64{i64{3000000000LL}}, base); + } +} + +// ----------------------------------------------- +// from_chars with out-of-bounds values +// ----------------------------------------------- + +void test_from_chars_out_of_bounds() +{ + // Value above maximum but within basis type range should throw + { + using type = bounded_int<-100, 100>; + const char str[] = "120"; + auto val {type{i8{50}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // Value below minimum but within basis type range should throw + { + using type = bounded_int<-100, 100>; + const char str[] = "-120"; + auto val {type{i8{50}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // Value above max for asymmetric range + { + using type = bounded_int<-50, 50>; + const char str[] = "51"; + auto val {type{i8{0}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // Value below min for asymmetric range + { + using type = bounded_int<-50, 50>; + const char str[] = "-51"; + auto val {type{i8{0}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // i16 range: value above max + { + using type = bounded_int<-1000, 1000>; + const char str[] = "1001"; + auto val {type{i16{500}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // i16 range: value below min + { + using type = bounded_int<-1000, 1000>; + const char str[] = "-1001"; + auto val {type{i16{500}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // i32 range: value above max + { + using type = bounded_int<-100000, 100000>; + const char str[] = "100001"; + auto val {type{i32{50000}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // i32 range: value below min + { + using type = bounded_int<-100000, 100000>; + const char str[] = "-100001"; + auto val {type{i32{50000}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } +} + +int main() +{ + test_i8_range_roundtrip(); + test_i8_negative_only_roundtrip(); + test_i16_range_roundtrip(); + test_i16_negative_min_roundtrip(); + test_i32_range_roundtrip(); + test_i32_wide_range_roundtrip(); + test_i64_range_roundtrip(); + test_i64_wide_range_roundtrip(); + test_multiple_bases(); + test_from_chars_out_of_bounds(); + + return boost::report_errors(); +} diff --git a/test/test_signed_bounded_fmt_format.cpp b/test/test_signed_bounded_fmt_format.cpp new file mode 100644 index 0000000..7510957 --- /dev/null +++ b/test/test_signed_bounded_fmt_format.cpp @@ -0,0 +1,115 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wfloat-equal" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wconversion" +#endif + +#define FMT_HEADER_ONLY + +#if __has_include() + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + const T x {42}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "42"); + BOOST_TEST_CSTR_EQ(fmt::format("{:08x}", x).c_str(), "0000002a"); + BOOST_TEST_CSTR_EQ(fmt::format("{:#010b}", x).c_str(), "0b00101010"); +} + +template +void test_negative() +{ + const T x {-42}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "-42"); +} + +void test_boundary_values() +{ + // i8-backed + { + using type = bounded_int<-100, 100>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{-100}).c_str(), "-100"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{100}).c_str(), "100"); + } + + // i16-backed + { + using type = bounded_int<-1000, 1000>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{-1000}).c_str(), "-1000"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{1000}).c_str(), "1000"); + } + + // i32-backed + { + using type = bounded_int<-100000, 100000>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{-100000}).c_str(), "-100000"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{100000}).c_str(), "100000"); + } + + // i64-backed + { + using type = bounded_int<-3000000000LL, 3000000000LL>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{-3000000000LL}).c_str(), "-3000000000"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{3000000000LL}).c_str(), "3000000000"); + } + + // Negative minimum format + { + using type = bounded_int<-100, 100>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{-42}).c_str(), "-42"); + BOOST_TEST_CSTR_EQ(fmt::format("{:04x}", type{42}).c_str(), "002a"); + } +} + +int main() +{ + using type8 = bounded_int<-100, 100>; + using type16 = bounded_int<-1000, 1000>; + using type32 = bounded_int<-100000, 100000>; + using type64 = bounded_int<-3000000000LL, 3000000000LL>; + + test(); + test(); + test(); + test(); + + test_negative(); + test_negative(); + test_negative(); + test_negative(); + + test_boundary_values(); + + return boost::report_errors(); +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/test/test_signed_bounded_std_format.cpp b/test/test_signed_bounded_std_format.cpp new file mode 100644 index 0000000..59aabc8 --- /dev/null +++ b/test/test_signed_bounded_std_format.cpp @@ -0,0 +1,116 @@ +// 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 + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +#endif + +#endif + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + const T x {42}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "42"); + BOOST_TEST_CSTR_EQ(std::format("{:08x}", x).c_str(), "0000002a"); + BOOST_TEST_CSTR_EQ(std::format("{:#010b}", x).c_str(), "0b00101010"); +} + +template +void test_negative() +{ + const T x {-42}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "-42"); +} + +void test_boundary_values() +{ + // i8-backed + { + using type = bounded_int<-100, 100>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{-100}).c_str(), "-100"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{100}).c_str(), "100"); + } + + // i16-backed + { + using type = bounded_int<-1000, 1000>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{-1000}).c_str(), "-1000"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{1000}).c_str(), "1000"); + } + + // i32-backed + { + using type = bounded_int<-100000, 100000>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{-100000}).c_str(), "-100000"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{100000}).c_str(), "100000"); + } + + // i64-backed + { + using type = bounded_int<-3000000000LL, 3000000000LL>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{-3000000000LL}).c_str(), "-3000000000"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{3000000000LL}).c_str(), "3000000000"); + } + + // Negative minimum format + { + using type = bounded_int<-100, 100>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{-42}).c_str(), "-42"); + BOOST_TEST_CSTR_EQ(std::format("{:04x}", type{42}).c_str(), "002a"); + } +} + +int main() +{ + using type8 = bounded_int<-100, 100>; + using type16 = bounded_int<-1000, 1000>; + using type32 = bounded_int<-100000, 100000>; + using type64 = bounded_int<-3000000000LL, 3000000000LL>; + + test(); + test(); + test(); + test(); + + test_negative(); + test_negative(); + test_negative(); + test_negative(); + + test_boundary_values(); + + return boost::report_errors(); +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/test/test_signed_charconv.cpp b/test/test_signed_charconv.cpp new file mode 100644 index 0000000..2093274 --- /dev/null +++ b/test/test_signed_charconv.cpp @@ -0,0 +1,155 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic ignored "-Wundef" +# pragma clang diagnostic ignored "-Wconversion" +# pragma clang diagnostic ignored "-Wsign-conversion" +# pragma clang diagnostic ignored "-Wfloat-equal" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Woverflow" + +# if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wdeprecated-copy" +# endif + +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wundef" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Woverflow" + +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4389) +# pragma warning(disable : 4127) +# pragma warning(disable : 4305) +# pragma warning(disable : 4309) +#endif + +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_COMPARE +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_CONVERSION + +#include + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +// Ignore [[nodiscard]] on the test that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include +#include +#include + +#endif + +using namespace boost::safe_numbers; +using boost::charconv::to_chars; +using boost::charconv::from_chars; + +static std::mt19937_64 rng {42}; + +template +void test(T value, int base = 10) +{ + char buffer[256]; + const auto r_to {to_chars(buffer, buffer + sizeof(buffer), value, base)}; + BOOST_TEST(r_to.ec == std::errc{}); + + T return_val {}; + const auto r_from {from_chars(buffer, r_to.ptr, return_val, base)}; + BOOST_TEST(r_from.ec == std::errc{}); + + BOOST_TEST_EQ(value, return_val); +} + +template +void test_roundtrip() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution dist {std::numeric_limits::min(), + std::numeric_limits::max()}; + + for (int base = 2; base <= 36; ++base) + { + // Guarantee that we get the max and min cases + test(std::numeric_limits::max(), base); + test(std::numeric_limits::min(), base); + + // Test negative values explicitly + test(T{-42}, base); + test(T{-1}, base); + + for (int i {}; i < 1024; ++i) + { + test(T{dist(rng)}, base); + } + } +} + +template +void test_roundtrip_positive_only() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution dist {basis_type{0}, + std::numeric_limits::max()}; + + for (int base = 2; base <= 36; ++base) + { + test(std::numeric_limits::max(), base); + test(T{0}, base); + test(T{42}, base); + test(T{1}, base); + + for (int i {}; i < 512; ++i) + { + test(T{dist(rng)}, base); + } + } +} + +int main() +{ + test_roundtrip(); + test_roundtrip(); + test_roundtrip(); + test_roundtrip(); + + // i128 from_chars has limited negative value support; test positive only + test_roundtrip_positive_only(); + + return boost::report_errors(); +} diff --git a/test/test_signed_fmt_format.cpp b/test/test_signed_fmt_format.cpp new file mode 100644 index 0000000..98b34d2 --- /dev/null +++ b/test/test_signed_fmt_format.cpp @@ -0,0 +1,68 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wfloat-equal" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wconversion" +#endif + +#define FMT_HEADER_ONLY + +#if __has_include() + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + const T x {42}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "42"); + BOOST_TEST_CSTR_EQ(fmt::format("{:08x}", x).c_str(), "0000002a"); + BOOST_TEST_CSTR_EQ(fmt::format("{:#010b}", x).c_str(), "0b00101010"); +} + +template +void test_negative() +{ + const T x {-42}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "-42"); +} + +int main() +{ + test(); + test(); + test(); + test(); + test(); + + test_negative(); + test_negative(); + test_negative(); + test_negative(); + test_negative(); + + return boost::report_errors(); +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/test/test_signed_gcd.cpp b/test/test_signed_gcd.cpp new file mode 100644 index 0000000..2a50b23 --- /dev/null +++ b/test/test_signed_gcd.cpp @@ -0,0 +1,219 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// Runtime tests: gcd(0, x) and gcd(x, 0) +// ============================================================================= + +template +void test_gcd_zero() +{ + using underlying = typename detail::underlying_type_t; + + // gcd(0, 0) == 0 + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(0)}), T{static_cast(0)}); + + // gcd(0, n) == |n| + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(1)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(7)}), T{static_cast(7)}); + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(42)}), T{static_cast(42)}); + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(-1)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(-7)}), T{static_cast(7)}); + BOOST_TEST_EQ(gcd(T{static_cast(0)}, T{static_cast(-42)}), T{static_cast(42)}); + + // gcd(n, 0) == |n| + BOOST_TEST_EQ(gcd(T{static_cast(1)}, T{static_cast(0)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(7)}, T{static_cast(0)}), T{static_cast(7)}); + BOOST_TEST_EQ(gcd(T{static_cast(-1)}, T{static_cast(0)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(-7)}, T{static_cast(0)}), T{static_cast(7)}); +} + +// ============================================================================= +// Runtime tests: sign doesn't affect result +// ============================================================================= + +template +void test_gcd_sign_invariance() +{ + using underlying = typename detail::underlying_type_t; + + // gcd(-a, b) == gcd(a, b) + BOOST_TEST_EQ(gcd(T{static_cast(-12)}, T{static_cast(8)}), + gcd(T{static_cast(12)}, T{static_cast(8)})); + + // gcd(a, -b) == gcd(a, b) + BOOST_TEST_EQ(gcd(T{static_cast(12)}, T{static_cast(-8)}), + gcd(T{static_cast(12)}, T{static_cast(8)})); + + // gcd(-a, -b) == gcd(a, b) + BOOST_TEST_EQ(gcd(T{static_cast(-12)}, T{static_cast(-8)}), + gcd(T{static_cast(12)}, T{static_cast(8)})); + + BOOST_TEST_EQ(gcd(T{static_cast(-54)}, T{static_cast(-24)}), + gcd(T{static_cast(54)}, T{static_cast(24)})); +} + +// ============================================================================= +// Runtime tests: gcd with 1 and -1 +// ============================================================================= + +template +void test_gcd_one() +{ + using underlying = typename detail::underlying_type_t; + + // gcd(1, n) == 1 for any n + BOOST_TEST_EQ(gcd(T{static_cast(1)}, T{static_cast(1)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(1)}, T{static_cast(-1)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(1)}, T{static_cast(100)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(1)}, T{static_cast(-100)}), T{static_cast(1)}); + + // gcd(-1, n) == 1 for any n + BOOST_TEST_EQ(gcd(T{static_cast(-1)}, T{static_cast(2)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(-1)}, T{static_cast(-2)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(-1)}, T{static_cast(100)}), T{static_cast(1)}); +} + +// ============================================================================= +// Runtime tests: known values +// ============================================================================= + +template +void test_gcd_known_values() +{ + using underlying = typename detail::underlying_type_t; + + // Classic examples + BOOST_TEST_EQ(gcd(T{static_cast(12)}, T{static_cast(18)}), T{static_cast(6)}); + BOOST_TEST_EQ(gcd(T{static_cast(54)}, T{static_cast(24)}), T{static_cast(6)}); + BOOST_TEST_EQ(gcd(T{static_cast(48)}, T{static_cast(18)}), T{static_cast(6)}); + BOOST_TEST_EQ(gcd(T{static_cast(56)}, T{static_cast(98)}), T{static_cast(14)}); + + // Coprime pairs + BOOST_TEST_EQ(gcd(T{static_cast(3)}, T{static_cast(5)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(7)}, T{static_cast(11)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(13)}, T{static_cast(17)}), T{static_cast(1)}); + + // With negative values + BOOST_TEST_EQ(gcd(T{static_cast(-12)}, T{static_cast(18)}), T{static_cast(6)}); + BOOST_TEST_EQ(gcd(T{static_cast(12)}, T{static_cast(-18)}), T{static_cast(6)}); + BOOST_TEST_EQ(gcd(T{static_cast(-12)}, T{static_cast(-18)}), T{static_cast(6)}); + + // Commutativity + BOOST_TEST_EQ(gcd(T{static_cast(12)}, T{static_cast(8)}), + gcd(T{static_cast(8)}, T{static_cast(12)})); + BOOST_TEST_EQ(gcd(T{static_cast(-54)}, T{static_cast(24)}), + gcd(T{static_cast(24)}, T{static_cast(-54)})); +} + +// ============================================================================= +// Type-specific tests for larger values +// ============================================================================= + +void test_gcd_i16() +{ + BOOST_TEST_EQ(gcd(i16{static_cast(1000)}, i16{static_cast(750)}), + i16{static_cast(250)}); + BOOST_TEST_EQ(gcd(i16{static_cast(-1000)}, i16{static_cast(750)}), + i16{static_cast(250)}); + BOOST_TEST_EQ(gcd(i16{static_cast(12345)}, i16{static_cast(6789)}), + i16{static_cast(3)}); +} + +void test_gcd_i32() +{ + BOOST_TEST_EQ(gcd(i32{static_cast(1000000)}, i32{static_cast(750000)}), + i32{static_cast(250000)}); + BOOST_TEST_EQ(gcd(i32{static_cast(-1000000)}, i32{static_cast(750000)}), + i32{static_cast(250000)}); + BOOST_TEST_EQ(gcd(i32{static_cast(1234567890)}, i32{static_cast(987654321)}), + i32{static_cast(9)}); + + // Fibonacci-adjacent (stress test for Euclidean algorithm) + BOOST_TEST_EQ(gcd(i32{static_cast(46368)}, i32{static_cast(28657)}), + i32{static_cast(1)}); +} + +void test_gcd_i64() +{ + BOOST_TEST_EQ(gcd(i64{static_cast(1000000000000LL)}, i64{static_cast(750000000000LL)}), + i64{static_cast(250000000000LL)}); + BOOST_TEST_EQ(gcd(i64{static_cast(-1000000000000LL)}, i64{static_cast(750000000000LL)}), + i64{static_cast(250000000000LL)}); + + // Large Fibonacci numbers are coprime to each other + BOOST_TEST_EQ(gcd(i64{static_cast(1346269LL)}, i64{static_cast(832040LL)}), + i64{static_cast(1)}); +} + +// ============================================================================= +// Equal values +// ============================================================================= + +template +void test_gcd_equal() +{ + using underlying = typename detail::underlying_type_t; + + // gcd(n, n) == |n| + BOOST_TEST_EQ(gcd(T{static_cast(1)}, T{static_cast(1)}), T{static_cast(1)}); + BOOST_TEST_EQ(gcd(T{static_cast(7)}, T{static_cast(7)}), T{static_cast(7)}); + BOOST_TEST_EQ(gcd(T{static_cast(42)}, T{static_cast(42)}), T{static_cast(42)}); + + // gcd(-n, -n) == |n| + BOOST_TEST_EQ(gcd(T{static_cast(-7)}, T{static_cast(-7)}), T{static_cast(7)}); + BOOST_TEST_EQ(gcd(T{static_cast(-42)}, T{static_cast(-42)}), T{static_cast(42)}); +} + +int main() +{ + // Zero cases - all types + test_gcd_zero(); + test_gcd_zero(); + test_gcd_zero(); + test_gcd_zero(); + + // Sign invariance - all types + test_gcd_sign_invariance(); + test_gcd_sign_invariance(); + test_gcd_sign_invariance(); + test_gcd_sign_invariance(); + + // One cases - all types + test_gcd_one(); + test_gcd_one(); + test_gcd_one(); + test_gcd_one(); + + // Equal values - all types + test_gcd_equal(); + test_gcd_equal(); + test_gcd_equal(); + test_gcd_equal(); + + // Known values - all types + test_gcd_known_values(); + test_gcd_known_values(); + test_gcd_known_values(); + test_gcd_known_values(); + + // Type-specific larger values + test_gcd_i16(); + test_gcd_i32(); + test_gcd_i64(); + + return boost::report_errors(); +} diff --git a/test/test_signed_lcm.cpp b/test/test_signed_lcm.cpp new file mode 100644 index 0000000..e77f94a --- /dev/null +++ b/test/test_signed_lcm.cpp @@ -0,0 +1,216 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// Runtime tests: lcm(0, x) and lcm(x, 0) +// ============================================================================= + +template +void test_lcm_zero() +{ + using underlying = typename detail::underlying_type_t; + + // lcm(0, 0) == 0 + BOOST_TEST_EQ(lcm(T{static_cast(0)}, T{static_cast(0)}), T{static_cast(0)}); + + // lcm(0, n) == 0 + BOOST_TEST_EQ(lcm(T{static_cast(0)}, T{static_cast(1)}), T{static_cast(0)}); + BOOST_TEST_EQ(lcm(T{static_cast(0)}, T{static_cast(7)}), T{static_cast(0)}); + BOOST_TEST_EQ(lcm(T{static_cast(0)}, T{static_cast(-7)}), T{static_cast(0)}); + + // lcm(n, 0) == 0 + BOOST_TEST_EQ(lcm(T{static_cast(1)}, T{static_cast(0)}), T{static_cast(0)}); + BOOST_TEST_EQ(lcm(T{static_cast(7)}, T{static_cast(0)}), T{static_cast(0)}); + BOOST_TEST_EQ(lcm(T{static_cast(-7)}, T{static_cast(0)}), T{static_cast(0)}); +} + +// ============================================================================= +// Runtime tests: lcm with 1 and -1 +// ============================================================================= + +template +void test_lcm_one() +{ + using underlying = typename detail::underlying_type_t; + + // lcm(1, n) == |n| + BOOST_TEST_EQ(lcm(T{static_cast(1)}, T{static_cast(1)}), T{static_cast(1)}); + BOOST_TEST_EQ(lcm(T{static_cast(1)}, T{static_cast(2)}), T{static_cast(2)}); + BOOST_TEST_EQ(lcm(T{static_cast(1)}, T{static_cast(100)}), T{static_cast(100)}); + + // lcm(-1, n) == |n| + BOOST_TEST_EQ(lcm(T{static_cast(-1)}, T{static_cast(2)}), T{static_cast(2)}); + BOOST_TEST_EQ(lcm(T{static_cast(-1)}, T{static_cast(-5)}), T{static_cast(5)}); + + // lcm(n, 1) == |n| + BOOST_TEST_EQ(lcm(T{static_cast(2)}, T{static_cast(1)}), T{static_cast(2)}); + BOOST_TEST_EQ(lcm(T{static_cast(100)}, T{static_cast(1)}), T{static_cast(100)}); + BOOST_TEST_EQ(lcm(T{static_cast(-100)}, T{static_cast(1)}), T{static_cast(100)}); +} + +// ============================================================================= +// Runtime tests: lcm with negative values -- result should be non-negative +// ============================================================================= + +template +void test_lcm_negative_values() +{ + using underlying = typename detail::underlying_type_t; + + // lcm(-a, b) == lcm(a, b) (result is always non-negative) + BOOST_TEST_EQ(lcm(T{static_cast(-4)}, T{static_cast(6)}), + lcm(T{static_cast(4)}, T{static_cast(6)})); + + BOOST_TEST_EQ(lcm(T{static_cast(4)}, T{static_cast(-6)}), + lcm(T{static_cast(4)}, T{static_cast(6)})); + + BOOST_TEST_EQ(lcm(T{static_cast(-4)}, T{static_cast(-6)}), + lcm(T{static_cast(4)}, T{static_cast(6)})); + + // The result should be non-negative + const auto result = lcm(T{static_cast(-12)}, T{static_cast(-18)}); + BOOST_TEST(result >= T{static_cast(0)}); +} + +// ============================================================================= +// Runtime tests: known values +// ============================================================================= + +template +void test_lcm_known_values() +{ + using underlying = typename detail::underlying_type_t; + + // Classic examples + BOOST_TEST_EQ(lcm(T{static_cast(4)}, T{static_cast(6)}), T{static_cast(12)}); + BOOST_TEST_EQ(lcm(T{static_cast(6)}, T{static_cast(8)}), T{static_cast(24)}); + BOOST_TEST_EQ(lcm(T{static_cast(12)}, T{static_cast(18)}), T{static_cast(36)}); + BOOST_TEST_EQ(lcm(T{static_cast(15)}, T{static_cast(20)}), T{static_cast(60)}); + + // Coprime pairs: lcm(a, b) == |a * b| + BOOST_TEST_EQ(lcm(T{static_cast(3)}, T{static_cast(5)}), T{static_cast(15)}); + BOOST_TEST_EQ(lcm(T{static_cast(7)}, T{static_cast(11)}), T{static_cast(77)}); + + // One divides the other: lcm(a, b) == max(|a|, |b|) + BOOST_TEST_EQ(lcm(T{static_cast(6)}, T{static_cast(12)}), T{static_cast(12)}); + BOOST_TEST_EQ(lcm(T{static_cast(15)}, T{static_cast(45)}), T{static_cast(45)}); + + // With negative values + BOOST_TEST_EQ(lcm(T{static_cast(-4)}, T{static_cast(6)}), T{static_cast(12)}); + BOOST_TEST_EQ(lcm(T{static_cast(4)}, T{static_cast(-6)}), T{static_cast(12)}); + BOOST_TEST_EQ(lcm(T{static_cast(-4)}, T{static_cast(-6)}), T{static_cast(12)}); + + // Commutativity + BOOST_TEST_EQ(lcm(T{static_cast(4)}, T{static_cast(6)}), + lcm(T{static_cast(6)}, T{static_cast(4)})); + BOOST_TEST_EQ(lcm(T{static_cast(-12)}, T{static_cast(18)}), + lcm(T{static_cast(18)}, T{static_cast(-12)})); +} + +// ============================================================================= +// Runtime tests: lcm equal values +// ============================================================================= + +template +void test_lcm_equal() +{ + using underlying = typename detail::underlying_type_t; + + // lcm(n, n) == |n| + BOOST_TEST_EQ(lcm(T{static_cast(1)}, T{static_cast(1)}), T{static_cast(1)}); + BOOST_TEST_EQ(lcm(T{static_cast(7)}, T{static_cast(7)}), T{static_cast(7)}); + BOOST_TEST_EQ(lcm(T{static_cast(42)}, T{static_cast(42)}), T{static_cast(42)}); + + // lcm(-n, -n) == |n| + BOOST_TEST_EQ(lcm(T{static_cast(-7)}, T{static_cast(-7)}), T{static_cast(7)}); + BOOST_TEST_EQ(lcm(T{static_cast(-42)}, T{static_cast(-42)}), T{static_cast(42)}); +} + +// ============================================================================= +// Type-specific tests for larger values +// ============================================================================= + +void test_lcm_i16() +{ + BOOST_TEST_EQ(lcm(i16{static_cast(100)}, i16{static_cast(75)}), + i16{static_cast(300)}); + BOOST_TEST_EQ(lcm(i16{static_cast(-100)}, i16{static_cast(75)}), + i16{static_cast(300)}); + BOOST_TEST_EQ(lcm(i16{static_cast(120)}, i16{static_cast(90)}), + i16{static_cast(360)}); +} + +void test_lcm_i32() +{ + BOOST_TEST_EQ(lcm(i32{static_cast(1000)}, i32{static_cast(750)}), + i32{static_cast(3000)}); + BOOST_TEST_EQ(lcm(i32{static_cast(-1000)}, i32{static_cast(750)}), + i32{static_cast(3000)}); + BOOST_TEST_EQ(lcm(i32{static_cast(100000)}, i32{static_cast(12500)}), + i32{static_cast(100000)}); +} + +void test_lcm_i64() +{ + BOOST_TEST_EQ(lcm(i64{static_cast(1000000LL)}, i64{static_cast(750000LL)}), + i64{static_cast(3000000LL)}); + BOOST_TEST_EQ(lcm(i64{static_cast(-1000000LL)}, i64{static_cast(750000LL)}), + i64{static_cast(3000000LL)}); + + // Coprime consecutive Fibonacci + BOOST_TEST_EQ(lcm(i64{static_cast(1346269LL)}, i64{static_cast(832040LL)}), + i64{static_cast(1346269LL)} * i64{static_cast(832040LL)}); +} + +int main() +{ + // Zero cases - all types + test_lcm_zero(); + test_lcm_zero(); + test_lcm_zero(); + test_lcm_zero(); + + // One cases - all types + test_lcm_one(); + test_lcm_one(); + test_lcm_one(); + test_lcm_one(); + + // Negative values - all types + test_lcm_negative_values(); + test_lcm_negative_values(); + test_lcm_negative_values(); + test_lcm_negative_values(); + + // Equal values - all types + test_lcm_equal(); + test_lcm_equal(); + test_lcm_equal(); + test_lcm_equal(); + + // Known values - all types + test_lcm_known_values(); + test_lcm_known_values(); + test_lcm_known_values(); + test_lcm_known_values(); + + // Type-specific larger values + test_lcm_i16(); + test_lcm_i32(); + test_lcm_i64(); + + return boost::report_errors(); +} diff --git a/test/test_signed_limits.cpp b/test/test_signed_limits.cpp new file mode 100644 index 0000000..dc626dd --- /dev/null +++ b/test/test_signed_limits.cpp @@ -0,0 +1,213 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// Test is_signed for signed types +// ============================================================================= + +void test_is_signed() +{ + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::is_signed); +} + +// ============================================================================= +// Test is_integer for signed types +// ============================================================================= + +void test_is_integer() +{ + BOOST_TEST(std::numeric_limits::is_integer); + BOOST_TEST(std::numeric_limits::is_integer); + BOOST_TEST(std::numeric_limits::is_integer); + BOOST_TEST(std::numeric_limits::is_integer); + BOOST_TEST(std::numeric_limits::is_integer); +} + +// ============================================================================= +// Test is_specialized +// ============================================================================= + +void test_is_specialized() +{ + BOOST_TEST(std::numeric_limits::is_specialized); + BOOST_TEST(std::numeric_limits::is_specialized); + BOOST_TEST(std::numeric_limits::is_specialized); + BOOST_TEST(std::numeric_limits::is_specialized); + BOOST_TEST(std::numeric_limits::is_specialized); +} + +// ============================================================================= +// Test digits (number of value bits, excluding sign bit) +// ============================================================================= + +void test_digits() +{ + BOOST_TEST_EQ(std::numeric_limits::digits, 7); + BOOST_TEST_EQ(std::numeric_limits::digits, 15); + BOOST_TEST_EQ(std::numeric_limits::digits, 31); + BOOST_TEST_EQ(std::numeric_limits::digits, 63); + BOOST_TEST_EQ(std::numeric_limits::digits, 127); +} + +// ============================================================================= +// Test min() and max() values +// ============================================================================= + +void test_min_max() +{ + // i8 + BOOST_TEST(std::numeric_limits::min() == i8{std::numeric_limits::min()}); + BOOST_TEST(std::numeric_limits::max() == i8{std::numeric_limits::max()}); + BOOST_TEST(std::numeric_limits::min() == i8{static_cast(-128)}); + BOOST_TEST(std::numeric_limits::max() == i8{static_cast(127)}); + + // i16 + BOOST_TEST(std::numeric_limits::min() == i16{std::numeric_limits::min()}); + BOOST_TEST(std::numeric_limits::max() == i16{std::numeric_limits::max()}); + BOOST_TEST(std::numeric_limits::min() == i16{static_cast(-32768)}); + BOOST_TEST(std::numeric_limits::max() == i16{static_cast(32767)}); + + // i32 + BOOST_TEST(std::numeric_limits::min() == i32{std::numeric_limits::min()}); + BOOST_TEST(std::numeric_limits::max() == i32{std::numeric_limits::max()}); + + // i64 + BOOST_TEST(std::numeric_limits::min() == i64{std::numeric_limits::min()}); + BOOST_TEST(std::numeric_limits::max() == i64{std::numeric_limits::max()}); +} + +// ============================================================================= +// Test lowest() == min() for integer types +// ============================================================================= + +void test_lowest() +{ + BOOST_TEST(std::numeric_limits::lowest() == std::numeric_limits::min()); + BOOST_TEST(std::numeric_limits::lowest() == std::numeric_limits::min()); + BOOST_TEST(std::numeric_limits::lowest() == std::numeric_limits::min()); + BOOST_TEST(std::numeric_limits::lowest() == std::numeric_limits::min()); + BOOST_TEST(std::numeric_limits::lowest() == std::numeric_limits::min()); +} + +// ============================================================================= +// Test min() < 0 for signed types +// ============================================================================= + +void test_min_is_negative() +{ + BOOST_TEST(std::numeric_limits::min() < i8{static_cast(0)}); + BOOST_TEST(std::numeric_limits::min() < i16{static_cast(0)}); + BOOST_TEST(std::numeric_limits::min() < i32{static_cast(0)}); + BOOST_TEST(std::numeric_limits::min() < i64{static_cast(0)}); +} + +// ============================================================================= +// Test max() > 0 for signed types +// ============================================================================= + +void test_max_is_positive() +{ + BOOST_TEST(std::numeric_limits::max() > i8{static_cast(0)}); + BOOST_TEST(std::numeric_limits::max() > i16{static_cast(0)}); + BOOST_TEST(std::numeric_limits::max() > i32{static_cast(0)}); + BOOST_TEST(std::numeric_limits::max() > i64{static_cast(0)}); +} + +// ============================================================================= +// Constexpr tests +// ============================================================================= + +void test_constexpr() +{ + static_assert(std::numeric_limits::is_signed); + static_assert(std::numeric_limits::is_integer); + static_assert(std::numeric_limits::digits == 7); + static_assert(std::numeric_limits::digits == 15); + static_assert(std::numeric_limits::digits == 31); + static_assert(std::numeric_limits::digits == 63); + static_assert(std::numeric_limits::digits == 127); + + static_assert(std::numeric_limits::min() == i8{static_cast(-128)}); + static_assert(std::numeric_limits::max() == i8{static_cast(127)}); + static_assert(std::numeric_limits::min() == i16{static_cast(-32768)}); + static_assert(std::numeric_limits::max() == i16{static_cast(32767)}); +} + +// ============================================================================= +// bounded_int limits +// ============================================================================= + +void test_bounded_int_limits() +{ + // bounded_int with negative min + using temp_range = bounded_int(-100), static_cast(100)>; + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::is_integer); + BOOST_TEST(std::numeric_limits::min() == temp_range{i8{static_cast(-100)}}); + BOOST_TEST(std::numeric_limits::max() == temp_range{i8{static_cast(100)}}); + + // Non-zero minimum bounded_int + using positive_range = bounded_int(10), static_cast(50)>; + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::min() == positive_range{i8{static_cast(10)}}); + BOOST_TEST(std::numeric_limits::max() == positive_range{i8{static_cast(50)}}); + + // Negative range bounded_int + using neg_range = bounded_int(-50), static_cast(-10)>; + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::min() == neg_range{i8{static_cast(-50)}}); + BOOST_TEST(std::numeric_limits::max() == neg_range{i8{static_cast(-10)}}); + + // Wider bounded_int + using wide_range = bounded_int(-10000), static_cast(10000)>; + BOOST_TEST(std::numeric_limits::is_signed); + BOOST_TEST(std::numeric_limits::min() == wide_range{i16{static_cast(-10000)}}); + BOOST_TEST(std::numeric_limits::max() == wide_range{i16{static_cast(10000)}}); +} + +// ============================================================================= +// Test radix +// ============================================================================= + +void test_radix() +{ + BOOST_TEST_EQ(std::numeric_limits::radix, 2); + BOOST_TEST_EQ(std::numeric_limits::radix, 2); + BOOST_TEST_EQ(std::numeric_limits::radix, 2); + BOOST_TEST_EQ(std::numeric_limits::radix, 2); + BOOST_TEST_EQ(std::numeric_limits::radix, 2); +} + +int main() +{ + test_is_signed(); + test_is_integer(); + test_is_specialized(); + test_digits(); + test_min_max(); + test_lowest(); + test_min_is_negative(); + test_max_is_positive(); + test_constexpr(); + test_bounded_int_limits(); + test_radix(); + + return boost::report_errors(); +} diff --git a/test/test_signed_midpoint.cpp b/test/test_signed_midpoint.cpp new file mode 100644 index 0000000..c08e9b3 --- /dev/null +++ b/test/test_signed_midpoint.cpp @@ -0,0 +1,253 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// Runtime tests: midpoint(a, a) == a +// ============================================================================= + +template +void test_midpoint_equal() +{ + using underlying = typename detail::underlying_type_t; + + BOOST_TEST_EQ(midpoint(T{static_cast(0)}, T{static_cast(0)}), T{static_cast(0)}); + BOOST_TEST_EQ(midpoint(T{static_cast(1)}, T{static_cast(1)}), T{static_cast(1)}); + BOOST_TEST_EQ(midpoint(T{static_cast(-1)}, T{static_cast(-1)}), T{static_cast(-1)}); + BOOST_TEST_EQ(midpoint(T{static_cast(42)}, T{static_cast(42)}), T{static_cast(42)}); + BOOST_TEST_EQ(midpoint(T{static_cast(-42)}, T{static_cast(-42)}), T{static_cast(-42)}); + BOOST_TEST_EQ(midpoint(T{static_cast(100)}, T{static_cast(100)}), T{static_cast(100)}); + BOOST_TEST_EQ(midpoint(T{static_cast(-100)}, T{static_cast(-100)}), T{static_cast(-100)}); +} + +// ============================================================================= +// Runtime tests: cross-zero midpoints +// ============================================================================= + +template +void test_midpoint_cross_zero() +{ + using underlying = typename detail::underlying_type_t; + + // midpoint(-10, 10) = 0 (even distance, exact) + BOOST_TEST_EQ(midpoint(T{static_cast(-10)}, T{static_cast(10)}), T{static_cast(0)}); + BOOST_TEST_EQ(midpoint(T{static_cast(10)}, T{static_cast(-10)}), T{static_cast(0)}); + + // midpoint(-11, 10) = -1 (odd distance, rounds toward first arg) + BOOST_TEST_EQ(midpoint(T{static_cast(-11)}, T{static_cast(10)}), T{static_cast(-1)}); + // midpoint(10, -11) = 0 (rounds toward first arg = 10, so rounds up) + BOOST_TEST_EQ(midpoint(T{static_cast(10)}, T{static_cast(-11)}), T{static_cast(0)}); + + // midpoint(-1, 1) = 0 (even distance) + BOOST_TEST_EQ(midpoint(T{static_cast(-1)}, T{static_cast(1)}), T{static_cast(0)}); + BOOST_TEST_EQ(midpoint(T{static_cast(1)}, T{static_cast(-1)}), T{static_cast(0)}); + + // midpoint(-1, 2) = 0 (rounds toward first arg = -1) + BOOST_TEST_EQ(midpoint(T{static_cast(-1)}, T{static_cast(2)}), T{static_cast(0)}); + // midpoint(2, -1) = 1 (rounds toward first arg = 2) + BOOST_TEST_EQ(midpoint(T{static_cast(2)}, T{static_cast(-1)}), T{static_cast(1)}); +} + +// ============================================================================= +// Runtime tests: negative midpoints +// ============================================================================= + +template +void test_midpoint_negative() +{ + using underlying = typename detail::underlying_type_t; + + // midpoint(-10, -20) = -15 (even distance) + BOOST_TEST_EQ(midpoint(T{static_cast(-10)}, T{static_cast(-20)}), T{static_cast(-15)}); + BOOST_TEST_EQ(midpoint(T{static_cast(-20)}, T{static_cast(-10)}), T{static_cast(-15)}); + + // midpoint(-10, -21) = -15 (odd distance, rounds toward first arg) + BOOST_TEST_EQ(midpoint(T{static_cast(-10)}, T{static_cast(-21)}), T{static_cast(-15)}); + // midpoint(-21, -10) = -16 (rounds toward first arg = -21) + BOOST_TEST_EQ(midpoint(T{static_cast(-21)}, T{static_cast(-10)}), T{static_cast(-16)}); + + // midpoint(-50, -100) = -75 + BOOST_TEST_EQ(midpoint(T{static_cast(-50)}, T{static_cast(-100)}), T{static_cast(-75)}); +} + +// ============================================================================= +// Runtime tests: midpoint with zero +// ============================================================================= + +template +void test_midpoint_zero() +{ + using underlying = typename detail::underlying_type_t; + + BOOST_TEST_EQ(midpoint(T{static_cast(0)}, T{static_cast(0)}), T{static_cast(0)}); + + // midpoint(0, 10) = 5 + BOOST_TEST_EQ(midpoint(T{static_cast(0)}, T{static_cast(10)}), T{static_cast(5)}); + // midpoint(0, -10) = -5 + BOOST_TEST_EQ(midpoint(T{static_cast(0)}, T{static_cast(-10)}), T{static_cast(-5)}); + + // midpoint(0, 11) = 5 (rounds toward first arg = 0) + BOOST_TEST_EQ(midpoint(T{static_cast(0)}, T{static_cast(11)}), T{static_cast(5)}); + // midpoint(0, -11) = -5 (rounds toward first arg = 0) + BOOST_TEST_EQ(midpoint(T{static_cast(0)}, T{static_cast(-11)}), T{static_cast(-5)}); +} + +// ============================================================================= +// Runtime tests: even distance (exact midpoint) +// ============================================================================= + +template +void test_midpoint_even_distance() +{ + using underlying = typename detail::underlying_type_t; + + BOOST_TEST_EQ(midpoint(T{static_cast(2)}, T{static_cast(8)}), T{static_cast(5)}); + BOOST_TEST_EQ(midpoint(T{static_cast(8)}, T{static_cast(2)}), T{static_cast(5)}); + BOOST_TEST_EQ(midpoint(T{static_cast(-2)}, T{static_cast(-8)}), T{static_cast(-5)}); + BOOST_TEST_EQ(midpoint(T{static_cast(-8)}, T{static_cast(-2)}), T{static_cast(-5)}); + BOOST_TEST_EQ(midpoint(T{static_cast(10)}, T{static_cast(20)}), T{static_cast(15)}); +} + +// ============================================================================= +// Runtime tests: rounding towards first argument +// ============================================================================= + +template +void test_midpoint_rounding() +{ + using underlying = typename detail::underlying_type_t; + + // midpoint(1, 4) = 2 (rounds down towards a=1) + BOOST_TEST_EQ(midpoint(T{static_cast(1)}, T{static_cast(4)}), T{static_cast(2)}); + // midpoint(4, 1) = 3 (rounds up towards a=4) + BOOST_TEST_EQ(midpoint(T{static_cast(4)}, T{static_cast(1)}), T{static_cast(3)}); + + // midpoint(-4, -1) = -3 (rounds towards a=-4, which is more negative) + BOOST_TEST_EQ(midpoint(T{static_cast(-4)}, T{static_cast(-1)}), T{static_cast(-3)}); + // midpoint(-1, -4) = -2 (rounds towards a=-1, which is less negative) + BOOST_TEST_EQ(midpoint(T{static_cast(-1)}, T{static_cast(-4)}), T{static_cast(-2)}); +} + +// ============================================================================= +// Type-specific tests for larger values +// ============================================================================= + +void test_midpoint_i16() +{ + // Near extremes + BOOST_TEST_EQ(midpoint(i16{std::numeric_limits::min()}, i16{std::numeric_limits::max()}), + i16{static_cast(-1)}); + BOOST_TEST_EQ(midpoint(i16{std::numeric_limits::max()}, i16{std::numeric_limits::min()}), + i16{static_cast(0)}); + + BOOST_TEST_EQ(midpoint(i16{static_cast(-1000)}, i16{static_cast(1000)}), + i16{static_cast(0)}); +} + +void test_midpoint_i32() +{ + // Values near extremes: would overflow with naive (a+b)/2 + BOOST_TEST_EQ(midpoint(i32{std::numeric_limits::min()}, i32{std::numeric_limits::max()}), + i32{static_cast(-1)}); + BOOST_TEST_EQ(midpoint(i32{std::numeric_limits::max()}, i32{std::numeric_limits::min()}), + i32{static_cast(0)}); + + BOOST_TEST_EQ(midpoint(i32{static_cast(-1000000000)}, i32{static_cast(1000000000)}), + i32{static_cast(0)}); +} + +void test_midpoint_i64() +{ + // Values near extremes + BOOST_TEST_EQ(midpoint(i64{std::numeric_limits::min()}, i64{std::numeric_limits::max()}), + i64{static_cast(-1)}); + BOOST_TEST_EQ(midpoint(i64{std::numeric_limits::max()}, i64{std::numeric_limits::min()}), + i64{static_cast(0)}); + + BOOST_TEST_EQ(midpoint(i64{static_cast(-1000000000000LL)}, i64{static_cast(1000000000000LL)}), + i64{static_cast(0)}); +} + +// ============================================================================= +// Adjacent values +// ============================================================================= + +template +void test_midpoint_adjacent() +{ + using underlying = typename detail::underlying_type_t; + + // midpoint(n, n+1) == n (rounds towards a) + // midpoint(n+1, n) == n+1 (rounds towards a) + for (int i {-50}; i < 50; ++i) + { + const auto a = T{static_cast(i)}; + const auto b = T{static_cast(i + 1)}; + BOOST_TEST_EQ(midpoint(a, b), a); + BOOST_TEST_EQ(midpoint(b, a), b); + } +} + +int main() +{ + // Equal values - all types + test_midpoint_equal(); + test_midpoint_equal(); + test_midpoint_equal(); + test_midpoint_equal(); + + // Cross-zero - all types + test_midpoint_cross_zero(); + test_midpoint_cross_zero(); + test_midpoint_cross_zero(); + test_midpoint_cross_zero(); + + // Negative - all types + test_midpoint_negative(); + test_midpoint_negative(); + test_midpoint_negative(); + test_midpoint_negative(); + + // Zero cases - all types + test_midpoint_zero(); + test_midpoint_zero(); + test_midpoint_zero(); + test_midpoint_zero(); + + // Even distance - all types + test_midpoint_even_distance(); + test_midpoint_even_distance(); + test_midpoint_even_distance(); + test_midpoint_even_distance(); + + // Rounding behaviour - all types + test_midpoint_rounding(); + test_midpoint_rounding(); + test_midpoint_rounding(); + test_midpoint_rounding(); + + // Adjacent values - all types + test_midpoint_adjacent(); + test_midpoint_adjacent(); + test_midpoint_adjacent(); + test_midpoint_adjacent(); + + // Type-specific larger values + test_midpoint_i16(); + test_midpoint_i32(); + test_midpoint_i64(); + + return boost::report_errors(); +} diff --git a/test/test_signed_std_format.cpp b/test/test_signed_std_format.cpp new file mode 100644 index 0000000..f5504f2 --- /dev/null +++ b/test/test_signed_std_format.cpp @@ -0,0 +1,69 @@ +// 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 + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +#endif + +#endif + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + const T x {42}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "42"); + BOOST_TEST_CSTR_EQ(std::format("{:08x}", x).c_str(), "0000002a"); + BOOST_TEST_CSTR_EQ(std::format("{:#010b}", x).c_str(), "0b00101010"); +} + +template +void test_negative() +{ + const T x {-42}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "-42"); +} + +int main() +{ + test(); + test(); + test(); + test(); + test(); + + test_negative(); + test_negative(); + test_negative(); + test_negative(); + test_negative(); + + return boost::report_errors(); +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/test/test_signed_streaming.cpp b/test/test_signed_streaming.cpp new file mode 100644 index 0000000..a17abc3 --- /dev/null +++ b/test/test_signed_streaming.cpp @@ -0,0 +1,428 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + T val; + std::stringstream in; + in.str("42"); + in >> val; + + BOOST_TEST_EQ(val, T{42}); + + in.clear(); + const auto endpos {in.tellg()}; + BOOST_TEST_EQ(endpos, 2); + + std::stringstream out; + out << val; + + BOOST_TEST_CSTR_EQ(out.str().c_str(), "42"); +} + +template +void test_negative_value() +{ + T val; + std::stringstream in; + in.str("-42"); + in >> val; + + BOOST_TEST_EQ(val, T{-42}); + + in.clear(); + const auto endpos {in.tellg()}; + BOOST_TEST_EQ(endpos, 3); + + std::stringstream out; + out << val; + + BOOST_TEST_CSTR_EQ(out.str().c_str(), "-42"); +} + +// --- Output tests (operator<<) --- + +template +void test_dec_output() +{ + std::stringstream out; + out << std::dec << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "42"); +} + +template +void test_oct_output() +{ + std::stringstream out; + out << std::oct << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "52"); +} + +template +void test_hex_output() +{ + std::stringstream out; + out << std::hex << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "2a"); +} + +template +void test_hex_uppercase_output() +{ + std::stringstream out; + out << std::hex << std::uppercase << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "2A"); +} + +template +void test_hex_nouppercase_output() +{ + std::stringstream out; + out << std::hex << std::nouppercase << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "2a"); +} + +// --- Input tests (operator>>) --- + +template +void test_dec_input() +{ + T val; + std::istringstream in("42"); + in >> std::dec >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_oct_input() +{ + T val; + std::istringstream in("52"); + in >> std::oct >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_hex_input() +{ + T val; + std::istringstream in("2a"); + in >> std::hex >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_hex_uppercase_input() +{ + T val; + std::istringstream in("2A"); + in >> std::hex >> std::uppercase >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_hex_nouppercase_input() +{ + T val; + std::istringstream in("2a"); + in >> std::hex >> std::nouppercase >> val; + BOOST_TEST_EQ(val, T{42}); +} + +// --- showbase / noshowbase output tests --- + +template +void test_showbase_dec_output() +{ + std::stringstream out; + out << std::showbase << std::dec << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "42"); +} + +template +void test_showbase_oct_output() +{ + std::stringstream out; + out << std::showbase << std::oct << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "052"); +} + +template +void test_showbase_hex_output() +{ + std::stringstream out; + out << std::showbase << std::hex << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "0x2a"); +} + +template +void test_showbase_hex_uppercase_output() +{ + std::stringstream out; + out << std::showbase << std::hex << std::uppercase << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "0X2A"); +} + +template +void test_noshowbase_dec_output() +{ + std::stringstream out; + out << std::noshowbase << std::dec << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "42"); +} + +template +void test_noshowbase_oct_output() +{ + std::stringstream out; + out << std::noshowbase << std::oct << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "52"); +} + +template +void test_noshowbase_hex_output() +{ + std::stringstream out; + out << std::noshowbase << std::hex << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "2a"); +} + +template +void test_noshowbase_hex_uppercase_output() +{ + std::stringstream out; + out << std::noshowbase << std::hex << std::uppercase << T{42}; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "2A"); +} + +// --- showbase / noshowbase input tests --- + +template +void test_showbase_dec_input() +{ + T val; + std::istringstream in("42"); + in >> std::showbase >> std::dec >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_showbase_oct_input() +{ + T val; + std::istringstream in("52"); + in >> std::showbase >> std::oct >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_showbase_hex_input() +{ + T val; + std::istringstream in("2a"); + in >> std::showbase >> std::hex >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_noshowbase_dec_input() +{ + T val; + std::istringstream in("42"); + in >> std::noshowbase >> std::dec >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_noshowbase_oct_input() +{ + T val; + std::istringstream in("52"); + in >> std::noshowbase >> std::oct >> val; + BOOST_TEST_EQ(val, T{42}); +} + +template +void test_noshowbase_hex_input() +{ + T val; + std::istringstream in("2a"); + in >> std::noshowbase >> std::hex >> val; + BOOST_TEST_EQ(val, T{42}); +} + +int main() +{ + test(); + test(); + test(); + test(); + test(); + + test_negative_value(); + test_negative_value(); + test_negative_value(); + test_negative_value(); + test_negative_value(); + + // Output tests + test_dec_output(); + test_dec_output(); + test_dec_output(); + test_dec_output(); + test_dec_output(); + + test_oct_output(); + test_oct_output(); + test_oct_output(); + test_oct_output(); + test_oct_output(); + + test_hex_output(); + test_hex_output(); + test_hex_output(); + test_hex_output(); + test_hex_output(); + + test_hex_uppercase_output(); + test_hex_uppercase_output(); + test_hex_uppercase_output(); + test_hex_uppercase_output(); + test_hex_uppercase_output(); + + test_hex_nouppercase_output(); + test_hex_nouppercase_output(); + test_hex_nouppercase_output(); + test_hex_nouppercase_output(); + test_hex_nouppercase_output(); + + // Input tests + test_dec_input(); + test_dec_input(); + test_dec_input(); + test_dec_input(); + test_dec_input(); + + test_oct_input(); + test_oct_input(); + test_oct_input(); + test_oct_input(); + test_oct_input(); + + test_hex_input(); + test_hex_input(); + test_hex_input(); + test_hex_input(); + test_hex_input(); + + test_hex_uppercase_input(); + test_hex_uppercase_input(); + test_hex_uppercase_input(); + test_hex_uppercase_input(); + test_hex_uppercase_input(); + + test_hex_nouppercase_input(); + test_hex_nouppercase_input(); + test_hex_nouppercase_input(); + test_hex_nouppercase_input(); + test_hex_nouppercase_input(); + + // showbase output tests + test_showbase_dec_output(); + test_showbase_dec_output(); + test_showbase_dec_output(); + test_showbase_dec_output(); + test_showbase_dec_output(); + + test_showbase_oct_output(); + test_showbase_oct_output(); + test_showbase_oct_output(); + test_showbase_oct_output(); + test_showbase_oct_output(); + + test_showbase_hex_output(); + test_showbase_hex_output(); + test_showbase_hex_output(); + test_showbase_hex_output(); + test_showbase_hex_output(); + + test_showbase_hex_uppercase_output(); + test_showbase_hex_uppercase_output(); + test_showbase_hex_uppercase_output(); + test_showbase_hex_uppercase_output(); + test_showbase_hex_uppercase_output(); + + // noshowbase output tests + test_noshowbase_dec_output(); + test_noshowbase_dec_output(); + test_noshowbase_dec_output(); + test_noshowbase_dec_output(); + test_noshowbase_dec_output(); + + test_noshowbase_oct_output(); + test_noshowbase_oct_output(); + test_noshowbase_oct_output(); + test_noshowbase_oct_output(); + test_noshowbase_oct_output(); + + test_noshowbase_hex_output(); + test_noshowbase_hex_output(); + test_noshowbase_hex_output(); + test_noshowbase_hex_output(); + test_noshowbase_hex_output(); + + test_noshowbase_hex_uppercase_output(); + test_noshowbase_hex_uppercase_output(); + test_noshowbase_hex_uppercase_output(); + test_noshowbase_hex_uppercase_output(); + test_noshowbase_hex_uppercase_output(); + + // showbase input tests + test_showbase_dec_input(); + test_showbase_dec_input(); + test_showbase_dec_input(); + test_showbase_dec_input(); + test_showbase_dec_input(); + + test_showbase_oct_input(); + test_showbase_oct_input(); + test_showbase_oct_input(); + test_showbase_oct_input(); + test_showbase_oct_input(); + + test_showbase_hex_input(); + test_showbase_hex_input(); + test_showbase_hex_input(); + test_showbase_hex_input(); + test_showbase_hex_input(); + + // noshowbase input tests + test_noshowbase_dec_input(); + test_noshowbase_dec_input(); + test_noshowbase_dec_input(); + test_noshowbase_dec_input(); + test_noshowbase_dec_input(); + + test_noshowbase_oct_input(); + test_noshowbase_oct_input(); + test_noshowbase_oct_input(); + test_noshowbase_oct_input(); + test_noshowbase_oct_input(); + + test_noshowbase_hex_input(); + test_noshowbase_hex_input(); + test_noshowbase_hex_input(); + test_noshowbase_hex_input(); + test_noshowbase_hex_input(); + + return boost::report_errors(); +} diff --git a/test/test_signed_to_from_be.cpp b/test/test_signed_to_from_be.cpp new file mode 100644 index 0000000..b76ea46 --- /dev/null +++ b/test/test_signed_to_from_be.cpp @@ -0,0 +1,289 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// to_be followed by from_be should round-trip to the original value +// ============================================================================= + +template +void test_round_trip() +{ + using basis_type = detail::underlying_type_t; + + // Zero + { + const auto val = T{static_cast(0)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + // One + { + const auto val = T{static_cast(1)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + // Negative one + { + const auto val = T{static_cast(-1)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + // Negative forty-two + { + const auto val = T{static_cast(-42)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + // Min + { + const auto val = T{std::numeric_limits::min()}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + // Max + { + const auto val = T{std::numeric_limits::max()}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + // Arbitrary positive value + { + const auto val = T{static_cast(0x42)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val = T{static_cast(-0x1234)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val = T{static_cast(-100000)}; + BOOST_TEST(from_be(to_be(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 8) + { + const auto val = T{static_cast(-1000000000000LL)}; + BOOST_TEST(from_be(to_be(val)) == val); + } +} + +// ============================================================================= +// to_be should produce the same result as manually byteswapping on little-endian, +// or be a no-op on big-endian +// ============================================================================= + +template +void test_to_be_matches_byteswap() +{ + using basis_type = detail::underlying_type_t; + + const auto val = T{static_cast(-1)}; + const auto be_val = to_be(val); + + if constexpr (std::endian::native == std::endian::big) + { + BOOST_TEST(be_val == val); + } + else + { + BOOST_TEST(be_val == byteswap(val)); + } + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val2 = T{static_cast(-42)}; + const auto be_val2 = to_be(val2); + + if constexpr (std::endian::native == std::endian::big) + { + BOOST_TEST(be_val2 == val2); + } + else + { + BOOST_TEST(be_val2 == byteswap(val2)); + } + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val4 = T{static_cast(-100000)}; + const auto be_val4 = to_be(val4); + + if constexpr (std::endian::native == std::endian::big) + { + BOOST_TEST(be_val4 == val4); + } + else + { + BOOST_TEST(be_val4 == byteswap(val4)); + } + } + + if constexpr (sizeof(basis_type) >= 8) + { + const auto val8 = T{static_cast(-1000000000000LL)}; + const auto be_val8 = to_be(val8); + + if constexpr (std::endian::native == std::endian::big) + { + BOOST_TEST(be_val8 == val8); + } + else + { + BOOST_TEST(be_val8 == byteswap(val8)); + } + } +} + +// ============================================================================= +// from_be is the same operation as to_be (self-inverse) +// ============================================================================= + +template +void test_from_be_equals_to_be() +{ + using basis_type = detail::underlying_type_t; + + const auto val = T{static_cast(-1)}; + BOOST_TEST(from_be(val) == to_be(val)); + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val2 = T{static_cast(-42)}; + BOOST_TEST(from_be(val2) == to_be(val2)); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val4 = T{static_cast(-100000)}; + BOOST_TEST(from_be(val4) == to_be(val4)); + } +} + +// ============================================================================= +// Double application of to_be should return the original value +// ============================================================================= + +template +void test_double_to_be_is_identity() +{ + using basis_type = detail::underlying_type_t; + + { + const auto val = T{static_cast(0)}; + BOOST_TEST(to_be(to_be(val)) == val); + } + + { + const auto val = T{static_cast(-1)}; + BOOST_TEST(to_be(to_be(val)) == val); + } + + { + const auto val = T{static_cast(-42)}; + BOOST_TEST(to_be(to_be(val)) == val); + } + + { + const auto val = T{std::numeric_limits::min()}; + BOOST_TEST(to_be(to_be(val)) == val); + } + + { + const auto val = T{std::numeric_limits::max()}; + BOOST_TEST(to_be(to_be(val)) == val); + } +} + +// ============================================================================= +// Verify known byte patterns on little-endian systems for negative values +// ============================================================================= + +void test_known_byte_patterns() +{ + if constexpr (std::endian::native == std::endian::little) + { + // i8: single byte, should be identity + { + const auto val = i8{static_cast(-1)}; + BOOST_TEST(to_be(val) == val); + } + + // i16: -1 is 0xFFFF in both byte orders, so to_be(-1) == -1 + { + const auto val = i16{static_cast(-1)}; + BOOST_TEST(to_be(val) == val); + } + + // i16: 0x0102 should become 0x0201 + { + const auto val = i16{static_cast(0x0102)}; + const auto expected = i16{static_cast(0x0201)}; + BOOST_TEST(to_be(val) == expected); + } + + // i32: 0x01020304 should become 0x04030201 + { + const auto val = i32{static_cast(0x01020304)}; + const auto expected = i32{static_cast(0x04030201)}; + BOOST_TEST(to_be(val) == expected); + } + + // i32: -1 is 0xFFFFFFFF, symmetric under byte swap + { + const auto val = i32{static_cast(-1)}; + BOOST_TEST(to_be(val) == val); + } + } +} + +int main() +{ + // Round-trip tests for all signed types + test_round_trip(); + test_round_trip(); + test_round_trip(); + test_round_trip(); + // i128 skipped: boost::core::byteswap does not support int128_t + + // to_be matches byteswap + test_to_be_matches_byteswap(); + test_to_be_matches_byteswap(); + test_to_be_matches_byteswap(); + test_to_be_matches_byteswap(); + + // from_be == to_be (self-inverse property) + test_from_be_equals_to_be(); + test_from_be_equals_to_be(); + test_from_be_equals_to_be(); + test_from_be_equals_to_be(); + + // Double application is identity + test_double_to_be_is_identity(); + test_double_to_be_is_identity(); + test_double_to_be_is_identity(); + test_double_to_be_is_identity(); + // i128 skipped: boost::core::byteswap does not support int128_t + + // Known byte patterns on little-endian + test_known_byte_patterns(); + + return boost::report_errors(); +} diff --git a/test/test_signed_to_from_be_bytes.cpp b/test/test_signed_to_from_be_bytes.cpp new file mode 100644 index 0000000..7a6ed30 --- /dev/null +++ b/test/test_signed_to_from_be_bytes.cpp @@ -0,0 +1,273 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// Ignore [[nodiscard]] on the test that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE +import boost.safe_numbers; +#else +#include +#include +#include +#endif + +using namespace boost::safe_numbers; + +// ============================================================================= +// to_be_bytes: known byte patterns for signed types +// ============================================================================= + +void test_to_be_bytes_i8() +{ + // Positive value + { + const auto bytes = to_be_bytes(i8{static_cast(0x42)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{1}); + BOOST_TEST(bytes[0] == std::byte{0x42}); + } + + // Negative value: -1 is 0xFF in two's complement + { + const auto bytes = to_be_bytes(i8{static_cast(-1)}); + BOOST_TEST(bytes[0] == std::byte{0xFF}); + } + + // Negative value: -42 is 0xD6 + { + const auto bytes = to_be_bytes(i8{static_cast(-42)}); + BOOST_TEST(bytes[0] == std::byte{0xD6}); + } +} + +void test_to_be_bytes_i16() +{ + // Positive value: big-endian most significant byte first + { + const auto bytes = to_be_bytes(i16{static_cast(0x0102)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{2}); + BOOST_TEST(bytes[0] == std::byte{0x01}); + BOOST_TEST(bytes[1] == std::byte{0x02}); + } + + // -1 is 0xFFFF + { + const auto bytes = to_be_bytes(i16{static_cast(-1)}); + BOOST_TEST(bytes[0] == std::byte{0xFF}); + BOOST_TEST(bytes[1] == std::byte{0xFF}); + } + + // -42 is 0xFFD6 + { + const auto bytes = to_be_bytes(i16{static_cast(-42)}); + BOOST_TEST(bytes[0] == std::byte{0xFF}); + BOOST_TEST(bytes[1] == std::byte{0xD6}); + } +} + +void test_to_be_bytes_i32() +{ + { + const auto bytes = to_be_bytes(i32{static_cast(0x01020304)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{4}); + BOOST_TEST(bytes[0] == std::byte{0x01}); + BOOST_TEST(bytes[1] == std::byte{0x02}); + BOOST_TEST(bytes[2] == std::byte{0x03}); + BOOST_TEST(bytes[3] == std::byte{0x04}); + } + + // -1 is 0xFFFFFFFF + { + const auto bytes = to_be_bytes(i32{static_cast(-1)}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0xFF}); + } + } + + // -42 is 0xFFFFFFD6 + { + const auto bytes = to_be_bytes(i32{static_cast(-42)}); + BOOST_TEST(bytes[0] == std::byte{0xFF}); + BOOST_TEST(bytes[1] == std::byte{0xFF}); + BOOST_TEST(bytes[2] == std::byte{0xFF}); + BOOST_TEST(bytes[3] == std::byte{0xD6}); + } +} + +void test_to_be_bytes_i64() +{ + { + const auto bytes = to_be_bytes(i64{static_cast(0x0102030405060708LL)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{8}); + BOOST_TEST(bytes[0] == std::byte{0x01}); + BOOST_TEST(bytes[1] == std::byte{0x02}); + BOOST_TEST(bytes[2] == std::byte{0x03}); + BOOST_TEST(bytes[3] == std::byte{0x04}); + BOOST_TEST(bytes[4] == std::byte{0x05}); + BOOST_TEST(bytes[5] == std::byte{0x06}); + BOOST_TEST(bytes[6] == std::byte{0x07}); + BOOST_TEST(bytes[7] == std::byte{0x08}); + } + + // -1 is all 0xFF + { + const auto bytes = to_be_bytes(i64{static_cast(-1)}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0xFF}); + } + } +} + +void test_to_be_bytes_zero() +{ + { + const auto bytes = to_be_bytes(i8{static_cast(0)}); + BOOST_TEST(bytes[0] == std::byte{0x00}); + } + { + const auto bytes = to_be_bytes(i32{static_cast(0)}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0x00}); + } + } +} + +// ============================================================================= +// from_be_bytes: reconstruct from known byte patterns +// ============================================================================= + +void test_from_be_bytes_i8() +{ + const std::array bytes{std::byte{0xD6}}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == i8{static_cast(-42)}); +} + +void test_from_be_bytes_i16() +{ + // -42 in big-endian: 0xFF 0xD6 + const std::array bytes{std::byte{0xFF}, std::byte{0xD6}}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == i16{static_cast(-42)}); +} + +void test_from_be_bytes_i32() +{ + const std::array bytes{std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == i32{static_cast(0x01020304)}); +} + +void test_from_be_bytes_i64() +{ + const std::array bytes{ + std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, + std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF} + }; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == i64{static_cast(-1)}); +} + +void test_from_be_bytes_zero() +{ + const std::array bytes{}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == i32{static_cast(0)}); +} + +// ============================================================================= +// Round-trip: to_be_bytes -> from_be_bytes == identity +// ============================================================================= + +template +void test_round_trip(T value) +{ + const auto bytes = to_be_bytes(value); + const auto result = from_be_bytes(std::span{bytes}); + BOOST_TEST(result == value); +} + +void test_round_trips() +{ + test_round_trip(i8{static_cast(0)}); + test_round_trip(i8{static_cast(-1)}); + test_round_trip(i8{static_cast(-42)}); + test_round_trip(i8{static_cast(127)}); + test_round_trip(i8{static_cast(-128)}); + + test_round_trip(i16{static_cast(0)}); + test_round_trip(i16{static_cast(-1)}); + test_round_trip(i16{static_cast(-42)}); + test_round_trip(i16{std::numeric_limits::min()}); + test_round_trip(i16{std::numeric_limits::max()}); + + test_round_trip(i32{static_cast(0)}); + test_round_trip(i32{static_cast(-1)}); + test_round_trip(i32{static_cast(-100000)}); + test_round_trip(i32{std::numeric_limits::min()}); + test_round_trip(i32{std::numeric_limits::max()}); + + test_round_trip(i64{static_cast(0)}); + test_round_trip(i64{static_cast(-1)}); + test_round_trip(i64{static_cast(-1000000000000LL)}); + test_round_trip(i64{std::numeric_limits::min()}); + test_round_trip(i64{std::numeric_limits::max()}); +} + +// ============================================================================= +// from_be_bytes with dynamic extent: runtime size mismatch throws +// ============================================================================= + +void test_from_be_bytes_dynamic_size_mismatch() +{ + const std::array bytes{std::byte{0x01}, std::byte{0x02}}; + std::span dynamic_span{bytes}; + + BOOST_TEST_THROWS(from_be_bytes(dynamic_span), std::domain_error); +} + +void test_from_be_bytes_dynamic_size_match() +{ + const std::array bytes{std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xD6}}; + std::span dynamic_span{bytes}; + + const auto val = from_be_bytes(dynamic_span); + BOOST_TEST(val == i32{static_cast(-42)}); +} + +int main() +{ + test_to_be_bytes_i8(); + test_to_be_bytes_i16(); + test_to_be_bytes_i32(); + test_to_be_bytes_i64(); + test_to_be_bytes_zero(); + + test_from_be_bytes_i8(); + test_from_be_bytes_i16(); + test_from_be_bytes_i32(); + test_from_be_bytes_i64(); + test_from_be_bytes_zero(); + + test_round_trips(); + + test_from_be_bytes_dynamic_size_mismatch(); + test_from_be_bytes_dynamic_size_match(); + + return boost::report_errors(); +} diff --git a/test/test_signed_to_from_le.cpp b/test/test_signed_to_from_le.cpp new file mode 100644 index 0000000..bf6b0ea --- /dev/null +++ b/test/test_signed_to_from_le.cpp @@ -0,0 +1,330 @@ +// 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 +#include +#endif + +#include + +using namespace boost::safe_numbers; + +// ============================================================================= +// to_le followed by from_le should round-trip to the original value +// ============================================================================= + +template +void test_round_trip() +{ + using basis_type = detail::underlying_type_t; + + // Zero + { + const auto val = T{static_cast(0)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // One + { + const auto val = T{static_cast(1)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Negative one + { + const auto val = T{static_cast(-1)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Negative forty-two + { + const auto val = T{static_cast(-42)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Min + { + const auto val = T{std::numeric_limits::min()}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Max + { + const auto val = T{std::numeric_limits::max()}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Arbitrary positive value + { + const auto val = T{static_cast(0x42)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val = T{static_cast(-0x1234)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val = T{static_cast(-100000)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 8) + { + const auto val = T{static_cast(-1000000000000LL)}; + BOOST_TEST(from_le(to_le(val)) == val); + } +} + +// ============================================================================= +// to_le should produce the same result as manually byteswapping on big-endian, +// or be a no-op on little-endian +// ============================================================================= + +template +void test_to_le_matches_byteswap() +{ + using basis_type = detail::underlying_type_t; + + const auto val = T{static_cast(-1)}; + const auto le_val = to_le(val); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val == val); + } + else + { + BOOST_TEST(le_val == byteswap(val)); + } + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val2 = T{static_cast(-42)}; + const auto le_val2 = to_le(val2); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val2 == val2); + } + else + { + BOOST_TEST(le_val2 == byteswap(val2)); + } + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val4 = T{static_cast(-100000)}; + const auto le_val4 = to_le(val4); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val4 == val4); + } + else + { + BOOST_TEST(le_val4 == byteswap(val4)); + } + } + + if constexpr (sizeof(basis_type) >= 8) + { + const auto val8 = T{static_cast(-1000000000000LL)}; + const auto le_val8 = to_le(val8); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val8 == val8); + } + else + { + BOOST_TEST(le_val8 == byteswap(val8)); + } + } +} + +// ============================================================================= +// from_le is the same operation as to_le (self-inverse) +// ============================================================================= + +template +void test_from_le_equals_to_le() +{ + using basis_type = detail::underlying_type_t; + + const auto val = T{static_cast(-1)}; + BOOST_TEST(from_le(val) == to_le(val)); + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val2 = T{static_cast(-42)}; + BOOST_TEST(from_le(val2) == to_le(val2)); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val4 = T{static_cast(-100000)}; + BOOST_TEST(from_le(val4) == to_le(val4)); + } +} + +// ============================================================================= +// Double application of to_le should return the original value +// ============================================================================= + +template +void test_double_to_le_is_identity() +{ + using basis_type = detail::underlying_type_t; + + { + const auto val = T{static_cast(0)}; + BOOST_TEST(to_le(to_le(val)) == val); + } + + { + const auto val = T{static_cast(-1)}; + BOOST_TEST(to_le(to_le(val)) == val); + } + + { + const auto val = T{static_cast(-42)}; + BOOST_TEST(to_le(to_le(val)) == val); + } + + { + const auto val = T{std::numeric_limits::min()}; + BOOST_TEST(to_le(to_le(val)) == val); + } + + { + const auto val = T{std::numeric_limits::max()}; + BOOST_TEST(to_le(to_le(val)) == val); + } +} + +// ============================================================================= +// Verify known byte patterns +// ============================================================================= + +void test_known_byte_patterns() +{ + if constexpr (std::endian::native == std::endian::little) + { + // On little-endian, to_le is identity + { + const auto val = i16{static_cast(-1)}; + BOOST_TEST(to_le(val) == val); + } + + { + const auto val = i32{static_cast(-42)}; + BOOST_TEST(to_le(val) == val); + } + + { + const auto val = i64{static_cast(-1000000000000LL)}; + BOOST_TEST(to_le(val) == val); + } + + { + const auto val = i8{static_cast(-1)}; + BOOST_TEST(to_le(val) == val); + } + } + else + { + // On big-endian, to_le swaps bytes + // i16: 0x0102 should become 0x0201 + { + const auto val = i16{static_cast(0x0102)}; + const auto expected = i16{static_cast(0x0201)}; + BOOST_TEST(to_le(val) == expected); + } + + // i32: 0x01020304 should become 0x04030201 + { + const auto val = i32{static_cast(0x01020304)}; + const auto expected = i32{static_cast(0x04030201)}; + BOOST_TEST(to_le(val) == expected); + } + + // i8: should be identity (single byte) + { + const auto val = i8{static_cast(-42)}; + BOOST_TEST(to_le(val) == val); + } + } +} + +// ============================================================================= +// to_le and to_be should both round-trip correctly +// ============================================================================= + +template +void test_le_be_relationship() +{ + using basis_type = detail::underlying_type_t; + + { + const auto val = T{static_cast(-42)}; + BOOST_TEST(from_le(to_le(val)) == val); + BOOST_TEST(from_be(to_be(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val = T{static_cast(-100000)}; + BOOST_TEST(from_le(to_le(val)) == val); + BOOST_TEST(from_be(to_be(val)) == val); + } +} + +int main() +{ + // Round-trip tests for all signed types + test_round_trip(); + test_round_trip(); + test_round_trip(); + test_round_trip(); + // i128 skipped: boost::core::byteswap does not support int128_t + + // to_le matches byteswap on big-endian, identity on little-endian + test_to_le_matches_byteswap(); + test_to_le_matches_byteswap(); + test_to_le_matches_byteswap(); + test_to_le_matches_byteswap(); + + // from_le == to_le (self-inverse property) + test_from_le_equals_to_le(); + test_from_le_equals_to_le(); + test_from_le_equals_to_le(); + test_from_le_equals_to_le(); + + // Double application is identity + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + // i128 skipped: boost::core::byteswap does not support int128_t + + // Known byte patterns + test_known_byte_patterns(); + + // Relationship between to_le and to_be + test_le_be_relationship(); + test_le_be_relationship(); + test_le_be_relationship(); + test_le_be_relationship(); + + return boost::report_errors(); +} diff --git a/test/test_signed_to_from_le_bytes.cpp b/test/test_signed_to_from_le_bytes.cpp new file mode 100644 index 0000000..e0a0d28 --- /dev/null +++ b/test/test_signed_to_from_le_bytes.cpp @@ -0,0 +1,278 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// Ignore [[nodiscard]] on the test that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE +import boost.safe_numbers; +#else +#include +#include +#include +#endif + +using namespace boost::safe_numbers; + +// ============================================================================= +// to_le_bytes: known byte patterns for signed types +// ============================================================================= + +void test_to_le_bytes_i8() +{ + { + const auto bytes = to_le_bytes(i8{static_cast(0x42)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{1}); + BOOST_TEST(bytes[0] == std::byte{0x42}); + } + + // -1 is 0xFF + { + const auto bytes = to_le_bytes(i8{static_cast(-1)}); + BOOST_TEST(bytes[0] == std::byte{0xFF}); + } + + // -42 is 0xD6 + { + const auto bytes = to_le_bytes(i8{static_cast(-42)}); + BOOST_TEST(bytes[0] == std::byte{0xD6}); + } +} + +void test_to_le_bytes_i16() +{ + // Little-endian: least significant byte first + // 0x0102 -> bytes[0]=0x02, bytes[1]=0x01 + { + const auto bytes = to_le_bytes(i16{static_cast(0x0102)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{2}); + BOOST_TEST(bytes[0] == std::byte{0x02}); + BOOST_TEST(bytes[1] == std::byte{0x01}); + } + + // -1 is 0xFFFF -> 0xFF 0xFF + { + const auto bytes = to_le_bytes(i16{static_cast(-1)}); + BOOST_TEST(bytes[0] == std::byte{0xFF}); + BOOST_TEST(bytes[1] == std::byte{0xFF}); + } + + // -42 is 0xFFD6 -> little-endian: 0xD6 0xFF + { + const auto bytes = to_le_bytes(i16{static_cast(-42)}); + BOOST_TEST(bytes[0] == std::byte{0xD6}); + BOOST_TEST(bytes[1] == std::byte{0xFF}); + } +} + +void test_to_le_bytes_i32() +{ + // 0x01020304 -> little-endian: 04 03 02 01 + { + const auto bytes = to_le_bytes(i32{static_cast(0x01020304)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{4}); + BOOST_TEST(bytes[0] == std::byte{0x04}); + BOOST_TEST(bytes[1] == std::byte{0x03}); + BOOST_TEST(bytes[2] == std::byte{0x02}); + BOOST_TEST(bytes[3] == std::byte{0x01}); + } + + // -1 is all 0xFF + { + const auto bytes = to_le_bytes(i32{static_cast(-1)}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0xFF}); + } + } + + // -42 is 0xFFFFFFD6 -> little-endian: D6 FF FF FF + { + const auto bytes = to_le_bytes(i32{static_cast(-42)}); + BOOST_TEST(bytes[0] == std::byte{0xD6}); + BOOST_TEST(bytes[1] == std::byte{0xFF}); + BOOST_TEST(bytes[2] == std::byte{0xFF}); + BOOST_TEST(bytes[3] == std::byte{0xFF}); + } +} + +void test_to_le_bytes_i64() +{ + // 0x0102030405060708 -> little-endian: 08 07 06 05 04 03 02 01 + { + const auto bytes = to_le_bytes(i64{static_cast(0x0102030405060708LL)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{8}); + BOOST_TEST(bytes[0] == std::byte{0x08}); + BOOST_TEST(bytes[1] == std::byte{0x07}); + BOOST_TEST(bytes[2] == std::byte{0x06}); + BOOST_TEST(bytes[3] == std::byte{0x05}); + BOOST_TEST(bytes[4] == std::byte{0x04}); + BOOST_TEST(bytes[5] == std::byte{0x03}); + BOOST_TEST(bytes[6] == std::byte{0x02}); + BOOST_TEST(bytes[7] == std::byte{0x01}); + } + + // -1 is all 0xFF + { + const auto bytes = to_le_bytes(i64{static_cast(-1)}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0xFF}); + } + } +} + +void test_to_le_bytes_zero() +{ + { + const auto bytes = to_le_bytes(i8{static_cast(0)}); + BOOST_TEST(bytes[0] == std::byte{0x00}); + } + { + const auto bytes = to_le_bytes(i32{static_cast(0)}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0x00}); + } + } +} + +// ============================================================================= +// from_le_bytes: reconstruct from known byte patterns +// ============================================================================= + +void test_from_le_bytes_i8() +{ + const std::array bytes{std::byte{0xD6}}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == i8{static_cast(-42)}); +} + +void test_from_le_bytes_i16() +{ + // -42 in little-endian: 0xD6 0xFF + const std::array bytes{std::byte{0xD6}, std::byte{0xFF}}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == i16{static_cast(-42)}); +} + +void test_from_le_bytes_i32() +{ + // 0x01020304 in little-endian: 04 03 02 01 + const std::array bytes{std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01}}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == i32{static_cast(0x01020304)}); +} + +void test_from_le_bytes_i64() +{ + // -1 in little-endian: all 0xFF + const std::array bytes{ + std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, + std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF} + }; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == i64{static_cast(-1)}); +} + +void test_from_le_bytes_zero() +{ + const std::array bytes{}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == i32{static_cast(0)}); +} + +// ============================================================================= +// Round-trip: to_le_bytes -> from_le_bytes == identity +// ============================================================================= + +template +void test_round_trip(T value) +{ + const auto bytes = to_le_bytes(value); + const auto result = from_le_bytes(std::span{bytes}); + BOOST_TEST(result == value); +} + +void test_round_trips() +{ + test_round_trip(i8{static_cast(0)}); + test_round_trip(i8{static_cast(-1)}); + test_round_trip(i8{static_cast(-42)}); + test_round_trip(i8{static_cast(127)}); + test_round_trip(i8{static_cast(-128)}); + + test_round_trip(i16{static_cast(0)}); + test_round_trip(i16{static_cast(-1)}); + test_round_trip(i16{static_cast(-42)}); + test_round_trip(i16{std::numeric_limits::min()}); + test_round_trip(i16{std::numeric_limits::max()}); + + test_round_trip(i32{static_cast(0)}); + test_round_trip(i32{static_cast(-1)}); + test_round_trip(i32{static_cast(-100000)}); + test_round_trip(i32{std::numeric_limits::min()}); + test_round_trip(i32{std::numeric_limits::max()}); + + test_round_trip(i64{static_cast(0)}); + test_round_trip(i64{static_cast(-1)}); + test_round_trip(i64{static_cast(-1000000000000LL)}); + test_round_trip(i64{std::numeric_limits::min()}); + test_round_trip(i64{std::numeric_limits::max()}); +} + +// ============================================================================= +// from_le_bytes with dynamic extent: runtime size mismatch throws +// ============================================================================= + +void test_from_le_bytes_dynamic_size_mismatch() +{ + const std::array bytes{std::byte{0x01}, std::byte{0x02}}; + std::span dynamic_span{bytes}; + + BOOST_TEST_THROWS(from_le_bytes(dynamic_span), std::domain_error); +} + +void test_from_le_bytes_dynamic_size_match() +{ + // -42 in little-endian i32: D6 FF FF FF + const std::array bytes{std::byte{0xD6}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}}; + std::span dynamic_span{bytes}; + + const auto val = from_le_bytes(dynamic_span); + BOOST_TEST(val == i32{static_cast(-42)}); +} + +int main() +{ + test_to_le_bytes_i8(); + test_to_le_bytes_i16(); + test_to_le_bytes_i32(); + test_to_le_bytes_i64(); + test_to_le_bytes_zero(); + + test_from_le_bytes_i8(); + test_from_le_bytes_i16(); + test_from_le_bytes_i32(); + test_from_le_bytes_i64(); + test_from_le_bytes_zero(); + + test_round_trips(); + + test_from_le_bytes_dynamic_size_mismatch(); + test_from_le_bytes_dynamic_size_match(); + + return boost::report_errors(); +} diff --git a/test/test_signed_to_from_ne_bytes.cpp b/test/test_signed_to_from_ne_bytes.cpp new file mode 100644 index 0000000..de4e763 --- /dev/null +++ b/test/test_signed_to_from_ne_bytes.cpp @@ -0,0 +1,183 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// Ignore [[nodiscard]] on the test that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE +import boost.safe_numbers; +#else +#include +#include +#include +#endif + +using namespace boost::safe_numbers; + +// ============================================================================= +// to_ne_bytes: the byte representation matches the platform's native layout +// ============================================================================= + +void test_to_ne_bytes_matches_platform() +{ + // On little-endian, to_ne_bytes should match to_le_bytes + // On big-endian, to_ne_bytes should match to_be_bytes + { + const auto ne_bytes = to_ne_bytes(i32{static_cast(-42)}); + if constexpr (std::endian::native == std::endian::little) + { + const auto le_bytes = to_le_bytes(i32{static_cast(-42)}); + BOOST_TEST(ne_bytes == le_bytes); + } + else + { + const auto be_bytes = to_be_bytes(i32{static_cast(-42)}); + BOOST_TEST(ne_bytes == be_bytes); + } + } + { + const auto ne_bytes = to_ne_bytes(i64{static_cast(-1000000000000LL)}); + if constexpr (std::endian::native == std::endian::little) + { + const auto le_bytes = to_le_bytes(i64{static_cast(-1000000000000LL)}); + BOOST_TEST(ne_bytes == le_bytes); + } + else + { + const auto be_bytes = to_be_bytes(i64{static_cast(-1000000000000LL)}); + BOOST_TEST(ne_bytes == be_bytes); + } + } +} + +void test_to_ne_bytes_is_identity() +{ + // to_ne_bytes should produce the same bytes as a bit_cast of the value + { + const auto val = i32{static_cast(-42)}; + const auto ne_bytes = to_ne_bytes(val); + const auto raw_bytes = std::bit_cast>(val); + BOOST_TEST(ne_bytes == raw_bytes); + } + { + const auto val = i16{static_cast(-1)}; + const auto ne_bytes = to_ne_bytes(val); + const auto raw_bytes = std::bit_cast>(val); + BOOST_TEST(ne_bytes == raw_bytes); + } + { + const auto val = i64{static_cast(-1000000000000LL)}; + const auto ne_bytes = to_ne_bytes(val); + const auto raw_bytes = std::bit_cast>(val); + BOOST_TEST(ne_bytes == raw_bytes); + } +} + +// ============================================================================= +// from_ne_bytes: reconstruct from native byte patterns +// ============================================================================= + +void test_from_ne_bytes_matches_platform() +{ + { + const std::array bytes{std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + const auto ne_val = from_ne_bytes(std::span{bytes}); + if constexpr (std::endian::native == std::endian::little) + { + const auto le_val = from_le_bytes(std::span{bytes}); + BOOST_TEST(ne_val == le_val); + } + else + { + const auto be_val = from_be_bytes(std::span{bytes}); + BOOST_TEST(ne_val == be_val); + } + } +} + +// ============================================================================= +// Round-trip: to_ne_bytes -> from_ne_bytes == identity +// ============================================================================= + +template +void test_round_trip(T value) +{ + const auto bytes = to_ne_bytes(value); + const auto result = from_ne_bytes(std::span{bytes}); + BOOST_TEST(result == value); +} + +void test_round_trips() +{ + test_round_trip(i8{static_cast(0)}); + test_round_trip(i8{static_cast(-1)}); + test_round_trip(i8{static_cast(-42)}); + test_round_trip(i8{static_cast(127)}); + test_round_trip(i8{static_cast(-128)}); + + test_round_trip(i16{static_cast(0)}); + test_round_trip(i16{static_cast(-1)}); + test_round_trip(i16{std::numeric_limits::min()}); + test_round_trip(i16{std::numeric_limits::max()}); + + test_round_trip(i32{static_cast(0)}); + test_round_trip(i32{static_cast(-1)}); + test_round_trip(i32{static_cast(-100000)}); + test_round_trip(i32{std::numeric_limits::min()}); + test_round_trip(i32{std::numeric_limits::max()}); + + test_round_trip(i64{static_cast(0)}); + test_round_trip(i64{static_cast(-1)}); + test_round_trip(i64{static_cast(-1000000000000LL)}); + test_round_trip(i64{std::numeric_limits::min()}); + test_round_trip(i64{std::numeric_limits::max()}); +} + +// ============================================================================= +// from_ne_bytes with dynamic extent +// ============================================================================= + +void test_from_ne_bytes_dynamic_size_mismatch() +{ + const std::array bytes{std::byte{0x01}, std::byte{0x02}}; + std::span dynamic_span{bytes}; + + BOOST_TEST_THROWS(from_ne_bytes(dynamic_span), std::domain_error); +} + +void test_from_ne_bytes_dynamic_size_match() +{ + const auto val = i32{static_cast(-42)}; + const auto bytes = to_ne_bytes(val); + std::span dynamic_span{bytes}; + + const auto result = from_ne_bytes(dynamic_span); + BOOST_TEST(result == val); +} + +int main() +{ + test_to_ne_bytes_matches_platform(); + test_to_ne_bytes_is_identity(); + + test_from_ne_bytes_matches_platform(); + + test_round_trips(); + + test_from_ne_bytes_dynamic_size_mismatch(); + test_from_ne_bytes_dynamic_size_match(); + + return boost::report_errors(); +}