diff --git a/README.md b/README.md index 9c70be77..cadecdbc 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ [![](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 scientific notation (`%e`, `%g`, `%a`), and locale 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 scientific notation (`%e`, `%g`), and locale 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 *~580-2800 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 *~610-3160 bytes of object code* on a Cortex-M4 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 large integer arithmetic calls your compiler might emit. As usual, some Windows-specific headers are required if you're compiling natively for msvc. @@ -83,7 +83,8 @@ nanoprintf has the following static configuration flags. * `NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables field width specifiers. * `NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables precision specifiers. -* `NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables floating-point specifiers. +* `NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables floating-point specifiers (`%f`/`%F`). +* `NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER`: Set to `0` or `1`. Enables hex float specifier (`%a`/`%A`). Requires `NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1`. * `NANOPRINTF_USE_SMALL_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables small modifiers. * `NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables oversized modifiers. * `NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS`: Set to `0` or `1`. Enables binary specifiers. @@ -159,7 +160,7 @@ Like `printf`, `nanoprintf` expects a conversion specification string of the fol * `f`/`F`: Floating-point decimal * `e`/`E`: Floating-point scientific (unimplemented, prints float decimal) * `g`/`G`: Floating-point shortest (unimplemented, prints float decimal) - * `a`/`A`: Floating-point hex (unimplemented, prints float decimal) + * `a`/`A`: Floating-point hex * `b`/`B`: Binary integers ## Floating-Point @@ -168,98 +169,32 @@ Floating-point conversion is performed by extracting the integer and fraction pa 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! :) +The `%a`/`%A` hex float specifier is optionally supported via `NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER`. It operates directly on the IEEE 754 binary representation, emitting the mantissa as hex nibbles and the exponent in decimal. No floating-point arithmetic is performed. Rounding uses a nibble-at-a-time carry loop with only constant 3- and 4-bit shifts, keeping the code compact on architectures without a barrel shifter (e.g. Cortex-M0). + +The `%e`/`%E` 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! :) ## Limitations No wide-character support exists: the `%lc` and `%ls` fields require that the arg be converted to a char array as if by a call to [wcrtomb](http://man7.org/linux/man-pages/man3/wcrtomb.3.html). When locale and character set conversions get involved, it's hard to keep the name "nano". Accordingly, `%lc` and `%ls` behave like `%c` and `%s`, respectively. -Currently the only supported float conversions are the decimal forms: `%f` and `%F`. Pull requests welcome! +Currently the supported float conversions are `%f`/`%F` and `%a`/`%A`. Pull requests for `%e`/`%g` welcome! ## Measurement The CI build is set up to use gcc and nm to measure the compiled size of every pull request. See the [Presubmit Checks](https://github.com/charlesnicholson/nanoprintf/actions/workflows/presubmit.yml) "size reports" job output for recent runs. -The following size measurements are taken against the Cortex-M0 build. +All measurements compiled with `arm-none-eabi-gcc -Os` and `-ffunction-sections`. + +| Configuration | Cortex-M0 | Cortex-M4 | +|---|--:|--:| +| Minimal | 610 | 678 | +| Binary | 674 | 738 | +| Field Width + Precision | 1494 | 1570 | +| Field Width + Precision + Binary | 1698 | 1706 | +| Float | 1826 | 1942 | +| Float + Hex Float | 2254 | 2378 | +| Everything | 3142 | 3158 | -``` -Configuration "Minimal": -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_SMALL_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_ALT_FORM_FLAG=0 - -arm-none-eabi-nm --print-size --size-sort npf.o -00000046 00000002 t npf_bufputc_nop -00000032 00000014 t npf_bufputc -000001ce 00000016 T npf_pprintf -0000022c 00000016 T npf_snprintf -00000000 00000032 t npf_utoa_rev -000001e4 00000048 T npf_vsnprintf -00000048 00000186 T npf_vpprintf -Total size: 0x242 (578) 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_SMALL_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_ALT_FORM_FLAG=0 - -arm-none-eabi-nm --print-size --size-sort npf.o -00000046 00000002 t npf_bufputc_nop -00000048 00000010 t npf_putc_cnt -00000032 00000014 t npf_bufputc -00000228 00000016 T npf_pprintf -00000288 00000016 T npf_snprintf -00000000 00000032 t npf_utoa_rev -0000023e 0000004a T npf_vsnprintf -00000058 000001d0 T npf_vpprintf -Total size: 0x29e (670) 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_SMALL_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_ALT_FORM_FLAG=1 - -arm-none-eabi-nm --print-size --size-sort npf.o -00000046 00000002 t npf_bufputc_nop -00000048 00000010 t npf_putc_cnt -00000032 00000014 t npf_bufputc -00000532 00000016 T npf_pprintf -00000590 00000016 T npf_snprintf -00000000 00000032 t npf_utoa_rev -00000548 00000048 T npf_vsnprintf -00000058 000004da T npf_vpprintf -Total size: 0x5a6 (1446) bytes - -Configuration "Field Width + Precision + 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=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_SMALL_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_ALT_FORM_FLAG=1 - -arm-none-eabi-nm --print-size --size-sort npf.o -00000046 00000002 t npf_bufputc_nop -00000048 00000010 t npf_putc_cnt -00000032 00000014 t npf_bufputc -000005ba 00000016 T npf_pprintf -00000618 00000016 T npf_snprintf -00000000 00000032 t npf_utoa_rev -000005d0 00000048 T npf_vsnprintf -00000058 00000562 T npf_vpprintf -Total size: 0x62e (1582) 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_SMALL_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_ALT_FORM_FLAG=1 - -arm-none-eabi-nm --print-size --size-sort npf.o -00000046 00000002 t npf_bufputc_nop -00000048 00000010 t npf_putc_cnt -00000032 00000014 t npf_bufputc -00000650 00000016 T npf_pprintf -000006b0 00000016 T npf_snprintf -00000000 00000032 t npf_utoa_rev -00000666 0000004a T npf_vsnprintf -00000058 000005f8 T npf_vpprintf -Total size: 0x6c6 (1734) 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_SMALL_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_ALT_FORM_FLAG=1 - -arm-none-eabi-nm --print-size --size-sort npf.o -0000005a 00000002 t npf_bufputc_nop -0000005c 00000010 t npf_putc_cnt -00000046 00000014 t npf_bufputc -00000ab4 00000016 T npf_pprintf -00000b14 00000016 T npf_snprintf -00000000 00000046 t npf_utoa_rev -00000aca 0000004a T npf_vsnprintf -0000006c 00000a48 T npf_vpprintf -Total size: 0xb2a (2858) bytes -``` ## Development diff --git a/nanoprintf.h b/nanoprintf.h index b6ca9a8c..9ced7078 100644 --- a/nanoprintf.h +++ b/nanoprintf.h @@ -107,6 +107,11 @@ NPF_VISIBILITY int npf_vpprintf(npf_putc pc, #define NANOPRINTF_USE_ALT_FORM_FLAG 1 #endif +// Optional flag, defaults to 0 if not explicitly configured. +#ifndef NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER + #define NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER 0 +#endif + // If anything's been configured, everything must be configured. #ifndef NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS #error NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS must be #defined to 0 or 1 @@ -136,6 +141,11 @@ NPF_VISIBILITY int npf_vpprintf(npf_putc pc, #error Precision format specifiers must be enabled if float support is enabled. #endif +#if (NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1) && \ + (NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS == 0) + #error Float format specifiers must be enabled if float hex support is enabled. +#endif + // intmax_t / uintmax_t require stdint from c99 / c++11 #if NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 1 #ifndef _MSC_VER @@ -281,8 +291,10 @@ enum { NPF_FMT_SPEC_CONV_FLOAT_DEC, // 'f', 'F' NPF_FMT_SPEC_CONV_FLOAT_SCI, // 'e', 'E' NPF_FMT_SPEC_CONV_FLOAT_SHORTEST, // 'g', 'G' +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 NPF_FMT_SPEC_CONV_FLOAT_HEX, // 'a', 'A' #endif +#endif }; typedef struct npf_format_spec { @@ -466,11 +478,13 @@ static int npf_parse_format_spec(char const *format, npf_format_spec_t *out_spec out_spec->conv_spec = NPF_FMT_SPEC_CONV_FLOAT_SHORTEST; break; +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 case 'A': out_spec->case_adjust = 0; case 'a': out_spec->conv_spec = NPF_FMT_SPEC_CONV_FLOAT_HEX; break; #endif +#endif #if NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS == 1 case 'n': @@ -718,6 +732,62 @@ static int npf_ftoa_rev(char *buf, npf_format_spec_t const *spec, double f) { return -(int)i; } +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 + +static NPF_NOINLINE int npf_atoa_rev( + char *buf, npf_format_spec_t const *spec, double f) { + npf_double_bin_t bin = npf_double_to_int_rep(f); + 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) { return 0; } // caller uses ftoa_rev + + if (exp) { + bin |= (npf_double_bin_t)0x1 << NPF_DOUBLE_MAN_BITS; + exp = (npf_ftoa_exp_t)(exp - NPF_DOUBLE_EXP_BIAS); + } else if (bin) { + exp = (npf_ftoa_exp_t)(1 - NPF_DOUBLE_EXP_BIAS); + } + + { int const n_frac_dig = (NPF_DOUBLE_MAN_BITS + 3) / 4; + int const prec = NPF_MIN(spec->prec, n_frac_dig); + int end, i; + + // Discard low nibbles and round (only constant shifts of 3 and 4) + { npf_double_bin_t carry = 0; + for (i = n_frac_dig - prec; i > 0; --i) { + carry = (bin >> 3) & 1; + bin >>= 4; + } + bin += carry; + } + + { npf_ftoa_exp_t const ae = (exp < 0) ? (npf_ftoa_exp_t)-exp : exp; + end = npf_utoa_rev((npf_uint_t)ae, buf, 10, 0); + buf[end++] = (exp < 0) ? '-' : '+'; + buf[end++] = (char)('P' + spec->case_adjust); + } + + for (i = 0; i < prec; ++i) { + int_fast8_t const d = (int_fast8_t)(bin & 0xF); + buf[end++] = (char)(((d < 10) ? '0' : ('A' - 10 + spec->case_adjust)) + d); + bin >>= 4; + } + + if (prec > 0 +#if NANOPRINTF_USE_ALT_FORM_FLAG == 1 + || spec->alt_form +#endif + ) { buf[end++] = '.'; } + + buf[end++] = (char)('0' + (int_fast8_t)(bin & 0xF)); + return end; + } +} + +#endif // NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER + #endif // NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS #if NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS == 1 @@ -825,9 +895,13 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { case NPF_FMT_SPEC_CONV_FLOAT_DEC: case NPF_FMT_SPEC_CONV_FLOAT_SCI: case NPF_FMT_SPEC_CONV_FLOAT_SHORTEST: - case NPF_FMT_SPEC_CONV_FLOAT_HEX: fs.prec = 6; break; +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 + case NPF_FMT_SPEC_CONV_FLOAT_HEX: + fs.prec = (NPF_DOUBLE_MAN_BITS + 3) / 4; + break; +#endif default: break; } @@ -1023,7 +1097,10 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { case NPF_FMT_SPEC_CONV_FLOAT_DEC: case NPF_FMT_SPEC_CONV_FLOAT_SCI: case NPF_FMT_SPEC_CONV_FLOAT_SHORTEST: - case NPF_FMT_SPEC_CONV_FLOAT_HEX: { +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 + case NPF_FMT_SPEC_CONV_FLOAT_HEX: +#endif + { double val; if (fs.length_modifier == NPF_FMT_SPEC_LEN_MOD_LONG_DOUBLE) { val = (double)va_arg(args, long double); @@ -1031,11 +1108,25 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { val = va_arg(args, double); } +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 + { npf_double_bin_t const b = npf_double_to_int_rep(val); + sign_c = (b >> NPF_DOUBLE_SIGN_POS) ? '-' : fs.prepend; +#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 + zero = !(b & ~((npf_double_bin_t)1 << NPF_DOUBLE_SIGN_POS)); +#endif + } + if ((fs.conv_spec == NPF_FMT_SPEC_CONV_FLOAT_HEX) && + ((cbuf_len = npf_atoa_rev(cbuf, &fs, val)) > 0)) { + need_0x = (char)('X' + fs.case_adjust); + } else + { cbuf_len = npf_ftoa_rev(cbuf, &fs, val); } +#else sign_c = (npf_double_to_int_rep(val) >> NPF_DOUBLE_SIGN_POS) ? '-' : fs.prepend; #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 zero = (val == 0.); #endif cbuf_len = npf_ftoa_rev(cbuf, &fs, val); +#endif if (cbuf_len < 0) { // negative means text (not number), so ignore the '0' flag cbuf_len = -cbuf_len; #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 @@ -1069,8 +1160,11 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { // float precision is after the decimal point if ((fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_DEC) && (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_SCI) && - (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_SHORTEST) && - (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_HEX)) + (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_SHORTEST) +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 + && (fs.conv_spec != NPF_FMT_SPEC_CONV_FLOAT_HEX) +#endif + ) #endif { prec_pad = NPF_MAX(0, fs.prec - cbuf_len); } } @@ -1094,10 +1188,18 @@ int npf_vpprintf(npf_putc pc, void *pc_ctx, char const *format, va_list args) { } while (field_pad-- > 0) { NPF_PUTC(pad_c); } // Pad byte is ' ', write '0x' after ' ' pad chars but before number. - if ((pad_c != '0') && need_0x) { NPF_PUTC('0'); NPF_PUTC(need_0x); } + if ((pad_c != '0') && need_0x) { + if (sign_c) { NPF_PUTC(sign_c); sign_c = 0; } + NPF_PUTC('0'); NPF_PUTC(need_0x); + } } else #endif - { if (need_0x) { NPF_PUTC('0'); NPF_PUTC(need_0x); } } // no pad, '0x' requested. + { // no pad, '0x' requested. + if (need_0x) { + if (sign_c) { NPF_PUTC(sign_c); sign_c = 0; } + NPF_PUTC('0'); NPF_PUTC(need_0x); + } + } // Write the converted payload if (fs.conv_spec == NPF_FMT_SPEC_CONV_STRING) { diff --git a/tests/conformance.c b/tests/conformance.c index 979774af..6e6ed5a9 100644 --- a/tests/conformance.c +++ b/tests/conformance.c @@ -188,10 +188,15 @@ int NPF_TEST_FUNC(void) { NPF_TEST("0", "%hhd", 256); NPF_TEST("-1", "%hhd", 255); NPF_TEST("127", "%hhd", 127); - NPF_TEST_SYS("%hhd", CHAR_MAX); - NPF_TEST_SYS("%hhd", CHAR_MIN); - NPF_TEST_SYS("%hd", SHRT_MAX); - NPF_TEST_SYS("%hd", SHRT_MIN); +#if CHAR_MIN < 0 + NPF_TEST("127", "%hhd", CHAR_MAX); + NPF_TEST("-128", "%hhd", CHAR_MIN); +#else + NPF_TEST("-1", "%hhd", CHAR_MAX); + NPF_TEST("0", "%hhd", CHAR_MIN); +#endif + NPF_TEST("32767", "%hd", SHRT_MAX); + NPF_TEST("-32768", "%hd", SHRT_MIN); #endif #if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 @@ -372,10 +377,15 @@ int NPF_TEST_FUNC(void) { #endif /* extremal signed integer values */ - NPF_TEST_SYS("%d", INT_MIN); - NPF_TEST_SYS("%d", INT_MAX); - NPF_TEST_SYS("%ld", LONG_MIN); - NPF_TEST_SYS("%ld", LONG_MAX); + NPF_TEST("-2147483648", "%d", INT_MIN); + NPF_TEST("2147483647", "%d", INT_MAX); +#if LONG_MAX == 2147483647L + NPF_TEST("-2147483648", "%ld", LONG_MIN); + NPF_TEST("2147483647", "%ld", LONG_MAX); +#else + NPF_TEST("-9223372036854775808", "%ld", LONG_MIN); + NPF_TEST("9223372036854775807", "%ld", LONG_MAX); +#endif #if (NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 1) #if LLONG_MAX == 9223372036854775807ll @@ -417,8 +427,13 @@ int NPF_TEST_FUNC(void) { NPF_TEST("-9223372036854775807", "%lli", -9223372036854775807LL); NPF_TEST("9223372036854775807", "%lli", 9223372036854775807LL); NPF_TEST("-2147483647", "%ji", (intmax_t)-2147483647L); - NPF_TEST_SYS("%lld", LLONG_MIN); - NPF_TEST_SYS("%lld", LLONG_MAX); +#if LLONG_MAX == 9223372036854775807ll + NPF_TEST("-9223372036854775808", "%lld", LLONG_MIN); + NPF_TEST("9223372036854775807", "%lld", LLONG_MAX); +#else + NPF_TEST("-2147483648", "%lld", LLONG_MIN); + NPF_TEST("2147483647", "%lld", LLONG_MAX); +#endif #endif /* NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS */ /* ===== unsigned int ===== */ @@ -443,8 +458,8 @@ int NPF_TEST_FUNC(void) { NPF_TEST("13", "%hu", (1 << 21u) + 13u); NPF_TEST("255", "%hhu", (unsigned char)0xFFU); NPF_TEST("4660", "%hu", (unsigned short)0x1234u); - NPF_TEST_SYS("%hhu", (unsigned char)UCHAR_MAX); - NPF_TEST_SYS("%hu", (unsigned short)USHRT_MAX); + NPF_TEST("255", "%hhu", (unsigned char)UCHAR_MAX); + NPF_TEST("65535", "%hu", (unsigned short)USHRT_MAX); #endif #if ULONG_MAX > UINT_MAX @@ -517,8 +532,11 @@ int NPF_TEST_FUNC(void) { NPF_TEST(" ", "%02.0u", 0U); #endif - NPF_TEST_SYS("%u", UINT_MAX); - NPF_TEST_SYS("%lu", ULONG_MAX); +#if ULONG_MAX == 4294967295UL + NPF_TEST("4294967295", "%lu", ULONG_MAX); +#else + NPF_TEST("18446744073709551615", "%lu", ULONG_MAX); +#endif #if (NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS == 1) #if ULLONG_MAX == 18446744073709551615llu @@ -549,7 +567,6 @@ int NPF_TEST_FUNC(void) { NPF_TEST("a", "%tx", &ptrdiff_buf[10] - &ptrdiff_buf[0]); } - NPF_TEST_SYS("%llu", ULLONG_MAX); #endif /* NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS */ /* ===== octal ===== */ @@ -1217,6 +1234,153 @@ int NPF_TEST_FUNC(void) { /* misc float */ NPF_TEST("-67224.54687500000000000", "%.17f", -67224.546875); NPF_TEST("0.33", "%.*f", 2, 0.33333333); + + /* ===== hex float (%a / %A) ===== */ +#if NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER == 1 + /* basic zero */ + NPF_TEST("0x0p+0", "%.0a", 0.0); + NPF_TEST("-0x0p+0", "%.0a", -0.0); + NPF_TEST("0x0.0p+0", "%.1a", 0.0); + NPF_TEST("0x0.00p+0", "%.2a", 0.0); + + /* case: %A vs %a */ + NPF_TEST("0X0P+0", "%.0A", 0.0); + NPF_TEST("0X1P+0", "%.0A", 1.0); + NPF_TEST("0x1p+0", "%.0a", 1.0); + NPF_TEST("0X1.8P+0", "%.1A", 1.5); + + /* special values: inf */ + NPF_TEST("inf", "%a", (double)INFINITY); + NPF_TEST("-inf", "%a", -(double)INFINITY); + NPF_TEST("INF", "%A", (double)INFINITY); + NPF_TEST("-INF", "%A", -(double)INFINITY); + NPF_TEST("+inf", "%+a", (double)INFINITY); + NPF_TEST(" inf", "% a", (double)INFINITY); + + /* special values: nan */ + NPF_TEST("nan", "%a", (double)NAN); + NPF_TEST("-nan", "%a", -(double)NAN); + NPF_TEST("NAN", "%A", (double)NAN); + NPF_TEST("+nan", "%+a", (double)NAN); + NPF_TEST(" nan", "% a", (double)NAN); + +#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 + NPF_TEST(" nan", "%10a", (double)NAN); + NPF_TEST("nan ", "%-10a", (double)NAN); + NPF_TEST(" inf", "%10a", (double)INFINITY); + NPF_TEST(" -inf", "%10a", -(double)INFINITY); +#endif + + /* basic normals */ + NPF_TEST("0x1p+0", "%.0a", 1.0); + NPF_TEST("0x1.8p+0", "%.1a", 1.5); + NPF_TEST("0x1.0p+1", "%.1a", 2.0); + NPF_TEST("0x1.4p+1", "%.1a", 2.5); + NPF_TEST("0x1.8p+1", "%.1a", 3.0); + NPF_TEST("0x1.0p+2", "%.1a", 4.0); + NPF_TEST("-0x1.8p+0", "%.1a", -1.5); + + /* powers of 2 */ + NPF_TEST("0x1.0p-1", "%.1a", 0.5); + NPF_TEST("0x1.0p-2", "%.1a", 0.25); + NPF_TEST("0x1.0p-3", "%.1a", 0.125); + NPF_TEST("0x1.0p+10", "%.1a", 1024.0); + + /* negative zero with flags */ + NPF_TEST("-0x0.0000000000000p+0", "%a", -0.0); + NPF_TEST("-0x0p+0", "%.0a", -0.0); + NPF_TEST("+0x0p+0", "%+.0a", 0.0); + NPF_TEST("-0x0p+0", "%+.0a", -0.0); + + /* default precision: 13 hex digits for float64 */ + NPF_TEST("0x0.0000000000000p+0", "%a", 0.0); + NPF_TEST("0x1.0000000000000p+0", "%a", 1.0); + NPF_TEST("0x1.8000000000000p+0", "%a", 1.5); + NPF_TEST("-0x1.999999999999ap-4", "%a", -0.1); + + /* explicit precision */ + NPF_TEST("0x1.921fb54442d18p+2", "%a", 6.283185307179586); + NPF_TEST("0x1.9p+2", "%.1a", 6.283185307179586); + NPF_TEST("0x1.92p+2", "%.2a", 6.283185307179586); + NPF_TEST("0x1.922p+2", "%.3a", 6.283185307179586); + + /* rounding */ + NPF_TEST("0x1p+0", "%.0a", 1.0); + NPF_TEST("0x1.cp+0", "%.1a", 1.75); + NPF_TEST("0x1.0000000000000p+0", "%.13a", 1.0); + + /* rounding carry propagation: 0x1.ff8 -> %.1a rounds up nibble f */ + NPF_TEST("0x2.0p+0", "%.1a", 1.99609375); /* 0x1.ffp+0 -> carry ripples */ + NPF_TEST("0x2p+0", "%.0a", 1.99609375); /* 0x1.ffp+0 -> carry to lead */ + NPF_TEST("0x1.f0p+0", "%.2a", 1.9375); /* 0x1.f0p+0 -> no round needed */ + NPF_TEST("0x1.fp+0", "%.1a", 1.9375); /* 0x1.f0p+0 -> truncate zeros */ + + /* sign flags */ + NPF_TEST("+0x1p+0", "%+.0a", 1.0); + NPF_TEST("-0x1p+0", "%+.0a", -1.0); + NPF_TEST(" 0x1p+0", "% .0a", 1.0); + NPF_TEST("-0x1p+0", "% .0a", -1.0); + +#if NANOPRINTF_USE_ALT_FORM_FLAG == 1 + /* # flag: always show decimal point */ + NPF_TEST("0x1.p+0", "%#.0a", 1.0); + NPF_TEST("0x0.p+0", "%#.0a", 0.0); + NPF_TEST("-0x0.p+0", "%#.0a", -0.0); +#endif + +#if NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS == 1 + /* field width, right-justified */ + NPF_TEST(" 0x1p+0", "%10.0a", 1.0); + NPF_TEST(" -0x1p+0", "%10.0a", -1.0); + + /* field width, left-justified */ + NPF_TEST("0x1p+0 ", "%-10.0a", 1.0); + NPF_TEST("-0x1p+0 ", "%-10.0a", -1.0); + + /* zero-padded: sign -> 0x -> zeros -> digits */ + NPF_TEST("0x001p+0", "%08.0a", 1.0); + NPF_TEST("-0x01p+0", "%08.0a", -1.0); + NPF_TEST("+0x01p+0", "%+08.0a", 1.0); + NPF_TEST(" 0x01p+0", "% 08.0a", 1.0); + + /* space flag with width */ + NPF_TEST(" 0x1p+0 ", "% -10.0a", 1.0); + NPF_TEST("-0x1p+0 ", "% -10.0a", -1.0); + + /* width + precision combos */ + NPF_TEST(" 0x1.80p+0", "%11.2a", 1.5); + NPF_TEST("0x1.80p+0 ", "%-11.2a", 1.5); + NPF_TEST("0x001.80p+0", "%011.2a", 1.5); +#endif + + /* long double */ + NPF_TEST("0x1.8000000000000p+0", "%La", (long double)1.5); + NPF_TEST("-0x1.0000000000000p+0", "%La", (long double)-1.0); + + /* return value */ + NPF_TEST_RET(6, "%.0a", 1.0); /* "0x1p+0" = 6 */ + NPF_TEST_RET(7, "%.0a", -1.0); /* "-0x1p+0" = 7 */ + NPF_TEST_RET(20, "%a", 1.0); /* "0x1.0000000000000p+0" = 20 */ + NPF_TEST_RET(3, "%a", (double)NAN); /* "nan" = 3 */ + NPF_TEST_RET(3, "%a", (double)INFINITY); /* "inf" = 3 */ + + /* large exponent */ + NPF_TEST("0x1.fffffffffffffp+1023", "%a", 1.7976931348623157e+308); + + /* smallest normal (DBL_MIN) */ + NPF_TEST("0x1.0000000000000p-1022", "%a", 2.2250738585072014e-308); + NPF_TEST("0x1p-1022", "%.0a", 2.2250738585072014e-308); + + /* subnormals */ + NPF_TEST("0x0.0000000000001p-1022", "%a", 5e-324); + NPF_TEST("0x0p-1022", "%.0a", 5e-324); + NPF_TEST("0x0.8000000000000p-1022", "%a", 1.1125369292536007e-308); + NPF_TEST("0x0.8p-1022", "%.1a", 1.1125369292536007e-308); + + /* multi-conversion in one format string */ + NPF_TEST("0x1p+0 0x1.8p+0", "%.0a %.1a", 1.0, 1.5); +#endif /* NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER */ + #endif /* NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS */ /* ===== non-standard specifiers ===== */ diff --git a/tests/gen_tests.py b/tests/gen_tests.py index e885af87..36edb31c 100644 --- a/tests/gen_tests.py +++ b/tests/gen_tests.py @@ -113,6 +113,9 @@ def define_flags(combo: dict[str, int], idx: int, *, msvc: bool = False) -> str: """Return the -D flags string for a single combo.""" pfx = "/D" if msvc else "-D" parts = [f"{pfx}{k}={v}" for k, v in combo.items()] + # Enable hex float whenever float is enabled + hex_val = combo.get("NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS", 0) + parts.append(f"{pfx}NANOPRINTF_USE_FLOAT_HEX_FORMAT_SPECIFIER={hex_val}") parts.append(f"{pfx}NPF_TEST_FUNC=npf_test_combo_{idx}") parts.append(f"{pfx}NPF_TEST_PASS_COUNT=npf_test_combo_{idx}_pass_count") return " ".join(parts) diff --git a/tests/test_harness.h b/tests/test_harness.h index 523ad323..e2167277 100644 --- a/tests/test_harness.h +++ b/tests/test_harness.h @@ -33,17 +33,6 @@ static void npf_test_null_putc(int c, void *ctx) { (void)c; (void)ctx; } } else { ++npf_test_pass_count; } \ } while(0) -/* NPF_TEST_SYS: like require_conform(nullptr,...) -- compare against system printf */ -#define NPF_TEST_SYS(fmt, ...) do { \ - snprintf(npf_test_sys_buf, sizeof(npf_test_sys_buf), fmt, ##__VA_ARGS__); \ - npf_snprintf(npf_test_buf, sizeof(npf_test_buf), fmt, ##__VA_ARGS__); \ - if (strcmp(npf_test_buf, npf_test_sys_buf) != 0) { \ - fprintf(stderr, "FAIL [%s:%d]: fmt=\"%s\" sys=\"%s\" got=\"%s\"\n", \ - __FILE__, __LINE__, fmt, npf_test_sys_buf, npf_test_buf); \ - ++npf_test_fail_count; \ - } else { ++npf_test_pass_count; } \ -} while(0) - #define NPF_TEST_RET(expected_ret, fmt, ...) do { \ int npf_test_ret_ = npf_snprintf(npf_test_buf, sizeof(npf_test_buf), fmt, ##__VA_ARGS__); \ if (npf_test_ret_ != (expected_ret)) { \ diff --git a/tests/unit_nanoprintf.h b/tests/unit_nanoprintf.h index 4d1f8af4..c3a83781 100644 --- a/tests/unit_nanoprintf.h +++ b/tests/unit_nanoprintf.h @@ -4,6 +4,7 @@ #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_FLOAT_HEX_FORMAT_SPECIFIER 1 #define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1 #define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 1 #define NANOPRINTF_USE_ALT_FORM_FLAG 1