diff --git a/README.md b/README.md index 0b39488..f86b7e4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Therefore I decided to write an own, final implementation which meets the follow - Support of decimal/floating number representation (with an own fast itoa/ftoa) - Reentrant and thread-safe, malloc free, no static vars/buffers - LINT and compiler L4 warning free, mature, coverity clean, automotive ready - - Extensive test suite (> 370 test cases) passing + - Extensive test suite (> 390 test cases) passing - Simply the best *printf* around the net - MIT license @@ -92,6 +92,8 @@ The following format specifiers are supported: | x | Unsigned hexadecimal integer (lowercase) | | X | Unsigned hexadecimal integer (uppercase) | | f or F | Decimal floating point | +| e or E | Scientific-notation (exponential) floating point | +| g or G | Scientific or decimal floating point | | c | Single character | | s | String of characters | | p | Pointer address | @@ -164,6 +166,7 @@ int length = sprintf(NULL, "Hello, world"); // length is set to 12 | PRINTF_NTOA_BUFFER_SIZE | 32 | ntoa (integer) conversion buffer size. This must be big enough to hold one converted numeric number _including_ leading zeros, normally 32 is a sufficient value. Created on the stack | | PRINTF_FTOA_BUFFER_SIZE | 32 | ftoa (float) conversion buffer size. This must be big enough to hold one converted float number _including_ leading zeros, normally 32 is a sufficient value. Created on the stack | | PRINTF_DISABLE_SUPPORT_FLOAT | undefined | Define this to disable floating point (%f) support | +| PRINTF_DISABLE_SUPPORT_EXPONENTIAL | undefined | Define this to disable exponential floating point (%e) support | | PRINTF_DISABLE_SUPPORT_LONG_LONG | undefined | Define this to disable long long (%ll) support | | PRINTF_DISABLE_SUPPORT_PTRDIFF_T | undefined | Define this to disable ptrdiff_t (%t) support | diff --git a/printf.c b/printf.c index ed2b3ba..1770122 100644 --- a/printf.c +++ b/printf.c @@ -64,6 +64,21 @@ #define PRINTF_SUPPORT_FLOAT #endif +// support for exponential floating point notation (%e/%g) +#ifndef PRINTF_DISABLE_SUPPORT_EXPONENTIAL +#define PRINTF_SUPPORT_EXPONENTIAL +#endif + +// define the default floating point precision +#ifndef PRINTF_DEFAULT_FLOAT_PRECISION +#define PRINTF_DEFAULT_FLOAT_PRECISION 6U +#endif + +// define the largest float suitable to print with %f +#ifndef PRINTF_MAX_FLOAT +#define PRINTF_MAX_FLOAT 1e9 +#endif + // support for the long long types (%llu or %p) // default: activated #ifndef PRINTF_DISABLE_SUPPORT_LONG_LONG @@ -91,6 +106,7 @@ #define FLAGS_LONG (1U << 8U) #define FLAGS_LONG_LONG (1U << 9U) #define FLAGS_PRECISION (1U << 10U) +#define FLAGS_ADAPT_EXP (1U << 11U) // output function type @@ -169,12 +185,34 @@ static unsigned int _atoi(const char** str) return i; } +// output the specified string in reverse, taking care of any zero-padding +static size_t _out_rev(out_fct_type out, char* buffer, size_t idx, size_t maxlen, const char* buf, size_t len, unsigned int width, unsigned int flags) +{ + const size_t start_idx = idx; + + // pad spaces up to given width + if (!(flags & FLAGS_LEFT) && !(flags & FLAGS_ZEROPAD)) { + for (size_t i = len; i < width; i++) { + out(' ', buffer, idx++, maxlen); + } + } + + // reverse string + while (len) out(buf[--len], buffer, idx++, maxlen); + + // append pad spaces up to given width + if (flags & FLAGS_LEFT) { + while (idx - start_idx < width) { + out(' ', buffer, idx++, maxlen); + } + } + + return idx; +} // internal itoa format static size_t _ntoa_format(out_fct_type out, char* buffer, size_t idx, size_t maxlen, char* buf, size_t len, bool negative, unsigned int base, unsigned int prec, unsigned int width, unsigned int flags) { - const size_t start_idx = idx; - // pad leading zeros if (!(flags & FLAGS_LEFT)) { if (width && (flags & FLAGS_ZEROPAD) && (negative || (flags & (FLAGS_PLUS | FLAGS_SPACE)))) { @@ -222,26 +260,7 @@ static size_t _ntoa_format(out_fct_type out, char* buffer, size_t idx, size_t ma } } - // pad spaces up to given width - if (!(flags & FLAGS_LEFT) && !(flags & FLAGS_ZEROPAD)) { - for (size_t i = len; i < width; i++) { - out(' ', buffer, idx++, maxlen); - } - } - - // reverse string - for (size_t i = 0U; i < len; i++) { - out(buf[len - i - 1U], buffer, idx++, maxlen); - } - - // append pad spaces up to given width - if (flags & FLAGS_LEFT) { - while (idx - start_idx < width) { - out(' ', buffer, idx++, maxlen); - } - } - - return idx; + return _out_rev(out, buffer, idx, maxlen, buf, len, width, flags); } @@ -296,26 +315,38 @@ static size_t _ntoa_long_long(out_fct_type out, char* buffer, size_t idx, size_t #if defined(PRINTF_SUPPORT_FLOAT) +#include +#if defined(PRINTF_SUPPORT_EXPONENTIAL) +// forward declaration so that _ftoa can switch to exp notation for values > PRINTF_MAX_FLOAT +static size_t _etoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags); +#endif + +// internal ftoa for fixed decimal floating point static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags) { - const size_t start_idx = idx; - char buf[PRINTF_FTOA_BUFFER_SIZE]; size_t len = 0U; double diff = 0.0; - // if input is larger than thres_max, revert to exponential - const double thres_max = (double)0x7FFFFFFF; - // powers of 10 static const double pow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 }; - // test for NaN - if (value != value) { - out('n', buffer, idx++, maxlen); - out('a', buffer, idx++, maxlen); - out('n', buffer, idx++, maxlen); - return idx; + // test for special values + if (value != value) + return _out_rev(out, buffer, idx, maxlen, "nan", 3, width, flags); + if (value < -DBL_MAX) + return _out_rev(out, buffer, idx, maxlen, "fni-", 4, width, flags); + if (value > DBL_MAX) + return _out_rev(out, buffer, idx, maxlen, (flags & FLAGS_PLUS) ? "fni+" : "fni", (flags & FLAGS_PLUS) ? 4 : 3, width, flags); + + // test for very large values + // standard printf behavior is to print EVERY whole number digit -- which could be 100s of characters overflowing your buffers == bad + if ((value > PRINTF_MAX_FLOAT)||(value < -PRINTF_MAX_FLOAT)) { +#if defined(PRINTF_SUPPORT_EXPONENTIAL) + return _etoa(out, buffer, idx, maxlen, value, prec, width, flags); +#else + return 0U; +#endif } // test for negative @@ -325,9 +356,9 @@ static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, d value = 0 - value; } - // set default precision to 6, if not set explicitly + // set default precision, if not set explicitly if (!(flags & FLAGS_PRECISION)) { - prec = 6U; + prec = PRINTF_DEFAULT_FLOAT_PRECISION; } // limit precision to 9, cause a prec >= 10 can lead to overflow errors while ((len < PRINTF_FTOA_BUFFER_SIZE) && (prec > 9U)) { @@ -355,12 +386,6 @@ static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, d ++frac; } - // TBD: for very large numbers switch back to native sprintf for exponentials. Anyone want to write code to replace this? - // Normal printf behavior is to print EVERY whole number digit which can be 100s of characters overflowing your buffers == bad - if (value > thres_max) { - return 0U; - } - if (prec == 0U) { diff = value - (double)whole; if ((!(diff < 0.5) || (diff > 0.5)) && (whole & 1)) { @@ -419,27 +444,109 @@ static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, d } } - // pad spaces up to given width - if (!(flags & FLAGS_LEFT) && !(flags & FLAGS_ZEROPAD)) { - for (size_t i = len; i < width; i++) { - out(' ', buffer, idx++, maxlen); + return _out_rev(out, buffer, idx, maxlen, buf, len, width, flags); +} + +#if defined(PRINTF_SUPPORT_EXPONENTIAL) +// internal ftoa variant for exponential floating-point type +// contributed by Martijn Jasperse +static size_t _etoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags) +{ + // check for special values + if ((value != value)||(value > DBL_MAX)||(value < -DBL_MAX)) + return _ftoa(out, buffer, idx, maxlen, value, prec, width, flags); + + // determine the sign + bool negative = value < 0; + if (negative) value = -value; + + // default precision + if (!(flags & FLAGS_PRECISION)) { + prec = PRINTF_DEFAULT_FLOAT_PRECISION; + } + + // determine the decimal exponent + // based on the algorithm by David Gay (https://www.ampl.com/netlib/fp/dtoa.c) + union { + uint64_t U; + double F; + } conv; + conv.F = value; + int exp2 = (int)((conv.U >> 52) & 0x07FF) - 1023; // effectively log2 + conv.U = (conv.U & ((1ULL << 52) - 1)) | (1023ULL << 52); // drop the exponent so conv.F is now in [1,2) + // now approximate log10 from the log2 integer part and an expansion of ln around 1.5 + int expval = (int)(0.1760912590558 + exp2*0.301029995663981 + (conv.F - 1.5)*0.289529654602168); + // now we want to compute 10^expval but we want to be sure it won't overflow + exp2 = (int)(expval*3.321928094887362 + 0.5); + double z = expval*2.302585092994046 - exp2*0.6931471805599453; + double z2 = z*z; + conv.U = (uint64_t)(exp2 + 1023) << 52; + // compute exp(z) using continued fractions + // https://en.wikipedia.org/wiki/Exponential_function#Continued_fractions_for_ex + conv.F *= 1 + 2*z/(2 - z + (z2/(6 + (z2/(10 + z2/14))))); + // correct for rounding errors + if (value < conv.F) { + expval--; + conv.F /= 10; + } + + // the exponent format is "%+03d" and largest value is "307", so set aside 4-5 characters + unsigned int minwidth = ((expval < 100)&&(expval > -100)) ? 4 : 5; + + // in "%g" mode, "prec" is the number of *significant figures* not decimals + if (flags & FLAGS_ADAPT_EXP) { + // do we want to fall-back to "%f" mode? + if ((value >= 1e-4)&&(value < 1e6)) { + if ((int)prec > expval) { + prec = (unsigned)((int)prec - expval - 1); + } else { + prec = 0; + } + flags |= FLAGS_PRECISION; // make sure _ftoa respects precision + // no characters in exponent + minwidth = 0; + expval = 0; + } else { + // we use one sigfig for the whole part + if ((prec > 0)&&(flags & FLAGS_PRECISION)) --prec; } } - - // reverse string - for (size_t i = 0U; i < len; i++) { - out(buf[len - i - 1U], buffer, idx++, maxlen); + // will everything fit? + unsigned int fwidth = width; + if (width > minwidth) { + // we didn't fall-back so subtract the characters required for the exponent + fwidth -= minwidth; + } else { + // not enough characters, so go back to default sizing + fwidth = 0; + } + if ((flags & FLAGS_LEFT) && minwidth) { + // if we're padding on the right, DON'T pad the floating part + fwidth = 0; } - // append pad spaces up to given width - if (flags & FLAGS_LEFT) { - while (idx - start_idx < width) { - out(' ', buffer, idx++, maxlen); + // rescale the float value + if (expval) value /= conv.F; + + // output the floating part + const size_t start_idx = idx; + idx = _ftoa(out, buffer, idx, maxlen, negative ? -value : value, prec, fwidth, flags & ~FLAGS_ADAPT_EXP); + + // output the exponent part + if (minwidth) { + // output the exponential symbol + out((flags & FLAGS_UPPERCASE) ? 'E' : 'e', buffer, idx++, maxlen); + // output the exponent value + idx = _ntoa_long(out, buffer, idx, maxlen, (expval < 0) ? -expval : expval, expval < 0, 10, 0, minwidth-1, FLAGS_ZEROPAD | FLAGS_PLUS); + // might need to right-pad spaces + if (flags & FLAGS_LEFT) { + while (idx - start_idx < width) out(' ', buffer, idx++, maxlen); } } - return idx; } + +#endif // PRINTF_SUPPORT_EXPONENTIAL #endif // PRINTF_SUPPORT_FLOAT @@ -627,9 +734,21 @@ static int _vsnprintf(out_fct_type out, char* buffer, const size_t maxlen, const #if defined(PRINTF_SUPPORT_FLOAT) case 'f' : case 'F' : + if (*format == 'F') flags |= FLAGS_UPPERCASE; idx = _ftoa(out, buffer, idx, maxlen, va_arg(va, double), precision, width, flags); format++; break; +#if defined(PRINTF_SUPPORT_EXPONENTIAL) + case 'e': + case 'E': + case 'g': + case 'G': + if ((*format == 'g')||(*format == 'G')) flags |= FLAGS_ADAPT_EXP; + if ((*format == 'E')||(*format == 'G')) flags |= FLAGS_UPPERCASE; + idx = _etoa(out, buffer, idx, maxlen, va_arg(va, double), precision, width, flags); + format++; + break; +#endif // PRINTF_SUPPORT_EXPONENTIAL #endif // PRINTF_SUPPORT_FLOAT case 'c' : { unsigned int l = 1U; diff --git a/test/test_suite.cpp b/test/test_suite.cpp index 00e5f48..a113db0 100644 --- a/test/test_suite.cpp +++ b/test/test_suite.cpp @@ -346,6 +346,13 @@ TEST_CASE("- flag", "[]" ) { test::sprintf(buffer, "%0-15d", -42); REQUIRE(!strcmp(buffer, "-42 ")); + + test::sprintf(buffer, "%0-15.3e", -42.); + REQUIRE(!strcmp(buffer, "-4.200e+01 ")); + + test::sprintf(buffer, "%0-15.3g", -42.); + REQUIRE(!strcmp(buffer, "-42.0 ")); + } @@ -915,6 +922,15 @@ TEST_CASE("float padding neg numbers", "[]" ) { test::sprintf(buffer, "% 5.1f", -5.); REQUIRE(!strcmp(buffer, " -5.0")); + test::sprintf(buffer, "% 6.1g", -5.); + REQUIRE(!strcmp(buffer, " -5")); + + test::sprintf(buffer, "% 6.1e", -5.); + REQUIRE(!strcmp(buffer, "-5.0e+00")); + + test::sprintf(buffer, "% 10.1e", -5.); + REQUIRE(!strcmp(buffer, " -5.0e+00")); + // zero padding test::sprintf(buffer, "%03.1f", -5.); @@ -925,6 +941,9 @@ TEST_CASE("float padding neg numbers", "[]" ) { test::sprintf(buffer, "%05.1f", -5.); REQUIRE(!strcmp(buffer, "-05.0")); + + test::sprintf(buffer, "%010.1e", -5.); + REQUIRE(!strcmp(buffer, "-005.0e+00")); // zero padding no decimal point @@ -936,6 +955,12 @@ TEST_CASE("float padding neg numbers", "[]" ) { test::sprintf(buffer, "%03.0f", -5.); REQUIRE(!strcmp(buffer, "-05")); + + test::sprintf(buffer, "%07.0E", -5.); + REQUIRE(!strcmp(buffer, "-05E+00")); + + test::sprintf(buffer, "%03.0g", -5.); + REQUIRE(!strcmp(buffer, "-05")); } TEST_CASE("length", "[]" ) { @@ -1024,9 +1049,19 @@ TEST_CASE("length", "[]" ) { TEST_CASE("float", "[]" ) { char buffer[100]; - test::sprintf(buffer, "%.4f", NAN); // using the NAN macro of math.h - REQUIRE(!strcmp(buffer, "nan")); + // test special-case floats using math.h macros + test::sprintf(buffer, "%8f", NAN); + REQUIRE(!strcmp(buffer, " nan")); + + test::sprintf(buffer, "%8f", INFINITY); + REQUIRE(!strcmp(buffer, " inf")); + test::sprintf(buffer, "%-8f", -INFINITY); + REQUIRE(!strcmp(buffer, "-inf ")); + + test::sprintf(buffer, "%+8e", INFINITY); + REQUIRE(!strcmp(buffer, " +inf")); + test::sprintf(buffer, "%.4f", 3.1415354); REQUIRE(!strcmp(buffer, "3.1415")); @@ -1106,9 +1141,39 @@ TEST_CASE("float", "[]" ) { test::sprintf(buffer, "a%-5.1fend", 0.5); REQUIRE(!strcmp(buffer, "a0.5 end")); - // out of range in the moment, need to be fixed by someone + test::sprintf(buffer, "%G", 12345.678); + REQUIRE(!strcmp(buffer, "12345.7")); + + test::sprintf(buffer, "%.7G", 12345.678); + REQUIRE(!strcmp(buffer, "12345.68")); + + test::sprintf(buffer, "%.5G", 123456789.); + REQUIRE(!strcmp(buffer, "1.2346E+08")); + + test::sprintf(buffer, "%.6G", 12345.); + REQUIRE(!strcmp(buffer, "12345.0")); + + test::sprintf(buffer, "%+12.4g", 123456789.); + REQUIRE(!strcmp(buffer, " +1.235e+08")); + + test::sprintf(buffer, "%.2G", 0.001234); + REQUIRE(!strcmp(buffer, "0.0012")); + + test::sprintf(buffer, "%+10.4G", 0.001234); + REQUIRE(!strcmp(buffer, " +0.001234")); + + test::sprintf(buffer, "%+012.4g", 0.00001234); + REQUIRE(!strcmp(buffer, "+001.234e-05")); + + test::sprintf(buffer, "%.3g", -1.2345e-308); + REQUIRE(!strcmp(buffer, "-1.23e-308")); + + test::sprintf(buffer, "%+.3E", 1.23e+308); + REQUIRE(!strcmp(buffer, "+1.230E+308")); + + // out of range for float: should switch to exp notation test::sprintf(buffer, "%.1f", 1E20); - REQUIRE(!strcmp(buffer, "")); + REQUIRE(!strcmp(buffer, "1.0e+20")); } @@ -1362,6 +1427,12 @@ TEST_CASE("misc", "[]" ) { test::sprintf(buffer, "%.*f", 2, 0.33333333); REQUIRE(!strcmp(buffer, "0.33")); + test::sprintf(buffer, "%.*g", 2, 0.33333333); + REQUIRE(!strcmp(buffer, "0.33")); + + test::sprintf(buffer, "%.*e", 2, 0.33333333); + REQUIRE(!strcmp(buffer, "3.33e-01")); + test::sprintf(buffer, "%.*d", -1, 1); REQUIRE(!strcmp(buffer, "1"));