5
\$\begingroup\$

Part 1 here.

I improved my code from my previous question (linked above).

using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// Used to store data as bits. Acts as a queue - first data added is the first data removed.
/// Data should be read in the same order it is written. If read in a different order, it gives undefined results.
/// Reading from an empty BitStream returns 0.
/// </summary>
public class BitStream
{
    ulong scratchWrite = 0;
    int scratchWriteBits = 0;
    ulong scratchRead = 0;
    int scratchReadBits = 0;
    Queue<ulong> buffer = new Queue<ulong>();

    /// <summary>
    /// How many bits are currently in the BitStream
    /// </summary>
    public long StoredBits
    {
        get;
        private set;
    }

    /// <summary>
    /// Make a new BitStream
    /// </summary>
    public BitStream() { }

    /// <summary>
    /// Make a new BitStream
    /// </summary>
    /// <param name="bitCount">How many bits you expect this stream will hold. A closer value nets increased performance.</param>
    public BitStream(long bitCount)
    {
        buffer = new Queue<ulong>((int) IntDivideRoundUp(bitCount, 64));
    }

    /// <summary>
    /// Make a new BitStream containing bits from the byte array
    /// NOTE: StoredBits may return a higher count than there are actual bits to read if the byte array came from another BitStream.
    /// </summary>
    /// <param name="bits">contains bits to be stored in the bitstream</param>
    public BitStream(byte[] bits)
    {
        foreach (var bite in bits)
        {
            Write(bite, byte.MinValue, byte.MaxValue);
        }
    }

    /// <summary>
    /// Get the bits stored in a ulong array (left-endian)
    /// </summary>
    /// <returns>ulong array of bits</returns>
    public ulong[] GetUlongArray()
    {
        ResetBuffer();

        if (scratchWriteBits > 0)
        {
            ulong[] result = new ulong[buffer.Count + 1];
            Array.Copy(buffer.ToArray(), result, buffer.Count);
            result[buffer.Count] = scratchWrite;
            return result;
        }
        return buffer.ToArray();
    }

    /// <summary>
    /// Get the bits stored in a byte array (left-endian)
    /// </summary>
    /// <returns>byte array of bits</returns>
    public byte[] GetByteArray()
    {
        ResetBuffer();

        int extraBytes = (int) IntDivideRoundUp(scratchWriteBits, 8);
        byte[] result = new byte[buffer.Count * 8 + extraBytes];
        Buffer.BlockCopy(buffer.ToArray(), 0, result, 0, result.Length - extraBytes);

        int index = buffer.Count * 8;
        int bits = scratchWriteBits;
        ulong scratch = scratchWrite;
        while (bits > 0)
        {
            int bitsToStore = bits >= 8 ? 8 : bits;
            result[index] = (byte) (scratch >> (64 - bitsToStore));
            scratch <<= bitsToStore;
            bits -= bitsToStore;
            index++;
        }
        return result;
    }

    /// <summary>
    /// Get the bits stored in a BitArray
    /// </summary>
    /// <returns>all bits in the stream in a BitArray</returns>
    public BitArray GetBitArray()
    {
        ResetBuffer();

        BitArray ba = new BitArray(buffer.Count * 64 + scratchWriteBits);
        var tempBuf = buffer.ToArray();
        int counter = 0;
        for (int i = 0; i < ba.Count; i++)
        {
            for (int j = 0; j < 64; j++)
            {
                ba[counter] = (tempBuf[i] & ((ulong) 1 << (63 - j))) > 0;
                counter++;
            }
        }

        for (int i = 0; i < scratchWriteBits; i++)
        {
            ba[counter] = (scratchWrite & ((ulong) 1 << (63 - i))) > 0;
            counter++;
        }

        return ba;
    }

    #region Write

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="bits">how many bits. Range(0, 64]</param>
    protected void Write(ulong data, int bits)
    {
        if (bits <= 0 || bits > 64)
        {
            return;
        }
        scratchWrite |= ((data << (64 - bits)) >> scratchWriteBits);
        scratchWriteBits += bits;

        if (scratchWriteBits >= 64)
        {
            var temp = scratchWrite;
            buffer.Enqueue(scratchWrite);
            scratchWrite = 0;
            scratchWriteBits -= 64;
            if (scratchWriteBits > 0)
            {
                scratchWrite |= (data << (64 - scratchWriteBits));
            }
        }
        StoredBits += bits;
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(long data, long min, long max)
    {
        if (min > max)
        {
            min ^= max;
            max ^= min;
            min ^= max;
        }
        if (data < min || data > max)
        {
            throw new ArgumentOutOfRangeException("data", data, "must be between min and max");
        }

        ulong bdata = 0;
        ulong bmin = 0;
        ulong bmax = 0;

        if (max < 0)
        {
            max -= min;
            bmax = (ulong) max;
        }
        else
        {
            bmax = (ulong) max;
            if (min == long.MinValue)
            {
                bmax += (ulong) long.MaxValue + 1;
            }
            else if (min < 0)
            {
                bmin = (ulong) -min;
                bmax += bmin;
            }
            else
            {
                bmin = (ulong) min;
                bmax -= bmin;
            }
        }
        data -= min;
        bdata = (ulong) data;

        Write(bdata, BitsRequired(bmax));
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(ulong data, ulong min, ulong max)
    {
        if (min > max)
        {
            min ^= max;
            max ^= min;
            min ^= max;
        }
        if (data < min || data > max)
        {
            throw new ArgumentOutOfRangeException("data", data, "must be between min and max");
        }

        if (min > 0)
        {
            max -= min;
        }

        Write(data, BitsRequired(max));
    }

    /// <summary>
    /// Write 1 bit to the stream
    /// </summary>
    /// <param name="data">bit to be written</param>
    public void Write(bool data)
    {
        Write(Convert.ToByte(data), 1);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(byte data, byte min, byte max)
    {
        Write((ulong) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(sbyte data, sbyte min, sbyte max)
    {
        Write((long) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(char data, char min, char max)
    {
        Write((ulong) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(short data, short min, short max)
    {
        Write((long) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(ushort data, ushort min, ushort max)
    {
        Write((ulong) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(int data, int min, int max)
    {
        Write((long) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    public void Write(uint data, uint min, uint max)
    {
        Write((ulong) data, min, max);
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    /// <param name="precision">how many digits after the decimal</param>
    public void Write(float data, float min, float max, byte precision)
    {
        if (min > max)
        {
            var temp = min;
            min = max;
            max = temp;
        }
        if (data < min || data > max)
        {
            throw new ArgumentOutOfRangeException("data", data, "must be between min and max");
        }

        float newMax = max - min;
        int mult = IntPow(10, precision);

        if (float.IsInfinity(newMax))
        {
            if (precision > 0)
            {
                Write((ulong) BitConverter.DoubleToInt64Bits(data), 32);
                return;
            }

            double infoMax = Math.Round(max, MidpointRounding.AwayFromZero);
            double infoMin = Math.Round(min, MidpointRounding.AwayFromZero);
            if (infoMax > uint.MaxValue || -infoMin > uint.MaxValue)
            {
                Write((ulong) BitConverter.DoubleToInt64Bits(data), 32);
                return;
            }

            long info = (long) Math.Round(data * mult, MidpointRounding.AwayFromZero);
            Write(info, (long) infoMin, (long) infoMax);
        }
        else
        {
            double infoMax = Math.Round(newMax * mult, MidpointRounding.AwayFromZero);
            if (infoMax > uint.MaxValue)
            {
                Write((ulong) BitConverter.DoubleToInt64Bits(data), 32);
                return;
            }

            data -= min;
            ulong info = (ulong) Math.Round(data * mult, MidpointRounding.AwayFromZero);
            Write(info, BitsRequired((ulong) infoMax));
        }
    }

    /// <summary>
    /// Write bits to the stream
    /// </summary>
    /// <param name="data">bits to be written</param>
    /// <param name="min">the minimum number that can be written</param>
    /// <param name="max">the maximum number that can be written</param>
    /// <param name="precision">how many digits after the decimal</param>
    public void Write(double data, double min, double max, byte precision)
    {
        if (min > max)
        {
            var temp = min;
            min = max;
            max = temp;
        }
        if (data < min || data > max)
        {
            throw new ArgumentOutOfRangeException("data", data, "must be between min and max");
        }

        double newMax = max - min;
        int mult = IntPow(10, precision);

        if (double.IsInfinity(newMax))
        {
            if (precision > 0)
            {
                Write((ulong) BitConverter.DoubleToInt64Bits(data), 64);
                return;
            }

            double infoMax = Math.Round(max, MidpointRounding.AwayFromZero);
            double infoMin = Math.Round(min, MidpointRounding.AwayFromZero);
            if (infoMax > ulong.MaxValue || -infoMin > ulong.MaxValue)
            {
                Write((ulong) BitConverter.DoubleToInt64Bits(data), 64);
                return;
            }

            long info = (long) Math.Round(data * mult, MidpointRounding.AwayFromZero);
            Write(info, (long) infoMin, (long) infoMax);
        }
        else
        {
            double infoMax = Math.Round(newMax * mult, MidpointRounding.AwayFromZero);
            if (infoMax > ulong.MaxValue)
            {
                Write((ulong) BitConverter.DoubleToInt64Bits(data), 64);
                return;
            }

            data -= min;
            ulong info = (ulong) Math.Round(data * mult, MidpointRounding.AwayFromZero);
            Write(info, BitsRequired((ulong) infoMax));
        }
    }
    #endregion

    #region Read

    /// <summary>
    /// Read bits from the stream. Returns 0 if bits > StoredBits.
    /// </summary>
    /// <param name="bits">How many bits to read. Range(0, 64]</param>
    /// <returns>bits read in ulong form</returns>
    protected ulong Read(int bits)
    {
        if (bits <= 0 || bits > 64)
        {
            return 0;
        }
        StoredBits -= bits;

        ulong data = scratchRead >> (64 - bits);

        if (scratchReadBits < bits)
        {
            ulong tempScratch;
            int difference = bits - scratchReadBits;
            if (buffer.Count == 0)
            {
                scratchRead |= (scratchWrite >> scratchReadBits);
                data = scratchRead >> (64 - bits);
                scratchWrite <<= difference;
                scratchWriteBits -= difference;
                scratchRead = 0;
                scratchReadBits = 0;
            }
            else
            {
                tempScratch = buffer.Dequeue();
                scratchRead |= (tempScratch >> scratchReadBits);
                data = scratchRead >> (64 - bits);
                tempScratch <<= difference;
                scratchRead = tempScratch;
                scratchReadBits = 64 - difference;
            }
        }
        else
        {
            scratchRead <<= bits;
            scratchReadBits -= bits;
        }

        if (StoredBits < 0) // handle the case of asking for more bits than exist in the stream
        {
            data = 0;
            StoredBits = 0;
            scratchWrite = 0;
            scratchWriteBits = 0;
            scratchRead = 0;
            scratchReadBits = 0;
        }

        return data;
    }

    /// <summary>
    /// Read a bit from the stream and write it to data
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    public void Read(out bool data)
    {
        data = Read(1) > 0;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out ulong data, ulong min, ulong max)
    {
        if (min > max)
        {
            min ^= max;
            max ^= min;
            min ^= max;
        }

        if (min > 0)
        {
            max -= min;
        }

        data = Read(BitsRequired(max)) - min;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out uint data, uint min, uint max)
    {
        ulong tempData;
        Read(out tempData, min, max);
        data = (uint) tempData;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out ushort data, ushort min, ushort max)
    {
        ulong tempData;
        Read(out tempData, min, max);
        data = (ushort) tempData;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out byte data, byte min, byte max)
    {
        ulong tempData;
        Read(out tempData, min, max);
        data = (byte) tempData;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out char data, char min, char max)
    {
        ulong tempData;
        Read(out tempData, min, max);
        data = (char) tempData;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out long data, long min, long max)
    {
        if (min > max)
        {
            min ^= max;
            max ^= min;
            min ^= max;
        }

        ulong bmin = 0;
        ulong bmax = 0;

        if (max < 0)
        {
            max -= min;
            bmax = (ulong) max;
        }
        else
        {
            bmax = (ulong) max;
            if (min == long.MinValue)
            {
                bmax += (ulong) long.MaxValue + 1;
            }
            else if (min < 0)
            {
                bmin = (ulong) -min;
                bmax += bmin;
            }
            else
            {
                bmin = (ulong) min;
                bmax -= bmin;
            }
        }

        if (BitsRequired(bmax) > StoredBits)
        {
            data = 0;
        }
        else
        {
            data = (long) (Read(BitsRequired(bmax)) - bmin);
        }
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out int data, int min, int max)
    {
        long tempData;
        Read(out tempData, min, max);
        data = (int) tempData;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out short data, short min, short max)
    {
        long tempData;
        Read(out tempData, min, max);
        data = (short) tempData;
    }

    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    public void Read(out sbyte data, sbyte min, sbyte max)
    {
        long tempData;
        Read(out tempData, min, max);
        data = (sbyte) tempData;
    }


    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    /// <param name="precision">how many digits after the decimal</param>
    public void Read(out double data, double min, double max, byte precision)
    {
        if (min > max)
        {
            var temp = min;
            min = max;
            max = temp;
        }

        double newMax = max - min;
        int mult = IntPow(10, precision);

        if (double.IsInfinity(newMax))
        {
            if (precision > 0)
            {
                data = BitConverter.Int64BitsToDouble((long) Read(64));
                return;
            }

            double infoMax = Math.Round(max, MidpointRounding.AwayFromZero);
            double infoMin = Math.Round(min, MidpointRounding.AwayFromZero);
            if (infoMax > ulong.MaxValue || -infoMin > ulong.MaxValue)
            {
                data = BitConverter.Int64BitsToDouble((long) Read(64));
                return;
            }

            long tempData;
            Read(out tempData, (long) infoMin, (long) infoMax);
            data = tempData / (double) mult;
        }
        else
        {
            double infoMax = Math.Round(newMax * mult, MidpointRounding.AwayFromZero);
            if (infoMax > ulong.MaxValue)
            {
                data = BitConverter.Int64BitsToDouble((long) Read(64));
                return;
            }

            long tempData = (long) Read(BitsRequired((ulong) infoMax));
            data = tempData / (double) mult;
        }
    }


    /// <summary>
    /// Read bits from the stream and write that information to data.
    /// WARNING: If you read data in a different order than written, there is a possibility that the actual number written to data is outside of the given range. In such a case, you may want to check the bounds yourself.
    /// </summary>
    /// <param name="data">the variable to be written to</param>
    /// <param name="min">the smallest possible number that could have been written</param>
    /// <param name="max">the largest possible number that could have been written</param>
    /// <param name="precision">how many digits after the decimal</param>
    public void Read(out float data, float min, float max, byte precision)
    {
        if (min > max)
        {
            var temp = min;
            min = max;
            max = temp;
        }

        float newMax = max - min;
        int mult = IntPow(10, precision);

        if (float.IsInfinity(newMax))
        {
            if (precision > 0)
            {
                data = (float) BitConverter.Int64BitsToDouble((long) Read(64));
                return;
            }

            float infoMax = (float) Math.Round(max, MidpointRounding.AwayFromZero);
            float infoMin = (float) Math.Round(min, MidpointRounding.AwayFromZero);
            if (infoMax > uint.MaxValue || -infoMin > uint.MaxValue)
            {
                data = (float) BitConverter.Int64BitsToDouble((long) Read(64));
                return;
            }

            int tempData;
            Read(out tempData, (int) infoMin, (int) infoMax);
            data = tempData / (float) mult;
        }
        else
        {
            float infoMax = (float) Math.Round(newMax * mult, MidpointRounding.AwayFromZero);
            if (infoMax > uint.MaxValue)
            {
                data = (float) BitConverter.Int64BitsToDouble((long) Read(64));
                return;
            }

            int tempData = (int) Read(BitsRequired((ulong) infoMax));
            data = tempData / (float) mult;
        }
    }
    #endregion

    /// <summary>
    /// Returns how many bits it takes to store a number
    /// </summary>
    /// <param name="max">the maximum number that will be written</param>
    /// <returns>how many bits are needed</returns>
    protected int BitsRequired(ulong max)
    {
        if (max == 0)
        {
            return 1;
        }
        for (int i = 1; i < 64; i++)
        {
            if (max < ((ulong) 1 << i))
            {
                return i;
            }
        }
        return 64;
    }

    /// <summary>
    /// If scratch_read contains any bits, moves them to the head of the buffer.
    /// NOTE: Not a short operation, use only when necessary!
    /// </summary>
    protected void ResetBuffer()
    {
        if (scratchReadBits > 0 && scratchRead != scratchWrite)
        {
            StoredBits = 0;
            var oldBuf = buffer.ToArray();
            buffer.Clear();
            var tempScratch = scratchWrite >> (64 - scratchWriteBits);
            int tempBits = scratchWriteBits;
            scratchWrite = 0;
            scratchWriteBits = 0;
            Write(scratchRead >> (64 - scratchReadBits), scratchReadBits);
            scratchRead = 0;
            scratchReadBits = 0;
            for (int i = 0; i < oldBuf.Length - 1; i++)
            {
                Write(oldBuf[i], 64);
            }
            Write(tempScratch, tempBits);
        }
    }

    long IntDivideRoundUp(long upper, long lower)
    {
        return (upper + lower - 1) / lower;
    }

    int IntPow(int x, uint pow)
    {
        int ret = 1;
        while (pow != 0)
        {
            if ((pow & 1) == 1)
            {
                ret *= x;
            }
            x *= x;
            pow >>= 1;
        }
        return ret;
    }
}

Example use:

BitStream bs = new BitStream();
int min1 = -20, max1 = 1000, num1 = 287;
float min2 = 0f, max2 = 50f, num2 = 16.78634f;
double min3 = double.MinValue, max3 = double.MaxValue, num3 = 9845216.1916526;
byte fltPrec = 2, dblPrec = 0;

int num4;
float num5;
double num6;
bool checker;

bs.Write(num1, min1, max1); // 10 bits (difference between max/min is 1020)
bs.Write(num2, min2, max2, fltPrec); // converts to 1679 int, 13 bits (difference between max/min converted to int is 5000)
bs.Read(out num4, min1, max1); // num4 = 287
bs.Write(num3, min3, max3, dblPrec); // precision is ignored here as min/max are too high to try to convert to an integer, so the value is stored using all 64 bits of the double
bs.Write(true); // 1 bit
bs.Read(out num5, min2, max2, fltPrec); // num5 = 16.79, there is some loss of precision here
bs.Read(out num6, min3, max3, dblPrec); // num6 = 9845216.1916526, no loss of precision
bs.Read(out checker); // checker = true
int newNum;
bs.Read(out newNum, -100, 100); // newNum = 0 as there are no bits left in the BitStream

And I also made a simple serializer class utilizing the BitStream class:

using System;

/// <summary>
/// Used to serialize and deserialize data using the BitStream class.
/// </summary>
public class BitSerializer
{
    BitStream bs = new BitStream();
    bool isReading;

    public bool IsReading
    {
        get { return isReading; }
        private set { isReading = value; }
    }
    public bool IsWriting
    {
        get { return !isReading; }
        private set { isReading = !value; }
    }

    /// <summary>
    /// Create a new BitSerializer
    /// </summary>
    /// <param name="read">Writing or reading. Set to true to read, false to write.</param>
    public BitSerializer(bool read = false)
    {
        isReading = read;
    }

    /// <summary>
    /// Create a new BitSerializer
    /// </summary>
    /// <param name="bits">How many bits you expect this serializer will hold. A closer value nets increased performance.</param>
    /// <param name="read">Writing or reading. Set to true to read, false to write.</param>
    public BitSerializer(long bits, bool read = false)
        : this(read)
    {
        bs = new BitStream(bits);
    }

    /// <summary>
    /// Create a new BitSerializer
    /// </summary>
    /// <param name="bitStream">The BitStream to read from or write to</param>
    /// <param name="read">Writing or reading. Set to true to read, false to write.</param>
    public BitSerializer(BitStream bitStream, bool read = true)
        : this(read)
    {
        bs = bitStream;
    }

    /// <summary>
    /// Create a new BitSerializer
    /// </summary>
    /// <param name="data">The data to be read from</param>
    /// <param name="read">Writing or reading. Set to true to read, false to write.</param>
    public BitSerializer(byte[] data, bool read = true)
        : this(read)
    {
        bs = new BitStream(data);
    }

    /// <summary>
    /// Get the bits stored in a byte array (left-endian)
    /// </summary>
    /// <returns>byte array of bits</returns>
    public byte[] GetByteArray()
    {
        return bs.GetByteArray();
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref ulong data, ulong min, ulong max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref uint data, uint min, uint max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref ushort data, ushort min, ushort max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref byte data, byte min, byte max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref char data, char min, char max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref long data, long min, long max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref int data, int min, int max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref short data, short min, short max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    public void Serialize(ref sbyte data, sbyte min, sbyte max)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    public void Serialize(ref bool data)
    {
        if (IsReading)
        {
            bs.Read(out data);
        }
        else
        {
            bs.Write(data);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    /// <param name="precision">How many digits after the decimal</param>
    public void Serialize(ref float data, float min, float max, byte precision)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max, precision);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max, precision);
        }
    }

    /// <summary>
    /// Serialize or Deserialize the data
    /// </summary>
    /// <param name="data">Data to be serialized or deserialized</param>
    /// <param name="min">The smallest possible number to be serialized</param>
    /// <param name="max">The largest possible number to be serialized</param>
    /// <param name="precision">How many digits after the decimal</param>
    public void Serialize(ref double data, double min, double max, byte precision)
    {
        if (IsReading)
        {
            bs.Read(out data, min, max, precision);
            if (data < min || data > max)
            {
                throw new Exception("An error occurred: data is outside of bounds!");
            }
        }
        else
        {
            bs.Write(data, min, max, precision);
        }
    }
}

Any comments welcome.

\$\endgroup\$
4
\$\begingroup\$

Little bit late answer but...

First of all I have some perplexities about Write() methods. Most of them are in the form Write(T value, T min, T max) where the only purpose of min and max is to validate value. In my opinion this validation does not belong in any way to BitStream class. Yes, you're using those parameters also to minimize the number of required bits but IMO you're adding too many responsibilities to this class, think about Stream design: you decorate each layer with its own responsibility.

In this case I'd imagine a plain BitStream class (even if name is little bit misleading unless you want to derive from Stream) which has only one responsibility: keep a bit queue with only one write method (eventually with an overload to omit numberOfBits parameter):

Write(long bits, int numberOfBits);

Everything else should be moved to another decorator class which will handle all the required conversions/minimizations. I imagine this usage pattern:

Random rnd = new Random();
using (var bits = new BitStreamFormatter(new BitStream)) {
    bits.Write(rnd.Next(0, 100), 0, 100);
}

Also I'd make this class sealed (I do not see any extension point). I do not see the point of your Serializer class, it adds nothing to existing BitStream methods. If you want to properly support binary serialization you may add another class which implements ISerializationSurrogate.

Methods to convert to array are (but this is pretty subjective) misleading in their name. With Get*() I expect a getter methods which will return a view of object internal state (in this case) or some property (too expansive or complex to be a simple property). In this case however, you create a new detached object with a complex logic. I'd use another name pattern for this: ToByteArray(), ToUInt64Array() and similar. Note that I used UInt64 instead of ToULongArray(): ulong is C# specific, an alias for .NET System.UInt64 type; other languages call it with a different name then you'd better avoid C#ish names in your library (you may not mix C# and VB.NET but it's not unreasonable to have a C++/CLI library where it's named __int64 or long long). Also: these functions actually mutate object state, it's pretty astonishing for a get function! I'd rework that code to do not mutate internal state (or to make it clear at calling point, something like TrimAndGetInt32Array()).

Here and there I see magic numbers (like 8, 63, 64, 32...). It's obvious what they represent but it's also true that any change you will make will be harder (and reader will need to think, each time, what that magic number is and why it's there). Help them and use const int fields for them:

const int BitsPerByte = 8;
static readonly int Int32SizeInBits = sizeof(int) * BitsPerByte;
static readonly int Int64SizeInBits = sizeof(long) * BitsPerByte;

IntDivideRoundUp(), BitsRequired() and IntPow() should be marked static. Pretty small performance optimization but if you're using IntPow (BTW I do not see any overflow checking) then you may want it (even if usually I do it for clarity instead of performance, when I see that half of my class is made of static methods then I immediately know there is something to do there).

Read() methods with an out parameters are little bit weird to me. Why? You do not need return value for error codes, it's much easier, less error prone and less verbose to return such value. It will also simplify your Read() function (just return, no else branches).

Some functions are protected, unless you really design your class to be a base class then they should not be. To add more Read() and Write() overloads we already have above mentioned decorator. In general think twice before deploying a class as inheritable, it will make its internal design part of its contract and it's absolutely more expensive in terms of maintenance and compatibility.

Sometimes you just ignore wrong inputs (for example out-of-range bits in Read()) returning a dummy value. Do not do it, wrong inputs should cause an exception otherwise your program will misbehave and you will have hard-time to find it in production. If you have an out-of-range parameter then always throw ArgumentOutOfRange exception, you will thank yourself in future.

You do not prefix private class members with _ (for example). It may be questionable or not (there are good reasons to both do it and don't do it) but if you don't then prefix them with this. every single time you use them. It will help to understand which variable you're referencing. Also buffer should be readonly.

\$\endgroup\$
0
\$\begingroup\$

BUG:

Read(ulong) function doesn't set data to 0 if bitsrequired > storedbits while Read(long) function does.

Edit: Also, Read(float) function reads 64 bits when it should be reading 32 bits.

\$\endgroup\$
  • \$\begingroup\$ You know, as long as there are no answers to your question its perfectly legal to edit your question to remove the bugs. \$\endgroup\$ – Heslacher Jul 14 '16 at 6:35
  • \$\begingroup\$ It's best to make amends to the question than putting the bug in the answer section. \$\endgroup\$ – Siobhan Jul 14 '16 at 8:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.