Optimize hex formatting and parsing implementation #KT-58218
Merge-request: KT-MR-11702 Merged-by: Abduqodiri Qurbonzoda <abduqodiri.qurbonzoda@jetbrains.com>
This commit is contained in:
committed by
Space Team
parent
eb0a301a8f
commit
ba374bb45c
@@ -5,15 +5,56 @@
|
|||||||
|
|
||||||
package kotlin.text
|
package kotlin.text
|
||||||
|
|
||||||
|
// Benchmarks repository: https://github.com/qurbonzoda/KotlinHexFormatBenchmarks
|
||||||
|
|
||||||
private const val LOWER_CASE_HEX_DIGITS = "0123456789abcdef"
|
private const val LOWER_CASE_HEX_DIGITS = "0123456789abcdef"
|
||||||
private const val UPPER_CASE_HEX_DIGITS = "0123456789ABCDEF"
|
private const val UPPER_CASE_HEX_DIGITS = "0123456789ABCDEF"
|
||||||
|
|
||||||
// case-insensitive parsing
|
/**
|
||||||
private val HEX_DIGITS_TO_DECIMAL = IntArray(128) { -1 }.apply {
|
* The table for converting Byte values to their two-digit hex representation.
|
||||||
|
*
|
||||||
|
* It's used for formatting ByteArray. Storing the hex representation
|
||||||
|
* of each Byte value makes it possible to access the table only once per Byte.
|
||||||
|
* This noticeably improves performance, especially for large ByteArray's.
|
||||||
|
*/
|
||||||
|
private val BYTE_TO_LOWER_CASE_HEX_DIGITS = IntArray(256) {
|
||||||
|
(LOWER_CASE_HEX_DIGITS[(it shr 4)].code shl 8) or LOWER_CASE_HEX_DIGITS[(it and 0xF)].code
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table for converting Byte values to their two-digit hex representation.
|
||||||
|
*
|
||||||
|
* @see BYTE_TO_LOWER_CASE_HEX_DIGITS
|
||||||
|
*/
|
||||||
|
private val BYTE_TO_UPPER_CASE_HEX_DIGITS = IntArray(256) {
|
||||||
|
(UPPER_CASE_HEX_DIGITS[(it shr 4)].code shl 8) or UPPER_CASE_HEX_DIGITS[(it and 0xF)].code
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table for converting hex digits (both lowercase and uppercase) to their `Int` decimal value.
|
||||||
|
*
|
||||||
|
* Although `Char.code` of every hex digit is less than 128, the table size is 256 for the following reason:
|
||||||
|
* If the string whose chars are being converted is ASCII-encoded, the JIT optimizes `charAt` to a byte-sized load.
|
||||||
|
* When the table is 256 entries wide then both the `code ushr 8 == 0` and array bounds check are eliminated.
|
||||||
|
* This noticeably improves performance for ASCII strings.
|
||||||
|
*/
|
||||||
|
private val HEX_DIGITS_TO_DECIMAL = IntArray(256) { -1 }.apply {
|
||||||
LOWER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index }
|
LOWER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index }
|
||||||
UPPER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index }
|
UPPER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table for converting hex digits (both lowercase and uppercase) to their `Long` decimal value.
|
||||||
|
*
|
||||||
|
* Because `Int.toLong()` noticeably impacted performance, this separate table was introduced.
|
||||||
|
*
|
||||||
|
* @see HEX_DIGITS_TO_DECIMAL
|
||||||
|
*/
|
||||||
|
private val HEX_DIGITS_TO_LONG_DECIMAL = LongArray(256) { -1 }.apply {
|
||||||
|
LOWER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index.toLong() }
|
||||||
|
UPPER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index.toLong() }
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------- format and parse ByteArray --------------------------
|
// -------------------------- format and parse ByteArray --------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,9 +96,101 @@ public fun ByteArray.toHexString(
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS
|
val byteToDigits = if (format.upperCase) BYTE_TO_UPPER_CASE_HEX_DIGITS else BYTE_TO_LOWER_CASE_HEX_DIGITS
|
||||||
|
|
||||||
val bytesFormat = format.bytes
|
val bytesFormat = format.bytes
|
||||||
|
|
||||||
|
// Optimize for formats with unspecified bytesPerLine and bytesPerGroup
|
||||||
|
if (bytesFormat.noLineAndGroupSeparator) {
|
||||||
|
return toHexStringNoLineAndGroupSeparator(startIndex, endIndex, bytesFormat, byteToDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toHexStringSlowPath(startIndex, endIndex, bytesFormat, byteToDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun ByteArray.toHexStringNoLineAndGroupSeparator(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat,
|
||||||
|
byteToDigits: IntArray
|
||||||
|
): String {
|
||||||
|
// Optimize for formats with a short byteSeparator and no bytePrefix/Suffix
|
||||||
|
if (bytesFormat.shortByteSeparatorNoPrefixAndSuffix) {
|
||||||
|
return toHexStringShortByteSeparatorNoPrefixAndSuffix(startIndex, endIndex, bytesFormat, byteToDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toHexStringNoLineAndGroupSeparatorSlowPath(startIndex, endIndex, bytesFormat, byteToDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun ByteArray.toHexStringShortByteSeparatorNoPrefixAndSuffix(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat,
|
||||||
|
byteToDigits: IntArray
|
||||||
|
): String {
|
||||||
|
val byteSeparatorLength = bytesFormat.byteSeparator.length
|
||||||
|
require(byteSeparatorLength <= 1)
|
||||||
|
|
||||||
|
val numberOfBytes = endIndex - startIndex
|
||||||
|
var charIndex = 0
|
||||||
|
|
||||||
|
if (byteSeparatorLength == 0) {
|
||||||
|
val charArray = CharArray(checkFormatLength(2L * numberOfBytes))
|
||||||
|
for (byteIndex in startIndex until endIndex) {
|
||||||
|
charIndex = formatByteAt(byteIndex, byteToDigits, charArray, charIndex)
|
||||||
|
}
|
||||||
|
return charArray.concatToString()
|
||||||
|
} else {
|
||||||
|
val charArray = CharArray(checkFormatLength(3L * numberOfBytes - 1))
|
||||||
|
val byteSeparatorChar = bytesFormat.byteSeparator[0]
|
||||||
|
|
||||||
|
charIndex = formatByteAt(startIndex, byteToDigits, charArray, charIndex)
|
||||||
|
for (byteIndex in startIndex + 1 until endIndex) {
|
||||||
|
charArray[charIndex++] = byteSeparatorChar
|
||||||
|
charIndex = formatByteAt(byteIndex, byteToDigits, charArray, charIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return charArray.concatToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun ByteArray.toHexStringNoLineAndGroupSeparatorSlowPath(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat,
|
||||||
|
byteToDigits: IntArray
|
||||||
|
): String {
|
||||||
|
val bytePrefix = bytesFormat.bytePrefix
|
||||||
|
val byteSuffix = bytesFormat.byteSuffix
|
||||||
|
val byteSeparator = bytesFormat.byteSeparator
|
||||||
|
|
||||||
|
val formatLength = formattedStringLength(
|
||||||
|
numberOfBytes = endIndex - startIndex,
|
||||||
|
byteSeparator.length,
|
||||||
|
bytePrefix.length,
|
||||||
|
byteSuffix.length
|
||||||
|
)
|
||||||
|
val charArray = CharArray(formatLength)
|
||||||
|
var charIndex = 0
|
||||||
|
|
||||||
|
charIndex = formatByteAt(startIndex, bytePrefix, byteSuffix, byteToDigits, charArray, charIndex)
|
||||||
|
for (byteIndex in startIndex + 1 until endIndex) {
|
||||||
|
charIndex = byteSeparator.toCharArrayIfNotEmpty(charArray, charIndex)
|
||||||
|
charIndex = formatByteAt(byteIndex, bytePrefix, byteSuffix, byteToDigits, charArray, charIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return charArray.concatToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun ByteArray.toHexStringSlowPath(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat,
|
||||||
|
byteToDigits: IntArray
|
||||||
|
): String {
|
||||||
val bytesPerLine = bytesFormat.bytesPerLine
|
val bytesPerLine = bytesFormat.bytesPerLine
|
||||||
val bytesPerGroup = bytesFormat.bytesPerGroup
|
val bytesPerGroup = bytesFormat.bytesPerGroup
|
||||||
val bytePrefix = bytesFormat.bytePrefix
|
val bytePrefix = bytesFormat.bytePrefix
|
||||||
@@ -66,7 +199,7 @@ public fun ByteArray.toHexString(
|
|||||||
val groupSeparator = bytesFormat.groupSeparator
|
val groupSeparator = bytesFormat.groupSeparator
|
||||||
|
|
||||||
val formatLength = formattedStringLength(
|
val formatLength = formattedStringLength(
|
||||||
totalBytes = endIndex - startIndex,
|
numberOfBytes = endIndex - startIndex,
|
||||||
bytesPerLine,
|
bytesPerLine,
|
||||||
bytesPerGroup,
|
bytesPerGroup,
|
||||||
groupSeparator.length,
|
groupSeparator.length,
|
||||||
@@ -74,42 +207,77 @@ public fun ByteArray.toHexString(
|
|||||||
bytePrefix.length,
|
bytePrefix.length,
|
||||||
byteSuffix.length
|
byteSuffix.length
|
||||||
)
|
)
|
||||||
|
val charArray = CharArray(formatLength)
|
||||||
|
var charIndex = 0
|
||||||
|
|
||||||
var indexInLine = 0
|
var indexInLine = 0
|
||||||
var indexInGroup = 0
|
var indexInGroup = 0
|
||||||
|
|
||||||
return buildString(formatLength) {
|
for (byteIndex in startIndex until endIndex) {
|
||||||
for (i in startIndex until endIndex) {
|
if (indexInLine == bytesPerLine) {
|
||||||
val byte = this@toHexString[i].toInt() and 0xFF
|
charArray[charIndex++] = '\n'
|
||||||
|
indexInLine = 0
|
||||||
if (indexInLine == bytesPerLine) {
|
indexInGroup = 0
|
||||||
append('\n')
|
} else if (indexInGroup == bytesPerGroup) {
|
||||||
indexInLine = 0
|
charIndex = groupSeparator.toCharArrayIfNotEmpty(charArray, charIndex)
|
||||||
indexInGroup = 0
|
indexInGroup = 0
|
||||||
} else if (indexInGroup == bytesPerGroup) {
|
}
|
||||||
append(groupSeparator)
|
if (indexInGroup != 0) {
|
||||||
indexInGroup = 0
|
charIndex = byteSeparator.toCharArrayIfNotEmpty(charArray, charIndex)
|
||||||
}
|
|
||||||
if (indexInGroup != 0) {
|
|
||||||
append(byteSeparator)
|
|
||||||
}
|
|
||||||
|
|
||||||
append(bytePrefix)
|
|
||||||
append(digits[byte shr 4])
|
|
||||||
append(digits[byte and 0xF])
|
|
||||||
append(byteSuffix)
|
|
||||||
|
|
||||||
indexInGroup += 1
|
|
||||||
indexInLine += 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check(formatLength == length)
|
charIndex = formatByteAt(byteIndex, bytePrefix, byteSuffix, byteToDigits, charArray, charIndex)
|
||||||
|
|
||||||
|
indexInGroup += 1
|
||||||
|
indexInLine += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check(charIndex == formatLength)
|
||||||
|
return charArray.concatToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.formatByteAt(
|
||||||
|
index: Int,
|
||||||
|
bytePrefix: String,
|
||||||
|
byteSuffix: String,
|
||||||
|
byteToDigits: IntArray,
|
||||||
|
destination: CharArray,
|
||||||
|
destinationOffset: Int
|
||||||
|
): Int {
|
||||||
|
var offset = bytePrefix.toCharArrayIfNotEmpty(destination, destinationOffset)
|
||||||
|
offset = formatByteAt(index, byteToDigits, destination, offset)
|
||||||
|
return byteSuffix.toCharArrayIfNotEmpty(destination, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.formatByteAt(
|
||||||
|
index: Int,
|
||||||
|
byteToDigits: IntArray,
|
||||||
|
destination: CharArray,
|
||||||
|
destinationOffset: Int
|
||||||
|
): Int {
|
||||||
|
val byte = this[index].toInt() and 0xFF
|
||||||
|
val byteDigits = byteToDigits[byte]
|
||||||
|
destination[destinationOffset] = (byteDigits shr 8).toChar()
|
||||||
|
destination[destinationOffset + 1] = (byteDigits and 0xFF).toChar()
|
||||||
|
return destinationOffset + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formattedStringLength(
|
||||||
|
numberOfBytes: Int,
|
||||||
|
byteSeparatorLength: Int,
|
||||||
|
bytePrefixLength: Int,
|
||||||
|
byteSuffixLength: Int
|
||||||
|
): Int {
|
||||||
|
require(numberOfBytes > 0)
|
||||||
|
|
||||||
|
val charsPerByte = 2L + bytePrefixLength + byteSuffixLength + byteSeparatorLength
|
||||||
|
val formatLength = numberOfBytes * charsPerByte - byteSeparatorLength
|
||||||
|
return checkFormatLength(formatLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declared internal for testing
|
// Declared internal for testing
|
||||||
internal fun formattedStringLength(
|
internal fun formattedStringLength(
|
||||||
totalBytes: Int,
|
numberOfBytes: Int,
|
||||||
bytesPerLine: Int,
|
bytesPerLine: Int,
|
||||||
bytesPerGroup: Int,
|
bytesPerGroup: Int,
|
||||||
groupSeparatorLength: Int,
|
groupSeparatorLength: Int,
|
||||||
@@ -117,34 +285,37 @@ internal fun formattedStringLength(
|
|||||||
bytePrefixLength: Int,
|
bytePrefixLength: Int,
|
||||||
byteSuffixLength: Int
|
byteSuffixLength: Int
|
||||||
): Int {
|
): Int {
|
||||||
require(totalBytes > 0)
|
require(numberOfBytes > 0)
|
||||||
// By contract bytesPerLine and bytesPerGroup are > 0
|
// By contract bytesPerLine and bytesPerGroup are > 0
|
||||||
|
|
||||||
val lineSeparators = (totalBytes - 1) / bytesPerLine
|
val lineSeparators = (numberOfBytes - 1) / bytesPerLine
|
||||||
val groupSeparators = run {
|
val groupSeparators = run {
|
||||||
val groupSeparatorsPerLine = (bytesPerLine - 1) / bytesPerGroup
|
val groupSeparatorsPerLine = (bytesPerLine - 1) / bytesPerGroup
|
||||||
val bytesInLastLine = (totalBytes % bytesPerLine).let { if (it == 0) bytesPerLine else it }
|
val bytesInLastLine = (numberOfBytes % bytesPerLine).let { if (it == 0) bytesPerLine else it }
|
||||||
val groupSeparatorsInLastLine = (bytesInLastLine - 1) / bytesPerGroup
|
val groupSeparatorsInLastLine = (bytesInLastLine - 1) / bytesPerGroup
|
||||||
lineSeparators * groupSeparatorsPerLine + groupSeparatorsInLastLine
|
lineSeparators * groupSeparatorsPerLine + groupSeparatorsInLastLine
|
||||||
}
|
}
|
||||||
val byteSeparators = totalBytes - 1 - lineSeparators - groupSeparators
|
val byteSeparators = numberOfBytes - 1 - lineSeparators - groupSeparators
|
||||||
|
|
||||||
// The max totalLength is achieved when
|
// The max formatLength is achieved when
|
||||||
// totalBytes, bytePrefix/Suffix/Separator.length = Int.MAX_VALUE.
|
// numberOfBytes, bytePrefix/Suffix/Separator.length = Int.MAX_VALUE.
|
||||||
// The result is 3 * Int.MAX_VALUE * Int.MAX_VALUE + Int.MAX_VALUE,
|
// The result is 3 * Int.MAX_VALUE * Int.MAX_VALUE + Int.MAX_VALUE,
|
||||||
// which is > Long.MAX_VALUE, but < ULong.MAX_VALUE.
|
// which is > Long.MAX_VALUE, but < ULong.MAX_VALUE.
|
||||||
|
|
||||||
val totalLength: Long = lineSeparators.toLong() /* * lineSeparator.length = 1 */ +
|
val formatLength: Long = lineSeparators.toLong() /* * lineSeparator.length = 1 */ +
|
||||||
groupSeparators.toLong() * groupSeparatorLength.toLong() +
|
groupSeparators.toLong() * groupSeparatorLength.toLong() +
|
||||||
byteSeparators.toLong() * byteSeparatorLength.toLong() +
|
byteSeparators.toLong() * byteSeparatorLength.toLong() +
|
||||||
totalBytes.toLong() * (bytePrefixLength.toLong() + 2L + byteSuffixLength.toLong())
|
numberOfBytes.toLong() * (bytePrefixLength.toLong() + 2L + byteSuffixLength.toLong())
|
||||||
|
|
||||||
if (totalLength !in 0..Int.MAX_VALUE) {
|
return checkFormatLength(formatLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkFormatLength(formatLength: Long): Int {
|
||||||
|
if (formatLength !in 0..Int.MAX_VALUE) {
|
||||||
// TODO: Common OutOfMemoryError?
|
// TODO: Common OutOfMemoryError?
|
||||||
throw IllegalArgumentException("The resulting string length is too big: ${totalLength.toULong()}")
|
throw IllegalArgumentException("The resulting string length is too big: ${formatLength.toULong()}")
|
||||||
}
|
}
|
||||||
|
return formatLength.toInt()
|
||||||
return totalLength.toInt()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,14 +362,119 @@ private fun String.hexToByteArray(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bytesFormat = format.bytes
|
val bytesFormat = format.bytes
|
||||||
|
|
||||||
|
// Optimize for formats with unspecified bytesPerLine and bytesPerGroup
|
||||||
|
if (bytesFormat.noLineAndGroupSeparator) {
|
||||||
|
hexToByteArrayNoLineAndGroupSeparator(startIndex, endIndex, bytesFormat)?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexToByteArraySlowPath(startIndex, endIndex, bytesFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun String.hexToByteArrayNoLineAndGroupSeparator(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat
|
||||||
|
): ByteArray? {
|
||||||
|
// Optimize for formats with a short byteSeparator and no bytePrefix/Suffix
|
||||||
|
if (bytesFormat.shortByteSeparatorNoPrefixAndSuffix) {
|
||||||
|
return hexToByteArrayShortByteSeparatorNoPrefixAndSuffix(startIndex, endIndex, bytesFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexToByteArrayNoLineAndGroupSeparatorSlowPath(startIndex, endIndex, bytesFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun String.hexToByteArrayShortByteSeparatorNoPrefixAndSuffix(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat
|
||||||
|
): ByteArray? {
|
||||||
|
val byteSeparatorLength = bytesFormat.byteSeparator.length
|
||||||
|
require(byteSeparatorLength <= 1)
|
||||||
|
|
||||||
|
val numberOfChars = endIndex - startIndex
|
||||||
|
var charIndex = 0
|
||||||
|
|
||||||
|
if (byteSeparatorLength == 0) {
|
||||||
|
if (numberOfChars and 1 != 0) return null
|
||||||
|
val numberOfBytes = numberOfChars shr 1
|
||||||
|
val byteArray = ByteArray(numberOfBytes)
|
||||||
|
for (byteIndex in 0 until numberOfBytes) {
|
||||||
|
byteArray[byteIndex] = parseByteAt(charIndex)
|
||||||
|
charIndex += 2
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
|
} else {
|
||||||
|
if (numberOfChars % 3 != 2) return null
|
||||||
|
val numberOfBytes = numberOfChars / 3 + 1
|
||||||
|
val byteArray = ByteArray(numberOfBytes)
|
||||||
|
val byteSeparatorChar = bytesFormat.byteSeparator[0]
|
||||||
|
byteArray[0] = parseByteAt(charIndex)
|
||||||
|
charIndex += 2
|
||||||
|
for (byteIndex in 1 until numberOfBytes) {
|
||||||
|
if (this[charIndex] != byteSeparatorChar) {
|
||||||
|
checkContainsAt(charIndex, endIndex, bytesFormat.byteSeparator, bytesFormat.ignoreCase, "byte separator")
|
||||||
|
}
|
||||||
|
byteArray[byteIndex] = parseByteAt(charIndex + 1)
|
||||||
|
charIndex += 3
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun String.hexToByteArrayNoLineAndGroupSeparatorSlowPath(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat
|
||||||
|
): ByteArray? {
|
||||||
|
val bytePrefix = bytesFormat.bytePrefix
|
||||||
|
val byteSuffix = bytesFormat.byteSuffix
|
||||||
|
val byteSeparator = bytesFormat.byteSeparator
|
||||||
|
val byteSeparatorLength = byteSeparator.length
|
||||||
|
val charsPerByte = 2L + bytePrefix.length + byteSuffix.length + byteSeparatorLength
|
||||||
|
val numberOfChars = (endIndex - startIndex).toLong()
|
||||||
|
val numberOfBytes = ((numberOfChars + byteSeparatorLength) / charsPerByte).toInt()
|
||||||
|
|
||||||
|
// Go to the default implementation when the string length doesn't match
|
||||||
|
if (numberOfBytes * charsPerByte - byteSeparatorLength != numberOfChars) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val ignoreCase = bytesFormat.ignoreCase
|
||||||
|
|
||||||
|
val byteArray = ByteArray(numberOfBytes)
|
||||||
|
var charIndex = startIndex
|
||||||
|
|
||||||
|
charIndex = checkContainsAt(charIndex, endIndex, bytePrefix, ignoreCase, "byte prefix")
|
||||||
|
val between = byteSuffix + byteSeparator + bytePrefix
|
||||||
|
for (byteIndex in 0 until numberOfBytes - 1) {
|
||||||
|
byteArray[byteIndex] = parseByteAt(charIndex)
|
||||||
|
charIndex = checkContainsAt(charIndex + 2, endIndex, between, ignoreCase, "byte suffix + byte separator + byte prefix")
|
||||||
|
}
|
||||||
|
byteArray[numberOfBytes - 1] = parseByteAt(charIndex)
|
||||||
|
checkContainsAt(charIndex + 2, endIndex, byteSuffix, ignoreCase, "byte suffix")
|
||||||
|
|
||||||
|
return byteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun String.hexToByteArraySlowPath(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
bytesFormat: HexFormat.BytesHexFormat
|
||||||
|
): ByteArray {
|
||||||
val bytesPerLine = bytesFormat.bytesPerLine
|
val bytesPerLine = bytesFormat.bytesPerLine
|
||||||
val bytesPerGroup = bytesFormat.bytesPerGroup
|
val bytesPerGroup = bytesFormat.bytesPerGroup
|
||||||
val bytePrefix = bytesFormat.bytePrefix
|
val bytePrefix = bytesFormat.bytePrefix
|
||||||
val byteSuffix = bytesFormat.byteSuffix
|
val byteSuffix = bytesFormat.byteSuffix
|
||||||
val byteSeparator = bytesFormat.byteSeparator
|
val byteSeparator = bytesFormat.byteSeparator
|
||||||
val groupSeparator = bytesFormat.groupSeparator
|
val groupSeparator = bytesFormat.groupSeparator
|
||||||
|
val ignoreCase = bytesFormat.ignoreCase
|
||||||
|
|
||||||
val resultCapacity = parsedByteArrayMaxSize(
|
val parseMaxSize = parsedByteArrayMaxSize(
|
||||||
stringLength = endIndex - startIndex,
|
stringLength = endIndex - startIndex,
|
||||||
bytesPerLine,
|
bytesPerLine,
|
||||||
bytesPerGroup,
|
bytesPerGroup,
|
||||||
@@ -207,37 +483,42 @@ private fun String.hexToByteArray(
|
|||||||
bytePrefix.length,
|
bytePrefix.length,
|
||||||
byteSuffix.length
|
byteSuffix.length
|
||||||
)
|
)
|
||||||
val result = ByteArray(resultCapacity)
|
val byteArray = ByteArray(parseMaxSize)
|
||||||
|
|
||||||
var i = startIndex
|
var charIndex = startIndex
|
||||||
var byteIndex = 0
|
var byteIndex = 0
|
||||||
var indexInLine = 0
|
var indexInLine = 0
|
||||||
var indexInGroup = 0
|
var indexInGroup = 0
|
||||||
|
|
||||||
while (i < endIndex) {
|
while (charIndex < endIndex) {
|
||||||
if (indexInLine == bytesPerLine) {
|
if (indexInLine == bytesPerLine) {
|
||||||
i = checkNewLineAt(i, endIndex)
|
charIndex = checkNewLineAt(charIndex, endIndex)
|
||||||
indexInLine = 0
|
indexInLine = 0
|
||||||
indexInGroup = 0
|
indexInGroup = 0
|
||||||
} else if (indexInGroup == bytesPerGroup) {
|
} else if (indexInGroup == bytesPerGroup) {
|
||||||
i = checkContainsAt(groupSeparator, i, endIndex, "group separator")
|
charIndex = checkContainsAt(charIndex, endIndex, groupSeparator, ignoreCase, "group separator")
|
||||||
indexInGroup = 0
|
indexInGroup = 0
|
||||||
} else if (indexInGroup != 0) {
|
} else if (indexInGroup != 0) {
|
||||||
i = checkContainsAt(byteSeparator, i, endIndex, "byte separator")
|
charIndex = checkContainsAt(charIndex, endIndex, byteSeparator, ignoreCase, "byte separator")
|
||||||
}
|
}
|
||||||
indexInLine += 1
|
indexInLine += 1
|
||||||
indexInGroup += 1
|
indexInGroup += 1
|
||||||
|
|
||||||
i = checkContainsAt(bytePrefix, i, endIndex, "byte prefix")
|
charIndex = checkContainsAt(charIndex, endIndex, bytePrefix, ignoreCase, "byte prefix")
|
||||||
|
if (endIndex - 2 < charIndex) {
|
||||||
checkHexLength(i, (i + 2).coerceAtMost(endIndex), maxDigits = 2, requireMaxLength = true)
|
throwInvalidNumberOfDigits(charIndex, endIndex, maxDigits = 2, requireMaxLength = true)
|
||||||
|
}
|
||||||
result[byteIndex++] = ((decimalFromHexDigitAt(i++) shl 4) or decimalFromHexDigitAt(i++)).toByte()
|
byteArray[byteIndex++] = parseByteAt(charIndex)
|
||||||
|
charIndex = checkContainsAt(charIndex + 2, endIndex, byteSuffix, ignoreCase, "byte suffix")
|
||||||
i = checkContainsAt(byteSuffix, i, endIndex, "byte suffix")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (byteIndex == result.size) result else result.copyOf(byteIndex)
|
return if (byteIndex == byteArray.size) byteArray else byteArray.copyOf(byteIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.parseByteAt(index: Int): Byte {
|
||||||
|
val high = decimalFromHexDigitAt(index)
|
||||||
|
val low = decimalFromHexDigitAt(index + 1)
|
||||||
|
return ((high shl 4) or low).toByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declared internal for testing
|
// Declared internal for testing
|
||||||
@@ -329,7 +610,24 @@ private fun String.checkNewLineAt(index: Int, endIndex: Int): Int {
|
|||||||
*/
|
*/
|
||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
@SinceKotlin("1.9")
|
@SinceKotlin("1.9")
|
||||||
public fun Byte.toHexString(format: HexFormat = HexFormat.Default): String = toLong().toHexStringImpl(format, bits = 8)
|
public fun Byte.toHexString(format: HexFormat = HexFormat.Default): String {
|
||||||
|
val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS
|
||||||
|
val numberFormat = format.number
|
||||||
|
|
||||||
|
// Optimize for digits-only formats
|
||||||
|
if (numberFormat.isDigitsOnly) {
|
||||||
|
val charArray = CharArray(2)
|
||||||
|
val value = this.toInt()
|
||||||
|
charArray[0] = digits[(value shr 4) and 0xF]
|
||||||
|
charArray[1] = digits[value and 0xF]
|
||||||
|
return if (numberFormat.removeLeadingZeros)
|
||||||
|
charArray.concatToString(startIndex = (countLeadingZeroBits() shr 2).coerceAtMost(1))
|
||||||
|
else
|
||||||
|
charArray.concatToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toLong().toHexStringImpl(numberFormat, digits, bits = 8)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a `Byte` value from this string using the specified [format].
|
* Parses a `Byte` value from this string using the specified [format].
|
||||||
@@ -362,7 +660,7 @@ public fun String.hexToByte(format: HexFormat = HexFormat.Default): Byte = hexTo
|
|||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
//@SinceKotlin("1.9")
|
//@SinceKotlin("1.9")
|
||||||
private fun String.hexToByte(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Byte =
|
private fun String.hexToByte(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Byte =
|
||||||
hexToLongImpl(startIndex, endIndex, format, maxDigits = 2).toByte()
|
hexToIntImpl(startIndex, endIndex, format, maxDigits = 2).toByte()
|
||||||
|
|
||||||
// -------------------------- format and parse Short --------------------------
|
// -------------------------- format and parse Short --------------------------
|
||||||
|
|
||||||
@@ -375,7 +673,26 @@ private fun String.hexToByte(startIndex: Int = 0, endIndex: Int = length, format
|
|||||||
*/
|
*/
|
||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
@SinceKotlin("1.9")
|
@SinceKotlin("1.9")
|
||||||
public fun Short.toHexString(format: HexFormat = HexFormat.Default): String = toLong().toHexStringImpl(format, bits = 16)
|
public fun Short.toHexString(format: HexFormat = HexFormat.Default): String {
|
||||||
|
val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS
|
||||||
|
val numberFormat = format.number
|
||||||
|
|
||||||
|
// Optimize for digits-only formats
|
||||||
|
if (numberFormat.isDigitsOnly) {
|
||||||
|
val charArray = CharArray(4)
|
||||||
|
val value = this.toInt()
|
||||||
|
charArray[0] = digits[(value shr 12) and 0xF]
|
||||||
|
charArray[1] = digits[(value shr 8) and 0xF]
|
||||||
|
charArray[2] = digits[(value shr 4) and 0xF]
|
||||||
|
charArray[3] = digits[value and 0xF]
|
||||||
|
return if (numberFormat.removeLeadingZeros)
|
||||||
|
charArray.concatToString(startIndex = (countLeadingZeroBits() shr 2).coerceAtMost(3))
|
||||||
|
else
|
||||||
|
charArray.concatToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toLong().toHexStringImpl(numberFormat, digits, bits = 16)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a `Short` value from this string using the specified [format].
|
* Parses a `Short` value from this string using the specified [format].
|
||||||
@@ -408,7 +725,7 @@ public fun String.hexToShort(format: HexFormat = HexFormat.Default): Short = hex
|
|||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
//@SinceKotlin("1.9")
|
//@SinceKotlin("1.9")
|
||||||
private fun String.hexToShort(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Short =
|
private fun String.hexToShort(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Short =
|
||||||
hexToLongImpl(startIndex, endIndex, format, maxDigits = 4).toShort()
|
hexToIntImpl(startIndex, endIndex, format, maxDigits = 4).toShort()
|
||||||
|
|
||||||
// -------------------------- format and parse Int --------------------------
|
// -------------------------- format and parse Int --------------------------
|
||||||
|
|
||||||
@@ -421,7 +738,30 @@ private fun String.hexToShort(startIndex: Int = 0, endIndex: Int = length, forma
|
|||||||
*/
|
*/
|
||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
@SinceKotlin("1.9")
|
@SinceKotlin("1.9")
|
||||||
public fun Int.toHexString(format: HexFormat = HexFormat.Default): String = toLong().toHexStringImpl(format, bits = 32)
|
public fun Int.toHexString(format: HexFormat = HexFormat.Default): String {
|
||||||
|
val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS
|
||||||
|
val numberFormat = format.number
|
||||||
|
|
||||||
|
// Optimize for digits-only formats
|
||||||
|
if (numberFormat.isDigitsOnly) {
|
||||||
|
val charArray = CharArray(8)
|
||||||
|
val value = this
|
||||||
|
charArray[0] = digits[(value shr 28) and 0xF]
|
||||||
|
charArray[1] = digits[(value shr 24) and 0xF]
|
||||||
|
charArray[2] = digits[(value shr 20) and 0xF]
|
||||||
|
charArray[3] = digits[(value shr 16) and 0xF]
|
||||||
|
charArray[4] = digits[(value shr 12) and 0xF]
|
||||||
|
charArray[5] = digits[(value shr 8) and 0xF]
|
||||||
|
charArray[6] = digits[(value shr 4) and 0xF]
|
||||||
|
charArray[7] = digits[value and 0xF]
|
||||||
|
return if (numberFormat.removeLeadingZeros)
|
||||||
|
charArray.concatToString(startIndex = (countLeadingZeroBits() shr 2).coerceAtMost(7))
|
||||||
|
else
|
||||||
|
charArray.concatToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toLong().toHexStringImpl(numberFormat, digits, bits = 32)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses an `Int` value from this string using the specified [format].
|
* Parses an `Int` value from this string using the specified [format].
|
||||||
@@ -454,7 +794,7 @@ public fun String.hexToInt(format: HexFormat = HexFormat.Default): Int = hexToIn
|
|||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
//@SinceKotlin("1.9")
|
//@SinceKotlin("1.9")
|
||||||
private fun String.hexToInt(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Int =
|
private fun String.hexToInt(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Int =
|
||||||
hexToLongImpl(startIndex, endIndex, format, maxDigits = 8).toInt()
|
hexToIntImpl(startIndex, endIndex, format, maxDigits = 8)
|
||||||
|
|
||||||
// -------------------------- format and parse Long --------------------------
|
// -------------------------- format and parse Long --------------------------
|
||||||
|
|
||||||
@@ -467,7 +807,38 @@ private fun String.hexToInt(startIndex: Int = 0, endIndex: Int = length, format:
|
|||||||
*/
|
*/
|
||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
@SinceKotlin("1.9")
|
@SinceKotlin("1.9")
|
||||||
public fun Long.toHexString(format: HexFormat = HexFormat.Default): String = toHexStringImpl(format, bits = 64)
|
public fun Long.toHexString(format: HexFormat = HexFormat.Default): String {
|
||||||
|
val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS
|
||||||
|
val numberFormat = format.number
|
||||||
|
|
||||||
|
// Optimize for digits-only formats
|
||||||
|
if (numberFormat.isDigitsOnly) {
|
||||||
|
val charArray = CharArray(16)
|
||||||
|
val value = this
|
||||||
|
charArray[0] = digits[((value shr 60) and 0xF).toInt()]
|
||||||
|
charArray[1] = digits[((value shr 56) and 0xF).toInt()]
|
||||||
|
charArray[2] = digits[((value shr 52) and 0xF).toInt()]
|
||||||
|
charArray[3] = digits[((value shr 48) and 0xF).toInt()]
|
||||||
|
charArray[4] = digits[((value shr 44) and 0xF).toInt()]
|
||||||
|
charArray[5] = digits[((value shr 40) and 0xF).toInt()]
|
||||||
|
charArray[6] = digits[((value shr 36) and 0xF).toInt()]
|
||||||
|
charArray[7] = digits[((value shr 32) and 0xF).toInt()]
|
||||||
|
charArray[8] = digits[((value shr 28) and 0xF).toInt()]
|
||||||
|
charArray[9] = digits[((value shr 24) and 0xF).toInt()]
|
||||||
|
charArray[10] = digits[((value shr 20) and 0xF).toInt()]
|
||||||
|
charArray[11] = digits[((value shr 16) and 0xF).toInt()]
|
||||||
|
charArray[12] = digits[((value shr 12) and 0xF).toInt()]
|
||||||
|
charArray[13] = digits[((value shr 8) and 0xF).toInt()]
|
||||||
|
charArray[14] = digits[((value shr 4) and 0xF).toInt()]
|
||||||
|
charArray[15] = digits[(value and 0xF).toInt()]
|
||||||
|
return if (numberFormat.removeLeadingZeros)
|
||||||
|
charArray.concatToString(startIndex = (countLeadingZeroBits() shr 2).coerceAtMost(15))
|
||||||
|
else
|
||||||
|
charArray.concatToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toHexStringImpl(numberFormat, digits, bits = 64)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a `Long` value from this string using the specified [format].
|
* Parses a `Long` value from this string using the specified [format].
|
||||||
@@ -505,86 +876,174 @@ private fun String.hexToLong(startIndex: Int = 0, endIndex: Int = length, format
|
|||||||
// -------------------------- private format and parse functions --------------------------
|
// -------------------------- private format and parse functions --------------------------
|
||||||
|
|
||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
private fun Long.toHexStringImpl(format: HexFormat, bits: Int): String {
|
private fun Long.toHexStringImpl(numberFormat: HexFormat.NumberHexFormat, digits: String, bits: Int): String {
|
||||||
require(bits and 0x3 == 0)
|
require(bits and 0x3 == 0)
|
||||||
|
|
||||||
val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS
|
|
||||||
val value = this
|
val value = this
|
||||||
|
val numberOfHexDigits = bits shr 2
|
||||||
|
|
||||||
val prefix = format.number.prefix
|
val prefix = numberFormat.prefix
|
||||||
val suffix = format.number.suffix
|
val suffix = numberFormat.suffix
|
||||||
val formatLength = prefix.length + (bits shr 2) + suffix.length
|
var removeZeros = numberFormat.removeLeadingZeros
|
||||||
var removeZeros = format.number.removeLeadingZeros
|
|
||||||
|
|
||||||
return buildString(formatLength) {
|
val formatLength = prefix.length.toLong() + numberOfHexDigits + suffix.length
|
||||||
append(prefix)
|
val charArray = CharArray(checkFormatLength(formatLength))
|
||||||
|
|
||||||
var shift = bits
|
var charIndex = prefix.toCharArrayIfNotEmpty(charArray, 0)
|
||||||
while (shift > 0) {
|
|
||||||
shift -= 4
|
var shift = bits
|
||||||
val decimal = ((value shr shift) and 0xF).toInt()
|
repeat(numberOfHexDigits) {
|
||||||
removeZeros = removeZeros && decimal == 0 && shift > 0
|
shift -= 4
|
||||||
if (!removeZeros) {
|
val decimal = ((value shr shift) and 0xF).toInt()
|
||||||
append(digits[decimal])
|
removeZeros = removeZeros && decimal == 0 && shift > 0
|
||||||
}
|
if (!removeZeros) {
|
||||||
|
charArray[charIndex++] = digits[decimal]
|
||||||
}
|
}
|
||||||
|
|
||||||
append(suffix)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
charIndex = suffix.toCharArrayIfNotEmpty(charArray, charIndex)
|
||||||
|
|
||||||
|
return if (charIndex == charArray.size) charArray.concatToString() else charArray.concatToString(endIndex = charIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private fun String.toCharArrayIfNotEmpty(destination: CharArray, destinationOffset: Int): Int {
|
||||||
|
when (length) {
|
||||||
|
0 -> { /* do nothing */ }
|
||||||
|
1 -> destination[destinationOffset] = this[0]
|
||||||
|
else -> toCharArray(destination, destinationOffset)
|
||||||
|
}
|
||||||
|
return destinationOffset + length
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalStdlibApi
|
@ExperimentalStdlibApi
|
||||||
private fun String.hexToLongImpl(startIndex: Int = 0, endIndex: Int = length, format: HexFormat, maxDigits: Int): Long {
|
private fun String.hexToIntImpl(startIndex: Int, endIndex: Int, format: HexFormat, maxDigits: Int): Int {
|
||||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
||||||
|
|
||||||
val prefix = format.number.prefix
|
val numberFormat = format.number
|
||||||
val suffix = format.number.suffix
|
|
||||||
|
|
||||||
if (prefix.length + suffix.length >= endIndex - startIndex) {
|
// Optimize for digits-only formats
|
||||||
throw NumberFormatException(
|
if (numberFormat.isDigitsOnly) {
|
||||||
"Expected a hexadecimal number with prefix \"$prefix\" and suffix \"$suffix\", but was ${substring(startIndex, endIndex)}"
|
checkMaxDigits(startIndex, endIndex, maxDigits)
|
||||||
)
|
return parseInt(startIndex, endIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
val digitsStartIndex = checkContainsAt(prefix, startIndex, endIndex, "prefix")
|
val prefix = numberFormat.prefix
|
||||||
|
val suffix = numberFormat.suffix
|
||||||
|
checkPrefixSuffixMaxDigits(startIndex, endIndex, prefix, suffix, numberFormat.ignoreCase, maxDigits)
|
||||||
|
return parseInt(startIndex + prefix.length, endIndex - suffix.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
private fun String.hexToLongImpl(startIndex: Int, endIndex: Int, format: HexFormat, maxDigits: Int): Long {
|
||||||
|
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
||||||
|
|
||||||
|
val numberFormat = format.number
|
||||||
|
|
||||||
|
// Optimize for digits-only formats
|
||||||
|
if (numberFormat.isDigitsOnly) {
|
||||||
|
checkMaxDigits(startIndex, endIndex, maxDigits)
|
||||||
|
return parseLong(startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
val prefix = numberFormat.prefix
|
||||||
|
val suffix = numberFormat.suffix
|
||||||
|
checkPrefixSuffixMaxDigits(startIndex, endIndex, prefix, suffix, numberFormat.ignoreCase, maxDigits)
|
||||||
|
return parseLong(startIndex + prefix.length, endIndex - suffix.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.checkPrefixSuffixMaxDigits(
|
||||||
|
startIndex: Int,
|
||||||
|
endIndex: Int,
|
||||||
|
prefix: String,
|
||||||
|
suffix: String,
|
||||||
|
ignoreCase: Boolean,
|
||||||
|
maxDigits: Int
|
||||||
|
) {
|
||||||
|
if (endIndex - startIndex - prefix.length <= suffix.length) {
|
||||||
|
throwInvalidPrefixSuffix(startIndex, endIndex, prefix, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
val digitsStartIndex = checkContainsAt(startIndex, endIndex, prefix, ignoreCase, "prefix")
|
||||||
val digitsEndIndex = endIndex - suffix.length
|
val digitsEndIndex = endIndex - suffix.length
|
||||||
checkContainsAt(suffix, digitsEndIndex, endIndex, "suffix")
|
checkContainsAt(digitsEndIndex, endIndex, suffix, ignoreCase, "suffix")
|
||||||
|
|
||||||
checkHexLength(digitsStartIndex, digitsEndIndex, maxDigits, requireMaxLength = false)
|
checkMaxDigits(digitsStartIndex, digitsEndIndex, maxDigits)
|
||||||
|
}
|
||||||
|
|
||||||
var result = 0L
|
private fun String.checkMaxDigits(startIndex: Int, endIndex: Int, maxDigits: Int) {
|
||||||
for (i in digitsStartIndex until digitsEndIndex) {
|
if (startIndex >= endIndex || endIndex - startIndex > maxDigits) {
|
||||||
result = (result shl 4) or decimalFromHexDigitAt(i).toLong()
|
throwInvalidNumberOfDigits(startIndex, endIndex, maxDigits, requireMaxLength = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.parseInt(startIndex: Int, endIndex: Int): Int {
|
||||||
|
var result = 0
|
||||||
|
for (i in startIndex until endIndex) {
|
||||||
|
result = (result shl 4) or decimalFromHexDigitAt(i)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.checkContainsAt(part: String, index: Int, endIndex: Int, partName: String): Int {
|
private fun String.parseLong(startIndex: Int, endIndex: Int): Long {
|
||||||
val end = index + part.length
|
var result = 0L
|
||||||
if (end > endIndex || !regionMatches(index, part, 0, part.length, ignoreCase = true)) {
|
for (i in startIndex until endIndex) {
|
||||||
throw NumberFormatException(
|
result = (result shl 4) or longDecimalFromHexDigitAt(i)
|
||||||
"Expected $partName \"$part\" at index $index, but was ${this.substring(index, end.coerceAtMost(endIndex))}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return end
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.checkHexLength(startIndex: Int, endIndex: Int, maxDigits: Int, requireMaxLength: Boolean) {
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
val digitsLength = endIndex - startIndex
|
private inline fun String.checkContainsAt(index: Int, endIndex: Int, part: String, ignoreCase: Boolean, partName: String): Int {
|
||||||
val isCorrectLength = if (requireMaxLength) digitsLength == maxDigits else digitsLength <= maxDigits
|
if (part.isEmpty()) return index
|
||||||
if (!isCorrectLength) {
|
for (i in part.indices) {
|
||||||
val specifier = if (requireMaxLength) "exactly" else "at most"
|
if (!part[i].equals(this[index + i], ignoreCase)) {
|
||||||
val substring = substring(startIndex, endIndex)
|
throwNotContainedAt(index, endIndex, part, partName)
|
||||||
throw NumberFormatException(
|
}
|
||||||
"Expected $specifier $maxDigits hexadecimal digits at index $startIndex, but was $substring of length $digitsLength"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return index + part.length
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.decimalFromHexDigitAt(index: Int): Int {
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun String.decimalFromHexDigitAt(index: Int): Int {
|
||||||
val code = this[index].code
|
val code = this[index].code
|
||||||
if (code > 127 || HEX_DIGITS_TO_DECIMAL[code] < 0) {
|
if (code ushr 8 == 0 && HEX_DIGITS_TO_DECIMAL[code] >= 0) {
|
||||||
throw NumberFormatException("Expected a hexadecimal digit at index $index, but was ${this[index]}")
|
return HEX_DIGITS_TO_DECIMAL[code]
|
||||||
}
|
}
|
||||||
return HEX_DIGITS_TO_DECIMAL[code]
|
throwInvalidDigitAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun String.longDecimalFromHexDigitAt(index: Int): Long {
|
||||||
|
val code = this[index].code
|
||||||
|
if (code ushr 8 == 0 && HEX_DIGITS_TO_LONG_DECIMAL[code] >= 0) {
|
||||||
|
return HEX_DIGITS_TO_LONG_DECIMAL[code]
|
||||||
|
}
|
||||||
|
throwInvalidDigitAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.throwInvalidNumberOfDigits(startIndex: Int, endIndex: Int, maxDigits: Int, requireMaxLength: Boolean) {
|
||||||
|
val specifier = if (requireMaxLength) "exactly" else "at most"
|
||||||
|
val substring = substring(startIndex, endIndex)
|
||||||
|
throw NumberFormatException(
|
||||||
|
"Expected $specifier $maxDigits hexadecimal digits at index $startIndex, but was $substring of length ${endIndex - startIndex}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.throwNotContainedAt(index: Int, endIndex: Int, part: String, partName: String) {
|
||||||
|
val substring = substring(index, (index + part.length).coerceAtMost(endIndex))
|
||||||
|
throw NumberFormatException(
|
||||||
|
"Expected $partName \"$part\" at index $index, but was $substring"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.throwInvalidPrefixSuffix(startIndex: Int, endIndex: Int, prefix: String, suffix: String) {
|
||||||
|
val substring = substring(startIndex, endIndex)
|
||||||
|
throw NumberFormatException(
|
||||||
|
"Expected a hexadecimal number with prefix \"$prefix\" and suffix \"$suffix\", but was $substring"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.throwInvalidDigitAt(index: Int): Nothing {
|
||||||
|
throw NumberFormatException("Expected a hexadecimal digit at index $index, but was ${this[index]}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ public class HexFormat internal constructor(
|
|||||||
val byteSuffix: String
|
val byteSuffix: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
internal val noLineAndGroupSeparator: Boolean =
|
||||||
|
bytesPerLine == Int.MAX_VALUE && bytesPerGroup == Int.MAX_VALUE
|
||||||
|
|
||||||
|
internal val shortByteSeparatorNoPrefixAndSuffix: Boolean =
|
||||||
|
bytePrefix.isEmpty() && byteSuffix.isEmpty() && byteSeparator.length <= 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to ignore case when parsing format strings.
|
||||||
|
* If false, case-sensitive parsing is conducted, which is faster.
|
||||||
|
*/
|
||||||
|
internal val ignoreCase: Boolean =
|
||||||
|
groupSeparator.isCaseSensitive() ||
|
||||||
|
byteSeparator.isCaseSensitive() ||
|
||||||
|
bytePrefix.isCaseSensitive() ||
|
||||||
|
byteSuffix.isCaseSensitive()
|
||||||
|
|
||||||
override fun toString(): String = buildString {
|
override fun toString(): String = buildString {
|
||||||
append("BytesHexFormat(").appendLine()
|
append("BytesHexFormat(").appendLine()
|
||||||
appendOptionsTo(this, indent = " ").appendLine()
|
appendOptionsTo(this, indent = " ").appendLine()
|
||||||
@@ -227,6 +243,14 @@ public class HexFormat internal constructor(
|
|||||||
val removeLeadingZeros: Boolean
|
val removeLeadingZeros: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
internal val isDigitsOnly: Boolean = prefix.isEmpty() && suffix.isEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to ignore case when parsing format strings.
|
||||||
|
* If false, case-sensitive parsing is conducted, which is faster.
|
||||||
|
*/
|
||||||
|
internal val ignoreCase: Boolean = prefix.isCaseSensitive() || suffix.isCaseSensitive()
|
||||||
|
|
||||||
override fun toString(): String = buildString {
|
override fun toString(): String = buildString {
|
||||||
append("NumberHexFormat(").appendLine()
|
append("NumberHexFormat(").appendLine()
|
||||||
appendOptionsTo(this, indent = " ").appendLine()
|
appendOptionsTo(this, indent = " ").appendLine()
|
||||||
@@ -409,3 +433,9 @@ public class HexFormat internal constructor(
|
|||||||
public inline fun HexFormat(builderAction: HexFormat.Builder.() -> Unit): HexFormat {
|
public inline fun HexFormat(builderAction: HexFormat.Builder.() -> Unit): HexFormat {
|
||||||
return HexFormat.Builder().apply(builderAction).build()
|
return HexFormat.Builder().apply(builderAction).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- private functions ---
|
||||||
|
|
||||||
|
private fun String.isCaseSensitive(): Boolean {
|
||||||
|
return this.any { it >= '\u0080' || it.isLetter() }
|
||||||
|
}
|
||||||
@@ -21,6 +21,28 @@ class BytesHexFormatTest {
|
|||||||
assertContentEquals(bytes.asUByteArray(), expected.hexToUByteArray(format))
|
assertContentEquals(bytes.asUByteArray(), expected.hexToUByteArray(format))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun testFormatAndParse(bytes: ByteArray, startIndex: Int, endIndex: Int, expected: String, format: HexFormat) {
|
||||||
|
val subArray = bytes.copyOfRange(startIndex, endIndex)
|
||||||
|
assertEquals(expected, bytes.toHexString(startIndex, endIndex, format))
|
||||||
|
assertContentEquals(subArray, expected.hexToByteArray(format))
|
||||||
|
|
||||||
|
assertEquals(expected, bytes.asUByteArray().toHexString(startIndex, endIndex, format))
|
||||||
|
assertContentEquals(subArray.asUByteArray(), expected.hexToUByteArray(format))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyByteArray() {
|
||||||
|
testFormatAndParse(byteArrayOf(), "", HexFormat.Default)
|
||||||
|
testFormatAndParse(fourBytes, 2, 2, "", HexFormat.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun invalidRange() {
|
||||||
|
assertFailsWith<IndexOutOfBoundsException> { fourBytes.toHexString(startIndex = -1) }
|
||||||
|
assertFailsWith<IndexOutOfBoundsException> { fourBytes.toHexString(endIndex = 5) }
|
||||||
|
assertFailsWith<IllegalArgumentException> { fourBytes.toHexString(startIndex = 2, endIndex = 1) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun ignoreNumberFormat() {
|
fun ignoreNumberFormat() {
|
||||||
val format = HexFormat {
|
val format = HexFormat {
|
||||||
@@ -38,34 +60,55 @@ class BytesHexFormatTest {
|
|||||||
fun upperCase() {
|
fun upperCase() {
|
||||||
testFormatAndParse(fourBytes, "0A0B0C0D", HexFormat { upperCase = true })
|
testFormatAndParse(fourBytes, "0A0B0C0D", HexFormat { upperCase = true })
|
||||||
testFormatAndParse(fourBytes, "0A0B0C0D", HexFormat.UpperCase)
|
testFormatAndParse(fourBytes, "0A0B0C0D", HexFormat.UpperCase)
|
||||||
|
testFormatAndParse(fourBytes, 0, 3, "0A0B0C", HexFormat.UpperCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun lowerCase() {
|
fun lowerCase() {
|
||||||
testFormatAndParse(fourBytes, "0a0b0c0d", HexFormat { upperCase = false })
|
testFormatAndParse(fourBytes, "0a0b0c0d", HexFormat { upperCase = false })
|
||||||
|
testFormatAndParse(fourBytes, 1, 4, "0b0c0d", HexFormat { upperCase = false })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun defaultCase() {
|
fun defaultCase() {
|
||||||
testFormatAndParse(fourBytes, "0a0b0c0d", HexFormat.Default)
|
testFormatAndParse(fourBytes, "0a0b0c0d", HexFormat.Default)
|
||||||
|
testFormatAndParse(fourBytes, 1, 3, "0b0c", HexFormat.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun byteSeparatorPrefixSuffix() {
|
fun byteSeparatorPrefixSuffix() {
|
||||||
// byteSeparator
|
// byteSeparator
|
||||||
testFormatAndParse(fourBytes, "0a 0b 0c 0d", HexFormat { bytes.byteSeparator = " " })
|
HexFormat { bytes.byteSeparator = " " }.let { format ->
|
||||||
|
testFormatAndParse(fourBytes, "0a 0b 0c 0d", format)
|
||||||
|
testFormatAndParse(fourBytes, 0, 1, "0a", format)
|
||||||
|
assertFailsWith<NumberFormatException> { "0a-0b 0c 0d".hexToByteArray(format) }
|
||||||
|
assertFailsWith<NumberFormatException> { "0a0b 0c 0d".hexToByteArray(format) }
|
||||||
|
}
|
||||||
// bytePrefix
|
// bytePrefix
|
||||||
testFormatAndParse(fourBytes, "0x0a0x0b0x0c0x0d", HexFormat { bytes.bytePrefix = "0x" })
|
HexFormat { bytes.bytePrefix = "0x" }.let { format ->
|
||||||
// bytePrefix
|
testFormatAndParse(fourBytes, "0x0a0x0b0x0c0x0d", format)
|
||||||
testFormatAndParse(fourBytes, "0a;0b;0c;0d;", HexFormat { bytes.byteSuffix = ";" })
|
testFormatAndParse(fourBytes, 2, 4, "0x0c0x0d", HexFormat { bytes.bytePrefix = "0x" })
|
||||||
|
assertFailsWith<NumberFormatException> { "0x0a0x0b0x0c0#0d".hexToByteArray(format) }
|
||||||
|
assertFailsWith<NumberFormatException> { "0x0a0x0b0x0c0d".hexToByteArray(format) }
|
||||||
|
}
|
||||||
|
// byteSuffix
|
||||||
|
HexFormat { bytes.byteSuffix = ";" }.let { format ->
|
||||||
|
testFormatAndParse(fourBytes, "0a;0b;0c;0d;", format)
|
||||||
|
testFormatAndParse(fourBytes, 0, 4, "0a;0b;0c;0d;", format)
|
||||||
|
assertFailsWith<NumberFormatException> { "0a;0b:0c;0d;".hexToByteArray(format) }
|
||||||
|
assertFailsWith<NumberFormatException> { "0a;0b0c;0d;".hexToByteArray(format) }
|
||||||
|
}
|
||||||
// all together
|
// all together
|
||||||
testFormatAndParse(fourBytes, "0x0a; 0x0b; 0x0c; 0x0d;", HexFormat {
|
HexFormat {
|
||||||
bytes {
|
bytes {
|
||||||
byteSeparator = " "
|
byteSeparator = " "
|
||||||
bytePrefix = "0x"
|
bytePrefix = "0x"
|
||||||
byteSuffix = ";"
|
byteSuffix = ";"
|
||||||
}
|
}
|
||||||
})
|
}.let { format ->
|
||||||
|
testFormatAndParse(fourBytes, "0x0a; 0x0b; 0x0c; 0x0d;", format)
|
||||||
|
testFormatAndParse(fourBytes, 1, 3, "0x0b; 0x0c;", format)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -75,6 +118,8 @@ class BytesHexFormatTest {
|
|||||||
val format = HexFormat { bytes.bytesPerLine = 8 }
|
val format = HexFormat { bytes.bytesPerLine = 8 }
|
||||||
val expected = "0001020304050607\n08090a0b0c0d0e0f\n10111213"
|
val expected = "0001020304050607\n08090a0b0c0d0e0f\n10111213"
|
||||||
testFormatAndParse(twentyBytes, expected, format)
|
testFormatAndParse(twentyBytes, expected, format)
|
||||||
|
val missingNewLine = "0001020304050607\n08090a0b0c0d0e0f10111213"
|
||||||
|
assertFailsWith<NumberFormatException> { missingNewLine.hexToByteArray(format) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The last line is not ended by the line separator
|
// The last line is not ended by the line separator
|
||||||
@@ -116,7 +161,9 @@ class BytesHexFormatTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun bytesPerLineAndBytesPerGroup() {
|
fun bytesPerLineAndBytesPerGroup() {
|
||||||
val format = HexFormat {
|
val byteArray = ByteArray(31) { it.toByte() }
|
||||||
|
|
||||||
|
HexFormat {
|
||||||
upperCase = true
|
upperCase = true
|
||||||
bytes {
|
bytes {
|
||||||
bytesPerLine = 10
|
bytesPerLine = 10
|
||||||
@@ -126,18 +173,22 @@ class BytesHexFormatTest {
|
|||||||
bytePrefix = "#"
|
bytePrefix = "#"
|
||||||
byteSuffix = ";"
|
byteSuffix = ";"
|
||||||
}
|
}
|
||||||
|
}.let { format ->
|
||||||
|
val expected = """
|
||||||
|
#00; #01; #02; #03;---#04; #05; #06; #07;---#08; #09;
|
||||||
|
#0A; #0B; #0C; #0D;---#0E; #0F; #10; #11;---#12; #13;
|
||||||
|
#14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D;
|
||||||
|
#1E;
|
||||||
|
""".trimIndent()
|
||||||
|
testFormatAndParse(byteArray, expected, format)
|
||||||
|
|
||||||
|
val expectedRange = """
|
||||||
|
#05; #06; #07; #08;---#09; #0A; #0B; #0C;---#0D; #0E;
|
||||||
|
#0F; #10; #11; #12;---#13; #14; #15;
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
testFormatAndParse(byteArray, 5, 22, expectedRange, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = ByteArray(31) { it.toByte() }
|
|
||||||
|
|
||||||
val expected = """
|
|
||||||
#00; #01; #02; #03;---#04; #05; #06; #07;---#08; #09;
|
|
||||||
#0A; #0B; #0C; #0D;---#0E; #0F; #10; #11;---#12; #13;
|
|
||||||
#14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D;
|
|
||||||
#1E;
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
testFormatAndParse(byteArray, expected, format)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -396,7 +447,7 @@ class BytesHexFormatTest {
|
|||||||
run {
|
run {
|
||||||
// 00010203\n04050607\n08090a0b\n0c0d0e0f\n10111213
|
// 00010203\n04050607\n08090a0b\n0c0d0e0f\n10111213
|
||||||
val length = formattedStringLength(
|
val length = formattedStringLength(
|
||||||
totalBytes = 20,
|
numberOfBytes = 20,
|
||||||
bytesPerLine = 4,
|
bytesPerLine = 4,
|
||||||
bytesPerGroup = Int.MAX_VALUE,
|
bytesPerGroup = Int.MAX_VALUE,
|
||||||
groupSeparatorLength = 2,
|
groupSeparatorLength = 2,
|
||||||
@@ -409,7 +460,7 @@ class BytesHexFormatTest {
|
|||||||
run {
|
run {
|
||||||
// 0001020304050607---08090a0b0c0d0e0f---10111213
|
// 0001020304050607---08090a0b0c0d0e0f---10111213
|
||||||
val length = formattedStringLength(
|
val length = formattedStringLength(
|
||||||
totalBytes = 20,
|
numberOfBytes = 20,
|
||||||
bytesPerLine = Int.MAX_VALUE,
|
bytesPerLine = Int.MAX_VALUE,
|
||||||
bytesPerGroup = 8,
|
bytesPerGroup = 8,
|
||||||
groupSeparatorLength = 3,
|
groupSeparatorLength = 3,
|
||||||
@@ -425,7 +476,7 @@ class BytesHexFormatTest {
|
|||||||
// #14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D;
|
// #14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D;
|
||||||
// #1E;
|
// #1E;
|
||||||
val length = formattedStringLength(
|
val length = formattedStringLength(
|
||||||
totalBytes = 31,
|
numberOfBytes = 31,
|
||||||
bytesPerLine = 10,
|
bytesPerLine = 10,
|
||||||
bytesPerGroup = 4,
|
bytesPerGroup = 4,
|
||||||
groupSeparatorLength = 3,
|
groupSeparatorLength = 3,
|
||||||
@@ -437,7 +488,7 @@ class BytesHexFormatTest {
|
|||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val length = formattedStringLength(
|
val length = formattedStringLength(
|
||||||
totalBytes = Int.MAX_VALUE / 2,
|
numberOfBytes = Int.MAX_VALUE / 2,
|
||||||
bytesPerLine = Int.MAX_VALUE,
|
bytesPerLine = Int.MAX_VALUE,
|
||||||
bytesPerGroup = Int.MAX_VALUE,
|
bytesPerGroup = Int.MAX_VALUE,
|
||||||
groupSeparatorLength = 0,
|
groupSeparatorLength = 0,
|
||||||
@@ -449,7 +500,7 @@ class BytesHexFormatTest {
|
|||||||
}
|
}
|
||||||
assertFailsWith<IllegalArgumentException> {
|
assertFailsWith<IllegalArgumentException> {
|
||||||
formattedStringLength(
|
formattedStringLength(
|
||||||
totalBytes = Int.MAX_VALUE,
|
numberOfBytes = Int.MAX_VALUE,
|
||||||
bytesPerLine = Int.MAX_VALUE,
|
bytesPerLine = Int.MAX_VALUE,
|
||||||
bytesPerGroup = Int.MAX_VALUE,
|
bytesPerGroup = Int.MAX_VALUE,
|
||||||
groupSeparatorLength = Int.MAX_VALUE,
|
groupSeparatorLength = Int.MAX_VALUE,
|
||||||
@@ -460,7 +511,7 @@ class BytesHexFormatTest {
|
|||||||
}
|
}
|
||||||
assertFailsWith<IllegalArgumentException> {
|
assertFailsWith<IllegalArgumentException> {
|
||||||
formattedStringLength(
|
formattedStringLength(
|
||||||
totalBytes = Int.MAX_VALUE / 2 + 1,
|
numberOfBytes = Int.MAX_VALUE / 2 + 1,
|
||||||
bytesPerLine = Int.MAX_VALUE,
|
bytesPerLine = Int.MAX_VALUE,
|
||||||
bytesPerGroup = Int.MAX_VALUE,
|
bytesPerGroup = Int.MAX_VALUE,
|
||||||
groupSeparatorLength = 0,
|
groupSeparatorLength = 0,
|
||||||
|
|||||||
@@ -16,35 +16,35 @@ class NumberHexFormatTest {
|
|||||||
private const val longHex3A: String = "000000000000003A" // 16 hex digits
|
private const val longHex3A: String = "000000000000003A" // 16 hex digits
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testFormatAndParse(number: Long, string: String, format: HexFormat) {
|
private fun testFormatAndParse(number: Long, digits: String, format: HexFormat) {
|
||||||
testFormat(number, string, format)
|
testFormat(number, digits, format)
|
||||||
testParse(string, number, format)
|
testParse(digits, number, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testFormat(number: Long, string: String, format: HexFormat) {
|
private fun testFormat(number: Long, digits: String, format: HexFormat) {
|
||||||
require(format.number.prefix.isEmpty() && format.number.suffix.isEmpty())
|
fun String.withPrefixAndSuffix(): String = format.number.prefix + this + format.number.suffix
|
||||||
|
|
||||||
assertEquals(string.takeLast(2), number.toByte().toHexString(format))
|
assertEquals(digits.takeLast(2).withPrefixAndSuffix(), number.toByte().toHexString(format))
|
||||||
assertEquals(string.takeLast(2), number.toUByte().toHexString(format))
|
assertEquals(digits.takeLast(2).withPrefixAndSuffix(), number.toUByte().toHexString(format))
|
||||||
assertEquals(string.takeLast(4), number.toShort().toHexString(format))
|
assertEquals(digits.takeLast(4).withPrefixAndSuffix(), number.toShort().toHexString(format))
|
||||||
assertEquals(string.takeLast(4), number.toUShort().toHexString(format))
|
assertEquals(digits.takeLast(4).withPrefixAndSuffix(), number.toUShort().toHexString(format))
|
||||||
assertEquals(string.takeLast(8), number.toInt().toHexString(format))
|
assertEquals(digits.takeLast(8).withPrefixAndSuffix(), number.toInt().toHexString(format))
|
||||||
assertEquals(string.takeLast(8), number.toUInt().toHexString(format))
|
assertEquals(digits.takeLast(8).withPrefixAndSuffix(), number.toUInt().toHexString(format))
|
||||||
assertEquals(string, number.toHexString(format))
|
assertEquals(digits.withPrefixAndSuffix(), number.toHexString(format))
|
||||||
assertEquals(string, number.toULong().toHexString(format))
|
assertEquals(digits.withPrefixAndSuffix(), number.toULong().toHexString(format))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testParse(string: String, number: Long, format: HexFormat) {
|
private fun testParse(digits: String, number: Long, format: HexFormat) {
|
||||||
require(format.number.prefix.isEmpty() && format.number.suffix.isEmpty())
|
fun String.withPrefixAndSuffix(): String = format.number.prefix + this + format.number.suffix
|
||||||
|
|
||||||
assertEquals(number.toByte(), string.takeLast(2).hexToByte(format))
|
assertEquals(number.toByte(), digits.takeLast(2).withPrefixAndSuffix().hexToByte(format))
|
||||||
assertEquals(number.toUByte(), string.takeLast(2).hexToUByte(format))
|
assertEquals(number.toUByte(), digits.takeLast(2).withPrefixAndSuffix().hexToUByte(format))
|
||||||
assertEquals(number.toShort(), string.takeLast(4).hexToShort(format))
|
assertEquals(number.toShort(), digits.takeLast(4).withPrefixAndSuffix().hexToShort(format))
|
||||||
assertEquals(number.toUShort(), string.takeLast(4).hexToUShort(format))
|
assertEquals(number.toUShort(), digits.takeLast(4).withPrefixAndSuffix().hexToUShort(format))
|
||||||
assertEquals(number.toInt(), string.takeLast(8).hexToInt(format))
|
assertEquals(number.toInt(), digits.takeLast(8).withPrefixAndSuffix().hexToInt(format))
|
||||||
assertEquals(number.toUInt(), string.takeLast(8).hexToUInt(format))
|
assertEquals(number.toUInt(), digits.takeLast(8).withPrefixAndSuffix().hexToUInt(format))
|
||||||
assertEquals(number, string.hexToLong(format))
|
assertEquals(number, digits.withPrefixAndSuffix().hexToLong(format))
|
||||||
assertEquals(number.toULong(), string.hexToULong(format))
|
assertEquals(number.toULong(), digits.withPrefixAndSuffix().hexToULong(format))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -92,10 +92,10 @@ class NumberHexFormatTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun formatPrefixSuffix() {
|
fun formatAndParsePrefixSuffix() {
|
||||||
assertEquals("0x0000003a", 58.toHexString(HexFormat { number.prefix = "0x" }))
|
testFormatAndParse(58, "000000000000003a", HexFormat { number.prefix = "0x" })
|
||||||
assertEquals("0000003ah", 58.toHexString(HexFormat { number.suffix = "h" }))
|
testFormatAndParse(58, "000000000000003a", HexFormat { number.suffix = "h" })
|
||||||
assertEquals("0x0000003ah", 58.toHexString(HexFormat { number.prefix = "0x"; number.suffix = "h" }))
|
testFormatAndParse(58, "000000000000003a", HexFormat { number.prefix = "0x"; number.suffix = "h" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -130,6 +130,14 @@ class NumberHexFormatTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parseInvalidDigit() {
|
||||||
|
assertFailsWith<NumberFormatException> { "3g".hexToByte() }
|
||||||
|
assertFailsWith<NumberFormatException> { "3g".hexToShort() }
|
||||||
|
assertFailsWith<NumberFormatException> { "3g".hexToInt() }
|
||||||
|
assertFailsWith<NumberFormatException> { "3g".hexToLong() }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseRequiresAtLeastOneHexDigit() {
|
fun parseRequiresAtLeastOneHexDigit() {
|
||||||
assertFailsWith<NumberFormatException> {
|
assertFailsWith<NumberFormatException> {
|
||||||
|
|||||||
Reference in New Issue
Block a user