From 10ccc6e62a7b7280ae599c6798a77cf793de175c Mon Sep 17 00:00:00 2001 From: Oskars Rubenis <104319900+Okarss@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:24:31 +0300 Subject: [PATCH] Float conversion with dynamic scaling (#263) * Float conversion with dynamic scaling Complete rework of the floating point conversion code. * Fix C++ and portability issues * Add rounding for dynamic scaling * Swap the integer and fraction parts * Adjust the unit tests * Fix OOR errors and Wconversion, NULL warnings * Retarget the submodule for testing * Add the new unit tests * Fix the unit test issues * Update the documentation * Change the error text and retarget the submodule to main --------- Co-authored-by: Charles Nicholson --- CMakeLists.txt | 5 +- README.md | 80 ++++---- nanoprintf.h | 377 ++++++++++++++++++++------------------ tests/doctest_main.cc | 6 +- tests/mpaland-conformance | 2 +- tests/unit_fsplit_abs.cc | 73 -------- tests/unit_ftoa_rev.cc | 82 ++++++++- tests/unit_ftoa_rev_08.cc | 76 ++++++++ tests/unit_ftoa_rev_16.cc | 81 ++++++++ tests/unit_ftoa_rev_32.cc | 81 ++++++++ tests/unit_ftoa_rev_64.cc | 81 ++++++++ tests/unit_nanoprintf.h | 19 +- 12 files changed, 658 insertions(+), 305 deletions(-) delete mode 100644 tests/unit_fsplit_abs.cc create mode 100644 tests/unit_ftoa_rev_08.cc create mode 100644 tests/unit_ftoa_rev_16.cc create mode 100644 tests/unit_ftoa_rev_32.cc create mode 100644 tests/unit_ftoa_rev_64.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 74fe7ea..aebc264 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,9 +203,12 @@ set(unit_test_files tests/unit_binary.cc tests/unit_bufputc.cc tests/unit_ftoa_rev.cc + tests/unit_ftoa_rev_08.cc + tests/unit_ftoa_rev_16.cc + tests/unit_ftoa_rev_32.cc + tests/unit_ftoa_rev_64.cc tests/unit_itoa_rev.cc tests/unit_utoa_rev.cc - tests/unit_fsplit_abs.cc tests/unit_snprintf.cc tests/unit_snprintf_safe_empty.cc tests/unit_vpprintf.cc) diff --git a/README.md b/README.md index 7c28968..a4b393a 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ [![](https://img.shields.io/badge/license-public_domain-brightgreen.svg)](https://github.com/charlesnicholson/nanoprintf/blob/master/LICENSE) [![](https://img.shields.io/badge/license-0BSD-brightgreen)](https://github.com/charlesnicholson/nanoprintf/blob/master/LICENSE) -nanoprintf is an unencumbered implementation of snprintf and vsnprintf for embedded systems that, when fully enabled, aim for C11 standard compliance. The primary exceptions are `double` (they get casted to `float`), scientific notation (`%e`, `%g`, `%a`), and the conversions that require `wcrtomb` to exist. C23 binary integer output is optionally supported as per [N2630](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2630.pdf). Safety extensions for snprintf and vsnprintf can be optionally configured to return trimmed or fully-empty strings on buffer overflow events. +nanoprintf is an unencumbered implementation of snprintf and vsnprintf for embedded systems that, when fully enabled, aim for C11 standard compliance. The primary exceptions are floating-point, scientific notation (`%e`, `%g`, `%a`), and the conversions that require `wcrtomb` to exist. C23 binary integer output is optionally supported as per [N2630](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2630.pdf). Safety extensions for snprintf and vsnprintf can be optionally configured to return trimmed or fully-empty strings on buffer overflow events. Additionally, nanoprintf can be used to parse printf-style format strings to extract the various parameters and conversion specifiers, without doing any actual text formatting. -nanoprintf makes no memory allocations and uses less than 100 bytes of stack. It compiles to between *~760-2500 bytes of object code* on a Cortex-M0 architecture, depending on configuration. +nanoprintf makes no memory allocations and uses less than 100 bytes of stack. It compiles to between *~800-2700 bytes of object code* on a Cortex-M0 architecture, depending on configuration. -All code is written in a minimal dialect of C99 for maximal compiler compatibility, compiles cleanly at the highest warning levels on clang + gcc + msvc, raises no issues from UBsan or Asan, and is exhaustively tested on 32-bit and 64-bit architectures. nanoprintf does include C standard headers but only uses them for C99 types and argument lists; no calls are made into stdlib / libc, with the exception of any internal double-to-float conversion ABI calls your compiler might emit. As usual, some Windows-specific headers are required if you're compiling natively for msvc. +All code is written in a minimal dialect of C99 for maximal compiler compatibility, compiles cleanly at the highest warning levels on clang + gcc + msvc, raises no issues from UBsan or Asan, and is exhaustively tested on 32-bit and 64-bit architectures. nanoprintf does include C standard headers but only uses them for C99 types and argument lists; no calls are made into stdlib / libc, with the exception of any internal large integer arithmetic calls your compiler might emit. As usual, some Windows-specific headers are required if you're compiling natively for msvc. nanoprintf is a [single header file](https://github.com/charlesnicholson/nanoprintf/blob/master/nanoprintf.h) in the style of the [stb libraries](https://github.com/nothings/stb). The rest of the repository is tests and scaffolding and not required for use. -nanoprintf is statically configurable so users can find a balance between size, compiler requirements, and feature set. Floating point conversion, "large" length modifiers, and size write-back are all configurable and are only compiled if explicitly requested, see [Configuration](https://github.com/charlesnicholson/nanoprintf#configuration) for details. +nanoprintf is statically configurable so users can find a balance between size, compiler requirements, and feature set. Floating-point conversion, "large" length modifiers, and size write-back are all configurable and are only compiled if explicitly requested, see [Configuration](https://github.com/charlesnicholson/nanoprintf#configuration) for details. ## Usage @@ -93,6 +93,12 @@ If no configuration flags are specified, nanoprintf will default to "reasonable" If a disabled format specifier feature is used, no conversion will occur and the format specifier string simply will be printed instead. +### Floating-Point Conversion +nanoprintf has the following floating-point specific configuration defines. + +* `NANOPRINTF_CONVERSION_BUFFER_SIZE`: Optional, defaults to `23`. Sets the size of a character buffer used for storing the converted value. Set to a larger number to enable printing of floating-point numbers with more characters. The buffer size does include the integer part, the fraction part and the decimal separator, but does not include the sign and the padding characters. If the number does not fit into buffer, an `err` is printed. Be careful with large sizes as the conversion buffer is allocated on stack memory. +* `NANOPRINTF_CONVERSION_FLOAT_TYPE`: Optional, defaults to `unsigned int`. Sets the integer type used for float conversion algorithm, which determines the conversion accuracy. Can be set to any unsigned integer type, like for example `uint64_t` or `uint8_t`. + ### Sprintf Safety By default, npf_snprintf and npf_vsnprintf behave according to the C Standard: the provided buffer will be filled but not overrun. If the string would have overrun the buffer, a null-terminator byte will be written to the final byte of the buffer. If the buffer is `null` or zero-sized, no bytes will be written. @@ -127,7 +133,7 @@ Like `printf`, `nanoprintf` expects a conversion specification string of the fol None or more of the following: * `h`: Use `short` for integral and write-back vararg width. - * `L`: Use `long double` for float vararg width (note: it will then be casted down to `float`) + * `L`: Use `long double` for float vararg width (note: it will then be casted down to `double`) * `l`: Use `long`, `double`, or wide vararg width. * `hh`: Use `char` for integral and write-back vararg width. * `ll`: (large specifier) Use `long long` for integral and write-back vararg width. @@ -152,11 +158,11 @@ Like `printf`, `nanoprintf` expects a conversion specification string of the fol * `a`/`A`: Floating-point hex (unimplemented, prints float decimal) * `b`/`B`: Binary integers -## Floating Point +## Floating-Point -Floating point conversion is performed by extracting the value into 64:64 fixed-point with an extra field that specifies the number of leading zero fractional digits before the first nonzero digit. This is done for simplicity, speed, and code footprint. +Floating-point conversion is performed by extracting the integer and fraction parts of the number into two separate integer variables. For each part the exponent is then scaled from base-2 to base-10 by iteratively multiplying and dividing the mantissa by 2 and 5 appropriately. The order of the scaling operations is selected dynamically (depending on value) to retain as much of the most significant bits of the mantissa as possible. The further the value is away from the decimal separator, the more of an error the scaling will accumulate. With a conversion integer type width of `N` bits on average the algorithm retains `N - log2(5)` or `N - 2.322` bits of accuracy. In addition integer parts up to `2 ^^ N - 1` and fraction parts with up to `N - 2.322` bits after the decimal separator are converted perfectly without loosing any bits. -Because the float -> fixed code operates on the raw float value bits, no floating point operations are performed. This allows nanoprintf to efficiently format floats on soft-float architectures like Cortex-M0, and to function identically with or without optimizations like "fast math". Despite `nano` in the name, there's no way to do away with double entirely, since the C language standard says that floats are promoted to double any time they're passed into variadic argument lists. nanoprintf casts all doubles back down to floats before doing any conversions. No other single- or double- precision operations are performed. +Because the float -> fixed code operates on the raw float value bits, no floating-point operations are performed. This allows nanoprintf to efficiently format floats on soft-float architectures like Cortex-M0, to function identically with or without optimizations like "fast math", and to minimize the code footprint. The `%e`/`%E`, `%a`/`%A`, and `%g`/`%G` specifiers are parsed but not formatted. If used, the output will be identical to if `%f`/`%F` was used. Pull requests welcome! :) @@ -179,11 +185,11 @@ arm-none-eabi-nm --print-size --size-sort npf.o 00000014 00000002 t npf_bufputc_nop 00000016 00000010 t npf_putc_cnt 00000000 00000014 t npf_bufputc -00000298 00000016 T npf_pprintf -000002e0 00000016 T npf_snprintf -000002ae 00000032 T npf_vsnprintf -00000026 00000272 T npf_vpprintf -Total size: 0x2f6 (758) bytes +000002b6 00000016 T npf_pprintf +00000314 00000016 T npf_snprintf +000002cc 00000048 T npf_vsnprintf +00000026 00000290 T npf_vpprintf +Total size: 0x32a (810) bytes Configuration "Binary": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - @@ -191,11 +197,11 @@ arm-none-eabi-nm --print-size --size-sort npf.o 00000014 00000002 t npf_bufputc_nop 00000016 00000010 t npf_putc_cnt 00000000 00000014 t npf_bufputc -000002de 00000016 T npf_pprintf -00000328 00000016 T npf_snprintf -000002f4 00000034 T npf_vsnprintf -00000026 000002b8 T npf_vpprintf -Total size: 0x33e (830) bytes +000002f2 00000016 T npf_pprintf +00000350 00000016 T npf_snprintf +00000308 00000048 T npf_vsnprintf +00000026 000002cc T npf_vpprintf +Total size: 0x366 (870) bytes Configuration "Field Width + Precision": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - @@ -203,10 +209,10 @@ arm-none-eabi-nm --print-size --size-sort npf.o 00000014 00000002 t npf_bufputc_nop 00000016 00000010 t npf_putc_cnt 00000000 00000014 t npf_bufputc -00000546 00000016 T npf_pprintf +00000534 00000016 T npf_pprintf 00000590 00000016 T npf_snprintf -0000055c 00000034 T npf_vsnprintf -00000026 00000520 T npf_vpprintf +0000054a 00000046 T npf_vsnprintf +00000026 0000050e T npf_vpprintf Total size: 0x5a6 (1446) bytes Configuration "Field Width + Precision + Binary": @@ -215,11 +221,11 @@ arm-none-eabi-nm --print-size --size-sort npf.o 00000014 00000002 t npf_bufputc_nop 00000016 00000010 t npf_putc_cnt 00000000 00000014 t npf_bufputc -00000590 00000016 T npf_pprintf -000005d8 00000016 T npf_snprintf -000005a6 00000032 T npf_vsnprintf -00000026 0000056a T npf_vpprintf -Total size: 0x5ee (1518) bytes +0000058c 00000016 T npf_pprintf +000005e8 00000016 T npf_snprintf +000005a2 00000046 T npf_vsnprintf +00000026 00000566 T npf_vpprintf +Total size: 0x5fe (1534) bytes Configuration "Float": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - @@ -227,11 +233,11 @@ arm-none-eabi-nm --print-size --size-sort npf.o 00000014 00000002 t npf_bufputc_nop 00000016 00000010 t npf_putc_cnt 00000000 00000014 t npf_bufputc -0000059c 00000016 T npf_pprintf -000005e4 00000016 T npf_snprintf -000005b2 00000032 T npf_vsnprintf -00000026 00000576 T npf_vpprintf -Total size: 0x5fa (1530) bytes +0000067c 00000016 T npf_pprintf +000006d8 00000016 T npf_snprintf +00000692 00000046 T npf_vsnprintf +00000026 00000656 T npf_vpprintf +Total size: 0x6ee (1774) bytes Configuration "Everything": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=1 - @@ -239,11 +245,11 @@ arm-none-eabi-nm --print-size --size-sort npf.o 00000014 00000002 t npf_bufputc_nop 00000016 00000010 t npf_putc_cnt 00000000 00000014 t npf_bufputc -00000934 00000016 T npf_pprintf -0000097c 00000016 T npf_snprintf -0000094a 00000032 T npf_vsnprintf -00000026 0000090e T npf_vpprintf -Total size: 0x992 (2450) bytes +00000a24 00000016 T npf_pprintf +00000a80 00000016 T npf_snprintf +00000a3a 00000046 T npf_vsnprintf +00000026 000009fe T npf_vpprintf +Total size: 0xa96 (2710) bytes ``` ## Development @@ -265,7 +271,7 @@ One test suite is a fork from the [printf test suite](), which is MIT licensed. ## Acknowledgments -I implemented Float-to-int conversion using the ideas from [Wojciech Muła](mailto:zdjęcia@garnek.pl)'s [float -> 64:64 fixed algorithm](http://0x80.pl/notesen/2015-12-29-float-to-string.html). +The basic idea of float-to-int conversion was inspired by [Wojciech Muła](mailto:zdjęcia@garnek.pl)'s [float -> 64:64 fixed algorithm](http://0x80.pl/notesen/2015-12-29-float-to-string.html) and extended further by adding dynamic scaling and configurable integer width by [Oskars Rubenis](https://github.com/Okarss). I ported the [printf test suite](https://github.com/eyalroz/printf/blob/master/test/test_suite.cpp) to nanoprintf. It was originally from the [mpaland printf project](https://github.com/mpaland/printf) codebase but adopted and improved by [Eyal Rozenberg](https://github.com/eyalroz) and others. (Nanoprintf has many of its own tests, but these are also very thorough and very good!) diff --git a/nanoprintf.h b/nanoprintf.h index 23e4f05..e9fc971 100644 --- a/nanoprintf.h +++ b/nanoprintf.h @@ -62,8 +62,17 @@ NPF_VISIBILITY int npf_vpprintf( #ifndef NANOPRINTF_IMPLEMENTATION_INCLUDED #define NANOPRINTF_IMPLEMENTATION_INCLUDED -#include +#include #include +#include + +// The conversion buffer must fit at least UINT64_MAX in octal format with the leading '0'. +#ifndef NANOPRINTF_CONVERSION_BUFFER_SIZE + #define NANOPRINTF_CONVERSION_BUFFER_SIZE 23 +#endif +#if NANOPRINTF_CONVERSION_BUFFER_SIZE < 23 + #error The size of the conversion buffer must be at least 23 bytes. +#endif // Pick reasonable defaults if nothing's been configured. #if !defined(NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS) && \ @@ -153,6 +162,7 @@ NPF_VISIBILITY int npf_vpprintf( #pragma GCC diagnostic ignored "-Wc++98-compat-pedantic" #pragma GCC diagnostic ignored "-Wcovered-switch-default" #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" + #pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" #ifndef __APPLE__ #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #endif @@ -163,16 +173,20 @@ NPF_VISIBILITY int npf_vpprintf( #ifdef _MSC_VER #pragma warning(push) - #pragma warning(disable:4514) // unreferenced inline function removed - #pragma warning(disable:4505) // unreferenced function removed - #pragma warning(disable:4701) // possibly uninitialized - #pragma warning(disable:4706) // assignment in conditional - #pragma warning(disable:4710) // not inlined - #pragma warning(disable:4711) // selected for inline - #pragma warning(disable:4820) // padding after data member - #pragma warning(disable:5039) // extern "C" throw - #pragma warning(disable:5045) // spectre mitigation + #pragma warning(disable:4619) // there is no warning number 'number' + // C4619 has to be disabled first! + #pragma warning(disable:4127) // conditional expression is constant + #pragma warning(disable:4505) // unreferenced local function has been removed + #pragma warning(disable:4514) // unreferenced inline function has been removed + #pragma warning(disable:4701) // potentially uninitialized local variable used + #pragma warning(disable:4706) // assignment within conditional expression + #pragma warning(disable:4710) // function not inlined + #pragma warning(disable:4711) // function selected for inline expansion + #pragma warning(disable:4820) // padding added after struct member + #pragma warning(disable:5039) // potentially throwing function passed to extern C function + #pragma warning(disable:5045) // compiler will insert Spectre mitigation for memory load #pragma warning(disable:5262) // implicit switch fall-through + #pragma warning(disable:26812) // enum type is unscoped #endif #if (NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1) || \ @@ -263,11 +277,7 @@ static int npf_itoa_rev(char *buf, npf_int_t i); static int npf_utoa_rev(char *buf, npf_uint_t i, unsigned base, unsigned case_adjust); #if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 -static int npf_fsplit_abs(float f, - uint64_t *out_int_part, - uint64_t *out_frac_part, - int *out_frac_base10_neg_e); -static int npf_ftoa_rev(char *buf, float f, npf_format_spec_t const *spec, int *out_frac_chars); +static int npf_ftoa_rev(char *buf, npf_format_spec_t const *spec, double f); #endif #if NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS == 1 @@ -488,134 +498,192 @@ int npf_utoa_rev(char *buf, npf_uint_t i, unsigned base, unsigned case_adj) { } #if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 -enum { - NPF_MANTISSA_BITS = 23, - NPF_EXPONENT_BITS = 8, - NPF_EXPONENT_BIAS = 127, - NPF_FRACTION_BIN_DIGITS = 64, - NPF_MAX_FRACTION_DEC_DIGITS = 9 -}; -int npf_fsplit_abs(float f, uint64_t *out_int_part, uint64_t *out_frac_part, - int *out_frac_base10_neg_exp) { - /* conversion algorithm by Wojciech Muła (zdjęcia@garnek.pl) - http://0x80.pl/notesen/2015-12-29-float-to-string.html - grisu2 (https://bit.ly/2JgMggX) and ryu (https://bit.ly/2RLXSg0) - are fast + precise + round, but require large lookup tables. */ +#include + +#if (DBL_MANT_DIG <= 11) && (DBL_MAX_EXP <= 16) + typedef uint_fast16_t npf_double_bin_t; + typedef int_fast8_t npf_ftoa_exp_t; +#elif (DBL_MANT_DIG <= 24) && (DBL_MAX_EXP <= 128) + typedef uint_fast32_t npf_double_bin_t; + typedef int_fast8_t npf_ftoa_exp_t; +#elif (DBL_MANT_DIG <= 53) && (DBL_MAX_EXP <= 1024) + typedef uint_fast64_t npf_double_bin_t; + typedef int_fast16_t npf_ftoa_exp_t; +#else + #error Unsupported width of the double type. +#endif - uint32_t f_bits; { // union-cast is UB, let compiler optimize byte-copy loop. - char const *src = (char const *)&f; - char *dst = (char *)&f_bits; - for (unsigned i = 0; i < sizeof(f_bits); ++i) { dst[i] = src[i]; } - } +// The floating point conversion code works with an unsigned integer type of any size. +#ifndef NANOPRINTF_CONVERSION_FLOAT_TYPE + #define NANOPRINTF_CONVERSION_FLOAT_TYPE unsigned int +#endif +typedef NANOPRINTF_CONVERSION_FLOAT_TYPE npf_ftoa_man_t; - int const exponent = - ((int)((f_bits >> NPF_MANTISSA_BITS) & ((1u << NPF_EXPONENT_BITS) - 1u)) - - NPF_EXPONENT_BIAS) - NPF_MANTISSA_BITS; +#if (NANOPRINTF_CONVERSION_BUFFER_SIZE <= UINT_FAST8_MAX) && (UINT_FAST8_MAX <= INT_MAX) + typedef uint_fast8_t npf_ftoa_dec_t; +#else + typedef int npf_ftoa_dec_t; +#endif - if (exponent >= (64 - NPF_MANTISSA_BITS)) { return 0; } // value is out of range +enum { + NPF_DOUBLE_EXP_MASK = DBL_MAX_EXP * 2 - 1, + NPF_DOUBLE_EXP_BIAS = DBL_MAX_EXP - 1, + NPF_DOUBLE_MAN_BITS = DBL_MANT_DIG - 1, + NPF_DOUBLE_BIN_BITS = sizeof(npf_double_bin_t) * CHAR_BIT, + NPF_FTOA_MAN_BITS = sizeof(npf_ftoa_man_t) * CHAR_BIT, + NPF_FTOA_SHIFT_BITS = ((NPF_FTOA_MAN_BITS < DBL_MANT_DIG) ? NPF_FTOA_MAN_BITS : DBL_MANT_DIG) - 1 +}; - uint32_t const implicit_one = ((uint32_t)1) << NPF_MANTISSA_BITS; - uint32_t const mantissa = f_bits & (implicit_one - 1); - uint32_t const mantissa_norm = mantissa | implicit_one; +/* Generally floating-point conversion implementations use + grisu2 (https://bit.ly/2JgMggX) and ryu (https://bit.ly/2RLXSg0) algorithms, + which are mathematically exact and fast, but require large lookup tables. - if (exponent > 0) { - *out_int_part = (uint64_t)mantissa_norm << exponent; - } else if (exponent < 0) { - if (-exponent > NPF_MANTISSA_BITS) { - *out_int_part = 0; - } else { - *out_int_part = mantissa_norm >> -exponent; - } - } else { - *out_int_part = mantissa_norm; + This implementation was inspired by Wojciech Muła's (zdjęcia@garnek.pl) + algorithm (http://0x80.pl/notesen/2015-12-29-float-to-string.html) and + extended further by adding dynamic scaling and configurable integer width by + Oskars Rubenis (https://github.com/Okarss). +*/ +static int npf_ftoa_rev(char *buf, npf_format_spec_t const *spec, double f) { + char const *ret = NULL; + npf_double_bin_t bin; { // Union-cast is UB, let compiler optimize byte-copy loop. + char const *src = (char const *)&f; + char *dst = (char *)&bin; + for (uint_fast8_t i = 0; i < sizeof(f); ++i) { dst[i] = src[i]; } } - uint64_t frac; { - int const shift = NPF_FRACTION_BIN_DIGITS + exponent - 4; - if ((shift >= (NPF_FRACTION_BIN_DIGITS - 4)) || (shift < 0)) { - frac = 0; - } else { - frac = ((uint64_t)mantissa_norm) << shift; - } - // multiply off the leading one's digit - frac &= 0x0fffffffffffffffllu; - frac *= 10; + // Unsigned to signed integer casting is UB, but it works for two's complement implementations. + npf_ftoa_exp_t exp = (npf_ftoa_exp_t)((npf_ftoa_exp_t)(bin >> NPF_DOUBLE_MAN_BITS) & NPF_DOUBLE_EXP_MASK); + bin &= ((npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS) - 1; + if (exp == (npf_ftoa_exp_t)NPF_DOUBLE_EXP_MASK) { // special value + ret = (bin) ? "NAN" : "FNI"; + goto exit; } - - { // Count the number of 0s at the beginning of the fractional part. - int frac_base10_neg_exp = 0; - while (frac && ((frac >> (NPF_FRACTION_BIN_DIGITS - 4))) == 0) { - ++frac_base10_neg_exp; - frac &= 0x0fffffffffffffffllu; - frac *= 10; - } - *out_frac_base10_neg_exp = frac_base10_neg_exp; + if (spec->prec > (NANOPRINTF_CONVERSION_BUFFER_SIZE - 2)) { goto exit; } + if (exp) { // normal number + bin |= (npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS; + } else { // subnormal number + ++exp; } + exp = (npf_ftoa_exp_t)(exp - NPF_DOUBLE_EXP_BIAS); - { // Convert the fractional part to base 10. - uint64_t frac_part = 0; - for (int i = 0; frac && (i < NPF_MAX_FRACTION_DEC_DIGITS); ++i) { - frac_part *= 10; - frac_part += (uint64_t)(frac >> (NPF_FRACTION_BIN_DIGITS - 4)); - frac &= 0x0fffffffffffffffllu; - frac *= 10; - } - *out_frac_part = frac_part; + uint_fast8_t carry; carry = 0; + npf_ftoa_dec_t end, dec; dec = (npf_ftoa_dec_t)spec->prec; + if (dec || spec->alt_form) { + buf[dec++] = '.'; } - return 1; -} -int npf_ftoa_rev(char *buf, float f, npf_format_spec_t const *spec, int *out_frac_chars) { - uint32_t f_bits; { // union-cast is UB, let compiler optimize byte-copy loop. - char const *src = (char const *)&f; - char *dst = (char *)&f_bits; - for (unsigned i = 0; i < sizeof(f_bits); ++i) { dst[i] = src[i]; } - } + { // Integer part + npf_ftoa_man_t man_i; + + if (exp >= 0) { + int_fast8_t shift_i = (int_fast8_t)((exp > NPF_FTOA_SHIFT_BITS) ? (int)NPF_FTOA_SHIFT_BITS : exp); + npf_ftoa_exp_t exp_i = (npf_ftoa_exp_t)(exp - shift_i); + shift_i = (int_fast8_t)(NPF_DOUBLE_MAN_BITS - shift_i); + man_i = (npf_ftoa_man_t)(bin >> shift_i); - if ((uint8_t)(f_bits >> 23) == 0xFF) { - if (f_bits & 0x7fffff) { - for (int i = 0; i < 3; ++i) { *buf++ = (char)("NAN"[i] + spec->case_adjust); } + if (exp_i) { + if (shift_i) { + carry = (bin >> (shift_i - 1)) & 0x1; + } + exp = NPF_DOUBLE_MAN_BITS; // invalidate the fraction part + } + + // Scale the exponent from base-2 to base-10. + for (; exp_i; --exp_i) { + if (!(man_i & ((npf_ftoa_man_t)0x1 << (NPF_FTOA_MAN_BITS - 1)))) { + man_i = (npf_ftoa_man_t)(man_i << 1); + man_i = (npf_ftoa_man_t)(man_i | carry); carry = 0; + } else { + if (dec >= NANOPRINTF_CONVERSION_BUFFER_SIZE) { goto exit; } + buf[dec++] = '0'; + carry = (((uint_fast8_t)(man_i % 5) + carry) > 2); + man_i /= 5; + } + } } else { - for (int i = 0; i < 3; ++i) { *buf++ = (char)("FNI"[i] + spec->case_adjust); } + man_i = 0; } - return -3; + end = dec; + + // Print the integer + do { + if (end >= NANOPRINTF_CONVERSION_BUFFER_SIZE) { goto exit; } + buf[end++] = (char)('0' + (char)(man_i % 10)); + man_i /= 10; + } while (man_i); } - uint64_t int_part, frac_part; - int frac_base10_neg_exp; - if (npf_fsplit_abs(f, &int_part, &frac_part, &frac_base10_neg_exp) == 0) { - for (int i = 0; i < 3; ++i) { *buf++ = (char)("ROO"[i] + spec->case_adjust); } - return -3; - } + { // Fraction part + npf_ftoa_man_t man_f; + npf_ftoa_dec_t dec_f = (npf_ftoa_dec_t)spec->prec; - char *dst = buf; + if (exp < NPF_DOUBLE_MAN_BITS) { + int_fast8_t shift_f = (int_fast8_t)((exp < 0) ? -1 : exp); + npf_ftoa_exp_t exp_f = (npf_ftoa_exp_t)(exp - shift_f); + npf_double_bin_t bin_f = bin << ((NPF_DOUBLE_BIN_BITS - NPF_DOUBLE_MAN_BITS) + shift_f); - while (frac_part) { // write the fractional digits - *dst++ = (char)('0' + (frac_part % 10)); - frac_part /= 10; - } + // This if-else statement can be completely optimized at compile time. + if (NPF_DOUBLE_BIN_BITS > NPF_FTOA_MAN_BITS) { + man_f = (npf_ftoa_man_t)(bin_f >> ((unsigned)(NPF_DOUBLE_BIN_BITS - NPF_FTOA_MAN_BITS) % NPF_DOUBLE_BIN_BITS)); + carry = (uint_fast8_t)((bin_f >> ((unsigned)(NPF_DOUBLE_BIN_BITS - NPF_FTOA_MAN_BITS - 1) % NPF_DOUBLE_BIN_BITS)) & 0x1); + } else { + man_f = (npf_ftoa_man_t)((npf_ftoa_man_t)bin_f << ((unsigned)(NPF_FTOA_MAN_BITS - NPF_DOUBLE_BIN_BITS) % NPF_FTOA_MAN_BITS)); + carry = 0; + } + + // Scale the exponent from base-2 to base-10 and prepare the first digit. + for (uint_fast8_t digit = 0; dec_f && (exp_f < 4); ++exp_f) { + if ((man_f > ((npf_ftoa_man_t)-4 / 5)) || digit) { + carry = (uint_fast8_t)(man_f & 0x1); + man_f = (npf_ftoa_man_t)(man_f >> 1); + } else { + man_f = (npf_ftoa_man_t)(man_f * 5); + if (carry) { man_f = (npf_ftoa_man_t)(man_f + 3); carry = 0; } + if (exp_f < 0) { + buf[--dec_f] = '0'; + } else { + ++digit; + } + } + } + man_f = (npf_ftoa_man_t)(man_f + carry); + carry = (exp_f >= 0); + dec = 0; + } else { + man_f = 0; + } - // write the 0 digits between the . and the first fractional digit - while (frac_base10_neg_exp-- > 0) { *dst++ = '0'; } - *out_frac_chars = (int)(dst - buf); - - // round the value to the specified precision - if (spec->prec < *out_frac_chars) { - char *digit = dst - spec->prec - 1; - unsigned carry = (*digit >= '5'); - while (carry && (++digit < dst)) { - carry = (*digit == '9'); - *digit = carry ? '0' : (*digit + 1); + if (dec_f) { + // Print the fraction + for (;;) { + buf[--dec_f] = (char)('0' + (char)(man_f >> (NPF_FTOA_MAN_BITS - 4))); + man_f = (npf_ftoa_man_t)(man_f & ~((npf_ftoa_man_t)0xF << (NPF_FTOA_MAN_BITS - 4))); + if (!dec_f) { break; } + man_f = (npf_ftoa_man_t)(man_f * 10); + } + man_f = (npf_ftoa_man_t)(man_f << 4); + } + if (exp < NPF_DOUBLE_MAN_BITS) { + carry &= (uint_fast8_t)(man_f >> (NPF_FTOA_MAN_BITS - 1)); } - int_part += carry; // overflow is not possible } - *dst++ = '.'; + // Round the number + for (; carry; ++dec) { + if (dec >= NANOPRINTF_CONVERSION_BUFFER_SIZE) { goto exit; } + if (dec >= end) { buf[end++] = '0'; } + if (buf[dec] == '.') { continue; } + carry = (buf[dec] == '9'); + buf[dec] = (char)(carry ? '0' : (buf[dec] + 1)); + } - // write the integer digits - do { *dst++ = (char)('0' + (int_part % 10)); int_part /= 10; } while (int_part); - return (int)(dst - buf); + return (int)end; +exit: + if (!ret) { ret = "RRE"; } + uint_fast8_t i; + for (i = 0; ret[i]; ++i) { buf[i] = (char)(ret[i] + spec->case_adjust); } + return (int)i; } #endif // NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS @@ -641,9 +709,9 @@ int npf_bin_len(npf_uint_t u) { #elif defined(NANOPRINTF_CLANG) || defined(NANOPRINTF_GCC_PAST_4_6) #define NPF_HAVE_BUILTIN_CLZ #if NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 1 - #define NPF_CLZ(X) ((sizeof(long long) * 8) - (size_t)__builtin_clzll(X)) + #define NPF_CLZ(X) ((sizeof(long long) * CHAR_BIT) - (size_t)__builtin_clzll(X)) #else - #define NPF_CLZ(X) ((sizeof(long) * 8) - (size_t)__builtin_clzl(X)) + #define NPF_CLZ(X) ((sizeof(long) * CHAR_BIT) - (size_t)__builtin_clzl(X)) #endif return (int)NPF_CLZ(u); #endif @@ -718,7 +786,7 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { } #endif - union { char cbuf_mem[32]; npf_uint_t binval; } u; + union { char cbuf_mem[NANOPRINTF_CONVERSION_BUFFER_SIZE]; npf_uint_t binval; } u; char *cbuf = u.cbuf_mem, sign_c = 0; int cbuf_len = 0, need_0x = 0; #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 @@ -730,9 +798,6 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 int zero = 0; #endif -#endif -#if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 - int frac_chars = 0, inf_or_nan = 0; #endif // Extract and convert the argument to string, point cbuf at the text. @@ -876,27 +941,18 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { case NPF_FMT_SPEC_CONV_FLOAT_SCI: case NPF_FMT_SPEC_CONV_FLOAT_SHORTEST: case NPF_FMT_SPEC_CONV_FLOAT_HEX: { - float val; + double val; if (fs.length_modifier == NPF_FMT_SPEC_LEN_MOD_LONG_DOUBLE) { - val = (float)va_arg(args, long double); + val = (double)va_arg(args, long double); } else { - val = (float)va_arg(args, double); + val = va_arg(args, double); } - sign_c = (val < 0.f) ? '-' : fs.prepend; + sign_c = (val < 0.) ? '-' : fs.prepend; #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 - zero = (val == 0.f); + zero = (val == 0.); #endif - cbuf_len = npf_ftoa_rev(cbuf, val, &fs, &frac_chars); - - if (cbuf_len < 0) { - cbuf_len = -cbuf_len; - inf_or_nan = 1; - } else { - int const prec_adj = npf_max(0, frac_chars - fs.prec); - cbuf += prec_adj; - cbuf_len -= prec_adj; - } + cbuf_len = npf_ftoa_rev(cbuf, &fs, val); } break; #endif default: break; @@ -921,35 +977,25 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { #endif // Compute the number of bytes to truncate or '0'-pad. +#if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1 if (fs.conv_spec != NPF_FMT_SPEC_CONV_STRING) { #if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 - if (!inf_or_nan) { // float precision is after the decimal point - int const prec_start = - (fs.conv_spec == NPF_FMT_SPEC_CONV_FLOAT_DEC) ? frac_chars : cbuf_len; - prec_pad = npf_max(0, fs.prec - prec_start); - } -#elif NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1 - prec_pad = npf_max(0, fs.prec - cbuf_len); + // float precision is after the decimal point + if (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_DEC) #endif + { prec_pad = npf_max(0, fs.prec - cbuf_len); } } +#endif #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 // Given the full converted length, how many pad bytes? field_pad = fs.field_width - cbuf_len - !!sign_c; if (need_0x) { field_pad -= 2; } - -#if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 - if ((fs.conv_spec == NPF_FMT_SPEC_CONV_FLOAT_DEC) && !fs.prec && !fs.alt_form) { - ++field_pad; // 0-pad, no decimal point. - } -#endif #if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1 field_pad -= prec_pad; #endif field_pad = npf_max(0, field_pad); -#endif // NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS -#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 // Apply right-justified field width if requested if (!fs.left_justified && pad_c) { // If leading zeros pad, sign goes first. if (pad_c == '0') { @@ -969,35 +1015,15 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { for (int i = 0; i < cbuf_len; ++i) { NPF_PUTC(cbuf[i]); } } else { if (sign_c) { NPF_PUTC(sign_c); } -#if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 - if (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_DEC) { -#endif - #if NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS == 1 - while (prec_pad-- > 0) { NPF_PUTC('0'); } // int precision leads. + while (prec_pad-- > 0) { NPF_PUTC('0'); } // int precision leads. #endif - -#if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 - } else { - // if 0 precision, skip the fractional part and '.' - // if 0 prec + alternative form, keep the '.' - if (!fs.prec && !fs.alt_form) { ++cbuf; --cbuf_len; } - } -#endif - #if NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS == 1 if (fs.conv_spec == NPF_FMT_SPEC_CONV_BINARY) { while (cbuf_len) { NPF_PUTC('0' + ((u.binval >> --cbuf_len) & 1)); } } else #endif { while (cbuf_len-- > 0) { NPF_PUTC(cbuf[cbuf_len]); } } // payload is reversed - -#if NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 1 - // real precision comes after the number. - if ((fs.conv_spec == NPF_FMT_SPEC_CONV_FLOAT_DEC) && !inf_or_nan) { - while (prec_pad-- > 0) { NPF_PUTC('0'); } - } -#endif } #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 @@ -1113,4 +1139,3 @@ int npf_vsnprintf(char *buffer, size_t bufsz, char const *format, va_list vlist) ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ - diff --git a/tests/doctest_main.cc b/tests/doctest_main.cc index 5e90be1..1546d88 100644 --- a/tests/doctest_main.cc +++ b/tests/doctest_main.cc @@ -1,7 +1,9 @@ #ifdef _MSC_VER - #pragma warning(disable:5246) // initialization of subobject needs braces + #pragma warning(disable:4619) // there is no warning number 'number' + // C4619 has to be disabled first! + #pragma warning(disable:5246) // initialization of a subobject should be wrapped in braces #pragma warning(disable:5262) // implicit switch fall-through - #pragma warning(disable:5264) // unused const variable + #pragma warning(disable:5264) // const variable is not used #endif #if defined(__clang__) && !defined(__APPLE__) diff --git a/tests/mpaland-conformance b/tests/mpaland-conformance index 38f318f..7b954a3 160000 --- a/tests/mpaland-conformance +++ b/tests/mpaland-conformance @@ -1 +1 @@ -Subproject commit 38f318ff21fc44a97f81506b2419a03ef90b1413 +Subproject commit 7b954a34cdff72b8ed68c0088dd37335d8f11a32 diff --git a/tests/unit_fsplit_abs.cc b/tests/unit_fsplit_abs.cc deleted file mode 100644 index cde8fbf..0000000 --- a/tests/unit_fsplit_abs.cc +++ /dev/null @@ -1,73 +0,0 @@ -#include "unit_nanoprintf.h" - -#include -#include - -#if NANOPRINTF_HAVE_GCC_WARNING_PRAGMAS - #pragma GCC diagnostic push - #if NANOPRINTF_CLANG - #pragma GCC diagnostic ignored "-Wformat-pedantic" - #pragma GCC diagnostic ignored "-Wmissing-prototypes" - #endif -#endif - -void require_fsplit_abs(float f, - uint64_t expected_int_part, - uint64_t expected_frac_part, - int expected_frac_base10_neg_e) { - uint64_t int_part, frac_part; - int frac_neg_exp; - REQUIRE(npf_fsplit_abs(f, &int_part, &frac_part, &frac_neg_exp)); - REQUIRE(int_part == expected_int_part); - REQUIRE(frac_part == expected_frac_part); - REQUIRE(frac_neg_exp == expected_frac_base10_neg_e); -} - -TEST_CASE("npf_fsplit_abs") { - require_fsplit_abs(0.f, 0, 0, 0); - require_fsplit_abs(1.f, 1, 0, 0); - require_fsplit_abs(-1.f, 1, 0, 0); - require_fsplit_abs(123456.f, 123456, 0, 0); - require_fsplit_abs(-123456.f, 123456, 0, 0); - require_fsplit_abs(powf(2.0f, 63.f), 9223372036854775808ULL, 0, 0); - - SUBCASE("exponent too large") { - uint64_t i, f; - int f_neg_exp; - REQUIRE(!npf_fsplit_abs(powf(2.0f, 64.f), &i, &f, &f_neg_exp)); - } - - // fractional base-10 negative exponent - require_fsplit_abs(0.03125f, 0, 3125, 1); - require_fsplit_abs(0.0078125f, 0, 78125, 2); - require_fsplit_abs(2.4414062E-4f, 0, 244140625, 3); - require_fsplit_abs(3.8146973E-6f, 0, 381469726, 5); - - // perfectly-representable fractions, adding 1 bit to mantissa each time. - require_fsplit_abs(1.5f, 1, 5, 0); - require_fsplit_abs(1.625f, 1, 625, 0); - require_fsplit_abs(1.875f, 1, 875, 0); - require_fsplit_abs(1.9375f, 1, 9375, 0); - require_fsplit_abs(1.96875f, 1, 96875, 0); - require_fsplit_abs(1.984375f, 1, 984375, 0); - require_fsplit_abs(1.9921875f, 1, 9921875, 0); - - require_fsplit_abs(1.9960938f, 1, 99609375, 0); // first truncation divergence. - - // truncations, but continue adding mantissa bits - require_fsplit_abs(1.9980469f, 1, 998046875, 0); // 1.998046875 is stored. - require_fsplit_abs(1.9990234f, 1, 999023437, 0); // 1.9990234375 is stored. - require_fsplit_abs(1.9995117f, 1, 999511718, 0); // 1.99951171875 is stored. - require_fsplit_abs(1.9997559f, 1, 999755859, 0); // 1.999755859375 is stored. - require_fsplit_abs(1.9998779f, 1, 999877929, 0); // 1.9998779296875 is stored. - require_fsplit_abs(1.999939f, 1, 999938964, 0); // 1.99993896484375 is stored. - require_fsplit_abs(1.9999695f, 1, 999969482, 0); // 1.999969482421875 is stored. - require_fsplit_abs(1.9999847f, 1, 999984741, 0); // 1.9999847412109375 is stored. - require_fsplit_abs(1.9999924f, 1, 999992370, 0); // 1.99999237060546875 is stored. - require_fsplit_abs(1.9999962f, 1, 999996185, 0); // 1.999996185302734375 is stored. - require_fsplit_abs(1.9999981f, 1, 999998092, 0); // 1.9999980926513671875 is stored. - require_fsplit_abs(1.999999f, 1, 999999046, 0); // 1.99999904632568359375 is stored. - require_fsplit_abs(1.9999995f, 1, 999999523, 0); // 1.999999523162841796875 is stored. - require_fsplit_abs(1.9999998f, 1, 999999761, 0); // 1.9999997615814208984375 is stored. - require_fsplit_abs(1.9999999f, 1, 999999880, 0); // 1.99999988079071044921875 is stored. -} diff --git a/tests/unit_ftoa_rev.cc b/tests/unit_ftoa_rev.cc index f5cc575..e28a1af 100644 --- a/tests/unit_ftoa_rev.cc +++ b/tests/unit_ftoa_rev.cc @@ -1,25 +1,91 @@ #include "unit_nanoprintf.h" +#include #include #include #if NANOPRINTF_HAVE_GCC_WARNING_PRAGMAS #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wunused-function" #if NANOPRINTF_CLANG #pragma GCC diagnostic ignored "-Wformat-pedantic" + #pragma GCC diagnostic ignored "-Wmissing-prototypes" + #pragma GCC diagnostic ignored "-Wold-style-cast" #endif #endif +static npf_format_spec_t spec; + +static void memrev(char *lhs, char *rhs) { + --rhs; + while (lhs < rhs) { + char c = *lhs; + *lhs++ = *rhs; + *rhs-- = c; + } +} + +static void require_ftoa_rev(std::string const &expected, double dbl) { + char buf[NANOPRINTF_CONVERSION_BUFFER_SIZE + 1]; + int const n = npf_ftoa_rev(buf, &spec, dbl); + REQUIRE(n <= NANOPRINTF_CONVERSION_BUFFER_SIZE); + memrev(buf, &buf[n]); + buf[n] = '\0'; + CHECK(std::string{buf} == std::string{expected}); + CHECK(n == (int)expected.size()); +} + +static void require_ftoa_rev_bin(char const *expected, npf_double_bin_t bin) { + double dbl; + memcpy(&dbl, &bin, sizeof(dbl)); + require_ftoa_rev(expected, dbl); +} + TEST_CASE("ftoa_rev") { - char buf[64]; - npf_format_spec_t spec; - int frac_bytes; - memset(buf, 0, sizeof(buf)); memset(&spec, 0, sizeof(spec)); - spec.prec = 1; - SUBCASE("zero") { - REQUIRE(npf_ftoa_rev(buf, 0.f, &spec, &frac_bytes) == 2); - REQUIRE(std::string{".0"} == buf); + SUBCASE("special values") { + require_ftoa_rev("NAN", (double)+NAN); + require_ftoa_rev("NAN", (double)-NAN); + require_ftoa_rev("INF", (double)+INFINITY); + require_ftoa_rev("INF", (double)-INFINITY); + require_ftoa_rev("ERR", DBL_MAX); + spec.prec = NANOPRINTF_CONVERSION_BUFFER_SIZE - 2; + require_ftoa_rev("ERR", 10.); + spec.prec += 1; + require_ftoa_rev("ERR", 9.); + spec.case_adjust = 'a' - 'A'; // lowercase + require_ftoa_rev("err", 0.); + } + + SUBCASE("zero and decimal separator") { + require_ftoa_rev("0", +0.); + require_ftoa_rev("0", -0.); + spec.alt_form = '#'; + require_ftoa_rev("0.", 0.); + spec.prec = 1; + require_ftoa_rev("0.0", 0.); + } + + SUBCASE("rounding") { + require_ftoa_rev("9", 8.5); + require_ftoa_rev("10", 9.5); + require_ftoa_rev("49", 48.5); + require_ftoa_rev("50", 49.5); + require_ftoa_rev("99", 98.5); + require_ftoa_rev("100", 99.5); + + require_ftoa_rev("0", 0.40625); + require_ftoa_rev("1", 0.5); + + spec.prec = 1; + require_ftoa_rev("0.3", 0.34375); + require_ftoa_rev("0.3", 0.25); + require_ftoa_rev("0.9", 0.9375); + require_ftoa_rev("1.0", 0.96875); + + spec.prec = 4; + require_ftoa_rev("0.9375", 0.9375); + require_ftoa_rev("0.9688", 0.96875); } } diff --git a/tests/unit_ftoa_rev_08.cc b/tests/unit_ftoa_rev_08.cc new file mode 100644 index 0000000..95441ae --- /dev/null +++ b/tests/unit_ftoa_rev_08.cc @@ -0,0 +1,76 @@ +#define NANOPRINTF_CONVERSION_BUFFER_SIZE 512 +#define NANOPRINTF_CONVERSION_FLOAT_TYPE uint8_t + +#include "unit_ftoa_rev.cc" + +TEST_CASE("ftoa_rev_08") { + memset(&spec, 0, sizeof(spec)); + + SUBCASE("integer overflow") { + spec.prec = 1; + require_ftoa_rev("255.5", (npf_ftoa_man_t)-1 + 0.5); + require_ftoa_rev("256.0", (npf_ftoa_man_t)-1 + 0.96875); + require_ftoa_rev("260.0", (npf_ftoa_man_t)-1 + 1.); + require_ftoa_rev("260.0", (npf_ftoa_man_t)-1 + 1.5); + require_ftoa_rev("260.0", (npf_ftoa_man_t)-1 + 9.); + require_ftoa_rev("270.0", (npf_ftoa_man_t)-1 + 10.); + } + + SUBCASE("fraction accuracy") { + spec.prec = 22 + 3; + require_ftoa_rev("1.0312500000000000000000000", 1. + 1. / ((npf_double_bin_t)0x1 << (NPF_FTOA_MAN_BITS - 3))); + require_ftoa_rev("0.9687500000000000000000000", 1. - 1. / ((npf_double_bin_t)0x1 << (NPF_FTOA_MAN_BITS - 3))); + require_ftoa_rev("0.6687500000000000000000000", 2. / 3.); + require_ftoa_rev("0.0000000000000000000006188", 2. / 3. / 1e21); + } + + SUBCASE("limits") { + // largest representable number + require_ftoa_rev_bin( + "16200000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000", + ((npf_double_bin_t)NPF_DOUBLE_EXP_MASK << NPF_DOUBLE_MAN_BITS) - 1); + + spec.prec = 315 + 3; + + // smallest normal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000009875000000", + (npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS); + + // largest subnormal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000009875000000", + ((npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS) - 1); + + // smallest representable numbers + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000125000000", + (npf_double_bin_t)0x3 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000077500000", + (npf_double_bin_t)0x2 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000046875000", + (npf_double_bin_t)0x1 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + } +} diff --git a/tests/unit_ftoa_rev_16.cc b/tests/unit_ftoa_rev_16.cc new file mode 100644 index 0000000..444d2fa --- /dev/null +++ b/tests/unit_ftoa_rev_16.cc @@ -0,0 +1,81 @@ +#define NANOPRINTF_CONVERSION_BUFFER_SIZE 512 +#define NANOPRINTF_CONVERSION_FLOAT_TYPE uint16_t + +#include "unit_ftoa_rev.cc" + +TEST_CASE("ftoa_rev_16") { + memset(&spec, 0, sizeof(spec)); + + SUBCASE("integer overflow") { + spec.prec = 1; + require_ftoa_rev("65535.5", (npf_ftoa_man_t)-1 + 0.5); + require_ftoa_rev("65536.0", (npf_ftoa_man_t)-1 + 0.96875); + require_ftoa_rev("65540.0", (npf_ftoa_man_t)-1 + 1.); + require_ftoa_rev("65540.0", (npf_ftoa_man_t)-1 + 1.5); + require_ftoa_rev("65540.0", (npf_ftoa_man_t)-1 + 9.); + require_ftoa_rev("65550.0", (npf_ftoa_man_t)-1 + 10.); + } + + SUBCASE("fraction accuracy") { + spec.prec = 24 + 3; + require_ftoa_rev("1.000122070312500000000000000", 1. + 1. / ((npf_double_bin_t)0x1 << (NPF_FTOA_MAN_BITS - 3))); + require_ftoa_rev("0.999877929687500000000000000", 1. - 1. / ((npf_double_bin_t)0x1 << (NPF_FTOA_MAN_BITS - 3))); + require_ftoa_rev("0.666674804687500000000000000", 2. / 3.); + require_ftoa_rev("0.000000000000000000000666479", 2. / 3. / 1e21); + } + + SUBCASE("limits") { + // largest representable number + require_ftoa_rev_bin( + "17858000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000", + ((npf_double_bin_t)NPF_DOUBLE_EXP_MASK << NPF_DOUBLE_MAN_BITS) - 1); + + spec.prec = 325 + 3; + + // smallest normal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000022180175781" + "2500000000", + (npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS); + + // largest subnormal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000022180175781" + "2500000000", + ((npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS) - 1); + + // smallest representable numbers + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000001083007" + "8125000000", + (npf_double_bin_t)0x3 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000676806" + "6406250000", + (npf_double_bin_t)0x2 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000405883" + "7890625000", + (npf_double_bin_t)0x1 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + } +} diff --git a/tests/unit_ftoa_rev_32.cc b/tests/unit_ftoa_rev_32.cc new file mode 100644 index 0000000..83b94df --- /dev/null +++ b/tests/unit_ftoa_rev_32.cc @@ -0,0 +1,81 @@ +#define NANOPRINTF_CONVERSION_BUFFER_SIZE 512 +#define NANOPRINTF_CONVERSION_FLOAT_TYPE uint32_t + +#include "unit_ftoa_rev.cc" + +TEST_CASE("ftoa_rev_32") { + memset(&spec, 0, sizeof(spec)); + + SUBCASE("integer overflow") { + spec.prec = 1; + require_ftoa_rev("4294967295.5", (npf_ftoa_man_t)-1 + 0.5); + require_ftoa_rev("4294967296.0", (npf_ftoa_man_t)-1 + 0.96875); + require_ftoa_rev("4294967300.0", (npf_ftoa_man_t)-1 + 1.); + require_ftoa_rev("4294967300.0", (npf_ftoa_man_t)-1 + 1.5); + require_ftoa_rev("4294967300.0", (npf_ftoa_man_t)-1 + 9.); + require_ftoa_rev("4294967310.0", (npf_ftoa_man_t)-1 + 10.); + } + + SUBCASE("fraction accuracy") { + spec.prec = 29 + 3; + require_ftoa_rev("1.00000000186264514923095703125000", 1. + 1. / ((npf_double_bin_t)0x1 << (NPF_FTOA_MAN_BITS - 3))); + require_ftoa_rev("0.99999999813735485076904296875000", 1. - 1. / ((npf_double_bin_t)0x1 << (NPF_FTOA_MAN_BITS - 3))); + require_ftoa_rev("0.66666666679084300994873046875000", 2. / 3.); + require_ftoa_rev("0.00000000000000000000066666666493", 2. / 3. / 1e21); + } + + SUBCASE("limits") { + // largest representable number + require_ftoa_rev_bin( + "17976929680000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000", + ((npf_double_bin_t)NPF_DOUBLE_EXP_MASK << NPF_DOUBLE_MAN_BITS) - 1); + + spec.prec = 346 + 3; + + // smallest normal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000022250737510" + "6215476989746093750000000000000", + (npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS); + + // largest subnormal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000022250737510" + "6215476989746093750000000000000", + ((npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS) - 1); + + // smallest representable numbers + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000016" + "5780913084745407104492187500000", + (npf_double_bin_t)0x3 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000010" + "3613070771098136901855468750000", + (npf_double_bin_t)0x2 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000006" + "2167842201888561248779296875000", + (npf_double_bin_t)0x1 << (NPF_DOUBLE_MAN_BITS - NPF_FTOA_MAN_BITS)); + } +} diff --git a/tests/unit_ftoa_rev_64.cc b/tests/unit_ftoa_rev_64.cc new file mode 100644 index 0000000..3574e3a --- /dev/null +++ b/tests/unit_ftoa_rev_64.cc @@ -0,0 +1,81 @@ +#define NANOPRINTF_CONVERSION_BUFFER_SIZE 512 +#define NANOPRINTF_CONVERSION_FLOAT_TYPE uint64_t + +#include "unit_ftoa_rev.cc" + +TEST_CASE("ftoa_rev_64") { + memset(&spec, 0, sizeof(spec)); + + SUBCASE("integer overflow") { + spec.prec = 1; + require_ftoa_rev("2251799813685247.3", ((npf_double_bin_t)0x1 << (DBL_MANT_DIG - 2)) - 1 + 0.25); + require_ftoa_rev("2251799813685247.5", ((npf_double_bin_t)0x1 << (DBL_MANT_DIG - 2)) - 1 + 0.5); + require_ftoa_rev("2251799813685247.8", ((npf_double_bin_t)0x1 << (DBL_MANT_DIG - 2)) - 1 + 0.75); + require_ftoa_rev("4503599627370495.5", ((npf_double_bin_t)0x1 << (DBL_MANT_DIG - 1)) - 1 + 0.5); + require_ftoa_rev("9007199254740991.0", ((npf_double_bin_t)0x1 << DBL_MANT_DIG) - 1); + require_ftoa_rev("18446744073709549568.0", (npf_double_bin_t)-1 << (NPF_DOUBLE_BIN_BITS - DBL_MANT_DIG)); + } + + SUBCASE("fraction accuracy") { + spec.prec = 53 + 3; + require_ftoa_rev("1.00000000000000022204460492503130808472633361816406250000", 1. + 1. / ((npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS)); + require_ftoa_rev("0.99999999999999988897769753748434595763683319091796875000", 1. - 1. / ((npf_double_bin_t)0x1 << DBL_MANT_DIG)); + require_ftoa_rev("0.66666666666666662965923251249478198587894439697265625000", 2. / 3.); + require_ftoa_rev("0.00000000000000000000066666666666666663633791789500548930", 2. / 3. / 1e21); + } + + SUBCASE("limits") { + // largest representable number + require_ftoa_rev_bin( + "17976931348623156688000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000", + ((npf_double_bin_t)NPF_DOUBLE_EXP_MASK << NPF_DOUBLE_MAN_BITS) - 1); + + spec.prec = 384 + 3; + + // smallest normal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000022250738585" + "072013598145646007253617426613345742225646972656250000000000000000000", + (npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS); + + // largest subnormal number + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000022250738585" + "072008645510122093469362880568951368331909179687500000000000000000000", + ((npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS) - 1); + + // smallest representable numbers + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000014821969375237396167321879403289131005294620990753173828125000000", + (npf_double_bin_t)0x3 << 0); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000009881312916824930773877777578917402934166602790355682373046875000", + (npf_double_bin_t)0x2 << 0); + + require_ftoa_rev_bin( + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000004940656458412465387372569658452903240686282515525817871093750000", + (npf_double_bin_t)0x1 << 0); + } +} diff --git a/tests/unit_nanoprintf.h b/tests/unit_nanoprintf.h index b2e5d5d..d640ce9 100644 --- a/tests/unit_nanoprintf.h +++ b/tests/unit_nanoprintf.h @@ -1,16 +1,19 @@ #pragma once -// Each unit test file compiles nanoprintf privately for access to helper functions. #define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1 #define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1 #define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1 #define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1 #define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 1 + +// Each unit test file compiles nanoprintf privately for access to helper functions. #define NANOPRINTF_VISIBILITY_STATIC #define NANOPRINTF_IMPLEMENTATION #ifdef _MSC_VER - #pragma warning(disable:4464) // relative include uses .. + #pragma warning(disable:4619) // there is no warning number 'number' + // C4619 has to be disabled first! + #pragma warning(disable:4464) // relative include path contains '..' #endif #include "../nanoprintf.h" @@ -27,12 +30,14 @@ #endif #ifdef _MSC_VER - #pragma warning(disable:4710) // function wasn't inlined - #pragma warning(disable:4711) // function was inlined + #pragma warning(disable:4365) // type conversion, signed/unsigned mismatch + #pragma warning(disable:4505) // unreferenced local function has been removed #pragma warning(disable:4514) // unreferenced inline function has been removed - #pragma warning(disable:5039) // could throw inside extern c function - #pragma warning(disable:5264) // const variable not used (shut up doctest) + #pragma warning(disable:4710) // function not inlined + #pragma warning(disable:4711) // function selected for inline expansion + #pragma warning(disable:5039) // potentially throwing function passed to extern C function + #pragma warning(disable:5264) // const variable is not used + #pragma warning(disable:26451) // casting the operator result value to a wider type #endif #include "doctest.h" -