Skip to content

Commit

Permalink
Implemented ToBase and FromBase extension methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaoticz committed Oct 22, 2024
1 parent e73d7db commit 14969ec
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 1 deletion.
1 change: 0 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Avatar.png</PackageIcon>
<IsPublishable>false</IsPublishable>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
Expand Down
225 changes: 225 additions & 0 deletions Kotz.Extensions/INumberExt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;

namespace Kotz.Extensions;

/// <summary>
/// Provides methods for converting numbers between base 10 and other numeric bases.
/// </summary>
public static class NumberBaseExt
{
/// <summary>
/// Converts the string representation of a number in the specified <paramref name="base"/> to an integer of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The integer type to convert the string to.</typeparam>
/// <param name="number">The string representing the number.</param>
/// <param name="base">The base of the <paramref name="number"/> representation in the string.</param>
/// <remarks>Negative numbers from certain bases cannot be converted and will throw an <see cref="InvalidOperationException"/>.</remarks>
/// <returns>The <paramref name="number"/> converted to an integer of type <typeparamref name="T"/>.</returns>
/// <exception cref="ArgumentException">
/// Occurs when <paramref name="number"/> is not representable in <paramref name="base"/>.
/// Occurs when <paramref name="number"/> is not representable in <typeparamref name="T"/>.
/// Occurs when <paramref name="number"/> contains characters that are not valid ASCII letters or digits.
/// </exception>
/// <exception cref="InvalidOperationException">Occurs when a negative number could not be converted to decimal.</exception>
/// <exception cref="OverflowException">Occurs when the equivalent of <typeparamref name="T"/>.MaxValue or <typeparamref name="T"/>.MinValue are provided.</exception>
/// <exception cref="UnreachableException">Occurs when <paramref name="number"/> contains invalid characters for the given base.</exception>
public static T FromDigits<T>(this string number, int @base) where T : struct, IBinaryInteger<T>
{
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<T>(), @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<T>(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;
}

/// <summary>
/// Converts the specified <paramref name="number"/> to its representation in the given <paramref name="base"/>.
/// </summary>
/// <typeparam name="T">The numeric type of the <paramref name="number"/>.</typeparam>
/// <param name="number">The number to convert.</param>
/// <param name="base">The base for the conversion.</param>
/// <returns>A string representing the <paramref name="number"/> in the specified <paramref name="base"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Occurs when <paramref name="base"/> is less than 2 or greater than 36.</exception>
/// <exception cref="InvalidOperationException">Occurs when <paramref name="base"/> is 10 and <typeparamref name="T"/> returns <see langword="null"/> from <see cref="object.ToString()"/>>.</exception>
/// <exception cref="OverflowException">Occurs when <typeparamref name="T"/>.MinValue is provided.</exception>
public static string ToDigits<T>(this T number, int @base) where T : struct, IBinaryInteger<T>
{
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<char> buffer = stackalloc char[FractionalDigits(Unsafe.SizeOf<T>(), @base)];

var result = (int.IsPow2(@base))
? ToArbitraryBaseOptimized(number, @base, buffer)
: ToArbitraryBase(number, @base, buffer);

return (result.Length > 1)
? result.TrimStart('0').ToString()
: result.ToString();
}

/// <summary>
/// Converts a <paramref name="number"/> to the specified base, which must be a power of two.
/// If <paramref name="number"/> is negative, it will be represented using Two's Complement.
/// </summary>
/// <typeparam name="T">The numeric type of the <paramref name="number"/>.</typeparam>
/// <param name="number">The number to convert.</param>
/// <param name="base">The base (power of two) for the conversion.</param>
/// <param name="buffer">The buffer that will store the digits of the converted number.</param>
/// <returns>A span containing the digits of <paramref name="number"/> in the specified <paramref name="base"/>.</returns>
private static Span<char> ToArbitraryBaseOptimized<T>(T number, int @base, Span<char> buffer) where T : IBinaryInteger<T>
{
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..];
}

/// <summary>
/// Converts a <paramref name="number"/> to the specified <paramref name="base"/>.
/// </summary>
/// <typeparam name="T">The numeric type of the <paramref name="number"/>.</typeparam>
/// <param name="number">The number to convert.</param>
/// <param name="base">The base for the conversion.</param>
/// <param name="buffer">The buffer that will store the digits of the converted number.</param>
/// <returns>A span containing the digits of <paramref name="number"/> in the specified <paramref name="base"/>.</returns>
/// <exception cref="OverflowException">Occurs when <typeparamref name="T"/>.MinValue is provided.</exception>
private static Span<char> ToArbitraryBase<T>(T number, int @base, Span<char> buffer) where T : IBinaryInteger<T>
{
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..];
}

/// <summary>
/// Converts a negative <paramref name="number"/> to the specified base using Two's Complement representation.
/// </summary>
/// <typeparam name="T">The numeric type of the <paramref name="number"/>.</typeparam>
/// <param name="number">The negative number to convert.</param>
/// <param name="base">The base (power of two) for the conversion.</param>
/// <param name="buffer">The buffer that will store the digits of the converted number.</param>
/// <returns>
/// A span containing the digits of <paramref name="number"/> in the specified <paramref name="base"/>,
/// represented using Two's Complement.
/// </returns>
/// <exception cref="OverflowException">Occurs when <typeparamref name="T"/>.MinValue is provided.</exception>
private static Span<char> ToTwosComplement<T>(T number, int @base, Span<char> buffer) where T : IBinaryInteger<T>
{
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;
}

/// <summary>
/// Converts a positive <paramref name="number"/> from the specified <see langword="base"/> to a <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The numeric type of the <paramref name="number"/>.</typeparam>
/// <param name="number">The negative number to convert.</param>
/// <param name="base">The base for the conversion.</param>
/// <returns>The decimal representation of the <paramref name="number"/>.</returns>
/// <exception cref="UnreachableException">Occurs when <paramref name="number"/> contains invalid digits.</exception>
private static T FromPositiveDigits<T>(string number, int @base) where T : IBinaryInteger<T>
{
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;
}

/// <summary>
/// Calculates the maximum amount of fractional digits a number can have for
/// a given base and type size.
/// </summary>
/// <param name="typeSize">The size of the type, in bytes.</param>
/// <param name="targetBase">The base to calculate the digits for.</param>
/// <returns>The amount of fractional digits in the number.</returns>
private static int FractionalDigits(int typeSize, int targetBase)
=> (int)Math.Ceiling((typeSize * 8 * Math.Log(2)) / Math.Log(targetBase));
}
3 changes: 3 additions & 0 deletions Kotz.Extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Defines the following extension methods:
- **List\<T> Extensions**
- AsSpan: Gets the current list as a `Span<T>` object.
- AsReadOnlySpan: Gets the current list as a `ReadOnlySpan<T>` 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\<T> Extensions**
Expand Down
91 changes: 91 additions & 0 deletions Kotz.Tests/Extensions/NumberBaseExtTests.cs
Original file line number Diff line number Diff line change
@@ -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<int>(@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<int>(@base).ToDigits(@base)}");
}
}
}
}

0 comments on commit 14969ec

Please sign in to comment.