Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

BigInt inconsistencies in PowerShell and C#

According to microsoft documentation the [BigInt] datatype seems to have no defined maximum value and theoretically can hold an infinitely large number, but I found that after the 28th digit, some weird things start to occur:

PS C:\Users\Neko> [BigInt]9999999999999999999999999999
9999999999999999999999999999
PS C:\Users\Neko> [BigInt]99999999999999999999999999999
99999999999999991433150857216

As you can see, on the first command1, the BigInt works as intended, but with one more digit, some falloff seems to occur where it translates 99999999999999999999999999999 to 99999999999999991433150857216 however, the prompt throws no error and you can continue to add more digits until the 310th digit

PS C:\Users\Neko> [BigInt]99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
100000000000000001097906362944045541740492309677311846336810682903157585404911491537163328978494688899061249669721172515611590283743140088328307009198146046031271664502933027185697489699588559043338384466165001178426897626212945177628091195786707458122783970171784415105291802893207873272974885715430223118336
PS C:\Users\Neko\> [BigInt]999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999

which will throw the error

At line:1 char:318
+ ... 999999999999999999999999999999999999999999999999999999999999999999999
+                                                                          ~
The numeric constant 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 is not valid.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : BadNumericConstant

Which I believe is a console issue rather than a BigInt issue because the error doesn't mention the [BigInt] datatype unlike numbers too big for other datatypes like

PS C:\Users\Neko> [UInt64]18446744073709551615
18446744073709551615
PS C:\Users\Neko> [UInt64]18446744073709551616
Cannot convert value "18446744073709551616" to type "System.UInt64". Error: "Value was either too large or too small
for a UInt64."
At line:1 char:1
+ [UInt64]18446744073709551616
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvalidCastIConvertible

As for C#, System.Numerics.BigInt will start throwing an error at the 20th digit, 99999999999999999999 when hard coded:

namespace Test
{
    class Test
    {
        static void Main()
        {
            System.Numerics.BigInteger TestInput;
            System.Numerics.BigInteger Test = 99999999999999999999;
            System.Console.WriteLine(Test);
        }
    }
}

When trying to build in Visual Studio I get the error

Integral constant is too large

However, I can enter a bigger number to ReadLine without causing an error

namespace Test
{
    class Test
    {
        static void Main()
        {
            System.Numerics.BigInteger TestInput;
            TestInput = System.Numerics.BigInteger.Parse(System.Console.ReadLine());
            System.Console.WriteLine(TestInput);
        }
    }
}

Which seems to indeed be infinite. The input

99999999999...

(24720 characters total) works fine2

So what is causing all of this weird activity with [BigInt]?


1which is 28 digits according to ([Char[]]"$([BigInt]9999999999999999999999999999)").count

2I am too lazy to count the digits and trying to parse the digits into PowerShell causes an error. According to this it is 24720 characters

like image 638
Nico Nekoru Avatar asked Jul 18 '20 21:07

Nico Nekoru


2 Answers

TLDR: Use [BigInt]::Parse or 'literal' syntax prior to Powershell Core 7.0; otherwise use the n suffix.

The Problem - double literals

When it comes to un-suffixed literals, Powershell will use the first type the value fits in. The order for integral literals is int, long, decimal and then double. From the documentation for Powershell 5.1 (bolding mine; this paragraph is the same for Powershell Core):

For an integer literal with no type suffix:

  • If the value can be represented by type [int], that is its type.
  • Otherwise, if the value can be represented by type [long], that is its type.
  • Otherwise, if the value can be represented by type [decimal], that is its type.
  • Otherwise, it is represented by type [double].

In your case the value exceeds that of decimal.MaxValue so your literal is by default a double literal. That double value is not exactly representable and is "converted" to the closest representable double.

$h = [double]99999999999999999999999999999
"{0:G29}" -f $h

Outputs

99999999999999991000000000000

Obviously that's not the exact number, just a representation in string form. But it gives you an idea what's going on. Now we take this inexact double value and we cast it to BigInt. The original loss in precision is transferred over and compounded upon by the conversion operator. This is what is actually happening in Powershell (note the cast to BigInt):

$h = [BigInt][double]99999999999999999999999999999
"{0:G}" -f $h

Outputs

99999999999999991433150857216

This is in fact the closest representable double value. If you could print the exact value of the double from the first example, this is what it would print. When you add the additional extra digits, you exceed the largest value of a numeric literal, thus the other exception you received.

C# Inconsistencies

Unlike Powershell, C# uses integral literals by default which is why you get the exception for a lot fewer digits. Adding the D suffix in C# will give you a larger range. The following works fine and will be a double.

var h = 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999D;

Adding one more digit will raise the following error:

error CS0594: Floating-point constant is outside the range of type 'double'

Note that in Powershell the D suffix is used for decimal literals and not double. There is not an explicit suffix for double--it is assumed to be the default.

Solutions

Back to your original problem, depending on your Powershell version the solution may vary:

[BigInt]::Parse

If you are using Windows Powershell or Powershell Core <= v6.2, one option is to use BigInteger.Parse:

[bigint]::Parse("99999999999999999999999999999") 

Outputs:

99999999999999999999999999999

Large Value Literals

As pointed out in the comments, another option that works is to enclose the literal in quotes.

[bigint]'99999999999999999999999999999' 

Outputs

99999999999999999999999999999

Despite how it looks, this is not shorthand for [bigint]::new([string]) (see below). This is instead a way to ensure that the literal is not treated as a double but rather as an integral literal with many digits, a so-called "large value literal". See this section of the docs.

N Integral Suffix (v7.0+)

Powershell Core 6.2 introduced many new literal suffixes for integral types such as unsigned, short, and byte but did not introduce one for bigint. That came along in Powershell Core 7.0 via the n suffix. This means you can now do the following:

99999999999999999999999999999n 

Outputs:

99999999999999999999999999999

See the documentation for more information on the suffixes available in Powershell Core.

[BigInt]::new

If you were to try [bigint]::new('literal') Powershell recognizes that you intend to use the value as a literal. There is in fact no constructor for BigInt that accepts a string (we use Parse for that) nor is there a constructor which accepts another BigInt. There is however a constructor that takes a double. Our large-value literal will start as a BigInt, Powershell will then implicitly convert that to a double (losing precision) and then pass it to [bigint]::new([double]) as the best match, once again giving an incorrect result:

[bigint]::new('99999999999999999999999999999') 

Outputs:

99999999999999991433150857216
like image 125
pinkfloydx33 Avatar answered Nov 09 '22 01:11

pinkfloydx33


Unfortunately C# have not literal for BigInteger. There are two ways to instantiate BigInteger:

  1. Convert from primitive type like int, long (cast or using constructor)
  2. Parse from string using BigInteger.Parse

BigInteger test = BigInteger.Parse("32439845934875938475398457938457389475983475893475389457839475");
Console.WriteLine(test.ToString());
// output: 32439845934875938475398457938457389475983475893475389457839475

See How PowerShell parses numeric literals

like image 10
Dmitry Kolchev Avatar answered Nov 09 '22 00:11

Dmitry Kolchev