c - representar - punto flotante normalizado
Especificador de ancho de impresión para mantener la precisión del valor de coma flotante (6)
En uno de mis comentarios a una respuesta, me lamenté de haber deseado durante mucho tiempo alguna forma de imprimir todos los dígitos significativos en un valor de punto flotante en forma decimal, de la misma forma en que lo hace la pregunta. Bueno, finalmente me senté y lo escribí. No es perfecto, y este es un código de demostración que imprime información adicional, pero funciona principalmente para mis pruebas. Por favor, avíseme si usted (es decir, alguien) desea una copia de todo el programa envoltorio que lo impulsa a probar.
static unsigned int
ilog10(uintmax_t v);
/*
* Note: As presented this demo code prints a whole line including information
* about how the form was arrived with, as well as in certain cases a couple of
* interesting details about the number, such as the number of decimal places,
* and possibley the magnitude of the value and the number of significant
* digits.
*/
void
print_decimal(double d)
{
size_t sigdig;
int dplaces;
double flintmax;
/*
* If we really want to see a plain decimal presentation with all of
* the possible significant digits of precision for a floating point
* number, then we must calculate the correct number of decimal places
* to show with "%.*f" as follows.
*
* This is in lieu of always using either full on scientific notation
* with "%e" (where the presentation is always in decimal format so we
* can directly print the maximum number of significant digits
* supported by the representation, taking into acount the one digit
* represented by by the leading digit)
*
* printf("%1.*e", DBL_DECIMAL_DIG - 1, d)
*
* or using the built-in human-friendly formatting with "%g" (where a
* ''*'' parameter is used as the number of significant digits to print
* and so we can just print exactly the maximum number supported by the
* representation)
*
* printf("%.*g", DBL_DECIMAL_DIG, d)
*
*
* N.B.: If we want the printed result to again survive a round-trip
* conversion to binary and back, and to be rounded to a human-friendly
* number, then we can only print DBL_DIG significant digits (instead
* of the larger DBL_DECIMAL_DIG digits).
*
* Note: "flintmax" here refers to the largest consecutive integer
* that can be safely stored in a floating point variable without
* losing precision.
*/
#ifdef PRINT_ROUND_TRIP_SAFE
# ifdef DBL_DIG
sigdig = DBL_DIG;
# else
sigdig = ilog10(uipow(FLT_RADIX, DBL_MANT_DIG - 1));
# endif
#else
# ifdef DBL_DECIMAL_DIG
sigdig = DBL_DECIMAL_DIG;
# else
sigdig = (size_t) lrint(ceil(DBL_MANT_DIG * log10((double) FLT_RADIX))) + 1;
# endif
#endif
flintmax = pow((double) FLT_RADIX, (double) DBL_MANT_DIG); /* xxx use uipow() */
if (d == 0.0) {
printf("z = %.*s/n", (int) sigdig + 1, "0.000000000000000000000"); /* 21 */
} else if (fabs(d) >= 0.1 &&
fabs(d) <= flintmax) {
dplaces = (int) (sigdig - (size_t) lrint(ceil(log10(ceil(fabs(d))))));
if (dplaces < 0) {
/* XXX this is likely never less than -1 */
/*
* XXX the last digit is not significant!!! XXX
*
* This should also be printed with sprintf() and edited...
*/
printf("R = %.0f [%d too many significant digits!!!, zero decimal places]/n", d, abs(dplaces));
} else if (dplaces == 0) {
/*
* The decimal fraction here is not significant and
* should always be zero (XXX I''ve never seen this)
*/
printf("R = %.0f [zero decimal places]/n", d);
} else {
if (fabs(d) == 1.0) {
/*
* This is a special case where the calculation
* is off by one because log10(1.0) is 0, but
* we still have the leading ''1'' whole digit to
* count as a significant digit.
*/
#if 0
printf("ceil(1.0) = %f, log10(ceil(1.0)) = %f, ceil(log10(ceil(1.0))) = %f/n",
ceil(fabs(d)), log10(ceil(fabs(d))), ceil(log10(ceil(fabs(d)))));
#endif
dplaces--;
}
/* this is really the "useful" range of %f */
printf("r = %.*f [%d decimal places]/n", dplaces, d, dplaces);
}
} else {
if (fabs(d) < 1.0) {
int lz;
lz = abs((int) lrint(floor(log10(fabs(d)))));
/* i.e. add # of leading zeros to the precision */
dplaces = (int) sigdig - 1 + lz;
printf("f = %.*f [%d decimal places]/n", dplaces, d, dplaces);
} else { /* d > flintmax */
size_t n;
size_t i;
char *df;
/*
* hmmmm... the easy way to suppress the "invalid",
* i.e. non-significant digits is to do a string
* replacement of all dgits after the first
* DBL_DECIMAL_DIG to convert them to zeros, and to
* round the least significant digit.
*/
df = malloc((size_t) 1);
n = (size_t) snprintf(df, (size_t) 1, "%.1f", d);
n++; /* for the NUL */
df = realloc(df, n);
(void) snprintf(df, n, "%.1f", d);
if ((n - 2) > sigdig) {
/*
* XXX rounding the integer part here is "hard"
* -- we would have to convert the digits up to
* this point back into a binary format and
* round that value appropriately in order to
* do it correctly.
*/
if (df[sigdig] >= ''5'' && df[sigdig] <= ''9'') {
if (df[sigdig - 1] == ''9'') {
/*
* xxx fixing this is left as
* an exercise to the reader!
*/
printf("F = *** failed to round integer part at the least significant digit!!! ***/n");
free(df);
return;
} else {
df[sigdig - 1]++;
}
}
for (i = sigdig; df[i] != ''.''; i++) {
df[i] = ''0'';
}
} else {
i = n - 1; /* less the NUL */
if (isnan(d) || isinf(d)) {
sigdig = 0; /* "nan" or "inf" */
}
}
printf("F = %.*s. [0 decimal places, %lu digits, %lu digits significant]/n",
(int) i, df, (unsigned long int) i, (unsigned long int) sigdig);
free(df);
}
}
return;
}
static unsigned int
msb(uintmax_t v)
{
unsigned int mb = 0;
while (v >>= 1) { /* unroll for more speed... (see ilog2()) */
mb++;
}
return mb;
}
static unsigned int
ilog10(uintmax_t v)
{
unsigned int r;
static unsigned long long int const PowersOf10[] =
{ 1LLU, 10LLU, 100LLU, 1000LLU, 10000LLU, 100000LLU, 1000000LLU,
10000000LLU, 100000000LLU, 1000000000LLU, 10000000000LLU,
100000000000LLU, 1000000000000LLU, 10000000000000LLU,
100000000000000LLU, 1000000000000000LLU, 10000000000000000LLU,
100000000000000000LLU, 1000000000000000000LLU,
10000000000000000000LLU };
if (!v) {
return ~0U;
}
/*
* By the relationship "log10(v) = log2(v) / log2(10)", we need to
* multiply "log2(v)" by "1 / log2(10)", which is approximately
* 1233/4096, or (1233, followed by a right shift of 12).
*
* Finally, since the result is only an approximation that may be off
* by one, the exact value is found by subtracting "v < PowersOf10[r]"
* from the result.
*/
r = ((msb(v) * 1233) >> 12) + 1;
return r - (v < PowersOf10[r]);
}
¿Existe un especificador de ancho de impresión que se pueda aplicar a un especificador de punto flotante que formatee automáticamente la salida en el número necesario de dígitos significativos de modo que cuando se vuelva a escanear la cadena, se adquiera el valor de punto flotante original?
Por ejemplo, supongamos que imprimo un float
con una precisión de 2
decimales:
float foobar = 0.9375;
printf("%.2f", foobar); // prints out 0.94
Cuando escaneo la salida 0.94
, no tengo garantía de conformidad con los estándares de que recuperaré el valor 0.9375
de punto flotante original (en este ejemplo, probablemente no lo haga).
Me gustaría una forma de decirle a printf
que imprima automáticamente el valor de punto flotante en la cantidad necesaria de dígitos significativos para garantizar que se pueda escanear de nuevo al valor original pasado a printf
.
Podría usar algunas de las macros en float.h
para derivar el ancho máximo para pasar a printf
, pero ¿hay un especificador que imprima automáticamente en el número necesario de dígitos significativos , o al menos en el ancho máximo?
La respuesta corta para imprimir números de coma flotante sin pérdida (de modo que puedan volverse a leer exactamente al mismo número, excepto NaN e Infinity):
- Si su tipo es flotante: use
printf("%.9g", number)
. - Si su tipo es doble: use
printf("%.17g", number)
.
NO use %f
, ya que eso solo especifica cuántos dígitos significativos después del decimal y truncarán los números pequeños. Como referencia, los números mágicos 9 y 17 se pueden encontrar en float.h
que define FLT_DECIMAL_DIG
y DBL_DECIMAL_DIG
.
No, no existe tal ancho de impresión específico para imprimir punto flotante con la máxima precisión . Déjame explicar por qué.
La precisión máxima de float
y double
es variable , y depende del valor real de float
o double
.
Recordar float
y double
se almacenan en formato sign.exponent.mantissa . Esto significa que hay muchos más bits utilizados para el componente fraccionario para números pequeños que para números grandes.
Por ejemplo, el float
puede distinguir fácilmente entre 0.0 y 0.1.
float r = 0;
printf( "%.6f/n", r ) ; // 0.000000
r+=0.1 ;
printf( "%.6f/n", r ) ; // 0.100000
Pero el float
no tiene idea de la diferencia entre 1e27
y 1e27 + 0.1
.
r = 1e27;
printf( "%.6f/n", r ) ; // 999999988484154753734934528.000000
r+=0.1 ;
printf( "%.6f/n", r ) ; // still 999999988484154753734934528.000000
Esto se debe a que toda la precisión (que está limitada por el número de bits de mantisa) se utiliza para la gran parte del número, a la izquierda del decimal.
El modificador %.f
simplemente indica cuántos valores decimales desea imprimir desde el número de letra flotante en lo que respecta al formateo . El hecho de que la precisión disponible depende del tamaño del número depende de usted como el programador a manejar. printf
no puede / no maneja eso por ti.
Recomiendo la solución hexadecimal @Jens Gustedt: use% a.
OP quiere "imprimir con la máxima precisión (o al menos con el decimal más significativo)".
Un ejemplo simple sería imprimir una séptima como en:
#include <float.h>
int Digs = DECIMAL_DIG;
double OneSeventh = 1.0/7.0;
printf("%.*e/n", Digs, OneSeventh);
// 1.428571428571428492127e-01
Pero profundicemos más ...
Matemáticamente, la respuesta es "0.142857 142857 142857 ...", pero estamos usando números de coma flotante de precisión finita. Supongamos que es binario de doble precisión IEEE 754 . Entonces OneSeventh = 1.0/7.0
da como resultado el siguiente valor. También se muestran los números anteriores y siguientes de double
punto flotante representable.
OneSeventh before = 0.1428571428571428 214571170656199683435261249542236328125
OneSeventh = 0.1428571428571428 49212692681248881854116916656494140625
OneSeventh after = 0.1428571428571428 769682682968777953647077083587646484375
Imprimir la representación decimal exacta de un double
tiene usos limitados.
C tiene 2 familias de macros en <float.h>
para ayudarnos.
El primer conjunto es el número de dígitos significativos para imprimir en una cadena en decimal, por lo que al escanear la cadena hacia atrás, obtenemos el punto flotante original. Se muestran con el valor mínimo de la especificación C y un compilador C11 de muestra .
FLT_DECIMAL_DIG 6, 9 (float) (C11)
DBL_DECIMAL_DIG 10, 17 (double) (C11)
LDBL_DECIMAL_DIG 10, 21 (long double) (C11)
DECIMAL_DIG 10, 21 (widest supported floating type) (C99)
El segundo conjunto es el número de dígitos significativos que una cadena puede escanearse en un punto flotante y luego imprimirse el FP, conservando la misma presentación de cadena. Se muestran con el valor mínimo de la especificación C y un compilador C11 de muestra . Creo que está disponible antes de C99.
FLT_DIG 6, 6 (float)
DBL_DIG 10, 15 (double)
LDBL_DIG 10, 18 (long double)
El primer conjunto de macros parece cumplir el objetivo de OP de dígitos significativos . Pero esa macro no siempre está disponible.
#ifdef DBL_DECIMAL_DIG
#define OP_DBL_Digs (DBL_DECIMAL_DIG)
#else
#ifdef DECIMAL_DIG
#define OP_DBL_Digs (DECIMAL_DIG)
#else
#define OP_DBL_Digs (DBL_DIG + 3)
#endif
#endif
El "+3" fue el quid de mi respuesta anterior. Se centra en si se conoce la cadena de conversión de ida y vuelta-FP-string (conjunto de macros # 2 disponibles C89), ¿cómo se determinarían los dígitos para FP-string-FP (set # 1 macros available post C89)? En general, agregar 3 fue el resultado.
Ahora, cuántos dígitos significativos para imprimir se conocen y manejan a través de <float.h>
.
Para imprimir N dígitos decimales significativos, uno puede usar varios formatos.
Con "%e"
, el campo de precisión es el número de dígitos después del dígito principal y el punto decimal. Entonces - 1
está en orden. Nota: Este -1 is not in the initial
int Digs = DECIMAL_DIG; `
printf("%.*e/n", OP_DBL_Digs - 1, OneSeventh);
// 1.4285714285714285e-01
Con "%f"
, el campo de precisión es el número de dígitos después del punto decimal. Para un número como OneSeventh/1000000.0
, uno necesitaría OP_DBL_Digs + 6
para ver todos los dígitos significativos .
printf("%.*f/n", OP_DBL_Digs , OneSeventh);
// 0.14285714285714285
printf("%.*f/n", OP_DBL_Digs + 6, OneSeventh/1000000.0);
// 0.00000014285714285714285
Nota: Muchos se usan para "%f"
. Eso muestra 6 dígitos después del punto decimal; 6 es la pantalla predeterminada, no la precisión del número.
Si solo está interesado en el bit (resp hex pattern) puede usar el formato %a
. Esto te garantiza:
La precisión por defecto es suficiente para una representación exacta del valor si existe una representación exacta en la base 2 y, de lo contrario, es suficientemente grande para distinguir los valores del tipo double.
Tendría que agregar que esto solo está disponible desde C99.
Simplemente use las macros de <float.h>
y el especificador de conversión de ancho variable ( ".*"
):
float f = 3.14159265358979323846;
printf("%.*f/n", FLT_DIG, f);