From c5fa6fcc37db2e08070557f55fd56be710dc92fa Mon Sep 17 00:00:00 2001 From: chylex <contact@chylex.com> Date: Thu, 15 Nov 2018 11:49:31 +0100 Subject: [PATCH] Add BitStream w/ unit tests --- BrotliLib/IO/BitStream.cs | 214 ++++++++++++++++++++++++++++++++++ UnitTests/IO/TestBitStream.fs | 135 +++++++++++++++++++++ UnitTests/UnitTests.fsproj | 1 + 3 files changed, 350 insertions(+) create mode 100644 BrotliLib/IO/BitStream.cs create mode 100644 UnitTests/IO/TestBitStream.fs diff --git a/BrotliLib/IO/BitStream.cs b/BrotliLib/IO/BitStream.cs new file mode 100644 index 0000000..27a8f19 --- /dev/null +++ b/BrotliLib/IO/BitStream.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BrotliLib.IO{ + public class BitStream : IEnumerable<bool>{ + private const char False = '0'; + private const char True = '1'; + + private const int ByteSize = 8; + private const int BytesPerEntry = sizeof(ulong); + private const int BitEntrySize = ByteSize * BytesPerEntry; + + // Instance + + public int Length { get; private set; } + + private readonly LinkedList<ulong> bitCollection = new LinkedList<ulong>(); + private LinkedListNode<ulong> lastNode; + private int lastNodeIndex; + + #region Construction + + /// <summary> + /// Initializes an empty <see cref="BitStream"/>. + /// </summary> + public BitStream(){ + this.lastNode = this.bitCollection.AddLast(0L); + this.lastNodeIndex = 0; + } + + /// <summary> + /// Initializes a <see cref="BitStream"/> from a string consisting of 0s and 1s. + /// </summary> + /// <param name="bits">Input string. Must be either empty, or only contain the characters 0 and 1.</param> + /// <exception cref="ArgumentOutOfRangeException">Thrown when the input <paramref name="bits"/> string contains a character that is not 0 or 1.</exception> + public BitStream(string bits) : this(){ + foreach(char chr in bits){ + switch(chr){ + case False: this.Add(false); break; + case True: this.Add(true); break; + default: throw new ArgumentOutOfRangeException(nameof(bits), "Invalid character found in input string: "+chr); + } + } + } + + /// <summary> + /// Initializes a <see cref="BitStream"/> from a byte array. + /// </summary> + /// <param name="bytes">Input byte array segment.</param> + public BitStream(byte[] bytes) : this(){ + int index = 0; + + foreach(byte value in bytes){ + int offset = index % BytesPerEntry; + + if (offset == 0 && index > 0){ + this.lastNode = this.bitCollection.AddLast(0L); + this.lastNodeIndex += BitEntrySize; + } + + this.lastNode.Value |= (ulong)value << (ByteSize * offset); + ++index; + } + + this.Length = ByteSize * index; + } + + /// <summary> + /// Initializes a new <see cref="BitStream"/> as a clone of <paramref name="source"/>. + /// </summary> + /// <param name="source">Source stream.</param> + private BitStream(BitStream source){ + foreach(ulong bitEntry in source.bitCollection){ + this.bitCollection.AddLast(bitEntry); + } + + this.lastNode = this.bitCollection.Last; + this.lastNodeIndex = source.lastNodeIndex; + this.Length = source.Length; + } + + /// <summary> + /// Returns a copy of the object, which can be modified without affecting the original object. + /// </summary> + public BitStream Clone(){ + return new BitStream(this); + } + + #endregion + + #region Mutation + + /// <summary> + /// Appends a bit to the end of the stream. + /// </summary> + /// <param name="bit">Input bit.</param> + public void Add(bool bit){ + int offset = Length - lastNodeIndex; + + if (offset >= BitEntrySize){ + lastNode = bitCollection.AddLast(0L); + lastNodeIndex += BitEntrySize; + offset -= BitEntrySize; + } + + if (bit){ + lastNode.Value |= 1UL << offset; + } + else{ + lastNode.Value &= lastNode.Value & ~(1UL << offset); + } + + ++Length; + } + + #endregion + + #region Enumeration + + /// <summary> + /// Returns an enumerator that traverses the stream, converting 0s to false, and 1s to true. + /// </summary> + public IEnumerator<bool> GetEnumerator(){ + int bitsLeft = Length; + + foreach(ulong bitEntry in bitCollection){ + for(int bitIndex = 0; bitIndex < BitEntrySize; bitIndex++){ + if (--bitsLeft < 0){ + yield break; + } + + yield return (bitEntry & (1UL << bitIndex)) != 0; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + #endregion + + #region Conversion + + /// <summary> + /// Converts the stream into a byte array, with zero padding at the end if needed. + /// </summary> + public byte[] ToByteArray(){ + const int ByteMask = (1 << ByteSize) - 1; + + byte[] bytes = new byte[(Length + ByteSize - 1) / ByteSize]; + int index = -1; + + foreach(ulong bitEntry in bitCollection){ + if (++index >= bytes.Length){ + break; + } + + bytes[index] = (byte)(bitEntry & ByteMask); + + for(int byteOffset = 1; byteOffset < BytesPerEntry; byteOffset++){ + if (++index >= bytes.Length){ + break; + } + + bytes[index] = (byte)((bitEntry >> (ByteSize * byteOffset)) & ByteMask); + } + } + + return bytes; + } + + /// <summary> + /// Converts the stream into its text representation. The returned string is empty if the stream is also empty, or contains a sequence 0s and 1s. + /// </summary> + public override string ToString(){ + StringBuilder build = new StringBuilder(Length); + + foreach(bool bit in this){ + build.Append(bit ? True : False); + } + + return build.ToString(); + } + + #endregion + + #region Equality + + /// <summary> + /// Returns the hash code for this instance. + /// </summary> + public override int GetHashCode(){ + int hash = Length * 17; + + foreach(ulong bitEntry in bitCollection){ + hash = unchecked((hash * 31) + (bitEntry.GetHashCode())); + } + + return hash; + } + + /// <summary> + /// Returns true if and only if the <paramref name="obj"/> parameter is a <see cref="BitStream"/> with the same <see cref="Length"/> and contents. + /// </summary> + /// <param name="obj"></param> + public override bool Equals(object obj){ + return obj is BitStream other && Length == other.Length && bitCollection.SequenceEqual(other.bitCollection); + } + + #endregion + } +} diff --git a/UnitTests/IO/TestBitStream.fs b/UnitTests/IO/TestBitStream.fs new file mode 100644 index 0000000..e66ae4b --- /dev/null +++ b/UnitTests/IO/TestBitStream.fs @@ -0,0 +1,135 @@ +namespace UnitTests.IO.TestBitStream + +open Xunit +open System +open BrotliLib.IO + + +module Representations = + [<Theory>] + [<InlineData((* 0*)"")>] + [<InlineData((* 1*)"0")>] + [<InlineData((* 2*)"00")>] + [<InlineData((* 3*)"000")>] + [<InlineData((* 4*)"0000")>] + [<InlineData((* 8*)"11110000")>] + [<InlineData((* 16*)"1111000011110000")>] + [<InlineData((* 32*)"11110000111100001111000011110000")>] + [<InlineData((* 64*)"1111000011110000111100001111000011110000111100001111000011110000")>] + [<InlineData((*128*)"11110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000")>] + [<InlineData((*129*)"111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001")>] + let ``constructing from string yields same length and string representation`` (bits: string) = + let stream = BitStream(bits) + Assert.Equal(bits.Length, stream.Length) + Assert.Equal(bits, stream.ToString()) + + [<Theory>] + [<InlineData("1100 ")>] + [<InlineData("1111211")>] + [<InlineData("0000_1111")>] + let ``constructing from string with invalid characters throws exception`` (bits: string) = + Assert.Throws<ArgumentOutOfRangeException>(fun () -> BitStream(bits) |> ignore) + + [<Theory>] + [<InlineData()>] + [<InlineData(0b00000000uy)>] + [<InlineData(0b11100110uy)>] + [<InlineData(0b00000000uy, 0b11111111uy)>] + [<InlineData(0b11111111uy, 0b00000000uy, 0b11111111uy)>] + [<InlineData(0b00000000uy, 0b11111111uy, 0b00000000uy, 0b11111111uy)>] + [<InlineData(0b11111111uy, 0b00000000uy, 0b11111111uy, 0b00000000uy, 0b11111111uy)>] + [<InlineData(0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy)>] + let ``constructing from byte array yields same length and byte array representation`` ([<ParamArray>] bytes: byte array) = + let stream = BitStream(bytes) + Assert.Equal(bytes.Length * 8, stream.Length) + Assert.Equal<byte array>(bytes, stream.ToByteArray()) + + [<Theory>] + [<InlineData("0", 0b00000000uy)>] + [<InlineData("1", 0b00000001uy)>] + [<InlineData("11", 0b00000011uy)>] + [<InlineData("1111111", 0b01111111uy)>] + [<InlineData("11111111", 0b11111111uy)>] + [<InlineData("01010101", 0b10101010uy)>] + [<InlineData("111111110", 0b11111111uy, 0b00000000uy)>] + [<InlineData("111111111", 0b11111111uy, 0b00000001uy)>] + [<InlineData("11110000111100001111001", 0b00001111uy, 0b00001111uy, 0b01001111uy)>] + let ``constructing from string yields correct byte array representation with zero padding`` (bits: string, [<ParamArray>] bytes: byte array) = + Assert.Equal<byte array>(bytes, BitStream(bits).ToByteArray()) + + +module Equality = + let equal : obj array seq = seq { + yield [| "" |] + yield [| "0" |] + yield [| "1" |] + yield [| "10101010" |] + yield [| "000000000" |] + } + + let notequal : obj array seq = seq { + yield [| ""; "0" |] + yield [| "0"; "1" |] + yield [| "00"; "000" |] + yield [| "10101010"; "01010101" |] + yield [| "00000000"; "0000000000" |] + yield [| "000000001"; "000000000" |] + } + + [<Theory>] + [<MemberData("equal")>] + let ``two streams are equal if they have same contents`` (bits: string) = + Assert.True(BitStream(bits).Equals(BitStream(bits))) + + [<Theory>] + [<MemberData("notequal")>] + let ``two streams are not equal if they have different contents`` (first: string, second: string) = + Assert.False(BitStream(first).Equals(BitStream(second))) + + [<Theory>] + [<MemberData("equal")>] + let ``two equal streams have the same hash code`` (bits: string) = + Assert.Equal(BitStream(bits).GetHashCode(), BitStream(bits).GetHashCode()) + + [<Theory>] + [<MemberData("notequal")>] + let ``two different streams should generally have different hash codes`` (first: string, second: string) = + Assert.NotEqual(BitStream(first).GetHashCode(), BitStream(second).GetHashCode()) + + +module Mutability = + [<Theory>] + [<InlineData("")>] + [<InlineData("1", true)>] + [<InlineData("0", false)>] + [<InlineData("10", true, false)>] + [<InlineData("11110000", true, true, true, true, false, false, false, false)>] + [<InlineData("11110000111100001", true, true, true, true, false, false, false, false, true, true, true, true, false, false, false, false, true)>] + let ``appending to empty stream yields correct text representation`` (expected: string, [<ParamArray>] values: bool array) = + let stream = BitStream() + Array.iter (stream.Add) <| values + + Assert.Equal(expected, stream.ToString()) + + +module Cloning = + [<Theory>] + [<InlineData("")>] + [<InlineData("0")>] + [<InlineData("1")>] + [<InlineData("101010101")>] + let ``cloned stream is equal to original stream but not the same object`` (bits: string) = + let original = BitStream(bits) + let clone = original.Clone() + + Assert.Equal<BitStream>(original, clone) + Assert.NotSame(original, clone) + + [<Fact>] + let ``appending to cloned stream keeps the original stream intact`` () = + let original = BitStream("11100") + let clone = original.Clone() + clone.Add(true) + + Assert.Equal("11100", original.ToString()) + Assert.Equal("111001", clone.ToString()) diff --git a/UnitTests/UnitTests.fsproj b/UnitTests/UnitTests.fsproj index 35d5079..f4d3a5e 100644 --- a/UnitTests/UnitTests.fsproj +++ b/UnitTests/UnitTests.fsproj @@ -56,6 +56,7 @@ </Target> <Import Project="..\packages\xunit.core.2.4.1\build\xunit.core.targets" Condition="Exists('..\packages\xunit.core.2.4.1\build\xunit.core.targets')" /> <ItemGroup> + <Compile Include="IO\TestBitStream.fs" /> <Content Include="packages.config" /> </ItemGroup> <ItemGroup>