diff --git a/src/lang/numeric/decimal.cc b/src/lang/numeric/decimal.cc index 4ce4f6275..3b7d9c7b9 100644 --- a/src/lang/numeric/decimal.cc +++ b/src/lang/numeric/decimal.cc @@ -16,6 +16,17 @@ #include // mpd_* +namespace { +template + requires std::is_floating_point_v +auto floating_point_to_string(const FloatingPointType value) -> std::string { + std::ostringstream oss; + oss << std::setprecision(std::numeric_limits::max_digits10) + << value; + return oss.str(); +} +} // namespace + namespace sourcemeta::core { struct Decimal::Data { @@ -248,6 +259,12 @@ Decimal::Decimal(const std::uint64_t integral_value) { } } +Decimal::Decimal(const float floating_point_value) + : Decimal{floating_point_to_string(floating_point_value)} {} + +Decimal::Decimal(const double floating_point_value) + : Decimal{floating_point_to_string(floating_point_value)} {} + Decimal::Decimal(const char *const string_value) { new (this->storage) Data{}; this->data()->value.flags = MPD_STATIC | MPD_STATIC_DATA; diff --git a/src/lang/numeric/include/sourcemeta/core/numeric_decimal.h b/src/lang/numeric/include/sourcemeta/core/numeric_decimal.h index 53d38256d..3c492afb0 100644 --- a/src/lang/numeric/include/sourcemeta/core/numeric_decimal.h +++ b/src/lang/numeric/include/sourcemeta/core/numeric_decimal.h @@ -48,6 +48,12 @@ class SOURCEMETA_CORE_NUMERIC_EXPORT Decimal { /// Construct a decimal number from a 64-bit unsigned integer Decimal(std::uint64_t value); + /// Construct a decimal number from a 32-bit float + explicit Decimal(float value); + + /// Construct a decimal number from a 64-bit double + explicit Decimal(double value); + /// Construct a decimal number from a C-string explicit Decimal(const char *const value); diff --git a/test/numeric/numeric_decimal_test.cc b/test/numeric/numeric_decimal_test.cc index 7b448dbae..7b1d86e5c 100644 --- a/test/numeric/numeric_decimal_test.cc +++ b/test/numeric/numeric_decimal_test.cc @@ -759,6 +759,66 @@ TEST(Numeric_decimal, to_double_negative_infinity) { EXPECT_LT(result, 0.0); } +TEST(Numeric_decimal, to_float_not_exactly_representable_gets_rounded) { + const sourcemeta::core::Decimal value{"3.2"}; + EXPECT_FALSE(value.is_float()); + const float result{value.to_float()}; + EXPECT_FLOAT_EQ(result, 3.2f); +} + +TEST(Numeric_decimal, to_double_not_exactly_representable_gets_rounded) { + const sourcemeta::core::Decimal value{"3.2"}; + EXPECT_FALSE(value.is_double()); + const double result{value.to_double()}; + EXPECT_DOUBLE_EQ(result, 3.2); +} + +TEST(Numeric_decimal, to_float_exceeds_max_float_throws) { + const sourcemeta::core::Decimal value{"1e100"}; + EXPECT_FALSE(value.is_float()); + EXPECT_THROW( + { [[maybe_unused]] const float result = value.to_float(); }, + std::out_of_range); +} + +TEST(Numeric_decimal, to_double_exceeds_max_double_throws) { + const sourcemeta::core::Decimal value{"1e500"}; + EXPECT_FALSE(value.is_double()); + EXPECT_THROW( + { [[maybe_unused]] const double result = value.to_double(); }, + std::out_of_range); +} + +TEST(Numeric_decimal, to_float_below_min_float_throws) { + const sourcemeta::core::Decimal value{"1e-100"}; + EXPECT_FALSE(value.is_float()); + EXPECT_THROW( + { [[maybe_unused]] const float result = value.to_float(); }, + std::out_of_range); +} + +TEST(Numeric_decimal, to_double_below_min_double_throws) { + const sourcemeta::core::Decimal value{"1e-500"}; + EXPECT_FALSE(value.is_double()); + EXPECT_THROW( + { [[maybe_unused]] const double result = value.to_double(); }, + std::out_of_range); +} + +TEST(Numeric_decimal, to_float_high_precision_gets_rounded) { + const sourcemeta::core::Decimal value{"1.23456789"}; + EXPECT_FALSE(value.is_float()); + const float result{value.to_float()}; + EXPECT_FLOAT_EQ(result, 1.23456789f); +} + +TEST(Numeric_decimal, to_double_high_precision_gets_rounded) { + const sourcemeta::core::Decimal value{"1.234567890123456789"}; + EXPECT_FALSE(value.is_double()); + const double result{value.to_double()}; + EXPECT_DOUBLE_EQ(result, 1.234567890123456789); +} + TEST(Numeric_decimal, divisible_by_integer_true) { const sourcemeta::core::Decimal dividend{10}; const sourcemeta::core::Decimal divisor{5}; @@ -1222,6 +1282,7 @@ TEST(Numeric_decimal, is_uint64_false_too_large) { const sourcemeta::core::Decimal value{"18446744073709551616"}; EXPECT_FALSE(value.is_uint64()); } + TEST(Numeric_decimal, exception_conversion_syntax_invalid_string) { EXPECT_THROW( { const sourcemeta::core::Decimal value{"not_a_number"}; }, @@ -1455,3 +1516,105 @@ TEST(Numeric_decimal, negative_zero_preserves_sign) { EXPECT_TRUE(copy.is_signed()); EXPECT_TRUE(copy.is_zero()); } + +TEST(Numeric_decimal, construct_from_float_simple) { + const float value{3.5f}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_float()); + EXPECT_TRUE(decimal.is_double()); + EXPECT_EQ(decimal.to_float(), 3.5f); + EXPECT_EQ(decimal.to_double(), 3.5); + EXPECT_EQ(decimal.to_string(), "3.5"); +} + +TEST(Numeric_decimal, construct_from_float_negative) { + const float value{-7.25f}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_float()); + EXPECT_TRUE(decimal.is_double()); + EXPECT_EQ(decimal.to_float(), -7.25f); + EXPECT_EQ(decimal.to_double(), -7.25); + EXPECT_EQ(decimal.to_string(), "-7.25"); +} + +TEST(Numeric_decimal, construct_from_float_zero) { + const float value{0.0f}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_float()); + EXPECT_TRUE(decimal.is_double()); + EXPECT_TRUE(decimal.is_zero()); + EXPECT_EQ(decimal.to_float(), 0.0f); + EXPECT_EQ(decimal.to_double(), 0.0); +} + +TEST(Numeric_decimal, construct_from_float_very_small) { + const float value{0.125f}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_float()); + EXPECT_TRUE(decimal.is_double()); + EXPECT_EQ(decimal.to_float(), 0.125f); + EXPECT_EQ(decimal.to_double(), 0.125); + EXPECT_EQ(decimal.to_string(), "0.125"); +} + +TEST(Numeric_decimal, construct_from_double_simple) { + const double value{3.5}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_double()); + EXPECT_EQ(decimal.to_double(), 3.5); + EXPECT_EQ(decimal.to_string(), "3.5"); +} + +TEST(Numeric_decimal, construct_from_double_negative) { + const double value{-7.25}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_double()); + EXPECT_EQ(decimal.to_double(), -7.25); + EXPECT_EQ(decimal.to_string(), "-7.25"); +} + +TEST(Numeric_decimal, construct_from_double_zero) { + const double value{0.0}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_double()); + EXPECT_TRUE(decimal.is_zero()); + EXPECT_EQ(decimal.to_double(), 0.0); +} + +TEST(Numeric_decimal, construct_from_double_very_small) { + const double value{0.125}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_double()); + EXPECT_EQ(decimal.to_double(), 0.125); + EXPECT_EQ(decimal.to_string(), "0.125"); +} + +TEST(Numeric_decimal, construct_from_double_roundtrip) { + const double value{3.2}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_double()); + const double roundtrip{decimal.to_double()}; + EXPECT_EQ(roundtrip, value); +} + +TEST(Numeric_decimal, construct_from_float_roundtrip) { + const float value{3.2f}; + const sourcemeta::core::Decimal decimal{value}; + EXPECT_TRUE(decimal.is_float()); + const float roundtrip{decimal.to_float()}; + EXPECT_EQ(roundtrip, value); +} + +TEST(Numeric_decimal, construct_from_double_high_precision) { + const double value{1.234567890123456}; + const sourcemeta::core::Decimal decimal{value}; + const double roundtrip{decimal.to_double()}; + EXPECT_EQ(roundtrip, value); +} + +TEST(Numeric_decimal, construct_from_float_high_precision) { + const float value{1.234567f}; + const sourcemeta::core::Decimal decimal{value}; + const float roundtrip{decimal.to_float()}; + EXPECT_EQ(roundtrip, value); +}