diff --git a/src/lang/numeric/decimal.cc b/src/lang/numeric/decimal.cc index 8cd41e650..4ce4f6275 100644 --- a/src/lang/numeric/decimal.cc +++ b/src/lang/numeric/decimal.cc @@ -1,14 +1,18 @@ #include #include -#include // assert -#include // std::isfinite -#include // std::int64_t, std::uint64_t -#include // std::strlen -#include // std::numeric_limits -#include // placement new -#include // std::string, std::stof, std::stod -#include // std::move +#include // assert +#include // std::isfinite +#include // std::int64_t, std::uint64_t +#include // std::strlen +#include // std::setprecision +#include // std::numeric_limits +#include // new +#include // std::ostringstream +#include // std::out_of_range +#include // std::string, std::stof, std::stod +#include // std::is_same_v +#include // std::move #include // mpd_* @@ -57,6 +61,41 @@ thread_local mpd_context_t max_context = []() { return context; }(); +template +auto is_representable_as_floating_point( + const mpd_t *const mpd_value, const sourcemeta::core::Decimal &decimal) + -> bool { + if (mpd_isnan(mpd_value) || mpd_isinfinite(mpd_value)) { + return true; + } + + if (!mpd_isfinite(mpd_value)) { + return false; + } + + const std::string decimal_string{decimal.to_scientific_string()}; + FloatingPointType converted_value; + try { + if constexpr (std::is_same_v) { + converted_value = std::stof(decimal_string); + } else if constexpr (std::is_same_v) { + converted_value = std::stod(decimal_string); + } + } catch (const std::out_of_range &) { + return false; + } + + if (!std::isfinite(converted_value)) { + return false; + } + + std::ostringstream oss; + oss << std::setprecision(std::numeric_limits::max_digits10) + << converted_value; + const sourcemeta::core::Decimal roundtrip{oss.str()}; + return decimal == roundtrip; +} + } // namespace namespace sourcemeta::core { @@ -294,15 +333,13 @@ auto Decimal::to_uint32() const -> std::uint32_t { } auto Decimal::to_float() const -> float { - assert(this->is_float()); - const std::string str{this->to_scientific_string()}; - return std::stof(str); + const std::string scientific_notation{this->to_scientific_string()}; + return std::stof(scientific_notation); } auto Decimal::to_double() const -> double { - assert(this->is_double()); - const std::string str{this->to_scientific_string()}; - return std::stod(str); + const std::string scientific_notation{this->to_scientific_string()}; + return std::stod(scientific_notation); } auto Decimal::is_zero() const -> bool { @@ -323,41 +360,12 @@ auto Decimal::is_real() const -> bool { } auto Decimal::is_float() const -> bool { - if (mpd_isnan(&this->data()->value) || mpd_isinfinite(&this->data()->value)) { - return true; - } - - if (!mpd_isfinite(&this->data()->value)) { - return false; - } - - const std::string str{this->to_scientific_string()}; - const float float_value{std::stof(str)}; - if (!std::isfinite(float_value)) { - return false; - } - - const Decimal roundtrip{std::to_string(float_value)}; - return *this == roundtrip; + return is_representable_as_floating_point(&this->data()->value, *this); } auto Decimal::is_double() const -> bool { - if (mpd_isnan(&this->data()->value) || mpd_isinfinite(&this->data()->value)) { - return true; - } - - if (!mpd_isfinite(&this->data()->value)) { - return false; - } - - const std::string str{this->to_scientific_string()}; - const double double_value{std::stod(str)}; - if (!std::isfinite(double_value)) { - return false; - } - - const Decimal roundtrip{std::to_string(double_value)}; - return *this == roundtrip; + return is_representable_as_floating_point(&this->data()->value, + *this); } auto Decimal::is_int32() const -> bool { diff --git a/test/numeric/numeric_decimal_test.cc b/test/numeric/numeric_decimal_test.cc index 91a8b0dcb..7b448dbae 100644 --- a/test/numeric/numeric_decimal_test.cc +++ b/test/numeric/numeric_decimal_test.cc @@ -497,7 +497,7 @@ TEST(Numeric_decimal, is_float_simple_values) { const sourcemeta::core::Decimal value2{"3.14"}; const sourcemeta::core::Decimal value3{"-2.5"}; EXPECT_TRUE(value1.is_float()); - EXPECT_TRUE(value2.is_float()); + EXPECT_FALSE(value2.is_float()); EXPECT_TRUE(value3.is_float()); } @@ -515,12 +515,92 @@ TEST(Numeric_decimal, is_float_high_precision_loss) { EXPECT_FALSE(value.is_float()); } +TEST(Numeric_decimal, is_float_large_exponent_in_range) { + const sourcemeta::core::Decimal value{"1.5e30"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_large_negative_exponent_in_range) { + const sourcemeta::core::Decimal value{"1.5e-30"}; + EXPECT_TRUE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_too_large) { + const sourcemeta::core::Decimal value{"1e100"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_too_small) { + const sourcemeta::core::Decimal value{"1e-100"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_near_max_float) { + const sourcemeta::core::Decimal value{"3.4e38"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_exceeds_max_float) { + const sourcemeta::core::Decimal value{"4e38"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_near_min_float) { + const sourcemeta::core::Decimal value{"1.2e-38"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_below_min_float) { + const sourcemeta::core::Decimal value{"1e-50"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_6_significant_digits) { + const sourcemeta::core::Decimal value{"1.234375"}; + EXPECT_TRUE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_7_significant_digits_no_loss) { + const sourcemeta::core::Decimal value{"1.2343750"}; + EXPECT_TRUE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_many_digits_with_loss) { + const sourcemeta::core::Decimal value{"1.23456789"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_big_integer_in_float_range) { + const sourcemeta::core::Decimal value{"16777216"}; + EXPECT_TRUE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_big_integer_exceeds_precision) { + const sourcemeta::core::Decimal value{"16777217"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_small_value_with_exponent) { + const sourcemeta::core::Decimal value{"1.0e-10"}; + EXPECT_FALSE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_zero) { + const sourcemeta::core::Decimal value{"0.0"}; + EXPECT_TRUE(value.is_float()); +} + +TEST(Numeric_decimal, is_float_negative_zero) { + const sourcemeta::core::Decimal value{"-0.0"}; + EXPECT_TRUE(value.is_float()); +} + TEST(Numeric_decimal, is_double_simple_values) { const sourcemeta::core::Decimal value1{3}; const sourcemeta::core::Decimal value2{"3.14"}; const sourcemeta::core::Decimal value3{"-2.5"}; EXPECT_TRUE(value1.is_double()); - EXPECT_TRUE(value2.is_double()); + EXPECT_FALSE(value2.is_double()); EXPECT_TRUE(value3.is_double()); } @@ -539,6 +619,86 @@ TEST(Numeric_decimal, is_double_high_precision_loss) { EXPECT_FALSE(value.is_double()); } +TEST(Numeric_decimal, is_double_large_exponent_in_range) { + const sourcemeta::core::Decimal value{"1.5e100"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_large_negative_exponent_in_range) { + const sourcemeta::core::Decimal value{"1.5e-100"}; + EXPECT_TRUE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_too_large) { + const sourcemeta::core::Decimal value{"1e500"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_too_small) { + const sourcemeta::core::Decimal value{"1e-500"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_near_max_double) { + const sourcemeta::core::Decimal value{"1.7e308"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_exceeds_max_double) { + const sourcemeta::core::Decimal value{"2e308"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_near_min_double) { + const sourcemeta::core::Decimal value{"2.2e-308"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_below_min_double) { + const sourcemeta::core::Decimal value{"1e-400"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_15_significant_digits) { + const sourcemeta::core::Decimal value{"1.23456789012345"}; + EXPECT_TRUE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_16_significant_digits_no_loss) { + const sourcemeta::core::Decimal value{"1.234567890123456"}; + EXPECT_TRUE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_many_digits_with_loss) { + const sourcemeta::core::Decimal value{"1.234567890123456789"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_big_integer_in_double_range) { + const sourcemeta::core::Decimal value{"9007199254740992"}; + EXPECT_TRUE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_big_integer_exceeds_precision) { + const sourcemeta::core::Decimal value{"9007199254740993"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_small_value_with_exponent) { + const sourcemeta::core::Decimal value{"1.0e-28"}; + EXPECT_FALSE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_zero) { + const sourcemeta::core::Decimal value{"0.0"}; + EXPECT_TRUE(value.is_double()); +} + +TEST(Numeric_decimal, is_double_negative_zero) { + const sourcemeta::core::Decimal value{"-0.0"}; + EXPECT_TRUE(value.is_double()); +} + TEST(Numeric_decimal, to_float_simple) { const sourcemeta::core::Decimal value{"3.14"}; EXPECT_FLOAT_EQ(value.to_float(), 3.14f);