39

Is there a way to get the logarithm of a BigInt in JavaScript?

With normal numbers, you would use this code:

const largeNumber = 1000;
const result = Math.log(largeNumber);

However, I need to work with factorial numbers, potentially higher than 170!, so the regular number type doesn't work. Math.log doesn't work with BigInt. So how do I get the logarithm?

const largeNumber = BigInt(1000);
const result = ???
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Mielipuoli
  • 1,306
  • 2
  • 12
  • 23
  • A BigInt must be converted to a number first. – evolutionxbox Dec 16 '21 at 16:18
  • 2
    You might have to calculate it yourself. – evolutionxbox Dec 16 '21 at 16:28
  • This might help? https://stackoverflow.com/questions/67445759/what-is-the-definition-of-the-natural-logarithm-finding-the-value-of-natural – evolutionxbox Dec 16 '21 at 16:31
  • 10
    what logarithm do you want? – Nina Scholz Dec 16 '21 at 16:32
  • 2
    See the [BigInt Math for JavaScript proposal](//github.com/tc39/proposal-bigint-math). – Sebastian Simon Dec 16 '21 at 16:37
  • 3
    Which data type you expect as return value? Can you edit your question and give specifications of the function you are looking for, including (extreme) examples of input and expected output? – trincot Dec 16 '21 at 16:49
  • 8
    @wahwahwah What makes you think OP is confused here? Taking the logarithm of a BigInt seems like a very valid question. – idmean Dec 16 '21 at 16:59
  • Apparently my question was more ambiguous than I anticipated, so I elaborated on it. – Mielipuoli Dec 16 '21 at 18:35
  • Have a look at https://github.com/AlexSp3/Basenumber.js – Gabriele Petrioli Dec 16 '21 at 19:24
  • @idmean - because of the comments. The OP hadnt updated their post to describe whether they're looking for "how BigInt works" vs how to do the conversion properly. Check the edits. It's easy to assume that there was maybe a language barrier issue vs a technical problem. – wahwahwah Dec 16 '21 at 20:48
  • 1
    @NinaScholz, it really doesn't matter which base logarithm they want, since they only differ by a constant factor. Unless you mean to round the result, e.g. a base-2 rounded to an integer would be easy, but only useful in some cases. – ilkkachu Dec 17 '21 at 12:58
  • 2
    For those finding their way here and needing high precision logarithms of BigInt size numbers, another simple option is to leverage decimal.js at https://github.com/MikeMcl/decimal.js ... – Trentium Dec 17 '21 at 19:29
  • Depending on the precision you need, "convert to string and get the length" can be a surprisingly viable option to calculate `log10` that I have personally used in situations like this. – Silvio Mayolo Dec 18 '21 at 21:49

6 Answers6

32

In case you don't want to return a BigInt, then the following might work for you too:

function log10(bigint) {
  if (bigint < 0) return NaN;
  const s = bigint.toString(10);

  return s.length + Math.log10("0." + s.substring(0, 15))
}

function log(bigint) {
  return log10(bigint) * Math.log(10);
}

function natlog(bigint) {
  if (bigint < 0) return NaN;

  const s = bigint.toString(16);
  const s15 = s.substring(0, 15);

  return Math.log(16) * (s.length - s15.length) + Math.log("0x" + s15);
}

const largeNumber = BigInt('9039845039485903949384755723427863486200719925474009384509283489374539477777093824750398247503894750384750238947502389475029384755555555555555555555555555555555555555554444444444444444444444444222222222222222222222255666666666666938475938475938475938408932475023847502384750923847502389475023987450238947509238475092384750923847502389457028394750293847509384570238497575938475938475938475938475555555555559843991');

console.log(natlog(largeNumber)); // 948.5641152531601
console.log(log10(largeNumber), log(largeNumber), log(-1))
// 411.95616098588766
// 948.5641152531603
// NaN

log10() will return a standard precision float for any BigInt or Int number you enter as an argument.


As @Mielipuoli quite rightly mentioned, the natural logarithm can be calculated as

function log(bigint) {
  return log10(bigint) / Math.log10(Math.E);
}

Or, even simpler, as shown in my snippet above, as log10(bigint) * Math.log(10).

@Nat already explained in a comment below, how this approach works, i.e. by calculating the integer and fractional parts of the logarithm separately and summing them up. With regards to the precision of the result: the Math.log10() works on a float number with its usual 13 to 14 decimal digits precision, and so, for a result, this is all you can expect too.

For this reason, I truncated the string representation of the BigInt number to 15 characters. Any further decimal places would have been ignored in the implicit type conversion to float anyway.

I also added the hex-string version here, suggested by @PeterCordes and further developed by @somebody as natlog(). It works - probably faster than my original solution - and produces the "same" result (only the very last shown digit deviates between the two results)!

double-beep
  • 5,031
  • 17
  • 33
  • 41
Carsten Massmann
  • 26,510
  • 2
  • 22
  • 43
  • 3
    Thank you, this works! Small notes: it returns the log_10 instead of the natural log, but that can be fixed by dividing the result by Math.log10(Math.E). Also I would return NaN instead of null if bigint < 0. – Mielipuoli Dec 16 '21 at 22:15
  • 2
    This is one of the places where explanatory comments on why this calculation is used, and how correct the approximation is, would be a really good thing. – Thorbjørn Ravn Andersen Dec 17 '21 at 02:06
  • 4
    @Mielipuoli: If you don't actually want log10, then base 10 is an unnecessarily expensive choice of a base to convert to. Assuming BigInt internally uses binary chunks, converting to hex should be much cheaper, since each hex digit only depends on 4 bits, not on all higher bits (which requires BigInt division). But getting the fractional part then becomes trickier; unless JS allows a radix point in non-decimal numbers like `0x0.abc123`. – Peter Cordes Dec 17 '21 at 02:12
  • 2
    To explain the algorithm: log(a*b)=log(a)+log(b) ==> log(a/b)=log(a)-log(b) ==> log(a)=log(a/b)+log(b) ==> log10(BigInt)=log10("0."+BigInt)+log10(10^BigInt.Length) ==> log10(BigInt)=log10("0."+BigInt)+BigInt.Length. – Nat Dec 17 '21 at 03:46
  • 1
    @PeterCordes of course... you could always just use a hex substring, *without* the decimal point, and subtract `Math.min(length, 15)` from it – somebody Dec 17 '21 at 08:06
  • 1
    rather, `Math.min(length, 15) / Math.log(base)` or something – somebody Dec 17 '21 at 08:14
  • @somebody: Nice. If you want this to be an optimization, IDK if JS JIT compilers will constant-propagate through `Math.log(16)`. If not, you might need to manually do constant propagation and reciprocal to do `Math.min(length, 15) * 0.36067376022224085184 // 1.0 / ln(16)`. ([Multiplication is significantly cheaper than division in asm](https://stackoverflow.com/a/45899202/224132), but compilers can't do that for you except for values like `2.0` whose reciprocal can be exactly represented as `double`, i.e. that form a fraction with a power-of-2 denominator. Assuming strict / precise FP.) – Peter Cordes Dec 17 '21 at 08:18
  • right. was just suggesting a way to get the fractional part - i'd imagine the slow part (converting to a decimal string) is the main concern (?) here anyway? for maintainability it should probably be named though: `const INV_LN16 = 0.36...` – somebody Dec 17 '21 at 08:22
  • oh, nice trick. – ilkkachu Dec 17 '21 at 13:02
25

The other answers have adequately addressed the question you give in the title, viz.: "how do I compute the logarithm of a BigInt?". However, you also mention that you are particularly interested in logarithms of factorials, for which a different algorithm avoids your range difficulties.

Applying log(ab) = log(a) + log(b), the following function computes the log of a factorial:

function logFactorial(n) {
  let total = 0;
  for (let current = 1; current <= n; ++current) {
    total += Math.log10(current);
  }

  return total;
}

console.log(logFactorial(170));
double-beep
  • 5,031
  • 17
  • 33
  • 41
Jacob Manaker
  • 723
  • 4
  • 17
  • 4
    note that this would accumulate some error. how bad the error is... who knows. but it might be worth trying other approaches. like summing the log10 or log of every prime number below n e.g. `Math.log(2) * (Math.floor(n / 2) + Math.floor(n / 4) + Math.floor(n / 8) ...etc)` – somebody Dec 17 '21 at 08:09
  • ... but of course something much simpler like this is probably good enough for most purposes – somebody Dec 17 '21 at 08:10
  • 3
    That's actually brilliant! Then I don't need a BigInt at all. Technically it doesn't answer the question as I posted it, so I cannot mark it as such. But it is exactly what I need, so thank you very much! :) – Mielipuoli Dec 17 '21 at 08:15
  • 3
    Yes, it seems almost like an https://www.google.com/search?q=xy+question . Adding up logarithms makes more sense than working with very large integers! – Carsten Massmann Dec 17 '21 at 09:12
  • 2
    As a micro-optimization you can start the loop from 2, since log(1) = 0. – Ilmari Karonen Dec 17 '21 at 13:01
  • 5
    If you want the logarithm of a factorial, then Stirling's approximation: log(n!)~ n log n - n + O(log n) should suffice. You can sharpen the approximation. See eg https://en.wikipedia.org/wiki/Stirling%27s_approximation – delsim Dec 17 '21 at 19:57
1

Inspired from MWO's answer, you could simply convert the BigInt into a string with the same base as the logarithm that you want to calculate and get the string length.

For example to calculate floor(log2(9007199254740991)) you can do BigInt("9007199254740991").toString(2).length - 1.

Note that toString only allows bases from 2 to 36.

idmean
  • 14,540
  • 9
  • 54
  • 83
  • 1
    The `- 1` will just introduce bias in the opposite direction. `BigInt("8").toString(2).length` is `4` and `BigInt("31").toString(2).length - 1` is also `4`. – Sebastian Simon Dec 16 '21 at 21:07
  • @SebastianSimon Perhaps I'm just tired and confused, but I don't understand your example. Yes, `BigInt("31").toString(2).length - 1` is 4. But that's correct, isn't it? floor(log2(31)) = 4. (Although there is a possibility the answer was edited after your comment, which would explain it) – Stef Dec 17 '21 at 11:28
  • @Stef Yes, of course it’s correct; I was indeed referring to the edit history, where the author changed _“to calculate `floor(log2(`…`)) + 1` you can do `BigInt("`…`").toString(2).length`”_ to _“to calculate `floor(log2(`…`))` you can do `BigInt("`…`").toString(2).length - 1`”_, maybe unintentionally since these edits were a minute apart. – Sebastian Simon Dec 17 '21 at 11:34
  • 1
    well, you only get the log rounded/truncated to an integer with this, so e.g. log10(1000) == log10(9999), which might not be that useful in many contexts... – ilkkachu Dec 17 '21 at 13:03
1

Following up on my earlier comment, if one ever finds themselves seeking a really high precision logarithm, there are a couple of big decimal packages available that offer this capability. For example, the code snippet below makes use of decimal.js to a precision of 1000 digits in order to calculate...

  • 170! using BigInt to validate 170! when using decimal.js
  • 170! using decimal.js
  • ln( 170! )
  • log10( 170! )
  • exp( ln( 170! ) )
  • round( exp( ln( 170! ) ) )

<style>
textarea {
    width: 100%;
    height: 100vh;
}
</style>

<textarea id=result width:"100%" height:"100vh"></textarea>

<script src="https://cdnjs.cloudflare.com/ajax/libs/decimal.js/10.3.1/decimal.min.js"></script>

<script>

let result = document.getElementById( 'result' );

Decimal.precision = 1000;
Decimal.toExpPos = 1000;


b = BigInt( 1 );
d = new Decimal( 1 );
for ( let di = 2, bi = 2n; di <= 170; di++, bi++ ) {
  d = Decimal.mul( d, di );
  b = b * bi;
}

result.value = `BigInt 170! = ${b}\n\n`;
result.value += `decimal.js 170! = ${d.toString()}\n\n`;

result.value += `ln( 170! ) = ${Decimal.ln( d ).toString()}\n\n`;
result.value += `log10( 170! ) = ${Decimal.log10( d ).toString()}\n\n`;

result.value += `exp( ln ( 170! ) ) = ${Decimal.exp( Decimal.ln( d ) ).toString()}\n\n`;
result.value += `round( exp( ln ( 170! ) ) ) = ${Decimal.round( Decimal.exp( Decimal.ln( d ) ) ).toString()}\n\n`;
  
</script>

As an aside, amusingly, even at a 1000 digits, there are still rounding errors. Typically one will make the calculations with some addition precision by including a few more "hidden" decimal places, and then round back to the desired precision.

Trentium
  • 3,419
  • 2
  • 12
  • 19
0

Could you check if this works for you? The function returns a BigInt.

function log10(bigint) {
    const n = bigint.toString(10).length;
    return bigint > 0n ? BigInt(n - 1) : null;
}

const largeNumber = BigInt('9039845039485903949384755723427863486200719925474009384509283489374539477777093824750398247503894750384750238947502389475029384755555555555555555555555555555555555555554444444444444444444444444222222222222222222222255666666666666938475938475938475938408932475023847502384750923847502389475023987450238947509238475092384750923847502389457028394750293847509384570238497575938475938475938475938475555555555559843991')

console.log(log10(largeNumber).toString())

For Log2 would be this respectively:

 const largeNumber = BigInt('9039845039485903949384755723427863486200719925474009384509283489374539477777093824750398247503894750384750238947502389475029384755555555555555555555555555555555555555554444444444444444444444444222222222222222222222255666666666666938475938475938475938408932475023847502384750923847502389475023987450238947509238475092384750923847502389457028394750293847509384570238497575938475938475938475938475555555555559843991')

    function log2(bigint) {
        const n = bigint.toString(2).length;
        return bigint > 0n ? BigInt(n - 1) : null;
    }
    
    console.log(log2(largeNumber).toString())
MWO
  • 2,627
  • 2
  • 10
  • 25
  • 2
    It works if I wanted a BigInt in return. But the point was to actually get a number with decimal precision, so that I can continue with other mathematics more complex than the mere basic operators that work with BigInt. But thanks! – Mielipuoli Dec 16 '21 at 22:26
0

If it's purely in string form, for mine I just lazily do

- log()    here means natural-log ln()
- length() here means string length, sometimes called len()

- input bigint_str x

     ( length(x) * log(10) ) + log( "0." x )

Single liner, no loops, no recursion, no specialized bigint library - nothing.

Granted, it's precision is capped by IEEE 64-bit double precision FP, so its's accurate to 15 or so significant decimal digits.

Because one is prepending "0." in the 2nd half, that portion won't overflow or underflow unless your string is too long e.g. like more than 500k digits etc

If that's the case, trim it down to first 300 digits or so - that's way more than sufficient, since, by and large, it's dominated by the left side term describing order of magnitude, with right side only performing minor accuracy adjustments

RARE Kpop Manifesto
  • 2,453
  • 3
  • 11