Skip to content

Instantly share code, notes, and snippets.

@fabiomsr
Last active April 24, 2024 08:41
Show Gist options
  • Star 65 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save fabiomsr/845664a9c7e92bafb6fb0ca70d4e44fd to your computer and use it in GitHub Desktop.
Save fabiomsr/845664a9c7e92bafb6fb0ca70d4e44fd to your computer and use it in GitHub Desktop.
ByteArray and String extension to add hexadecimal methods in Kotlin
private val HEX_CHARS = "0123456789ABCDEF".toCharArray()
fun ByteArray.toHex() : String{
val result = StringBuffer()
forEach {
val octet = it.toInt()
val firstIndex = (octet and 0xF0).ushr(4)
val secondIndex = octet and 0x0F
result.append(HEX_CHARS[firstIndex])
result.append(HEX_CHARS[secondIndex])
}
return result.toString()
}
private val HEX_CHARS = "0123456789ABCDEF"
fun String.hexStringToByteArray() : ByteArray {
val result = ByteArray(length / 2)
for (i in 0 until length step 2) {
val firstIndex = HEX_CHARS.indexOf(this[i]);
val secondIndex = HEX_CHARS.indexOf(this[i + 1]);
val octet = firstIndex.shl(4).or(secondIndex)
result.set(i.shr(1), octet.toByte())
}
return result
}
@32bitkid
Copy link

probably not the best performance, but if you are looking for one-liners:

fun ByteArray.toHex() = this.joinToString(separator = "") { it.toInt().and(0xff).toString(16).padStart(2, '0') }
fun String.hexStringToByteArray() = ByteArray(this.length / 2) { this.substring(it * 2, it * 2 + 2).toInt(16).toByte() }

@Sloy
Copy link

Sloy commented Jan 5, 2018

Be careful, it only accepts uppercase strings.
It took me a half an hour to realize that my problem was there 🤦‍♂️

@dkowis
Copy link

dkowis commented Aug 22, 2018

Another one liner:

byteArray.map { String.format("%02X", (it.toInt() and 0xFF)) }.joinToString(separator = "")

@kspar
Copy link

kspar commented Nov 10, 2018

Or

byteArray.joinToString("") { String.format("%02X", (it.toInt() and 0xFF)) }

@dfpalomar
Copy link

I would also add some validation

if (firstIndex < 0) throw Exception("${this[i]} is not a valid Hex value")

same with secondIndex

@ZenLiuCN
Copy link

ZenLiuCN commented Feb 14, 2019

	fun ByteArray.toHexString()=this.joinToString(""){ String.format("%02X",(it.toInt() and 0xFF)) }
	fun String.byteArrayFromHexString()=this.chunked(2).map { it.toInt(16).toByte() }.toByteArray()

update for

//will handle Upper or Lower sequence
fun String.byteArrayFromHexString()=this.chunked(2).map { it.toUpperCase().toInt(16).toByte() }.toByteArray()

I more like this,inline or not take easy

val ByteArray.asHexLower inline get() = this.joinToString(separator = ""){ String.format("%02x",(it.toInt() and 0xFF))}
val ByteArray.asHexUpper inline get() = this.joinToString(separator = ""){ String.format("%02X",(it.toInt() and 0xFF))}
val String.hexAsByteArray inline get() = this.chunked(2).map { it.toUpperCase().toInt(16).toByte() }.toByteArray()

if make it more safe
I realy suggestion that better not throw Exception that not meanful,return null is may better choice

val String.tryHexAsByteArray inline get() = try{this.chunked(2).map { it.toUpperCase().toInt(16).toByte() }.toByteArray()}catch(e:Throwable){null}
//could be used as
func someAction(src:String){
    val bytes=src.tryHexAsByteArray?:throw Exception("not valid HEX string")
   //... do something with ByteArray
}

@gmk57
Copy link

gmk57 commented Feb 19, 2020

What's wrong with byteArray.joinToString("") { "%02X".format(it) }? It seems to produce exactly the same results as the other proposed answers. Is (it.toInt() and 0xFF) really necessary?

@ZenLiuCN
Copy link

@gmk57
a full testing

	val HEX_CHARS = "0123456789abcdef"
		val toHEX = { b: ByteArray ->
			val result = StringBuffer()
			b.forEach {
				val octet = it.toInt()
				val firstIndex = (octet and 0xF0).ushr(4)
				val secondIndex = octet and 0x0F
				result.append(HEX_CHARS[firstIndex])
				result.append(HEX_CHARS[secondIndex])
			}
			result.toString()
		}
		val toHEX2 = { b: ByteArray ->
			buildString {
				b.forEach {
					val octet = it.toInt()
					val firstIndex = (octet and 0xF0).ushr(4)
					val secondIndex = octet and 0x0F
					append(HEX_CHARS[firstIndex])
					append(HEX_CHARS[secondIndex])
				}
			}

		}
		val source = "zzhz中午".toByteArray()
		val measureOp = { op: String, act: () -> String ->
			measureNanoTime {
				(0..1000000).forEach {
					act()
				}
			}.apply {
				println("$op: ${this / 1000000.0 / 1000.0 / 8} ms/op/byte")
			}
		}
		source.joinToString("") { String.format("%02x", it) }.apply(::println)
		//7a7a687ae4b8ade58d88 good
		source.joinToString("") { it.toString(16).padStart(2, '0') }.apply(::println)
		//7a7a687a-1c-48-53-1b-73-78 not good
		source.joinToString("") { it.toInt().and(0xff).toString(16).padStart(2, '0') }.apply(::println)
		//7a7a687ae4b8ade58d88 good
		toHEX(source).apply(::println)
		//7a7a687ae4b8ade58d88 good
		toHEX2(source).apply(::println)
		//7a7a687ae4b8ade58d88 good

		mapOf(
			"toHEX" to { toHEX(source) },
			"toHEX2" to { toHEX2(source) },
			"format" to { source.joinToString("") { String.format("%02x", it) } },
			"pad" to { source.joinToString("") { it.toInt().and(0xff).toString(16).padStart(2, '0') } }
		).forEach { o, a -> measureOp(o, a) }

and result show

toHEX: 0.018858339749999998 ms/op/byte
toHEX2: 0.012625058 ms/op/byte
format: 0.723910916375 ms/op/byte
pad: 0.042981906875 ms/op/byte

current speed winner is

{ b: ByteArray ->
			buildString {
				b.forEach {
					val octet = it.toInt()
					val firstIndex = (octet and 0xF0).ushr(4)
					val secondIndex = octet and 0x0F
					append(HEX_CHARS[firstIndex])
					append(HEX_CHARS[secondIndex])
				}
			}
		}

and avg winner is

source.joinToString("") { it.toInt().and(0xff).toString(16).padStart(2, '0') }

@ZenLiuCN
Copy link

recover test

	@Test
	fun fromHEX() {
		val hex = "7a7a687ae4b8ade58d88"
		val f1 = { hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() }
		val f2 = {
			val len = hex.length
			val result = ByteArray(len / 2)
			for (i in 0 until len step 2) {
				val firstIndex = HEX_CHARS.indexOf(hex[i])
				val secondIndex = HEX_CHARS.indexOf(hex[i + 1])
				val octet = firstIndex.shl(4).or(secondIndex)
				result[i.shr(1)] = octet.toByte()
			}
			result
		}
		val f3 = {
			val len = hex.length
			val result = ByteArray(len / 2)
			(0 until len step 2).forEach { i ->
				val firstIndex = HEX_CHARS.indexOf(hex[i])
				val secondIndex = HEX_CHARS.indexOf(hex[i + 1])
				val octet = firstIndex.shl(4).or(secondIndex)
				result[i.shr(1)] = octet.toByte()
			}
			result
		}
		val f4 = {
			val len = hex.length
			val result = ByteArray(len / 2)
			(0 until len step 2).forEach { i ->
				result[i.shr(1)] = HEX_CHARS.indexOf(hex[i]).shl(4).or(HEX_CHARS.indexOf(hex[i + 1])).toByte()
			}
			result
		}
		val f5 = {
			(hex.indices step 2).map { i ->
				HEX_CHARS.indexOf(hex[i]).shl(4).or(HEX_CHARS.indexOf(hex[i + 1])).toByte()
			}.toByteArray()
		}
		f1().apply(print)
		f2().apply(print)
		f3().apply(print)
		f4().apply(print)
		f5().apply(print)
		mapOf(
			"f1" to f1
			, "f2" to f2
			, "f3" to f3
			, "f4" to f4
			, "f5" to f5
		).forEach { o, a -> measureOp(o, a) }
	}
f1: 0.480646209 ms/op
f2: 0.126997772 ms/op
f3: 0.113088663 ms/op
f4: 0.098126381 ms/op
f5: 0.201921568 ms/op

@todorus
Copy link

todorus commented Mar 5, 2021

For people commenting alternatives to the gist, bear in mind that the gist is in Kotlin. The API does not have String.format, as this is from the Kotlin JVM lib. The posted examples won't work in Kotlin Native.

Copy link

ghost commented Jul 29, 2021

For people commenting alternatives to the gist, bear in mind that the gist is in Kotlin. The API does not have String.format, as this is from the Kotlin JVM lib. The posted examples won't work in Kotlin Native.

source.joinToString("") { it.toInt().and(0xff).toString(16).padStart(2, '0') }

works with Kotlin/Native at least for the targets JVM, Android, iOS & macOS as expected

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment