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.
Table of Contents
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 initializes a dictionary with character keys and hex byte values.
- Checks the hex string input for errors.
- Loops through the string, getting two characters at a time:
- Converts each character to its hex half-byte.
- Combines the two half-bytes into one byte (by doing a bitwise left-shift).
- Adds the byte to an array.
Here is the code:
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");
}
}
}
Code language: C# (cs)
Hex string to byte array tests
Here are the parameterized 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);
}
}
Code language: C# (cs)
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 chars | 320 chars | 3,200 chars | 32,000 chars | 320,000 chars | 3,200,000 chars | |
Lookup/shift | 0.0007 ms | 0.013 ms | 0.056 ms | 0.428 ms | 5 ms | 41 ms |
Linq | 0.0043 ms | 0.049 ms | 0.121 ms | 1.173 ms | 13.4 ms | 103 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();
}));
Code language: C# (cs)