Search code examples
boostmultiprecision

how to check/and convert if a boost::multiprecision::cpp_dec_floa can be "safely" converted to int,double,float?


Question:

I want to check if a boost::multiprecision::cpp_dec_float<100> value can be safely converted into a float/double or (u)int8,16,32,... without range problems

Background:

The cpp_dec_float is comming from an CSV import (i can't change that - its not my code) with constant values that "should" always fit (column dependent) into int8, int16, ...,double or float

some sort of:

bool can_be_converted_to_double(const cpp_dec_float<100>& ft)
double converted_to_double(const cpp_dec_float<100>& ft)

bool can_be_converted_to_int8(const cpp_dec_float<100>& ft)
int8_t converted_to_int8(const cpp_dec_float<100>& ft)

bool can_be_converted_to_uint8(const cpp_dec_float<100>& ft)
uint8_t converted_to_uint8(const cpp_dec_float<100>& ft)

its ok to loose precision when converting to float/double but the integral part value should fit

for integers it should not work if fractional part != 0 or the integral part does not fit

i tried to use .extract_signed_long_long and extract_part but that could fail if the values too large and there seem to be no convert_to helper available

how to test if the conversion can work and also do it?

Update

how to check if the fractional part is 0 - this way?

cpp_dec_float_100 value( "123.1" );
cpp_dec_float_100 int_part = value.backend().extract_integer_part();
cpp_dec_float_100 fractional_part = value - int_part;
bool has_fractional_zero = fractional_part.is_zero();

Solution

  • I suppose this would be a good start:

    template <typename To, typename From>
    bool can_be_converted_to(From const& value) {
        return value >= std::numeric_limits<To>::min() &&
            value <= std::numeric_limits<To>::max();
    }
    
    template <typename To, typename From> //
    To converted_to(From const& value) {
        if (not can_be_converted_to<To, From>(value))
            throw std::range_error(__PRETTY_FUNCTION__);
        return value.template convert_to<To>();
    }
    

    Here's some tests to verify your requirements:

    Live On Coliru

    #include <boost/multiprecision/cpp_dec_float.hpp>
    #include <boost/core/demangle.hpp> // only for test output
    #include <cassert>
    #include <iostream>
    using boost::multiprecision::cpp_dec_float_100;
    
    template <typename To, typename From>
    bool can_be_converted_to(From const& value) {
        return value >= std::numeric_limits<To>::min() &&
            value <= std::numeric_limits<To>::max();
    }
    
    template <typename To, typename From> //
    To converted_to(From const& value) {
        if (not can_be_converted_to<To, From>(value))
            throw std::range_error(__PRETTY_FUNCTION__);
        return value.template convert_to<To>();
    }
    
    template <typename To, typename From> void test(From const& value) {
        auto display = [](auto v) {
            if constexpr (sizeof(v)==1)
                // avoid int8_t stupidly printing as char
                return static_cast<int>(v);
            else
                return v;
        };
    
        try {
            std::cout << "To " << boost::core::demangle(typeid(To).name())
                      << " from " << value << ": ";
            std::cout << display(converted_to<To>(value)) << "\n";
        } catch(std::range_error const&) {
            std::cout << "RANGE ERROR\n";
        }
    }
    
    int main() {
        cpp_dec_float_100 cases[]{
            1, -1,
            127, -127,
            128, -128,
            129, -129,
            255, -255,
            1e5, -1e5,
            1e10, -1e10,
            1e100, -1e100,
            1e1000l, -1e1000l,
            cpp_dec_float_100("1e10000"), cpp_dec_float_100("-1e10000"),
            cpp_dec_float_100(std::numeric_limits<double>::max()),
            cpp_dec_float_100(std::numeric_limits<long double>::max()),
            cpp_dec_float_100(std::numeric_limits<double>::infinity()),
            cpp_dec_float_100(-std::numeric_limits<double>::infinity()),
        };
    
        for (auto num : cases) test<int8_t>(num);
        for (auto num : cases) test<uint8_t>(num);
        for (auto num : cases) test<int32_t>(num);
        for (auto num : cases) test<int64_t>(num);
        for (auto num : cases) test<float>(num);
        for (auto num : cases) test<double>(num);
        for (auto num : cases) test<long double>(num);
    
        // special cases
        for (auto num :
             {
                 cpp_dec_float_100{"inf"},
                 cpp_dec_float_100{"-inf"},
                 cpp_dec_float_100{"nan"},
             }) //
        {
            test<long double>(num);
        }
    }
    

    Prints

    To signed char from 1: 1
    To signed char from -1: -1
    To signed char from 127: 127
    To signed char from -127: -127
    To signed char from 128: RANGE ERROR
    To signed char from -128: -128
    To signed char from 129: RANGE ERROR
    To signed char from -129: RANGE ERROR
    To signed char from 255: RANGE ERROR
    To signed char from -255: RANGE ERROR
    To signed char from 100000: RANGE ERROR
    To signed char from -100000: RANGE ERROR
    To signed char from 1e+10: RANGE ERROR
    To signed char from -1e+10: RANGE ERROR
    To signed char from 1e+100: RANGE ERROR
    To signed char from -1e+100: RANGE ERROR
    To signed char from 1e+1000: RANGE ERROR
    To signed char from -1e+1000: RANGE ERROR
    To signed char from 1e+10000: RANGE ERROR
    To signed char from -1e+10000: RANGE ERROR
    To signed char from 1.79769e+308: RANGE ERROR
    To signed char from 1.18973e+4932: RANGE ERROR
    To signed char from inf: RANGE ERROR
    To signed char from -inf: RANGE ERROR
    To unsigned char from 1: 1
    To unsigned char from -1: RANGE ERROR
    To unsigned char from 127: 127
    To unsigned char from -127: RANGE ERROR
    To unsigned char from 128: 128
    To unsigned char from -128: RANGE ERROR
    To unsigned char from 129: 129
    To unsigned char from -129: RANGE ERROR
    To unsigned char from 255: 255
    To unsigned char from -255: RANGE ERROR
    To unsigned char from 100000: RANGE ERROR
    To unsigned char from -100000: RANGE ERROR
    To unsigned char from 1e+10: RANGE ERROR
    To unsigned char from -1e+10: RANGE ERROR
    To unsigned char from 1e+100: RANGE ERROR
    To unsigned char from -1e+100: RANGE ERROR
    To unsigned char from 1e+1000: RANGE ERROR
    To unsigned char from -1e+1000: RANGE ERROR
    To unsigned char from 1e+10000: RANGE ERROR
    To unsigned char from -1e+10000: RANGE ERROR
    To unsigned char from 1.79769e+308: RANGE ERROR
    To unsigned char from 1.18973e+4932: RANGE ERROR
    To unsigned char from inf: RANGE ERROR
    To unsigned char from -inf: RANGE ERROR
    To int from 1: 1
    To int from -1: -1
    To int from 127: 127
    To int from -127: -127
    To int from 128: 128
    To int from -128: -128
    To int from 129: 129
    To int from -129: -129
    To int from 255: 255
    To int from -255: -255
    To int from 100000: 100000
    To int from -100000: -100000
    To int from 1e+10: RANGE ERROR
    To int from -1e+10: RANGE ERROR
    To int from 1e+100: RANGE ERROR
    To int from -1e+100: RANGE ERROR
    To int from 1e+1000: RANGE ERROR
    To int from -1e+1000: RANGE ERROR
    To int from 1e+10000: RANGE ERROR
    To int from -1e+10000: RANGE ERROR
    To int from 1.79769e+308: RANGE ERROR
    To int from 1.18973e+4932: RANGE ERROR
    To int from inf: RANGE ERROR
    To int from -inf: RANGE ERROR
    To long from 1: 1
    To long from -1: -1
    To long from 127: 127
    To long from -127: -127
    To long from 128: 128
    To long from -128: -128
    To long from 129: 129
    To long from -129: -129
    To long from 255: 255
    To long from -255: -255
    To long from 100000: 100000
    To long from -100000: -100000
    To long from 1e+10: 10000000000
    To long from -1e+10: -10000000000
    To long from 1e+100: RANGE ERROR
    To long from -1e+100: RANGE ERROR
    To long from 1e+1000: RANGE ERROR
    To long from -1e+1000: RANGE ERROR
    To long from 1e+10000: RANGE ERROR
    To long from -1e+10000: RANGE ERROR
    To long from 1.79769e+308: RANGE ERROR
    To long from 1.18973e+4932: RANGE ERROR
    To long from inf: RANGE ERROR
    To long from -inf: RANGE ERROR
    To float from 1: 1
    To float from -1: RANGE ERROR
    To float from 127: 127
    To float from -127: RANGE ERROR
    To float from 128: 128
    To float from -128: RANGE ERROR
    To float from 129: 129
    To float from -129: RANGE ERROR
    To float from 255: 255
    To float from -255: RANGE ERROR
    To float from 100000: 100000
    To float from -100000: RANGE ERROR
    To float from 1e+10: 1e+10
    To float from -1e+10: RANGE ERROR
    To float from 1e+100: RANGE ERROR
    To float from -1e+100: RANGE ERROR
    To float from 1e+1000: RANGE ERROR
    To float from -1e+1000: RANGE ERROR
    To float from 1e+10000: RANGE ERROR
    To float from -1e+10000: RANGE ERROR
    To float from 1.79769e+308: RANGE ERROR
    To float from 1.18973e+4932: RANGE ERROR
    To float from inf: RANGE ERROR
    To float from -inf: RANGE ERROR
    To double from 1: 1
    To double from -1: RANGE ERROR
    To double from 127: 127
    To double from -127: RANGE ERROR
    To double from 128: 128
    To double from -128: RANGE ERROR
    To double from 129: 129
    To double from -129: RANGE ERROR
    To double from 255: 255
    To double from -255: RANGE ERROR
    To double from 100000: 100000
    To double from -100000: RANGE ERROR
    To double from 1e+10: 1e+10
    To double from -1e+10: RANGE ERROR
    To double from 1e+100: 1e+100
    To double from -1e+100: RANGE ERROR
    To double from 1e+1000: RANGE ERROR
    To double from -1e+1000: RANGE ERROR
    To double from 1e+10000: RANGE ERROR
    To double from -1e+10000: RANGE ERROR
    To double from 1.79769e+308: 1.79769e+308
    To double from 1.18973e+4932: RANGE ERROR
    To double from inf: RANGE ERROR
    To double from -inf: RANGE ERROR
    To long double from 1: 1
    To long double from -1: RANGE ERROR
    To long double from 127: 127
    To long double from -127: RANGE ERROR
    To long double from 128: 128
    To long double from -128: RANGE ERROR
    To long double from 129: 129
    To long double from -129: RANGE ERROR
    To long double from 255: 255
    To long double from -255: RANGE ERROR
    To long double from 100000: 100000
    To long double from -100000: RANGE ERROR
    To long double from 1e+10: 1e+10
    To long double from -1e+10: RANGE ERROR
    To long double from 1e+100: 1e+100
    To long double from -1e+100: RANGE ERROR
    To long double from 1e+1000: 1e+1000
    To long double from -1e+1000: RANGE ERROR
    To long double from 1e+10000: RANGE ERROR
    To long double from -1e+10000: RANGE ERROR
    To long double from 1.79769e+308: 1.79769e+308
    To long double from 1.18973e+4932: 1.18973e+4932
    To long double from inf: RANGE ERROR
    To long double from -inf: RANGE ERROR
    To long double from inf: RANGE ERROR
    To long double from -inf: RANGE ERROR
    To long double from nan: RANGE ERROR
    

    UPDATE

    I had kinda missed the requirement to check for fractional parts. In my mind, rounding to integer is "just" precision loss, so nothing to worry about.

    To also detect fractional parts, I'd suggest the simplest:

    template <typename To, typename From> //
    To converted_to(From const& value) {
        if (not can_be_converted_to<To, From>(value))
            throw std::range_error(__PRETTY_FUNCTION__);
        auto result = value.template convert_to<To>();
        if (result != value)
            throw std::range_error(__PRETTY_FUNCTION__);
        return result;
    }
    

    Using implementation details of the backend type might speed it up a little, at the expense of making it less generic/potentially more error-prone.

    Extendeding the tests using it (choosing a "noise" value relative to the source value and significant digits of the decimal float type):

    try {
        std::cout << "To " << boost::core::demangle(typeid(To).name())
                  << " from " << value << ": ";
        std::cout << display(converted_to<To>(value)) << "\n";
    
        try {
            From noise = value / 1e-50;
            /*auto should_fail =*/converted_to<To>(From{value + noise});
            std::cout << " -- WARNING: Fractional noise not detected" << std::endl;
        } catch (std::range_error const&) { }
    } catch (std::range_error const&) {
        std::cout << "RANGE ERROR\n";
    }
    

    Still prints the same output: Live On Coliru