├── .gitignore ├── BitPacking.Tests ├── App.config ├── BinaryNumberTests.cs ├── BitReaderTests.cs ├── BitWriterTests.cs ├── Bitpacking.Tests.csproj ├── MaskUtilityTests.cs ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── BitPacking.sln ├── BitPacking ├── App.config ├── BinaryNumber.cs ├── BinaryNumber_ExtraMethods.cs ├── BitPacking.csproj ├── BitReader.cs ├── BitWriter.cs ├── MaskUtility.cs └── Properties │ └── AssemblyInfo.cs ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /packages 3 | bin 4 | obj -------------------------------------------------------------------------------- /BitPacking.Tests/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BitPacking.Tests/BinaryNumberTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Linq; 3 | 4 | namespace SickDev.BitPacking.Tests 5 | { 6 | [TestFixture] 7 | class BinaryNumberTests 8 | { 9 | [Test] 10 | public void Value_Is_TheSame() 11 | { 12 | BinaryNumber binary = 123456789; 13 | Assert.AreEqual(binary.value, 123456789); 14 | } 15 | 16 | [Test] 17 | public void SignificantBits_Is_1_For_Zero() 18 | { 19 | BinaryNumber binary = 0; 20 | Assert.AreEqual(binary.significantBits, 1); 21 | } 22 | 23 | [Test] 24 | public void SignificantBits_Is_1_For_One() 25 | { 26 | BinaryNumber binary = 1; 27 | Assert.AreEqual(binary.significantBits, 1); 28 | } 29 | 30 | [Test] 31 | public void SignificantBits_Is_8_For_MaxByte() 32 | { 33 | BinaryNumber binary = byte.MaxValue; 34 | Assert.AreEqual(8, binary.significantBits); 35 | } 36 | 37 | [Test] 38 | public void SignificantBits_Is_16_For_MaxUShort() 39 | { 40 | BinaryNumber binary = ushort.MaxValue; 41 | Assert.AreEqual(16, binary.significantBits); 42 | } 43 | 44 | [Test] 45 | public void SignificantBits_Is_32_For_MaxUInt() 46 | { 47 | BinaryNumber binary = uint.MaxValue; 48 | Assert.AreEqual(32, binary.significantBits); 49 | } 50 | 51 | [Test] 52 | public void SignificantBits_Is_64_For_MaxULong() 53 | { 54 | BinaryNumber binary = ulong.MaxValue; 55 | Assert.AreEqual(64, binary.significantBits); 56 | } 57 | 58 | [Test] 59 | public void WorksForNegativeNumbers() 60 | { 61 | Assert.DoesNotThrow(() => new BinaryNumber(-1)); 62 | } 63 | 64 | [Test, Sequential] 65 | public void GetBytes_Returns_1ByteForEvery8Bits([Values(1, 9, 25, 57)] int significantBits, [Values(1, 2, 4, 8)] int numberOfBytes) 66 | { 67 | BinaryNumber binary = ulong.MaxValue; 68 | Assert.AreEqual(numberOfBytes, binary.GetBytes(significantBits).Length); 69 | } 70 | 71 | [Test] 72 | public void GetBytes_Returns_RightmostBytes() 73 | { 74 | /* The full number is 00000101 00001100 01000010 00100011‬ 75 | * but we are only getting bytes from 100 01000010 00100011 76 | * That should be 4, 66, 35; reversed 77 | */ 78 | BinaryNumber binary = 84689443; 79 | Assert.AreEqual(new byte[] { 35, 66, 4 }, binary.GetBytes(19)); 80 | } 81 | 82 | [Test] 83 | public void GetBytes_With_0_Returns_Empty() 84 | { 85 | BinaryNumber binary = 84689443; 86 | Assert.IsEmpty(binary.GetBytes(0)); 87 | } 88 | 89 | [Test] 90 | public void GetBytes_Throws_With_Negative() 91 | { 92 | BinaryNumber binary = 84689443; 93 | Assert.That(() => binary.GetBytes(-1), Throws.TypeOf()); 94 | } 95 | 96 | [Test] 97 | public void GetBytes_Throws_With_GreaterThan64() 98 | { 99 | BinaryNumber binary = 84689443; 100 | Assert.That(() => binary.GetBytes(65), Throws.TypeOf()); 101 | } 102 | 103 | [Test] 104 | public void ToString_Is_MultipleOf8() 105 | { 106 | //This is 11110001001000000 107 | //But I want it like this 108 | //000000011110001001000000 109 | BinaryNumber binary = 123456; 110 | string binaryString = binary.ToString(); 111 | Assert.That(() => binaryString.Replace(" ", string.Empty).Length % 8, Is.Zero); 112 | } 113 | 114 | [Test] 115 | public void ToString_Is_SeparatedByBytes() 116 | { 117 | //This is 11110001001000000 118 | //But I want it like this 119 | //00000001 11100010 01000000 120 | BinaryNumber binary = 123456; 121 | string binaryString = binary.ToString()+" "; 122 | int chunks = binaryString.Count(x => x == ' '); 123 | int[] chunksLengths = new int[chunks]; 124 | 125 | for (int i = 0; i < chunks; i++) 126 | { 127 | int index = binaryString.IndexOf(" "); 128 | string chunk = binaryString.Substring(0, index); 129 | chunksLengths[i] = chunk.Length; 130 | binaryString = binaryString.Remove(0, chunk.Length + 1); 131 | } 132 | 133 | Assert.That(chunksLengths, Is.All.EqualTo(8)); 134 | } 135 | 136 | [Test] 137 | public void LeftShiftOperator_Works() 138 | { 139 | BinaryNumber binary = new BinaryNumber(8); 140 | binary <<= 2; 141 | Assert.AreEqual(binary.value, 32); 142 | } 143 | 144 | [Test] 145 | public void RightShiftOperator_Works() 146 | { 147 | BinaryNumber binary = new BinaryNumber(32); 148 | binary >>= 2; 149 | Assert.AreEqual(binary.value, 8); 150 | } 151 | 152 | [Test] 153 | public void BitwiseOrOperator_Works() 154 | { 155 | BinaryNumber binary = new BinaryNumber(32); 156 | binary |= 8; 157 | Assert.AreEqual(binary.value, 40); 158 | } 159 | 160 | [Test] 161 | public void BitwiseAndOperator_Works() 162 | { 163 | BinaryNumber binary = new BinaryNumber(40); 164 | binary &= 12; 165 | Assert.AreEqual(binary.value, 8); 166 | } 167 | 168 | [Test] 169 | public void BitwiseExorOperator_Works() 170 | { 171 | BinaryNumber binary = new BinaryNumber(40); 172 | binary ^= 12; 173 | Assert.AreEqual(binary.value, 36); 174 | } 175 | 176 | [Test] 177 | public void EqualOperator_Works() 178 | { 179 | BinaryNumber binary = new BinaryNumber(40); 180 | BinaryNumber binary2 = new BinaryNumber(40); 181 | Assert.IsTrue(binary == binary2); 182 | } 183 | 184 | [Test] 185 | public void NotEqualOperator_Works() 186 | { 187 | BinaryNumber binary = new BinaryNumber(40); 188 | BinaryNumber binary2 = new BinaryNumber(42); 189 | Assert.IsTrue(binary != binary2); 190 | } 191 | 192 | [Test] 193 | public void GreaterThanOperator_Works() 194 | { 195 | BinaryNumber binary = new BinaryNumber(40); 196 | BinaryNumber binary2 = new BinaryNumber(42); 197 | Assert.IsTrue(binary2 > binary); 198 | } 199 | 200 | [Test] 201 | public void GreaterThanOrEqualOperator_Works() 202 | { 203 | BinaryNumber binary = new BinaryNumber(40); 204 | BinaryNumber binary2 = new BinaryNumber(42); 205 | Assert.IsTrue(binary2 >= binary); 206 | } 207 | 208 | [Test] 209 | public void LessThanOperator_Works() 210 | { 211 | BinaryNumber binary = new BinaryNumber(40); 212 | BinaryNumber binary2 = new BinaryNumber(42); 213 | Assert.IsTrue(binary < binary2); 214 | } 215 | 216 | [Test] 217 | public void LessThanOrEqualOperator_Works() 218 | { 219 | BinaryNumber binary = new BinaryNumber(40); 220 | BinaryNumber binary2 = new BinaryNumber(42); 221 | Assert.IsTrue(binary <= binary2); 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /BitPacking.Tests/BitReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace SickDev.BitPacking.Tests 5 | { 6 | [TestFixture] 7 | class BitReaderTests 8 | { 9 | [Test] 10 | public void BitsLeft_Is_All_After_Construct() 11 | { 12 | BitReader reader = new BitReader(1, 2, 3); 13 | Assert.AreEqual(24, reader.bitsLeft); 14 | } 15 | 16 | [Test] 17 | public void BitsLeft_Is_Substracted_After_Read() 18 | { 19 | BitReader reader = new BitReader(1, 2, 3); 20 | reader.Read(5); 21 | Assert.AreEqual(19, reader.bitsLeft); 22 | } 23 | 24 | [Test] 25 | public void Read_Throws_With_NegativeNumber() 26 | { 27 | BitReader reader = new BitReader(1, 2, 3); 28 | Assert.That(()=>reader.Read(-1), Throws.InstanceOf()); 29 | } 30 | 31 | [Test] 32 | public void Read_Throws_When_ReadingTooManyBits() 33 | { 34 | BitReader reader = new BitReader(1, 2, 3); 35 | Assert.That(()=>reader.Read(25), Throws.InstanceOf()); 36 | } 37 | 38 | [Test] 39 | public void Read_Works() 40 | { 41 | BitReader reader = new BitReader( 42 | 0b00000001, 43 | 0b00000010, 44 | 0b00000011 45 | ); 46 | Assert.AreEqual(513, (ulong)reader.Read(10)); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /BitPacking.Tests/BitWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace SickDev.BitPacking.Tests 5 | { 6 | [TestFixture] 7 | class BitWriterTests 8 | { 9 | [Test] 10 | public void GetBytes_Returns_Empty_When_NewWriter() 11 | { 12 | Assert.IsEmpty(new BitWriter().GetBytes()); 13 | } 14 | 15 | [Test] 16 | public void GetBytes_Works_When_MoreThan64Bits() 17 | { 18 | /* There are 12 entries, 19 | * and we will Write them together in groups of 4. 20 | * In total, 96 bits, forcing the Writer to make 2 BinaryNumber 21 | * The bytes from GetBytes should match perfectly with these 22 | */ 23 | byte[] input = new byte[] 24 | { 25 | 12, 0, 157, 212, 26 | 255, 2, 42, 128, 27 | 188, 200, 10, 32 28 | }; 29 | 30 | BitWriter writer = new BitWriter(); 31 | for (int i = 0; i < input.Length; i += 4) 32 | writer.Write(BitConverter.ToUInt32(input, i)); 33 | Assert.AreEqual(input, writer.GetBytes()); 34 | } 35 | 36 | [Test] 37 | public void GetBytes_Works_When_WriteWithBits() 38 | { 39 | /* 1705 is 110 10101001‬ 40 | * But when writting only 10 bits, 41 | * 10 10101001‬ is 681 42 | * which is 2 169 43 | */ 44 | BitWriter writer = new BitWriter(); 45 | writer.Write(1705, 10); 46 | Assert.AreEqual(new byte[] { 169, 2 }, writer.GetBytes()); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /BitPacking.Tests/Bitpacking.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Debug 8 | AnyCPU 9 | {74766FA7-0143-4810-B269-B74C9C150DDE} 10 | Library 11 | Test 12 | Test 13 | v4.6.1 14 | 512 15 | true 16 | 17 | 18 | 19 | 20 | AnyCPU 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | AnyCPU 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | 41 | 42 | 43 | ..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {daad79dc-30a3-4a9a-99f8-3ac4ae6d201f} 61 | BitPacking 62 | 63 | 64 | 65 | 66 | 67 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /BitPacking.Tests/MaskUtilityTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace SickDev.BitPacking.Tests 4 | { 5 | [TestFixture] 6 | class MaskUtilityTests 7 | { 8 | [Test] 9 | public void MakeShifted_Shifts_1() 10 | { 11 | Assert.AreEqual(1024, MaskUtility.MakeShifted(10).value); 12 | } 13 | 14 | [Test] 15 | public void MakeFilled_MakesEverything1() 16 | { 17 | Assert.AreEqual(1023, MaskUtility.MakeFilled(10).value); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BitPacking.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Test")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("74766fa7-0143-4810-b269-b74c9c150dde")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /BitPacking.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BitPacking.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29324.140 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitpacking.Tests", "BitPacking.Tests\Bitpacking.Tests.csproj", "{74766FA7-0143-4810-B269-B74C9C150DDE}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitPacking", "BitPacking\BitPacking.csproj", "{DAAD79DC-30A3-4A9A-99F8-3AC4AE6D201F}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {74766FA7-0143-4810-B269-B74C9C150DDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {74766FA7-0143-4810-B269-B74C9C150DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {74766FA7-0143-4810-B269-B74C9C150DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {74766FA7-0143-4810-B269-B74C9C150DDE}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {DAAD79DC-30A3-4A9A-99F8-3AC4AE6D201F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DAAD79DC-30A3-4A9A-99F8-3AC4AE6D201F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DAAD79DC-30A3-4A9A-99F8-3AC4AE6D201F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DAAD79DC-30A3-4A9A-99F8-3AC4AE6D201F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {E9FBA8AE-DD27-44E1-AEC1-3CBDC74D737D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /BitPacking/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BitPacking/BinaryNumber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Collections.Generic; 4 | using DebugBinaryNumber = 5 | #if DEBUG 6 | SickDev.BitPacking.BinaryNumber 7 | #else 8 | System.UInt64 9 | #endif 10 | ; 11 | 12 | namespace SickDev.BitPacking 13 | { 14 | public readonly partial struct BinaryNumber : IConvertible, IComparable, IEquatable 15 | { 16 | public const int bitsPerByte = 8; 17 | public const int maxBits = 64; 18 | 19 | static Dictionary stringRepresentations = new Dictionary(); 20 | 21 | public readonly ulong value; 22 | public readonly int significantBits; 23 | 24 | public BinaryNumber(IConvertible value) 25 | { 26 | this.value = value.ToUInt64(null); 27 | significantBits = maxBits - CountLeadingZeros(this.value); 28 | } 29 | 30 | //Taken from https://stackoverflow.com/questions/31374628/fast-way-of-finding-most-and-least-significant-bit-set-in-a-64-bit-integer 31 | public static int CountLeadingZeros(ulong input) 32 | { 33 | if (input == 0) 34 | return 63; 35 | 36 | ulong n = 1; 37 | if ((input >> 32) == 0) {n += 32; input <<= 32;} 38 | if ((input >> 48) == 0) {n += 16; input <<= 16;} 39 | if ((input >> 56) == 0) {n += 8; input <<= 8;} 40 | if ((input >> 60) == 0) {n += 4; input <<= 4;} 41 | if ((input >> 62) == 0) {n += 2; input <<= 2;} 42 | n -= input >> 63; 43 | 44 | return (int)n; 45 | } 46 | 47 | //Transforms the whole number into an array of bytes 48 | public byte[] GetBytes() => GetBytes(significantBits); 49 | 50 | //Transforms the first "bits" into an array of bytes 51 | public byte[] GetBytes(int bits) 52 | { 53 | if (bits < 0 || bits > 64) 54 | throw new ArgumentOutOfRangeException(nameof(bits), $"Must be 0 < {nameof(bits)} < 64"); 55 | int length = (int)Math.Ceiling((float)bits / bitsPerByte); 56 | byte[] bytes = new byte[length]; 57 | 58 | //clamp value to the bits we were asked for 59 | DebugBinaryNumber clampedValue = value & MaskUtility.MakeFilled(bits); 60 | 61 | //Here we shift packs of 8 bits to the right so that we can get that particular byte value 62 | for (int i = 0; i < length; i++) 63 | { 64 | bytes[i] = clampedValue; 65 | clampedValue >>= bitsPerByte; 66 | } 67 | 68 | return bytes; 69 | } 70 | 71 | public override string ToString() 72 | { 73 | if (!stringRepresentations.TryGetValue(value, out string toString)) 74 | { 75 | toString = CreateStringRepresentation(value); 76 | stringRepresentations.Add(value, toString); 77 | } 78 | return toString; 79 | } 80 | 81 | static string CreateStringRepresentation(ulong value) 82 | { 83 | //This very first line would suffice... 84 | StringBuilder builder = new StringBuilder(Convert.ToString((long)value, 2)); 85 | 86 | //...but I'm interested in making the string multiple of 8... 87 | int zerosLeft = builder.Length % bitsPerByte; 88 | if (zerosLeft > 0) 89 | { 90 | zerosLeft = bitsPerByte - zerosLeft; 91 | for (int i = 0; i < zerosLeft; i++) 92 | builder.Insert(0, "0"); 93 | } 94 | 95 | //...and separating the bytes for an easier visualization 96 | int spaces = builder.Length / bitsPerByte; 97 | for (int i = 1; i < spaces; i++) 98 | builder.Insert(i * bitsPerByte + i - 1, " "); 99 | 100 | return builder.ToString(); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /BitPacking/BinaryNumber_ExtraMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SickDev.BitPacking 4 | { 5 | public readonly partial struct BinaryNumber : IConvertible, IComparable, IEquatable 6 | { 7 | 8 | #region IConvertible 9 | public TypeCode GetTypeCode() => value.GetTypeCode(); 10 | 11 | public bool ToBoolean() => ToBoolean(null); 12 | public bool ToBoolean(IFormatProvider provider) => Convert.ToBoolean(value); 13 | public char ToChar() => ToChar(null); 14 | public char ToChar(IFormatProvider provider) => Convert.ToChar(value); 15 | public sbyte ToSByte() => ToSByte(null); 16 | public sbyte ToSByte(IFormatProvider provider) => Convert.ToSByte(value); 17 | public byte ToByte() => ToByte(null); 18 | public byte ToByte(IFormatProvider provider) => Convert.ToByte(value); 19 | public short ToInt16() => ToInt16(null); 20 | public short ToInt16(IFormatProvider provider) => Convert.ToInt16(value); 21 | public ushort ToUInt16() => ToUInt16(null); 22 | public ushort ToUInt16(IFormatProvider provider) => Convert.ToUInt16(value); 23 | public int ToInt32() => ToInt32(null); 24 | public int ToInt32(IFormatProvider provider) => Convert.ToInt32(value); 25 | public uint ToUInt32() => ToUInt32(null); 26 | public uint ToUInt32(IFormatProvider provider) => Convert.ToUInt32(value); 27 | public long ToInt64() => ToInt64(null); 28 | public long ToInt64(IFormatProvider provider) => Convert.ToInt64(value); 29 | public ulong ToUInt64() => value; 30 | public ulong ToUInt64(IFormatProvider provider) => value; 31 | public float ToSingle() => ToSingle(null); 32 | public float ToSingle(IFormatProvider provider) => Convert.ToSingle(value); 33 | public double ToDouble() => ToDouble(null); 34 | public double ToDouble(IFormatProvider provider) => Convert.ToDouble(value); 35 | public decimal ToDecimal() => ToDecimal(null); 36 | public decimal ToDecimal(IFormatProvider provider) => Convert.ToDecimal(value); 37 | public DateTime ToDateTime() => ToDateTime(null); 38 | public DateTime ToDateTime(IFormatProvider provider) => Convert.ToDateTime(value); 39 | public string ToString(IFormatProvider provider) => ToString(); 40 | public object ToType(Type conversionType) => ToType(conversionType); 41 | public object ToType(Type conversionType, IFormatProvider provider) => Convert.ChangeType(value, conversionType); 42 | #endregion 43 | 44 | #region Operators 45 | public static BinaryNumber operator <<(in BinaryNumber binary, int bits) => binary.value << bits; 46 | public static BinaryNumber operator >>(in BinaryNumber binary, int bits) => binary.value >> bits; 47 | public static BinaryNumber operator |(in BinaryNumber binary, IConvertible number) => binary.value | number.ToUInt64(null); 48 | public static BinaryNumber operator &(in BinaryNumber binary, IConvertible number) => binary.value & number.ToUInt64(null); 49 | public static BinaryNumber operator ^(in BinaryNumber binary, IConvertible number) => binary.value ^ number.ToUInt64(null); 50 | 51 | public static bool operator ==(in BinaryNumber binary, IConvertible number) => binary.value == number.ToUInt64(null); 52 | public static bool operator !=(in BinaryNumber binary, IConvertible number) => binary.value != number.ToUInt64(null); 53 | public static bool operator >(in BinaryNumber binary, IConvertible number) => binary.value > number.ToUInt64(null); 54 | public static bool operator <(in BinaryNumber binary, IConvertible number) => binary.value < number.ToUInt64(null); 55 | public static bool operator >=(in BinaryNumber binary, IConvertible number) => binary.value >= number.ToUInt64(null); 56 | public static bool operator <=(in BinaryNumber binary, IConvertible number) => binary.value <= number.ToUInt64(null); 57 | 58 | public static implicit operator BinaryNumber(sbyte number) => new BinaryNumber(number); 59 | public static implicit operator BinaryNumber(byte number) => new BinaryNumber(number); 60 | public static implicit operator BinaryNumber(ushort number) => new BinaryNumber(number); 61 | public static implicit operator BinaryNumber(short number) => new BinaryNumber(number); 62 | public static implicit operator BinaryNumber(uint number) => new BinaryNumber(number); 63 | public static implicit operator BinaryNumber(int number) => new BinaryNumber(number); 64 | public static implicit operator BinaryNumber(ulong number) => new BinaryNumber(number); 65 | public static implicit operator BinaryNumber(long number) => new BinaryNumber(number); 66 | 67 | public static implicit operator sbyte(in BinaryNumber number) => (sbyte)number.value; 68 | public static implicit operator byte(in BinaryNumber number) => (byte)number.value; 69 | public static implicit operator ushort(in BinaryNumber number) => (ushort)number.value; 70 | public static implicit operator short(in BinaryNumber number) => (short)number.value; 71 | public static implicit operator uint(in BinaryNumber number) => (uint)number.value; 72 | public static implicit operator int(in BinaryNumber number) => (int)number.value; 73 | public static implicit operator ulong(in BinaryNumber number) => number.value; 74 | public static implicit operator long(in BinaryNumber number) => (long)number.value; 75 | #endregion 76 | 77 | public int CompareTo(BinaryNumber other) => value.CompareTo(other.value); 78 | public bool Equals(BinaryNumber other) => value.Equals(other.value); 79 | public override bool Equals(object obj) => value.Equals(obj); 80 | public override int GetHashCode() => value.GetHashCode(); 81 | } 82 | } -------------------------------------------------------------------------------- /BitPacking/BitPacking.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DAAD79DC-30A3-4A9A-99F8-3AC4AE6D201F} 8 | Library 9 | Properties 10 | BinaryStream 11 | BinaryStream 12 | v4.5.2 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 60 | -------------------------------------------------------------------------------- /BitPacking/BitReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DebugBinaryNumber = 3 | #if DEBUG 4 | SickDev.BitPacking.BinaryNumber 5 | #else 6 | System.UInt64 7 | #endif 8 | ; 9 | 10 | namespace SickDev.BitPacking 11 | { 12 | public class BitReader 13 | { 14 | readonly long length; 15 | readonly byte[] data; 16 | long byteIndex; 17 | byte bitIndex; 18 | 19 | long position => byteIndex * BinaryNumber.bitsPerByte + bitIndex; 20 | public long bitsLeft => length - position; 21 | public bool canRead => bitsLeft > 0; 22 | 23 | public BitReader(params byte[] data) 24 | { 25 | this.data = data; 26 | length = data.LongLength * BinaryNumber.bitsPerByte; 27 | } 28 | 29 | public DebugBinaryNumber Read(int bits) 30 | { 31 | if (bits < 0 || position + bits > length) 32 | throw new ArgumentOutOfRangeException($"Attempting to read {bits} bits, but there's only {bitsLeft} bits left"); 33 | 34 | DebugBinaryNumber value = 0; 35 | 36 | //For every bit we want to read... 37 | for (int i = 0; i < bits; i++) 38 | { 39 | //...first, get the byte we are currently reading from... 40 | DebugBinaryNumber @byte = data[byteIndex]; 41 | //...and then get the appropiate bit from that byte 42 | DebugBinaryNumber mask = MaskUtility.MakeShifted(bitIndex); 43 | DebugBinaryNumber bit = @byte & mask; 44 | 45 | //Put the bit into the correct position be want to write 46 | int shiftAmount = i - bitIndex; 47 | if (shiftAmount < 0) 48 | bit >>= -shiftAmount; 49 | else 50 | bit <<= shiftAmount; 51 | 52 | //And write that bit into the final value 53 | value |= bit; 54 | 55 | //Update the bit and byte we next have to read from 56 | bitIndex++; 57 | if (bitIndex == 8) 58 | { 59 | byteIndex++; 60 | bitIndex = 0; 61 | } 62 | } 63 | 64 | return value; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /BitPacking/BitWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Collections.Generic; 3 | using DebugBinaryNumber = 4 | #if DEBUG 5 | SickDev.BitPacking.BinaryNumber 6 | #else 7 | System.UInt64 8 | #endif 9 | ; 10 | 11 | namespace SickDev.BitPacking 12 | { 13 | public class BitWriter 14 | { 15 | List numbers = new List(); 16 | int bitsUsed; 17 | 18 | int freeBits => BinaryNumber.maxBits - bitsUsed; 19 | 20 | DebugBinaryNumber currentNumber 21 | { 22 | get => numbers[numbers.Count - 1]; 23 | set => numbers[numbers.Count - 1] = value; 24 | } 25 | 26 | public BitWriter() => CreateNewNumber(); 27 | 28 | void CreateNewNumber() 29 | { 30 | numbers.Add(0); 31 | bitsUsed = 0; 32 | } 33 | 34 | void WriteValue(BinaryNumber value) => WriteValue(value, value.significantBits); 35 | 36 | //Write only the specified number of bits of the specified value 37 | void WriteValue(DebugBinaryNumber value, int bits) 38 | { 39 | //If we don't have enough space to write the whole value... 40 | if (bits > freeBits) 41 | { 42 | bits -= freeBits; 43 | DebugBinaryNumber mask = MaskUtility.MakeFilled(freeBits); 44 | 45 | //...write only the first bits of the value... 46 | int bitsToShift = freeBits; 47 | DebugBinaryNumber maskedValue = value & mask; 48 | WriteToCurrentNumber(maskedValue, freeBits); 49 | 50 | //...and then remove those bits... 51 | value >>= bitsToShift; 52 | } 53 | 54 | WriteToCurrentNumber(value, bits); 55 | } 56 | 57 | void WriteToCurrentNumber(DebugBinaryNumber value, int bits) 58 | { 59 | //Write the value at the end of the currentNumber 60 | value <<= bitsUsed; 61 | currentNumber |= value; 62 | 63 | bitsUsed += bits; 64 | 65 | if (freeBits == 0) 66 | CreateNewNumber(); 67 | } 68 | 69 | public byte[] GetBytes() 70 | { 71 | int numbersCount = numbers.Count; 72 | byte[][] bytesPerNumber = new byte[numbersCount][]; 73 | ulong totalBytes = 0; 74 | 75 | //For every number FULLY written, get its bytes 76 | for (int i = 0; i < numbersCount - 1; i++) 77 | { 78 | byte[] bytes = System.BitConverter.GetBytes(numbers[i].value); 79 | bytesPerNumber[i] = bytes; 80 | totalBytes += (ulong)bytes.Length; 81 | } 82 | 83 | //Then get the bytes from the current number. We do this out of the for loop because we only want so many bits and not the whole pack 84 | //Also, the reason why we create a new BinaryNumber is because currentNumber may not be a BinaryNumber itself 85 | byte[] lastBytes = new BinaryNumber(currentNumber).GetBytes(bitsUsed); 86 | bytesPerNumber[numbersCount - 1] = lastBytes; 87 | totalBytes += (ulong)lastBytes.Length; 88 | 89 | //Here we convert the 2D array into the final 1D result 90 | byte[] result = new byte[totalBytes]; 91 | int index = 0; 92 | 93 | for (int i = 0; i < numbersCount; i++) 94 | { 95 | for (int j = 0; j < bytesPerNumber[i].Length; j++) 96 | { 97 | result[index] = bytesPerNumber[i][j]; 98 | index++; 99 | } 100 | } 101 | 102 | return result; 103 | } 104 | 105 | public override string ToString() 106 | { 107 | StringBuilder builder = new StringBuilder(); 108 | for (int i = 0; i < numbers.Count; i++) 109 | { 110 | builder.Insert(0, numbers[i].ToString()); 111 | if (i != numbers.Count - 1) 112 | builder.Insert(0, " "); 113 | } 114 | return builder.ToString(); 115 | } 116 | 117 | public void Write(BinaryNumber value) => WriteValue(value); 118 | public void Write(byte value) => WriteValue(value); 119 | public void Write(ushort value) => WriteValue(value); 120 | public void Write(short value) => WriteValue((ulong)value); 121 | public void Write(uint value) => WriteValue(value); 122 | public void Write(int value) => WriteValue((ulong)value); 123 | public void Write(ulong value) => WriteValue(value); 124 | public void Write(long value) => WriteValue((ulong)value); 125 | public void Write(byte value, int bits) => WriteValue(value, bits); 126 | public void Write(ushort value, int bits) => WriteValue(value, bits); 127 | public void Write(short value, int bits) => WriteValue((ulong)value, bits); 128 | public void Write(uint value, int bits) => WriteValue(value, bits); 129 | public void Write(int value, int bits) => WriteValue((ulong)value, bits); 130 | public void Write(ulong value, int bits) => WriteValue(value, bits); 131 | public void Write(long value, int bits) => WriteValue((ulong)value, bits); 132 | } 133 | } -------------------------------------------------------------------------------- /BitPacking/MaskUtility.cs: -------------------------------------------------------------------------------- 1 | using DebugBinaryNumber = 2 | #if DEBUG 3 | SickDev.BitPacking.BinaryNumber 4 | #else 5 | System.UInt64 6 | #endif 7 | ; 8 | 9 | namespace SickDev.BitPacking 10 | { 11 | public static class MaskUtility 12 | { 13 | static DebugBinaryNumber[] filledMasks = new DebugBinaryNumber[BinaryNumber.maxBits + 1]; 14 | 15 | static MaskUtility() 16 | { 17 | for (int i = 0; i < filledMasks.Length; i++) 18 | filledMasks[i] = MakeFilledInternal(i); 19 | } 20 | 21 | static DebugBinaryNumber MakeFilledInternal(int amount) 22 | { 23 | DebugBinaryNumber mask = 0; 24 | for (int i = 0; i < amount; i++) 25 | mask |= MakeShifted(i); 26 | return mask; 27 | } 28 | 29 | //Create numbers in the form of 0000100 being the 1 in the position determined by the parameter 30 | public static DebugBinaryNumber MakeShifted(int position) => 1UL << position; 31 | 32 | //Create numbers in the form of 1111111 with as many 1s as amount parameter 33 | //This is a slightly slower operation than the shifted version, which is why the values are cached 34 | public static DebugBinaryNumber MakeFilled(int amount) => filledMasks[amount]; 35 | } 36 | } -------------------------------------------------------------------------------- /BitPacking/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("BitPacking")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("BitPacking")] 12 | [assembly: AssemblyCopyright("Copyright © 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("daad79dc-30a3-4a9a-99f8-3ac4ae6d201f")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cobo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitPacking 2 | 3 | Bit packing is a compression technique in which unnecessary bits are removed from the data we want to compress. 4 | 5 | As an example, say you want to serialize someone's age. You would most likely represent that piece of data with an integer structure; say, 32 bits. However, because the range of that data is known to be between {0, 100} (for the purpose of this example), you only need 7 bits at most to represent it. 6 | 7 | Even if you initially represented the data as a single byte (8 bits), if you wanted to serialize the age of 100 different people, using this techinque, all that data would only take 700 bits (88 bytes) instead of 800 bits (100 bytes). 8 | 9 | This library provides an easy to use API so that you don't have to worry about all the math involved in the process. 10 | 11 | ## Usage 12 | The *BitWriter* class packs the data we want to compress. 13 | 14 | ```csharp 15 | using SickDev.BitPacking; 16 | 17 | ... 18 | 19 | //The data we want to serialize 20 | byte[] ages = new byte[100]; 21 | for (int i = 0; i < ages.Length; i++) 22 | ages[i] = (byte) random.Next(0, 101); 23 | 24 | //Pack the data, specifying that only the first 7 bits matter 25 | BitWriter writer = new BitWriter(); 26 | for (int i = 0; i < ages.Length; i++) 27 | writer.Write(ages[i], 7); 28 | ``` 29 | 30 | Once all the data is written, we can get the compressed version. 31 | ```csharp 32 | //Get the compressed version of the data 33 | byte[] compressedAges = writer.GetBytes(); 34 | 35 | //Non compressed data size: 100, compressed data size: 88 36 | Console.WriteLine($"Non compressed data size: {ages.Length}, " + 37 | $"compressed data size: {compressedAges.Length}"); 38 | ``` 39 | 40 | On the other hand, we use *BitReader* to read from the compressed data. 41 | ```csharp 42 | byte[] uncompressedData = new byte[100]; 43 | 44 | //Unpack the data, reading only 7 bits at a time 45 | BitReader reader = new BitReader(compressedAges); 46 | for (int i = 0; i < uncompressedData.Length; i++) 47 | uncompressedData[i] = reader.Read(7); 48 | 49 | //Test passed! 50 | Assert.AreEqual(ages, uncompressedData); 51 | ``` 52 | 53 | #### Neagtive Numbers 54 | Negative numebrs are not currently supported. However, it is fairly simple to work around that. When you need to know whether some data is positive or negative, simply write an extra sign indicating the sign. 55 | 56 | ```csharp 57 | //The data we want to serialize 58 | int[] data = new int[10]; 59 | for (int i = 0; i < data.Length; i++) 60 | data[i] = random.Next(-1024, 1025); 61 | 62 | BitWriter writer = new BitWriter(); 63 | for (int i = 0; i < data.Length; i++) 64 | { 65 | //When packing, write the absolute value of the data 66 | writer.Write(Math.Abs(data[i]), 10); 67 | //After that, write 1 bit indicating the sign of the data 68 | writer.Write(data[i] < 0 ? 0 : 1, 1); 69 | } 70 | 71 | byte[] compressedData = writer.GetBytes(); 72 | 73 | int[] uncompressedData = new int[10]; 74 | 75 | BitReader reader = new BitReader(compressedData); 76 | for (int i = 0; i < uncompressedData.Length; i++) 77 | { 78 | uncompressedData[i] = reader.Read(10); 79 | //When unpacking, if the next bit is a 0, multiply by -1 80 | if (reader.Read(1) == 0) 81 | uncompressedData[i] *= -1; 82 | } 83 | 84 | //Test passed! 85 | Assert.AreEqual(data, uncompressedData); 86 | ``` 87 | 88 | ### Performance 89 | The class _BinaryNumber_ provides a cool binary string representation of any numeric type; which works wonders for debugging. As such, the library is made to use them when built in Debug configuration; however, that vastly reduces performance. In a production scenario, it is advised to build the library in Release configuration. 90 | 91 | ## Dependencies 92 | 93 | The _BitPacking.Tests_ project has the following dependencies: 94 | - [NUnit 3.12.0](https://www.nuget.org/packages/NUnit/3.12.0) 95 | - [NUnit3TestAdapter 3.16.1](https://www.nuget.org/packages/NUnit3TestAdapter/3.16.1) 96 | 97 | ## Contributing 98 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 99 | 100 | Please make sure to update tests as appropriate. 101 | 102 | ## License 103 | [MIT](/blob/master/LICENSE.md) 104 | --------------------------------------------------------------------------------