4
\$\begingroup\$

I came up with this simple function to return the length of a double, working fine in all my test but want to make sure that this code will work in all doubles, can't think of a case in which it would not work but I'm sure if there is, then you can find it, here it is:

int length(double number){
     int result = 0; 
     if ( number < 0 ) number *= -1;

     while ( number > 1){
         result++; 
         number /= 10;
     }
     return result + ( number == 1 );
}

I think that the (number == 1) is pretty clever, but there might be some better way to accomplish the correct result. It's meant to work whenever the number is an exact power of ten but I think it might be a bit cryptic.

\$\endgroup\$
10
  • 2
    \$\begingroup\$ It would be easier to tell if it was working if you gave a little more description of what you want it to do. \$\endgroup\$ Oct 8, 2018 at 23:39
  • 1
    \$\begingroup\$ What does "length" mean here - number of digits? Number of digits? Number of digits in the integer portion? \$\endgroup\$ Oct 8, 2018 at 23:49
  • \$\begingroup\$ sorry, it does mean number of digits. I guess for starters thats a good improvement, it would be better called numOfDigitsInInteger() and its the digits, of the integer part only \$\endgroup\$ Oct 9, 2018 at 0:29
  • 1
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$
    – Mast
    Oct 9, 2018 at 4:47
  • 1
    \$\begingroup\$ Should the “length” of 0.5 be 1 (because of the leading zero digit) or zero? What should NaN return? Any value returned for +Inf and -Inf would be wrong since they require an infinite number of digits, and the return type int cannot return +Inf. \$\endgroup\$
    – AJNeufeld
    Oct 9, 2018 at 5:18

6 Answers 6

9
\$\begingroup\$

You say it "works fine" in all your tests, but you didn't show us the tests.

Unfortunately, it failed the very first test I wrote:

#include <gtest/gtest.h>

TEST(length, zero)
{
    EXPECT_EQ(1, length(0));
}

Of the next batch, one_minus fails for the same reason:

TEST(length, one)
{
    EXPECT_EQ(1, length(1.0));
}

TEST(length, one_plus)
{
    EXPECT_EQ(1, length(1.0 + 1e-12));
}

TEST(length, one_minus)
{
    EXPECT_EQ(1, length(1.0 - 1e-12));
}


TEST(length, two)
{
    EXPECT_EQ(1, length(2.0));
}

TEST(length, two_plus)
{
    EXPECT_EQ(1, length(2.0 + 1e-12));
}

TEST(length, two_minus)
{
    EXPECT_EQ(1, length(2.0 - 1e-12));
}


TEST(length, ten)
{
    EXPECT_EQ(2, length(10.0));
}

TEST(length, ten_plus)
{
    EXPECT_EQ(2, length(10.0 + 1e-12));
}

TEST(length, ten_minus)
{
    EXPECT_EQ(1, length(10.0 - 1e-12));
}

We can fix this by adding a simple check before the first division:

if (number < 0) number *= -1;
if (number < 1) return 1;

Let's add some more tests:

TEST(length, zillions)
{
    EXPECT_EQ(36, length(1.0e35));
}

TEST(length, negative_zillions)
{
    EXPECT_EQ(36, length(-1.0e35));
}

Now we just need some tests for invalid inputs (infinities, NaNs, etc) and we're golden.


Let's look at the implementation now:

while (number > 1) {
    result++;
    number /= 10;
}
return result + (number == 1);

We can avoid adding number == 1 if we include that case within the while loop:

while (number >= 1) {
    result++;
    number /= 10;
}
return result;

And our tests prove we haven't broken it.

We might now want to improve speed for large numbers: consider our zillions test that loops 36 times. We could shorten that, at a very small cost to lower numbers by first looping (say) 6 digits at a time:

int result = 0;
while (number >= 1e6) {
    result += 6;
    number /= 1e6;
}
while (number >= 1) {
    result++;
    number /= 10;
}
return result;

When I try this, it initially shows promise, but then fails the zillions tests with an off-by one error. Is there a subtle difference in rounding between repeatedly dividing by ten and repeatedly dividing by a million when you're dealing with inexact numbers? Actually, the problem is with our constant - 1.0e35 is most closely represented as 99999999999999999997871448567840768, so we suffered a rounding error in our test - it happened that sequential division by ten introduces a rounding error that compensates for this, and we wrongly pass!

Which brings me neatly to the alternative implementation. The most convenient tool for measuring digits in the integer part isn't in <math.h> at all - it's in <stdio.h>! We can ask printf to measure the length for us:

#include <stdio.h>

int length(double number)
{
    /* "unround" the number, as printf rounds to nearest */
    number += .5 - (number > 0);
    /* format the sign, then subtract one, so we don't count
       minus as a digit */
    return snprintf(NULL, 0, "%+1.f", number) - 1;
}

Note that I've left the handling of NaNs and infinities in this version as an exercise...

\$\endgroup\$
5
  • 1
    \$\begingroup\$ To avoid rounding up, use trunc(number). number - .5 imparts its own round-off errors. \$\endgroup\$ Oct 9, 2018 at 17:45
  • 1
    \$\begingroup\$ Lovely, just lovely, I knew I could count on codereview to make me feel amazed and humbled. Thank you dear sir, thanks \$\endgroup\$ Oct 9, 2018 at 18:54
  • \$\begingroup\$ I checked and the sprintf technique given should round correctly for all values, but the reasons seem very subtle (it depends on round-to-even). \$\endgroup\$
    – Veedrac
    Oct 9, 2018 at 18:55
  • 1
    \$\begingroup\$ @chux, I would have used trunc(), but that violates the requirement of avoiding <math.h>. But good advice otherwise! (That said, 0.5 is exact, so shouldn't introduce errors - what have I overlooked? Oh, I see - it rounds the wrong way for negative numbers - I should add tests for values around -10) \$\endgroup\$ Oct 10, 2018 at 7:16
  • \$\begingroup\$ On review, Although number - .5 can be inexact and cause a "printing" of the wrong un-rounded value, it is the result of snprintf() that is important. Try number = pow(2,53)-1 - it un-rounds to 9007199254740990. So if the wrong number is "printed", does its length differ from the right number? I have not found one. Nice answer. \$\endgroup\$ Oct 10, 2018 at 13:22
3
\$\begingroup\$

Your +(number == 1) code is tricky and non-obvious. Why is 1 a special case (yes, I know for exact powers of 10). Why is adding a Boolean to an integer count a valid operation, and what does it do, or is it an implementation dependent undefined operation?

Can we rewrite the code to be clear, without needing a C Reference Manual kept handy? Sure!

int length(double number) {
     int result = 0; 
     if (number < 0) number *= -1.0;

     while (number >= 1) {
         result++; 
         number /= 10.0;
     }
     return result;
}

If the number becomes exactly equal to 1, because of >=, we take one last pass through the loop.

\$\endgroup\$
2
\$\begingroup\$

Your code only handles finite numbers. If it is given a NaN (Not a Number, like the result of 0.0 / 0.0) you'll get a 0 width (which may be OK), but if you pass in an infinity your while loop will never end.

\$\endgroup\$
1
  • \$\begingroup\$ The irony of it all is that it is technically correct. I will go ahead and count all digits in an infnity and get back to you with proof!!! \$\endgroup\$ Oct 9, 2018 at 18:58
2
\$\begingroup\$

can't think of a case in which it would not work

Code fails with select values.

Precession

Each iteration of number /= 10; can impart a round-off error. This will not affect result for double == 12345.0, yet for values near large powers of 10, the results may be off-by-1.

A fix is not easy as it requires higher precision math, significant additional code or <math.h> functions.


Below test code readily fails OP's lenth(). If also fails, though less often @Toby Speight good improvement: length_TS().

I tried to further reduce the errors length_CD(), but is still not exact. The 22 comes from 53/log2(5) or about the max power 10 exactly representable with common double. This reduces the number of divisions and so reduces round-off effects.

int length0(double number) {
  int result = 0;
  if (number < 0)
    number *= -1;

  while (number > 1) {
    result++;
    number /= 10;
  }
  return result + (number == 1);
}

int length_TS(double number) {
  int result = 0;
  while (number >= 1e6) {
    result += 6;
    number /= 1e6;
  }
  while (number >= 1) {
    result++;
    number /= 10;
  }
  return result;
}

#if DBL_DIG == 15
#define IM10 22
#define M10 1.0e22
#endif

int length_CD(double number) {
  int result = 0;
  while (number >= M10) {
    result += IM10;
    number /= M10;
  }
  while (number >= 1) {
    result++;
    number /= 10;
  }
  return result;
}

Test code

int length_test(int length(double), double x) {
  char buf[1000];
  sprintf(buf, "%.20f", x);
  int l0 = strchr(buf, '.') - buf;
  int l1 = length(x);
  if (l0 != l1) {
    // printf("%3d %3d %.20e\n", l1, l0, x);
    return 1;
  }
  return 0;
}

int length_tests(const char *s, int length(double)) {
  int err = 0;
  for (int i = 0; i < 308; i += 1) {
    double p10 = pow(10.0, i);
    err += length_test(length, nextafter(p10, 0));
    err += length_test(length, p10);
    err += length_test(length, nextafter(p10, DBL_MAX));
  }
  printf("%-11s Errors %d\n", s, err);
}

int main(void) {
  length_tests("length0()", length0);
  length_tests("length_TS()",length_TS);
  length_tests("length_CD()",length_CD);
}

Output

length0()   Errors 365
length_TS() Errors 222
length_CD() Errors 189

Below is example code that passes the test with 0 errors for all finite double >= 1.0. This is extended math needed for a precise answer.

// TODO cope with FLT_RADIX > 10 as it can overflow acc
int buf_times_add(char *buf, int mul, int add) {
  char *s = buf;
  while (*s) {
    int acc = (*s - '0') * mul + add;
    *s++ = (acc % 10) + '0';
    add = acc / 10;
  }
  if (add > 0) {
    *s++ = add + '0';
    *s = '\0';
  }
  return s - buf;
}

int length_s(double x) {
  printf("%24.16e ", x);
  assert(x >= 1.0 && x <= DBL_MAX);
  int expo = 0;
  while (x >= FLT_RADIX) {
    x /= FLT_RADIX;
    expo++;
  }
  char buf[1024];
  buf[0] = '0';
  buf[1] = '\0';
  int len = 0;
  while (x && expo > 0) {
    int msd = (int) x;
    x -= msd;
    x *= FLT_RADIX;
    expo--;
    len = buf_times_add(buf, FLT_RADIX, msd);
  }
  while (expo >= 0) {
    expo--;
    len = buf_times_add(buf, FLT_RADIX, 0);
  }
  printf("<%s>\n", buf);
  return len;
}
\$\endgroup\$
0
0
\$\begingroup\$

The sprintf technique is neat, but it has a few disadvantages:

  • It's really hard to figure out that it rounds correctly. I had to write a Python program to analyse it to be confident.

  • It doesn't seem particularly efficient; formatting doubles is expensive.

The technique I would prefer is less neat, but easier to verify and more readily made fast: search an array for the boundary case. Have an array with these elements:

10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 10000000.0, 100000000.0, 1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0, 10000000000000.0, 100000000000000.0, 1000000000000000.0, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22, 1.0000000000000001e+23, 1.0000000000000001e+24, 1e+25, 1e+26, 1e+27, 1.0000000000000002e+28, 1.0000000000000001e+29, 1e+30, 1.0000000000000001e+31, 1e+32, 1.0000000000000001e+33, 1.0000000000000001e+34, 1.0000000000000002e+35, 1e+36, 1.0000000000000001e+37, 1.0000000000000002e+38, 1.0000000000000001e+39, 1e+40, 1e+41, 1e+42, 1e+43, 1e+44, 1.0000000000000001e+45, 1.0000000000000001e+46, 1e+47, 1e+48, 1.0000000000000001e+49, 1e+50, 1.0000000000000002e+51, 1.0000000000000001e+52, 1.0000000000000002e+53, 1e+54, 1e+55, 1e+56, 1e+57, 1.0000000000000001e+58, 1.0000000000000001e+59, 1.0000000000000001e+60, 1.0000000000000001e+61, 1e+62, 1e+63, 1e+64, 1.0000000000000001e+65, 1.0000000000000001e+66, 1.0000000000000001e+67, 1.0000000000000001e+68, 1e+69, 1e+70, 1e+71, 1.0000000000000001e+72, 1.0000000000000001e+73, 1.0000000000000001e+74, 1.0000000000000001e+75, 1e+76, 1.0000000000000001e+77, 1e+78, 1.0000000000000001e+79, 1e+80, 1.0000000000000001e+81, 1.0000000000000001e+82, 1e+83, 1e+84, 1e+85, 1e+86, 1.0000000000000002e+87, 1.0000000000000001e+88, 1.0000000000000001e+89, 1.0000000000000001e+90, 1e+91, 1e+92, 1e+93, 1e+94, 1e+95, 1e+96, 1e+97, 1.0000000000000001e+98, 1.0000000000000001e+99, 1e+100, 1.0000000000000001e+101, 1.0000000000000001e+102, 1e+103, 1e+104, 1.0000000000000001e+105, 1e+106, 1.0000000000000001e+107, 1e+108, 1.0000000000000002e+109, 1e+110, 1.0000000000000001e+111, 1.0000000000000001e+112, 1e+113, 1e+114, 1e+115, 1e+116, 1e+117, 1.0000000000000001e+118, 1.0000000000000001e+119, 1.0000000000000001e+120, 1e+121, 1e+122, 1.0000000000000001e+123, 1.0000000000000001e+124, 1.0000000000000001e+125, 1.0000000000000001e+126, 1.0000000000000001e+127, 1e+128, 1.0000000000000002e+129, 1e+130, 1.0000000000000001e+131, 1.0000000000000001e+132, 1e+133, 1.0000000000000001e+134, 1.0000000000000001e+135, 1e+136, 1e+137, 1e+138, 1e+139, 1e+140, 1e+141, 1e+142, 1e+143, 1e+144, 1.0000000000000001e+145, 1.0000000000000002e+146, 1.0000000000000002e+147, 1e+148, 1e+149, 1.0000000000000002e+150, 1e+151, 1e+152, 1.0000000000000002e+153, 1e+154, 1e+155, 1.0000000000000002e+156, 1.0000000000000001e+157, 1.0000000000000001e+158, 1.0000000000000001e+159, 1e+160, 1e+161, 1.0000000000000001e+162, 1.0000000000000001e+163, 1e+164, 1.0000000000000001e+165, 1.0000000000000001e+166, 1e+167, 1.0000000000000001e+168, 1.0000000000000001e+169, 1e+170, 1.0000000000000002e+171, 1e+172, 1e+173, 1e+174, 1.0000000000000001e+175, 1e+176, 1e+177, 1e+178, 1.0000000000000001e+179, 1e+180, 1.0000000000000001e+181, 1e+182, 1.0000000000000001e+183, 1e+184, 1.0000000000000001e+185, 1.0000000000000001e+186, 1.0000000000000001e+187, 1e+188, 1e+189, 1e+190, 1e+191, 1e+192, 1e+193, 1.0000000000000001e+194, 1.0000000000000001e+195, 1.0000000000000002e+196, 1.0000000000000001e+197, 1e+198, 1e+199, 1.0000000000000001e+200, 1e+201, 1.0000000000000001e+202, 1.0000000000000002e+203, 1.0000000000000001e+204, 1e+205, 1e+206, 1e+207, 1.0000000000000001e+208, 1e+209, 1.0000000000000001e+210, 1.0000000000000001e+211, 1.0000000000000001e+212, 1.0000000000000001e+213, 1.0000000000000001e+214, 1.0000000000000001e+215, 1e+216, 1.0000000000000001e+217, 1e+218, 1.0000000000000001e+219, 1.0000000000000001e+220, 1e+221, 1e+222, 1e+223, 1.0000000000000002e+224, 1.0000000000000001e+225, 1.0000000000000001e+226, 1e+227, 1.0000000000000001e+228, 1.0000000000000001e+229, 1e+230, 1e+231, 1e+232, 1.0000000000000002e+233, 1e+234, 1e+235, 1e+236, 1.0000000000000001e+237, 1e+238, 1.0000000000000001e+239, 1e+240, 1e+241, 1e+242, 1e+243, 1e+244, 1e+245, 1e+246, 1.0000000000000001e+247, 1e+248, 1.0000000000000001e+249, 1.0000000000000001e+250, 1e+251, 1e+252, 1.0000000000000001e+253, 1.0000000000000001e+254, 1.0000000000000002e+255, 1e+256, 1e+257, 1e+258, 1.0000000000000001e+259, 1e+260, 1.0000000000000001e+261, 1e+262, 1e+263, 1e+264, 1e+265, 1e+266, 1.0000000000000001e+267, 1.0000000000000002e+268, 1e+269, 1e+270, 1.0000000000000001e+271, 1e+272, 1.0000000000000001e+273, 1.0000000000000001e+274, 1.0000000000000001e+275, 1e+276, 1e+277, 1.0000000000000001e+278, 1e+279, 1e+280, 1e+281, 1e+282, 1.0000000000000002e+283, 1e+284, 1.0000000000000001e+285, 1e+286, 1e+287, 1e+288, 1e+289, 1e+290, 1.0000000000000001e+291, 1e+292, 1.0000000000000001e+293, 1e+294, 1.0000000000000001e+295, 1.0000000000000002e+296, 1e+297, 1.0000000000000001e+298, 1e+299, 1e+300, 1e+301, 1e+302, 1e+303, 1.0000000000000001e+304, 1.0000000000000001e+305, 1e+306, 1.0000000000000001e+307, 1e+308

This was generated with this code:

import math
import sys
from decimal import Decimal

def nextafter(x, dir):
    m, e = math.frexp(x)
    return x + dir * sys.float_info.epsilon * 2.0**(e - 1)

for i in range(1, 320):
    pow10 = Decimal(10) ** i

    if Decimal(float(pow10)).logb() != i:
        pow10 = Decimal(nextafter(float(pow10), 1))
        assert Decimal(float(pow10)).logb() == i
    assert Decimal(nextafter(float(pow10), -1)).logb() < i

    print(float(pow10))

Searching this can be done linearly if speed isn't an issue, which is presumably the case here; otherwise either use a binary search, or (faster) estimate a lower bound using a bit of math, and do a couple of checks to refine that estimate.

\$\endgroup\$
1
  • \$\begingroup\$ This seems to be a review of my answer, rather than of the original code! \$\endgroup\$ Oct 10, 2018 at 7:19
0
\$\begingroup\$

suggest keeping the code simple, short, etc.

#include <string.h>
#include <stdio.h>

size_t length(double number)
{
    char buffer[1024] = {'\0'};

    sprintf( buffer, "%d", (int)number );
    return strlen( buffer );
}
\$\endgroup\$
1
  • \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Oct 10, 2018 at 7:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.