Перевод дробных чисел в строку в другой системе счисления на 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 шт):

Автор решения: aepot

Так как вопрос алгоритмический, код писать не буду. Укажу только на ошибки.

Есть стандарт, описывающий числа с плавающей точкой 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'); // удалим нули в конце
    }
    ```
→ Ссылка