diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 881918d2e..86d6b9071 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -15,6 +15,40 @@ read first.
---
+## Unreleased — since 2.2.0
+
+### At a glance
+
+| Silent? | What | Was | Is |
+|---|---|---|---|
+| | `(Entity.Number)someBigInteger` | `FormatException: Illegal character found`, for almost every value | the number |
+
+### A `BigInteger` converts to a `Number` instead of throwing
+
+`Entity` and `Entity.Number` both offer an implicit conversion from
+`System.Numerics.BigInteger`, and they did not agree. `Entity`'s read the two's-complement bytes;
+`Number`'s passed the same bytes to `EInteger.FromString`, whose `byte[]` overload reads them as
+**ASCII digits**:
+
+```csharp
+Entity fine = new BigInteger(123456789); // 123456789
+Entity.Number threw = new BigInteger(123456789); // FormatException: Illegal character found
+```
+
+So the conversion failed for every value whose bytes are not digit characters — which is nearly all
+of them, `1` included. The few that worked did so by accident: `12594` is the two bytes `'2'` and
+`'1'`, and came out as `21`.
+
+Nothing in the library reached it, which is why no test caught it; it is only on the public surface.
+Both conversions now read the bytes, and a test requires the two to agree rather than merely to
+work.
+
+Found while documenting the members a `#pragma warning disable CS1591` was covering — the two
+conversions sit in different files and had to be read side by side to look wrong.
+[#585](https://github.com/asc-community/AngouriMath/issues/585).
+
+---
+
## 2.2.0 — since 2.1.0
### At a glance
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs
index 6195a01f0..c7d1a8c7b 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs
@@ -42,26 +42,84 @@ public abstract partial record Number : ContinuousNode
/// For example, integers and rationals are such
///
public abstract bool IsExact { get; }
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ // Each of these lands on the narrowest kind that holds the value exactly: the
+ // integral types on Integer, a ratio on Rational, the rest on Real. Number is
+ // abstract, so what comes back is always one of its concrete kinds.
+
+ /// The number as an .
public static implicit operator Number(sbyte value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(byte value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(short value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(ushort value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(int value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(uint value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(long value) => Integer.Create(value);
+
+ /// The number as an .
public static implicit operator Number(ulong value) => Integer.Create(value);
+
+ ///
+ /// The integer as an , of any size — this is the conversion to
+ /// reach for where a value will not fit in a .
+ ///
public static implicit operator Number(EInteger value) => Integer.Create(value);
+
+ ///
+ /// The ratio as a , in lowest terms and with a positive
+ /// denominator — or an where the denominator reduces to one.
+ ///
public static implicit operator Number(ERational value) => Rational.Create(value);
+
+ /// The decimal as a .
public static implicit operator Number(EDecimal value) => Real.Create(value);
+
+ ///
+ /// The value as a , read as the binary it
+ /// is — so 0.1f becomes the value that literal holds, not one tenth.
+ ///
public static implicit operator Number(float value) => Real.Create(EDecimal.FromSingle(value));
+
+ ///
+ /// The value as a , read as the binary it
+ /// is rather than as the decimal it was written as.
+ ///
public static implicit operator Number(double value) => Real.Create(EDecimal.FromDouble(value));
+
+ ///
+ /// The value as a . A is decimal
+ /// already, so this one keeps the digits that were written.
+ ///
public static implicit operator Number(decimal value) => Real.Create(EDecimal.FromDecimal(value));
+
+ ///
+ /// The .NET complex number as a , through its two
+ /// parts and their precision.
+ ///
public static implicit operator Number(System.Numerics.Complex value)
=> Complex.Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary));
+ /// The big integer as an , exactly and at any size.
+ ///
+ /// Read as the two's-complement bytes it is, which is what
+ /// BigInteger.ToByteArray returns and what the same conversion on
+ /// has always done. The overload reached before was
+ /// EInteger.FromString(byte[]), which reads a byte array as ASCII digits, so
+ /// every value whose bytes are not digit characters threw a FormatException —
+ /// which is nearly all of them, 1 included.
+ ///
public static implicit operator Number(System.Numerics.BigInteger bigInt)
- => Integer.Create(EInteger.FromString(bigInt.ToByteArray()));
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+ => Integer.Create(EInteger.FromBytes(bigInt.ToByteArray(), littleEndian: true));
}
}
}
diff --git a/Sources/Tests/UnitTests/Core/BigIntegerConversionTest.cs b/Sources/Tests/UnitTests/Core/BigIntegerConversionTest.cs
new file mode 100644
index 000000000..17cfc48b7
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/BigIntegerConversionTest.cs
@@ -0,0 +1,75 @@
+//
+// Copyright (c) 2019-2026 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using System.Numerics;
+using AngouriMath;
+using Xunit;
+
+namespace AngouriMath.Tests.Core
+{
+ ///
+ /// The two conversions from , which used to disagree: the one to
+ /// read the bytes and the one to parsed
+ /// them as text, so the second threw on nearly every input.
+ ///
+ [Trait("Area", "Core")]
+ public sealed class BigIntegerConversionTest
+ {
+ public static readonly TheoryData Values = new()
+ {
+ "0", "1", "-1", "2", "-2", "127", "128", "255", "256", "-128", "-129",
+ "123456789", "-123456789", "9223372036854775807", "9223372036854775808",
+ "-9223372036854775808", "-9223372036854775809",
+ "170141183460469231731687303715884105728",
+ "-170141183460469231731687303715884105728",
+ };
+
+ /// To , which is the one that was broken.
+ [Theory]
+ [MemberData(nameof(Values))]
+ public void ToNumber(string text)
+ {
+ Entity.Number converted = BigInteger.Parse(text);
+ Assert.Equal(text, converted.Stringize());
+ }
+
+ /// And to , which was not, pinned so it stays that way.
+ [Theory]
+ [MemberData(nameof(Values))]
+ public void ToEntity(string text)
+ {
+ Entity converted = BigInteger.Parse(text);
+ Assert.Equal(text, converted.Stringize());
+ }
+
+ ///
+ /// And the two agree, which is the property whose absence was the defect: one conversion
+ /// of one value should not depend on which of them the caller reaches.
+ ///
+ [Theory]
+ [MemberData(nameof(Values))]
+ public void AndTheTwoAgree(string text)
+ {
+ var big = BigInteger.Parse(text);
+ Entity asEntity = big;
+ Entity.Number asNumber = big;
+ Assert.Equal(asEntity, (Entity)asNumber);
+ }
+
+ ///
+ /// A value whose bytes happen to be ASCII digits was the only kind that used to work,
+ /// and it worked by accident: 12594 is the two bytes '2' and '1', which the old code
+ /// read as the number 21.
+ ///
+ [Fact]
+ public void TheValuesThatUsedToWorkWorkedByAccident()
+ {
+ Entity.Number converted = new BigInteger(12594);
+ Assert.Equal("12594", converted.Stringize());
+ }
+ }
+}