commonMain.kotlin.text.HexExtensions.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-stdlib Show documentation
Show all versions of kotlin-stdlib Show documentation
Kotlin Standard Library for JVM
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.text
// Benchmarks repository: https://github.com/qurbonzoda/KotlinHexFormatBenchmarks
private const val LOWER_CASE_HEX_DIGITS = "0123456789abcdef"
private const val UPPER_CASE_HEX_DIGITS = "0123456789ABCDEF"
/**
* 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.
*/
@ExperimentalStdlibApi
internal 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 }
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 --------------------------
/**
* Formats bytes in this array using the specified [format].
*
* Note that only the [HexFormat.upperCase] and [HexFormat.bytes] properties of the [format] instance affect
* the formatting result of this byte array.
*
* Each byte in the array is converted into two hexadecimal digits. The first digit represents the most significant 4 bits,
* and the second digit represents the least significant 4 bits of the byte. The [HexFormat.upperCase] property determines whether
* upper-case (`0-9`, `A-F`) or lower-case (`0-9`, `a-f`) hexadecimal digits are used for this conversion.
* The [HexFormat.bytes] property specifies the strings that prefix and suffix each byte representation, and defines
* how these representations are arranged.
*
* Refer to [HexFormat.BytesHexFormat] for details about the available format options and their impact on formatting.
*
* @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default.
* @return the result of formatting this array using the specified [format].
*
* @throws IllegalArgumentException if the result length exceeds the maximum capacity of [String].
*
* @sample samples.text.HexFormats.Extensions.byteArrayToHexString
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun ByteArray.toHexString(format: HexFormat = HexFormat.Default): String = toHexString(0, size, format)
/**
* Formats bytes in this array using the specified [format].
*
* Note that only the [HexFormat.upperCase] and [HexFormat.bytes] properties of the [format] instance affect
* the formatting result of this byte array.
*
* Each byte in the array is converted into two hexadecimal digits. The first digit represents the most significant 4 bits,
* and the second digit represents the least significant 4 bits of the byte. The [HexFormat.upperCase] property determines whether
* upper-case (`0-9`, `A-F`) or lower-case (`0-9`, `a-f`) hexadecimal digits are used for this conversion.
* The [HexFormat.bytes] property specifies the strings that prefix and suffix each byte representation, and defines
* how these representations are arranged.
*
* Refer to [HexFormat.BytesHexFormat] for details about the available format options and their impact on formatting.
*
* @param startIndex the beginning (inclusive) of the subrange to format, 0 by default.
* @param endIndex the end (exclusive) of the subrange to format, size of this array by default.
* @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default.
* @return the result of formatting this array using the specified [format].
*
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this array indices.
* @throws IllegalArgumentException when `startIndex > endIndex`.
* @throws IllegalArgumentException if the result length exceeds the maximum capacity of [String].
*
* @sample samples.text.HexFormats.Extensions.byteArrayToHexString
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun ByteArray.toHexString(
startIndex: Int = 0,
endIndex: Int = size,
format: HexFormat = HexFormat.Default
): String {
AbstractList.checkBoundsIndexes(startIndex, endIndex, size)
if (startIndex == endIndex) {
return ""
}
val byteToDigits = if (format.upperCase) BYTE_TO_UPPER_CASE_HEX_DIGITS else BYTE_TO_LOWER_CASE_HEX_DIGITS
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 bytesPerGroup = bytesFormat.bytesPerGroup
val bytePrefix = bytesFormat.bytePrefix
val byteSuffix = bytesFormat.byteSuffix
val byteSeparator = bytesFormat.byteSeparator
val groupSeparator = bytesFormat.groupSeparator
val formatLength = formattedStringLength(
numberOfBytes = endIndex - startIndex,
bytesPerLine,
bytesPerGroup,
groupSeparator.length,
byteSeparator.length,
bytePrefix.length,
byteSuffix.length
)
val charArray = CharArray(formatLength)
var charIndex = 0
var indexInLine = 0
var indexInGroup = 0
for (byteIndex in startIndex until endIndex) {
if (indexInLine == bytesPerLine) {
charArray[charIndex++] = '\n'
indexInLine = 0
indexInGroup = 0
} else if (indexInGroup == bytesPerGroup) {
charIndex = groupSeparator.toCharArrayIfNotEmpty(charArray, charIndex)
indexInGroup = 0
}
if (indexInGroup != 0) {
charIndex = byteSeparator.toCharArrayIfNotEmpty(charArray, charIndex)
}
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
internal fun formattedStringLength(
numberOfBytes: Int,
bytesPerLine: Int,
bytesPerGroup: Int,
groupSeparatorLength: Int,
byteSeparatorLength: Int,
bytePrefixLength: Int,
byteSuffixLength: Int
): Int {
require(numberOfBytes > 0)
// By contract bytesPerLine and bytesPerGroup are > 0
val lineSeparators = (numberOfBytes - 1) / bytesPerLine
val groupSeparators = run {
val groupSeparatorsPerLine = (bytesPerLine - 1) / bytesPerGroup
val bytesInLastLine = (numberOfBytes % bytesPerLine).let { if (it == 0) bytesPerLine else it }
val groupSeparatorsInLastLine = (bytesInLastLine - 1) / bytesPerGroup
lineSeparators * groupSeparatorsPerLine + groupSeparatorsInLastLine
}
val byteSeparators = numberOfBytes - 1 - lineSeparators - groupSeparators
// The max formatLength is achieved when
// numberOfBytes, bytePrefix/Suffix/Separator.length = 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.
val formatLength: Long = lineSeparators.toLong() /* * lineSeparator.length = 1 */ +
groupSeparators.toLong() * groupSeparatorLength.toLong() +
byteSeparators.toLong() * byteSeparatorLength.toLong() +
numberOfBytes.toLong() * (bytePrefixLength.toLong() + 2L + byteSuffixLength.toLong())
return checkFormatLength(formatLength)
}
private fun checkFormatLength(formatLength: Long): Int {
if (formatLength !in 0..Int.MAX_VALUE) {
// TODO: Common OutOfMemoryError?
throw IllegalArgumentException("The resulting string length is too big: ${formatLength.toULong()}")
}
return formatLength.toInt()
}
/**
* Parses a byte array from this string using the specified [format].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.bytes] property of the [format] instance affects the parsing of a byte array.
*
* Parsing is performed in a case-insensitive manner for both the hexadecimal digits and the elements
* (prefix, suffix, separators) defined in the [HexFormat.bytes] property. Additionally, any of the
* char sequences CRLF, LF and CR is considered a valid line separator.
*
* Refer to [HexFormat.BytesHexFormat] for details about the available format options and their impact on parsing.
*
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the byte array parsed from this string.
*
* @throws IllegalArgumentException if this string does not conform to the specified [format].
*
* @sample samples.text.HexFormats.Extensions.hexToByteArray
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun String.hexToByteArray(format: HexFormat = HexFormat.Default): ByteArray = hexToByteArray(0, length, format)
/**
* Parses a byte array from this string using the specified [format].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.bytes] property of the [format] instance affects the parsing of a byte array.
*
* Parsing is performed in a case-insensitive manner for both the hexadecimal digits and the elements
* (prefix, suffix, separators) defined in the [HexFormat.bytes] property. Additionally, any of the
* char sequences CRLF, LF and CR is considered a valid line separator.
*
* Refer to [HexFormat.BytesHexFormat] for details about the available format options and their impact on parsing.
*
* @param startIndex the beginning (inclusive) of the substring to parse, 0 by default.
* @param endIndex the end (exclusive) of the substring to parse, length of this string by default.
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the byte array parsed from this string.
*
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices.
* @throws IllegalArgumentException when `startIndex > endIndex`.
* @throws IllegalArgumentException if the substring does not conform to the specified [format].
*/
@ExperimentalStdlibApi
//@SinceKotlin("1.9")
private fun String.hexToByteArray(
startIndex: Int = 0,
endIndex: Int = length,
format: HexFormat = HexFormat.Default
): ByteArray {
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
if (startIndex == endIndex) {
return byteArrayOf()
}
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 bytesPerGroup = bytesFormat.bytesPerGroup
val bytePrefix = bytesFormat.bytePrefix
val byteSuffix = bytesFormat.byteSuffix
val byteSeparator = bytesFormat.byteSeparator
val groupSeparator = bytesFormat.groupSeparator
val ignoreCase = bytesFormat.ignoreCase
val parseMaxSize = parsedByteArrayMaxSize(
stringLength = endIndex - startIndex,
bytesPerLine,
bytesPerGroup,
groupSeparator.length,
byteSeparator.length,
bytePrefix.length,
byteSuffix.length
)
val byteArray = ByteArray(parseMaxSize)
var charIndex = startIndex
var byteIndex = 0
var indexInLine = 0
var indexInGroup = 0
while (charIndex < endIndex) {
if (indexInLine == bytesPerLine) {
charIndex = checkNewLineAt(charIndex, endIndex)
indexInLine = 0
indexInGroup = 0
} else if (indexInGroup == bytesPerGroup) {
charIndex = checkContainsAt(charIndex, endIndex, groupSeparator, ignoreCase, "group separator")
indexInGroup = 0
} else if (indexInGroup != 0) {
charIndex = checkContainsAt(charIndex, endIndex, byteSeparator, ignoreCase, "byte separator")
}
indexInLine += 1
indexInGroup += 1
charIndex = checkContainsAt(charIndex, endIndex, bytePrefix, ignoreCase, "byte prefix")
if (endIndex - 2 < charIndex) {
throwInvalidNumberOfDigits(charIndex, endIndex, "exactly", 2)
}
byteArray[byteIndex++] = parseByteAt(charIndex)
charIndex = checkContainsAt(charIndex + 2, endIndex, byteSuffix, ignoreCase, "byte suffix")
}
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
internal fun parsedByteArrayMaxSize(
stringLength: Int,
bytesPerLine: Int,
bytesPerGroup: Int,
groupSeparatorLength: Int,
byteSeparatorLength: Int,
bytePrefixLength: Int,
byteSuffixLength: Int
): Int {
require(stringLength > 0)
// By contract bytesPerLine and bytesPerGroup are > 0
// The max charsPerSet is achieved when
// bytesPerLine/Group, bytePrefix/Suffix/SeparatorLength = 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.
val charsPerByte = bytePrefixLength + 2L + byteSuffixLength
val charsPerGroup = charsPerSet(charsPerByte, bytesPerGroup, byteSeparatorLength)
val charsPerLine = if (bytesPerLine <= bytesPerGroup) {
charsPerSet(charsPerByte, bytesPerLine, byteSeparatorLength)
} else {
val groupsPerLine = bytesPerLine / bytesPerGroup
var result = charsPerSet(charsPerGroup, groupsPerLine, groupSeparatorLength)
val bytesPerLastGroupInLine = bytesPerLine % bytesPerGroup
if (bytesPerLastGroupInLine != 0) {
result += groupSeparatorLength
result += charsPerSet(charsPerByte, bytesPerLastGroupInLine, byteSeparatorLength)
}
result
}
var numberOfChars = stringLength.toLong()
// assume one-character line separator to maximize size
val wholeLines = wholeElementsPerSet(numberOfChars, charsPerLine, 1)
numberOfChars -= wholeLines * (charsPerLine + 1)
val wholeGroupsInLastLine = wholeElementsPerSet(numberOfChars, charsPerGroup, groupSeparatorLength)
numberOfChars -= wholeGroupsInLastLine * (charsPerGroup + groupSeparatorLength)
val wholeBytesInLastGroup = wholeElementsPerSet(numberOfChars, charsPerByte, byteSeparatorLength)
numberOfChars -= wholeBytesInLastGroup * (charsPerByte + byteSeparatorLength)
// If numberOfChars is bigger than zero here:
// * CRLF might have been used as line separator
// * or there are dangling characters at the end of string
// Anyhow, have a spare capacity to let parsing continue.
// In case of dangling characters it will throw later on with a correct message.
val spare = if (numberOfChars > 0L) 1 else 0
// The number of parsed bytes will always fit into Int, each parsed byte consumes at least 2 chars of the input string.
return ((wholeLines * bytesPerLine) + (wholeGroupsInLastLine * bytesPerGroup) + wholeBytesInLastGroup + spare).toInt()
}
private fun charsPerSet(charsPerElement: Long, elementsPerSet: Int, elementSeparatorLength: Int): Long {
require(elementsPerSet > 0)
return (charsPerElement * elementsPerSet) + (elementSeparatorLength * (elementsPerSet - 1L))
}
private fun wholeElementsPerSet(charsPerSet: Long, charsPerElement: Long, elementSeparatorLength: Int): Long {
return if (charsPerSet <= 0 || charsPerElement <= 0) 0
else (charsPerSet + elementSeparatorLength) / (charsPerElement + elementSeparatorLength)
}
private fun String.checkNewLineAt(index: Int, endIndex: Int): Int {
return if (this[index] == '\r') {
if (index + 1 < endIndex && this[index + 1] == '\n') index + 2 else index + 1
} else if (this[index] == '\n') {
index + 1
} else {
throw NumberFormatException("Expected a new line at index $index, but was ${this[index]}")
}
}
// -------------------------- format and parse Byte --------------------------
/**
* Formats this `Byte` value using the specified [format].
*
* Note that only the [HexFormat.upperCase] and [HexFormat.number] properties of the [format] instance affect
* the formatting result of this numeric value.
*
* This function converts the `Byte` value into its hexadecimal representation by mapping each four-bit chunk
* of its binary representation to the corresponding hexadecimal digit, starting from the most significant bits.
* The [HexFormat.upperCase] property determines whether upper-case (`0-9`, `A-F`) or lower-case (`0-9`, `a-f`)
* hexadecimal digits are used for this conversion.
* The [HexFormat.number] property adjusts the length of the hexadecimal representation by adding or removing
* leading zeros as needed, and specifies the strings that prefix and suffix the resulting representation.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on formatting.
*
* @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default.
* @return the result of formatting this value using the specified [format].
*
* @sample samples.text.HexFormats.Extensions.byteToHexString
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
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.isDigitsOnlyAndNoPadding) {
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].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds two, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into an 8-bit `Byte`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Byte` value parsed from this string.
*
* @throws IllegalArgumentException if this string does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into a `Byte`.
*
* @sample samples.text.HexFormats.Extensions.hexToByte
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun String.hexToByte(format: HexFormat = HexFormat.Default): Byte = hexToByte(0, length, format)
/**
* Parses a `Byte` value from this string using the specified [format].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds two, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into an 8-bit `Byte`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param startIndex the beginning (inclusive) of the substring to parse, 0 by default.
* @param endIndex the end (exclusive) of the substring to parse, length of this string by default.
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Byte` value parsed from this string.
*
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices.
* @throws IllegalArgumentException when `startIndex > endIndex`.
* @throws IllegalArgumentException if the substring does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into a `Byte`.
*/
@ExperimentalStdlibApi
//@SinceKotlin("1.9")
private fun String.hexToByte(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Byte =
hexToIntImpl(startIndex, endIndex, format, typeHexLength = 2).toByte()
// -------------------------- format and parse Short --------------------------
/**
* Formats this `Short` value using the specified [format].
*
* Note that only the [HexFormat.upperCase] and [HexFormat.number] properties of the [format] instance affect
* the formatting result of this numeric value.
*
* This function converts the `Short` value into its hexadecimal representation by mapping each four-bit chunk
* of its binary representation to the corresponding hexadecimal digit, starting from the most significant bits.
* The [HexFormat.upperCase] property determines whether upper-case (`0-9`, `A-F`) or lower-case (`0-9`, `a-f`)
* hexadecimal digits are used for this conversion.
* The [HexFormat.number] property adjusts the length of the hexadecimal representation by adding or removing
* leading zeros as needed, and specifies the strings that prefix and suffix the resulting representation.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on formatting.
*
* @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default.
* @return the result of formatting this value using the specified [format].
*
* @sample samples.text.HexFormats.Extensions.shortToHexString
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
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.isDigitsOnlyAndNoPadding) {
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].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds four, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into a 16-bit `Short`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Short` value parsed from this string.
*
* @throws IllegalArgumentException if this string does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into a `Short`.
*
* @sample samples.text.HexFormats.Extensions.hexToShort
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun String.hexToShort(format: HexFormat = HexFormat.Default): Short = hexToShort(0, length, format)
/**
* Parses a `Short` value from this string using the specified [format].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds four, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into a 16-bit `Short`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param startIndex the beginning (inclusive) of the substring to parse, 0 by default.
* @param endIndex the end (exclusive) of the substring to parse, length of this string by default.
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Short` value parsed from this string.
*
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices.
* @throws IllegalArgumentException when `startIndex > endIndex`.
* @throws IllegalArgumentException if the substring does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into a `Short`.
*/
@ExperimentalStdlibApi
//@SinceKotlin("1.9")
private fun String.hexToShort(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Short =
hexToIntImpl(startIndex, endIndex, format, typeHexLength = 4).toShort()
// -------------------------- format and parse Int --------------------------
/**
* Formats this `Int` value using the specified [format].
*
* Note that only the [HexFormat.upperCase] and [HexFormat.number] properties of the [format] instance affect
* the formatting result of this numeric value.
*
* This function converts the `Int` value into its hexadecimal representation by mapping each four-bit chunk
* of its binary representation to the corresponding hexadecimal digit, starting from the most significant bits.
* The [HexFormat.upperCase] property determines whether upper-case (`0-9`, `A-F`) or lower-case (`0-9`, `a-f`)
* hexadecimal digits are used for this conversion.
* The [HexFormat.number] property adjusts the length of the hexadecimal representation by adding or removing
* leading zeros as needed, and specifies the strings that prefix and suffix the resulting representation.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on formatting.
*
* @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default.
* @return the result of formatting this value using the specified [format].
*
* @sample samples.text.HexFormats.Extensions.intToHexString
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
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.isDigitsOnlyAndNoPadding) {
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].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds eight, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into a 32-bit `Int`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Int` value parsed from this string.
*
* @throws IllegalArgumentException if this string does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into an `Int`.
*
* @sample samples.text.HexFormats.Extensions.hexToInt
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun String.hexToInt(format: HexFormat = HexFormat.Default): Int = hexToInt(0, length, format)
/**
* Parses an `Int` value from this string using the specified [format].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds eight, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into a 32-bit `Int`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param startIndex the beginning (inclusive) of the substring to parse, 0 by default.
* @param endIndex the end (exclusive) of the substring to parse, length of this string by default.
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Int` value parsed from this string.
*
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices.
* @throws IllegalArgumentException when `startIndex > endIndex`.
* @throws IllegalArgumentException if the substring does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into an `Int`.
*/
@ExperimentalStdlibApi
//@SinceKotlin("1.9")
private fun String.hexToInt(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Int =
hexToIntImpl(startIndex, endIndex, format, typeHexLength = 8)
// -------------------------- format and parse Long --------------------------
/**
* Formats this `Long` value using the specified [format].
*
* Note that only the [HexFormat.upperCase] and [HexFormat.number] properties of the [format] instance affect
* the formatting result of this numeric value.
*
* This function converts the `Long` value into its hexadecimal representation by mapping each four-bit chunk
* of its binary representation to the corresponding hexadecimal digit, starting from the most significant bits.
* The [HexFormat.upperCase] property determines whether upper-case (`0-9`, `A-F`) or lower-case (`0-9`, `a-f`)
* hexadecimal digits are used for this conversion.
* The [HexFormat.number] property adjusts the length of the hexadecimal representation by adding or removing
* leading zeros as needed, and specifies the strings that prefix and suffix the resulting representation.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on formatting.
*
* @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default.
* @return the result of formatting this value using the specified [format].
*
* @sample samples.text.HexFormats.Extensions.longToHexString
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
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.isDigitsOnlyAndNoPadding) {
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].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds 16, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into a 64-bit `Long`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Long` value parsed from this string.
*
* @throws IllegalArgumentException if this string does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into a `Long`.
*
* @sample samples.text.HexFormats.Extensions.hexToLong
*/
@ExperimentalStdlibApi
@SinceKotlin("1.9")
public fun String.hexToLong(format: HexFormat = HexFormat.Default): Long = hexToLong(0, length, format)
/**
* Parses a `Long` value from this string using the specified [format].
*
* The string must conform to the structure defined by the [format].
* Note that only the [HexFormat.number] property of the [format] instance affects the parsing of a numeric value.
*
* The input string must start with the prefix and end with the suffix defined in the [HexFormat.number] property.
* It must also contain at least one hexadecimal digit between them. If the number of hexadecimal digits
* exceeds 16, the excess leading digits must be zeros. This ensures that the value represented by the
* hexadecimal digits fits into a 64-bit `Long`.
* Parsing is performed in a case-insensitive manner, including for the hexadecimal digits, prefix, and suffix.
*
* Refer to [HexFormat.NumberHexFormat] for details about the available format options and their impact on parsing.
*
* @param startIndex the beginning (inclusive) of the substring to parse, 0 by default.
* @param endIndex the end (exclusive) of the substring to parse, length of this string by default.
* @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default.
* @return the `Long` value parsed from this string.
*
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices.
* @throws IllegalArgumentException when `startIndex > endIndex`.
* @throws IllegalArgumentException if the substring does not conform to the specified [format], or if the hexadecimal
* digits represent a value that does not fit into a `Long`.
*/
@ExperimentalStdlibApi
//@SinceKotlin("1.9")
internal fun String.hexToLong(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Long =
hexToLongImpl(startIndex, endIndex, format, typeHexLength = 16)
// -------------------------- private format and parse functions --------------------------
@ExperimentalStdlibApi
private fun Long.toHexStringImpl(numberFormat: HexFormat.NumberHexFormat, digits: String, bits: Int): String {
require(bits and 0x3 == 0)
val value = this
val typeHexLength = bits shr 2
val minLength = numberFormat.minLength
val pads = (minLength - typeHexLength).coerceAtLeast(0)
val prefix = numberFormat.prefix
val suffix = numberFormat.suffix
var removeZeros = numberFormat.removeLeadingZeros
val formatLength = prefix.length.toLong() + pads + typeHexLength + suffix.length
val charArray = CharArray(checkFormatLength(formatLength))
var charIndex = prefix.toCharArrayIfNotEmpty(charArray, 0)
if (pads > 0) {
charArray.fill(digits[0], charIndex, charIndex + pads)
charIndex += pads
}
var shift = bits
repeat(typeHexLength) {
shift -= 4
val decimal = ((value shr shift) and 0xF).toInt()
removeZeros = removeZeros && decimal == 0 && (shift shr 2) >= minLength
if (!removeZeros) {
charArray[charIndex++] = digits[decimal]
}
}
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
private fun String.hexToIntImpl(startIndex: Int, endIndex: Int, format: HexFormat, typeHexLength: Int): Int {
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
val numberFormat = format.number
// Optimize for digits-only formats
if (numberFormat.isDigitsOnly) {
checkNumberOfDigits(startIndex, endIndex, typeHexLength)
return parseInt(startIndex, endIndex)
}
val prefix = numberFormat.prefix
val suffix = numberFormat.suffix
checkPrefixSuffixNumberOfDigits(startIndex, endIndex, prefix, suffix, numberFormat.ignoreCase, typeHexLength)
return parseInt(startIndex + prefix.length, endIndex - suffix.length)
}
@ExperimentalStdlibApi
private fun String.hexToLongImpl(startIndex: Int, endIndex: Int, format: HexFormat, typeHexLength: Int): Long {
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
val numberFormat = format.number
// Optimize for digits-only formats
if (numberFormat.isDigitsOnly) {
checkNumberOfDigits(startIndex, endIndex, typeHexLength)
return parseLong(startIndex, endIndex)
}
val prefix = numberFormat.prefix
val suffix = numberFormat.suffix
checkPrefixSuffixNumberOfDigits(startIndex, endIndex, prefix, suffix, numberFormat.ignoreCase, typeHexLength)
return parseLong(startIndex + prefix.length, endIndex - suffix.length)
}
private fun String.checkPrefixSuffixNumberOfDigits(
startIndex: Int,
endIndex: Int,
prefix: String,
suffix: String,
ignoreCase: Boolean,
typeHexLength: 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
checkContainsAt(digitsEndIndex, endIndex, suffix, ignoreCase, "suffix")
checkNumberOfDigits(digitsStartIndex, digitsEndIndex, typeHexLength)
}
private fun String.checkNumberOfDigits(startIndex: Int, endIndex: Int, typeHexLength: Int) {
val digits = endIndex - startIndex
if (digits < 1) {
throwInvalidNumberOfDigits(startIndex, endIndex, "at least", 1)
} else if (digits > typeHexLength) {
checkZeroDigits(startIndex, startIndex + digits - typeHexLength)
}
}
private fun String.checkZeroDigits(startIndex: Int, endIndex: Int) {
for (index in startIndex until endIndex) {
if (this[index] != '0') {
throw NumberFormatException(
"Expected the hexadecimal digit '0' at index $index, but was '${this[index]}'.\n" +
"The result won't fit the type being parsed."
)
}
}
}
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
}
private fun String.parseLong(startIndex: Int, endIndex: Int): Long {
var result = 0L
for (i in startIndex until endIndex) {
result = (result shl 4) or longDecimalFromHexDigitAt(i)
}
return result
}
@Suppress("NOTHING_TO_INLINE")
private inline fun String.checkContainsAt(index: Int, endIndex: Int, part: String, ignoreCase: Boolean, partName: String): Int {
if (part.isEmpty()) return index
for (i in part.indices) {
if (!part[i].equals(this[index + i], ignoreCase)) {
throwNotContainedAt(index, endIndex, part, partName)
}
}
return index + part.length
}
@Suppress("NOTHING_TO_INLINE")
private inline fun String.decimalFromHexDigitAt(index: Int): Int {
val code = this[index].code
if (code ushr 8 == 0 && HEX_DIGITS_TO_DECIMAL[code] >= 0) {
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, specifier: String, expected: Int) {
val substring = substring(startIndex, endIndex)
throw NumberFormatException(
"Expected $specifier $expected 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]}")
}