├── .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 |
--------------------------------------------------------------------------------