From e08274189a3bad1589b21fc569580029cb9d42b9 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Thu, 28 Apr 2022 09:47:48 +0100 Subject: [PATCH] Add Pack/Unpack bits Allows a bool list (no more than 64 in length) to be packed to an integer, which can be unpacked again --- X10D.Tests/src/Collections/BoolListTests.cs | 56 +++++++++ X10D.Tests/src/Collections/ByteTests.cs | 57 +++++++++ X10D.Tests/src/Collections/Int16Tests.cs | 67 +++++++++++ X10D.Tests/src/Collections/Int32Tests.cs | 67 +++++++++++ X10D.Tests/src/Collections/Int64Tests.cs | 72 ++++++++++++ X10D/src/Collections/BoolListExtensions.cs | 123 ++++++++++++++++++++ X10D/src/Collections/ByteExtensions.cs | 44 +++++++ X10D/src/Collections/Int16Extensions.cs | 40 +++++++ X10D/src/Collections/Int32Extensions.cs | 40 +++++++ X10D/src/Collections/Int64Extensions.cs | 40 +++++++ 10 files changed, 606 insertions(+) create mode 100644 X10D.Tests/src/Collections/BoolListTests.cs create mode 100644 X10D.Tests/src/Collections/ByteTests.cs create mode 100644 X10D.Tests/src/Collections/Int16Tests.cs create mode 100644 X10D.Tests/src/Collections/Int32Tests.cs create mode 100644 X10D.Tests/src/Collections/Int64Tests.cs create mode 100644 X10D/src/Collections/BoolListExtensions.cs create mode 100644 X10D/src/Collections/ByteExtensions.cs create mode 100644 X10D/src/Collections/Int16Extensions.cs create mode 100644 X10D/src/Collections/Int32Extensions.cs create mode 100644 X10D/src/Collections/Int64Extensions.cs diff --git a/X10D.Tests/src/Collections/BoolListTests.cs b/X10D.Tests/src/Collections/BoolListTests.cs new file mode 100644 index 0000000..e844c4e --- /dev/null +++ b/X10D.Tests/src/Collections/BoolListTests.cs @@ -0,0 +1,56 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +[TestClass] +public class BoolListTests +{ + [TestMethod] + public void Pack8Bit_Should_Pack_Correctly() + { + var array = new[] {true, false, true, false, true, false, true, false}; + Assert.AreEqual(85, array.Pack8Bit()); // 01010101 + } + + [TestMethod] + public void Pack16Bit_Should_Pack_Correctly() + { + var array = new[] {true, false, true, false, true, false, true, false, true, true, false, true}; + Assert.AreEqual(2901, array.Pack16Bit()); // 101101010101 + } + + [TestMethod] + public void Pack32Bit_Should_Pack_Correctly() + { + var array = new[] {true, false, true, false, true, false, true, false, true, true, false, true}; + Assert.AreEqual(2901, array.Pack32Bit()); // 101101010101 + } + + [TestMethod] + public void Pack64Bit_Should_Pack_Correctly() + { + var array = new[] {true, false, true, false, true, false, true, false, true, true, false, true}; + Assert.AreEqual(2901, array.Pack64Bit()); // 101101010101 + } + + [TestMethod] + public void Pack_ShouldThrow_GivenLargeArray() + { + bool[] array = Enumerable.Repeat(false, 65).ToArray(); + Assert.ThrowsException(() => array.Pack8Bit()); + Assert.ThrowsException(() => array.Pack16Bit()); + Assert.ThrowsException(() => array.Pack32Bit()); + Assert.ThrowsException(() => array.Pack64Bit()); + } + + [TestMethod] + public void Pack_ShouldThrow_GivenNull() + { + bool[]? array = null; + Assert.ThrowsException(() => array!.Pack8Bit()); + Assert.ThrowsException(() => array!.Pack16Bit()); + Assert.ThrowsException(() => array!.Pack32Bit()); + Assert.ThrowsException(() => array!.Pack64Bit()); + } +} diff --git a/X10D.Tests/src/Collections/ByteTests.cs b/X10D.Tests/src/Collections/ByteTests.cs new file mode 100644 index 0000000..90a5db6 --- /dev/null +++ b/X10D.Tests/src/Collections/ByteTests.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +[TestClass] +public class ByteTests +{ + [TestMethod] + public void UnpackBits_ShouldUnpackToArrayCorrectly() + { + bool[] bits = ((byte)0b11010100).UnpackBits(); + + Assert.AreEqual(8, bits.Length); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + } + + [TestMethod] + public void UnpackBits_ShouldUnpackToSpanCorrectly() + { + Span bits = stackalloc bool[8]; + ((byte)0b11010100).UnpackBits(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + } + + [TestMethod] + public void UnpackBits_ShouldRepackEqually() + { + Assert.AreEqual(0b11010100, ((byte)0b11010100).UnpackBits().Pack8Bit()); + } + + [TestMethod] + public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + { + Assert.ThrowsException(() => + { + Span bits = stackalloc bool[0]; + ((byte)0b11010100).UnpackBits(bits); + }); + } +} diff --git a/X10D.Tests/src/Collections/Int16Tests.cs b/X10D.Tests/src/Collections/Int16Tests.cs new file mode 100644 index 0000000..b2e526f --- /dev/null +++ b/X10D.Tests/src/Collections/Int16Tests.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +[TestClass] +public class Int16Tests +{ + [TestMethod] + public void UnpackBits_ShouldUnpackToArrayCorrectly() + { + bool[] bits = ((short)0b11010100).UnpackBits(); + + Assert.AreEqual(16, bits.Length); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 16; index++) + { + Assert.IsFalse(bits[index]); + } + } + + [TestMethod] + public void UnpackBits_ShouldUnpackToSpanCorrectly() + { + Span bits = stackalloc bool[16]; + ((short)0b11010100).UnpackBits(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 16; index++) + { + Assert.IsFalse(bits[index]); + } + } + + [TestMethod] + public void UnpackBits_ShouldRepackEqually() + { + Assert.AreEqual(0b11010100, ((short)0b11010100).UnpackBits().Pack16Bit()); + } + + [TestMethod] + public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + { + Assert.ThrowsException(() => + { + Span bits = stackalloc bool[0]; + ((short)0b11010100).UnpackBits(bits); + }); + } +} diff --git a/X10D.Tests/src/Collections/Int32Tests.cs b/X10D.Tests/src/Collections/Int32Tests.cs new file mode 100644 index 0000000..3dd6b0d --- /dev/null +++ b/X10D.Tests/src/Collections/Int32Tests.cs @@ -0,0 +1,67 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; + +namespace X10D.Tests.Collections; + +[TestClass] +public class Int32Tests +{ + [TestMethod] + public void UnpackBits_ShouldUnpackToArrayCorrectly() + { + bool[] bits = 0b11010100.UnpackBits(); + + Assert.AreEqual(32, bits.Length); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 32; index++) + { + Assert.IsFalse(bits[index]); + } + } + + [TestMethod] + public void UnpackBits_ShouldUnpackToSpanCorrectly() + { + Span bits = stackalloc bool[32]; + 0b11010100.UnpackBits(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 32; index++) + { + Assert.IsFalse(bits[index]); + } + } + + [TestMethod] + public void UnpackBits_ShouldRepackEqually() + { + Assert.AreEqual(0b11010100, 0b11010100.UnpackBits().Pack32Bit()); + } + + [TestMethod] + public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + { + Assert.ThrowsException(() => + { + Span bits = stackalloc bool[0]; + 0b11010100.UnpackBits(bits); + }); + } +} diff --git a/X10D.Tests/src/Collections/Int64Tests.cs b/X10D.Tests/src/Collections/Int64Tests.cs new file mode 100644 index 0000000..31c3b6e --- /dev/null +++ b/X10D.Tests/src/Collections/Int64Tests.cs @@ -0,0 +1,72 @@ +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Collections; +using X10D.Linq; + +namespace X10D.Tests.Collections; + +[TestClass] +public class Int64Tests +{ + [TestMethod] + public void UnpackBits_ShouldUnpackToArrayCorrectly() + { + bool[] bits = 0b11010100L.UnpackBits(); + + Assert.AreEqual(64, bits.Length); + + Trace.WriteLine(Convert.ToString(0b11010100L, 2)); + Trace.WriteLine(string.Join("", bits.Select(b => b ? "1" : "0"))); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 64; index++) + { + Assert.IsFalse(bits[index], index.ToString()); + } + } + + [TestMethod] + public void UnpackBits_ShouldUnpackToSpanCorrectly() + { + Span bits = stackalloc bool[64]; + 0b11010100L.UnpackBits(bits); + + Assert.IsFalse(bits[0]); + Assert.IsFalse(bits[1]); + Assert.IsTrue(bits[2]); + Assert.IsFalse(bits[3]); + Assert.IsTrue(bits[4]); + Assert.IsFalse(bits[5]); + Assert.IsTrue(bits[6]); + Assert.IsTrue(bits[7]); + + for (var index = 8; index < 64; index++) + { + Assert.IsFalse(bits[index], index.ToString()); + } + } + + [TestMethod] + public void UnpackBits_ShouldRepackEqually() + { + Assert.AreEqual(0b11010100L, 0b11010100L.UnpackBits().Pack64Bit()); + } + + [TestMethod] + public void UnpackBits_ShouldThrow_GivenTooSmallSpan() + { + Assert.ThrowsException(() => + { + Span bits = stackalloc bool[0]; + 0b11010100L.UnpackBits(bits); + }); + } +} diff --git a/X10D/src/Collections/BoolListExtensions.cs b/X10D/src/Collections/BoolListExtensions.cs new file mode 100644 index 0000000..4fcfcbb --- /dev/null +++ b/X10D/src/Collections/BoolListExtensions.cs @@ -0,0 +1,123 @@ +namespace X10D.Collections; + +/// +/// Collection-related extension methods for of . +/// +public static class BoolListExtensions +{ + /// + /// Packs a collection of booleans into a . + /// + /// The collection of booleans to pack. + /// An 8-bit unsigned integer containing the packed booleans. + /// is . + /// contains more than 8 elements. + /// Alpha Anar + public static byte Pack8Bit(this IReadOnlyList source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.Count > 8) + { + throw new ArgumentException("Source cannot contain more than than 8 elements.", nameof(source)); + } + + unsafe + { + fixed (bool* p = source.ToArray()) + { + return (byte)(*(ulong*)p * 72624976668147840L >> 56); // evil fucking bit hack + } + } + } + + /// + /// Packs a collection of booleans into a . + /// + /// The collection of booleans to pack. + /// A 16-bit signed integer containing the packed booleans. + /// is . + /// contains more than 16 elements. + public static short Pack16Bit(this IReadOnlyList source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.Count > 16) + { + throw new ArgumentException("Source cannot contain more than than 16 elements.", nameof(source)); + } + + short result = 0; + + for (var i = 0; i < source.Count; i++) + { + result |= (short)(source[i] ? 1 << i : 0); + } + + return result; + } + + /// + /// Packs a collection of booleans into a . + /// + /// The collection of booleans to pack. + /// A 32-bit signed integer containing the packed booleans. + /// is . + /// contains more than 32 elements. + public static int Pack32Bit(this IReadOnlyList source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.Count > 32) + { + throw new ArgumentException("Source cannot contain more than than 32 elements.", nameof(source)); + } + + var result = 0; + + for (var i = 0; i < source.Count; i++) + { + result |= source[i] ? 1 << i : 0; + } + + return result; + } + + /// + /// Packs a collection of booleans into a . + /// + /// The collection of booleans to pack. + /// A 64-bit signed integer containing the packed booleans. + /// is . + /// contains more than 64 elements. + public static long Pack64Bit(this IReadOnlyList source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.Count > 64) + { + throw new ArgumentException("Source cannot contain more than than 64 elements.", nameof(source)); + } + + var result = 0L; + + for (var i = 0; i < source.Count; i++) + { + result |= source[i] ? 1L << i : 0; + } + + return result; + } +} diff --git a/X10D/src/Collections/ByteExtensions.cs b/X10D/src/Collections/ByteExtensions.cs new file mode 100644 index 0000000..9c621f2 --- /dev/null +++ b/X10D/src/Collections/ByteExtensions.cs @@ -0,0 +1,44 @@ +namespace X10D.Collections; + +/// +/// Collection-related extension methods for . +/// +public static class ByteExtensions +{ + private const int Size = sizeof(byte) * 8; + + /// + /// Unpacks this 8-bit unsigned integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// An array of with length 8. + public static bool[] UnpackBits(this byte value) + { + Span buffer = stackalloc bool[Size]; + value.UnpackBits(buffer); + return buffer.ToArray(); + } + + /// + /// Unpacks this 8-bit unsigned integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// When this method returns, contains the unpacked booleans from . + /// is not large enough to contain the result. + /// Alpha Anar + public static void UnpackBits(this byte value, Span destination) + { + if (destination.Length < Size) + { + throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + } + + unsafe + { + fixed (bool* p = destination) + { + *(ulong*)p = ((ulong)value * 567382630219905) & 72340172838076673UL; // evil fucking bit hack + } + } + } +} diff --git a/X10D/src/Collections/Int16Extensions.cs b/X10D/src/Collections/Int16Extensions.cs new file mode 100644 index 0000000..3b72c16 --- /dev/null +++ b/X10D/src/Collections/Int16Extensions.cs @@ -0,0 +1,40 @@ +namespace X10D.Collections; + +/// +/// Collection-related extension methods for . +/// +public static class Int16Extensions +{ + private const int Size = sizeof(short) * 8; + + /// + /// Unpacks this 16-bit signed integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// An array of with length 16. + public static bool[] UnpackBits(this short value) + { + Span buffer = stackalloc bool[Size]; + value.UnpackBits(buffer); + return buffer.ToArray(); + } + + /// + /// Unpacks this 16-bit signed integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// When this method returns, contains the unpacked booleans from . + /// is not large enough to contain the result. + public static void UnpackBits(this short value, Span destination) + { + if (destination.Length < Size) + { + throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + } + + for (var index = 0; index < Size; index++) + { + destination[index] = (value & (1 << index)) != 0; + } + } +} diff --git a/X10D/src/Collections/Int32Extensions.cs b/X10D/src/Collections/Int32Extensions.cs new file mode 100644 index 0000000..b61c54e --- /dev/null +++ b/X10D/src/Collections/Int32Extensions.cs @@ -0,0 +1,40 @@ +namespace X10D.Collections; + +/// +/// Collection-related extension methods for . +/// +public static class Int32Extensions +{ + private const int Size = sizeof(int) * 8; + + /// + /// Unpacks this 32-bit signed integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// An array of with length 32. + public static bool[] UnpackBits(this int value) + { + Span buffer = stackalloc bool[Size]; + value.UnpackBits(buffer); + return buffer.ToArray(); + } + + /// + /// Unpacks this 32-bit signed integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// When this method returns, contains the unpacked booleans from . + /// is not large enough to contain the result. + public static void UnpackBits(this int value, Span destination) + { + if (destination.Length < Size) + { + throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + } + + for (var index = 0; index < Size; index++) + { + destination[index] = (value & (1 << index)) != 0; + } + } +} diff --git a/X10D/src/Collections/Int64Extensions.cs b/X10D/src/Collections/Int64Extensions.cs new file mode 100644 index 0000000..848d4e2 --- /dev/null +++ b/X10D/src/Collections/Int64Extensions.cs @@ -0,0 +1,40 @@ +namespace X10D.Collections; + +/// +/// Collection-related extension methods for . +/// +public static class Int64Extensions +{ + private const int Size = sizeof(long) * 8; + + /// + /// Unpacks this 64-bit signed integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// An array of with length 64. + public static bool[] UnpackBits(this long value) + { + Span buffer = stackalloc bool[Size]; + value.UnpackBits(buffer); + return buffer.ToArray(); + } + + /// + /// Unpacks this 64-bit signed integer into a boolean list, treating it as a bit field. + /// + /// The value to unpack. + /// When this method returns, contains the unpacked booleans from . + /// is not large enough to contain the result. + public static void UnpackBits(this long value, Span destination) + { + if (destination.Length < Size) + { + throw new ArgumentException($"Destination must be at least {Size} in length.", nameof(destination)); + } + + for (var index = 0; index < Size; index++) + { + destination[index] = (value & (1L << index)) != 0; + } + } +}