BigInt inconsistencies in PowerShell and C#

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

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


To complement the existing, helpful answers - notably pinkfloydx33's - with a succinct summary:

By default, all PowerShell versions up to at least v7.0 use [double] as the data type for number literals that are larger than [decimal]::MaxValue, which invariably leads to a loss of accuracy.

  • Up to v6.x (which includes Windows PowerShell), the only way to avoid this is to use [bigint]::Parse() with the number represented as a string:
[bigint]::Parse('99999999999999999999999999999')

# A *cast* works too, as also shown in pinkfloydx33's answer:
[bigint] '99999999999999999999999999999'
  • In v7+, you can use the n suffix to designate a number literal as a [bigint]:
99999999999999999999999999999n  # v7+; parses as a [bigint], due to suffix 'n'

Note: Arguably, given that PowerShell usually automatically picks an appropriate number type, it should assume n in this case, i.e. it should parse unsuffixed 99999999999999999999999999999 as a [bigint], not as a [double] - see this GitHub proposal.


Further reading:

  • See about_Numeric_Literals, which shows all number-type suffixes, including what PowerShell version they were introduced in.

  • This answer summarizes how number literals are interpreted in PowerShell.

    • In short: For integer literals, [int] is the smallest type ever chosen, with [long] or [decimal] chosen as needed to accommodate larger values, with [double] used for values beyond [decimal]::MaxValue.