#ifndef _C4_CHARCONV_HPP_
#define _C4_CHARCONV_HPP_

/** @file charconv.hpp Lightweight generic type-safe wrappers for
 * converting individual values to/from strings.
 */

#include "c4/language.hpp"
#include <inttypes.h>
#include <type_traits>
#include <climits>
#include <limits>
#include <utility>

#include "c4/config.hpp"
#include "c4/substr.hpp"
#include "c4/std/std_fwd.hpp"
#include "c4/memory_util.hpp"
#include "c4/szconv.hpp"

#ifndef C4CORE_NO_FAST_FLOAT
#   if (C4_CPP >= 17)
#       if defined(_MSC_VER)
#           if (C4_MSVC_VERSION >= C4_MSVC_VERSION_2019) // VS2017 and lower do not have these macros
#               include <charconv>
#               define C4CORE_HAVE_STD_TOCHARS 1
#               define C4CORE_HAVE_STD_FROMCHARS 0 // prefer fast_float with MSVC
#               define C4CORE_HAVE_FAST_FLOAT 1
#           else
#               define C4CORE_HAVE_STD_TOCHARS 0
#               define C4CORE_HAVE_STD_FROMCHARS 0
#               define C4CORE_HAVE_FAST_FLOAT 1
#           endif
#       else
#           if __has_include(<charconv>)
#               include <charconv>
#               if defined(__cpp_lib_to_chars)
#                   define C4CORE_HAVE_STD_TOCHARS 1
#                   define C4CORE_HAVE_STD_FROMCHARS 0 // glibc uses fast_float internally
#                   define C4CORE_HAVE_FAST_FLOAT 1
#               else
#                   define C4CORE_HAVE_STD_TOCHARS 0
#                   define C4CORE_HAVE_STD_FROMCHARS 0
#                   define C4CORE_HAVE_FAST_FLOAT 1
#               endif
#           else
#               define C4CORE_HAVE_STD_TOCHARS 0
#               define C4CORE_HAVE_STD_FROMCHARS 0
#               define C4CORE_HAVE_FAST_FLOAT 1
#           endif
#       endif
#   else
#       define C4CORE_HAVE_STD_TOCHARS 0
#       define C4CORE_HAVE_STD_FROMCHARS 0
#       define C4CORE_HAVE_FAST_FLOAT 1
#   endif
#   if C4CORE_HAVE_FAST_FLOAT
#include "fast_float/fast_float.h"
#   endif
#elif (C4_CPP >= 17)
#   define C4CORE_HAVE_FAST_FLOAT 0
#   if defined(_MSC_VER)
#       if (C4_MSVC_VERSION >= C4_MSVC_VERSION_2019) // VS2017 and lower do not have these macros
#           include <charconv>
#           define C4CORE_HAVE_STD_TOCHARS 1
#           define C4CORE_HAVE_STD_FROMCHARS 1
#       else
#           define C4CORE_HAVE_STD_TOCHARS 0
#           define C4CORE_HAVE_STD_FROMCHARS 0
#       endif
#   else
#       if __has_include(<charconv>)
#           include <charconv>
#           if defined(__cpp_lib_to_chars)
#               define C4CORE_HAVE_STD_TOCHARS 1
#               define C4CORE_HAVE_STD_FROMCHARS 1 // glibc uses fast_float internally
#           else
#               define C4CORE_HAVE_STD_TOCHARS 0
#               define C4CORE_HAVE_STD_FROMCHARS 0
#           endif
#       else
#           define C4CORE_HAVE_STD_TOCHARS 0
#           define C4CORE_HAVE_STD_FROMCHARS 0
#       endif
#   endif
#else
#   define C4CORE_HAVE_STD_TOCHARS 0
#   define C4CORE_HAVE_STD_FROMCHARS 0
#   define C4CORE_HAVE_FAST_FLOAT 0
#endif


#if !C4CORE_HAVE_STD_FROMCHARS
#include <cstdio>
#endif


#if defined(_MSC_VER) && !defined(__clang__)
#   pragma warning(push)
#   pragma warning(disable: 4996) // snprintf/scanf: this function or variable may be unsafe
#   if C4_MSVC_VERSION != C4_MSVC_VERSION_2017
#       pragma warning(disable: 4800) //'int': forcing value to bool 'true' or 'false' (performance warning)
#   endif
#elif defined(__clang__)
#   pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare"
#   pragma clang diagnostic ignored "-Wformat-nonliteral"
#   pragma clang diagnostic ignored "-Wdouble-promotion" // implicit conversion increases floating-point precision
#   pragma clang diagnostic ignored "-Wold-style-cast"
#elif defined(__GNUC__)
#   pragma GCC diagnostic push
#   pragma GCC diagnostic ignored "-Wformat-nonliteral"
#   pragma GCC diagnostic ignored "-Wdouble-promotion" // implicit conversion increases floating-point precision
#   pragma GCC diagnostic ignored "-Wuseless-cast"
#   pragma GCC diagnostic ignored "-Wold-style-cast"
#endif

#if defined(__clang__)
#define C4_NO_UBSAN_IOVRFLW __attribute__((no_sanitize("signed-integer-overflow")))
#elif defined(__GNUC__)
#if __GNUC__ > 7
#define C4_NO_UBSAN_IOVRFLW __attribute__((no_sanitize("signed-integer-overflow")))
#else
#define C4_NO_UBSAN_IOVRFLW
#endif
#else
#define C4_NO_UBSAN_IOVRFLW
#endif

// NOLINTBEGIN(hicpp-signed-bitwise)

namespace c4 {

/** @defgroup doc_charconv Charconv utilities
 *
 * Lightweight, very fast generic type-safe wrappers for converting
 * individual values to/from strings. These are the main generic
 * functions:
 *   - @ref doc_to_chars and its alias @ref xtoa(): implemented by calling @ref itoa() / @ref utoa() / @ref ftoa() / @ref dtoa() (or generically @ref xtoa())
 *   - @ref doc_from_chars and its alias @ref atox(): implemented by calling @ref atoi() / @ref atou() / @ref atof() / @ref atod() (or generically @ref atox())
 *   - @ref to_chars_sub()
 *   - @ref from_chars_first()
 *   - @ref xtoa() and @ref atox() are implemented in terms of @ref write_dec() / @ref read_dec() et al (see @ref doc_write / @ref doc_read())
 *
 * And also some modest brag is in order: these functions are really
 * fast: faster even than C++17 `std::to_chars()` and
 * `std::to_chars()`, and many dozens of times faster than the
 * iostream abominations.
 *
 * For example, here are some benchmark comparisons for @ref
 * doc_from_chars (link leads to the main project README, where these
 * results are shown more systematically).
 *
 * <table>
 * <caption id="atox-i64-results">atox,int64_t</caption>
 * <tr><th>g++12, linux <th>Visual Studio 2019
 * <tr><td> \image html linux-x86_64-gxx12.1-Release-c4core-bm-charconv-atox-mega_bytes_per_second-i64.png <td> \image html windows-x86_64-vs2019-Release-c4core-bm-charconv-atox-mega_bytes_per_second-i64.png
 * </table>
 *
 * <table>
 * <caption id="xtoa-i64-results">xtoa,int64_t</caption>
 * <tr><th>g++12, linux <th>Visual Studio 2019
 * <tr><td> \image html linux-x86_64-gxx12.1-Release-c4core-bm-charconv-xtoa-mega_bytes_per_second-i64.png <td> \image html windows-x86_64-vs2019-Release-c4core-bm-charconv-xtoa-mega_bytes_per_second-i64.png
 * </table>
 *
 * To parse floating point, c4core uses
 * [fastfloat](https://github.com/fastfloat/fast_float), which is
 * extremely fast, by an even larger factor:
 *
 * <table>
 * <caption id="atox-float-results">atox,float</caption>
 * <tr><th>g++12, linux <th>Visual Studio 2019
 * <tr><td> \image html linux-x86_64-gxx12.1-Release-c4core-bm-charconv-atof-mega_bytes_per_second-float.png <td> \image html windows-x86_64-vs2019-Release-c4core-bm-charconv-atof-mega_bytes_per_second-float.png
 * </table>
 *
 * @{
 */

#if C4CORE_HAVE_STD_TOCHARS
/** @warning Use only the symbol. Do not rely on the type or naked value of this enum. */
typedef enum : std::underlying_type<std::chars_format>::type {
    /** print the real number in floating point format (like %f) */
    FTOA_FLOAT = static_cast<std::underlying_type<std::chars_format>::type>(std::chars_format::fixed),
    /** print the real number in scientific format (like %e) */
    FTOA_SCIENT = static_cast<std::underlying_type<std::chars_format>::type>(std::chars_format::scientific),
    /** print the real number in flexible format (like %g) */
    FTOA_FLEX = static_cast<std::underlying_type<std::chars_format>::type>(std::chars_format::general),
    /** print the real number in hexadecimal format (like %a) */
    FTOA_HEXA = static_cast<std::underlying_type<std::chars_format>::type>(std::chars_format::hex),
} RealFormat_e;
#else
/** @warning Use only the symbol. Do not rely on the type or naked value of this enum. */
typedef enum : char {
    /** print the real number in floating point format (like %f) */
    FTOA_FLOAT = 'f',
    /** print the real number in scientific format (like %e) */
    FTOA_SCIENT = 'e',
    /** print the real number in flexible format (like %g) */
    FTOA_FLEX = 'g',
    /** print the real number in hexadecimal format (like %a) */
    FTOA_HEXA = 'a',
} RealFormat_e;
#endif

/** @cond dev */
/** in some platforms, int,unsigned int
 *  are not any of int8_t...int64_t and
 *  long,unsigned long are not any of uint8_t...uint64_t */
template<class T>
struct is_fixed_length
{
    enum : bool {
        /** true if T is one of the fixed length signed types */
        value_i = (std::is_integral<T>::value
                   && (std::is_same<T, int8_t>::value
                       || std::is_same<T, int16_t>::value
                       || std::is_same<T, int32_t>::value
                       || std::is_same<T, int64_t>::value)),
        /** true if T is one of the fixed length unsigned types */
        value_u = (std::is_integral<T>::value
                   && (std::is_same<T, uint8_t>::value
                       || std::is_same<T, uint16_t>::value
                       || std::is_same<T, uint32_t>::value
                       || std::is_same<T, uint64_t>::value)),
        /** true if T is one of the fixed length signed or unsigned types */
        value = value_i || value_u
    };
};
/** @endcond */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

#if defined(_MSC_VER) && !defined(__clang__)
#   pragma warning(push)
#elif defined(__clang__)
#   pragma clang diagnostic push
#elif defined(__GNUC__)
#   pragma GCC diagnostic push
#   pragma GCC diagnostic ignored "-Wconversion"
#   if __GNUC__ >= 6
#       pragma GCC diagnostic ignored "-Wnull-dereference"
#   endif
#endif

/** @cond dev */
namespace detail {

/* python command to get the values below:
def dec(v):
    return str(v)
for bits in (8, 16, 32, 64):
    imin, imax, umax = (-(1 << (bits - 1))), (1 << (bits - 1)) - 1, (1 << bits) - 1
    for vname, v in (("imin", imin), ("imax", imax), ("umax", umax)):
        for f in (bin, oct, dec, hex):
            print(f"{bits}b: {vname}={v} {f.__name__}: len={len(f(v)):2d}: {v} {f(v)}")
*/

// do not use the type as the template argument because in some
// platforms long!=int32 and long!=int64. Just use the numbytes
// which is more generic and spares lengthy SFINAE code.
template<size_t num_bytes, bool is_signed> struct charconv_digits_;
template<class T> using charconv_digits = charconv_digits_<sizeof(T), std::is_signed<T>::value>;

template<> struct charconv_digits_<1u, true> // int8_t
{
    enum : size_t {
        maxdigits_bin       = 1 + 2 + 8, // -128==-0b10000000
        maxdigits_oct       = 1 + 2 + 3, // -128==-0o200
        maxdigits_dec       = 1     + 3, // -128
        maxdigits_hex       = 1 + 2 + 2, // -128==-0x80
        maxdigits_bin_nopfx =         8, // -128==-0b10000000
        maxdigits_oct_nopfx =         3, // -128==-0o200
        maxdigits_dec_nopfx =         3, // -128
        maxdigits_hex_nopfx =         2, // -128==-0x80
    };
    // min values without sign!
    static constexpr csubstr min_value_dec() noexcept { return csubstr("128"); }
    static constexpr csubstr min_value_hex() noexcept { return csubstr("80"); }
    static constexpr csubstr min_value_oct() noexcept { return csubstr("200"); }
    static constexpr csubstr min_value_bin() noexcept { return csubstr("10000000"); }
    static constexpr csubstr max_value_dec() noexcept { return csubstr("127"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 3) || (str.len == 3 && str[0] <= '1')); }
};
template<> struct charconv_digits_<1u, false> // uint8_t
{
    enum : size_t {
        maxdigits_bin       = 2 + 8, // 255 0b11111111
        maxdigits_oct       = 2 + 3, // 255 0o377
        maxdigits_dec       =     3, // 255
        maxdigits_hex       = 2 + 2, // 255 0xff
        maxdigits_bin_nopfx =     8, // 255 0b11111111
        maxdigits_oct_nopfx =     3, // 255 0o377
        maxdigits_dec_nopfx =     3, // 255
        maxdigits_hex_nopfx =     2, // 255 0xff
    };
    static constexpr csubstr max_value_dec() noexcept { return csubstr("255"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 3) || (str.len == 3 && str[0] <= '3')); }
};
template<> struct charconv_digits_<2u, true> // int16_t
{
    enum : size_t {
        maxdigits_bin       = 1 + 2 + 16, // -32768 -0b1000000000000000
        maxdigits_oct       = 1 + 2 +  6, // -32768 -0o100000
        maxdigits_dec       = 1     +  5, // -32768 -32768
        maxdigits_hex       = 1 + 2 +  4, // -32768 -0x8000
        maxdigits_bin_nopfx =         16, // -32768 -0b1000000000000000
        maxdigits_oct_nopfx =          6, // -32768 -0o100000
        maxdigits_dec_nopfx =          5, // -32768 -32768
        maxdigits_hex_nopfx =          4, // -32768 -0x8000
    };
    // min values without sign!
    static constexpr csubstr min_value_dec() noexcept { return csubstr("32768"); }
    static constexpr csubstr min_value_hex() noexcept { return csubstr("8000"); }
    static constexpr csubstr min_value_oct() noexcept { return csubstr("100000"); }
    static constexpr csubstr min_value_bin() noexcept { return csubstr("1000000000000000"); }
    static constexpr csubstr max_value_dec() noexcept { return csubstr("32767"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 6)); }
};
template<> struct charconv_digits_<2u, false> // uint16_t
{
    enum : size_t {
        maxdigits_bin       = 2 + 16, // 65535 0b1111111111111111
        maxdigits_oct       = 2 +  6, // 65535 0o177777
        maxdigits_dec       =      6, // 65535 65535
        maxdigits_hex       = 2 +  4, // 65535 0xffff
        maxdigits_bin_nopfx =     16, // 65535 0b1111111111111111
        maxdigits_oct_nopfx =      6, // 65535 0o177777
        maxdigits_dec_nopfx =      6, // 65535 65535
        maxdigits_hex_nopfx =      4, // 65535 0xffff
    };
    static constexpr csubstr max_value_dec() noexcept { return csubstr("65535"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 6) || (str.len == 6 && str[0] <= '1')); }
};
template<> struct charconv_digits_<4u, true> // int32_t
{
    enum : size_t {
        maxdigits_bin       = 1 + 2 + 32, // len=35: -2147483648 -0b10000000000000000000000000000000
        maxdigits_oct       = 1 + 2 + 11, // len=14: -2147483648 -0o20000000000
        maxdigits_dec       = 1     + 10, // len=11: -2147483648 -2147483648
        maxdigits_hex       = 1 + 2 +  8, // len=11: -2147483648 -0x80000000
        maxdigits_bin_nopfx =         32, // len=35: -2147483648 -0b10000000000000000000000000000000
        maxdigits_oct_nopfx =         11, // len=14: -2147483648 -0o20000000000
        maxdigits_dec_nopfx =         10, // len=11: -2147483648 -2147483648
        maxdigits_hex_nopfx =          8, // len=11: -2147483648 -0x80000000
    };
    // min values without sign!
    static constexpr csubstr min_value_dec() noexcept { return csubstr("2147483648"); }
    static constexpr csubstr min_value_hex() noexcept { return csubstr("80000000"); }
    static constexpr csubstr min_value_oct() noexcept { return csubstr("20000000000"); }
    static constexpr csubstr min_value_bin() noexcept { return csubstr("10000000000000000000000000000000"); }
    static constexpr csubstr max_value_dec() noexcept { return csubstr("2147483647"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 11) || (str.len == 11 && str[0] <= '1')); }
};
template<> struct charconv_digits_<4u, false> // uint32_t
{
    enum : size_t {
        maxdigits_bin       = 2 + 32, // len=34: 4294967295 0b11111111111111111111111111111111
        maxdigits_oct       = 2 + 11, // len=13: 4294967295 0o37777777777
        maxdigits_dec       =     10, // len=10: 4294967295 4294967295
        maxdigits_hex       = 2 +  8, // len=10: 4294967295 0xffffffff
        maxdigits_bin_nopfx =     32, // len=34: 4294967295 0b11111111111111111111111111111111
        maxdigits_oct_nopfx =     11, // len=13: 4294967295 0o37777777777
        maxdigits_dec_nopfx =     10, // len=10: 4294967295 4294967295
        maxdigits_hex_nopfx =      8, // len=10: 4294967295 0xffffffff
    };
    static constexpr csubstr max_value_dec() noexcept { return csubstr("4294967295"); }
    static constexpr bool is_oct_overflow(csubstr str) noexcept { return !((str.len < 11) || (str.len == 11 && str[0] <= '3')); }
};
template<> struct charconv_digits_<8u, true> // int64_t
{
    enum : size_t {
        maxdigits_bin       = 1 + 2 + 64, // len=67: -9223372036854775808 -0b1000000000000000000000000000000000000000000000000000000000000000
        maxdigits_oct       = 1 + 2 + 22, // len=25: -9223372036854775808 -0o1000000000000000000000
        maxdigits_dec       = 1     + 19, // len=20: -9223372036854775808 -9223372036854775808
        maxdigits_hex       = 1 + 2 + 16, // len=19: -9223372036854775808 -0x8000000000000000
        maxdigits_bin_nopfx =         64, // len=67: -9223372036854775808 -0b1000000000000000000000000000000000000000000000000000000000000000
        maxdigits_oct_nopfx =         22, // len=25: -9223372036854775808 -0o1000000000000000000000
        maxdigits_dec_nopfx =         19, // len=20: -9223372036854775808 -9223372036854775808
        maxdigits_hex_nopfx =         16, // len=19: -9223372036854775808 -0x8000000000000000
    };
    static constexpr csubstr min_value_dec() noexcept { return csubstr("9223372036854775808"); }
    static constexpr csubstr min_value_hex() noexcept { return csubstr("8000000000000000"); }
    static constexpr csubstr min_value_oct() noexcept { return csubstr("1000000000000000000000"); }
    static constexpr csubstr min_value_bin() noexcept { return csubstr("1000000000000000000000000000000000000000000000000000000000000000"); }
    static constexpr csubstr max_value_dec() noexcept { return csubstr("9223372036854775807"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 22)); }
};
template<> struct charconv_digits_<8u, false> // uint64_t
{
    enum : size_t {
        maxdigits_bin       = 2 + 64, // len=66: 18446744073709551615 0b1111111111111111111111111111111111111111111111111111111111111111
        maxdigits_oct       = 2 + 22, // len=24: 18446744073709551615 0o1777777777777777777777
        maxdigits_dec       =     20, // len=20: 18446744073709551615 18446744073709551615
        maxdigits_hex       = 2 + 16, // len=18: 18446744073709551615 0xffffffffffffffff
        maxdigits_bin_nopfx =     64, // len=66: 18446744073709551615 0b1111111111111111111111111111111111111111111111111111111111111111
        maxdigits_oct_nopfx =     22, // len=24: 18446744073709551615 0o1777777777777777777777
        maxdigits_dec_nopfx =     20, // len=20: 18446744073709551615 18446744073709551615
        maxdigits_hex_nopfx =     16, // len=18: 18446744073709551615 0xffffffffffffffff
    };
    static constexpr csubstr max_value_dec() noexcept { return csubstr("18446744073709551615"); }
    static constexpr bool    is_oct_overflow(csubstr str) noexcept { return !((str.len < 22) || (str.len == 22 && str[0] <= '1')); }
};
} // namespace detail

// Helper macros, undefined below
#define _c4append(c) { if(C4_LIKELY(pos < buf.len)) { buf.str[pos++] = static_cast<char>(c); } else { ++pos; } }
#define _c4appendhex(i) { if(C4_LIKELY(pos < buf.len)) { buf.str[pos++] = hexchars[i]; } else { ++pos; } }

/** @endcond */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------


/** @defgroup doc_digits Get number of digits
 *
 * @note At first sight this code may look heavily branchy and
 * therefore inefficient. However, measurements revealed this to be
 * the fastest among the alternatives.
 *
 * @see https://github.com/biojppm/c4core/pull/77
 *
 * @{
 */

/** decimal digits for 8 bit integers */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE
auto digits_dec(T v) noexcept
    -> typename std::enable_if<sizeof(T) == 1u, unsigned>::type
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    return ((v >= 100) ? 3u : ((v >= 10) ? 2u : 1u));
}

/** decimal digits for 16 bit integers */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE
auto digits_dec(T v) noexcept
    -> typename std::enable_if<sizeof(T) == 2u, unsigned>::type
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    return ((v >= 10000) ? 5u : (v >= 1000) ? 4u : (v >= 100) ? 3u : (v >= 10) ? 2u : 1u);
}

/** decimal digits for 32 bit integers */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE
auto digits_dec(T v) noexcept
    -> typename std::enable_if<sizeof(T) == 4u, unsigned>::type
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    return ((v >= 1000000000) ? 10u : (v >= 100000000) ? 9u : (v >= 10000000) ? 8u :
            (v >= 1000000) ? 7u : (v >= 100000) ? 6u : (v >= 10000) ? 5u :
            (v >= 1000) ? 4u : (v >= 100) ? 3u : (v >= 10) ? 2u : 1u);
}

/** decimal digits for 64 bit integers */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE
auto digits_dec(T v) noexcept
    -> typename std::enable_if<sizeof(T) == 8u, unsigned>::type
{
    // thanks @fargies!!!
    // https://github.com/biojppm/c4core/pull/77#issuecomment-1063753568
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    if(v >= 1000000000) // 10
    {
        if(v >= 100000000000000) // 15 [15-20] range
        {
            if(v >= 100000000000000000) // 18 (15 + (20 - 15) / 2)
            {
                if((typename std::make_unsigned<T>::type)v >= 10000000000000000000u) // 20
                    return 20u;
                else
                    return (v >= 1000000000000000000) ? 19u : 18u;
            }
            else if(v >= 10000000000000000) // 17
                return 17u;
            else
                return(v >= 1000000000000000) ? 16u : 15u;
        }
        else if(v >= 1000000000000) // 13
            return (v >= 10000000000000) ? 14u : 13u;
        else if(v >= 100000000000) // 12
            return 12;
        else
            return(v >= 10000000000) ? 11u : 10u;
    }
    else if(v >= 10000) // 5 [5-9] range
    {
        if(v >= 10000000) // 8
            return (v >= 100000000) ? 9u : 8u;
        else if(v >= 1000000) // 7
            return 7;
        else
            return (v >= 100000) ? 6u : 5u;
    }
    else if(v >= 100)
        return (v >= 1000) ? 4u : 3u;
    else
        return (v >= 10) ? 2u : 1u;
}


/** return the number of digits required to encode an hexadecimal number. */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE unsigned digits_hex(T v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    return v ? 1u + (msb((typename std::make_unsigned<T>::type)v) >> 2u) : 1u;
}

/** return the number of digits required to encode a binary number. */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE unsigned digits_bin(T v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    return v ? 1u + msb((typename std::make_unsigned<T>::type)v) : 1u;
}

/** return the number of digits required to encode an octal number. */
template<class T>
C4_CONSTEXPR14 C4_ALWAYS_INLINE unsigned digits_oct(T v_) noexcept
{
    // TODO: is there a better way?
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v_ >= 0);
    using U = typename std::conditional<sizeof(T) <= sizeof(unsigned),
                                        unsigned,
                                        typename std::make_unsigned<T>::type>::type;
    U v = (U) v_;  // safe because we require v_ >= 0 // NOLINT
    uint32_t __n = 1;
    enum : U {
        __b2 = 64u,
        __b3 = 64u * 8u,
        __b4 = 64u * 8u * 8u,
    };
    while(true)
	{
        if(v < 8u)
            return __n;
        else if(v < __b2)
            return __n + 1;
        else if(v < __b3)
            return __n + 2;
        else if(v < __b4)
            return __n + 3;
        v /= (U) __b4;
        __n += 4;
	}
}

/** @} */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @cond dev */
namespace detail {
C4_INLINE_CONSTEXPR const char hexchars[] = "0123456789abcdef";
C4_INLINE_CONSTEXPR const char digits0099[] =
    "0001020304050607080910111213141516171819"
    "2021222324252627282930313233343536373839"
    "4041424344454647484950515253545556575859"
    "6061626364656667686970717273747576777879"
    "8081828384858687888990919293949596979899";
} // namespace detail
/** @endcond */

C4_SUPPRESS_WARNING_GCC_PUSH
C4_SUPPRESS_WARNING_GCC("-Warray-bounds")  // gcc has false positives here
#if (defined(__GNUC__) && (__GNUC__ >= 7))
C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow")  // gcc has false positives here
#endif

/** @defgroup doc_write_unchecked Write with known number of digits
 *
 * Writes a value without checking the buffer length with regards to
 * the required number of digits to encode the value. It is the
 * responsibility of the caller to ensure that the provided number of
 * digits is enough to write the given value. Notwithstanding the
 * name, assertions are liberally performed, so this code is safe.
 *
 * @{ */

template<class T>
C4_HOT C4_ALWAYS_INLINE
void write_dec_unchecked(substr buf, T v, unsigned digits_v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    C4_ASSERT(buf.len >= digits_v);
    C4_XASSERT(digits_v == digits_dec(v));
    // in bm_xtoa: checkoncelog_singlediv_write2
    while(v >= T(100))
    {
        T quo = v;
        quo /= T(100);
        const auto num = (v - quo * T(100)) << 1u; // NOLINT
        v = quo;
        buf.str[--digits_v] = detail::digits0099[num + 1];
        buf.str[--digits_v] = detail::digits0099[num];
    }
    if(v >= T(10))
    {
        C4_ASSERT(digits_v == 2);
        const auto num = v << 1u;
        buf.str[1] = detail::digits0099[num + 1];
        buf.str[0] = detail::digits0099[num];
    }
    else
    {
        C4_ASSERT(digits_v == 1);
        buf.str[0] = (char)('0' + v);
    }
}


template<class T>
C4_HOT C4_ALWAYS_INLINE
void write_hex_unchecked(substr buf, T v, unsigned digits_v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    C4_ASSERT(buf.len >= digits_v);
    C4_XASSERT(digits_v == digits_hex(v));
    do {
        buf.str[--digits_v] = detail::hexchars[v & T(15)];
        v >>= 4;
    } while(v);
    C4_ASSERT(digits_v == 0);
}


template<class T>
C4_HOT C4_ALWAYS_INLINE
void write_oct_unchecked(substr buf, T v, unsigned digits_v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    C4_ASSERT(buf.len >= digits_v);
    C4_XASSERT(digits_v == digits_oct(v));
    do {
        buf.str[--digits_v] = (char)('0' + (v & T(7)));
        v >>= 3;
    } while(v);
    C4_ASSERT(digits_v == 0);
}


template<class T>
C4_HOT C4_ALWAYS_INLINE
void write_bin_unchecked(substr buf, T v, unsigned digits_v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    C4_ASSERT(buf.len >= digits_v);
    C4_XASSERT(digits_v == digits_bin(v));
    do {
        buf.str[--digits_v] = (char)('0' + (v & T(1)));
        v >>= 1;
    } while(v);
    C4_ASSERT(digits_v == 0);
}

/** @} */ // write_unchecked


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @defgroup doc_write Write a value
 *
 * Writes a value without checking the buffer length
 * decimal number -- but asserting.
 *
 * @{ */

/** write an integer to a string in decimal format. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the required size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t write_dec(substr buf, T v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    unsigned digits = digits_dec(v);
    if(C4_LIKELY(buf.len >= digits))
        write_dec_unchecked(buf, v, digits);
    return digits;
}

/** write an integer to a string in hexadecimal format. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note does not prefix with 0x
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the required size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t write_hex(substr buf, T v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    unsigned digits = digits_hex(v);
    if(C4_LIKELY(buf.len >= digits))
        write_hex_unchecked(buf, v, digits);
    return digits;
}

/** write an integer to a string in octal format. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note does not prefix with 0o
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the required size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t write_oct(substr buf, T v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    unsigned digits = digits_oct(v);
    if(C4_LIKELY(buf.len >= digits))
        write_oct_unchecked(buf, v, digits);
    return digits;
}

/** write an integer to a string in binary format. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note does not prefix with 0b
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the required size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t write_bin(substr buf, T v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_ASSERT(v >= 0);
    unsigned digits = digits_bin(v);
    C4_ASSERT(digits > 0);
    if(C4_LIKELY(buf.len >= digits))
        write_bin_unchecked(buf, v, digits);
    return digits;
}


/** @cond dev */
namespace detail {
template<class U> using NumberWriter = size_t (*)(substr, U);
template<class T, NumberWriter<T> writer>
size_t write_num_digits(substr buf, T v, size_t num_digits) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    const size_t ret = writer(buf, v);
    if(ret >= num_digits)
        return ret;
    else if(ret >= buf.len || num_digits > buf.len)
        return num_digits;
    C4_ASSERT(num_digits >= ret);
    const size_t delta = static_cast<size_t>(num_digits - ret); // NOLINT
    C4_ASSERT(ret + delta <= buf.len);
    if(ret)
        memmove(buf.str + delta, buf.str, ret);
    if(delta)
        memset(buf.str, '0', delta);
    return num_digits;
}
} // namespace detail
/** @endcond */


/** same as c4::write_dec(), but pad with zeroes on the left
 * such that the resulting string is @p num_digits wide.
 * If the given number is requires more than num_digits, then the number prevails. */
template<class T>
C4_ALWAYS_INLINE size_t write_dec(substr buf, T val, size_t num_digits) noexcept
{
    return detail::write_num_digits<T, &write_dec<T>>(buf, val, num_digits);
}

/** same as c4::write_hex(), but pad with zeroes on the left
 * such that the resulting string is @p num_digits wide.
 * If the given number is requires more than num_digits, then the number prevails. */
template<class T>
C4_ALWAYS_INLINE size_t write_hex(substr buf, T val, size_t num_digits) noexcept
{
    return detail::write_num_digits<T, &write_hex<T>>(buf, val, num_digits);
}

/** same as c4::write_bin(), but pad with zeroes on the left
 * such that the resulting string is @p num_digits wide.
 * If the given number is requires more than num_digits, then the number prevails. */
template<class T>
C4_ALWAYS_INLINE size_t write_bin(substr buf, T val, size_t num_digits) noexcept
{
    return detail::write_num_digits<T, &write_bin<T>>(buf, val, num_digits);
}

/** same as c4::write_oct(), but pad with zeroes on the left
 * such that the resulting string is @p num_digits wide.
 * If the given number is requires more than num_digits, then the number prevails. */
template<class T>
C4_ALWAYS_INLINE size_t write_oct(substr buf, T val, size_t num_digits) noexcept
{
    return detail::write_num_digits<T, &write_oct<T>>(buf, val, num_digits);
}

/** @} */ // write

C4_SUPPRESS_WARNING_GCC_POP


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------


C4_SUPPRESS_WARNING_MSVC_PUSH
C4_SUPPRESS_WARNING_MSVC(4365) // '=': conversion from 'int' to 'I', signed/unsigned mismatch

/** @defgroup doc_read Read a value
 *
 * @{ */

/** read a decimal integer from a string. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note The string must be trimmed. Whitespace is not accepted.
 * @note the string must not be empty
 * @note there is no check for overflow; the value wraps around
 * in a way similar to the standard C/C++ overflow behavior.
 * For example, `read_dec<int8_t>("128", &val)` returns true
 * and val will be set to 0 because 127 is the max i8 value.
 * @see overflows<T>() to find out if a number string overflows a type range
 * @return true if the conversion was successful (no overflow check) */
template<class I>
C4_NO_UBSAN_IOVRFLW
C4_ALWAYS_INLINE bool read_dec(csubstr s, I *C4_RESTRICT v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<I>::value);
    C4_ASSERT(!s.empty());
    *v = 0;
    for(char c : s)
    {
        if(C4_UNLIKELY(c < '0' || c > '9'))
            return false;
        *v = (*v) * I(10) + (I(c) - I('0'));
    }
    return true;
}

/** read an hexadecimal integer from a string. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note does not accept leading 0x or 0X
 * @note the string must not be empty
 * @note the string must be trimmed. Whitespace is not accepted.
 * @note there is no check for overflow; the value wraps around
 * in a way similar to the standard C/C++ overflow behavior.
 * For example, `read_hex<int8_t>("80", &val)` returns true
 * and val will be set to 0 because 7f is the max i8 value.
 * @see overflows<T>() to find out if a number string overflows a type range
 * @return true if the conversion was successful (no overflow check) */
template<class I>
C4_NO_UBSAN_IOVRFLW
C4_ALWAYS_INLINE bool read_hex(csubstr s, I *C4_RESTRICT v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<I>::value);
    C4_ASSERT(!s.empty());
    *v = 0;
    for(char c : s)
    {
        I cv;
        if(c >= '0' && c <= '9')
            cv = I(c) - I('0');
        else if(c >= 'a' && c <= 'f')
            cv = I(10) + (I(c) - I('a'));
        else if(c >= 'A' && c <= 'F')
            cv = I(10) + (I(c) - I('A'));
        else
            return false;
        *v = (*v) * I(16) + cv;
    }
    return true;
}

/** read a binary integer from a string. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note does not accept leading 0b or 0B
 * @note the string must not be empty
 * @note the string must be trimmed. Whitespace is not accepted.
 * @note there is no check for overflow; the value wraps around
 * in a way similar to the standard C/C++ overflow behavior.
 * For example, `read_bin<int8_t>("10000000", &val)` returns true
 * and val will be set to 0 because 1111111 is the max i8 value.
 * @see overflows<T>() to find out if a number string overflows a type range
 * @return true if the conversion was successful (no overflow check) */
template<class I>
C4_NO_UBSAN_IOVRFLW
C4_ALWAYS_INLINE bool read_bin(csubstr s, I *C4_RESTRICT v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<I>::value);
    C4_ASSERT(!s.empty());
    *v = 0;
    for(char c : s)
    {
        *v <<= 1;
        if(c == '1')
            *v |= 1;
        else if(c != '0')
            return false;
    }
    return true;
}

/** read an octal integer from a string. This is the
 * lowest level (and the fastest) function to do this task.
 * @note does not accept negative numbers
 * @note does not accept leading 0o or 0O
 * @note the string must not be empty
 * @note the string must be trimmed. Whitespace is not accepted.
 * @note there is no check for overflow; the value wraps around
 * in a way similar to the standard C/C++ overflow behavior.
 * For example, `read_oct<int8_t>("200", &val)` returns true
 * and val will be set to 0 because 177 is the max i8 value.
 * @see overflows<T>() to find out if a number string overflows a type range
 * @return true if the conversion was successful (no overflow check) */
template<class I>
C4_NO_UBSAN_IOVRFLW
C4_ALWAYS_INLINE bool read_oct(csubstr s, I *C4_RESTRICT v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<I>::value);
    C4_ASSERT(!s.empty());
    *v = 0;
    for(char c : s)
    {
        if(C4_UNLIKELY(c < '0' || c > '7'))
            return false;
        *v = (*v) * I(8) + (I(c) - I('0'));
    }
    return true;
}

/** @} */

C4_SUPPRESS_WARNING_MSVC_POP


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wswitch-default")

/** @cond dev */
namespace detail {
inline size_t _itoa2buf(substr buf, size_t pos, csubstr val) noexcept
{
    C4_ASSERT(pos < buf.len);
    C4_ASSERT(pos + val.len <= buf.len);
    C4_ASSERT(val.len > 0);
    memcpy(buf.str + pos, val.str, val.len);
    return pos + val.len;
}
inline size_t _itoa2bufwithdigits(substr buf, size_t pos, size_t num_digits, csubstr val) noexcept
{
    num_digits = num_digits > val.len ? num_digits - val.len : 0;
    C4_ASSERT(num_digits + val.len <= buf.len);
    for(size_t i = 0; i < num_digits; ++i)
        _c4append('0');
    return detail::_itoa2buf(buf, pos, val);
}
template<class I>
C4_NO_INLINE size_t _itoadec2buf(substr buf) noexcept
{
    using digits_type = detail::charconv_digits<I>;
    if(C4_UNLIKELY(buf.len < digits_type::maxdigits_dec))
        return digits_type::maxdigits_dec;
    buf.str[0] = '-';
    return detail::_itoa2buf(buf, 1, digits_type::min_value_dec());
}
template<class I>
C4_NO_INLINE size_t _itoa2buf(substr buf, I radix) noexcept
{
    using digits_type = detail::charconv_digits<I>;
    size_t pos = 0;
    if(C4_LIKELY(buf.len > 0))
        buf.str[pos++] = '-';
    switch(radix) // NOLINT(hicpp-multiway-paths-covered)
    {
    case I(10):
        if(C4_UNLIKELY(buf.len < digits_type::maxdigits_dec))
            return digits_type::maxdigits_dec;
        pos =_itoa2buf(buf, pos, digits_type::min_value_dec());
        break;
    case I(16):
        if(C4_UNLIKELY(buf.len < digits_type::maxdigits_hex))
            return digits_type::maxdigits_hex;
        buf.str[pos++] = '0';
        buf.str[pos++] = 'x';
        pos = _itoa2buf(buf, pos, digits_type::min_value_hex());
        break;
    case I( 2):
        if(C4_UNLIKELY(buf.len < digits_type::maxdigits_bin))
            return digits_type::maxdigits_bin;
        buf.str[pos++] = '0';
        buf.str[pos++] = 'b';
        pos = _itoa2buf(buf, pos, digits_type::min_value_bin());
        break;
    case I( 8):
        if(C4_UNLIKELY(buf.len < digits_type::maxdigits_oct))
            return digits_type::maxdigits_oct;
        buf.str[pos++] = '0';
        buf.str[pos++] = 'o';
        pos = _itoa2buf(buf, pos, digits_type::min_value_oct());
        break;
    }
    return pos;
}
template<class I>
C4_NO_INLINE size_t _itoa2buf(substr buf, I radix, size_t num_digits) noexcept
{
    using digits_type = detail::charconv_digits<I>;
    size_t pos = 0;
    size_t needed_digits = 0;
    if(C4_LIKELY(buf.len > 0))
        buf.str[pos++] = '-';
    switch(radix) // NOLINT(hicpp-multiway-paths-covered)
    {
    case I(10):
        // add 1 to account for -
        needed_digits = num_digits+1 > digits_type::maxdigits_dec ? num_digits+1 : digits_type::maxdigits_dec;
        if(C4_UNLIKELY(buf.len < needed_digits))
            return needed_digits;
        pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_dec());
        break;
    case I(16):
        // add 3 to account for -0x
        needed_digits = num_digits+3 > digits_type::maxdigits_hex ? num_digits+3 : digits_type::maxdigits_hex;
        if(C4_UNLIKELY(buf.len < needed_digits))
            return needed_digits;
        buf.str[pos++] = '0';
        buf.str[pos++] = 'x';
        pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_hex());
        break;
    case I(2):
        // add 3 to account for -0b
        needed_digits = num_digits+3 > digits_type::maxdigits_bin ? num_digits+3 : digits_type::maxdigits_bin;
        if(C4_UNLIKELY(buf.len < needed_digits))
            return needed_digits;
        C4_ASSERT(buf.len >= digits_type::maxdigits_bin);
        buf.str[pos++] = '0';
        buf.str[pos++] = 'b';
        pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_bin());
        break;
    case I(8):
        // add 3 to account for -0o
        needed_digits = num_digits+3 > digits_type::maxdigits_oct ? num_digits+3 : digits_type::maxdigits_oct;
        if(C4_UNLIKELY(buf.len < needed_digits))
            return needed_digits;
        C4_ASSERT(buf.len >= digits_type::maxdigits_oct);
        buf.str[pos++] = '0';
        buf.str[pos++] = 'o';
        pos = _itoa2bufwithdigits(buf, pos, num_digits, digits_type::min_value_oct());
        break;
    }
    return pos;
}
} // namespace detail
/** @endcond */


/** @defgroup doc_itoa itoa: signed to chars
 *
 * @{ */

/** convert an integral signed decimal to a string.
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the needed size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t itoa(substr buf, T v) noexcept
{
    C4_STATIC_ASSERT(std::is_signed<T>::value);
    if(v >= T(0))
    {
        // write_dec() checks the buffer size, so no need to check here
        return write_dec(buf, v);
    }
    // when T is the min value (eg i8: -128), negating it
    // will overflow, so treat the min as a special case
    if(C4_LIKELY(v != std::numeric_limits<T>::min()))
    {
        v = -v;
        unsigned digits = digits_dec(v);
        if(C4_LIKELY(buf.len >= digits + 1u))
        {
            buf.str[0] = '-';
            write_dec_unchecked(buf.sub(1), v, digits);
        }
        return digits + 1u;
    }
    return detail::_itoadec2buf<T>(buf);
}

/** convert an integral signed integer to a string, using a specific
 * radix. The radix must be 2, 8, 10 or 16.
 *
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the needed size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t itoa(substr buf, T v, T radix) noexcept
{
    C4_STATIC_ASSERT(std::is_signed<T>::value);
    C4_ASSERT(radix == 2 || radix == 8 || radix == 10 || radix == 16);
    C4_SUPPRESS_WARNING_GCC_PUSH
    #if (defined(__GNUC__) && (__GNUC__ >= 7))
        C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow")  // gcc has a false positive here
    #endif
    // when T is the min value (eg i8: -128), negating it
    // will overflow, so treat the min as a special case
    if(C4_LIKELY(v != std::numeric_limits<T>::min()))
    {
        unsigned pos = 0;
        if(v < 0)
        {
            v = -v;
            if(C4_LIKELY(buf.len > 0))
                buf.str[pos] = '-';
            ++pos;
        }
        unsigned digits = 0;
        switch(radix) // NOLINT(hicpp-multiway-paths-covered)
        {
        case T(10):
            digits = digits_dec(v);
            if(C4_LIKELY(buf.len >= pos + digits))
                write_dec_unchecked(buf.sub(pos), v, digits);
            break;
        case T(16):
            digits = digits_hex(v);
            if(C4_LIKELY(buf.len >= pos + 2u + digits))
            {
                buf.str[pos + 0] = '0';
                buf.str[pos + 1] = 'x';
                write_hex_unchecked(buf.sub(pos + 2), v, digits);
            }
            digits += 2u;
            break;
        case T(2):
            digits = digits_bin(v);
            if(C4_LIKELY(buf.len >= pos + 2u + digits))
            {
                buf.str[pos + 0] = '0';
                buf.str[pos + 1] = 'b';
                write_bin_unchecked(buf.sub(pos + 2), v, digits);
            }
            digits += 2u;
            break;
        case T(8):
            digits = digits_oct(v);
            if(C4_LIKELY(buf.len >= pos + 2u + digits))
            {
                buf.str[pos + 0] = '0';
                buf.str[pos + 1] = 'o';
                write_oct_unchecked(buf.sub(pos + 2), v, digits);
            }
            digits += 2u;
            break;
        }
        return pos + digits;
    }
    C4_SUPPRESS_WARNING_GCC_POP
    // when T is the min value (eg i8: -128), negating it
    // will overflow
    return detail::_itoa2buf<T>(buf, radix);
}


/** same as c4::itoa(), but pad with zeroes on the left such that the
 * resulting string is @p num_digits wide, not accounting for radix
 * prefix (0x,0o,0b). The @p radix must be 2, 8, 10 or 16.
 *
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the needed size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t itoa(substr buf, T v, T radix, size_t num_digits) noexcept
{
    C4_STATIC_ASSERT(std::is_signed<T>::value);
    C4_ASSERT(radix == 2 || radix == 8 || radix == 10 || radix == 16);
    C4_SUPPRESS_WARNING_GCC_PUSH
    #if (defined(__GNUC__) && (__GNUC__ >= 7))
        C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow")  // gcc has a false positive here
    #endif
    // when T is the min value (eg i8: -128), negating it
    // will overflow, so treat the min as a special case
    if(C4_LIKELY(v != std::numeric_limits<T>::min()))
    {
        unsigned pos = 0;
        if(v < 0)
        {
            v = -v;
            if(C4_LIKELY(buf.len > 0))
                buf.str[pos] = '-';
            ++pos;
        }
        unsigned total_digits = 0;
        switch(radix) // NOLINT(hicpp-multiway-paths-covered)
        {
        case T(10):
            total_digits = digits_dec(v);
            total_digits = pos + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
            if(C4_LIKELY(buf.len >= total_digits))
                write_dec(buf.sub(pos), v, num_digits);
            break;
        case T(16):
            total_digits = digits_hex(v);
            total_digits = pos + 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
            if(C4_LIKELY(buf.len >= total_digits))
            {
                buf.str[pos + 0] = '0';
                buf.str[pos + 1] = 'x';
                write_hex(buf.sub(pos + 2), v, num_digits);
            }
            break;
        case T(2):
            total_digits = digits_bin(v);
            total_digits = pos + 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
            if(C4_LIKELY(buf.len >= total_digits))
            {
                buf.str[pos + 0] = '0';
                buf.str[pos + 1] = 'b';
                write_bin(buf.sub(pos + 2), v, num_digits);
            }
            break;
        case T(8):
            total_digits = digits_oct(v);
            total_digits = pos + 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
            if(C4_LIKELY(buf.len >= total_digits))
            {
                buf.str[pos + 0] = '0';
                buf.str[pos + 1] = 'o';
                write_oct(buf.sub(pos + 2), v, num_digits);
            }
            break;
        }
        return total_digits;
    }
    C4_SUPPRESS_WARNING_GCC_POP
    // when T is the min value (eg i8: -128), negating it
    // will overflow
    return detail::_itoa2buf<T>(buf, radix, num_digits);
}

/** @} */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @defgroup doc_utoa utoa: unsigned to chars
 *
 * @{ */

/** convert an integral unsigned decimal to a string.
 *
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the needed size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t utoa(substr buf, T v) noexcept
{
    C4_STATIC_ASSERT(std::is_unsigned<T>::value);
    // write_dec() does the buffer length check, so no need to check here
    return write_dec(buf, v);
}

/** convert an integral unsigned integer to a string, using a specific
 * radix. The radix must be 2, 8, 10 or 16.
 *
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the needed size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t utoa(substr buf, T v, T radix) noexcept
{
    C4_STATIC_ASSERT(std::is_unsigned<T>::value);
    C4_ASSERT(radix == 10 || radix == 16 || radix == 2 || radix == 8);
    unsigned digits = 0;
    switch(radix) // NOLINT(hicpp-multiway-paths-covered)
    {
    case T(10):
        digits = digits_dec(v);
        if(C4_LIKELY(buf.len >= digits))
            write_dec_unchecked(buf, v, digits);
        break;
    case T(16):
        digits = digits_hex(v);
        if(C4_LIKELY(buf.len >= digits+2u))
        {
            buf.str[0] = '0';
            buf.str[1] = 'x';
            write_hex_unchecked(buf.sub(2), v, digits);
        }
        digits += 2u;
        break;
    case T(2):
        digits = digits_bin(v);
        if(C4_LIKELY(buf.len >= digits+2u))
        {
            buf.str[0] = '0';
            buf.str[1] = 'b';
            write_bin_unchecked(buf.sub(2), v, digits);
        }
        digits += 2u;
        break;
    case T(8):
        digits = digits_oct(v);
        if(C4_LIKELY(buf.len >= digits+2u))
        {
            buf.str[0] = '0';
            buf.str[1] = 'o';
            write_oct_unchecked(buf.sub(2), v, digits);
        }
        digits += 2u;
        break;
    }
    return digits;
}

/** same as c4::utoa(), but pad with zeroes on the left such that the
 * resulting string is @p num_digits wide. The @p radix must be 2,
 * 8, 10 or 16.
 *
 * @note the resulting string is NOT zero-terminated.
 * @note it is ok to call this with an empty or too-small buffer;
 * no writes will occur, and the needed size will be returned
 * @return the number of characters required for the buffer. */
template<class T>
C4_ALWAYS_INLINE size_t utoa(substr buf, T v, T radix, size_t num_digits) noexcept
{
    C4_STATIC_ASSERT(std::is_unsigned<T>::value);
    C4_ASSERT(radix == 10 || radix == 16 || radix == 2 || radix == 8);
    unsigned total_digits = 0;
    switch(radix) // NOLINT(hicpp-multiway-paths-covered)
    {
    case T(10):
        total_digits = digits_dec(v);
        total_digits = (unsigned)(num_digits > total_digits ? num_digits : total_digits);
        if(C4_LIKELY(buf.len >= total_digits))
            write_dec(buf, v, num_digits);
        break;
    case T(16):
        total_digits = digits_hex(v);
        total_digits = 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
        if(C4_LIKELY(buf.len >= total_digits))
        {
            buf.str[0] = '0';
            buf.str[1] = 'x';
            write_hex(buf.sub(2), v, num_digits);
        }
        break;
    case T(2):
        total_digits = digits_bin(v);
        total_digits = 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
        if(C4_LIKELY(buf.len >= total_digits))
        {
            buf.str[0] = '0';
            buf.str[1] = 'b';
            write_bin(buf.sub(2), v, num_digits);
        }
        break;
    case T(8):
        total_digits = digits_oct(v);
        total_digits = 2u + (unsigned)(num_digits > total_digits ? num_digits : total_digits);
        if(C4_LIKELY(buf.len >= total_digits))
        {
            buf.str[0] = '0';
            buf.str[1] = 'o';
            write_oct(buf.sub(2), v, num_digits);
        }
        break;
    }
    return total_digits;
}
C4_SUPPRESS_WARNING_GCC_POP

/** @} */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @defgroup doc_atoi atoi: chars to signed
 *
 * @{ */

/** Convert a trimmed string to a signed integral value. The input
 * string can be formatted as decimal, binary (prefix 0b or 0B), octal
 * (prefix 0o or 0O) or hexadecimal (prefix 0x or 0X). Strings with
 * leading zeroes are considered as decimal and not octal (unlike the
 * C/C++ convention). Every character in the input string is read for
 * the conversion; the input string must not contain any leading or
 * trailing whitespace.
 *
 * @return true if the conversion was successful.
 *
 * @note a positive sign is not accepted. ie, the string must not
 * start with '+'
 *
 * @note overflow is not detected: the return status is true even if
 * the conversion would return a value outside of the type's range, in
 * which case the result will wrap around the type's range.  This is
 * similar to native behavior. See @ref doc_overflows and @ref
 * doc_overflow_checked for overflow checking utilities.
 *
 * @see atoi_first() if the string is not trimmed to the value to read. */
template<class T>
C4_NO_UBSAN_IOVRFLW
C4_ALWAYS_INLINE bool atoi(csubstr str, T * C4_RESTRICT v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    C4_STATIC_ASSERT(std::is_signed<T>::value);

    if(C4_UNLIKELY(str.len == 0))
        return false;

    C4_ASSERT(str.str[0] != '+');

    T sign = 1;
    size_t start = 0;
    if(str.str[0] == '-')
    {
        if(C4_UNLIKELY(str.len == ++start))
            return false;
        sign = -1;
    }

    bool parsed_ok = true;
    if(str.str[start] != '0') // this should be the common case, so put it first
    {
        parsed_ok = read_dec(str.sub(start), v);
    }
    else if(str.len > start + 1)
    {
        // starts with 0: is it 0x, 0o, 0b?
        const char pfx = str.str[start + 1];
        if(pfx == 'x' || pfx == 'X')
            parsed_ok = str.len > start + 2 && read_hex(str.sub(start + 2), v);
        else if(pfx == 'b' || pfx == 'B')
            parsed_ok = str.len > start + 2 && read_bin(str.sub(start + 2), v);
        else if(pfx == 'o' || pfx == 'O')
            parsed_ok = str.len > start + 2 && read_oct(str.sub(start + 2), v);
        else
            parsed_ok = read_dec(str.sub(start + 1), v);
    }
    else
    {
        parsed_ok = read_dec(str.sub(start), v);
    }
    if(C4_LIKELY(parsed_ok))
        *v *= sign;
    return parsed_ok;
}


/** Select the next range of characters in the string that can be parsed
 * as a signed integral value, and convert it using atoi(). Leading
 * whitespace (space, newline, tabs) is skipped.
 * @return the number of characters read for conversion, or csubstr::npos if the conversion failed
 * @see atoi() if the string is already trimmed to the value to read.
 * @see csubstr::first_int_span() */
template<class T>
C4_ALWAYS_INLINE size_t atoi_first(csubstr str, T * C4_RESTRICT v)
{
    csubstr trimmed = str.first_int_span();
    if(trimmed.len == 0)
        return csubstr::npos;
    if(atoi(trimmed, v))
        return static_cast<size_t>(trimmed.end() - str.begin());
    return csubstr::npos;
}

/** @} */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @defgroup doc_atou atou: chars to unsigned
 *
 * @{ */

/** Convert a trimmed string to an unsigned integral value. The string can be
 * formatted as decimal, binary (prefix 0b or 0B), octal (prefix 0o or 0O)
 * or hexadecimal (prefix 0x or 0X). Every character in the input string is read
 * for the conversion; it must not contain any leading or trailing whitespace.
 *
 * @return true if the conversion was successful.
 *
 * @note overflow is not detected: the return status is true even if
 * the conversion would return a value outside of the type's range, in
 * which case the result will wrap around the type's range. See @ref
 * doc_overflows and @ref doc_overflow_checked for overflow checking
 * utilities.
 *
 * @note If the string has a minus character, the return status
 * will be false.
 *
 * @see atou_first() if the string is not trimmed to the value to read. */
template<class T>
bool atou(csubstr str, T * C4_RESTRICT v) noexcept
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);

    if(C4_UNLIKELY(str.len == 0 || str.front() == '-'))
        return false;

    bool parsed_ok = true;
    if(str.str[0] != '0')
    {
        parsed_ok = read_dec(str, v);
    }
    else
    {
        if(str.len > 1)
        {
            const char pfx = str.str[1];
            if(pfx == 'x' || pfx == 'X')
                parsed_ok = str.len > 2 && read_hex(str.sub(2), v);
            else if(pfx == 'b' || pfx == 'B')
                parsed_ok = str.len > 2 && read_bin(str.sub(2), v);
            else if(pfx == 'o' || pfx == 'O')
                parsed_ok = str.len > 2 && read_oct(str.sub(2), v);
            else
                parsed_ok = read_dec(str, v);
        }
        else
        {
            *v = 0; // we know the first character is 0
        }
    }
    return parsed_ok;
}


/** Select the next range of characters in the string that can be parsed
 * as an unsigned integral value, and convert it using atou(). Leading
 * whitespace (space, newline, tabs) is skipped.
 * @return the number of characters read for conversion, or csubstr::npos if the conversion faileds
 * @see atou() if the string is already trimmed to the value to read.
 * @see csubstr::first_uint_span() */
template<class T>
C4_ALWAYS_INLINE size_t atou_first(csubstr str, T *v)
{
    csubstr trimmed = str.first_uint_span();
    if(trimmed.len == 0)
        return csubstr::npos;
    if(atou(trimmed, v))
        return static_cast<size_t>(trimmed.end() - str.begin());
    return csubstr::npos;
}


/** @} */

#if defined(_MSC_VER) && !defined(__clang__)
#   pragma warning(pop)
#elif defined(__clang__)
#   pragma clang diagnostic pop
#elif defined(__GNUC__)
#   pragma GCC diagnostic pop
#endif


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @cond dev */
namespace detail {
inline bool check_overflow(csubstr str, csubstr limit) noexcept
{
    if(str.len != limit.len)
        return str.len > limit.len;
    for(size_t i = 0; i < limit.len; ++i)
    {
        if(str[i] < limit[i])
            return false;
        else if(str[i] > limit[i])
            return true;
    }
    return false;
}
} // namespace detail
/** @endcond */


/** @defgroup doc_overflows overflows: does a number string overflow a type
 *
 * @{ */

/** Test if the following string would overflow when converted to
 * associated integral types; this function is dispatched with SFINAE
 * to handle differently signed and unsigned types.
 * @return true if number will overflow, false if it fits (or doesn't parse)
 * @see doc_overflow_checked for format specifiers to enforce no-overflow reads
 */
template<class T>
auto overflows(csubstr str) noexcept
    -> typename std::enable_if<std::is_unsigned<T>::value, bool>::type
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);

    if(C4_UNLIKELY(str.len == 0))
    {
        return false;
    }
    else if(str.str[0] == '0')
    {
        if (str.len == 1)
            return false;
        switch (str.str[1])
        {
            case 'x':
            case 'X':
            {
                size_t fno = str.first_not_of('0', 2);
                if (fno == csubstr::npos)
                    return false;
                return !(str.len <= fno + (sizeof(T) * 2));
            }
            case 'b':
            case 'B':
            {
                size_t fno = str.first_not_of('0', 2);
                if (fno == csubstr::npos)
                    return false;
                return !(str.len <= fno +(sizeof(T) * 8));
            }
            case 'o':
            case 'O':
            {
                size_t fno = str.first_not_of('0', 2);
                if(fno == csubstr::npos)
                    return false;
                return detail::charconv_digits<T>::is_oct_overflow(str.sub(fno));
            }
            default:
            {
                size_t fno = str.first_not_of('0', 1);
                if(fno == csubstr::npos)
                    return false;
                return detail::check_overflow(str.sub(fno), detail::charconv_digits<T>::max_value_dec());
            }
        }
    }
    else if(C4_UNLIKELY(str[0] == '-'))
    {
        return true;
    }
    else
    {
        return detail::check_overflow(str, detail::charconv_digits<T>::max_value_dec());
    }
}


/** Test if the following string would overflow when converted to
 * associated integral types; this function is dispatched with SFINAE
 * to handle differently signed and unsigned types.
 *
 * @return true if number will overflow, false if it fits (or doesn't parse)
 * @see doc_overflow_checked for format specifiers to enforce no-overflow reads
 */
template<class T>
auto overflows(csubstr str) noexcept
    -> typename std::enable_if<std::is_signed<T>::value, bool>::type
{
    C4_STATIC_ASSERT(std::is_integral<T>::value);
    if(C4_UNLIKELY(str.len == 0))
        return false;
    if(str.str[0] == '-')
    {
        if(str.str[1] == '0')
        {
            if(str.len == 2)
                return false;
            switch(str.str[2])
            {
                case 'x':
                case 'X':
                {
                    size_t fno = str.first_not_of('0', 3);
                    if (fno == csubstr::npos)
                        return false;
                    return detail::check_overflow(str.sub(fno), detail::charconv_digits<T>::min_value_hex());
                }
                case 'b':
                case 'B':
                {
                    size_t fno = str.first_not_of('0', 3);
                    if (fno == csubstr::npos)
                        return false;
                    return detail::check_overflow(str.sub(fno), detail::charconv_digits<T>::min_value_bin());
                }
                case 'o':
                case 'O':
                {
                    size_t fno = str.first_not_of('0', 3);
                    if(fno == csubstr::npos)
                        return false;
                    return detail::check_overflow(str.sub(fno), detail::charconv_digits<T>::min_value_oct());
                }
                default:
                {
                    size_t fno = str.first_not_of('0', 2);
                    if(fno == csubstr::npos)
                        return false;
                    return detail::check_overflow(str.sub(fno), detail::charconv_digits<T>::min_value_dec());
                }
            }
        }
        else
        {
            return detail::check_overflow(str.sub(1), detail::charconv_digits<T>::min_value_dec());
        }
    }
    else if(str.str[0] == '0')
    {
        if (str.len == 1)
            return false;
        switch(str.str[1])
        {
            case 'x':
            case 'X':
            {
                size_t fno = str.first_not_of('0', 2);
                if (fno == csubstr::npos)
                    return false;
                const size_t len = str.len - fno;
                return !((len < sizeof (T) * 2) || (len == sizeof(T) * 2 && str[fno] <= '7'));
            }
            case 'b':
            case 'B':
            {
                size_t fno = str.first_not_of('0', 2);
                if (fno == csubstr::npos)
                    return false;
                return !(str.len <= fno + (sizeof(T) * 8 - 1));
            }
            case 'o':
            case 'O':
            {
                size_t fno = str.first_not_of('0', 2);
                if(fno == csubstr::npos)
                    return false;
                return detail::charconv_digits<T>::is_oct_overflow(str.sub(fno));
            }
            default:
            {
                size_t fno = str.first_not_of('0', 1);
                if(fno == csubstr::npos)
                    return false;
                return detail::check_overflow(str.sub(fno), detail::charconv_digits<T>::max_value_dec());
            }
        }
    }
    else
    {
        return detail::check_overflow(str, detail::charconv_digits<T>::max_value_dec());
    }
}

/** @} */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------

/** @cond dev */
namespace detail {


#if (!C4CORE_HAVE_STD_FROMCHARS)
/** @see http://www.exploringbinary.com/ for many good examples on float-str conversion */
template<size_t N>
void get_real_format_str(char (& C4_RESTRICT fmt)[N], int precision, RealFormat_e formatting, const char* length_modifier="")
{
    int iret;
    if(precision == -1)
        iret = snprintf(fmt, sizeof(fmt), "%%%s%c", length_modifier, formatting);
    else if(precision == 0)
        iret = snprintf(fmt, sizeof(fmt), "%%.%s%c", length_modifier, formatting);
    else
        iret = snprintf(fmt, sizeof(fmt), "%%.%d%s%c", precision, length_modifier, formatting);
    C4_ASSERT(iret >= 2 && size_t(iret) < sizeof(fmt));
    C4_UNUSED(iret);
}


/** @todo we're depending on snprintf()/sscanf() for converting to/from
 * floating point numbers. Apparently, this increases the binary size
 * by a considerable amount. There are some lightweight printf
 * implementations:
 *
 * @see http://www.sparetimelabs.com/tinyprintf/tinyprintf.php (BSD)
 * @see https://github.com/weiss/c99-snprintf
 * @see https://github.com/nothings/stb/blob/master/stb_sprintf.h
 * @see http://www.exploringbinary.com/
 * @see https://blog.benoitblanchon.fr/lightweight-float-to-string/
 * @see http://www.ryanjuckett.com/programming/printing-floating-point-numbers/
 */
template<class T>
size_t print_one(substr str, const char* full_fmt, T v)
{
#ifdef _MSC_VER
    /** use _snprintf() to prevent early termination of the output
     * for writing the null character at the last position
     * @see https://msdn.microsoft.com/en-us/library/2ts7cx93.aspx */
    int iret = _snprintf(str.str, str.len, full_fmt, v);
    if(iret < 0)
    {
        /* when buf.len is not enough, VS returns a negative value.
         * so call it again with a negative value for getting an
         * actual length of the string */
        iret = snprintf(nullptr, 0, full_fmt, v);
        C4_ASSERT(iret > 0);
    }
    size_t ret = (size_t) iret;
    return ret;
#else
    int iret = snprintf(str.str, str.len, full_fmt, v);
    C4_ASSERT(iret >= 0);
    size_t ret = (size_t) iret;
    if(ret >= str.len)
        ++ret; /* snprintf() reserves the last character to write \0 */
    return ret;
#endif
}
#endif // (!C4CORE_HAVE_STD_FROMCHARS)


#if (!C4CORE_HAVE_STD_FROMCHARS) && (!C4CORE_HAVE_FAST_FLOAT)
/** scans a string using the given type format, while at the same time
 * allowing non-null-terminated strings AND guaranteeing that the given
 * string length is strictly respected, so that no buffer overflows
 * might occur. */
template<typename T>
inline size_t scan_one(csubstr str, const char *type_fmt, T *v)
{
    /* snscanf() is absolutely needed here as we must be sure that
     * str.len is strictly respected, because substr is
     * generally not null-terminated.
     *
     * Alas, there is no snscanf().
     *
     * So we fake it by using a dynamic format with an explicit
     * field size set to the length of the given span.
     * This trick is taken from:
     * https://stackoverflow.com/a/18368910/5875572 */

    /* this is the actual format we'll use for scanning */
    char fmt[16];

    /* write the length into it. Eg "%12f".
     * Also, get the number of characters read from the string.
     * So the final format ends up as "%12f%n"*/
    int iret = std::snprintf(fmt, sizeof(fmt), "%%" "%zu" "%s" "%%n", str.len, type_fmt);
    /* no nasty surprises, please! */
    C4_ASSERT(iret >= 0 && size_t(iret) < C4_COUNTOF(fmt));

    /* now we scan with confidence that the span length is respected */
    int num_chars;
    iret = std::sscanf(str.str, fmt, v, &num_chars);
    /* scanf returns the number of successful conversions */
    if(iret != 1) return csubstr::npos;
    C4_ASSERT(num_chars >= 0);
    return (size_t)(num_chars);
}
#endif // (!C4CORE_HAVE_STD_FROMCHARS) && (!C4CORE_HAVE_FAST_FLOAT)


#if C4CORE_HAVE_STD_TOCHARS
template<class T>
C4_ALWAYS_INLINE size_t rtoa(substr buf, T v, int precision=-1, RealFormat_e formatting=FTOA_FLEX) noexcept
{
    std::to_chars_result result;
    size_t pos = 0;
    if(formatting == FTOA_HEXA)
    {
        if(buf.len > size_t(2))
        {
            buf.str[0] = '0';
            buf.str[1] = 'x';
        }
        pos += size_t(2);
    }
    if(precision == -1)
        result = std::to_chars(buf.str + pos, buf.str + buf.len, v, (std::chars_format)formatting);
    else
        result = std::to_chars(buf.str + pos, buf.str + buf.len, v, (std::chars_format)formatting, precision);
    if(result.ec == std::errc())
    {
        // all good, no errors.
        C4_ASSERT(result.ptr >= buf.str);
        ptrdiff_t delta = result.ptr - buf.str;
        return static_cast<size_t>(delta);
    }
    C4_ASSERT(result.ec == std::errc::value_too_large);
    // This is unfortunate.
    //
    // When the result can't fit in the given buffer,
    // std::to_chars() returns the end pointer it was originally
    // given, which is useless because here we would like to know
    // _exactly_ how many characters the buffer must have to fit
    // the result.
    //
    // So we take the pessimistic view, and assume as many digits
    // as could ever be required:
    size_t ret = static_cast<size_t>(std::numeric_limits<T>::max_digits10);
    return ret > buf.len ? ret : buf.len + 1;
}
#endif // C4CORE_HAVE_STD_TOCHARS


#if C4CORE_HAVE_FAST_FLOAT
template<class T>
C4_ALWAYS_INLINE bool scan_rhex(csubstr s, T *C4_RESTRICT val) noexcept
{
    C4_ASSERT(s.len > 0);
    C4_ASSERT(s.str[0] != '-');
    C4_ASSERT(s.str[0] != '+');
    C4_ASSERT(!s.begins_with("0x"));
    C4_ASSERT(!s.begins_with("0X"));
    size_t pos = 0;
    // integer part
    for( ; pos < s.len; ++pos)
    {
        const char c = s.str[pos];
        if(c >= '0' && c <= '9')
            *val = *val * T(16) + T(c - '0');
        else if(c >= 'a' && c <= 'f')
            *val = *val * T(16) + T(c - 'a');
        else if(c >= 'A' && c <= 'F')
            *val = *val * T(16) + T(c - 'A');
        else if(c == '.')
        {
            ++pos;
            break; // follow on to mantissa
        }
        else if(c == 'p' || c == 'P')
        {
            ++pos;
            goto power; // no mantissa given, jump to power // NOLINT
        }
        else
        {
            return false;
        }
    }
    // mantissa
    {
        // 0.0625 == 1/16 == value of first digit after the comma
        for(T digit = T(0.0625); pos < s.len; ++pos, digit /= T(16)) // NOLINT
        {
            const char c = s.str[pos];
            if(c >= '0' && c <= '9')
                *val += digit * T(c - '0');
            else if(c >= 'a' && c <= 'f')
                *val += digit * T(c - 'a');
            else if(c >= 'A' && c <= 'F')
                *val += digit * T(c - 'A');
            else if(c == 'p' || c == 'P')
            {
                ++pos;
                goto power; // mantissa finished, jump to power // NOLINT
            }
            else
            {
                return false;
            }
        }
    }
    return true;
power:
    if(C4_LIKELY(pos < s.len))
    {
        if(s.str[pos] == '+') // atoi() cannot handle a leading '+'
            ++pos;
        if(C4_LIKELY(pos < s.len))
        {
            int16_t powval = {};
            if(C4_LIKELY(atoi(s.sub(pos), &powval)))
            {
                *val *= ipow<T, int16_t, 16>(powval);
                return true;
            }
        }
    }
    return false;
}
#endif

} // namespace detail
/** @endcond */


#undef _c4appendhex
#undef _c4append


/** @defgroup doc_ftoa ftoa: float32 to chars
 *
 * @{ */

/** Convert a single-precision real number to string.  The string will
 * in general be NOT null-terminated.  For FTOA_FLEX, \p precision is
 * the number of significand digits. Otherwise \p precision is the
 * number of decimals. It is safe to call this function with an empty
 * or too-small buffer.
 *
 * @return the size of the buffer needed to write the number
 */
C4_ALWAYS_INLINE size_t ftoa(substr str, float v, int precision=-1, RealFormat_e formatting=FTOA_FLEX) noexcept
{
#if C4CORE_HAVE_STD_TOCHARS
    return detail::rtoa(str, v, precision, formatting);
#else
    char fmt[16];
    detail::get_real_format_str(fmt, precision, formatting, /*length_modifier*/"");
    return detail::print_one(str, fmt, v);
#endif
}

/** @} */


/** @defgroup doc_dtoa dtoa: float64 to chars
 *
 * @{ */

/** Convert a double-precision real number to string.  The string will
 * in general be NOT null-terminated.  For FTOA_FLEX, \p precision is
 * the number of significand digits. Otherwise \p precision is the
 * number of decimals. It is safe to call this function with an empty
 * or too-small buffer.
 *
 * @return the size of the buffer needed to write the number
 */
C4_ALWAYS_INLINE size_t dtoa(substr str, double v, int precision=-1, RealFormat_e formatting=FTOA_FLEX) noexcept
{
#if C4CORE_HAVE_STD_TOCHARS
    return detail::rtoa(str, v, precision, formatting);
#else
    char fmt[16];
    detail::get_real_format_str(fmt, precision, formatting, /*length_modifier*/"l");
    return detail::print_one(str, fmt, v);
#endif
}

/** @} */


/** @defgroup doc_atof atof: chars to float32
 *
 * @{ */

/** Convert a string to a single precision real number.
 * The input string must be trimmed to the value, ie
 * no leading or trailing whitespace can be present.
 * @return true iff the conversion succeeded
 * @see atof_first() if the string is not trimmed
 */
C4_ALWAYS_INLINE bool atof(csubstr str, float * C4_RESTRICT v) noexcept
{
    C4_ASSERT(str.len > 0);
    C4_ASSERT(str.triml(" \r\t\n").len == str.len);
#if C4CORE_HAVE_FAST_FLOAT
    // fastfloat cannot parse hexadecimal floats
    bool isneg = (str.str[0] == '-');
    csubstr rem = str.sub(isneg || str.str[0] == '+');
    if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X'))))
    {
        fast_float::from_chars_result result;
        result = fast_float::from_chars(str.str, str.str + str.len, *v);
        return result.ec == std::errc();
    }
    else if(detail::scan_rhex(rem.sub(2), v))
    {
        *v *= isneg ? -1.f : 1.f;
        return true;
    }
    return false;
#elif C4CORE_HAVE_STD_FROMCHARS
    std::from_chars_result result;
    result = std::from_chars(str.str, str.str + str.len, *v);
    return result.ec == std::errc();
#else
    csubstr rem = str.sub(str.str[0] == '-' || str.str[0] == '+');
    if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X'))))
        return detail::scan_one(str, "f", v) != csubstr::npos;
    else
        return detail::scan_one(str, "a", v) != csubstr::npos;
#endif
}


/** Convert a string to a single precision real number.
 * Leading whitespace is skipped until valid characters are found.
 * @return the number of characters read from the string, or npos if
 * conversion was not successful or if the string was empty */
inline size_t atof_first(csubstr str, float * C4_RESTRICT v) noexcept
{
    csubstr trimmed = str.first_real_span();
    if(trimmed.len == 0)
        return csubstr::npos;
    if(atof(trimmed, v))
        return static_cast<size_t>(trimmed.end() - str.begin());
    return csubstr::npos;
}

/** @} */


/** @defgroup doc_atod atod: chars to float64
 *
 * @{ */

/** Convert a string to a double precision real number.
 * The input string must be trimmed to the value, ie
 * no leading or trailing whitespace can be present.
 * @return true iff the conversion succeeded
 * @see atod_first() if the string is not trimmed
 */
C4_ALWAYS_INLINE bool atod(csubstr str, double * C4_RESTRICT v) noexcept
{
    C4_ASSERT(str.len > 0);
    C4_ASSERT(str.triml(" \r\t\n").len == str.len);
#if C4CORE_HAVE_FAST_FLOAT
    // fastfloat cannot parse hexadecimal floats
    bool isneg = (str.str[0] == '-');
    csubstr rem = str.sub(isneg || str.str[0] == '+');
    if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X'))))
    {
        fast_float::from_chars_result result;
        result = fast_float::from_chars(str.str, str.str + str.len, *v);
        return result.ec == std::errc();
    }
    else if(detail::scan_rhex(rem.sub(2), v))
    {
        *v *= isneg ? -1. : 1.;
        return true;
    }
    return false;
#elif C4CORE_HAVE_STD_FROMCHARS
    std::from_chars_result result;
    result = std::from_chars(str.str, str.str + str.len, *v);
    return result.ec == std::errc();
#else
    csubstr rem = str.sub(str.str[0] == '-' || str.str[0] == '+');
    if(!(rem.len >= 2 && (rem.str[0] == '0' && (rem.str[1] == 'x' || rem.str[1] == 'X'))))
        return detail::scan_one(str, "lf", v) != csubstr::npos;
    else
        return detail::scan_one(str, "la", v) != csubstr::npos;
#endif
}


/** Convert a string to a double precision real number.
 * Leading whitespace is skipped until valid characters are found.
 * @return the number of characters read from the string, or npos if
 * conversion was not successful or if the string was empty */
inline size_t atod_first(csubstr str, double * C4_RESTRICT v) noexcept
{
    csubstr trimmed = str.first_real_span();
    if(trimmed.len == 0)
        return csubstr::npos;
    if(atod(trimmed, v))
        return static_cast<size_t>(trimmed.end() - str.begin());
    return csubstr::npos;
}

/** @} */


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// generic versions

/** @cond dev */
// on some platforms, (unsigned) int and (unsigned) long
// are not any of the fixed length types above
#define _C4_IF_NOT_FIXED_LENGTH_I(T, ty) typename std::enable_if<std::  is_signed<T>::value && !is_fixed_length<T>::value_i, ty>
#define _C4_IF_NOT_FIXED_LENGTH_U(T, ty) typename std::enable_if<std::is_unsigned<T>::value && !is_fixed_length<T>::value_u, ty>
/** @endcond*/


/** @defgroup doc_xtoa xtoa: generic value to chars
 *
 * Dispatches to the most appropriate and efficient conversion
 * function
 *
 * @{ */
C4_ALWAYS_INLINE size_t xtoa(substr s,  uint8_t v) noexcept { return write_dec(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v) noexcept { return write_dec(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v) noexcept { return write_dec(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v) noexcept { return write_dec(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s,   int8_t v) noexcept { return itoa(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int16_t v) noexcept { return itoa(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int32_t v) noexcept { return itoa(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int64_t v) noexcept { return itoa(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s,    float v) noexcept { return ftoa(s, v); }
C4_ALWAYS_INLINE size_t xtoa(substr s,   double v) noexcept { return dtoa(s, v); }

C4_ALWAYS_INLINE size_t xtoa(substr s,  uint8_t v,  uint8_t radix) noexcept { return utoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v, uint16_t radix) noexcept { return utoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v, uint32_t radix) noexcept { return utoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v, uint64_t radix) noexcept { return utoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s,   int8_t v,   int8_t radix) noexcept { return itoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int16_t v,  int16_t radix) noexcept { return itoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int32_t v,  int32_t radix) noexcept { return itoa(s, v, radix); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int64_t v,  int64_t radix) noexcept { return itoa(s, v, radix); }

C4_ALWAYS_INLINE size_t xtoa(substr s,  uint8_t v,  uint8_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v, uint16_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v, uint32_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v, uint64_t radix, size_t num_digits) noexcept { return utoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s,   int8_t v,   int8_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int16_t v,  int16_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int32_t v,  int32_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); }
C4_ALWAYS_INLINE size_t xtoa(substr s,  int64_t v,  int64_t radix, size_t num_digits) noexcept { return itoa(s, v, radix, num_digits); }

C4_ALWAYS_INLINE size_t xtoa(substr s,  float v, int precision, RealFormat_e formatting=FTOA_FLEX) noexcept { return ftoa(s, v, precision, formatting); }
C4_ALWAYS_INLINE size_t xtoa(substr s, double v, int precision, RealFormat_e formatting=FTOA_FLEX) noexcept { return dtoa(s, v, precision, formatting); }

template <class T> C4_ALWAYS_INLINE auto xtoa(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type { return itoa(buf, v); }
template <class T> C4_ALWAYS_INLINE auto xtoa(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type { return write_dec(buf, v); }
template <class T>
C4_ALWAYS_INLINE size_t xtoa(substr s, T *v) noexcept { return itoa(s, (intptr_t)v, (intptr_t)16); }

/** @} */

/** @defgroup doc_atox atox: generic chars to value
 *
 * Dispatches to the most appropriate and efficient conversion
 * function
 *
 * @{ */

C4_ALWAYS_INLINE bool atox(csubstr s,  uint8_t *C4_RESTRICT v) noexcept { return atou(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s, uint16_t *C4_RESTRICT v) noexcept { return atou(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s, uint32_t *C4_RESTRICT v) noexcept { return atou(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s, uint64_t *C4_RESTRICT v) noexcept { return atou(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s,   int8_t *C4_RESTRICT v) noexcept { return atoi(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s,  int16_t *C4_RESTRICT v) noexcept { return atoi(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s,  int32_t *C4_RESTRICT v) noexcept { return atoi(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s,  int64_t *C4_RESTRICT v) noexcept { return atoi(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s,    float *C4_RESTRICT v) noexcept { return atof(s, v); }
C4_ALWAYS_INLINE bool atox(csubstr s,   double *C4_RESTRICT v) noexcept { return atod(s, v); }

template <class T> C4_ALWAYS_INLINE auto atox(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, bool)::type { return atoi(buf, v); }
template <class T> C4_ALWAYS_INLINE auto atox(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, bool)::type { return atou(buf, v); }
template <class T>
C4_ALWAYS_INLINE bool atox(csubstr s, T **v) noexcept { intptr_t tmp; bool ret = atox(s, &tmp); if(ret) { *v = (T*)tmp; } return ret; }

/** @} */


/** @defgroup doc_to_chars to_chars: generalized chars to value
 *
 * Convert the given value, writing into the string.  The resulting
 * string will NOT be null-terminated.  Return the number of
 * characters needed.  This function is safe to call when the string
 * is too small - no writes will occur beyond the string's last
 * character.
 *
 * Dispatches to the most appropriate and efficient conversion
 * function.
 *
 * @see write_dec, doc_utoa, doc_itoa, doc_ftoa, doc_dtoa
 *
 * @warning When serializing floating point values (float or double),
 * be aware that because it uses defaults, to_chars() may cause a
 * truncation of the precision. To enforce a particular precision, use
 * for example @ref c4::fmt::real, or call directly @ref c4::ftoa or
 * @ref c4::dtoa.
 *
 * @{ */

C4_ALWAYS_INLINE size_t to_chars(substr buf,  uint8_t v) noexcept { return write_dec(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf, uint16_t v) noexcept { return write_dec(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf, uint32_t v) noexcept { return write_dec(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf, uint64_t v) noexcept { return write_dec(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf,   int8_t v) noexcept { return itoa(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf,  int16_t v) noexcept { return itoa(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf,  int32_t v) noexcept { return itoa(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf,  int64_t v) noexcept { return itoa(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf,    float v) noexcept { return ftoa(buf, v); }
C4_ALWAYS_INLINE size_t to_chars(substr buf,   double v) noexcept { return dtoa(buf, v); }

template <class T> C4_ALWAYS_INLINE auto to_chars(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type { return itoa(buf, v); }
template <class T> C4_ALWAYS_INLINE auto to_chars(substr buf, T v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type { return write_dec(buf, v); }
template <class T>
C4_ALWAYS_INLINE size_t to_chars(substr s, T *v) noexcept { return itoa(s, (intptr_t)v, (intptr_t)16); }

/** @} */


/** @defgroup doc_from_chars from_chars: generalized chars to value
 *
 * Read a value from the string, which must be trimmed to the value
 * (ie, no leading/trailing whitespace).  return true if the
 * conversion succeeded.  There is no check for overflow; the value
 * wraps around in a way similar to the standard C/C++ overflow
 * behavior. For example, from_chars<int8_t>("128", &val) returns true
 * and val will be set tot 0. See @ref doc_overflows and @ref
 * doc_overflow_checked for facilities enforcing no-overflow.
 *
 * Dispatches to the most appropriate and efficient conversion
 * function
 *
 * @see doc_from_chars_first, atou, atoi, atof, atod
 * @{ */

C4_ALWAYS_INLINE bool from_chars(csubstr buf,  uint8_t *C4_RESTRICT v) noexcept { return atou(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint16_t *C4_RESTRICT v) noexcept { return atou(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint32_t *C4_RESTRICT v) noexcept { return atou(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint64_t *C4_RESTRICT v) noexcept { return atou(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf,   int8_t *C4_RESTRICT v) noexcept { return atoi(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf,  int16_t *C4_RESTRICT v) noexcept { return atoi(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf,  int32_t *C4_RESTRICT v) noexcept { return atoi(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf,  int64_t *C4_RESTRICT v) noexcept { return atoi(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf,    float *C4_RESTRICT v) noexcept { return atof(buf, v); }
C4_ALWAYS_INLINE bool from_chars(csubstr buf,   double *C4_RESTRICT v) noexcept { return atod(buf, v); }

template <class T> C4_ALWAYS_INLINE auto from_chars(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, bool)::type { return atoi(buf, v); }
template <class T> C4_ALWAYS_INLINE auto from_chars(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, bool)::type { return atou(buf, v); }
template <class T>
C4_ALWAYS_INLINE bool from_chars(csubstr buf, T **v) noexcept { intptr_t tmp; bool ret = from_chars(buf, &tmp); if(ret) { *v = (T*)tmp; } return ret; }

/** @defgroup doc_from_chars_first from_chars_first: generalized chars to value
 *
 * Read the first valid sequence of characters from the string,
 * skipping leading whitespace, and convert it using @ref doc_from_chars .
 * Return the number of characters read for converting.
 *
 * Dispatches to the most appropriate and efficient conversion
 * function.
 *
 * @see atou_first, atoi_first, atof_first, atod_first
 * @{ */

C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,  uint8_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint16_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint32_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint64_t *C4_RESTRICT v) noexcept { return atou_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,   int8_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,  int16_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,  int32_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,  int64_t *C4_RESTRICT v) noexcept { return atoi_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,    float *C4_RESTRICT v) noexcept { return atof_first(buf, v); }
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf,   double *C4_RESTRICT v) noexcept { return atod_first(buf, v); }

template <class T> C4_ALWAYS_INLINE auto from_chars_first(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type { return atoi_first(buf, v); }
template <class T> C4_ALWAYS_INLINE auto from_chars_first(csubstr buf, T *C4_RESTRICT v) noexcept -> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type { return atou_first(buf, v); }
template <class T>
C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, T **v) noexcept { intptr_t tmp; bool ret = from_chars_first(buf, &tmp); if(ret) { *v = (T*)tmp; } return ret; }

/** @} */

/** @} */

#undef _C4_IF_NOT_FIXED_LENGTH_I
#undef _C4_IF_NOT_FIXED_LENGTH_U


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/** call to_chars() and return a substr consisting of the
 * written portion of the input buffer. Ie, same as to_chars(),
 * but return a substr instead of a size_t.
 * Convert the given value to a string using to_chars(), and
 * return the resulting string, up to and including the last
 * written character.
 * @ingroup doc_to_chars
 * @see to_chars() */
template<class T>
C4_ALWAYS_INLINE substr to_chars_sub(substr buf, T const& C4_RESTRICT v) noexcept
{
    size_t sz = to_chars(buf, v);
    return buf.left_of(sz <= buf.len ? sz : buf.len);
}

//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// bool implementation

/** @ingroup doc_to_chars */
C4_ALWAYS_INLINE size_t to_chars(substr buf, bool v) noexcept
{
    int val = v;
    return to_chars(buf, val);
}

/** @ingroup doc_from_chars */
inline bool from_chars(csubstr buf, bool * C4_RESTRICT v) noexcept
{
    if(buf == '0')
    {
        *v = false; return true;
    }
    else if(buf == '1')
    {
        *v = true; return true;
    }
    else if(buf == "false")
    {
        *v = false; return true;
    }
    else if(buf == "true")
    {
        *v = true; return true;
    }
    else if(buf == "False")
    {
        *v = false; return true;
    }
    else if(buf == "True")
    {
        *v = true; return true;
    }
    else if(buf == "FALSE")
    {
        *v = false; return true;
    }
    else if(buf == "TRUE")
    {
        *v = true; return true;
    }
    // fallback to c-style int bools
    int val = 0;
    bool ret = from_chars(buf, &val);
    if(C4_LIKELY(ret))
    {
        *v = (val != 0);
    }
    return ret;
}

/** @ingroup doc_from_chars_first */
inline size_t from_chars_first(csubstr buf, bool * C4_RESTRICT v) noexcept
{
    csubstr trimmed = buf.first_non_empty_span();
    if(trimmed.len == 0 || !from_chars(buf, v))
        return csubstr::npos;
    return trimmed.len;
}


//-----------------------------------------------------------------------------
// single-char implementation

/** @ingroup doc_to_chars */
inline size_t to_chars(substr buf, char v) noexcept
{
    if(buf.len > 0)
    {
        C4_XASSERT(buf.str);
        buf.str[0] = v;
    }
    return 1;
}

/** extract a single character from a substring
 * @note to extract a string instead and not just a single character, use the csubstr overload
 * @ingroup doc_from_chars
 * */
inline bool from_chars(csubstr buf, char * C4_RESTRICT v) noexcept
{
    if(buf.len != 1)
        return false;
    C4_XASSERT(buf.str);
    *v = buf.str[0];
    return true;
}

/** @ingroup doc_from_chars_first */
inline size_t from_chars_first(csubstr buf, char * C4_RESTRICT v) noexcept
{
    if(buf.len < 1)
        return csubstr::npos;
    *v = buf.str[0];
    return 1;
}


//-----------------------------------------------------------------------------
// csubstr implementation

/** @ingroup doc_to_chars */
inline size_t to_chars(substr buf, csubstr v) noexcept
{
    C4_ASSERT(!buf.overlaps(v));
    size_t len = buf.len < v.len ? buf.len : v.len;
    // calling memcpy with null strings is undefined behavior
    // and will wreak havoc in calling code's branches.
    // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637
    if(len)
    {
        C4_ASSERT(buf.str != nullptr);
        C4_ASSERT(v.str != nullptr);
        memcpy(buf.str, v.str, len);
    }
    return v.len;
}

/** @ingroup doc_from_chars */
inline bool from_chars(csubstr buf, csubstr *C4_RESTRICT v) noexcept
{
    *v = buf;
    return true;
}

/** @ingroup doc_from_chars_first */
inline size_t from_chars_first(substr buf, csubstr * C4_RESTRICT v) noexcept
{
    csubstr trimmed = buf.first_non_empty_span();
    if(trimmed.len == 0)
        return csubstr::npos;
    *v = trimmed;
    return static_cast<size_t>(trimmed.end() - buf.begin());
}


//-----------------------------------------------------------------------------
// substr

/** @ingroup doc_to_chars */
inline size_t to_chars(substr buf, substr v) noexcept
{
    C4_ASSERT(!buf.overlaps(v));
    size_t len = buf.len < v.len ? buf.len : v.len;
    // calling memcpy with null strings is undefined behavior
    // and will wreak havoc in calling code's branches.
    // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637
    if(len)
    {
        C4_ASSERT(buf.str != nullptr);
        C4_ASSERT(v.str != nullptr);
        memcpy(buf.str, v.str, len);
    }
    return v.len;
}

/** @ingroup doc_from_chars */
inline bool from_chars(csubstr buf, substr * C4_RESTRICT v) noexcept
{
    C4_ASSERT(!buf.overlaps(*v));
    // is the destination buffer wide enough?
    if(v->len >= buf.len)
    {
        // calling memcpy with null strings is undefined behavior
        // and will wreak havoc in calling code's branches.
        // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637
        if(buf.len)
        {
            C4_ASSERT(buf.str != nullptr);
            C4_ASSERT(v->str != nullptr);
            memcpy(v->str, buf.str, buf.len);
        }
        v->len = buf.len;
        return true;
    }
    return false;
}

/** @ingroup doc_from_chars_first */
inline size_t from_chars_first(csubstr buf, substr * C4_RESTRICT v) noexcept
{
    csubstr trimmed = buf.first_non_empty_span();
    C4_ASSERT(!trimmed.overlaps(*v));
    if(C4_UNLIKELY(trimmed.len == 0))
        return csubstr::npos;
    size_t len = trimmed.len > v->len ? v->len : trimmed.len;
    // calling memcpy with null strings is undefined behavior
    // and will wreak havoc in calling code's branches.
    // see https://github.com/biojppm/rapidyaml/pull/264#issuecomment-1262133637
    if(len)
    {
        C4_ASSERT(buf.str != nullptr);
        C4_ASSERT(v->str != nullptr);
        memcpy(v->str, trimmed.str, len);
    }
    if(C4_UNLIKELY(trimmed.len > v->len))
        return csubstr::npos;
    return static_cast<size_t>(trimmed.end() - buf.begin());
}


//-----------------------------------------------------------------------------

/** @ingroup doc_to_chars */
template<size_t N>
inline size_t to_chars(substr buf, const char (& C4_RESTRICT v)[N]) noexcept
{
    csubstr sp(v);
    return to_chars(buf, sp);
}

/** @ingroup doc_to_chars */
inline size_t to_chars(substr buf, const char * C4_RESTRICT v) noexcept
{
    return to_chars(buf, to_csubstr(v));
}

/** @} */

} // namespace c4

// NOLINTEND(hicpp-signed-bitwise)

#if defined(_MSC_VER) && !defined(__clang__)
#   pragma warning(pop)
#elif defined(__clang__)
#   pragma clang diagnostic pop
#elif defined(__GNUC__)
#   pragma GCC diagnostic pop
#endif

#endif /* _C4_CHARCONV_HPP_ */
