mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat: add fast hex-to-Long slicing to Hex
Add Hex.toLong64/toLong128/toLong256 (plus the shared readLong helper) to pack the first 64, 128 or 256 bits of a hex string into a single Long, two Longs or four Longs. Big-endian, allocation-light, branch-free — 16 table lookups and shifts per word. Useful as cheap map/set keys or bucket hashes for 32-byte event ids and pubkeys without decoding to a ByteArray. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019CU1wR6NvQmdmNNsPe9GuN
This commit is contained in:
@@ -200,6 +200,68 @@ object Hex {
|
||||
return out.concatToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs 16 hex chars starting at [offset] into a single [Long] (big-endian:
|
||||
* the first char becomes the most-significant nibble). No allocations, no
|
||||
* branches — just 16 table lookups and shifts.
|
||||
*
|
||||
* Assumes [hex] has at least `offset + 16` valid hex chars; it does not
|
||||
* validate. Guard untrusted input with [isHex64] first, otherwise an invalid
|
||||
* char (table value `-1`) corrupts the result.
|
||||
*/
|
||||
fun readLong(
|
||||
hex: String,
|
||||
offset: Int,
|
||||
): Long =
|
||||
(hexToByte[hex[offset].code].toLong() shl 60) or
|
||||
(hexToByte[hex[offset + 1].code].toLong() shl 56) or
|
||||
(hexToByte[hex[offset + 2].code].toLong() shl 52) or
|
||||
(hexToByte[hex[offset + 3].code].toLong() shl 48) or
|
||||
(hexToByte[hex[offset + 4].code].toLong() shl 44) or
|
||||
(hexToByte[hex[offset + 5].code].toLong() shl 40) or
|
||||
(hexToByte[hex[offset + 6].code].toLong() shl 36) or
|
||||
(hexToByte[hex[offset + 7].code].toLong() shl 32) or
|
||||
(hexToByte[hex[offset + 8].code].toLong() shl 28) or
|
||||
(hexToByte[hex[offset + 9].code].toLong() shl 24) or
|
||||
(hexToByte[hex[offset + 10].code].toLong() shl 20) or
|
||||
(hexToByte[hex[offset + 11].code].toLong() shl 16) or
|
||||
(hexToByte[hex[offset + 12].code].toLong() shl 12) or
|
||||
(hexToByte[hex[offset + 13].code].toLong() shl 8) or
|
||||
(hexToByte[hex[offset + 14].code].toLong() shl 4) or
|
||||
hexToByte[hex[offset + 15].code].toLong()
|
||||
|
||||
/**
|
||||
* Reads the first 64 bits (16 hex chars) of [hex] as a single [Long].
|
||||
* Ideal as a cheap, collision-resistant map/set key or bucket hash for a
|
||||
* 32-byte event id or pubkey. Assumes [hex] is at least 16 valid hex chars —
|
||||
* see [readLong].
|
||||
*/
|
||||
fun toLong64(hex: String): Long = readLong(hex, 0)
|
||||
|
||||
/**
|
||||
* Reads the first 128 bits (32 hex chars) of [hex] as two [Long]s, most
|
||||
* significant first. Assumes [hex] is at least 32 valid hex chars — see
|
||||
* [readLong].
|
||||
*/
|
||||
fun toLong128(hex: String): LongArray =
|
||||
longArrayOf(
|
||||
readLong(hex, 0),
|
||||
readLong(hex, 16),
|
||||
)
|
||||
|
||||
/**
|
||||
* Reads a full 256-bit (64 hex char) id/pubkey/signature-half as four
|
||||
* [Long]s, most significant first. Assumes [hex] is at least 64 valid hex
|
||||
* chars — see [readLong].
|
||||
*/
|
||||
fun toLong256(hex: String): LongArray =
|
||||
longArrayOf(
|
||||
readLong(hex, 0),
|
||||
readLong(hex, 16),
|
||||
readLong(hex, 32),
|
||||
readLong(hex, 48),
|
||||
)
|
||||
|
||||
/**
|
||||
* True when the hex string [id] encodes exactly the bytes [ourId], compared
|
||||
* without allocating a decode buffer. Handy for matching an incoming hex id
|
||||
|
||||
@@ -77,4 +77,53 @@ class HexEncodingTest {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToLong64() {
|
||||
assertEquals(0x48a72b485d383386uL.toLong(), Hex.toLong64(testHex))
|
||||
assertEquals(0L, Hex.toLong64("0000000000000000"))
|
||||
assertEquals(-1L, Hex.toLong64("ffffffffffffffff"))
|
||||
// uppercase reads the same
|
||||
assertEquals(0x48a72b485d383386uL.toLong(), Hex.toLong64(testHex.uppercase()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToLong128() {
|
||||
val longs = Hex.toLong128(testHex)
|
||||
assertEquals(2, longs.size)
|
||||
assertEquals(0x48a72b485d383386uL.toLong(), longs[0])
|
||||
assertEquals(0x27ec9d427583551fuL.toLong(), longs[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToLong256() {
|
||||
val longs = Hex.toLong256(testHex)
|
||||
assertEquals(4, longs.size)
|
||||
assertEquals(0x48a72b485d383386uL.toLong(), longs[0])
|
||||
assertEquals(0x27ec9d427583551fuL.toLong(), longs[1])
|
||||
assertEquals(0x9af4f016c739b8ecuL.toLong(), longs[2])
|
||||
assertEquals(0x0d6313540a8b12cfuL.toLong(), longs[3])
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Test
|
||||
fun testToLongMatchesDecodedBytes() {
|
||||
for (i in 0..1000) {
|
||||
val bytes = Random.nextBytes(32)
|
||||
val hex = bytes.toHexString(HexFormat.Default)
|
||||
|
||||
// the four longs must reconstruct exactly the 32 decoded bytes, big-endian
|
||||
val longs = Hex.toLong256(hex)
|
||||
for (word in 0 until 4) {
|
||||
for (b in 0 until 8) {
|
||||
val expected = bytes[word * 8 + b].toLong() and 0xFF
|
||||
val actual = (longs[word] ushr ((7 - b) * 8)) and 0xFF
|
||||
assertEquals(expected, actual, hex)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(longs[0], Hex.toLong64(hex))
|
||||
assertEquals(longs.take(2), Hex.toLong128(hex).toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user