diff --git a/README.md b/README.md index 9cfc737..31d1cda 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Double to string conversion is done using the **Grisu2** algorithm, described by **Florian Loitsch** in its publication [Printing Floating-Point Numbers Quickly and Accurately with Integers](https://florian.loitsch.com/publications). -String to double conversion uses the **Eisel-Lemire** algorithm, without fallback for mantissas of up to 19 digits; longer mantissas fall back to strtod. +String to double conversion uses the **Eisel-Lemire** algorithm on the first 19 significant digits. Longer mantissas are bracketed between two Eisel-Lemire results; only when they disagree is the number rebuilt from the digits already read and passed to strtod. A stream is read once and never rewound; a buffer is only read again for mantissas longer than 19 digits. Any help to improve speed, accuracy or code quality is welcome. @@ -81,8 +81,25 @@ double value; const char* end = dconv::atod ("-2.22507e-308", value); ``` +`atod` also accepts a `std::string` or a `std::string_view`, and a `(first, length)` or `(first, last)` range. +A temporary `std::string` is rejected, since the returned position would point into it. + +It can also read from a stream, without buffering it: + +```cpp +#include +#include + +double value; +std::istringstream in ("-2.22507e-308"); +bool ok = dconv::atod (in, value); +``` + `dtoa` needs a buffer of at least 25 bytes and returns the end of the written characters. `atod` returns the end of the parsed characters, or `nullptr` on failure. +The probe and stream overloads return `false` on failure and stop right after the number on success. +They read the stream buffer directly: whitespaces are not skipped, the stream state is left unchanged and the characters read are consumed even on failure. +`atod` never changes `errno`. ## License diff --git a/dconv/include/dconv/atod.hpp b/dconv/include/dconv/atod.hpp index 78522c7..205164f 100644 --- a/dconv/include/dconv/atod.hpp +++ b/dconv/include/dconv/atod.hpp @@ -30,48 +30,22 @@ #include // C++. +#include +#include +#include #include #include -#include // C. -#include #include #include #include +#include namespace dconv { namespace details { - struct LocaleDelete - { - constexpr LocaleDelete () noexcept = default; - - void operator() (locale_t loc) noexcept - { - freelocale (loc); - } - }; - - using LocalePtr = std::unique_ptr, LocaleDelete>; - - /** - * @brief convert double using strtod. - * @param first first character of the number. - * @param last position past the last character of the number. - * @param value converted value. - * @return true on success, false otherwise. - */ - inline bool strtodSlow (const char* first, const char* last, double& value) - { - static LocalePtr locale (newlocale (LC_ALL_MASK, "C", nullptr)); - std::string num (first, last); - char* end = nullptr; - value = strtod_l (num.c_str (), &end, locale.get ()); - return (end && (*end == '\0')); - } - /** * @brief multiply two 64 bits unsigned integers. * @param a multiplicand. @@ -205,24 +179,176 @@ namespace dconv return (c == '+') || (c == '-'); } - inline const char* atod (StringProbe& probe, double& value) + /** + * @brief read an optional exponent. + * @param probe probe to read from. + * @param exponent read exponent. + * @return true on success, false otherwise. + */ + template + inline bool readExponent (Probe& probe, int64_t& exponent) + { + exponent = 0; + if (!probe.getIf ('e') && !probe.getIf ('E')) + { + return true; + } + + bool negExp = false; + if (isSign (probe.peek ())) + { + negExp = (probe.get () == '-'); + } + + if (DCONV_UNLIKELY (!isDigit (probe.peek ()))) + { + return false; + } + + exponent = probe.get () - '0'; + + while (isDigit (probe.peek ())) + { + int digit = probe.get () - '0'; + + if (DCONV_LIKELY (exponent <= ((std::numeric_limits::max () - digit) / 10))) + { + exponent = (exponent * 10) + digit; + } + } + + if (negExp) + { + exponent = -exponent; + } + + return true; + } + + /** + * @brief continue a conversion whose significand has more than 19 digits. + * @param probe probe to read from, positioned on the first digit beyond the significand. + * @param negative sign of the number. + * @param significand first 19 significant digits. + * @param scale power of ten applied to the significand so far. + * @param fraction true if the reading stopped in the fractional part. + * @param value converted value. + * @return true on success, false otherwise. + */ + template + __attribute__ ((cold, noinline)) bool atodLong (Probe& probe, bool negative, uint64_t significand, + int64_t scale, bool fraction, double& value) + { + static constexpr int maxDigits = 800; + char text[maxDigits + 32]; + char* const tail = std::to_chars (text, text + 20, significand).ptr; + char* cur = tail; + bool truncated = false; + + if (!fraction) + { + while (isDigit (probe.peek ())) + { + char digit = static_cast (probe.get ()); + if (cur < text + maxDigits) + { + *cur++ = digit; + } + else + { + truncated |= (digit != '0'); + } + ++scale; + } + + if (probe.getIf ('.')) + { + if (DCONV_UNLIKELY (!isDigit (probe.peek ()))) + { + return false; + } + } + } + + while (isDigit (probe.peek ())) + { + char digit = static_cast (probe.get ()); + if (cur < text + maxDigits) + { + *cur++ = digit; + } + else + { + truncated |= (digit != '0'); + } + } + + int64_t exponent = 0; + if (!readExponent (probe, exponent)) + { + return false; + } + + value = strtodFast (significand, exponent + scale); + if (value != strtodFast (significand + 1, exponent + scale)) + { + if (truncated) + { + *cur++ = '1'; + } + const int64_t power = exponent + scale - (cur - tail); + *cur++ = 'e'; + *std::to_chars (cur, cur + 21, power).ptr = '\0'; + int error = errno; + value = std::strtod (text, nullptr); + errno = error; + } + + value = negative ? -value : value; + return true; + } + + /** + * @brief string to double conversion. + * @param probe probe to read from. + * @param value converted value. + * @return true on success, false otherwise. + */ + template > + inline bool atod (Probe& probe, double& value) { - const char* beg = probe.tell (); + const char* beg = nullptr; + if constexpr (Rewind) + { + beg = probe.tell (); + } bool negative = probe.getIf ('-'); - uint64_t digits = 0; uint64_t u = 0; + size_t digits = 0; + int64_t scale = 0; if (probe.getIf ('0')) { if (DCONV_UNLIKELY (isDigit (probe.peek ()))) { - return nullptr; + return false; } } else if (DCONV_LIKELY (isDigit (probe.peek ()))) { - digits = probe.readDigits (u); + if constexpr (Rewind) + { + digits = probe.readDigits (u); + } + else + { + digits = probe.readDigits (u, 19); + if (DCONV_UNLIKELY (isDigit (probe.peek ()))) + { + return atodLong (probe, negative, u, scale, false, value); + } + } } else if (probe.getIfNoCase ('i') && probe.getIfNoCase ('n') && probe.getIfNoCase ('f')) { @@ -231,92 +357,105 @@ namespace dconv if (!(probe.getIfNoCase ('n') && probe.getIfNoCase ('i') && probe.getIfNoCase ('t') && probe.getIfNoCase ('y'))) { - return nullptr; + return false; } } value = negative ? -std::numeric_limits::infinity () : std::numeric_limits::infinity (); - return probe.tell (); + return true; } else if (probe.getIfNoCase ('n') && probe.getIfNoCase ('a') && probe.getIfNoCase ('n')) { value = negative ? -std::numeric_limits::quiet_NaN () : std::numeric_limits::quiet_NaN (); - return probe.tell (); + return true; } else { - return nullptr; + return false; } - int64_t frac = 0; if (probe.getIf ('.')) { if (DCONV_UNLIKELY (!isDigit (probe.peek ()))) { - return nullptr; + return false; } if (digits == 0) { while (probe.getIf ('0')) { - --frac; + --scale; } } - size_t count = probe.readDigits (u); + size_t count = probe.readDigits (u, Rewind ? std::numeric_limits::max () : 19 - digits); digits += count; - frac -= static_cast (count); - } - - int64_t exponent = 0; - if (probe.getIf ('e') || probe.getIf ('E')) - { - bool negExp = false; - if (isSign (probe.peek ())) + scale -= static_cast (count); + if constexpr (!Rewind) { - negExp = (probe.get () == '-'); - } - - if (DCONV_UNLIKELY (!isDigit (probe.peek ()))) - { - return nullptr; - } - - exponent = probe.get () - '0'; - - while (isDigit (probe.peek ())) - { - int digit = probe.get () - '0'; - - if (DCONV_LIKELY (exponent <= ((std::numeric_limits::max () - digit) / 10))) + if (DCONV_UNLIKELY (isDigit (probe.peek ()))) { - exponent = (exponent * 10) + digit; + return atodLong (probe, negative, u, scale, true, value); } } - - if (negExp) - { - exponent = -exponent; - } } - if (DCONV_LIKELY (digits <= 19)) + if constexpr (Rewind) { - value = strtodFast (u, exponent + frac); - value = negative ? -value : value; - return probe.tell (); + if (DCONV_UNLIKELY (digits > 19)) + { + probe.seek (beg); + return atod (probe, value); + } } - if (strtodSlow (beg, probe.tell (), value)) + int64_t exponent = 0; + if (!readExponent (probe, exponent)) { - return probe.tell (); + return false; } - return nullptr; // LCOV_EXCL_LINE + value = strtodFast (u, exponent + scale); + value = negative ? -value : value; + return true; } } + /** + * @brief stream to double conversion. + * @param probe probe to read from. + * @param value converted value. + * @return true on success, false otherwise. + */ + inline bool atod (StreamProbe& probe, double& value) + { + return details::atod (probe, value); + } + + /** + * @brief stream to double conversion. + * @param in stream to read from. + * @param value converted value. + * @return true on success, false otherwise. + */ + inline bool atod (std::istream& in, double& value) + { + StreamProbe probe (in); + return details::atod (probe, value); + } + + /** + * @brief string to double conversion. + * @param probe probe to read from. + * @param value converted value. + * @return true on success, false otherwise. + */ + inline bool atod (StringProbe& probe, double& value) + { + return details::atod (probe, value); + } + /** * @brief string to double conversion. * @param str string to parse. @@ -326,7 +465,7 @@ namespace dconv inline const char* atod (const char* str, double& value) { StringProbe probe (str); - return details::atod (probe, value); + return details::atod (probe, value) ? probe.tell () : nullptr; } /** @@ -339,7 +478,7 @@ namespace dconv inline const char* atod (const char* str, size_t length, double& value) { StringProbe probe (str, length); - return details::atod (probe, value); + return details::atod (probe, value) ? probe.tell () : nullptr; } /** @@ -352,8 +491,28 @@ namespace dconv inline const char* atod (const char* first, const char* last, double& value) { StringProbe probe (first, last); - return details::atod (probe, value); + return details::atod (probe, value) ? probe.tell () : nullptr; + } + + /** + * @brief string to double conversion. + * @param str string to parse. + * @param value converted value. + * @return end position on success, nullptr on failure. + */ + inline const char* atod (std::string_view str, double& value) + { + StringProbe probe (str.data (), str.size ()); + return details::atod (probe, value) ? probe.tell () : nullptr; } + + /** + * @brief string to double conversion from a temporary string. + * @param str temporary string. + * @param value converted value. + * @return end position on success, nullptr on failure. + */ + const char* atod (std::string&& str, double& value) = delete; } #endif diff --git a/dconv/tests/atod_test.cpp b/dconv/tests/atod_test.cpp index 7976d77..2e093f1 100644 --- a/dconv/tests/atod_test.cpp +++ b/dconv/tests/atod_test.cpp @@ -29,9 +29,13 @@ #include // C++. +#include +#include +#include #include // C. +#include #include /** @@ -377,6 +381,14 @@ TEST (atod, atod) EXPECT_EQ (dconv::atod (str, str + 5, value), str + 5); EXPECT_EQ (value, 1500.0); + std::string_view view (str, 3); + EXPECT_EQ (dconv::atod (view, value), str + 3); + EXPECT_EQ (value, 1.5); + + std::string string (str); + EXPECT_EQ (dconv::atod (string, value), string.data () + 5); + EXPECT_EQ (value, 1500.0); + str = "1234567890123456789012345678901234567890"; EXPECT_EQ (dconv::atod (str, 25, value), str + 25); EXPECT_EQ (value, 1234567890123456789012345.0); @@ -394,14 +406,69 @@ TEST (atod, atod) ASSERT_NE (dconv::atod ("1.7976931348623158079372897140530341506e308", value), nullptr); EXPECT_EQ (value, std::numeric_limits::max ()); + errno = 0; ASSERT_NE (dconv::atod ("-2.47032822920623272088284396434110686e-324", value), nullptr); EXPECT_EQ (value, 0.0); EXPECT_TRUE (std::signbit (value)); + EXPECT_EQ (errno, 0); + + dconv::StringProbe stringProbe ("1.5x"); + ASSERT_TRUE (dconv::atod (stringProbe, value)); + EXPECT_EQ (value, 1.5); + EXPECT_EQ (stringProbe.peek (), 'x'); + + std::istringstream stream ("-1.25e3x"); + dconv::StreamProbe probe (stream); + ASSERT_TRUE (dconv::atod (probe, value)); + EXPECT_EQ (value, -1250.0); + EXPECT_EQ (probe.peek (), 'x'); + + std::istringstream invalid ("-x"); + dconv::StreamProbe invalidProbe (invalid); + ASSERT_FALSE (dconv::atod (invalidProbe, value)); + + std::istringstream longInvalid ("12345678901234567890.x"); + dconv::StreamProbe longInvalidProbe (longInvalid); + ASSERT_FALSE (dconv::atod (longInvalidProbe, value)); + + std::istringstream longExponent ("12345678901234567890e+x"); + ASSERT_FALSE (dconv::atod (longExponent, value)); + + std::istringstream longFraction ("-9007199254740993.0000000000000000001x"); + ASSERT_TRUE (dconv::atod (longFraction, value)); + EXPECT_EQ (value, -9007199254740994.0); + EXPECT_EQ (longFraction.peek (), 'x'); + + std::istringstream longTruncated ("9007199254740993" + std::string (900, '0') + "1e-901x"); + ASSERT_TRUE (dconv::atod (longTruncated, value)); + EXPECT_EQ (value, 9007199254740994.0); + EXPECT_EQ (longTruncated.peek (), 'x'); + + std::istringstream in ("2.5e-1 "); + ASSERT_TRUE (dconv::atod (in, value)); + EXPECT_EQ (value, 0.25); + EXPECT_EQ (in.peek (), ' '); EXPECT_EQ (dconv::atod ("12345678901234567890.", value), nullptr); EXPECT_EQ (dconv::atod ("12345678901234567890e", value), nullptr); EXPECT_EQ (dconv::atod ("0.12345678901234567890e+", value), nullptr); + std::string longest = "9007199254740993." + std::string (900, '0'); + ASSERT_NE (dconv::atod (longest.c_str (), value), nullptr); + EXPECT_EQ (value, 9007199254740992.0); + + longest += "1"; + ASSERT_NE (dconv::atod (longest.c_str (), value), nullptr); + EXPECT_EQ (value, 9007199254740994.0); + + longest = "9007199254740993" + std::string (900, '0') + "e-900"; + ASSERT_NE (dconv::atod (longest.c_str (), value), nullptr); + EXPECT_EQ (value, 9007199254740992.0); + + longest = "9007199254740993" + std::string (900, '0') + "1e-901"; + ASSERT_NE (dconv::atod (longest.c_str (), value), nullptr); + EXPECT_EQ (value, 9007199254740994.0); + str = "infx"; EXPECT_EQ (dconv::atod (str, value), str + 3); EXPECT_EQ (value, std::numeric_limits::infinity ());