diff --git a/Directory.Build.props b/Directory.Build.props
index a819bc2..5504ed8 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -21,7 +21,6 @@
True
MIT
Avatar.png
- false
diff --git a/Kotz.Extensions/INumberExt.cs b/Kotz.Extensions/INumberExt.cs
new file mode 100644
index 0000000..20b4dae
--- /dev/null
+++ b/Kotz.Extensions/INumberExt.cs
@@ -0,0 +1,225 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Kotz.Extensions;
+
+///
+/// Provides methods for converting numbers between base 10 and other numeric bases.
+///
+public static class NumberBaseExt
+{
+ ///
+ /// Converts the string representation of a number in the specified to an integer of type .
+ ///
+ /// The integer type to convert the string to.
+ /// The string representing the number.
+ /// The base of the representation in the string.
+ /// Negative numbers from certain bases cannot be converted and will throw an .
+ /// The converted to an integer of type .
+ ///
+ /// Occurs when is not representable in .
+ /// Occurs when is not representable in .
+ /// Occurs when contains characters that are not valid ASCII letters or digits.
+ ///
+ /// Occurs when a negative number could not be converted to decimal.
+ /// Occurs when the equivalent of .MaxValue or .MinValue are provided.
+ /// Occurs when contains invalid characters for the given base.
+ public static T FromDigits(this string number, int @base) where T : struct, IBinaryInteger
+ {
+ if (!number.All(char.IsAsciiLetterOrDigit))
+ throw new ArgumentException("The string must contain ASCII letters or digits only.", nameof(number));
+
+ if (number.Any(x => (char.IsAsciiDigit(x)) ? x >= '0' + @base : (char.IsAsciiLetterUpper(x)) ? x >= 'A' + @base - 10 : x >= 'a' + @base - 10))
+ throw new ArgumentException($"The number '{number}' cannot be represented in base {@base}", nameof(@base));
+
+ if (number.Length > FractionalDigits(Unsafe.SizeOf(), @base))
+ throw new ArgumentException($"The number '{number}({@base})' cannot be represented as a {nameof(T)} type.", nameof(number));
+
+ if (number.All(x => x is '0'))
+ return T.Zero;
+
+ if (@base is 10)
+ return T.Parse(number, NumberStyles.Integer, null);
+
+ var positiveValue = FromPositiveDigits(number, @base);
+
+ // I could not get this to work consistently for negative numbers
+ // If you have a solution, please submit a pull request!
+ return (number.Equals(positiveValue.ToDigits(@base), StringComparison.OrdinalIgnoreCase))
+ ? positiveValue
+ : throw new InvalidOperationException($"The negative number '{number}' could not be converted from base {@base}.");
+
+ // If the number is negative, subtract from the maximum value representable
+ // var maxRepresentableValue = T.CreateTruncating(Math.Pow(@base, number.Length));
+
+ // Calculate the two's complement value (negative)
+ // return positiveValue - maxRepresentableValue;
+ }
+
+ ///
+ /// Converts the specified to its representation in the given .
+ ///
+ /// The numeric type of the .
+ /// The number to convert.
+ /// The base for the conversion.
+ /// A string representing the in the specified .
+ /// Occurs when is less than 2 or greater than 36.
+ /// Occurs when is 10 and returns from >.
+ /// Occurs when .MinValue is provided.
+ public static string ToDigits(this T number, int @base) where T : struct, IBinaryInteger
+ {
+ if (@base is 10)
+ return number.ToString() ?? throw new InvalidOperationException($"Type {nameof(T)} returned null for ToString().");
+
+ if (T.IsZero(number))
+ return "0";
+
+ // Representations can span from 0-9 (base 2 to 10) to A-Z (base 11 to 36).
+ if (@base is < 2 or > 36)
+ throw new ArgumentOutOfRangeException(nameof(@base), @base, "Base must be between 2 and 36.");
+
+ Span buffer = stackalloc char[FractionalDigits(Unsafe.SizeOf(), @base)];
+
+ var result = (int.IsPow2(@base))
+ ? ToArbitraryBaseOptimized(number, @base, buffer)
+ : ToArbitraryBase(number, @base, buffer);
+
+ return (result.Length > 1)
+ ? result.TrimStart('0').ToString()
+ : result.ToString();
+ }
+
+ ///
+ /// Converts a to the specified base, which must be a power of two.
+ /// If is negative, it will be represented using Two's Complement.
+ ///
+ /// The numeric type of the .
+ /// The number to convert.
+ /// The base (power of two) for the conversion.
+ /// The buffer that will store the digits of the converted number.
+ /// A span containing the digits of in the specified .
+ private static Span ToArbitraryBaseOptimized(T number, int @base, Span buffer) where T : IBinaryInteger
+ {
+ var truncatedBase = T.CreateTruncating(@base);
+ var counter = buffer.Length;
+ var bits = int.Log2(@base);
+
+ do
+ {
+ var nextChar = '0' + uint.CreateTruncating((number & (truncatedBase - T.One)));
+ buffer[--counter] = (char)((nextChar > '9') ? nextChar + 7 : nextChar);
+ number >>>= bits;
+ } while (!T.IsZero(number));
+
+ return buffer[counter..];
+ }
+
+ ///
+ /// Converts a to the specified .
+ ///
+ /// The numeric type of the .
+ /// The number to convert.
+ /// The base for the conversion.
+ /// The buffer that will store the digits of the converted number.
+ /// A span containing the digits of in the specified .
+ /// Occurs when .MinValue is provided.
+ private static Span ToArbitraryBase(T number, int @base, Span buffer) where T : IBinaryInteger
+ {
+ if (T.IsNegative(number))
+ return ToTwosComplement(number, @base, buffer);
+
+ var truncatedBase = T.CreateTruncating(@base);
+ var counter = buffer.Length;
+
+ do
+ {
+ var nextChar = '0' + uint.CreateTruncating(number % truncatedBase);
+ buffer[--counter] = (char)((nextChar > '9') ? nextChar + 7 : nextChar);
+ number /= @truncatedBase;
+
+ } while (!T.IsZero(number));
+
+ return buffer[counter..];
+ }
+
+ ///
+ /// Converts a negative to the specified base using Two's Complement representation.
+ ///
+ /// The numeric type of the .
+ /// The negative number to convert.
+ /// The base (power of two) for the conversion.
+ /// The buffer that will store the digits of the converted number.
+ ///
+ /// A span containing the digits of in the specified ,
+ /// represented using Two's Complement.
+ ///
+ /// Occurs when .MinValue is provided.
+ private static Span ToTwosComplement(T number, int @base, Span buffer) where T : IBinaryInteger
+ {
+ var truncatedNumber = T.Abs(number);
+ var truncatedBase = T.CreateTruncating(@base);
+
+ // Convert number to @base while complementing it.
+ for (var index = buffer.Length - 1; index >= 0; index--)
+ {
+ var nextChar = '0' + uint.CreateTruncating((truncatedBase - T.One) - (truncatedNumber % truncatedBase));
+ buffer[index] = (char)((nextChar > '9') ? nextChar + 7 : nextChar);
+ truncatedNumber /= truncatedBase;
+ }
+
+ // Add 1 to the result in @base;
+ for (var index = buffer.Length - 1; index >= 0; index--)
+ {
+ if (++buffer[index] != @base + '0')
+ break;
+
+ buffer[index] = '0';
+ }
+
+ return buffer;
+ }
+
+ ///
+ /// Converts a positive from the specified to a .
+ ///
+ /// The numeric type of the .
+ /// The negative number to convert.
+ /// The base for the conversion.
+ /// The decimal representation of the .
+ /// Occurs when contains invalid digits.
+ private static T FromPositiveDigits(string number, int @base) where T : IBinaryInteger
+ {
+ var result = T.Zero;
+ var truncatedBase = T.CreateTruncating(@base);
+ var digitOffset = T.CreateTruncating('0');
+ var upperOffset = T.CreateTruncating('A' - 10);
+ var lowerOffset = T.CreateTruncating('a' - 10);
+
+ foreach (var digit in number)
+ {
+ var offset = digit switch
+ {
+ >= '0' and <= '9' => digitOffset,
+ >= 'A' and <= 'Z' => upperOffset,
+ >= 'a' and <= 'z' => lowerOffset,
+ _ => throw new UnreachableException($"Invalid character: {digit}")
+ };
+
+ result = (result * truncatedBase) + T.CreateTruncating(digit) - offset;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Calculates the maximum amount of fractional digits a number can have for
+ /// a given base and type size.
+ ///
+ /// The size of the type, in bytes.
+ /// The base to calculate the digits for.
+ /// The amount of fractional digits in the number.
+ private static int FractionalDigits(int typeSize, int targetBase)
+ => (int)Math.Ceiling((typeSize * 8 * Math.Log(2)) / Math.Log(targetBase));
+}
\ No newline at end of file
diff --git a/Kotz.Extensions/README.md b/Kotz.Extensions/README.md
index e37024f..269aa8d 100644
--- a/Kotz.Extensions/README.md
+++ b/Kotz.Extensions/README.md
@@ -57,6 +57,9 @@ Defines the following extension methods:
- **List\ Extensions**
- AsSpan: Gets the current list as a `Span` object.
- AsReadOnlySpan: Gets the current list as a `ReadOnlySpan` object.
+- **Number Extensions**
+ - FromBase: Converts the string representation of a number in the specified base to an integer of the specified type.
+ - ToBase: Converts the specified number to its representation in the given base.
- **Object Extensions**
- EqualsAny: Checks whether the current object equals any of the specified objects.
- **ReadOnlySpan\ Extensions**
diff --git a/Kotz.Tests/Extensions/NumberBaseExtTests.cs b/Kotz.Tests/Extensions/NumberBaseExtTests.cs
new file mode 100644
index 0000000..9bba545
--- /dev/null
+++ b/Kotz.Tests/Extensions/NumberBaseExtTests.cs
@@ -0,0 +1,91 @@
+using System.Diagnostics;
+
+namespace Kotz.Tests.Extensions;
+
+public sealed class NumberBaseExtTests
+{
+ [Theory]
+ [InlineData(2146483647, 2147483647, 2)]
+ [InlineData(2146483647, 2147483647, 8)]
+ [InlineData(2146483647, 2147483647, 16)]
+ [InlineData(0, 10000, 2)]
+ [InlineData(0, 10000, 8)]
+ [InlineData(0, 10000, 16)]
+ [InlineData(-10000, 0, 2)]
+ [InlineData(-10000, 0, 8)]
+ [InlineData(-10000, 0, 16)]
+ [InlineData(-2147483648, -2146483648, 2)]
+ [InlineData(-2147483648, -2146483648, 8)]
+ [InlineData(-2147483648, -2146483648, 16)]
+ internal void FromDigitsTest(int min, int max, int @base)
+ {
+ while (min < max)
+ {
+ var number = Convert.ToString(min, @base);
+ Assert.Equal($"{min}: {Convert.ToInt32(number, @base)}", $"{min}: {number.FromDigits(@base)}");
+ min++;
+ }
+ }
+
+ [Theory]
+ [InlineData(2146483647, 2147483647)]
+ [InlineData(0, 10000)]
+ [InlineData(-10000, 0)]
+ [InlineData(-2147483648, -2146483648)]
+ internal void ToBinaryTest(int min, int max)
+ {
+ while (min < max)
+ {
+ Assert.Equal($"{min}: {Convert.ToString(min, 2).ToUpperInvariant()}", $"{min}: {min.ToDigits(2)}");
+ min++;
+ }
+ }
+
+ [Theory]
+ [InlineData(2146483647, 2147483647)]
+ [InlineData(0, 10000)]
+ [InlineData(-10000, 0)]
+ [InlineData(-2147483648, -2146483648)]
+ internal void ToOctalTest(int min, int max)
+ {
+ while (min < max)
+ {
+ Assert.Equal($"{min}: {Convert.ToString(min, 8).ToUpperInvariant()}", $"{min}: {min.ToDigits(8)}");
+ min++;
+ }
+ }
+
+ [Theory]
+ [InlineData(2146483647, 2147483647)]
+ [InlineData(0, 10000)]
+ [InlineData(-10000, 0)]
+ [InlineData(-2147483648, -2146483648)]
+ internal void ToHexadecimalTest(int min, int max)
+ {
+ while (min < max)
+ {
+ Assert.Equal($"{min}: {Convert.ToString(min, 16).ToUpperInvariant()}", $"{min}: {min.ToDigits(16)}");
+ min++;
+ }
+ }
+
+ [Theory]
+ [InlineData(2147482647, 2147483647)]
+ [InlineData(0, 1000)]
+ // [InlineData(-10000, 0)]
+ // [InlineData(-2147483648, -2146483648)]
+ internal void CrossBaseTest(int min, int max)
+ {
+ for (var @base = 2; @base <= 36; @base++)
+ {
+ if (int.IsPow2(@base))
+ continue;
+
+ for (var number = min; number < max; number++)
+ {
+ var convertedNumber = number.ToDigits(@base);
+ Assert.Equal($"{number}({@base}): {convertedNumber}", $"{number}({@base}): {convertedNumber.FromDigits(@base).ToDigits(@base)}");
+ }
+ }
+ }
+}
\ No newline at end of file