Modify itoa to process a floating point string with decimal point operator into ASCII char 23(123.456) in the output

527 Views Asked by At

Having trouble C language debug step into process decimal point operator in a floating point sting of numbers. Example: 1234.5678, itoa() output stops at 4 and terminates out array string with '\0' Null char* pointer also an array. So we only get 1234\0 in the out array even after adding test for '.' seems to just skip over it like it's not really there. Thanks for any ideas to get these counts and floating-point decimal point for serial UART data without using printf().

Also strnlen() has same issue, counts only to the decimal point and C sizeof returns the count of 2 (1234.5678) characters are put in the array1={0.0} yet sizeof operator is in violation of clang definition below since it only returns count of 2 for the string. This may be vendor related string.h

#if defined(_INLINE) || defined(_STRLEN)
_OPT_IDEFN size_t strlen(const char *string)
{
   size_t      n = (size_t)-1;
   const char *s = string;

   do n++; while (*s++);
   return n;
}
#endif /* _INLINE || _STRLEN */

size_t len = sizeof *varin;

sizeof object and sizeof(type name): yield an integer equal to the size of the specified object or type in bytes. (Strictly, sizeof produces an unsigned integer value whose type, size_ t, is defined in the header <stddef. h>.) An object can be a variable or array or structure. A type name can be the name of a basic type like int or double, or a derived type like a structure or a pointer.

```
/*****************************************************
*
*! Implementation of itoa()
*
* b\return converted character string
*
***************************************************/
char*
itoa(int16_t num, char* str, int base)//intmax_t
{
    uintptr_t  i = 0; //int 
    bool isNegative = false;

    /* Handle decimal point explicitely with ASCII (".")  */
    if(num == '.')
    {
        //str[i++] = '.';
        str[i] = 0x2E;
       //
        return str;
    }

    /* Handle unterminated end of string by adding NULL */
    else if(num == ' ')
    {
       str[i] = '\0';
       //
        return str;
    }

    /* Handle 0 explicitely, otherwise Null string is printed for 0 */
    if(num == '0') //0
    {
        str[i++] = '0';
        //str[i] = '\0';
        return str;
    }

    // In standard itoa(), negative numbers are handled only with
    // base 10. Otherwise numbers are considered unsigned.
    if (num < 0 && base == 10)
    {
        isNegative = true;
        num = -num;
    }

    // Process individual digits
    while (num != 0)
    {
        int16_t rem = num % base; //intmax_t
        str[i++] = (rem > 9)? (rem-10) + 'a' : rem + '0'; //
        num = num/base;
    }


    // If number is negative, append '-'
    if (isNegative)
    {
        str[i++] = '-';
    }

    // Append string terminator
    str[i] = '\0';

    // Reverse the string
    reverse(str, i);

    return str;
}
```
/* Decls */
static float32_t varin[1] = {0.0};
*varin = dbytes;
static char varout[8];

/* Convert hexadecimal to ASCII codes
 * terminate end NULL */
itoa(*varin, varout, 10);

Debug of the in/out arrays sent to itoa()

4

There are 4 best solutions below

5
On

I think there are three separate problems here. Let's cover them in turn.

C sizeof returns the count of 2 characters are put in the array

sizeof is for computing the size of an object as declared. It's not generally any good for determining the length of a string. I suspect you got 2 because you (a) tried to take the size of a pointer and (b) are using a 16-bit microcontroller. You can demonstrate that issue like this:

char *p = "Hello, world!";
printf("size of pointer: %d\n", (int)(sizeof(p)));
printf("length of string: %d\n", (int)(strlen(p)));

sizeof computes the size of the pointer itself, while strlen computes the length of the pointed-to string.

The second problem is that you seem to be imagining that a number — an int or float value — contains human-readable characters like '.' or ' ' or '0'. It does not. A number is a number you can do math on; it is not a string of characters you can see. In your itoa implementation, you're doing things like

if(num == '.')

and

if(num == ' ')

and

if(num == '0')

But these are all wrong. If an integer has the value 0, it does not contain the character '0'. A floating-point variable like float f = 123.456 looks to you and me like it contains a decimal point, but in the computer's memory, it does not, so it makes no sense to test whether num is equal to '.'. (It makes especially no sense since num in that code is an integer variable, not a floating-point variable.)

The third problem is that you're trying to use your itoa function to directly convert a floating-point value, and it's failing, and you're surprised at the way it's failing. But really, there's no surprise. If you have a function

char * itoa(int16_t num, char* str, int base);

that accepts an integer parameter num, and you pass a floating-point value like 123.456, the first thing the compiler is going to do is convert the floating-point number to an integer, by throwing away the fractional part. That's why everything after the decimal point seems to be missing.


So with those misconceptions out of the way, let's look at how to properly convert a floating-point number to its string representation. It turns out this is a very hard problem! In fact, a proper algorithm for performing this task, in all cases, without error, was not published until 1990. So the right way to convert a floating-point number to a string is generally to use printf or sprintf, because those functions have a pretty good chance of using a proper algorithm.

But you said you didn't want to use printf. It's possible to convert a floating-point number to a string "by hand" — it can actually be rather straightforward — but you should realize that any code you can write will probably not do a perfect job of it.

You can find some code and commentary about this problem at this former SO question and also this one.

Here is a simple, straightforward, basically simpleminded ftoa function for converting floating-point numbers to strings. It's written on top of your existing itoa function, as I think you were trying to do. It follows the approach suggested in this comment: it takes a floating-point number like 123.456 and breaks it up into an integer part 123 and a fractional part .456. It converts the integer part using itoa. Then it converts and appends the fractional part by repeatedly multiplying it by 10. It accepts a maxprec argument telling it how many digits after the decimal to print, just like the number you can use in %.6f in printf.

char *ftoa(float num, char buf[], int bufsize, int maxprec)
{
    char *p = buf;

    /* handle negative numbers */
    if(num < 0) {
        *p++ = '-';
        num = -num;
    }

    /* extract and convert integer part */
    int ipart = num;
    itoa(num, p, 10);

    /* extract and convert fractional part */
    float fpart = num - ipart;
    p += strlen(p);
    *p++ = '.';
    int ndig = 0;
    char *endp = buf + bufsize - 1;     /* leave room for \0 */

    /* keep going as long as (a) there's some fractional part left and */
    /* (b) we haven't converted too many digits and */
    /* (c) we haven't overflowed the destination buffer */
    while(fpart != 0 && ndig < maxprec && p < endp) {
        fpart *= 10;
        int digit = fpart;
        *p++ = digit + '0';
        fpart = fpart - digit;
        ndig++;
    }

    /* null-terminate and return final string */
    *p = '\0';
    return buf;
}

Here is an example call:

char buf[20];
printf("%s\n", ftoa(123.456, buf, sizeof(buf), 6));

But please note that this is not a very good function! It will work decently well for "simple" floating-point numbers, but it has plenty of problems. The example call ftoa(123.456, buf, sizeof(buf), 6) shows one of them: it converts to 123.456001 on my machine. (Although, that's not actually as wrong as it looks.)

As mentioned, properly converting floating-point numbers to strings is a very hard problem, and this simpleminded code does not attempt to address any of the hard parts of the problem. See chux's answer for a somewhat more complete approach.

Also, for this to work at all you're going to have to fix at least some of the problems in the underlying itoa function. At the very least, change if(num == '0') to if(num == 0) and uncomment the str[i] = '\0' line in that case. You should also get rid of the num == ' ' and num == '.' cases, which you don't need and which will cause unnecessary problems trying to convert the numbers 32 and 46.

2
On

The problem is C in this MCU compiler believes the decimal point of FP integer string is an operator. If that is how Borland saw the world 30++ years ago for base 10 before FPU existed perhaps it was the only way to express a decimal point. I found this out by trying to create an (enum) list with floating point values tied to exported variables of the same name. Anyway a decimal point of FPU base 10 string is NOT an C/C++ operator! Seems the C language does not account correctly for decimal points in IEEE FPU specification for base 10 integer strings.

17
On

As discussed, there are many issues with your approach. I would punt completely if I were you. Here is a possible approach that might work better. Refer to https://www.geeksforgeeks.org/convert-floating-point-number-string/

Mac_3.2.57$cat asciToFloat.c
// C program for implementation of ftoa()
#include <math.h>
#include <stdio.h>
 
// Reverses a string 'str' of length 'len'
void reverse(char* str, int len)
{
    int i = 0, j = len - 1, temp;
    while (i < j) {
        temp = str[i];
        str[i] = str[j];
        str[j] = temp;
        i++;
        j--;
    }
}
 
// Converts a given integer x to string str[].
// d is the number of digits required in the output.
// If d is more than the number of digits in x,
// then 0s are added at the beginning.
int intToStr(int x, char str[], int d)
{
    int i = 0;
    while (x) {
        str[i++] = (x % 10) + '0';
        x = x / 10;
    }
 
    // If number of digits required is more, then
    // add 0s at the beginning
    while (i < d)
        str[i++] = '0';
 
    reverse(str, i);
    str[i] = '\0';
    return i;
}
 
// Converts a floating-point/double number to a string.
void ftoa(float n, char* res, int afterpoint)
{
    // Extract integer part
    int ipart = (int)n;
 
    // Extract floating part
    float fpart = n - (float)ipart;
 
    // convert integer part to string
    int i = intToStr(ipart, res, 0);
 
    // check for display option after point
    if (afterpoint != 0) {
        res[i] = '.'; // add dot
 
        // Get the value of fraction part upto given no.
        // of points after dot. The third parameter
        // is needed to handle cases like 233.007
        fpart = fpart * pow(10, afterpoint);
 
        intToStr((int)fpart, res + i + 1, afterpoint);
    }
}
 
// Driver program to test above function
int main()
{
    char res[20];
    float n = 233.5;
    ftoa(n, res, 1);
    printf("\"%s\"\n", res);
    return 0;
}
Mac_3.2.57$cc asciToFloat.c
Mac_3.2.57$./a.out 
"233.5"
Mac_3.2.57$
4
On

float to text is easy to do - with poor results.
float to text is hard to do - with good results.

Here are some of the issues:

  • Is the destination char[] big enough?

  • Does conversion handle extreme values, Not-a-number, Infinity, -0.0, sub-normals? If not, what is the range limit?

  • Is the result rounded? What rules used to round?

  • How good is the conversion? How close to the best possible answer?


I took OP's over-all goal and had to morph it a bit to form a reasonable function.

Use |f| up front to use unsigned math.

I scaled the float and then converted to an integer, wider the better. unsigned __int128 is a compiler extension, else use uintmax_t and expect reduced conversion range and reduced ftoa_PREC_MAX. Extra scaling (by 2) allows for a simple rounding.

When the float is large, no scaling needed as the fraction is 0.

Modestly tested code went well with the resulting text within the converted text of f +/-1 ULP.
Better code would be within 0.5 ULP. Edge cases not well exercised.

#include <float.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>

#include <stdio.h>

#define ftoa_PREC_MAX 15
typedef unsigned __int128 uint_wide;  // At least 32 bit
// UINT_WIDE_MAX is the greatest convertible value.
#define UINT_WIDE_MAX_PLUS1 ((-(uint_wide)1 / 2 + 1)*2.0f)

// Convert `float` to a string.  Return NULL on error.
char* ftoa(size_t sz, char dest[sz], float f, unsigned prec) {
  // Check for pathological parameters.
  if (sz == 0 || dest == NULL) {
    return NULL;
  }
  dest[0] = '\0';  // Pre-set dest to "" for error cases.

  // First form result in an ample buffer.
  //       -       i digits           .   f digits       \0
  char buf[1 + (FLT_MAX_10_EXP + 1) + 1 + ftoa_PREC_MAX + 1];
  char *p = buf + sizeof buf;
  *--p = '\0';
  bool sign = signbit(f);
  if (sign) {
    f = -f;
  }

  // Test for more extreme values.
  if (!isfinite(f)) {
    return NULL;
  }
  if (prec > ftoa_PREC_MAX) {
    return NULL;
  }
  if (f >= UINT_WIDE_MAX_PLUS1) {
    return NULL;
  }

  // Determine fraction.
  uint_wide u;
  // If f is large enough, its fractional portion is .000...000.
  // No need to scale and potentially overflow. 
  if (f > 1.0f / FLT_EPSILON) {
    u = (uint_wide) f;
    for (; prec > 0; prec--) {
      *--p = '0';
    }
  } else {
    // Scale by power-of-10 (could use a look-up table instead) and 2.
    u = (uint_wide) (f * powf(10, (float) prec) * 2);
    // Round, ties up.
    u = (u + 1) / 2;  
    for (; prec > 0; prec--) {
      *--p = (char) (u % 10 + '0');
      u /= 10;
    }
  }
  *--p = '.';
  
  // Determine whole number part.
  do {
    *--p = (char) (u % 10 + '0');
    u /= 10;
  } while (u);
  if (sign) {
    *--p = '-';
  }
  
  // Copy to destination.
  size_t len = (size_t) (buf + sizeof buf - p);
  if (len > sz) {
    return NULL;
  }
  return strcpy(dest, p);
}

The source of errors include f * powf(10, (float) prec) as the power-of-10 is approximate when prec > 9 and the multiplication may incur rounding. A more advanced solution would use wider FP math.