From b6fc82cb717702634d945233eb0b1432076a4b95 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Fri, 7 Aug 2026 07:22:00 -0300 Subject: [PATCH] Add support for boost.decimal --- CMakeLists.txt | 5 + README.md | 4 +- cmake/fb-cppConfig.cmake.in | 6 + doc/Doxyfile | 1 + src/fb-cpp/CMakeLists.txt | 28 +++ src/fb-cpp/NumericConverter.h | 357 +++++++++++++++++++++++++++++---- src/fb-cpp/Row.h | 94 +++++++++ src/fb-cpp/Statement.h | 207 +++++++++++++++++++ src/fb-cpp/VariantTypeTraits.h | 20 ++ src/fb-cpp/config.h | 6 + src/fb-cpp/types.h | 21 ++ src/test/NumericConverter.cpp | 28 +++ src/test/Statement.cpp | 104 ++++++++++ vcpkg | 2 +- vcpkg-configuration.json | 2 +- vcpkg.json | 20 +- 16 files changed, 857 insertions(+), 48 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a90124..8b8eca1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,11 @@ if(FB_CPP_USE_BOOST_MULTIPRECISION) set(FB_CPP_USE_BOOST_MULTIPRECISION_VALUE 1) endif() +set(FB_CPP_USE_BOOST_DECIMAL_VALUE 0) +if(FB_CPP_USE_BOOST_DECIMAL) + set(FB_CPP_USE_BOOST_DECIMAL_VALUE 1) +endif() + include(CMakePackageConfigHelpers) write_basic_package_version_file( diff --git a/README.md b/README.md index 6e5b06e..884dcaa 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ It wraps the Firebird C++ API with RAII principles, smart pointers, and modern C - **RAII**: Automatic resource management with smart pointers - **Type Safety**: Strong typing for database operations - **Exception Safety**: Proper error handling with exceptions -- **Boost Integration**: Optional Boost.DLL for loading fbclient and Boost.Multiprecision support for large numbers +- **Boost Integration**: Optional Boost.DLL for loading fbclient, Boost.Multiprecision support for large numbers, and + Boost.Decimal support for decimal floating-point values ## Quick Start @@ -102,6 +103,7 @@ Or add it to your `vcpkg.json` manifest: The default features are: - `boost-dll`: Enable Boost.DLL support for runtime dynamic loading of Firebird client library - `boost-multiprecision`: Enable Boost.Multiprecision support for INT128 and DECFLOAT types +- `boost-decimal`: Enable Boost.Decimal support for DECFLOAT types ## Building diff --git a/cmake/fb-cppConfig.cmake.in b/cmake/fb-cppConfig.cmake.in index eac79a8..193c3db 100644 --- a/cmake/fb-cppConfig.cmake.in +++ b/cmake/fb-cppConfig.cmake.in @@ -6,6 +6,7 @@ find_dependency(firebird CONFIG) set(_fb_cpp_use_boost_dll "@FB_CPP_USE_BOOST_DLL_VALUE@") set(_fb_cpp_use_boost_multiprecision "@FB_CPP_USE_BOOST_MULTIPRECISION_VALUE@") +set(_fb_cpp_use_boost_decimal "@FB_CPP_USE_BOOST_DECIMAL_VALUE@") if(_fb_cpp_use_boost_dll) find_dependency(Boost COMPONENTS dll) @@ -15,9 +16,14 @@ if(_fb_cpp_use_boost_multiprecision) find_dependency(Boost COMPONENTS multiprecision) endif() +if(_fb_cpp_use_boost_decimal) + find_dependency(Boost COMPONENTS decimal) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/fb-cppTargets.cmake") unset(_fb_cpp_use_boost_dll) unset(_fb_cpp_use_boost_multiprecision) +unset(_fb_cpp_use_boost_decimal) check_required_components(fb-cpp) diff --git a/doc/Doxyfile b/doc/Doxyfile index 46d9ed0..c3b6d2d 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -195,6 +195,7 @@ SEARCH_INCLUDES = YES INCLUDE_PATH = INCLUDE_FILE_PATTERNS = PREDEFINED = FB_CPP_USE_BOOST_MULTIPRECISION=1 \ + FB_CPP_USE_BOOST_DECIMAL=1 \ FB_CPP_USE_BOOST_DLL=1 EXPAND_AS_DEFINED = SKIP_FUNCTION_MACROS = YES diff --git a/src/fb-cpp/CMakeLists.txt b/src/fb-cpp/CMakeLists.txt index 65c4904..811da0b 100644 --- a/src/fb-cpp/CMakeLists.txt +++ b/src/fb-cpp/CMakeLists.txt @@ -22,6 +22,16 @@ option(FB_CPP_USE_BOOST_MULTIPRECISION "Enable Boost.Multiprecision helpers for ${_fb_cpp_use_boost_multiprecision_default}) unset(_fb_cpp_use_boost_multiprecision_default) +set(_fb_cpp_use_boost_decimal_default ON) +if(DEFINED VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET) + if(NOT EXISTS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/boost-decimal") + set(_fb_cpp_use_boost_decimal_default OFF) + endif() +endif() +option(FB_CPP_USE_BOOST_DECIMAL "Enable Boost.Decimal helpers for DECFLOAT types" + ${_fb_cpp_use_boost_decimal_default}) +unset(_fb_cpp_use_boost_decimal_default) + file(GLOB_RECURSE SRC "*.h" "*.cpp" @@ -34,6 +44,9 @@ endif() if(FB_CPP_USE_BOOST_MULTIPRECISION) list(APPEND _fb_cpp_boost_components multiprecision) endif() +if(FB_CPP_USE_BOOST_DECIMAL) + list(APPEND _fb_cpp_boost_components decimal) +endif() if(_fb_cpp_boost_components) find_package(Boost REQUIRED @@ -77,10 +90,19 @@ if(NOT DEFINED FB_CPP_USE_BOOST_MULTIPRECISION_VALUE) endif() endif() +if(NOT DEFINED FB_CPP_USE_BOOST_DECIMAL_VALUE) + if(FB_CPP_USE_BOOST_DECIMAL) + set(FB_CPP_USE_BOOST_DECIMAL_VALUE 1) + else() + set(FB_CPP_USE_BOOST_DECIMAL_VALUE 0) + endif() +endif() + target_compile_definitions(${PROJECT_NAME} PUBLIC FB_CPP_USE_BOOST_DLL=${FB_CPP_USE_BOOST_DLL_VALUE} FB_CPP_USE_BOOST_MULTIPRECISION=${FB_CPP_USE_BOOST_MULTIPRECISION_VALUE} + FB_CPP_USE_BOOST_DECIMAL=${FB_CPP_USE_BOOST_DECIMAL_VALUE} ) target_link_libraries(${PROJECT_NAME} @@ -100,6 +122,12 @@ if(FB_CPP_USE_BOOST_MULTIPRECISION) ) endif() +if(FB_CPP_USE_BOOST_DECIMAL) + target_link_libraries(${PROJECT_NAME} + PUBLIC Boost::decimal + ) +endif() + unset(_fb_cpp_boost_components) file(GLOB_RECURSE HEADER_FILES diff --git a/src/fb-cpp/NumericConverter.h b/src/fb-cpp/NumericConverter.h index dfb12dc..5a74f45 100644 --- a/src/fb-cpp/NumericConverter.h +++ b/src/fb-cpp/NumericConverter.h @@ -55,13 +55,33 @@ namespace fbcpp::impl template <> struct NumberTypePriority { - static constexpr int value = 8; + static constexpr int value = 7; }; template <> struct NumberTypePriority { - static constexpr int value = 7; + static constexpr int value = 6; + }; +#endif + +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template <> + struct NumberTypePriority + { + static constexpr int value = 10; + }; + + template <> + struct NumberTypePriority + { + static constexpr int value = 9; + }; + + template <> + struct NumberTypePriority + { + static constexpr int value = 8; }; #endif @@ -114,6 +134,9 @@ namespace fbcpp::impl inline constexpr bool IsFloatingNumber = std::is_floating_point_v #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 || std::same_as || std::same_as +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + || std::same_as || std::same_as || std::same_as #endif ; @@ -208,21 +231,12 @@ namespace fbcpp::impl { using ComputeType = GreaterNumberType; - if constexpr (std::is_floating_point_v) - { - if (std::isnan(from) || std::isinf(from)) - throwNumericOutOfRange(); - } -#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 - else if constexpr (std::same_as || std::same_as) - { - if ((boost::multiprecision::isnan) (from) || (boost::multiprecision::isinf) (from)) - throwNumericOutOfRange(); - } -#endif + if (isNonFinite(from)) + throwNumericOutOfRange(); - ComputeType value{from}; + ComputeType value = convertFloatingValue(from); const ComputeType eps = conversionEpsilon(); + const ComputeType half{0.5}; if (toScale > 0) value /= powerOfTen(toScale); @@ -230,28 +244,28 @@ namespace fbcpp::impl value *= powerOfTen(-toScale); if (value > 0) - value += 0.5f + eps; + value += half + eps; else - value -= 0.5f + eps; + value -= half + eps; - static const auto minLimit = static_cast(std::numeric_limits::min()); - static const auto maxLimit = static_cast(std::numeric_limits::max()); + static const auto minLimit = convertFloatingValue(std::numeric_limits::min()); + static const auto maxLimit = convertFloatingValue(std::numeric_limits::max()); if (value < minLimit) { - if (value > minLimit - 1.0f) + if (value > minLimit - ComputeType{1}) return std::numeric_limits::min(); throwNumericOutOfRange(); } if (value > maxLimit) { - if (value < maxLimit + 1.0f) + if (value < maxLimit + ComputeType{1}) return std::numeric_limits::max(); throwNumericOutOfRange(); } - return static_cast(value); + return convertIntegralValue(value); } template @@ -261,7 +275,7 @@ namespace fbcpp::impl using ComputeType = GreaterNumberType; - ComputeType value = static_cast(from.value); // FIXME: decfloat + ComputeType value = convertFloatingValue(from.value); if (from.scale != 0) { @@ -287,10 +301,7 @@ namespace fbcpp::impl return boostDecFloat34ToBoostDecFloat16(from); #endif - if constexpr (std::is_floating_point_v && !std::is_floating_point_v) - return To{std::format("{:.16e}", from)}; - - return static_cast(from); + return convertFloatingValue(from); } template @@ -374,6 +385,17 @@ namespace fbcpp::impl return from > 0 ? "Infinity" : "-Infinity"; return from.str(); } +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr (std::same_as || std::same_as || + std::same_as) + { + if ((boost::decimal::isnan) (from)) + return "NaN"; + if ((boost::decimal::isinf) (from)) + return from > 0 ? "Infinity" : "-Infinity"; + return boost::decimal::to_string(from); + } #endif else return from.str(); @@ -506,6 +528,74 @@ namespace fbcpp::impl } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + BoostDecimal32 opaqueDecFloat16ToBoostDecimal32( + StatusWrapper* statusWrapper, const OpaqueDecFloat16& opaqueDecFloat16) + { + return stringToBoostDecimal(opaqueDecFloat16ToString(statusWrapper, opaqueDecFloat16)); + } + + BoostDecimal64 opaqueDecFloat16ToBoostDecimal64( + StatusWrapper* statusWrapper, const OpaqueDecFloat16& opaqueDecFloat16) + { + return stringToBoostDecimal(opaqueDecFloat16ToString(statusWrapper, opaqueDecFloat16)); + } + + BoostDecimal128 opaqueDecFloat16ToBoostDecimal128( + StatusWrapper* statusWrapper, const OpaqueDecFloat16& opaqueDecFloat16) + { + return stringToBoostDecimal(opaqueDecFloat16ToString(statusWrapper, opaqueDecFloat16)); + } + + BoostDecimal32 opaqueDecFloat34ToBoostDecimal32( + StatusWrapper* statusWrapper, const OpaqueDecFloat34& opaqueDecFloat34) + { + return stringToBoostDecimal(opaqueDecFloat34ToString(statusWrapper, opaqueDecFloat34)); + } + + BoostDecimal64 opaqueDecFloat34ToBoostDecimal64( + StatusWrapper* statusWrapper, const OpaqueDecFloat34& opaqueDecFloat34) + { + return stringToBoostDecimal(opaqueDecFloat34ToString(statusWrapper, opaqueDecFloat34)); + } + + BoostDecimal128 opaqueDecFloat34ToBoostDecimal128( + StatusWrapper* statusWrapper, const OpaqueDecFloat34& opaqueDecFloat34) + { + return stringToBoostDecimal(opaqueDecFloat34ToString(statusWrapper, opaqueDecFloat34)); + } + + OpaqueDecFloat16 boostDecimal32ToOpaqueDecFloat16(StatusWrapper* statusWrapper, const BoostDecimal32& value) + { + return boostDecimalToOpaqueDecFloat16(statusWrapper, value); + } + + OpaqueDecFloat16 boostDecimal64ToOpaqueDecFloat16(StatusWrapper* statusWrapper, const BoostDecimal64& value) + { + return boostDecimalToOpaqueDecFloat16(statusWrapper, value); + } + + OpaqueDecFloat16 boostDecimal128ToOpaqueDecFloat16(StatusWrapper* statusWrapper, const BoostDecimal128& value) + { + return boostDecimalToOpaqueDecFloat16(statusWrapper, value); + } + + OpaqueDecFloat34 boostDecimal32ToOpaqueDecFloat34(StatusWrapper* statusWrapper, const BoostDecimal32& value) + { + return boostDecimalToOpaqueDecFloat34(statusWrapper, value); + } + + OpaqueDecFloat34 boostDecimal64ToOpaqueDecFloat34(StatusWrapper* statusWrapper, const BoostDecimal64& value) + { + return boostDecimalToOpaqueDecFloat34(statusWrapper, value); + } + + OpaqueDecFloat34 boostDecimal128ToOpaqueDecFloat34(StatusWrapper* statusWrapper, const BoostDecimal128& value) + { + return boostDecimalToOpaqueDecFloat34(statusWrapper, value); + } +#endif + // FIXME: move std::byte stringToBoolean(std::string_view value) { @@ -530,6 +620,200 @@ namespace fbcpp::impl } private: + template + static bool isNonFinite(const T& value) + { + using ValueType = std::remove_cvref_t; + + if constexpr (std::is_floating_point_v) + return std::isnan(value) || std::isinf(value); +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + else if constexpr (std::same_as || std::same_as) + return (boost::multiprecision::isnan) (value) || (boost::multiprecision::isinf) (value); +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr (std::same_as || std::same_as || + std::same_as) + return (boost::decimal::isnan) (value) || (boost::decimal::isinf) (value); +#endif + else + return false; + } + + template + static bool isSignalingNaNValue(const T& value) + { +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 || FB_CPP_USE_BOOST_DECIMAL != 0 + using ValueType = std::remove_cvref_t; +#endif + +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + if constexpr (std::same_as || std::same_as) + return (boost::multiprecision::isnan) (value) && isSignalingNaN(value.str()); +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + if constexpr (std::same_as || std::same_as || + std::same_as) + return (boost::decimal::issignaling) (value); +#endif + return false; + } + + template + To convertFloatingValue(const From& from) + { + using ToType = std::remove_cvref_t; + using FromType = std::remove_cvref_t; + + if constexpr (std::same_as) + return from; +#if FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr (std::same_as || std::same_as || + std::same_as) + { + if (isSignalingNaNValue(from)) + return std::numeric_limits::signaling_NaN(); + if constexpr (std::is_floating_point_v) + return ToType{std::format("{:.16e}", from)}; +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + else if constexpr (std::same_as || std::same_as) + { + if ((boost::multiprecision::isnan) (from)) + return std::numeric_limits::quiet_NaN(); + if ((boost::multiprecision::isinf) (from)) + { + return from > 0 ? std::numeric_limits::infinity() + : -std::numeric_limits::infinity(); + } + return ToType{from.str()}; + } + else if constexpr (std::same_as) + return ToType{from.str()}; +#endif + else + return static_cast(from); + } +#endif +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 && FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr ((std::same_as || std::same_as) && + (std::same_as || std::same_as || + std::same_as) ) + { + if (isSignalingNaNValue(from)) + throw FbCppException("Boost.Multiprecision cannot represent a signaling NaN"); + if ((boost::decimal::isnan) (from)) + return ToType{"NaN"}; + if ((boost::decimal::isinf) (from)) + { + return from > 0 ? std::numeric_limits::infinity() + : -std::numeric_limits::infinity(); + } + return ToType{boost::decimal::to_string(from)}; + } +#endif + else + return static_cast(from); + } + + template + To convertIntegralValue(const From& from) + { + using ToType = std::remove_cvref_t; + +#if FB_CPP_USE_BOOST_DECIMAL != 0 + using FromType = std::remove_cvref_t; + + if constexpr (std::same_as || std::same_as || + std::same_as) + { +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + if constexpr (std::same_as) + { + char buffer[64]; + const auto result = boost::decimal::to_chars( + buffer, buffer + sizeof(buffer), from, boost::decimal::chars_format::fixed); + + if (result.ec != std::errc{}) + throwNumericOutOfRange(); + + std::string value{buffer, result.ptr}; + if (const auto decimalPoint = value.find('.'); decimalPoint != std::string::npos) + value.erase(decimalPoint); + + return ToType{value}; + } +#endif + + if constexpr (std::same_as) + return static_cast(static_cast(from)); + else if constexpr (std::is_signed_v) + return static_cast(static_cast(from)); + else + return static_cast(static_cast(from)); + } + else +#endif + return static_cast(from); + } + + static bool isSignalingNaN(std::string_view value) + { + std::string normalized{value}; + if (!normalized.empty() && (normalized.front() == '+' || normalized.front() == '-')) + normalized.erase(0, 1); + + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + + return normalized.starts_with("snan"); + } + +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template + T stringToBoostDecimal(const std::string& value) + { + if (isSignalingNaN(value)) + return std::numeric_limits::signaling_NaN(); + else if (value == "Infinity") + return std::numeric_limits::infinity(); + else if (value == "-Infinity") + return -std::numeric_limits::infinity(); + + try + { + return T{value}; + } + catch (const std::exception&) + { + throwConversionErrorFromString(value); + } + } + + template + OpaqueDecFloat16 boostDecimalToOpaqueDecFloat16(StatusWrapper* statusWrapper, const T& value) + { + if (isSignalingNaNValue(value)) + throw FbCppException("Boost.Decimal cannot represent a signaling NaN; use OpaqueDecFloat16"); + + OpaqueDecFloat16 result; + const auto stringValue = numberToString(value); + client->getDecFloat16Util(statusWrapper)->fromString(statusWrapper, stringValue.c_str(), &result); + return result; + } + + template + OpaqueDecFloat34 boostDecimalToOpaqueDecFloat34(StatusWrapper* statusWrapper, const T& value) + { + if (isSignalingNaNValue(value)) + throw FbCppException("Boost.Decimal cannot represent a signaling NaN; use OpaqueDecFloat34"); + + OpaqueDecFloat34 result; + const auto stringValue = numberToString(value); + client->getDecFloat34Util(statusWrapper)->fromString(statusWrapper, stringValue.c_str(), &result); + return result; + } +#endif + double powerOfTenDouble(int scale) noexcept { static constexpr double UPPER_PART[] = { @@ -616,17 +900,6 @@ namespace fbcpp::impl throwNumericOutOfRange(); } - static bool isSignalingNaN(std::string_view value) - { - std::string normalized{value}; - if (!normalized.empty() && (normalized.front() == '+' || normalized.front() == '-')) - normalized.erase(0, 1); - - std::transform(normalized.begin(), normalized.end(), normalized.begin(), - [](unsigned char ch) { return static_cast(std::tolower(ch)); }); - - return normalized.starts_with("snan"); - } #endif template @@ -681,6 +954,14 @@ namespace fbcpp::impl const auto epsilon = std::numeric_limits::epsilon(); return static_cast(epsilon * static_cast(10)); } +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr (std::same_as || std::same_as || + std::same_as) + { + const auto epsilon = std::numeric_limits::epsilon(); + return epsilon * static_cast(10); + } #endif else return std::numeric_limits::epsilon(); diff --git a/src/fb-cpp/Row.h b/src/fb-cpp/Row.h index 003ef53..9da9902 100644 --- a/src/fb-cpp/Row.h +++ b/src/fb-cpp/Row.h @@ -49,6 +49,10 @@ #include #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 +#include +#endif + /// /// fb-cpp namespace. @@ -266,6 +270,26 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Reads a Boost.Decimal 7-digit decimal floating-point column. + /// + std::optional getBoostDecimal32(unsigned index) + { + std::optional scale{0}; + return getNumber(index, scale, "BoostDecimal32"); + } + + /// + /// @brief Reads a Boost.Decimal 16-digit decimal floating-point column. + /// + std::optional getBoostDecimal64(unsigned index) + { + std::optional scale{0}; + return getNumber(index, scale, "BoostDecimal64"); + } +#endif + /// /// @brief Reads a Firebird 34-digit decimal floating-point column. /// @@ -297,6 +321,17 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Reads a Boost.Decimal 34-digit decimal floating-point column. + /// + std::optional getBoostDecimal128(unsigned index) + { + std::optional scale{0}; + return getNumber(index, scale, "BoostDecimal128"); + } +#endif + /// /// @brief Reads a date column. /// @@ -815,6 +850,10 @@ namespace fbcpp #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 else if constexpr (variantContainsV) return V{get>(index).value()}; +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr (variantContainsV) + return V{get>(index).value()}; #endif break; @@ -824,6 +863,10 @@ namespace fbcpp #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 else if constexpr (variantContainsV) return V{get>(index).value()}; +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + else if constexpr (variantContainsV) + return V{get>(index).value()}; #endif break; @@ -917,6 +960,11 @@ namespace fbcpp auto data = &message[descriptor.offset]; #if FB_CPP_USE_BOOST_MULTIPRECISION != 0 std::optional boostInt128; +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + std::optional boostDecimal64; + std::optional boostDecimal128; +#elif FB_CPP_USE_BOOST_MULTIPRECISION != 0 std::optional boostDecFloat16; std::optional boostDecFloat34; #endif @@ -930,7 +978,21 @@ namespace fbcpp &statusWrapper, *reinterpret_cast(data))); data = reinterpret_cast(&boostInt128.value()); break; +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + case DescriptorAdjustedType::DECFLOAT16: + boostDecimal64.emplace(numericConverter.opaqueDecFloat16ToBoostDecimal64( + &statusWrapper, *reinterpret_cast(data))); + data = reinterpret_cast(&boostDecimal64.value()); + break; + + case DescriptorAdjustedType::DECFLOAT34: + boostDecimal128.emplace(numericConverter.opaqueDecFloat34ToBoostDecimal128( + &statusWrapper, *reinterpret_cast(data))); + data = reinterpret_cast(&boostDecimal128.value()); + break; +#elif FB_CPP_USE_BOOST_MULTIPRECISION != 0 case DescriptorAdjustedType::DECFLOAT16: boostDecFloat16.emplace(numericConverter.opaqueDecFloat16ToBoostDecFloat16( &statusWrapper, *reinterpret_cast(data))); @@ -997,7 +1059,17 @@ namespace fbcpp return numericConverter.numberToNumber( ScaledBoostInt128{*reinterpret_cast(data), descriptor.scale}, toScale.value()); +#endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + case DescriptorAdjustedType::DECFLOAT16: + return numericConverter.numberToNumber( + *reinterpret_cast(data), toScale.value()); + + case DescriptorAdjustedType::DECFLOAT34: + return numericConverter.numberToNumber( + *reinterpret_cast(data), toScale.value()); +#elif FB_CPP_USE_BOOST_MULTIPRECISION != 0 case DescriptorAdjustedType::DECFLOAT16: return numericConverter.numberToNumber( *reinterpret_cast(data), toScale.value()); @@ -1126,6 +1198,20 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template <> + inline std::optional Row::get>(unsigned index) + { + return getBoostDecimal32(index); + } + + template <> + inline std::optional Row::get>(unsigned index) + { + return getBoostDecimal64(index); + } +#endif + template <> inline std::optional Row::get>(unsigned index) { @@ -1140,6 +1226,14 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template <> + inline std::optional Row::get>(unsigned index) + { + return getBoostDecimal128(index); + } +#endif + template <> inline std::optional Row::get>(unsigned index) { diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index 504773e..aa5616c 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -61,6 +61,10 @@ #include #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 +#include +#endif + /// /// fb-cpp namespace. /// @@ -678,6 +682,36 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Binds a 7-digit decimal floating-point value using Boost.Decimal or null. + /// + void setBoostDecimal32(unsigned index, std::optional optValue) + { + if (!optValue.has_value()) + { + setNull(index); + return; + } + + setNumber(index, DescriptorAdjustedType::DECFLOAT16, optValue.value(), 0, "BoostDecimal32"); + } + + /// + /// @brief Binds a 16-digit decimal floating-point value using Boost.Decimal or null. + /// + void setBoostDecimal64(unsigned index, std::optional optValue) + { + if (!optValue.has_value()) + { + setNull(index); + return; + } + + setNumber(index, DescriptorAdjustedType::DECFLOAT16, optValue.value(), 0, "BoostDecimal64"); + } +#endif + /// /// @brief Binds a 34-digit decimal floating-point value in Firebird's representation or null. /// @@ -724,6 +758,22 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Binds a 34-digit decimal floating-point value using Boost.Decimal or null. + /// + void setBoostDecimal128(unsigned index, std::optional optValue) + { + if (!optValue.has_value()) + { + setNull(index); + return; + } + + setNumber(index, DescriptorAdjustedType::DECFLOAT34, optValue.value(), 0, "BoostDecimal128"); + } +#endif + /// /// @brief Binds a date value or null. /// @@ -1363,6 +1413,24 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Convenience overload that binds a Boost.Decimal 7-digit decimal floating-point value. + /// + void set(unsigned index, BoostDecimal32 value) + { + setBoostDecimal32(index, value); + } + + /// + /// @brief Convenience overload that binds a Boost.Decimal 16-digit decimal floating-point value. + /// + void set(unsigned index, BoostDecimal64 value) + { + setBoostDecimal64(index, value); + } +#endif + /// /// @brief Convenience overload that binds a Firebird 34-digit decimal floating-point value. /// @@ -1381,6 +1449,16 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Convenience overload that binds a Boost.Decimal 34-digit decimal floating-point value. + /// + void set(unsigned index, BoostDecimal128 value) + { + setBoostDecimal128(index, value); + } +#endif + /// /// @brief Convenience overload that binds a Firebird date value. /// @@ -1628,6 +1706,26 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Reads a Boost.Decimal 7-digit decimal floating-point column. + /// + std::optional getBoostDecimal32(unsigned index) + { + assert(isValid()); + return outRow->getBoostDecimal32(index); + } + + /// + /// @brief Reads a Boost.Decimal 16-digit decimal floating-point column. + /// + std::optional getBoostDecimal64(unsigned index) + { + assert(isValid()); + return outRow->getBoostDecimal64(index); + } +#endif + /// /// @brief Reads a Firebird 34-digit decimal floating-point column. /// @@ -1648,6 +1746,17 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// @brief Reads a Boost.Decimal 34-digit decimal floating-point column. + /// + std::optional getBoostDecimal128(unsigned index) + { + assert(isValid()); + return outRow->getBoostDecimal128(index); + } +#endif + /// /// @brief Reads a date column. /// @@ -1917,6 +2026,72 @@ namespace fbcpp (set(static_cast(Is), std::get(value)), ...); } +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template + void setDecimalNumber(unsigned index, T value, std::optional& descriptorScale, const char* typeName) + { + const auto& descriptor = getInDescriptor(index); + auto* const message = inMessage.data(); + const auto descriptorData = &message[descriptor.offset]; + + switch (descriptor.adjustedType) + { + case DescriptorAdjustedType::INT16: + *reinterpret_cast(descriptorData) = + numericConverter.numberToNumber(value, descriptorScale.value()); + break; + + case DescriptorAdjustedType::INT32: + *reinterpret_cast(descriptorData) = + numericConverter.numberToNumber(value, descriptorScale.value()); + break; + + case DescriptorAdjustedType::INT64: + *reinterpret_cast(descriptorData) = + numericConverter.numberToNumber(value, descriptorScale.value()); + break; + +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + case DescriptorAdjustedType::INT128: + { + const auto int128Value = + numericConverter.numberToNumber(value, descriptorScale.value()); + *reinterpret_cast(descriptorData) = + numericConverter.boostInt128ToOpaqueInt128(&statusWrapper, int128Value); + break; + } +#endif + + case DescriptorAdjustedType::FLOAT: + *reinterpret_cast(descriptorData) = numericConverter.numberToNumber(value); + break; + + case DescriptorAdjustedType::DOUBLE: + *reinterpret_cast(descriptorData) = numericConverter.numberToNumber(value); + break; + + case DescriptorAdjustedType::DECFLOAT16: + { + const auto decimalValue = numericConverter.numberToNumber(value); + *reinterpret_cast(descriptorData) = + numericConverter.boostDecimal64ToOpaqueDecFloat16(&statusWrapper, decimalValue); + break; + } + + case DescriptorAdjustedType::DECFLOAT34: + { + const auto decimalValue = numericConverter.numberToNumber(value); + *reinterpret_cast(descriptorData) = + numericConverter.boostDecimal128ToOpaqueDecFloat34(&statusWrapper, decimalValue); + break; + } + + default: + throwInvalidType(typeName, descriptor.adjustedType); + } + } +#endif + /// /// @brief Converts and writes numeric parameter values following descriptor rules. /// @@ -1931,6 +2106,16 @@ namespace fbcpp const auto descriptorData = &message[descriptor.offset]; std::optional descriptorScale{descriptor.scale}; +#if FB_CPP_USE_BOOST_DECIMAL != 0 + if constexpr (std::is_same_v || std::is_same_v || + std::is_same_v) + { + setDecimalNumber(index, value, descriptorScale, typeName); + *reinterpret_cast(&message[descriptor.nullOffset]) = FB_FALSE; + return; + } +#endif + Descriptor valueDescriptor; valueDescriptor.adjustedType = valueType; valueDescriptor.scale = scale; @@ -2192,6 +2377,20 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template <> + inline std::optional Statement::get>(unsigned index) + { + return getBoostDecimal32(index); + } + + template <> + inline std::optional Statement::get>(unsigned index) + { + return getBoostDecimal64(index); + } +#endif + template <> inline std::optional Statement::get>(unsigned index) { @@ -2206,6 +2405,14 @@ namespace fbcpp } #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + template <> + inline std::optional Statement::get>(unsigned index) + { + return getBoostDecimal128(index); + } +#endif + template <> inline std::optional Statement::get>(unsigned index) { diff --git a/src/fb-cpp/VariantTypeTraits.h b/src/fb-cpp/VariantTypeTraits.h index abd77d1..899ad4e 100644 --- a/src/fb-cpp/VariantTypeTraits.h +++ b/src/fb-cpp/VariantTypeTraits.h @@ -34,6 +34,10 @@ #include #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 +#include +#endif + namespace fbcpp::impl::reflection { @@ -161,6 +165,22 @@ namespace fbcpp::impl::reflection }; #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + // Boost.Decimal types + template <> + struct IsSupportedVariantType : std::true_type + { + }; + template <> + struct IsSupportedVariantType : std::true_type + { + }; + template <> + struct IsSupportedVariantType : std::true_type + { + }; +#endif + // Opaque multiprecision types template <> struct IsSupportedVariantType : std::true_type diff --git a/src/fb-cpp/config.h b/src/fb-cpp/config.h index b8ad06b..b5dcded 100644 --- a/src/fb-cpp/config.h +++ b/src/fb-cpp/config.h @@ -37,4 +37,10 @@ #endif #endif +#if !defined(FB_CPP_USE_BOOST_DECIMAL) +#if __has_include() +#define FB_CPP_USE_BOOST_DECIMAL 1 +#endif +#endif + #endif // FBCPP_CONFIG_H diff --git a/src/fb-cpp/types.h b/src/fb-cpp/types.h index e175cdd..5fd0a82 100644 --- a/src/fb-cpp/types.h +++ b/src/fb-cpp/types.h @@ -39,6 +39,10 @@ #include #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 +#include +#endif + /// /// fb-cpp namespace. @@ -112,6 +116,23 @@ namespace fbcpp using BoostDecFloat34 = boost::multiprecision::number>; #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + /// + /// 7-digit decimal floating point using Boost.Decimal. + /// + using BoostDecimal32 = boost::decimal::decimal32_t; + + /// + /// 16-digit decimal floating point using Boost.Decimal. + /// + using BoostDecimal64 = boost::decimal::decimal64_t; + + /// + /// 34-digit decimal floating point using Boost.Decimal. + /// + using BoostDecimal128 = boost::decimal::decimal128_t; +#endif + /// /// Firebird SQL calendar date. /// diff --git a/src/test/NumericConverter.cpp b/src/test/NumericConverter.cpp index 187c175..e2752f6 100644 --- a/src/test/NumericConverter.cpp +++ b/src/test/NumericConverter.cpp @@ -884,4 +884,32 @@ BOOST_AUTO_TEST_CASE(decFloat34NumberLimits) #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + +BOOST_AUTO_TEST_CASE(convertBoostDecimal) +{ + impl::NumericConverter converter{getClient()}; + + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecimal32{"12.3"}, -2), 12'30); + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecimal64{"3276.7"}, 0), 3'277); + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecimal128{"3.2767"}, 0), 3); + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecimal64{"-3276.8"}, -1), -3'276'8); + +#if FB_CPP_USE_BOOST_MULTIPRECISION != 0 + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecimal128{"123456789012345678901234567890.5727"}, 0), + BoostInt128{"123456789012345678901234567891"}); +#endif + + BOOST_CHECK_CLOSE(converter.numberToNumber(BoostDecimal64{"12.3"}), 12.3, doubleTolerance); + BOOST_CHECK_EQUAL(converter.numberToNumber(BoostDecimal64{"12.3"}), BoostDecimal128{"12.3"}); + BOOST_CHECK_EQUAL(converter.numberToString(BoostDecimal64{"12.3"}), "12.3"); + + BOOST_CHECK_THROW( + converter.numberToNumber(std::numeric_limits::quiet_NaN(), 0), DatabaseException); + BOOST_CHECK_THROW( + converter.numberToNumber(std::numeric_limits::infinity(), 0), DatabaseException); +} + +#endif + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/Statement.cpp b/src/test/Statement.cpp index cf8ac4b..5481309 100644 --- a/src/test/Statement.cpp +++ b/src/test/Statement.cpp @@ -2135,6 +2135,78 @@ BOOST_AUTO_TEST_SUITE_END() #endif // FB_CPP_USE_BOOST_MULTIPRECISION +#if FB_CPP_USE_BOOST_DECIMAL != 0 + +BOOST_AUTO_TEST_SUITE(StatementBoostDecimalSuite) + +BOOST_AUTO_TEST_CASE(boostDecimalRoundTrips) +{ + const auto database = getTempFile("Statement-boostDecimalRoundTrips.fdb"); + + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + const BoostDecimal32 decimal32Value{"1234567"}; + Statement decimal32Statement{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + decimal32Statement.setBoostDecimal32(0, decimal32Value); + BOOST_REQUIRE(decimal32Statement.execute(transaction)); + BOOST_CHECK_EQUAL(decimal32Statement.getBoostDecimal32(0).value(), decimal32Value); + BOOST_CHECK_EQUAL(decimal32Statement.getBoostDecimal64(0).value(), BoostDecimal64{"1234567"}); + + const BoostDecimal64 decimal64Value{"1234567890123456"}; + Statement decimal64Statement{attachment, transaction, "select cast(? as decfloat(34)) from rdb$database"}; + decimal64Statement.setBoostDecimal64(0, decimal64Value); + BOOST_REQUIRE(decimal64Statement.execute(transaction)); + BOOST_CHECK_EQUAL(decimal64Statement.getBoostDecimal64(0).value(), decimal64Value); + BOOST_CHECK_EQUAL(decimal64Statement.getBoostDecimal128(0).value(), BoostDecimal128{"1234567890123456"}); + + const BoostDecimal128 decimal128Value{"1234567890123456789012345678901234"}; + Statement decimal128Statement{attachment, transaction, "select cast(? as decfloat(34)) from rdb$database"}; + decimal128Statement.setBoostDecimal128(0, decimal128Value); + BOOST_REQUIRE(decimal128Statement.execute(transaction)); + BOOST_CHECK_EQUAL(decimal128Statement.getBoostDecimal128(0).value(), decimal128Value); +} + +BOOST_AUTO_TEST_CASE(boostDecimalNullAndSpecialValues) +{ + const auto database = getTempFile("Statement-boostDecimalNullAndSpecialValues.fdb"); + + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + Statement nullValue{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + nullValue.setBoostDecimal64(0, std::nullopt); + BOOST_REQUIRE(nullValue.execute(transaction)); + BOOST_CHECK(!nullValue.getBoostDecimal64(0).has_value()); + + Statement positiveInfinity{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + positiveInfinity.setBoostDecimal64(0, std::numeric_limits::infinity()); + BOOST_REQUIRE(positiveInfinity.execute(transaction)); + const auto positiveInfinityValue = positiveInfinity.getBoostDecimal64(0); + BOOST_REQUIRE(positiveInfinityValue.has_value()); + BOOST_CHECK((boost::decimal::isinf) (positiveInfinityValue.value())); + BOOST_CHECK(positiveInfinityValue.value() > 0); + + Statement quietNaN{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + quietNaN.setBoostDecimal64(0, std::numeric_limits::quiet_NaN()); + BOOST_REQUIRE(quietNaN.execute(transaction)); + const auto quietNaNValue = quietNaN.getBoostDecimal64(0); + BOOST_REQUIRE(quietNaNValue.has_value()); + BOOST_CHECK((boost::decimal::isnan) (quietNaNValue.value())); + + Statement signalingNaN{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + BOOST_CHECK_THROW( + signalingNaN.setBoostDecimal64(0, std::numeric_limits::signaling_NaN()), FbCppException); +} + +BOOST_AUTO_TEST_SUITE_END() + +#endif // FB_CPP_USE_BOOST_DECIMAL + BOOST_AUTO_TEST_SUITE(StatementOpaqueDateSuite) @@ -3543,6 +3615,38 @@ BOOST_AUTO_TEST_CASE(getVariantOpaqueDecFloat34Preferred) #endif +#if FB_CPP_USE_BOOST_DECIMAL != 0 + +BOOST_AUTO_TEST_CASE(boostDecimalVariant) +{ + using MyVariant = std::variant; + + const auto database = getTempFile("Statement-boostDecimalVariant.fdb"); + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + Statement decFloat16{attachment, transaction, "select cast(123.456 as decfloat(16)) from rdb$database"}; + BOOST_REQUIRE(decFloat16.execute(transaction)); + const auto decFloat16Value = decFloat16.get(0); + BOOST_REQUIRE(std::holds_alternative(decFloat16Value)); + BOOST_CHECK_EQUAL(std::get(decFloat16Value), BoostDecimal64{"123.456"}); + + Statement decFloat34{attachment, transaction, "select cast(123.456 as decfloat(34)) from rdb$database"}; + BOOST_REQUIRE(decFloat34.execute(transaction)); + const auto decFloat34Value = decFloat34.get(0); + BOOST_REQUIRE(std::holds_alternative(decFloat34Value)); + BOOST_CHECK_EQUAL(std::get(decFloat34Value), BoostDecimal128{"123.456"}); + + Statement setValue{attachment, transaction, "select cast(? as decfloat(16)) from rdb$database"}; + setValue.set(0, MyVariant{BoostDecimal64{"987.654"}}); + BOOST_REQUIRE(setValue.execute(transaction)); + BOOST_CHECK_EQUAL(setValue.getBoostDecimal64(0).value(), BoostDecimal64{"987.654"}); +} + +#endif // FB_CPP_USE_BOOST_DECIMAL + BOOST_AUTO_TEST_CASE(rawNumericVariantsWorkWithoutBoostHelpers) { using NumericVariant = std::variant; diff --git a/vcpkg b/vcpkg index 522253c..8128322 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 522253caf47268c1724f486a035e927a42a90092 +Subproject commit 8128322a87623426aea726c44ea45ee7e77daaa8 diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 80c3b05..ace0bd4 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json", "default-registry": { "kind": "builtin", - "baseline": "522253caf47268c1724f486a035e927a42a90092" + "baseline": "8128322a87623426aea726c44ea45ee7e77daaa8" }, "registries": [ { diff --git a/vcpkg.json b/vcpkg.json index 761a59b..b998c50 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,6 +13,7 @@ "name": "boost-dll", "platform": "!static" }, + "boost-decimal", "boost-multiprecision", "boost-test", "firebird", @@ -21,23 +22,28 @@ "overrides": [ { "name": "boost-dll", - "version": "1.90.0", - "port-version": 1 + "version": "1.91.0", + "port-version": 0 + }, + { + "name": "boost-decimal", + "version": "1.91.0", + "port-version": 0 }, { "name": "boost-multiprecision", - "version": "1.90.0", - "port-version": 1 + "version": "1.91.0", + "port-version": 0 }, { "name": "boost-test", - "version": "1.90.0", - "port-version": 1 + "version": "1.91.0", + "port-version": 0 }, { "name": "firebird", "version": "5.0.4", - "port-version": 4 + "port-version": 6 }, { "name": "icu",