Перевод дробных чисел в строку в другой системе счисления на C#
Есть два числа ulong - целая и дробная часть, в двоичном виде выглядит как $"{IntegerPart.ToString("B")}.{FractionalPart.ToString("B")}", т.е. дробная часть - это фактическая дробная часть, умноженная на 2^64. Задача - отобразить приближенное дробное число в некоторой системе счисления. Снизу код всего числа. А на картинках тесты.
global using ExFixP = StandartPhysics.Math.ExtraFixedPoint;
using System.Numerics;
namespace StandartPhysics.Math
{
public struct ExtraFixedPoint
{
private readonly ulong IntegerPart;
private readonly ulong FractionalPart;
public readonly byte[] GetBytes()
{
return [.. new BigInteger(FractionalPart).ToByteArray(true).Concat(new BigInteger(IntegerPart).ToByteArray(true))];
}
#region ExtraFixedPoint
public ExtraFixedPoint()
{
}
internal ExtraFixedPoint(byte @int, byte frac)
{
IntegerPart = @int;
FractionalPart = (ulong)(new BigInteger(frac) << 56);
}
internal ExtraFixedPoint(ushort @int, ushort frac)
{
IntegerPart = @int;
FractionalPart = (ulong)(new BigInteger(frac) << 48);
}
internal ExtraFixedPoint(uint @int, uint frac)
{
IntegerPart = @int;
FractionalPart = (ulong)(new BigInteger(frac) << 32);
}
internal ExtraFixedPoint(ulong @int, ulong frac)
{
IntegerPart = @int;
FractionalPart = frac;
}
internal ExtraFixedPoint(Span<byte> @int, Span<byte> frac)
{
IntegerPart = (ulong)new BigInteger(@int, true);
FractionalPart = (ulong)new BigInteger(frac, true);
}
public ExtraFixedPoint(string text, int @base = 10)
{
string[] array = text.Split(new char[] { ',', '.' }, 2, StringSplitOptions.TrimEntries);
if (array.Length == 0) throw new ArgumentException("Wrong text value");
foreach (char digit in array[0].Replace(".", "").Replace(",", ""))
{
IntegerPart = IntegerPart * (ulong)@base + GetValueFromChar(digit);
}
if (array.Length == 2)
{
BigInteger baseMultiplier = new BigInteger(1) << 64;
for (int i = 0; i < array[1].Length; i++)
{
int digit = Convert.ToInt32(array[1][i].ToString(), @base);
baseMultiplier /= (ulong)@base;
FractionalPart += (ulong)((digit * baseMultiplier) & ulong.MaxValue);
}
}
}
private static ulong GetValueFromChar(char val)
{
val = char.ToLower(val);
return val switch
{
'0' => 0,
'1' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
'a' => 10,
'b' => 11,
'c' => 12,
'd' => 13,
'e' => 14,
'f' => 15,
'g' => 16,
'h' => 17,
'i' => 18,
'j' => 19,
'k' => 20,
'l' => 21,
'm' => 22,
'n' => 23,
'o' => 24,
'p' => 25,
'q' => 26,
'r' => 27,
's' => 28,
't' => 29,
'u' => 30,
'v' => 31,
'w' => 32,
'x' => 33,
'y' => 34,
'z' => 35,
_ => 0
};
}
#endregion
#region operators
public static ExFixP operator +(ExFixP a, ExFixP b)
{
BigInteger bigInteger = new BigInteger(a.GetBytes(), true) + new BigInteger(b.GetBytes(), true);
ulong fractional = (ulong)(bigInteger & ulong.MaxValue);
bigInteger >>= 8;
ulong integer = (ulong)(bigInteger & ulong.MaxValue);
return new ExFixP(integer, fractional);
}
public static ExFixP operator -(ExFixP a, ExFixP b)
{
BigInteger bigInteger = new BigInteger(a.GetBytes(), true) - new BigInteger(b.GetBytes(), true);
ulong fractional = (ulong)(bigInteger & ulong.MaxValue);
bigInteger >>= 8;
ulong integer = (ulong)(bigInteger & ulong.MaxValue);
return new ExFixP(integer, fractional);
}
public static ExFixP operator *(ExFixP a, ExFixP b)
{
BigInteger bigInteger = new BigInteger(a.GetBytes(), true) * new BigInteger(b.GetBytes(), true);
bigInteger >>= 8;
ulong fractional = (ulong)(bigInteger & ulong.MaxValue);
bigInteger >>= 8;
ulong integer = (ulong)(bigInteger & ulong.MaxValue);
return new ExFixP(integer, fractional);
}
//public static exfixp operator /(exfixp a, exfixp b)
//public static exfixp operator %(exfixp a, exfixp b)
//public static exfixp operator -(exfixp a)
//public static exfixp operator +(exfixp a)
//public static exfixp operator >>(exfixp a, int b)
//public static exfixp operator <<(exfixp a, int b)
//public static exfixp operator ++(exfixp a)
//public static exfixp operator --(exfixp a)
#endregion
#region other
public readonly override string ToString()
{
return ToString();
}
public readonly string ToDebugString()
{
return $"{IntegerPart:B}.{FractionalPart.ToString("B").PadLeft(64, '0')}";
}
public readonly string ToString(int Base = 10, int accuracy = 30)
{
Base = int.Clamp(Base, 2, 36);
string integerPartBaseN = ConvertToBaseN(IntegerPart, Base);
string fractionalPartBaseN = ConvertFractionalPartToBaseN(FractionalPart, Base, accuracy);
return $"{integerPartBaseN}.{fractionalPartBaseN}";
}
private static string ConvertToBaseN(ulong number, int n)
{
if (number == 0) return "0";
string result = "";
while (number > 0)
{
ulong remainder = number % (ulong)n;
result = GetCharFromNumber(remainder) + result;
number /= (ulong)n;
}
return result;
}
private static string ConvertFractionalPartToBaseN(ulong fractionalPart, int n, int accuracy)
{
accuracy = int.Clamp(accuracy, 2, 40);
string result = "";
const ulong denominator = 1UL << 64; // 2^64
for (int i = 0; i < accuracy; i++) // ограничим вывод accuracy разрядами для дробной части
{
fractionalPart *= (ulong)n;
ulong digit = fractionalPart / denominator;
result += GetCharFromNumber(digit);
fractionalPart %= denominator;
}
return result.TrimEnd('0').PadRight(1, '0'); // удалим нули в конце
}
private static char GetCharFromNumber(ulong number)
{
if (number < 10)
return (char)('0' + number);
else
return (char)('A' + (number - 10));
}
#endregion
}
}
Стоит попробовать объяснить, зачем мне это нужно. Я недавно (давно) изучил принцип работы стандарта IEEE754 и пытаюсь воспроизвести принцип его работы, но для 128-битного числа. Но перед реализацией его, решил попробовать сначала реализовать число с фиксированной точкой, а потом уже переходить к плавающей точке. Base в конструкторе и в ToString указывает, в какой системе счисления вводится или выводится число, чтобы вывести в 6-тиричной системе счисления (моя любимая), в 10-тичной (стандартная) или в любой другой (хоть и приближённо).
Ответы (2 шт):
Так как вопрос алгоритмический, код писать не буду. Укажу только на ошибки.
Есть стандарт, описывающий числа с плавающей точкой IEEE-754. И он говорит нам, что счисло с точкой содержит в себе 2 компоненты: мантисса и степень. Мантисса это значимая часть числа, а степень, это то, по что нужно возвести основание системы счисления, чтобы перемножить на результат мантиссу и получить представление числа, в этой системе счисления.
Например, чтобы получить 1.25
в десятичной системе, нужна мантисса 125
и экспонента (степень) -2
. Экспонента грубо говоря сообщает о том, что точку нужно сдвинуть на 2 позиции влево. Точно так же это будет работать для любой другой системы счисления. Кстати, экспонента может быть и положительной, например запись числа 125000
это мантисса 125
и экспонента 3
.
То есть структура данных могла бы выглядеть так
ulong mantissa;
ulong exponent;
Так как экспонента зависит от основания системы счисления, в IEEE-754 принято что она десятичная. Если планируете экспоненту в другой системе счисления хранить, то придётся добавить и её.
ulong base;
Математически
number = mantissa * (base ^ exponent)
Вот отсюда можно и логику строить.
Что касается математических операций, то вы зря считаете, что склеив 3 бигинта у вас что-то получится.
Чтобы произвести математическую операцию с двумя числами, их нужно привести к общей экспоненте так, чтобы не произошло округления мантиссы. Например 1.25 + 3
это 125 + 300
в приведённых мантиссах, а 1.25 + 300
это соответственно 12500 + 30000
. То есть к обеим экспнентам нужно добавить или отнять столько, чтобы минимальная из них стала 0
. Затем результат нормализовать по экспоненте, чтобы получить оптимальную ммантиссу, без нулей в справа.
Подход с хранением целой и дробной части отдельно (число с фиксированной точкой) тоже допустим, но на мой взгляд он реализуется сложнее, так как придётся мучаться с точностью той самой дробной части, так как максимальное значение дробной части должно быть равно цифре равной основанию системы счисления минус один и повторённое столько раз, сколько позволяет заявленная точность. То есть для ulong
в десятичной системе это 9999999999999999999
то есть 19 девяток. Для другой системы счисления будет уже другое число, и так далее. То есть даже в варианте хранения числа с фиксированной точкой участвет система счисления, а алгоритмы обработки такого формата значительно сложнее. Поэтому в процессорах применяется более простая система - с плавающей точкой и экспонентой.
Я смог решить эту проблему. Проблема была при переводе числа из хранимого в текст, а именно в неком denominator. Он был представлен в виде ulong, и записывалось в него 1 << 64, что превращало 1 снова в 1. Я изменил перенос и теперь всё работает. Теперь оно выглядит вот так
private static string ConvertFractionalPartToBaseN(BigInteger fractionalPart, int n, int accuracy)
{
string result = "";
ulong denominator = 1UL << 63; // 2^63
for (int i = 0; i < accuracy; i++) // ограничим вывод accuracy разрядами для дробной части
{
fractionalPart *= n;
ulong digit = fractionalPart / denominator;
result += GetCharFromNumber(digit);
fractionalPart %= denominator;
}
return result.TrimEnd('0').PadRight(1, '0'); // удалим нули в конце
}
```