Files
mruby-mruby/src/fmt_fp.c
T
John Bampton 67c5ced277 Fix spelling
2024-04-22 18:00:27 +10:00

373 lines
8.4 KiB
C

#include <mruby.h>
#include <string.h>
#ifndef MRB_NO_FLOAT
/***********************************************************************
Routine for converting a single-precision
floating-point number into a string.
The code in this function was inspired from Fred Bayer's pdouble.c.
Since pdouble.c was released as Public Domain, I'm releasing this
code as public domain as well.
Dave Hylands
The original code can be found in https://github.com/dhylands/format-float
***********************************************************************/
/***********************************************************************
I modified the routine for mruby:
* support `double`
* support `#` (alt_form) modifier
My modifications in this file are also placed in the public domain.
Matz (Yukihiro Matsumoto)
***********************************************************************/
#include <math.h>
#ifdef MRB_USE_FLOAT32
// 1 sign bit, 8 exponent bits, and 23 mantissa bits.
// exponent values 0 and 255 are reserved, exponent can be 1 to 254.
// exponent is stored with a bias of 127.
// The min and max floats are on the order of 1x10^37 and 1x10^-37
#define FLT_DECEXP 32
#define FLT_ROUND_TO_ONE 0.9999995F
#define FLT_MIN_BUF_SIZE 6 // -9e+99
#else
// 1 sign bit, 11 exponent bits, and 52 mantissa bits.
#define FLT_DECEXP 256
#define FLT_ROUND_TO_ONE 0.999999999995
#define FLT_MIN_BUF_SIZE 7 // -9e+199
#endif /* MRB_USE_FLOAT32 */
static const mrb_float g_pos_pow[] = {
#ifndef MRB_USE_FLOAT32
1e256, 1e128, 1e64,
#endif
1e32, 1e16, 1e8, 1e4, 1e2, 1e1
};
static const mrb_float g_neg_pow[] = {
#ifndef MRB_USE_FLOAT32
1e-256, 1e-128, 1e-64,
#endif
1e-32, 1e-16, 1e-8, 1e-4, 1e-2, 1e-1
};
/*
* mrb_format_float(mrb_float f, char *buf, size_t buf_size, char fmt, int prec, char sign)
*
* fmt: should be one of 'e', 'E', 'f', 'F', 'g', or 'G'. (|0x80 for '#')
* prec: is the precision (as specified in printf)
* sign: should be '\0', '+', or ' ' ('\0' is the normal one - only print
* a sign if ```f``` is negative. Anything else is printed as the
* sign character for positive numbers.
*/
int
mrb_format_float(mrb_float f, char *buf, size_t buf_size, char fmt, int prec, char sign) {
char *s = buf;
int buf_remaining = (int)buf_size - 1;
int alt_form = 0;
if ((uint8_t)fmt & 0x80) {
fmt &= 0x7f; /* turn off alt_form flag */
alt_form = 1;
}
if (buf_size <= FLT_MIN_BUF_SIZE) {
// Smallest exp notion is -9e+99 (-9e+199) which is 6 (7) chars plus terminating
// null.
if (buf_size >= 2) {
*s++ = '?';
}
if (buf_size >= 1) {
*s++ = '\0';
}
return buf_size >= 2;
}
if (signbit(f)) {
*s++ = '-';
f = -f;
}
else if (sign) {
*s++ = sign;
}
buf_remaining -= (int)(s - buf); // Adjust for sign
{
char uc = fmt & 0x20;
if (isinf(f)) {
*s++ = 'I' ^ uc;
*s++ = 'N' ^ uc;
*s++ = 'F' ^ uc;
goto ret;
}
else if (isnan(f)) {
*s++ = 'N' ^ uc;
*s++ = 'A' ^ uc;
*s++ = 'N' ^ uc;
ret:
*s = '\0';
return (int)(s - buf);
}
}
if (prec < 0) {
prec = 6;
}
char e_char = 'E' | (fmt & 0x20); // e_char will match case of fmt
fmt |= 0x20; // Force fmt to be lowercase
char org_fmt = fmt;
if (fmt == 'g' && prec == 0) {
prec = 1;
}
int e, e1;
int dec = 0;
char e_sign = '\0';
int num_digits = 0;
const mrb_float *pos_pow = g_pos_pow;
const mrb_float *neg_pow = g_neg_pow;
if (f == 0.0) {
e = 0;
if (fmt == 'e') {
e_sign = '+';
}
else if (fmt == 'f') {
num_digits = prec + 1;
}
}
else if (f < 1.0) { // f < 1.0
char first_dig = '0';
if (f >= FLT_ROUND_TO_ONE) {
first_dig = '1';
}
// Build negative exponent
for (e = 0, e1 = FLT_DECEXP; e1; e1 >>= 1, pos_pow++, neg_pow++) {
if (*neg_pow > f) {
e += e1;
f *= *pos_pow;
}
}
char e_sign_char = '-';
if (f < 1.0) {
if (f >= FLT_ROUND_TO_ONE) {
f = 1.0;
if (e == 0) {
e_sign_char = '+';
}
}
else {
e++;
f *= 10.0;
}
}
// If the user specified 'g' format, and e is <= 4, then we'll switch
// to the fixed format ('f')
if (fmt == 'f' || (fmt == 'g' && e <= 4)) {
fmt = 'f';
dec = -1;
*s++ = first_dig;
if (org_fmt == 'g') {
prec += (e - 1);
}
// truncate precision to prevent buffer overflow
if (prec + 2 > buf_remaining) {
prec = buf_remaining - 2;
}
num_digits = prec;
if (num_digits || alt_form) {
*s++ = '.';
while (--e && num_digits) {
*s++ = '0';
num_digits--;
}
}
}
else {
// For e & g formats, we'll be printing the exponent, so set the
// sign.
e_sign = e_sign_char;
dec = 0;
if (prec > (buf_remaining - FLT_MIN_BUF_SIZE)) {
prec = buf_remaining - FLT_MIN_BUF_SIZE;
if (fmt == 'g') {
prec++;
}
}
}
}
else {
// Build positive exponent
for (e = 0, e1 = FLT_DECEXP; e1; e1 >>= 1, pos_pow++, neg_pow++) {
if (*pos_pow <= f) {
e += e1;
f *= *neg_pow;
}
}
// If the user specified fixed format (fmt == 'f') and e makes the
// number too big to fit into the available buffer, then we'll
// switch to the 'e' format.
if (fmt == 'f') {
if (e >= buf_remaining) {
fmt = 'e';
}
else if ((e + prec + 2) > buf_remaining) {
prec = buf_remaining - e - 2;
if (prec < 0) {
// This means no decimal point, so we can add one back
// for the decimal.
prec++;
}
}
}
if (fmt == 'e' && prec > (buf_remaining - 6)) {
prec = buf_remaining - 6;
}
// If the user specified 'g' format, and e is < prec, then we'll switch
// to the fixed format.
if (fmt == 'g' && e < prec) {
fmt = 'f';
prec -= (e + 1);
}
if (fmt == 'f') {
dec = e;
num_digits = prec + e + 1;
}
else {
e_sign = '+';
}
}
if (prec < 0) {
// This can happen when the prec is trimmed to prevent buffer overflow
prec = 0;
}
// We now have f as a floating-point number between >= 1 and < 10
// (or equal to zero), and e contains the absolute value of the power of
// 10 exponent, and (dec + 1) == the number of digits before the decimal.
// For e, prec is # digits after the decimal
// For f, prec is # digits after the decimal
// For g, prec is the max number of significant digits
//
// For e & g there will be a single digit before the decimal
// for f there will be e digits before the decimal
if (fmt == 'e') {
num_digits = prec + 1;
if (prec == 0) prec = 1;
}
else if (fmt == 'g') {
num_digits = prec;
}
// Print the digits of the mantissa
for (int i = 0; i < num_digits; i++,dec--) {
int8_t d = (int8_t)((int)f)%10;
*s++ = '0' + d;
if (dec == 0 && (prec > 0 || alt_form)) {
*s++ = '.';
}
f -= (mrb_float)d;
f *= 10.0;
}
// Round
if (f >= 5.0) {
char *rs = s;
rs--;
while (1) {
if (*rs == '.') {
rs--;
continue;
}
if (*rs < '0' || *rs > '9') {
// + or -
rs++; // So we sit on the digit to the right of the sign
break;
}
if (*rs < '9') {
(*rs)++;
break;
}
*rs = '0';
if (rs == buf) {
break;
}
rs--;
}
if (*rs == '0') {
// We need to insert a 1
if (fmt != 'f' && rs[1] == '.') {
// We're going to round 9.99 to 10.00
// Move the decimal point
rs[0] = '.';
rs[1] = '0';
if (e_sign == '-') {
e--;
}
else {
e++;
}
}
s++;
char *ss = s;
while (ss > rs) {
*ss = ss[-1];
ss--;
}
*rs = '1';
if (f < 1.0 && fmt == 'f') {
// We rounded up to 1.0
prec--;
}
}
}
if (org_fmt == 'g' && prec > 0 && !alt_form) {
// Remove trailing zeros and a trailing decimal point
while (s[-1] == '0') {
s--;
}
if (s[-1] == '.') {
s--;
}
}
// Append the exponent
if (e_sign) {
*s++ = e_char;
*s++ = e_sign;
if (e >= 100) {
*s++ = '0' + (e / 100);
e %= 100;
}
*s++ = '0' + (e / 10);
*s++ = '0' + (e % 10);
}
*s = '\0';
return (int)(s - buf);
}
#endif