1. Overview
Hexadecimal representation is widely used for its human readability and compactness, making it an efficient and straightforward way to represent binary data in Kotlin. For this reason, Kotlin has introduced the HexFormat API as a convenience package for formatting data into hexadecimal string form and parsing it back.
In this tutorial, we’ll explore the HexFormat API and solve some common use cases involving hexadecimal representation.
2. Basics
Before exploring the usage of the HexFormat API, let’s learn its basics to build a strong foundation for the associated concepts.
2.1. The HexFormat Class
The HexFormat class in the kotlin.text package of the standard library represents the overall configuration of hexadecimal formatting. Let’s have a quick look at its definition:
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public class HexFormat internal constructor(
val upperCase: Boolean,
val bytes: BytesHexFormat,
val number: NumberHexFormat
) {
// Additional methods and properties
}
We must note that the @ExperimentalStdlibApi annotation marks it as experimental, so we must add the @OptIn(ExperimentalStdlibApi::class) annotation with the caller methods or classes.
Further, it contains three properties, namely, upperCase, bytes, and number, to specify different formatting options for hexadecimal representation. While upperCase applies for both types, the bytes and number properties are specific to the respective types.
2.2. NumberHexFormat and BytesHexFormat
The NumberHexFormat class in the HexFormat API defines formatting options for numeric values. Let’s look at the NumberHexFormat class to understand it in more detail:
public class NumberHexFormat internal constructor(
val prefix: String,
val suffix: String,
val removeLeadingZeros: Boolean
) {
// Additional methods and properties
}
We can specify the prefix and suffix strings while formatting numeric values. Additionally, we can trim the leading zeros using the removeLeadingZeros option.
Further, let’s see the BytesHexFormat class that plays a crucial role in defining the formatting options for ByteArray values:
public class BytesHexFormat internal constructor(
val bytesPerLine: Int,
val bytesPerGroup: Int,
val groupSeparator: String,
val byteSeparator: String,
val bytePrefix: String,
val byteSuffix: String
) {
// Additional methods and properties
}
It provides several options, such as bytesPerLine, bytesPerGroup, groupSeparator, and so on, to customize the hexadecimal formatting and control its layout.
Now that we’ve discussed the fundamentals, we’re ready to get our hands dirty and explore a few common use cases for the HexFormat API.
3. Formatting Numbers to Hexadecimal Values
In this section, we’ll learn how to format different number types to hexadecimal strings using the HexFormat API.
3.1. Default HexFormat
Let’s start by initializing the number variable of the Byte type:
val number: Byte = 63
Now, we can use the toHexString() extension function to format the numeric value to a hexadecimal string:
assertEquals("3f", number.toHexString())
In this case, we didn’t explicitly use an instance of HexFormat, so Kotlin used the default instance internally while applying the formatting.
Alternatively, we can explicitly pass the HexFormat.Default instance as a parameter to the toHexString() extension function:
assertEquals("3f", number.toHexString(HexFormat.Default))
As expected, we’ve got the same output.
Lastly, it’s important to note that we can apply the toHexString() extension function to format other numeric types, such as Short, Int, and Long.
3.2. Adding a Prefix
For some use cases, such as URL encoding, we add a prefix before the hexadecimal representation of the character’s ASCII value.
Let’s create prefixNumberFormat as an instance of the HexFormat class:
val prefixNumberFormat = HexFormat {
number.prefix = "%"
}
We’ve initialized the number.prefix property with the % character.
Now, we can call the toHexString() extension function to format the number variable into its hexadecimal representation:
assertEquals("%3f", number.toHexString(prefixNumberFormat))
We can notice the % character added as a prefix to the hexadecimal string.
3.3. Removing Leading Zeros
First, let’s initialize the number variable as a Long type:
val number: Long = 63
Now, let’s convert it to its hexadecimal representation using the toHexString() extension function:
assertEquals("000000000000003f", number.toHexString())
We can notice 14 zeros at the beginning because Long reserves 8 bytes for storage, while the value 63 takes only 2 bytes for its hex string.
Next, let’s create the prefixRemoveLeadingZerosNumberFormat instance of the HexFormat class:
val prefixRemoveLeadingZerosNumberFormat = HexFormat {
number {
removeLeadingZeros = true
}
}
We’ve set the removeLeadingZeros property to true.
Finally, let’s use the prefixRemoveLeadingZerosNumberFormat HexFormat to represent number in its hexadecimal value:
assertEquals("3f", number.toHexString(prefixRemoveLeadingZerosNumberFormat))
Great! There are no more leading zeros in the output.
4. Parsing Hexadecimal Values to Numbers
In this section, let’s learn how to parse hexadecimal values to get numeric values using the hexTo
Let’s use the hexToByte() extension function to convert the “3f” hexadecimal string to a numeric value in Byte:
assertEquals(63, "3f".hexToByte())
We got the correct result.
However, if we try to convert a hexadecimal string with an additional prefix, such as “*%3f*“, then we get the IllegalArgumentException:
assertThrows<IllegalArgumentException> { "%3f".hexToByte() }
We notice this behavior because, in the absence of a HexFormat instance, hexToByte() uses HexFormat.Default.
Lastly, let’s use the prefixNumberFormat instance of HexFormat with hexToByte() for converting the “*%3f*” hex string to a Byte value:
assertEquals(63, "%3f".hexToByte(prefixNumberFormat))
Perfect! We got it right this time.
5. Formatting ByteArray to Hexadecimal Values
In this section, let’s explore an interesting use case for formatting the byte representation of a MAC address to its hexadecimal representation.
Let’s create an instance of the HexFormat class named macAddressFormat:
val macAddressFormat = HexFormat {
upperCase = true
bytes.bytesPerGroup = 1
bytes.groupSeparator = ":"
}
We’ve set the bytes.bytesPerGroup and bytes.groupSeparator properties to 1 and : respectively. Additionally, we’ve set the upperCase property to true.
Now, let’s create the macAddressBytes ByteArray using the byteArrayOf() factory function:
val macAddressBytes = byteArrayOf(2, 66, -64, -117, -14, 94)
It’s interesting to note that a few values are negative because the range of signed bytes is -128 to 127.
Further, let’s use the toHexString() extension function to convert the macAddressBytes into its hexadecimal representation:
val macAddressHexValue = macAddressBytes.toHexString(macAddressFormat)
Lastly, we must test our approach:
assertEquals("02:42:C0:8B:F2:5E", macAddressHexValue)
Great! The MAC address is formatted correctly.
6. Parsing Hexadecimal Values to ByteArray
We already learned how to represent the MAC address bytes into its hexadecimal value. Now, let’s solve the use case of parsing the hex string of a MAC address into a ByteArray.
First, let’s initialize the macAddressHexValue with a sample MAC address hex string:
val macAddressHexValue = "02:42:C0:8B:F2:5E"
Next, let’s call the hexToByteArray() extension function with the macAddressFormat instance of the HexFormat class:
val macAddressBytes = macAddressHexValue.hexToByteArray(macAddressFormat)
Since the hex string is in a specific format, we passed the macAddressFormat. Otherwise, it’ll throw the NumberFormatException exception.
Finally, let’s confirm that we’ve got all the bytes in the correct order:
assertArrayEquals(byteArrayOf(2, 66, -64, -117, -14, 94), macAddressBytes)
Fantastic! It looks like we nailed this one.
7. Conclusion
In this article, we learned about the HexFormat API in Kotlin. Further, we explored the toHexString() and hexTo
As always, the code from this article is available over on GitHub.