Skip to content

[API Proposal]: Add System.Numerics.BigNumber #131221

Description

@eiriktsarpalis

Background and motivation

Interchange formats can carry finite base-10 values outside the ranges of the primitive numeric types. Callers currently preserve these values as text or maintain an ad hoc BigInteger significand and scale; neither provides shared parsing, conversion, numeric equality, or ordering.

BigNumber is an exchange type only. Arithmetic and generic-math number contracts are explicitly out of scope, leaving BigDecimal and BigFloat available for future arithmetic types. Related: #125611, #20681, #83907, #118134.

API Proposal

Parsing and formatting are invariant, and provider arguments are ignored. Numerically equivalent representations such as 1, 1.0, and 10e-1 compare equal and produce the same hash code. Signed zero preserves its sign but compares equal to zero.

namespace System.Numerics;

public readonly struct BigNumber :
    IComparable,
    IComparable<BigNumber>,
    IEquatable<BigNumber>,
    IFormattable
#if NET
    , ISpanFormattable
    , IParsable<BigNumber>
    , ISpanParsable<BigNumber>
    , IUtf8SpanFormattable
    , IUtf8SpanParsable<BigNumber>
    , IComparisonOperators<BigNumber, BigNumber, bool>
    , IEqualityOperators<BigNumber, BigNumber, bool>
#endif
{
    public static BigNumber Zero { get; }
    public bool IsZero { get; }
    public bool IsNegative { get; }
    public bool IsInteger { get; }

    public int CompareTo(BigNumber other);
    public int CompareTo(object? obj);
    public bool Equals(BigNumber other);
    public override bool Equals(object? obj);
    public override int GetHashCode();

    public static bool operator ==(BigNumber left, BigNumber right);
    public static bool operator !=(BigNumber left, BigNumber right);
    public static bool operator <(BigNumber left, BigNumber right);
    public static bool operator <=(BigNumber left, BigNumber right);
    public static bool operator >(BigNumber left, BigNumber right);
    public static bool operator >=(BigNumber left, BigNumber right);

    public static implicit operator BigNumber(byte value);
    [CLSCompliant(false)] public static implicit operator BigNumber(sbyte value);
    public static implicit operator BigNumber(short value);
    [CLSCompliant(false)] public static implicit operator BigNumber(ushort value);
    public static implicit operator BigNumber(int value);
    [CLSCompliant(false)] public static implicit operator BigNumber(uint value);
    public static implicit operator BigNumber(long value);
    [CLSCompliant(false)] public static implicit operator BigNumber(ulong value);
    public static implicit operator BigNumber(decimal value);
    public static explicit operator BigNumber(float value);
    public static explicit operator BigNumber(double value);
#if NET
    public static explicit operator BigNumber(Half value);
    public static implicit operator BigNumber(Int128 value);
    [CLSCompliant(false)] public static implicit operator BigNumber(UInt128 value);
#endif

    public static explicit operator byte(BigNumber value);
    [CLSCompliant(false)] public static explicit operator sbyte(BigNumber value);
    public static explicit operator short(BigNumber value);
    [CLSCompliant(false)] public static explicit operator ushort(BigNumber value);
    public static explicit operator int(BigNumber value);
    [CLSCompliant(false)] public static explicit operator uint(BigNumber value);
    public static explicit operator long(BigNumber value);
    [CLSCompliant(false)] public static explicit operator ulong(BigNumber value);
    public static explicit operator decimal(BigNumber value);
    public static explicit operator float(BigNumber value);
    public static explicit operator double(BigNumber value);
#if NET
    public static explicit operator Half(BigNumber value);
    public static explicit operator Int128(BigNumber value);
    [CLSCompliant(false)] public static explicit operator UInt128(BigNumber value);
#endif

    public bool TryGetByte(out byte value);
    [CLSCompliant(false)] public bool TryGetSByte(out sbyte value);
    public bool TryGetInt16(out short value);
    [CLSCompliant(false)] public bool TryGetUInt16(out ushort value);
    public bool TryGetInt32(out int value);
    [CLSCompliant(false)] public bool TryGetUInt32(out uint value);
    public bool TryGetInt64(out long value);
    [CLSCompliant(false)] public bool TryGetUInt64(out ulong value);
    public bool TryGetDecimal(out decimal value);
    public bool TryGetSingle(out float value);
    public bool TryGetDouble(out double value);
#if NET
    public bool TryGetHalf(out Half value);
    public bool TryGetInt128(out Int128 value);
    [CLSCompliant(false)] public bool TryGetUInt128(out UInt128 value);
#endif

    public static BigNumber Parse(string text);
    public static BigNumber Parse(string text, IFormatProvider? provider);
    public static BigNumber Parse(ReadOnlySpan<char> text);
    public static BigNumber Parse(ReadOnlySpan<char> text, IFormatProvider? provider);
    public static BigNumber Parse(ReadOnlySpan<byte> utf8Text);
    public static BigNumber Parse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider);

    public static bool TryParse([NotNullWhen(true)] string? text, out BigNumber result);
    public static bool TryParse([NotNullWhen(true)] string? text, IFormatProvider? provider, out BigNumber result);
    public static bool TryParse(ReadOnlySpan<char> text, out BigNumber result);
    public static bool TryParse(ReadOnlySpan<char> text, IFormatProvider? provider, out BigNumber result);
    public static bool TryParse(ReadOnlySpan<byte> utf8Text, out BigNumber result);
    public static bool TryParse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, out BigNumber result);

    public override string ToString();
    public string ToString(string? format, IFormatProvider? formatProvider);
    public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
    public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
}

Half, Int128, UInt128, and the generic parsing, formatting, and operator interfaces are available on current .NET only. The remaining surface is provided to downlevel targets, including netstandard2.0, through Microsoft.Bcl.Numerics.

Prototype: eiriktsarpalis@9599cd1

API Usage

BigNumber value = BigNumber.Parse("100000000000000000000000000000.25");
BigNumber limit = BigNumber.Parse("1e29");

bool exceedsLimit = value > limit;
bool fitsInDecimal = value.TryGetDecimal(out decimal decimalValue);
string roundTrip = value.ToString("R", null);

Alternative Designs

  • Raw text preserves the wire representation but provides no numeric equality, ordering, or checked conversion.
  • A BigInteger plus scale requires every consumer to implement parsing and normalization.
  • BigDecimal or BigFloat would imply arithmetic semantics that this type deliberately does not provide.

Risks

The significand is arbitrary precision, but the prototype bounds the base-10 exponent to ±decimal.MaxValue. The downlevel implementation and current-.NET type forwarding through Microsoft.Bcl.Numerics also become part of the compatibility contract.

Usage in dotnet/runtime

No runtime consumers are changed by the prototype. Sharing parts of NumberBuffer or adding CBOR integration may be evaluated separately; primitive numeric parsing remains bespoke.

Note

This proposal was drafted with GitHub Copilot.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions