| #include "yaml-cpp/fptostring.h" |
| #include "contrib/dragonbox.h" |
| |
| #include <array> |
| #include <cassert> |
| #include <cmath> |
| #include <limits> |
| #include <sstream> |
| #include <tuple> |
| |
| namespace YAML { |
| namespace detail { |
| namespace fp_formatting { |
| |
| /** |
| * Converts a integer into its ASCII digits. |
| * |
| * @param begin/end - a buffer, must be at least 20bytes long. |
| * @param value - input value. |
| * @param width - minimum number of digits, fill with '0' to the left. Must be equal or smaller than the buffer size. |
| * @return - number of digits filled into the buffer (or -1 if preconditions are not meet) |
| * |
| * Example: |
| * std::array<char, 20> buffer; |
| * auto ct = ConvertToChars(buffer.begin(), buffer.end(), 23, 3); |
| * assert(ct = 3); |
| * assert(buffer[0] == '0'); |
| * assert(buffer[1] == '2'); |
| * assert(buffer[2] == '3'); |
| */ |
| int ConvertToChars(char* begin, char* end, size_t value, int width=1) { |
| // precondition of this function (will trigger in debug build) |
| assert(width >= 1); |
| assert(end >= begin); // end must be after begin |
| assert(end-begin >= width); // Buffer must be large enough |
| assert(end-begin >= 20); // 2^64 has 20digits, so at least 20 digits must be available |
| |
| // defensive programming, abort if precondition are not met (will trigger in release build) |
| if (width < 1) { |
| return -1; |
| } |
| if (end < begin) { |
| return -1; |
| } |
| if (end-begin < width) { |
| return -1; |
| } |
| if (end-begin < 20) { |
| return -1; |
| } |
| |
| |
| // count number of digits, and fill digits array accordingly |
| int digits_ct{}; |
| while (value > 0) { |
| char c = value % 10 + '0'; |
| value = value / 10; |
| digits_ct += 1; |
| *(end-digits_ct) = c; |
| } |
| while(digits_ct < width) { |
| assert(digits_ct < 64); |
| digits_ct += 1; |
| *(end-digits_ct) = '0'; |
| } |
| // move data to the front of the array |
| std::memmove(begin, end-digits_ct, digits_ct); |
| return digits_ct; |
| } |
| |
| /** |
| * Converts a float or double to a string. |
| * |
| * converts a value 'v' to a string. Uses dragonbox for formatting. |
| */ |
| template <typename T> |
| std::string FpToString(T v, int precision = 0) { |
| // hard coded constant, at which exponent should switch to a scientific notation |
| int const lowerExponentThreshold = -5; |
| int const upperExponentThreshold = (precision==0)?6:precision; |
| if (precision == 0) { |
| precision = 6; |
| } |
| |
| // dragonbox/to_decimal does not handle value 0, inf, NaN |
| if (v == 0 || std::isinf(v) || std::isnan(v)) { |
| std::stringstream ss; |
| ss.imbue(std::locale::classic()); |
| ss << v; |
| return ss.str(); |
| } |
| |
| auto r = jkj::dragonbox::to_decimal(v); |
| |
| auto digits = std::array<char, 20>{}; // max digits of size_t is 20. |
| auto digits_ct = ConvertToChars(digits.data(), digits.data() + digits.size(), r.significand); |
| |
| // defensive programming, ConvertToChars arguments are invalid |
| if (digits_ct == -1) { |
| std::stringstream ss; |
| ss.imbue(std::locale::classic()); |
| ss << v; |
| return ss.str(); |
| } |
| |
| // check if requested precision is lower than |
| // required digits for exact representation |
| if (digits_ct > precision) { |
| auto diff = digits_ct - precision; |
| r.exponent += diff; |
| digits_ct = precision; |
| |
| // round numbers if required |
| if (digits[digits_ct] >= '5') { |
| int i{digits_ct-1}; |
| digits[i] += 1; |
| while (digits[i] == '9'+1) { |
| digits_ct -= 1; |
| r.exponent += 1; |
| if (i > 0) { |
| digits[i-1] += 1; |
| i -= 1; |
| } else { |
| digits_ct = 1; |
| digits[0] = '1'; |
| break; |
| } |
| } |
| } |
| } |
| |
| // Case 1 - scientific notation: max digits of size_t plus sign, a dot and 2 letters for 'e+' or 'e-' and 4 letters for the exponent |
| // Case 2 - default notation: require up to precision number of digits and one for a potential sign |
| std::array<char, 28> output_buffer; |
| auto output_ptr = &output_buffer[0]; |
| |
| // Helper variable that in Case 2 counts the overflowing number of zeros that do not fit into the buffer. |
| int overflow_zeros = 0; |
| |
| // print '-' symbol for negative numbers |
| if (r.is_negative) { |
| *(output_ptr++) = '-'; |
| } |
| |
| // exponent if only a single non-zero digit is before the decimal point |
| int const exponent = r.exponent + digits_ct - 1; |
| |
| // case 1: scientific notation |
| if (exponent >= upperExponentThreshold || exponent <= lowerExponentThreshold) { |
| // print first digit |
| *(output_ptr++) = digits[0]; |
| |
| // print digits after decimal point |
| if (digits_ct > 1) { |
| *(output_ptr++) = '.'; |
| // print significant numbers after decimal point |
| for (int i{1}; i < digits_ct; ++i) { |
| *(output_ptr++) = digits[i]; |
| } |
| } |
| *(output_ptr++) = 'e'; |
| *(output_ptr++) = (exponent>=0)?'+':'-'; |
| auto exp_digits = std::array<char, 20>{}; |
| auto exp_digits_ct = ConvertToChars(exp_digits.data(), exp_digits.data() + exp_digits.size(), std::abs(exponent), /*.precision=*/ 2); |
| |
| // defensive programming, ConvertToChars arguments are invalid |
| if (exp_digits_ct == -1) { |
| std::stringstream ss; |
| ss.imbue(std::locale::classic()); |
| ss << v; |
| return ss.str(); |
| } |
| |
| for (int i{0}; i < exp_digits_ct; ++i) { |
| *(output_ptr++) = exp_digits[i]; |
| } |
| |
| // case 2: default notation |
| } else { |
| auto const digits_end = digits.begin() + digits_ct; |
| auto digits_iter = digits.begin(); |
| |
| // print digits before point |
| int const before_decimal_digits = digits_ct + r.exponent; |
| if (before_decimal_digits > 0) { |
| // print non-zero digits before point |
| for (int i{0}; i < std::min(before_decimal_digits, digits_ct); ++i) { |
| *(output_ptr++) = *(digits_iter++); |
| } |
| |
| // number of digits that have to be zero |
| int const zero_digits_ct = before_decimal_digits - digits_ct; |
| |
| // space left in the output_buffer (-1 because we need it for null-termination) |
| int const buffer_empty_space = output_buffer.data() + output_buffer.size() - output_ptr - 1; |
| |
| // print all zeros not fitting into the buffer at the end of the function |
| overflow_zeros = std::max(0, zero_digits_ct - buffer_empty_space); |
| |
| // print trailing zeros before point |
| for (int i{0}; i < zero_digits_ct - overflow_zeros; ++i) { |
| *(output_ptr++) = '0'; |
| } |
| |
| // print 0 before point if none where printed before |
| } else { |
| *(output_ptr++) = '0'; |
| } |
| |
| if (digits_iter != digits_end) { |
| *(output_ptr++) = '.'; |
| // print 0 after decimal point, to fill until first digits |
| int const after_decimal_zeros = -digits_ct - r.exponent; |
| for (int i{0}; i < after_decimal_zeros; ++i) { |
| *(output_ptr++) = '0'; |
| } |
| |
| // print significant numbers after decimal point |
| for (;digits_iter < digits_end; ++digits_iter) { |
| *(output_ptr++) = *digits_iter; |
| } |
| } |
| } |
| *output_ptr = '\0'; |
| auto ret_value = std::string{&output_buffer[0], output_ptr}; |
| ret_value.resize(ret_value.size() + overflow_zeros, '0'); |
| return ret_value; |
| } |
| |
| } |
| } |
| |
| std::string FpToString(float v, size_t precision) { |
| return detail::fp_formatting::FpToString(v, precision); |
| } |
| |
| std::string FpToString(double v, size_t precision) { |
| return detail::fp_formatting::FpToString(v, precision); |
| } |
| |
| /** |
| * dragonbox only works for floats/doubles not long double |
| */ |
| std::string FpToString(long double v, size_t precision) { |
| std::stringstream ss; |
| ss.imbue(std::locale::classic()); |
| if (precision == 0) { |
| precision = std::numeric_limits<long double>::max_digits10; |
| } |
| ss.precision(precision); |
| ss << v; |
| return ss.str(); |
| } |
| |
| } |