diff --git a/README.md b/README.md index 31d1cda..2d87426 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ **dconv** is a header-only C++20 library for printing and parsing floating point numbers. -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). +Double to string conversion uses the **Grisu2** algorithm by default, described by **Florian Loitsch** in its publication [Printing Floating-Point Numbers Quickly and Accurately with Integers](https://florian.loitsch.com/publications). + +The **Grisu** and **Grisu3** algorithms from the same publication are also available. Grisu prints up to 18 digits without looking for the shortest output, Grisu2 prints the shortest output for about 99.9% of the doubles, and Grisu3 detects the remaining cases and falls back to `std::to_chars`. 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. @@ -72,6 +74,21 @@ char value[25]; char* end = dconv::dtoa (value, -2.22507e-308); ``` +Each algorithm has its own header and namespace: + +```cpp +#include + +char value[25]; +char* end = dconv::grisu3::dtoa (value, -2.22507e-308); +``` + +| Header | Function | +| :--- | :--- | +| `dconv/grisu3.hpp` | `dconv::grisu3::dtoa` | +| `dconv/grisu2.hpp` | `dconv::grisu2::dtoa` | +| `dconv/grisu.hpp` | `dconv::grisu::dtoa` | + The parsing API can be used this way: ```cpp @@ -95,7 +112,28 @@ 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. +A variant reads from a probe and stops on integers, so that integers can be kept as integers: + +```cpp +#include + +double value; +uint64_t significand; +int64_t exponent; +bool negative; +std::error_code ec; + +dconv::StringProbe probe ("18446744073709551615"); +if (dconv::atod (probe, value, significand, exponent, negative, ec) == -1 && ec == dconv::Errc::NotDouble) +{ + // significand holds the integer, negative its sign. +} +``` + +It returns `0` and the converted value for a real number, or `-1` with `Errc::NotDouble` for an integer without fraction nor exponent whose magnitude fits in 64 bits, or `-1` with `Errc::InvalidNumber` when parsing fails. +`significand`, `exponent` and `negative` hold what was read until the end or the error. + +`dtoa` needs a buffer of at least 25 bytes (26 bytes for `grisu::dtoa`) 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. diff --git a/dconv/CMakeLists.txt b/dconv/CMakeLists.txt index 36a7514..702fc57 100644 --- a/dconv/CMakeLists.txt +++ b/dconv/CMakeLists.txt @@ -1,12 +1,19 @@ cmake_minimum_required(VERSION 3.22.1) set(PUBLIC_HEADERS + include/dconv/error.hpp include/dconv/probe.hpp - include/dconv/diyfp.hpp + include/dconv/eisellemirepow.hpp + include/dconv/eisellemire.hpp include/dconv/atod.hpp - include/dconv/atodpow.hpp + include/dconv/digits.hpp + include/dconv/format.hpp + include/dconv/diyfp.hpp + include/dconv/grisupow.hpp + include/dconv/grisu.hpp + include/dconv/grisu2.hpp + include/dconv/grisu3.hpp include/dconv/dtoa.hpp - include/dconv/dtoapow.hpp ) add_library(${PROJECT_NAME} INTERFACE) diff --git a/dconv/include/dconv/atod.hpp b/dconv/include/dconv/atod.hpp index 205164f..9f51ac2 100644 --- a/dconv/include/dconv/atod.hpp +++ b/dconv/include/dconv/atod.hpp @@ -26,7 +26,8 @@ #define __DCONV_ATOD_HPP__ // dconv. -#include +#include +#include #include // C++. @@ -46,129 +47,6 @@ namespace dconv { namespace details { - /** - * @brief multiply two 64 bits unsigned integers. - * @param a multiplicand. - * @param b multiplier. - * @param high high 64 bits of the 128-bit result. - * @param low low 64 bits of the 128-bit result. - */ - inline void umul128 (uint64_t a, uint64_t b, uint64_t& high, uint64_t& low) noexcept - { -#if defined(__SIZEOF_INT128__) - __uint128_t p = static_cast<__uint128_t> (a) * b; - - high = static_cast (p >> 64); - low = static_cast (p); -#else - uint64_t b_lo = static_cast (b); - uint64_t b_hi = b >> 32; - uint64_t p0 = (a & 0xFFFFFFFF) * b_lo; - uint64_t p1 = (a >> 32) * b_lo; - uint64_t p2 = (a & 0xFFFFFFFF) * b_hi; - uint64_t p3 = (a >> 32) * b_hi; - uint64_t carry = (p0 >> 32) + (p1 & 0xFFFFFFFF) + (p2 & 0xFFFFFFFF); - - low = (carry << 32) | (p0 & 0xFFFFFFFF); - high = (carry >> 32) + (p1 >> 32) + (p2 >> 32) + p3; -#endif - } - - /** - * @brief convert double using fast path. - * @param significand significand digits (at most 19). - * @param exponent exponent. - * @return converted value. - */ - inline double strtodFast (uint64_t significand, int64_t exponent) noexcept - { - static constexpr double pow10[] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, - 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22}; - - double value = static_cast (significand); - - if (DCONV_LIKELY ((exponent >= -22) && (exponent <= 22) && (value <= 9007199254740991.0))) - { - return (exponent < 0) ? (value / pow10[-exponent]) : (value * pow10[exponent]); - } - - if (DCONV_UNLIKELY ((exponent > 22) && (exponent < (22 + 16)))) - { - const double scaled = value * pow10[exponent - 22]; - if (scaled <= 9007199254740991.0) - { - return scaled * pow10[22]; - } - } - - if (DCONV_UNLIKELY (significand == 0 || exponent < -342)) - { - return 0.0; - } - - if (DCONV_UNLIKELY (exponent > 308)) - { - return std::numeric_limits::infinity (); - } - - const int lz = __builtin_clzll (significand); - significand <<= lz; - - const Power& power = atodpow[exponent + 342]; - uint64_t high, low; - umul128 (power.hi, significand, high, low); - if ((high & 0x1FF) == 0x1FF) - { - uint64_t secondHigh, secondLow; - umul128 (power.lo, significand, secondHigh, secondLow); - low += secondHigh; - high += (secondHigh > low); - } - - const int upperbit = static_cast (high >> 63); - const int shift = upperbit + 9; - uint64_t mantissa = high >> shift; - int64_t power2 = (((152170 + 65536) * exponent) >> 16) + 63 + upperbit - lz + 1023; - - if (DCONV_UNLIKELY (power2 <= 0)) - { - if (-power2 + 1 >= 64) - { - return 0.0; - } - mantissa >>= -power2 + 1; - mantissa += (mantissa & 1); - mantissa >>= 1; - power2 = (mantissa < (uint64_t (1) << 52)) ? 0 : 1; - } - else - { - if ((low <= 1) && (exponent >= -4) && (exponent <= 23) && ((mantissa & 3) == 1) && - ((mantissa << shift) == high)) - { - mantissa &= ~uint64_t (1); - } - - mantissa += (mantissa & 1); - mantissa >>= 1; - - if (DCONV_UNLIKELY (mantissa >= (uint64_t (2) << 52))) - { - mantissa = uint64_t (1) << 52; - ++power2; - } - - if (DCONV_UNLIKELY (power2 >= 0x7FF)) - { - return std::numeric_limits::infinity (); - } - } - - const uint64_t bits = (static_cast (power2) << 52) | (mantissa & ((uint64_t (1) << 52) - 1)); - memcpy (&value, &bits, sizeof (double)); - return value; - } - inline constexpr bool isDigit (char c) noexcept { return static_cast (c - '0') <= 9u; @@ -230,14 +108,15 @@ namespace dconv * @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 scale power of ten applied to the significand, updated with the final exponent. * @param fraction true if the reading stopped in the fractional part. * @param value converted value. + * @param pending integer digit already read beyond the significand, or -1. * @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) + int64_t& scale, bool fraction, double& value, int pending = -1) { static constexpr int maxDigits = 800; char text[maxDigits + 32]; @@ -245,6 +124,12 @@ namespace dconv char* cur = tail; bool truncated = false; + if (pending >= 0) + { + *cur++ = static_cast ('0' + pending); + ++scale; + } + if (!fraction) { while (isDigit (probe.peek ())) @@ -289,14 +174,15 @@ namespace dconv return false; } - value = strtodFast (significand, exponent + scale); - if (value != strtodFast (significand + 1, exponent + scale)) + scale += exponent; + value = eisellemire::compute (significand, scale); + if (value != eisellemire::compute (significand + 1, scale)) { if (truncated) { *cur++ = '1'; } - const int64_t power = exponent + scale - (cur - tail); + const int64_t power = scale - (cur - tail); *cur++ = 'e'; *std::to_chars (cur, cur + 21, power).ptr = '\0'; int error = errno; @@ -309,13 +195,16 @@ namespace dconv } /** - * @brief string to double conversion. + * @brief string to double conversion that stops on integers. * @param probe probe to read from. * @param value converted value. - * @return true on success, false otherwise. + * @param significand integer value when the error is NotDouble, first significant digits read otherwise. + * @param exponent decimal exponent read. + * @param negative sign read. + * @return Errc::Success on success, the error otherwise. */ template > - inline bool atod (Probe& probe, double& value) + inline Errc atod (Probe& probe, double& value, uint64_t& significand, int64_t& exponent, bool& negative) { const char* beg = nullptr; if constexpr (Rewind) @@ -323,16 +212,18 @@ namespace dconv beg = probe.tell (); } - bool negative = probe.getIf ('-'); + significand = 0; + exponent = 0; + negative = probe.getIf ('-'); uint64_t u = 0; - size_t digits = 0; int64_t scale = 0; + size_t digits = 0; if (probe.getIf ('0')) { if (DCONV_UNLIKELY (isDigit (probe.peek ()))) { - return false; + return Errc::InvalidNumber; } } else if (DCONV_LIKELY (isDigit (probe.peek ()))) @@ -346,9 +237,23 @@ namespace dconv digits = probe.readDigits (u, 19); if (DCONV_UNLIKELY (isDigit (probe.peek ()))) { - return atodLong (probe, negative, u, scale, false, value); + significand = u; + const int pending = probe.get () - '0'; + const int next = probe.peek (); + if (!isDigit (next) && (next != '.') && (next != 'e') && (next != 'E') && + (u <= (std::numeric_limits::max () - pending) / 10)) + { + significand = (u * 10) + pending; + return Errc::NotDouble; + } + if (!atodLong (probe, negative, u, exponent, false, value, pending)) + { + return Errc::InvalidNumber; + } + return Errc::Success; } } + significand = u; } else if (probe.getIfNoCase ('i') && probe.getIfNoCase ('n') && probe.getIfNoCase ('f')) { @@ -357,28 +262,30 @@ namespace dconv if (!(probe.getIfNoCase ('n') && probe.getIfNoCase ('i') && probe.getIfNoCase ('t') && probe.getIfNoCase ('y'))) { - return false; + return Errc::InvalidNumber; } } value = negative ? -std::numeric_limits::infinity () : std::numeric_limits::infinity (); - return true; + return Errc::Success; } else if (probe.getIfNoCase ('n') && probe.getIfNoCase ('a') && probe.getIfNoCase ('n')) { value = negative ? -std::numeric_limits::quiet_NaN () : std::numeric_limits::quiet_NaN (); - return true; + return Errc::Success; } else { - return false; + return Errc::InvalidNumber; } + bool real = false; if (probe.getIf ('.')) { + real = true; if (DCONV_UNLIKELY (!isDigit (probe.peek ()))) { - return false; + return Errc::InvalidNumber; } if (digits == 0) @@ -390,13 +297,20 @@ namespace dconv } size_t count = probe.readDigits (u, Rewind ? std::numeric_limits::max () : 19 - digits); + significand = u; digits += count; scale -= static_cast (count); + exponent = scale; + if constexpr (!Rewind) { if (DCONV_UNLIKELY (isDigit (probe.peek ()))) { - return atodLong (probe, negative, u, scale, true, value); + if (!atodLong (probe, negative, u, exponent, true, value)) + { + return Errc::InvalidNumber; + } + return Errc::Success; } } } @@ -406,22 +320,73 @@ namespace dconv if (DCONV_UNLIKELY (digits > 19)) { probe.seek (beg); - return atod (probe, value); + return atod (probe, value, significand, exponent, negative); } } - int64_t exponent = 0; - if (!readExponent (probe, exponent)) + real |= (probe.peek () == 'e') || (probe.peek () == 'E'); + int64_t power = 0; + + if (!readExponent (probe, power)) { - return false; + return Errc::InvalidNumber; } - value = strtodFast (u, exponent + scale); + if (!real) + { + return Errc::NotDouble; + } + + exponent = scale + power; + value = eisellemire::compute (u, exponent); value = negative ? -value : value; - return true; + return Errc::Success; + } + + /** + * @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) + { + uint64_t significand; + int64_t exponent; + bool negative; + const Errc err = atod (probe, value, significand, exponent, negative); + if (err == Errc::NotDouble) + { + value = negative ? -static_cast (significand) : static_cast (significand); + return true; + } + return err == Errc::Success; } } + /** + * @brief stream to double conversion that stops on integers. + * @param probe probe to read from. + * @param value converted value. + * @param significand integer value when the error is NotDouble, first significant digits read otherwise. + * @param exponent decimal exponent read. + * @param negative sign read. + * @param ec error code set on failure. + * @return 0 on success, -1 on failure. + */ + inline int atod (StreamProbe& probe, double& value, uint64_t& significand, int64_t& exponent, bool& negative, + std::error_code& ec) + { + const Errc err = details::atod (probe, value, significand, exponent, negative); + if (err != Errc::Success) + { + ec = make_error_code (err); + return -1; + } + return 0; + } + /** * @brief stream to double conversion. * @param probe probe to read from. @@ -445,6 +410,28 @@ namespace dconv return details::atod (probe, value); } + /** + * @brief string to double conversion that stops on integers. + * @param probe probe to read from. + * @param value converted value. + * @param significand integer value when the error is NotDouble, first significant digits read otherwise. + * @param exponent decimal exponent read. + * @param negative sign read. + * @param ec error code set on failure. + * @return 0 on success, -1 on failure. + */ + inline int atod (StringProbe& probe, double& value, uint64_t& significand, int64_t& exponent, bool& negative, + std::error_code& ec) + { + const Errc err = details::atod (probe, value, significand, exponent, negative); + if (err != Errc::Success) + { + ec = make_error_code (err); + return -1; + } + return 0; + } + /** * @brief string to double conversion. * @param probe probe to read from. diff --git a/dconv/include/dconv/digits.hpp b/dconv/include/dconv/digits.hpp new file mode 100644 index 0000000..1009c8e --- /dev/null +++ b/dconv/include/dconv/digits.hpp @@ -0,0 +1,88 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_DIGITS_HPP__ +#define __DCONV_DIGITS_HPP__ + +// C. +#include + +namespace dconv +{ + namespace details + { + /// powers of ten that fit in 32 bits. + constexpr uint32_t pow10[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; + + /// two digits decimal representations of 0 to 99. + alignas (64) constexpr char digitPairs[] = + "0001020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152" + "5354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899"; + + /** + * @brief count the digits of a decimal significand. + * @param digits decimal significand. + * @return number of digits. + */ + inline int countDigits (uint64_t digits) noexcept + { + if (digits < 100000000) + { + if (digits < 10000) + { + if (digits < 100) + { + return (digits < 10) ? 1 : 2; + } + return (digits < 1000) ? 3 : 4; + } + if (digits < 1000000) + { + return (digits < 100000) ? 5 : 6; + } + return (digits < 10000000) ? 7 : 8; + } + if (digits >= 10000000000000000) + { + return 17; + } + if (digits >= 1000000000000000) + { + return 16; + } + if (digits >= 1000000000000) + { + return (digits >= 100000000000000) ? 15 : ((digits >= 10000000000000) ? 14 : 13); + } + if (digits >= 10000000000) + { + return (digits >= 100000000000) ? 12 : 11; + } + return (digits >= 1000000000) ? 10 : 9; + } + + } +} + +#endif diff --git a/dconv/include/dconv/dtoa.hpp b/dconv/include/dconv/dtoa.hpp index 0417aa9..58e3e14 100644 --- a/dconv/include/dconv/dtoa.hpp +++ b/dconv/include/dconv/dtoa.hpp @@ -26,194 +26,10 @@ #define __DCONV_DTOA_HPP__ // dconv. -#include - -// C. -#include -#include +#include namespace dconv { - namespace details - { - inline char* writeExponent (char* buffer, int k) noexcept - { - *buffer = '-'; - buffer += (k < 0); - k = (k < 0) ? -k : k; - if (k >= 100) - { - *buffer++ = '0' + k / 100; - k %= 100; - *buffer++ = '0' + k / 10; - k %= 10; - } - else if (k >= 10) - { - *buffer++ = '0' + k / 10; - k %= 10; - } - *buffer++ = '0' + k; - return buffer; - } - - inline char* prettify (char* buffer, int length, int k) noexcept - { - assert (length > 0); - const int kk = length + k; - - if ((length <= kk) && (kk <= 21)) - { - memset (buffer + length, '0', kk - length); - buffer[kk] = '.'; - buffer[kk + 1] = '0'; - return &buffer[kk + 2]; - } - else if ((0 < kk) && (kk <= 21)) - { - if ((length - kk <= 16) && (kk <= 7)) - { - char tmp[16]; - memcpy (tmp, &buffer[kk], 16); - memcpy (&buffer[kk + 1], tmp, 16); - } - else - { - memmove (&buffer[kk + 1], &buffer[kk], length - kk); - } - buffer[kk] = '.'; - return &buffer[length + 1]; - } - else if ((-6 < kk) && (kk <= 0)) - { - int offset = 2 - kk; - memmove (&buffer[offset], &buffer[0], length); - buffer[0] = '0'; - buffer[1] = '.'; - memset (&buffer[2], '0', offset - 2); - return &buffer[length + offset]; - } - else if (length == 1) - { - buffer[1] = 'e'; - return writeExponent (&buffer[2], kk - 1); - } - else - { - memmove (&buffer[2], &buffer[1], length - 1); - buffer[1] = '.'; - buffer[length + 1] = 'e'; - return writeExponent (&buffer[length + 2], kk - 1); - } - } - - inline void grisuRound (char* buffer, int length, uint64_t delta, uint64_t rest, uint64_t ten_kappa, - uint64_t wp_w) noexcept - { - while ((rest < wp_w && delta - rest >= ten_kappa) && - (rest + ten_kappa < wp_w || wp_w - rest > rest + ten_kappa - wp_w)) - { - --buffer[length - 1]; - rest += ten_kappa; - } - } - - constexpr int digitsCount (uint32_t n) noexcept - { - if (n < 10) - return 1; - if (n < 100) - return 2; - if (n < 1000) - return 3; - if (n < 10000) - return 4; - if (n < 100000) - return 5; - if (n < 1000000) - return 6; - if (n < 10000000) - return 7; - if (n < 100000000) - return 8; - if (n < 1000000000) - return 9; - return 10; - } - - inline void digitsGen (const DiyFp& W, const DiyFp& Mp, uint64_t delta, char* buffer, int& length, - int& k) noexcept - { - const DiyFp one (static_cast (1) << -Mp._exponent, Mp._exponent); - const DiyFp wp_w = Mp - W; - uint32_t p1 = static_cast (Mp._mantissa >> -one._exponent); - uint64_t p2 = Mp._mantissa & (one._mantissa - 1); - int kappa = digitsCount (p1); - length = 0; - - while (kappa > 0) - { - assert (kappa >= 1 && kappa <= 10); - uint32_t d = p1 / pow10[kappa - 1]; - p1 %= pow10[kappa - 1]; - if (d || length) - { - buffer[length++] = '0' + d; - } - --kappa; - uint64_t tmp = (static_cast (p1) << -one._exponent) + p2; - if (tmp <= delta) - { - k += kappa; - grisuRound (buffer, length, delta, tmp, static_cast (pow10[kappa]) << -one._exponent, - wp_w._mantissa); - return; - } - } - - uint64_t unit = 1; - for (;;) - { - assert (unit <= UINT64_MAX / 10); - p2 *= 10; - delta *= 10; - unit *= 10; - char d = static_cast (p2 >> -one._exponent); - buffer[length] = '0' + d; - length += (d || length); - p2 &= one._mantissa - 1; - --kappa; - if (p2 < delta) - { - k += kappa; - grisuRound (buffer, length, delta, p2, one._mantissa, wp_w._mantissa * unit); - return; - } - } - } - - inline void grisu2 (char* buffer, int& length, int& k, double value) noexcept - { - DiyFp val (value), minus, plus; - val.normalizedBoundaries (minus, plus); - - const int expr = -59 - (plus._exponent + 64) + 63; - const int mk = (expr * 30103 + 99999) / 100000; - - assert (mk + 343 >= 0 && mk + 343 < 687); - const DiyFp& c_mk = dtoapow[mk + 343]; - - minus *= c_mk; - plus *= c_mk; - - ++minus._mantissa; - --plus._mantissa; - - k = -mk; - digitsGen (val.normalize () * c_mk, plus, plus._mantissa - minus._mantissa, buffer, length, k); - } - } - /** * @brief double to string conversion. * @param buffer buffer to write the string representation to (must point to at least 25 writable bytes). @@ -222,23 +38,7 @@ namespace dconv */ inline char* dtoa (char* buffer, double value) noexcept { - uint64_t bits; - memcpy (&bits, &value, sizeof (double)); - bool is_negative = (bits >> 63) != 0; - - *buffer = '-'; - buffer += is_negative; - value = is_negative ? -value : value; - - if (value == 0.0) - { - memcpy (buffer, "0.0", 3); - return buffer + 3; - } - - int length = 0, k = 0; - details::grisu2 (buffer, length, k, value); - return details::prettify (buffer, length, k); + return grisu2::dtoa (buffer, value); } } diff --git a/dconv/include/dconv/eisellemire.hpp b/dconv/include/dconv/eisellemire.hpp new file mode 100644 index 0000000..cbe9e17 --- /dev/null +++ b/dconv/include/dconv/eisellemire.hpp @@ -0,0 +1,168 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_EISELLEMIRE_HPP__ +#define __DCONV_EISELLEMIRE_HPP__ + +// dconv. +#include +#include + +// C++. +#include + +// C. +#include +#include + +namespace dconv +{ + namespace eisellemire + { + /** + * @brief multiply two 64 bits unsigned integers. + * @param a multiplicand. + * @param b multiplier. + * @param high high 64 bits of the 128-bit result. + * @param low low 64 bits of the 128-bit result. + */ + inline void umul128 (uint64_t a, uint64_t b, uint64_t& high, uint64_t& low) noexcept + { +#if defined(__SIZEOF_INT128__) + __uint128_t p = static_cast<__uint128_t> (a) * b; + + high = static_cast (p >> 64); + low = static_cast (p); +#else + uint64_t b_lo = static_cast (b); + uint64_t b_hi = b >> 32; + uint64_t p0 = (a & 0xFFFFFFFF) * b_lo; + uint64_t p1 = (a >> 32) * b_lo; + uint64_t p2 = (a & 0xFFFFFFFF) * b_hi; + uint64_t p3 = (a >> 32) * b_hi; + uint64_t carry = (p0 >> 32) + (p1 & 0xFFFFFFFF) + (p2 & 0xFFFFFFFF); + + low = (carry << 32) | (p0 & 0xFFFFFFFF); + high = (carry >> 32) + (p1 >> 32) + (p2 >> 32) + p3; +#endif + } + + /** + * @brief convert double using fast path. + * @param significand significand digits (at most 19). + * @param exponent exponent. + * @return converted value. + */ + inline double compute (uint64_t significand, int64_t exponent) noexcept + { + static constexpr double pow10[] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22}; + + double value = static_cast (significand); + + if (DCONV_LIKELY ((exponent >= -22) && (exponent <= 22) && (value <= 9007199254740991.0))) + { + return (exponent < 0) ? (value / pow10[-exponent]) : (value * pow10[exponent]); + } + + if (DCONV_UNLIKELY ((exponent > 22) && (exponent < (22 + 16)))) + { + const double scaled = value * pow10[exponent - 22]; + if (scaled <= 9007199254740991.0) + { + return scaled * pow10[22]; + } + } + + if (DCONV_UNLIKELY (significand == 0 || exponent < -342)) + { + return 0.0; + } + + if (DCONV_UNLIKELY (exponent > 308)) + { + return std::numeric_limits::infinity (); + } + + const int lz = __builtin_clzll (significand); + significand <<= lz; + + const details::Power& power = details::eisellemirepow[exponent + 342]; + uint64_t high, low; + umul128 (power.hi, significand, high, low); + if ((high & 0x1FF) == 0x1FF) + { + uint64_t secondHigh, secondLow; + umul128 (power.lo, significand, secondHigh, secondLow); + low += secondHigh; + high += (secondHigh > low); + } + + const int upperbit = static_cast (high >> 63); + const int shift = upperbit + 9; + uint64_t mantissa = high >> shift; + int64_t power2 = (((152170 + 65536) * exponent) >> 16) + 63 + upperbit - lz + 1023; + + if (DCONV_UNLIKELY (power2 <= 0)) + { + if (-power2 + 1 >= 64) + { + return 0.0; + } + mantissa >>= -power2 + 1; + mantissa += (mantissa & 1); + mantissa >>= 1; + power2 = (mantissa < (uint64_t (1) << 52)) ? 0 : 1; + } + else + { + if ((low <= 1) && (exponent >= -4) && (exponent <= 23) && ((mantissa & 3) == 1) && + ((mantissa << shift) == high)) + { + mantissa &= ~uint64_t (1); + } + + mantissa += (mantissa & 1); + mantissa >>= 1; + + if (DCONV_UNLIKELY (mantissa >= (uint64_t (2) << 52))) + { + mantissa = uint64_t (1) << 52; + ++power2; + } + + if (DCONV_UNLIKELY (power2 >= 0x7FF)) + { + return std::numeric_limits::infinity (); + } + } + + const uint64_t bits = (static_cast (power2) << 52) | (mantissa & ((uint64_t (1) << 52) - 1)); + memcpy (&value, &bits, sizeof (double)); + return value; + } + } +} + +#endif diff --git a/dconv/include/dconv/atodpow.hpp b/dconv/include/dconv/eisellemirepow.hpp similarity index 99% rename from dconv/include/dconv/atodpow.hpp rename to dconv/include/dconv/eisellemirepow.hpp index 148949e..e5a20aa 100644 --- a/dconv/include/dconv/atodpow.hpp +++ b/dconv/include/dconv/eisellemirepow.hpp @@ -22,8 +22,8 @@ * SOFTWARE. */ -#ifndef __DCONV_ATODPOW_HPP__ -#define __DCONV_ATODPOW_HPP__ +#ifndef __DCONV_EISELLEMIREPOW_HPP__ +#define __DCONV_EISELLEMIREPOW_HPP__ #include @@ -37,7 +37,7 @@ namespace dconv uint64_t lo; }; - constexpr Power atodpow[] = { + constexpr Power eisellemirepow[] = { {0xeef453d6923bd65a, 0x113faa2906a13b3f}, {0x9558b4661b6565f8, 0x4ac7ca59a424c507}, {0xbaaee17fa23ebf76, 0x5d79bcf00d2df649}, {0xe95a99df8ace6f53, 0xf4d82c2c107973dc}, {0x91d8a02bb6c10594, 0x79071b9b8a4be869}, {0xb64ec836a47146f9, 0x9748e2826cdee284}, diff --git a/dconv/include/dconv/error.hpp b/dconv/include/dconv/error.hpp new file mode 100644 index 0000000..21fab9f --- /dev/null +++ b/dconv/include/dconv/error.hpp @@ -0,0 +1,120 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_ERROR_HPP__ +#define __DCONV_ERROR_HPP__ + +// C++. +#include +#include + +namespace dconv +{ + /** + * @brief conversion error codes. + */ + enum class Errc + { + Success = 0, /**< Success. */ + InvalidNumber, /**< Number is invalid. */ + NotDouble, /**< Number is an integer. */ + }; + + /** + * @brief conversion error category. + */ + class ErrorCategory : public std::error_category + { + public: + /** + * @brief get conversion error category name. + * @return conversion error category name. + */ + const char* name () const noexcept override + { + return "dconv"; + } + + /** + * @brief translate conversion error code to human readable error string. + * @param code error code. + * @return human readable error string. + */ + std::string message (int code) const override + { + switch (static_cast (code)) + { + case Errc::InvalidNumber: + return "number is invalid"; + case Errc::NotDouble: + return "number is an integer"; + default: + return "success"; + } + } + }; + + /// conversion error category instance. + inline const ErrorCategory errorCategoryInstance; + + /** + * @brief get error category. + * @return the created std::error_category object. + */ + inline const std::error_category& errorCategory () noexcept + { + return errorCategoryInstance; + } + + /** + * @brief create an std::error_code object. + * @param code error code number. + * @return the created std::error_code object. + */ + inline std::error_code make_error_code (Errc code) noexcept + { + return std::error_code (static_cast (code), errorCategory ()); + } + + /** + * @brief create an std::error_condition object. + * @param code error code number. + * @return the created std::error_condition object. + */ + inline std::error_condition make_error_condition (Errc code) noexcept + { + return std::error_condition (static_cast (code), errorCategory ()); + } +} + +namespace std +{ + /// conversion error code specialization. + template <> + struct is_error_code_enum : public true_type + { + }; +} + +#endif diff --git a/dconv/include/dconv/format.hpp b/dconv/include/dconv/format.hpp new file mode 100644 index 0000000..cd59943 --- /dev/null +++ b/dconv/include/dconv/format.hpp @@ -0,0 +1,137 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_FORMAT_HPP__ +#define __DCONV_FORMAT_HPP__ + +// dconv. +#include + +// C. +#include +#include +#include + +namespace dconv +{ + namespace details + { + /** + * @brief write a decimal exponent. + * @param buffer buffer to write the exponent to. + * @param k exponent. + * @return end position. + */ + inline char* writeExponent (char* buffer, int k) noexcept + { + *buffer = '-'; + buffer += (k < 0); + k = (k < 0) ? -k : k; + if (k >= 100) + { + *buffer++ = static_cast ('0' + k / 100); + __builtin_memcpy (buffer, &digitPairs[(k % 100) * 2], 2); + buffer += 2; + } + else if (k >= 10) + { + __builtin_memcpy (buffer, &digitPairs[k * 2], 2); + buffer += 2; + } + else + { + *buffer++ = static_cast ('0' + k); + } + return buffer; + } + + /** + * @brief lay out decimal digits as a fixed or scientific number. + * @param buffer buffer holding the digits. + * @param length number of digits. + * @param k decimal exponent. + * @return end position. + */ + inline char* prettify (char* buffer, int length, int k) noexcept + { + assert (length > 0); + const int kk = length + k; + + if ((length <= kk) && (kk <= 21)) + { + memset (buffer + length, '0', kk - length); + buffer[kk] = '.'; + buffer[kk + 1] = '0'; + return &buffer[kk + 2]; + } + else if ((0 < kk) && (kk <= 21)) + { + if ((length - kk <= 16) && (kk <= 7)) + { + char tmp[16]; + memcpy (tmp, &buffer[kk], 16); + memcpy (&buffer[kk + 1], tmp, 16); + } + else + { + memmove (&buffer[kk + 1], &buffer[kk], length - kk); + } + buffer[kk] = '.'; + return &buffer[length + 1]; + } + else if ((-6 < kk) && (kk <= 0)) + { + int offset = 2 - kk; + memmove (&buffer[offset], &buffer[0], length); + buffer[0] = '0'; + buffer[1] = '.'; + memset (&buffer[2], '0', offset - 2); + return &buffer[length + offset]; + } + else if (length == 1) + { + buffer[1] = 'e'; + return writeExponent (&buffer[2], kk - 1); + } + else + { + if (length - 1 <= 16) + { + char tmp[16]; + memcpy (tmp, &buffer[1], 16); + memcpy (&buffer[2], tmp, 16); + } + else + { + memmove (&buffer[2], &buffer[1], length - 1); + } + buffer[1] = '.'; + buffer[length + 1] = 'e'; + return writeExponent (&buffer[length + 2], kk - 1); + } + } + } +} + +#endif diff --git a/dconv/include/dconv/grisu.hpp b/dconv/include/dconv/grisu.hpp new file mode 100644 index 0000000..0dab8d5 --- /dev/null +++ b/dconv/include/dconv/grisu.hpp @@ -0,0 +1,135 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_GRISU_HPP__ +#define __DCONV_GRISU_HPP__ + +// dconv. +#include +#include + +// C. +#include +#include +#include + +namespace dconv +{ + namespace grisu + { + namespace details + { + /** + * @brief double to string conversion with the given digits generator. + * @param buffer buffer to write the string representation to. + * @param value value to convert. + * @param generate digits generator. + * @return end position. + */ + inline char* dtoa (char* buffer, double value, + void (*generate) (char*, int&, int&, double) noexcept) noexcept + { + uint64_t bits; + memcpy (&bits, &value, sizeof (double)); + bool is_negative = (bits >> 63) != 0; + + *buffer = '-'; + buffer += is_negative; + value = is_negative ? -value : value; + + if (value == 0.0) + { + memcpy (buffer, "0.0", 3); + return buffer + 3; + } + + int length = 0, k = 0; + generate (buffer, length, k, value); + return dconv::details::prettify (buffer, length, k); + } + } + + /** + * @brief get the cached power of ten that scales a binary exponent into the digit generation range. + * @param exponent binary exponent of the normalized value. + * @param k decimal exponent of the scaled value. + * @return cached power of ten. + */ + inline const DiyFp& cachedPower (int exponent, int& k) noexcept + { + const int expr = -59 - (exponent + 64) + 63; + const int mk = (expr * 30103 + 99999) / 100000; + + assert (mk + 343 >= 0 && mk + 343 < 687); + k = -mk; + return dconv::details::grisupow[mk + 343]; + } + + /** + * @brief write the decimal digits of the scaled value. + * @param buffer buffer to write the digits to. + * @param length number of digits written. + * @param k decimal exponent. + * @param value positive value to convert. + */ + inline void generate (char* buffer, int& length, int& k, double value) noexcept + { + DiyFp w (value); + w.normalize (); + w *= cachedPower (w._exponent, k); + + const DiyFp one (static_cast (1) << -w._exponent, w._exponent); + uint32_t part1 = static_cast (w._mantissa >> -one._exponent); + uint64_t part2 = w._mantissa & (one._mantissa - 1); + + length = 0; + for (int kappa = dconv::details::countDigits (part1); kappa > 0; --kappa) + { + buffer[length++] = static_cast ('0' + part1 / dconv::details::pow10[kappa - 1]); + part1 %= dconv::details::pow10[kappa - 1]; + } + + while ((length < 18) && (part2 != 0)) + { + part2 *= 10; + buffer[length++] = static_cast ('0' + (part2 >> -one._exponent)); + part2 &= one._mantissa - 1; + --k; + } + } + + /** + * @brief double to string conversion using Grisu. + * @param buffer buffer to write the string representation to (must point to at least 26 writable bytes). + * @param value value to convert. + * @return end position. + */ + inline char* dtoa (char* buffer, double value) noexcept + { + return details::dtoa (buffer, value, generate); + } + } +} + +#endif diff --git a/dconv/include/dconv/grisu2.hpp b/dconv/include/dconv/grisu2.hpp new file mode 100644 index 0000000..6e0be56 --- /dev/null +++ b/dconv/include/dconv/grisu2.hpp @@ -0,0 +1,170 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_GRISU2_HPP__ +#define __DCONV_GRISU2_HPP__ + +// dconv. +#include + +// C. +#include +#include + +namespace dconv +{ + namespace grisu2 + { + /** + * @brief compute the scaled value and its scaled boundaries. + * @param value positive value to convert. + * @param w scaled value. + * @param minus scaled lower boundary. + * @param plus scaled upper boundary. + * @param k decimal exponent of the scaled values. + */ + inline void scaledBoundaries (double value, DiyFp& w, DiyFp& minus, DiyFp& plus, int& k) noexcept + { + w = DiyFp (value); + w.normalizedBoundaries (minus, plus); + + const DiyFp& power = grisu::cachedPower (plus._exponent, k); + minus *= power; + plus *= power; + w.normalize (); + w *= power; + } + + /** + * @brief move the last digit closer to the value while it stays within the boundaries. + * @param buffer generated digits. + * @param length number of generated digits. + * @param delta distance between the boundaries. + * @param rest distance between the upper boundary and the generated number. + * @param tenKappa scaled power of ten of the last digit. + * @param wpW distance between the upper boundary and the value. + */ + inline void round (char* buffer, int length, uint64_t delta, uint64_t rest, uint64_t tenKappa, + uint64_t wpW) noexcept + { + while ((rest < wpW && delta - rest >= tenKappa) && + (rest + tenKappa < wpW || wpW - rest > rest + tenKappa - wpW)) + { + --buffer[length - 1]; + rest += tenKappa; + } + } + + /** + * @brief generate the shortest digits within the conservative boundaries. + * @param w scaled value. + * @param high conservative upper boundary. + * @param delta distance between the conservative boundaries. + * @param buffer buffer to write the digits to. + * @param length number of digits written. + * @param k decimal exponent. + */ + inline void digitsGen (const DiyFp& w, const DiyFp& high, uint64_t delta, char* buffer, int& length, + int& k) noexcept + { + const DiyFp one (static_cast (1) << -high._exponent, high._exponent); + const DiyFp wpW = high - w; + uint32_t p1 = static_cast (high._mantissa >> -one._exponent); + uint64_t p2 = high._mantissa & (one._mantissa - 1); + int kappa = details::countDigits (p1); + length = 0; + + while (kappa > 0) + { + assert (kappa >= 1 && kappa <= 10); + uint32_t d = p1 / details::pow10[kappa - 1]; + p1 %= details::pow10[kappa - 1]; + if (d || length) + { + buffer[length++] = '0' + d; + } + --kappa; + uint64_t rest = (static_cast (p1) << -one._exponent) + p2; + if (rest <= delta) + { + k += kappa; + round (buffer, length, delta, rest, static_cast (details::pow10[kappa]) << -one._exponent, + wpW._mantissa); + return; + } + } + + uint64_t unit = 1; + for (;;) + { + assert (unit <= UINT64_MAX / 10); + p2 *= 10; + delta *= 10; + unit *= 10; + char d = static_cast (p2 >> -one._exponent); + buffer[length] = '0' + d; + length += (d || length); + p2 &= one._mantissa - 1; + --kappa; + if (p2 < delta) + { + k += kappa; + round (buffer, length, delta, p2, one._mantissa, wpW._mantissa * unit); + return; + } + } + } + + /** + * @brief write the shortest decimal digits within the conservative boundaries. + * @param buffer buffer to write the digits to. + * @param length number of digits written. + * @param k decimal exponent. + * @param value positive value to convert. + */ + inline void generate (char* buffer, int& length, int& k, double value) noexcept + { + DiyFp w, minus, plus; + scaledBoundaries (value, w, minus, plus, k); + + ++minus._mantissa; + --plus._mantissa; + + digitsGen (w, plus, plus._mantissa - minus._mantissa, buffer, length, k); + } + + /** + * @brief double to string conversion using Grisu2. + * @param buffer buffer to write the string representation to (must point to at least 25 writable bytes). + * @param value value to convert. + * @return end position. + */ + inline char* dtoa (char* buffer, double value) noexcept + { + return grisu::details::dtoa (buffer, value, generate); + } + } +} + +#endif diff --git a/dconv/include/dconv/grisu3.hpp b/dconv/include/dconv/grisu3.hpp new file mode 100644 index 0000000..a1802fc --- /dev/null +++ b/dconv/include/dconv/grisu3.hpp @@ -0,0 +1,188 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __DCONV_GRISU3_HPP__ +#define __DCONV_GRISU3_HPP__ + +// dconv. +#include + +// C++. +#include + +// C. +#include + +namespace dconv +{ + namespace grisu3 + { + /** + * @brief round the last digit to the closest value and check it is safe. + * @param buffer generated digits. + * @param length number of generated digits. + * @param wpW distance between the liberal upper boundary and the value. + * @param delta liberal interval size. + * @param rest distance between the liberal upper boundary and the generated number. + * @param tenKappa scaled power of ten of the last digit. + * @param ulp scaled unit of the approximation error. + * @return true if the generated number is the shortest and closest, false otherwise. + */ + inline bool roundWeed (char* buffer, int length, uint64_t wpW, uint64_t delta, uint64_t rest, uint64_t tenKappa, + uint64_t ulp) noexcept + { + const uint64_t wpWup = wpW - ulp; + const uint64_t wpWdown = wpW + ulp; + + while ((rest < wpWup) && (delta - rest >= tenKappa) && + ((rest + tenKappa < wpWup) || (wpWup - rest >= rest + tenKappa - wpWup))) + { + --buffer[length - 1]; + rest += tenKappa; + } + + if ((rest < wpWdown) && (delta - rest >= tenKappa) && + ((rest + tenKappa < wpWdown) || (wpWdown - rest > rest + tenKappa - wpWdown))) + { + return false; + } + + return (2 * ulp <= rest) && (rest <= delta - 4 * ulp); + } + + /** + * @brief generate the shortest digits within the liberal boundaries. + * @param low lower boundary. + * @param w value. + * @param high upper boundary. + * @param buffer buffer to write the digits to. + * @param length number of digits written. + * @param k decimal exponent. + * @return true if the digits are the shortest and closest, false otherwise. + */ + inline bool digitsGen (const DiyFp& low, const DiyFp& w, const DiyFp& high, char* buffer, int& length, + int& k) noexcept + { + uint64_t unit = 1; + const uint64_t tooLow = low._mantissa - unit; + const uint64_t tooHigh = high._mantissa + unit; + uint64_t unsafe = tooHigh - tooLow; + + const DiyFp one (static_cast (1) << -w._exponent, w._exponent); + uint32_t p1 = static_cast (tooHigh >> -one._exponent); + uint64_t p2 = tooHigh & (one._mantissa - 1); + int kappa = details::countDigits (p1); + length = 0; + + while (kappa > 0) + { + buffer[length++] = static_cast ('0' + p1 / details::pow10[kappa - 1]); + p1 %= details::pow10[kappa - 1]; + --kappa; + + const uint64_t rest = (static_cast (p1) << -one._exponent) + p2; + if (rest < unsafe) + { + k += kappa; + return roundWeed (buffer, length, tooHigh - w._mantissa, unsafe, rest, + static_cast (details::pow10[kappa]) << -one._exponent, unit); + } + } + + for (;;) + { + p2 *= 10; + unit *= 10; + unsafe *= 10; + buffer[length++] = static_cast ('0' + (p2 >> -one._exponent)); + p2 &= one._mantissa - 1; + --kappa; + + if (p2 < unsafe) + { + k += kappa; + return roundWeed (buffer, length, (tooHigh - w._mantissa) * unit, unsafe, p2, one._mantissa, unit); + } + } + } + + /** + * @brief write the shortest and closest decimal digits using the standard library. + * @param buffer buffer to write the digits to. + * @param length number of digits written. + * @param k decimal exponent. + * @param value positive value to convert. + */ + inline void fallback (char* buffer, int& length, int& k, double value) noexcept + { + char text[32]; + const char* end = std::to_chars (text, text + sizeof (text), value, std::chars_format::scientific).ptr; + const char* cur = text; + + length = 0; + for (; *cur != 'e'; ++cur) + { + if (*cur != '.') + { + buffer[length++] = *cur; + } + } + + int exponent = 0; + std::from_chars (cur + 2, end, exponent); + k = ((cur[1] == '-') ? -exponent : exponent) - (length - 1); + } + + /** + * @brief write the shortest and closest decimal digits. + * @param buffer buffer to write the digits to. + * @param length number of digits written. + * @param k decimal exponent. + * @param value positive value to convert. + */ + inline void generate (char* buffer, int& length, int& k, double value) noexcept + { + DiyFp w, minus, plus; + grisu2::scaledBoundaries (value, w, minus, plus, k); + + if (!digitsGen (minus, w, plus, buffer, length, k)) + { + fallback (buffer, length, k, value); + } + } + + /** + * @brief double to string conversion using Grisu3. + * @param buffer buffer to write the string representation to (must point to at least 25 writable bytes). + * @param value value to convert. + * @return end position. + */ + inline char* dtoa (char* buffer, double value) noexcept + { + return grisu::details::dtoa (buffer, value, generate); + } + } +} + +#endif diff --git a/dconv/include/dconv/dtoapow.hpp b/dconv/include/dconv/grisupow.hpp similarity index 99% rename from dconv/include/dconv/dtoapow.hpp rename to dconv/include/dconv/grisupow.hpp index 5a86627..8cf09a8 100644 --- a/dconv/include/dconv/dtoapow.hpp +++ b/dconv/include/dconv/grisupow.hpp @@ -22,8 +22,8 @@ * SOFTWARE. */ -#ifndef __DCONV_DTOAPOW_HPP__ -#define __DCONV_DTOAPOW_HPP__ +#ifndef __DCONV_GRISUPOW_HPP__ +#define __DCONV_GRISUPOW_HPP__ // dconv. #include @@ -32,9 +32,8 @@ namespace dconv { namespace details { - constexpr uint32_t pow10[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; - - constexpr DiyFp dtoapow[] = { + /// cached powers of ten for the grisu algorithms. + constexpr DiyFp grisupow[] = { {0xbf29dcaba82fdeae, -1203}, {0xeef453d6923bd65a, -1200}, {0x9558b4661b6565f8, -1196}, {0xbaaee17fa23ebf76, -1193}, {0xe95a99df8ace6f54, -1190}, {0x91d8a02bb6c10594, -1186}, {0xb64ec836a47146fa, -1183}, {0xe3e27a444d8d98b8, -1180}, {0x8e6d8c6ab0787f73, -1176}, diff --git a/dconv/tests/CMakeLists.txt b/dconv/tests/CMakeLists.txt index 7dccca8..906b4ad 100644 --- a/dconv/tests/CMakeLists.txt +++ b/dconv/tests/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.22.1) find_package(GTest REQUIRED) +add_executable(error.gtest error_test.cpp) +target_link_libraries(error.gtest ${PROJECT_NAME} GTest::gtest_main pthread) +gtest_discover_tests(error.gtest) + add_executable(string_probe.gtest string_probe_test.cpp) target_link_libraries(string_probe.gtest ${PROJECT_NAME} GTest::gtest_main pthread) gtest_discover_tests(string_probe.gtest) @@ -14,6 +18,26 @@ add_executable(atod.gtest atod_test.cpp) target_link_libraries(atod.gtest ${PROJECT_NAME} GTest::gtest_main pthread) gtest_discover_tests(atod.gtest) +add_executable(digits.gtest digits_test.cpp) +target_link_libraries(digits.gtest ${PROJECT_NAME} GTest::gtest_main pthread) +gtest_discover_tests(digits.gtest) + +add_executable(format.gtest format_test.cpp) +target_link_libraries(format.gtest ${PROJECT_NAME} GTest::gtest_main pthread) +gtest_discover_tests(format.gtest) + +add_executable(grisu.gtest grisu_test.cpp) +target_link_libraries(grisu.gtest ${PROJECT_NAME} GTest::gtest_main pthread) +gtest_discover_tests(grisu.gtest) + +add_executable(grisu2.gtest grisu2_test.cpp) +target_link_libraries(grisu2.gtest ${PROJECT_NAME} GTest::gtest_main pthread) +gtest_discover_tests(grisu2.gtest) + +add_executable(grisu3.gtest grisu3_test.cpp) +target_link_libraries(grisu3.gtest ${PROJECT_NAME} GTest::gtest_main pthread) +gtest_discover_tests(grisu3.gtest) + add_executable(dtoa.gtest dtoa_test.cpp) target_link_libraries(dtoa.gtest ${PROJECT_NAME} GTest::gtest_main pthread) gtest_discover_tests(dtoa.gtest) diff --git a/dconv/tests/atod_test.cpp b/dconv/tests/atod_test.cpp index 2e093f1..57adee8 100644 --- a/dconv/tests/atod_test.cpp +++ b/dconv/tests/atod_test.cpp @@ -476,6 +476,148 @@ TEST (atod, atod) str = "nanx"; EXPECT_EQ (dconv::atod (str, value), str + 3); EXPECT_TRUE (std::isnan (value)); + + uint64_t significand = 0; + int64_t exponent = 0; + bool negative = false; + std::error_code ec; + + str = "-2.5e3"; + dconv::StringProbe realProbe (str); + ASSERT_EQ (dconv::atod (realProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, -2500.0); + EXPECT_EQ (significand, 25u); + EXPECT_EQ (exponent, 2); + EXPECT_TRUE (negative); + + str = "0.001"; + dconv::StringProbe fractionProbe (str); + ASSERT_EQ (dconv::atod (fractionProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, 0.001); + EXPECT_EQ (significand, 1u); + EXPECT_EQ (exponent, -3); + EXPECT_FALSE (negative); + + str = "1e0"; + dconv::StringProbe exponentProbe (str); + ASSERT_EQ (dconv::atod (exponentProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, 1.0); + EXPECT_EQ (significand, 1u); + EXPECT_EQ (exponent, 0); + EXPECT_FALSE (negative); + + str = "18446744073709551616"; + dconv::StringProbe overflowProbe (str); + ASSERT_EQ (dconv::atod (overflowProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, 18446744073709551616.0); + EXPECT_EQ (significand, 1844674407370955161u); + EXPECT_EQ (exponent, 1); + EXPECT_FALSE (negative); + + str = "12345678901234567890.5"; + dconv::StringProbe longProbe (str); + ASSERT_EQ (dconv::atod (longProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, 12345678901234567890.5); + EXPECT_EQ (significand, 1234567890123456789u); + EXPECT_EQ (exponent, 1); + EXPECT_FALSE (negative); + + str = "0.12345678901234567890123"; + dconv::StringProbe longFractionProbe (str); + ASSERT_EQ (dconv::atod (longFractionProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, 0.12345678901234567890123); + EXPECT_EQ (significand, 1234567890123456789u); + EXPECT_EQ (exponent, -19); + EXPECT_FALSE (negative); + + std::istringstream overflowStreamIn ("18446744073709551616"); + dconv::StreamProbe overflowStreamProbe (overflowStreamIn); + ASSERT_EQ (dconv::atod (overflowStreamProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, 18446744073709551616.0); + EXPECT_EQ (significand, 1844674407370955161u); + EXPECT_EQ (exponent, 1); + EXPECT_FALSE (negative); + + str = "123,"; + dconv::StringProbe integerProbe (str); + ASSERT_EQ (dconv::atod (integerProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::NotDouble); + EXPECT_EQ (significand, 123u); + EXPECT_EQ (exponent, 0); + EXPECT_FALSE (negative); + + str = "-0"; + dconv::StringProbe zeroProbe (str); + ASSERT_EQ (dconv::atod (zeroProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::NotDouble); + EXPECT_EQ (significand, 0u); + EXPECT_EQ (exponent, 0); + EXPECT_TRUE (negative); + + str = "18446744073709551615"; + dconv::StringProbe maxProbe (str); + ASSERT_EQ (dconv::atod (maxProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::NotDouble); + EXPECT_EQ (significand, 18446744073709551615u); + EXPECT_EQ (exponent, 0); + EXPECT_FALSE (negative); + + std::istringstream maxStreamIn ("18446744073709551615"); + dconv::StreamProbe maxStreamProbe (maxStreamIn); + ASSERT_EQ (dconv::atod (maxStreamProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::NotDouble); + EXPECT_EQ (significand, 18446744073709551615u); + EXPECT_FALSE (negative); + + str = "01"; + dconv::StringProbe leadingZeroProbe (str); + ASSERT_EQ (dconv::atod (leadingZeroProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "1."; + dconv::StringProbe dotProbe (str); + ASSERT_EQ (dconv::atod (dotProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "1e+"; + dconv::StringProbe exponentSignProbe (str); + ASSERT_EQ (dconv::atod (exponentSignProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "-"; + dconv::StringProbe signProbe (str); + ASSERT_EQ (dconv::atod (signProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "infinit"; + dconv::StringProbe infinityProbe (str); + ASSERT_EQ (dconv::atod (infinityProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "12345678901234567890."; + dconv::StringProbe longDotProbe (str); + ASSERT_EQ (dconv::atod (longDotProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "0.12345678901234567890e"; + dconv::StringProbe longExponentProbe (str); + ASSERT_EQ (dconv::atod (longExponentProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + std::istringstream longDotStreamIn ("12345678901234567890."); + dconv::StreamProbe longDotStreamProbe (longDotStreamIn); + ASSERT_EQ (dconv::atod (longDotStreamProbe, value, significand, exponent, negative, ec), -1); + EXPECT_EQ (ec, dconv::Errc::InvalidNumber); + + str = "-infinity"; + dconv::StringProbe infinityValueProbe (str); + ASSERT_EQ (dconv::atod (infinityValueProbe, value, significand, exponent, negative, ec), 0); + EXPECT_EQ (value, -std::numeric_limits::infinity ()); + + str = "nan"; + dconv::StringProbe nanProbe (str); + ASSERT_EQ (dconv::atod (nanProbe, value, significand, exponent, negative, ec), 0); + EXPECT_TRUE (std::isnan (value)); } /** diff --git a/dconv/tests/digits_test.cpp b/dconv/tests/digits_test.cpp new file mode 100644 index 0000000..50178e2 --- /dev/null +++ b/dconv/tests/digits_test.cpp @@ -0,0 +1,54 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// dconv. +#include + +// libraries. +#include + +/** + * @brief countDigits test. + */ +TEST (digits, countDigits) +{ + uint64_t power = 1; + for (int length = 1; length <= 17; ++length) + { + EXPECT_EQ (dconv::details::countDigits (power), length); + EXPECT_EQ (dconv::details::countDigits (power * 10 - 1), length); + power *= 10; + } + EXPECT_EQ (dconv::details::countDigits (0), 1); + EXPECT_EQ (dconv::details::countDigits (UINT32_MAX), 10); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/dconv/tests/dtoa_test.cpp b/dconv/tests/dtoa_test.cpp index d1f5882..f4c4f9e 100644 --- a/dconv/tests/dtoa_test.cpp +++ b/dconv/tests/dtoa_test.cpp @@ -28,33 +28,6 @@ // libraries. #include -/** - * @brief digitsCount test. - */ -TEST (dtoa, digitsCount) -{ - EXPECT_EQ (dconv::details::digitsCount (0), 1); - EXPECT_EQ (dconv::details::digitsCount (9), 1); - EXPECT_EQ (dconv::details::digitsCount (10), 2); - EXPECT_EQ (dconv::details::digitsCount (99), 2); - EXPECT_EQ (dconv::details::digitsCount (100), 3); - EXPECT_EQ (dconv::details::digitsCount (999), 3); - EXPECT_EQ (dconv::details::digitsCount (1000), 4); - EXPECT_EQ (dconv::details::digitsCount (9999), 4); - EXPECT_EQ (dconv::details::digitsCount (10000), 5); - EXPECT_EQ (dconv::details::digitsCount (99999), 5); - EXPECT_EQ (dconv::details::digitsCount (100000), 6); - EXPECT_EQ (dconv::details::digitsCount (999999), 6); - EXPECT_EQ (dconv::details::digitsCount (1000000), 7); - EXPECT_EQ (dconv::details::digitsCount (9999999), 7); - EXPECT_EQ (dconv::details::digitsCount (10000000), 8); - EXPECT_EQ (dconv::details::digitsCount (99999999), 8); - EXPECT_EQ (dconv::details::digitsCount (100000000), 9); - EXPECT_EQ (dconv::details::digitsCount (999999999), 9); - EXPECT_EQ (dconv::details::digitsCount (1000000000), 10); - EXPECT_EQ (dconv::details::digitsCount (UINT32_MAX), 10); -} - /** * @brief dtoa test. */ diff --git a/dconv/tests/error_test.cpp b/dconv/tests/error_test.cpp new file mode 100644 index 0000000..7b5d1d2 --- /dev/null +++ b/dconv/tests/error_test.cpp @@ -0,0 +1,78 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// dconv. +#include + +// libraries. +#include + +/** + * @brief name test. + */ +TEST (ErrorCategory, name) +{ + EXPECT_STREQ (dconv::errorCategory ().name (), "dconv"); +} + +/** + * @brief message test. + */ +TEST (ErrorCategory, message) +{ + EXPECT_EQ (dconv::errorCategory ().message (0), "success"); + EXPECT_EQ (dconv::errorCategory ().message (static_cast (dconv::Errc::InvalidNumber)), "number is invalid"); + EXPECT_EQ (dconv::errorCategory ().message (static_cast (dconv::Errc::NotDouble)), "number is an integer"); +} + +/** + * @brief make_error_code test. + */ +TEST (ErrorCategory, make_error_code) +{ + std::error_code ec = dconv::make_error_code (dconv::Errc::NotDouble); + EXPECT_EQ (ec.value (), static_cast (dconv::Errc::NotDouble)); + EXPECT_EQ (&ec.category (), &dconv::errorCategory ()); + EXPECT_TRUE (ec == dconv::Errc::NotDouble); + EXPECT_FALSE (ec == dconv::Errc::InvalidNumber); +} + +/** + * @brief make_error_condition test. + */ +TEST (ErrorCategory, make_error_condition) +{ + std::error_condition condition = dconv::make_error_condition (dconv::Errc::InvalidNumber); + EXPECT_EQ (condition.value (), static_cast (dconv::Errc::InvalidNumber)); + EXPECT_EQ (&condition.category (), &dconv::errorCategory ()); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/dconv/tests/format_test.cpp b/dconv/tests/format_test.cpp new file mode 100644 index 0000000..0ae0656 --- /dev/null +++ b/dconv/tests/format_test.cpp @@ -0,0 +1,125 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// dconv. +#include + +// libraries. +#include + +// C++. +#include +#include + +/** + * @brief writeExponent test. + */ +TEST (format, writeExponent) +{ + char buffer[8] = {}; + char* end = nullptr; + + end = dconv::details::writeExponent (buffer, 0); + EXPECT_EQ (std::string (buffer, end), "0"); + + end = dconv::details::writeExponent (buffer, 7); + EXPECT_EQ (std::string (buffer, end), "7"); + + end = dconv::details::writeExponent (buffer, -7); + EXPECT_EQ (std::string (buffer, end), "-7"); + + end = dconv::details::writeExponent (buffer, 42); + EXPECT_EQ (std::string (buffer, end), "42"); + + end = dconv::details::writeExponent (buffer, -42); + EXPECT_EQ (std::string (buffer, end), "-42"); + + end = dconv::details::writeExponent (buffer, 308); + EXPECT_EQ (std::string (buffer, end), "308"); + + end = dconv::details::writeExponent (buffer, -324); + EXPECT_EQ (std::string (buffer, end), "-324"); +} + +/** + * @brief prettify test. + */ +TEST (format, prettify) +{ + char buffer[40] = {}; + char* end = nullptr; + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, 0); + EXPECT_EQ (std::string (buffer, end), "12345.0"); + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, 2); + EXPECT_EQ (std::string (buffer, end), "1234500.0"); + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, -2); + EXPECT_EQ (std::string (buffer, end), "123.45"); + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, -5); + EXPECT_EQ (std::string (buffer, end), "0.12345"); + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, -7); + EXPECT_EQ (std::string (buffer, end), "0.0012345"); + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, -11); + EXPECT_EQ (std::string (buffer, end), "1.2345e-7"); + + std::memcpy (buffer, "12345", 5); + end = dconv::details::prettify (buffer, 5, 17); + EXPECT_EQ (std::string (buffer, end), "1.2345e21"); + + std::memcpy (buffer, "1", 1); + end = dconv::details::prettify (buffer, 1, -7); + EXPECT_EQ (std::string (buffer, end), "1e-7"); + + std::memcpy (buffer, "1", 1); + end = dconv::details::prettify (buffer, 1, 30); + EXPECT_EQ (std::string (buffer, end), "1e30"); + + std::memcpy (buffer, "123456789012345678901", 21); + end = dconv::details::prettify (buffer, 21, -1); + EXPECT_EQ (std::string (buffer, end), "12345678901234567890.1"); + + std::memcpy (buffer, "123456789012345678", 18); + end = dconv::details::prettify (buffer, 18, -1); + EXPECT_EQ (std::string (buffer, end), "12345678901234567.8"); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/dconv/tests/grisu2_test.cpp b/dconv/tests/grisu2_test.cpp new file mode 100644 index 0000000..94729bf --- /dev/null +++ b/dconv/tests/grisu2_test.cpp @@ -0,0 +1,155 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// dconv. +#include + +// libraries. +#include + +// C++. +#include +#include + +/** + * @brief scaledBoundaries test. + */ +TEST (grisu2, scaledBoundaries) +{ + for (double value : {5e-324, 2.2250738585072014e-308, 0.1, 1.0, 1e23, 1.7976931348623157e308}) + { + dconv::DiyFp w, minus, plus; + int k = 0; + dconv::grisu2::scaledBoundaries (value, w, minus, plus, k); + EXPECT_EQ (w._exponent, plus._exponent); + EXPECT_EQ (minus._exponent, plus._exponent); + EXPECT_LT (minus._mantissa, w._mantissa); + EXPECT_LT (w._mantissa, plus._mantissa); + } +} + +/** + * @brief round test. + */ +TEST (grisu2, round) +{ + char buffer[] = "13"; + dconv::grisu2::round (buffer, 2, 100, 10, 10, 35); + EXPECT_STREQ (buffer, "11"); + + char unchanged[] = "13"; + dconv::grisu2::round (unchanged, 2, 100, 40, 10, 35); + EXPECT_STREQ (unchanged, "13"); +} + +/** + * @brief digitsGen test. + */ +TEST (grisu2, digitsGen) +{ + dconv::DiyFp w, minus, plus; + int k = 0, length = 0; + char buffer[32] = {}; + + dconv::grisu2::scaledBoundaries (0.3, w, minus, plus, k); + ++minus._mantissa; + --plus._mantissa; + dconv::grisu2::digitsGen (w, plus, plus._mantissa - minus._mantissa, buffer, length, k); + EXPECT_EQ (std::string (buffer, length), "3"); + EXPECT_EQ (k, -1); + + dconv::grisu2::scaledBoundaries (1234567890123456.0, w, minus, plus, k); + ++minus._mantissa; + --plus._mantissa; + dconv::grisu2::digitsGen (w, plus, plus._mantissa - minus._mantissa, buffer, length, k); + EXPECT_EQ (std::string (buffer, length), "1234567890123456"); + EXPECT_EQ (k, 0); +} + +/** + * @brief generate test. + */ +TEST (grisu2, generate) +{ + char buffer[32] = {}; + int length = 0, k = 0; + + dconv::grisu2::generate (buffer, length, k, 1e23); + EXPECT_EQ (std::string (buffer, length), "9999999999999999"); + EXPECT_EQ (k, 7); +} + +/** + * @brief dtoa test. + */ +TEST (grisu2, dtoa) +{ + char beg[32] = {}; + char* end = nullptr; + + end = dconv::grisu2::dtoa (beg, 0.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.0"); + + end = dconv::grisu2::dtoa (beg, -0.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "-0.0"); + + end = dconv::grisu2::dtoa (beg, 0.1); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.1"); + + end = dconv::grisu2::dtoa (beg, -1.5); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "-1.5"); + + end = dconv::grisu2::dtoa (beg, 123456789.125); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "123456789.125"); + + end = dconv::grisu2::dtoa (beg, 1e21); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1e21"); + + end = dconv::grisu2::dtoa (beg, 1e23); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "9.999999999999999e22"); + + end = dconv::grisu2::dtoa (beg, 0.000123); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.000123"); + + end = dconv::grisu2::dtoa (beg, 1.2345e-7); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1.2345e-7"); + + end = dconv::grisu2::dtoa (beg, 5e-324); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "5e-324"); + + end = dconv::grisu2::dtoa (beg, 2.2250738585072014e-308); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "2.2250738585072014e-308"); + + end = dconv::grisu2::dtoa (beg, 1.7976931348623157e308); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1.7976931348623157e308"); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/dconv/tests/grisu3_test.cpp b/dconv/tests/grisu3_test.cpp new file mode 100644 index 0000000..51be656 --- /dev/null +++ b/dconv/tests/grisu3_test.cpp @@ -0,0 +1,162 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// dconv. +#include + +// libraries. +#include + +// C++. +#include + +/** + * @brief roundWeed test. + */ +TEST (grisu3, roundWeed) +{ + char buffer[] = "13"; + EXPECT_TRUE (dconv::grisu3::roundWeed (buffer, 2, 30, 100, 10, 10, 1)); + EXPECT_STREQ (buffer, "11"); + + char ambiguous[] = "13"; + EXPECT_FALSE (dconv::grisu3::roundWeed (ambiguous, 2, 35, 100, 10, 10, 1)); + + char unsafe[] = "13"; + EXPECT_FALSE (dconv::grisu3::roundWeed (unsafe, 2, 10, 15, 9, 10, 5)); + EXPECT_STREQ (unsafe, "13"); +} + +/** + * @brief digitsGen test. + */ +TEST (grisu3, digitsGen) +{ + dconv::DiyFp w, minus, plus; + int k = 0, length = 0; + char buffer[32] = {}; + + dconv::grisu2::scaledBoundaries (0.3, w, minus, plus, k); + EXPECT_TRUE (dconv::grisu3::digitsGen (minus, w, plus, buffer, length, k)); + EXPECT_EQ (std::string (buffer, length), "3"); + EXPECT_EQ (k, -1); + + dconv::grisu2::scaledBoundaries (9007199254740992.0, w, minus, plus, k); + EXPECT_TRUE (dconv::grisu3::digitsGen (minus, w, plus, buffer, length, k)); + EXPECT_EQ (std::string (buffer, length), "9007199254740992"); + EXPECT_EQ (k, 0); + + dconv::grisu2::scaledBoundaries (1e23, w, minus, plus, k); + EXPECT_FALSE (dconv::grisu3::digitsGen (minus, w, plus, buffer, length, k)); +} + +/** + * @brief fallback test. + */ +TEST (grisu3, fallback) +{ + char buffer[32] = {}; + int length = 0, k = 0; + + dconv::grisu3::fallback (buffer, length, k, 1e23); + EXPECT_EQ (std::string (buffer, length), "1"); + EXPECT_EQ (k, 23); + + dconv::grisu3::fallback (buffer, length, k, 0.30000000000000004); + EXPECT_EQ (std::string (buffer, length), "30000000000000004"); + EXPECT_EQ (k, -17); + + dconv::grisu3::fallback (buffer, length, k, 5e-324); + EXPECT_EQ (std::string (buffer, length), "5"); + EXPECT_EQ (k, -324); + + dconv::grisu3::fallback (buffer, length, k, 1.7976931348623157e308); + EXPECT_EQ (std::string (buffer, length), "17976931348623157"); + EXPECT_EQ (k, 292); +} + +/** + * @brief generate test. + */ +TEST (grisu3, generate) +{ + char buffer[32] = {}; + int length = 0, k = 0; + + dconv::grisu3::generate (buffer, length, k, 0.3); + EXPECT_EQ (std::string (buffer, length), "3"); + EXPECT_EQ (k, -1); + + dconv::grisu3::generate (buffer, length, k, 1e23); + EXPECT_EQ (std::string (buffer, length), "1"); + EXPECT_EQ (k, 23); +} + +/** + * @brief dtoa test. + */ +TEST (grisu3, dtoa) +{ + char beg[32] = {}; + char* end = nullptr; + + end = dconv::grisu3::dtoa (beg, 0.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.0"); + + end = dconv::grisu3::dtoa (beg, -0.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "-0.0"); + + end = dconv::grisu3::dtoa (beg, 0.1); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.1"); + + end = dconv::grisu3::dtoa (beg, -1.5); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "-1.5"); + + end = dconv::grisu3::dtoa (beg, 1e21); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1e21"); + + end = dconv::grisu3::dtoa (beg, 1e23); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1e23"); + + end = dconv::grisu3::dtoa (beg, 0.000123); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.000123"); + + end = dconv::grisu3::dtoa (beg, 5e-324); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "5e-324"); + + end = dconv::grisu3::dtoa (beg, 4.9406564584124654e-320); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "4.9407e-320"); + + end = dconv::grisu3::dtoa (beg, 1.7976931348623157e308); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1.7976931348623157e308"); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/dconv/tests/grisu_test.cpp b/dconv/tests/grisu_test.cpp new file mode 100644 index 0000000..aa671f6 --- /dev/null +++ b/dconv/tests/grisu_test.cpp @@ -0,0 +1,115 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// dconv. +#include + +// libraries. +#include + +// C++. +#include + +/** + * @brief cachedPower test. + */ +TEST (grisu, cachedPower) +{ + for (int exponent = -1137; exponent <= 960; ++exponent) + { + int k = 0; + const dconv::DiyFp& power = dconv::grisu::cachedPower (exponent, k); + const int scaled = exponent + power._exponent + 64; + EXPECT_GE (scaled, -60) << exponent; + EXPECT_LE (scaled, -32) << exponent; + } +} + +/** + * @brief generate test. + */ +TEST (grisu, generate) +{ + char buffer[32] = {}; + int length = 0, k = 0; + + dconv::grisu::generate (buffer, length, k, 0.5); + EXPECT_EQ (std::string (buffer, length), "50"); + EXPECT_EQ (k, -2); + + dconv::grisu::generate (buffer, length, k, 0.1); + EXPECT_EQ (std::string (buffer, length), "100000000000000005"); + EXPECT_EQ (k, -18); +} + +/** + * @brief dtoa test. + */ +TEST (grisu, dtoa) +{ + char beg[32] = {}; + char* end = nullptr; + + end = dconv::grisu::dtoa (beg, 0.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.0"); + + end = dconv::grisu::dtoa (beg, -0.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "-0.0"); + + end = dconv::grisu::dtoa (beg, 1.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1.0"); + + end = dconv::grisu::dtoa (beg, -1.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "-1.0"); + + end = dconv::grisu::dtoa (beg, 0.1); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.100000000000000005"); + + end = dconv::grisu::dtoa (beg, 1024.0); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1024.00000000000000"); + + end = dconv::grisu::dtoa (beg, 1e21); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1.000e21"); + + end = dconv::grisu::dtoa (beg, 1e23); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "9.99999999999999916e22"); + + end = dconv::grisu::dtoa (beg, 5e-324); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "4.94065645841246544e-324"); + + end = dconv::grisu::dtoa (beg, 1.7976931348623157e308); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "1.79769313486231570e308"); + + end = dconv::grisu::dtoa (beg, 0.00001); + EXPECT_STREQ (std::string (beg, end - beg).c_str (), "0.0000100000000000000008"); +} + +/** + * @brief main function. + */ +int main (int argc, char** argv) +{ + testing::InitGoogleTest (&argc, argv); + return RUN_ALL_TESTS (); +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 995022c..1deb54a 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,3 +1,4 @@ cmake_minimum_required(VERSION 3.22.1) -add_subdirectory(powgen) +add_subdirectory(eisellemirepowgen) +add_subdirectory(grisupowgen) diff --git a/tools/eisellemirepowgen/CMakeLists.txt b/tools/eisellemirepowgen/CMakeLists.txt new file mode 100644 index 0000000..007c6bc --- /dev/null +++ b/tools/eisellemirepowgen/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.22.1) + +set(EISELLEMIREPOWGEN_VERSION ${DCONV_VERSION}) + +set(EISELLEMIREPOWGEN_VERSION_MAJOR ${DCONV_VERSION_MAJOR}) +set(EISELLEMIREPOWGEN_VERSION_MINOR ${DCONV_VERSION_MINOR}) +set(EISELLEMIREPOWGEN_VERSION_PATCH ${DCONV_VERSION_PATCH}) + +message(STATUS "eisellemirepowgen version: ${EISELLEMIREPOWGEN_VERSION}") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.hpp.in" "${CMAKE_CURRENT_SOURCE_DIR}/include/version.hpp") + +set(PRIVATE_HEADER_DIRS + src +) + +set(PUBLIC_HEADER_DIRS + include +) + +set(PUBLIC_HEADERS + include/version.hpp + include/bigint.hpp +) + +set(SOURCES + src/main.cpp +) + +add_executable(eisellemirepowgen ${SOURCES}) +set_target_properties(eisellemirepowgen PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") + +target_include_directories(eisellemirepowgen PUBLIC ${PUBLIC_HEADER_DIRS}) +target_include_directories(eisellemirepowgen PRIVATE ${PRIVATE_HEADER_DIRS}) + +install(TARGETS eisellemirepowgen DESTINATION /bin) diff --git a/tools/powgen/README.md b/tools/eisellemirepowgen/README.md similarity index 89% rename from tools/powgen/README.md rename to tools/eisellemirepowgen/README.md index 37cee6e..894d18e 100644 --- a/tools/powgen/README.md +++ b/tools/eisellemirepowgen/README.md @@ -1,6 +1,6 @@ -# powgen +# eisellemirepowgen -**powgen** is a command-line tool that generates **lookup +**eisellemirepowgen** is a command-line tool that generates **lookup tables** of high-precision powers of 5. The tool outputs a fully self-contained C++ header containing: @@ -24,7 +24,7 @@ Generate powers for exponents −200 through +200 and write them to `power.hpp`: ``` bash -powgen -l -200 -u 200 > power.hpp +eisellemirepowgen -l -200 -u 200 > power.hpp ``` The generated header will contain definitions like: diff --git a/tools/powgen/include/bigint.hpp b/tools/eisellemirepowgen/include/bigint.hpp similarity index 93% rename from tools/powgen/include/bigint.hpp rename to tools/eisellemirepowgen/include/bigint.hpp index b0acbf2..e989e8f 100644 --- a/tools/powgen/include/bigint.hpp +++ b/tools/eisellemirepowgen/include/bigint.hpp @@ -22,8 +22,8 @@ * SOFTWARE. */ -#ifndef __POWGEN_BIGINT_HPP__ -#define __POWGEN_BIGINT_HPP__ +#ifndef __EISELLEMIREPOWGEN_BIGINT_HPP__ +#define __EISELLEMIREPOWGEN_BIGINT_HPP__ #include @@ -126,6 +126,18 @@ class BigInt return result; } + int bitLength () const + { + int length = static_cast (data.size ()) * 32; + uint32_t top = data.back (); + while (length > 0 && (top & 0x80000000) == 0) + { + top <<= 1; + --length; + } + return length; + } + Power getTop128 () const { if (data.empty () || (data.size () == 1 && data[0] == 0)) diff --git a/tools/powgen/src/main.cpp b/tools/eisellemirepowgen/src/main.cpp similarity index 96% rename from tools/powgen/src/main.cpp rename to tools/eisellemirepowgen/src/main.cpp index 99bfc7d..45a78ae 100644 --- a/tools/powgen/src/main.cpp +++ b/tools/eisellemirepowgen/src/main.cpp @@ -41,7 +41,8 @@ // ========================================================================= void version () { - std::cout << "powgen version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH << std::endl; + std::cout << "eisellemirepowgen version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH + << std::endl; } // ========================================================================= @@ -51,7 +52,7 @@ void version () void usage () { std::cout << "Usage" << std::endl; - std::cout << " powgen [options]" << std::endl; + std::cout << " eisellemirepowgen [options]" << std::endl; std::cout << std::endl; std::cout << "Options" << std::endl; std::cout << " -h show available options" << std::endl; diff --git a/tools/powgen/version.hpp.in b/tools/eisellemirepowgen/version.hpp.in similarity index 81% rename from tools/powgen/version.hpp.in rename to tools/eisellemirepowgen/version.hpp.in index 454e6b1..188c74c 100644 --- a/tools/powgen/version.hpp.in +++ b/tools/eisellemirepowgen/version.hpp.in @@ -22,11 +22,11 @@ * SOFTWARE. */ -#ifndef __POWGEN_VERSION_H__ -#define __POWGEN_VERSION_H__ +#ifndef __EISELLEMIREPOWGEN_VERSION_H__ +#define __EISELLEMIREPOWGEN_VERSION_H__ -#define VERSION_MAJOR "@POWGEN_VERSION_MAJOR@" -#define VERSION_MINOR "@POWGEN_VERSION_MINOR@" -#define VERSION_PATCH "@POWGEN_VERSION_PATCH@" +#define VERSION_MAJOR "@EISELLEMIREPOWGEN_VERSION_MAJOR@" +#define VERSION_MINOR "@EISELLEMIREPOWGEN_VERSION_MINOR@" +#define VERSION_PATCH "@EISELLEMIREPOWGEN_VERSION_PATCH@" #endif diff --git a/tools/grisupowgen/CMakeLists.txt b/tools/grisupowgen/CMakeLists.txt new file mode 100644 index 0000000..0c0bd79 --- /dev/null +++ b/tools/grisupowgen/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.22.1) + +set(GRISUPOWGEN_VERSION ${DCONV_VERSION}) + +set(GRISUPOWGEN_VERSION_MAJOR ${DCONV_VERSION_MAJOR}) +set(GRISUPOWGEN_VERSION_MINOR ${DCONV_VERSION_MINOR}) +set(GRISUPOWGEN_VERSION_PATCH ${DCONV_VERSION_PATCH}) + +message(STATUS "grisupowgen version: ${GRISUPOWGEN_VERSION}") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.hpp.in" "${CMAKE_CURRENT_SOURCE_DIR}/include/version.hpp") + +set(PRIVATE_HEADER_DIRS + src +) + +set(PUBLIC_HEADER_DIRS + include + ../eisellemirepowgen/include +) + +set(PUBLIC_HEADERS + include/version.hpp +) + +set(SOURCES + src/main.cpp +) + +add_executable(grisupowgen ${SOURCES}) +set_target_properties(grisupowgen PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") + +target_include_directories(grisupowgen PUBLIC ${PUBLIC_HEADER_DIRS}) +target_include_directories(grisupowgen PRIVATE ${PRIVATE_HEADER_DIRS}) + +install(TARGETS grisupowgen DESTINATION /bin) diff --git a/tools/grisupowgen/README.md b/tools/grisupowgen/README.md new file mode 100644 index 0000000..a34e7c2 --- /dev/null +++ b/tools/grisupowgen/README.md @@ -0,0 +1,61 @@ +# grisupowgen + +**grisupowgen** is a command-line tool that generates the **lookup +table** of cached powers of 10 used by the Grisu algorithms. + +Each power of 10 is stored as a normalized 64-bit mantissa rounded +to nearest and a binary exponent, so that `10^k ≈ mantissa × 2^exponent`. + +The tool outputs a fully self-contained C++ header containing: + +- `MIN_EXPONENT`, `MAX_EXPONENT` +- a `CachedPower { uint64_t mantissa; int exponent; }` structure +- a `constexpr CachedPower powers[]` lookup table + +## Usage + +| Option | Description | +|-----------------|---------------------------------------| +| `-h` | Show help message | +| `-l ` | Set lower exponent (default: `-343`) | +| `-u ` | Set upper exponent (default: `343`) | +| `-v` | Show program version | + +## Example + +Generate powers for exponents −200 through +200 and write them to +`cached_power.hpp`: + +``` bash +grisupowgen -l -200 -u 200 > cached_power.hpp +``` + +The generated header will contain definitions like: + +``` cpp +#ifndef __CACHED_POWER_HPP__ +#define __CACHED_POWER_HPP__ + +#include + +#define MIN_EXPONENT -200 +#define MAX_EXPONENT 200 + +struct CachedPower { + uint64_t mantissa; + int exponent; +}; + +constexpr CachedPower powers[] = { + {0xc3f490aa77bd60fd, -728}, + ... + ... + ... +}; + +#endif +``` + +## License + +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/tools/grisupowgen/src/main.cpp b/tools/grisupowgen/src/main.cpp new file mode 100644 index 0000000..3c9a1ee --- /dev/null +++ b/tools/grisupowgen/src/main.cpp @@ -0,0 +1,179 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "version.hpp" +#include "bigint.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +struct CachedPower +{ + uint64_t mantissa; + int exponent; +}; + +// ========================================================================= +// CLASS : +// METHOD : version +// ========================================================================= +void version () +{ + std::cout << "grisupowgen version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH << std::endl; +} + +// ========================================================================= +// CLASS : +// METHOD : usage +// ========================================================================= +void usage () +{ + std::cout << "Usage" << std::endl; + std::cout << " grisupowgen [options]" << std::endl; + std::cout << std::endl; + std::cout << "Options" << std::endl; + std::cout << " -h show available options" << std::endl; + std::cout << " -l lower lower exponent (default: -343)" << std::endl; + std::cout << " -u upper upper exponent (default: 343)" << std::endl; + std::cout << " -v print version" << std::endl; +} + +// ========================================================================= +// CLASS : +// METHOD : compute +// ========================================================================= +CachedPower compute (int exponent) +{ + BigInt val (1); + int shift = 0; + + if (exponent > 0) + { + for (int i = 0; i < exponent; ++i) + { + val.multiply (10); + } + } + else if (exponent < 0) + { + int absExp = -exponent; + shift = static_cast (absExp * std::log2 (10.0)) + 128; + val = BigInt::powerOfTwo (shift); + for (int i = 0; i < absExp; ++i) + { + val.divide (10); + } + } + + Power top = val.getTop128 (); + CachedPower power = {top.hi + (top.lo >> 63), val.bitLength () - 64 - shift}; + if (power.mantissa == 0) + { + power.mantissa = 1ULL << 63; + ++power.exponent; + } + return power; +} + +// ========================================================================= +// CLASS : +// METHOD : main +// ========================================================================= +int main (int argc, char* argv[]) +{ + int minExp = -343, maxExp = 343; + + int opt; + while ((opt = getopt (argc, argv, "hl:u:v")) != -1) + { + switch (opt) + { + case 'h': + usage (); + _exit (EXIT_SUCCESS); + case 'l': + minExp = std::stoi (optarg); + break; + case 'u': + maxExp = std::stoi (optarg); + break; + case 'v': + version (); + _exit (EXIT_SUCCESS); + default: + usage (); + _exit (EXIT_FAILURE); + } + } + + if (minExp > maxExp) + { + std::cerr << "min exponent must be less than max exponent" << std::endl; + _exit (EXIT_FAILURE); + } + + std::vector powers; + for (int exp = minExp; exp <= maxExp; ++exp) + { + powers.push_back (compute (exp)); + } + + std::cout << "#ifndef __CACHED_POWER_HPP__" << std::endl; + std::cout << "#define __CACHED_POWER_HPP__" << std::endl; + std::cout << std::endl; + + std::cout << "#include " << std::endl; + std::cout << std::endl; + + std::cout << "#define MIN_EXPONENT " << minExp << std::endl; + std::cout << "#define MAX_EXPONENT " << maxExp << std::endl; + std::cout << std::endl; + + std::cout << "struct CachedPower {" << std::endl; + std::cout << " uint64_t mantissa;" << std::endl; + std::cout << " int exponent;" << std::endl; + std::cout << "};" << std::endl; + std::cout << std::endl; + + std::cout << "constexpr CachedPower powers[] = {" << std::endl; + for (size_t i = 0; i < powers.size (); ++i) + { + std::cout << std::hex << " {0x" << std::setw (16) << std::setfill ('0') << powers[i].mantissa << ", " + << std::dec << powers[i].exponent << "}," << std::endl; + } + std::cout << "};" << std::endl; + std::cout << std::endl; + + std::cout << "#endif" << std::endl; + std::cout << std::endl; + + _exit (EXIT_SUCCESS); +} diff --git a/tools/grisupowgen/version.hpp.in b/tools/grisupowgen/version.hpp.in new file mode 100644 index 0000000..aaf71c7 --- /dev/null +++ b/tools/grisupowgen/version.hpp.in @@ -0,0 +1,32 @@ +/** + * MIT License + * + * Copyright (c) 2026 Mathieu Rabine + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef __GRISUPOWGEN_VERSION_H__ +#define __GRISUPOWGEN_VERSION_H__ + +#define VERSION_MAJOR "@GRISUPOWGEN_VERSION_MAJOR@" +#define VERSION_MINOR "@GRISUPOWGEN_VERSION_MINOR@" +#define VERSION_PATCH "@GRISUPOWGEN_VERSION_PATCH@" + +#endif diff --git a/tools/powgen/CMakeLists.txt b/tools/powgen/CMakeLists.txt deleted file mode 100644 index 717842a..0000000 --- a/tools/powgen/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -cmake_minimum_required(VERSION 3.22.1) - -set(POWGEN_VERSION ${DCONV_VERSION}) - -set(POWGEN_VERSION_MAJOR ${DCONV_VERSION_MAJOR}) -set(POWGEN_VERSION_MINOR ${DCONV_VERSION_MINOR}) -set(POWGEN_VERSION_PATCH ${DCONV_VERSION_PATCH}) - -message(STATUS "powgen version: ${POWGEN_VERSION}") -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.hpp.in" "${CMAKE_CURRENT_SOURCE_DIR}/include/version.hpp") - -set(PRIVATE_HEADER_DIRS - src -) - -set(PUBLIC_HEADER_DIRS - include -) - -set(PUBLIC_HEADERS - include/version.hpp - include/bigint.hpp -) - -set(SOURCES - src/main.cpp -) - -add_executable(powgen ${SOURCES}) -set_target_properties(powgen PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") - -target_include_directories(powgen PUBLIC ${PUBLIC_HEADER_DIRS}) -target_include_directories(powgen PRIVATE ${PRIVATE_HEADER_DIRS}) - -install(TARGETS powgen DESTINATION /bin)