C# – Hex string to byte array

This article shows code for converting a hex string to a byte array, unit tests, and a speed comparison.

First, this diagram shows the algorithm for converting a hex string to a byte array.

To convert a hex string to a byte array, you need to loop through the hex string and convert two characters to one byte at a time. This is because each hex character represents half a byte.

Hex string to byte array code

The following code converts a hex string to a byte array. It uses a lookup + bit shift approach.

It checks the hex string input for errors, handles mixed character casing, and skips over the starting “0x” if it exists. I believe it’s very important to always check for error conditions and to handle corner cases.

public static class HexUtil
{
	private readonly static Dictionary<char, byte> hexmap = new Dictionary<char, byte>()
	{
		{ 'a', 0xA },{ 'b', 0xB },{ 'c', 0xC },{ 'd', 0xD },
		{ 'e', 0xE },{ 'f', 0xF },{ 'A', 0xA },{ 'B', 0xB },
		{ 'C', 0xC },{ 'D', 0xD },{ 'E', 0xE },{ 'F', 0xF },
		{ '0', 0x0 },{ '1', 0x1 },{ '2', 0x2 },{ '3', 0x3 },
		{ '4', 0x4 },{ '5', 0x5 },{ '6', 0x6 },{ '7', 0x7 },
		{ '8', 0x8 },{ '9', 0x9 }
	};
	public static byte[] ToBytes(this string hex)
	{
		if (string.IsNullOrWhiteSpace(hex))
			throw new ArgumentException("Hex cannot be null/empty/whitespace");

		if (hex.Length % 2 != 0)
			throw new FormatException("Hex must have an even number of characters");

		bool startsWithHexStart = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase);

		if (startsWithHexStart && hex.Length == 2)
			throw new ArgumentException("There are no characters in the hex string");


		int startIndex = startsWithHexStart ? 2 : 0;

		byte[] bytesArr = new byte[(hex.Length - startIndex) / 2];

		char left;
		char right;

		try 
		{ 
			int x = 0;
			for(int i = startIndex; i < hex.Length; i += 2, x++)
			{
				left = hex[i];
				right = hex[i + 1];
				bytesArr[x] = (byte)((hexmap[left] << 4) | hexmap[right]);
			}
			return bytesArr;
		}
		catch(KeyNotFoundException)
		{
			throw new FormatException("Hex string has non-hex character");
		}
	}
}

Hex string to byte array tests

Here are the unit tests that verify the code handles error cases, handles different input formats, and correctly converts the hex to a byte array.

  • using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass()]
public class HexUtilTests
{
	[DataRow(null)]
	[DataRow("")]
	[DataRow(" ")]
	[DataTestMethod()]
	public void HexToByteArray_WhenNullEmptyOrWhitespace_ThrowsArgumentException(string hex)
	{
		//act & assert
		Assert.ThrowsException<ArgumentException>(() => hex.ToBytes());
	}
	[TestMethod()]
	public void HexToByteArray_WhenOddLength_ThrowsFormatException()
	{
		//arrange
		string hex = "A";

		//act & assert
		Assert.ThrowsException<FormatException>(() =>hex.ToBytes());
	}
	[DataRow("0x")]
	[DataRow("0X")]
	[DataTestMethod()]
	public void HexToByteArray_WhenStartsWithHexStart_AndNoDigitsAfter_ThrowsArgumentException(string hex)
	{
		//act && assert
		Assert.ThrowsException<ArgumentException>(() => hex.ToBytes());
	}
	[TestMethod]
	public void HexToByteArray_WhenHasUpperCaseLetters_ConvertsThemToBytes()
	{
		//arrange
		string hex = "ABCDEF";
		byte[] expected = new byte[]
		{
			0xAB,
			0xCD,
			0xEF
		};

		//act
		var actual = hex.ToBytes();

		//arrange
		CollectionAssert.AreEqual(expected, actual);

	}
	[DataRow("AM")]
	[DataRow("A!")]
	[TestMethod()]
	public void HexToByteArray_WhenHasInvalidHexCharacter_ThrowsFormatException(string hex)
	{
		//act && assert
		Assert.ThrowsException<FormatException>(() => hex.ToBytes());
	}
	[DataRow("0xab")]
	[DataRow("0Xab")]
	[DataRow("ab")]
	[TestMethod()]
	public void HexToByteArray_WhenHasLowercaseHexCharacters_ReturnsByteArray(string hex)
	{
		//arrange
		byte[] expected = new byte[] { 0xAB };

		//act
		var actual = hex.ToBytes();

		//act && assert
		CollectionAssert.AreEqual(expected, actual);
	}
	[DataRow("0xAB")]
	[DataRow("0XAB")]
	[DataRow("AB")]
	[TestMethod()]
	public void HexToByteArray_WhenHasUppercaseHexCharacters_ReturnsByteArray(string hex)
	{
		//arrange
		byte[] expected = new byte[] { 0xAB };

		//act
		var actual = hex.ToBytes();

		//act && assert
		CollectionAssert.AreEqual(expected, actual);
	}
	[DataRow("0x12")]
	[DataRow("0X12")]
	[DataRow("12")]
	[TestMethod()]
	public void HexToByteArray_WhenHasDigits_ReturnsByteArray(string hex)
	{
		//arrange
		byte[] expected = new byte[] { 0x12 };

		//act
		var actual = hex.ToBytes();

		//act && assert
		CollectionAssert.AreEqual(expected, actual);
	}
}

Speed comparison – Lookup/shift vs Linq

I compared this code with a one-line Linq approach (which has no error handling).

I generated a random hex string with mixed casing, then ran the two converters 10 times. Here are the average times in milliseconds for each input size.

32 chars320 chars3,200 chars32,000 chars320,000 chars3,200,000 chars
Lookup/shift0.0007 ms0.013 ms0.056 ms0.428 ms5 ms41 ms
Linq0.0043 ms0.049 ms0.121 ms1.173 ms13.4 ms103 ms

The lookup/shift approach is typically 2.5x faster than the Linq approach, even at lower input sizes.

Generating random hex strings

The following code generates a random hex string with mixed casing. You specify how many iterations, and the end result is a hex string with 32 characters for each iteration. In other words, if you specify 100,000, it’ll generate a hex string with 3,200,000 characters.

var randomHex = string.Join("", Enumerable.Range(0, 100_000).Select(t =>
{
	var guidHex = Guid.NewGuid().ToString().Replace("-", "");

	return t % 2 == 0 ? guidHex : guidHex.ToUpper();
}));

Leave a Comment