21

I need the count the number of decimal digits of a BigInteger. For example:

  • 99 returns 2
  • 1234 returns 4
  • 9999 returns 4
  • 12345678901234567890 returns 20

I need to do this for a BigInteger with 184948 decimal digits and more. How can I do this fast and scalable?

The convert-to-String approach is slow:

public String getWritableNumber(BigInteger number) {
   // Takes over 30 seconds for 184948 decimal digits
   return "10^" + (number.toString().length() - 1);
}

This loop-devide-by-ten approach is even slower:

public String getWritableNumber(BigInteger number) {
    int digitSize = 0;
    while (!number.equals(BigInteger.ZERO)) {
        number = number.divide(BigInteger.TEN);
        digitSize++;
    }
    return "10^" + (digitSize - 1);
}

Are there any faster methods?

Geoffrey De Smet
  • 26,223
  • 11
  • 73
  • 120
  • How slow is it and how fast do you need it to be? – Kayaman Sep 16 '13 at 12:46
  • @Kayaman The faster of the 2 takes over 30 seconds for a number of 184948 decimal digits. I need it to be less than 2 seconds. – Geoffrey De Smet Sep 16 '13 at 13:10
  • 2 seconds? That sounds a lot like the time limit of a programming competition. – Bernhard Barker Sep 16 '13 at 13:13
  • There's some nice twiddles to do this - try [here](http://stackoverflow.com/a/18054857/823393) to start with. May not apply to BigInteger. – OldCurmudgeon Sep 16 '13 at 13:22
  • @Dukeling No compo, I just don't want my JUnit tests to become so slow I won't run them before committing. The code is [here](https://github.com/droolsjbpm/optaplanner/blob/master/optaplanner-examples/src/main/java/org/optaplanner/examples/common/persistence/AbstractSolutionImporter.java#L96) if you're interested. – Geoffrey De Smet Sep 16 '13 at 15:00
  • 3
    With [Guava](https://code.google.com/p/guava-libraries/), this is the one-liner `BigIntegerMath.log10(x, RoundingMode.FLOOR) + 1`. Guava uses several of the tricks discussed here. – Louis Wasserman Sep 16 '13 at 16:02

6 Answers6

16

Here's a fast method based on Dariusz's answer:

public static int getDigitCount(BigInteger number) {
  double factor = Math.log(2) / Math.log(10);
  int digitCount = (int) (factor * number.bitLength() + 1);
  if (BigInteger.TEN.pow(digitCount - 1).compareTo(number) > 0) {
    return digitCount - 1;
  }
  return digitCount;
}

The following code tests the numbers 1, 9, 10, 99, 100, 999, 1000, etc. all the way to ten-thousand digits:

public static void test() {
  for (int i = 0; i < 10000; i++) {
    BigInteger n = BigInteger.TEN.pow(i);
    if (getDigitCount(n.subtract(BigInteger.ONE)) != i || getDigitCount(n) != i + 1) {
      System.out.println("Failure: " + i);
    }
  }
  System.out.println("Done");
}

This can check a BigInteger with 184,948 decimal digits and more in well under a second.

Community
  • 1
  • 1
dln385
  • 11,630
  • 12
  • 48
  • 58
  • would just add some small changes as for ZERO you'll get 0 instead of 1 and for negative values the number of digits are little bit of (so you need to negate the number first). But otherwise good job (you and Dariusz) (gave you both a +1) – Matej Tymes May 07 '16 at 20:52
10

This looks like it is working. I haven't run exhaustive tests yet, n'or have I run any time tests but it seems to have a reasonable run time.

public class Test {
  /**
   * Optimised for huge numbers.
   *
   * http://en.wikipedia.org/wiki/Logarithm#Change_of_base
   *
   * States that log[b](x) = log[k](x)/log[k](b)
   *
   * We can get log[2](x) as the bitCount of the number so what we need is
   * essentially bitCount/log[2](10). Sadly that will lead to inaccuracies so
   * here I will attempt an iterative process that should achieve accuracy.
   *
   * log[2](10) = 3.32192809488736234787 so if I divide by 10^(bitCount/4) we
   * should not go too far. In fact repeating that process while adding (bitCount/4)
   * to the running count of the digits will end up with an accurate figure
   * given some twiddling at the end.
   * 
   * So here's the scheme:
   * 
   * While there are more than 4 bits in the number
   *   Divide by 10^(bits/4)
   *   Increase digit count by (bits/4)
   * 
   * Fiddle around to accommodate the remaining digit - if there is one.
   * 
   * Essentially - each time around the loop we remove a number of decimal 
   * digits (by dividing by 10^n) keeping a count of how many we've removed.
   * 
   * The number of digits we remove is estimated from the number of bits in the 
   * number (i.e. log[2](x) / 4). The perfect figure for the reduction would be
   * log[2](x) / 3.3219... so dividing by 4 is a good under-estimate. We 
   * don't go too far but it does mean we have to repeat it just a few times.
   */
  private int log10(BigInteger huge) {
    int digits = 0;
    int bits = huge.bitLength();
    // Serious reductions.
    while (bits > 4) {
      // 4 > log[2](10) so we should not reduce it too far.
      int reduce = bits / 4;
      // Divide by 10^reduce
      huge = huge.divide(BigInteger.TEN.pow(reduce));
      // Removed that many decimal digits.
      digits += reduce;
      // Recalculate bitLength
      bits = huge.bitLength();
    }
    // Now 4 bits or less - add 1 if necessary.
    if ( huge.intValue() > 9 ) {
      digits += 1;
    }
    return digits;
  }

  // Random tests.
  Random rnd = new Random();
  // Limit the bit length.
  int maxBits = BigInteger.TEN.pow(200000).bitLength();

  public void test() {
    // 100 tests.
    for (int i = 1; i <= 100; i++) {
      BigInteger huge = new BigInteger((int)(Math.random() * maxBits), rnd);
      // Note start time.
      long start = System.currentTimeMillis();
      // Do my method.
      int myLength = log10(huge);
      // Record my result.
      System.out.println("Digits: " + myLength+ " Took: " + (System.currentTimeMillis() - start));
      // Check the result.
      int trueLength = huge.toString().length() - 1;
      if (trueLength != myLength) {
        System.out.println("WRONG!! " + (myLength - trueLength));
      }
    }
  }

  public static void main(String args[]) {
    new Test().test();
  }

}

Took about 3 seconds on my Celeron M laptop so it should hit sub 2 seconds on some decent kit.

OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
  • Takes about 0.35 seconds on my 64bit i7 for numbers of the order of 10^180000 – OldCurmudgeon Sep 18 '13 at 13:44
  • 1
    I found a mistake in the code while testing - I was using bitCount instead of bitLength - please refresh your copy if you took one. Now takes about 0.22 seconds for your huge numbers. – OldCurmudgeon Sep 18 '13 at 14:10
  • 2
    If you want a faster algorithm, take a look at what BigDecimal uses to calculate the length of its mantissa :-) – Eric Lagergren Dec 18 '17 at 23:01
9

I think that you could use bitLength() to get a log2 value, then change the base to 10.

The result may be wrong, however, by one digit, so this is just an approximation.

However, if that's acceptable, you could always add 1 to the result and bound it to be at most. Or, subtract 1, and get at least.

Dariusz
  • 21,561
  • 9
  • 74
  • 114
  • It may be possible to combine this with `testBit` in a loop to get an exact answer, although it would be slow for some cases. – Bernhard Barker Sep 16 '13 at 12:54
  • @Dukeling I can't think of how to do it without calculating the original number. You can test few last bits, but you'd deal in probabilities anyway. – Dariusz Sep 16 '13 at 12:57
  • Well, if you have 16 bits, the range is 65536 (5 digits) - 131071 (6 digits). If you were to check the first 3 bits and you get `111`, this would guarantee 6 digits, similarly `10` guarantees 5 digits. So you can really just keep checking from the left until you get into some range containing a fixed number of digits. – Bernhard Barker Sep 16 '13 at 13:03
  • 1
    Doing BigInteger.pow() is relatively fast (less than 1 second for 184948 decimal digits IIRC). So maybe `10.pow(result)` can be used to resolve the *off by 1 digit* problem – Geoffrey De Smet Sep 16 '13 at 13:15
2

You can first convert the BigInteger to a BigDecimal and then use this answer to compute the number of digits. This seems more efficient than using BigInteger.toString() as that would allocate memory for String representation.

    private static int numberOfDigits(BigInteger value) {
        return significantDigits(new BigDecimal(value));
    }

    private static int significantDigits(BigDecimal value) {
        return value.scale() < 0
               ? value.precision() - value.scale()
               : value.precision();
    }
ajay
  • 31
  • 5
1

This is an another way to do it faster than Convert-to-String method. Not the best run time, but still reasonable 0.65 seconds versus 2.46 seconds with Convert-to-String method (at 180000 digits).

This method computes the integer part of the base-10 logarithm from the given value. However, instead of using loop-divide, it uses a technique similar to Exponentiation by Squaring.

Here is a crude implementation that achieves the runtime mentioned earlier:

public static BigInteger log(BigInteger base,BigInteger num)
{
    /* The technique tries to get the products among the squares of base
     * close to the actual value as much as possible without exceeding it.
     * */
    BigInteger resultSet = BigInteger.ZERO;
    BigInteger actMult = BigInteger.ONE;
    BigInteger lastMult = BigInteger.ONE;
    BigInteger actor = base;
    BigInteger incrementor = BigInteger.ONE;
    while(actMult.multiply(base).compareTo(num)<1)
    {
        int count = 0;
        while(actMult.multiply(actor).compareTo(num)<1)
        {
            lastMult = actor; //Keep the old squares
            actor = actor.multiply(actor); //Square the base repeatedly until the value exceeds 
            if(count>0) incrementor = incrementor.multiply(BigInteger.valueOf(2));
            //Update the current exponent of the base
            count++;
        }
        if(count == 0) break;

        /* If there is no way to multiply the "actMult"
         * with squares of the base (including the base itself)
         * without keeping it below the actual value, 
         * it is the end of the computation 
         */
        actMult = actMult.multiply(lastMult);
        resultSet = resultSet.add(incrementor);
        /* Update the product and the exponent
         * */
        actor = base;
        incrementor = BigInteger.ONE;
        //Reset the values for another iteration
    }
    return resultSet;
}
public static int digits(BigInteger num)
{
    if(num.equals(BigInteger.ZERO)) return 1;
    if(num.compareTo(BigInteger.ZERO)<0) num = num.multiply(BigInteger.valueOf(-1));
    return log(BigInteger.valueOf(10),num).intValue()+1;
}

Hope this will helps.

0

Just for the record; I can answer in JS but should be the same in any reasonable language.

Obviously you can convert it into a string and check the length but that's... what can i say... UGLY.

  • If it is not a BigInt (say less than 16 digits) Than Math.floor(Math.log10(number)) should do it.
  • If it's a BigInt then you can always divide it n times by 1e16 up until it's remainder is less than 1e16. Then the number of digits is

n * 16 + Math.floor(Math.log10(remainder))

Redu
  • 25,060
  • 6
  • 56
  • 76