Coverage Summary for Class: GhostDoubleFormatter (com.ghost.serialization.writer)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GhostDoubleFormatter |
100%
(1/1)
|
100%
(3/3)
|
81.8%
(54/66)
|
86.4%
(133/154)
|
89.1%
(472/530)
|
package com.ghost.serialization.writer
import com.ghost.serialization.parser.GhostJsonConstants as C
import kotlin.math.roundToInt
/**
* High-performance, Zero-Allocation formatting engine for [Double] numbers.
*
* This utility writes the ASCII representation of floating-point numbers directly to a pre-allocated
* [ByteArray] buffer. This completely bypasses the garbage collection (GC) overhead typically associated
* with calling `Double.toString()` or `String.format()`, making it ideal for high-throughput JSON serialization.
*
* ### Thresholds and Precision Limits
* - **Fast-Path Precision:** Supports up to [MAX_DECIMALS] (9) decimal places of precision.
* - **Magnitude Limits:** Works with values within the range `[1e-9, 1e15]`.
* - **Fallback Mechanism:** Values outside this range, non-finite values (NaN, Infinity), or microscopic
* numbers are delegated to the provided [fallback] lambda (which usually wraps native platform formatters).
*
* ### Algorithm Overview
* 1. **Sign Handling:** Negative values write `-` to the buffer, and the absolute value is processed.
* 2. **Whole Number Fast-Path:** Numbers without decimal fractions are converted to [Long] and written
* using [writeLongDirect] with a appended `.0` suffix.
* 3. **Split and Scale:** Splits the number into its integer part `intPart` and fractional part `fracPart`.
* The fractional part is multiplied by `1_000_000_000.0` (precision scale) and rounded to a 9-digit integer `fracInt`.
* 4. **Carry-over Correction:** If rounding `fracInt` causes a carry-over to the integer boundary (i.e. `fracInt >= 1e9`),
* the integer part is incremented by 1, and printed with `.0`.
* 5. **Trailing Zeros Trimming:** If `fracInt` ends with zeros, they are trimmed by dividing by 10
* to write the shortest equivalent decimal representation (e.g., `3.5` instead of `3.500000000`).
* 6. **Digit Emission:** The integer part is written. A decimal point `.` is appended. Then, digits of `fracInt`
* are emitted. Digits are processed in pairs (base 100) using [C.DOUBLE_DIGIT_LUT] for fast lookup
* and written backward to maintain correct decimal alignment.
*/
internal object GhostDoubleFormatter {
/** Maximum value below which a whole Double is formatted directly as a Long + ".0" */
private const val SMALL_WHOLE_THRESHOLD = 1_000_000_000.0
/** Multiplier to scale up 9 fractional decimal digits to a Long integer space */
private const val PRECISION_MULTIPLIER = 1_000_000_000.0
/** The lowest positive Double value processed by the fast-path without fallback */
private const val MICROSCOPIC_DOUBLE_THRESHOLD = 1e-9
/** The maximum Double value processed by the fast-path without fallback */
private const val MASSIVE_DOUBLE_THRESHOLD = 1e9
/** The carry-over boundary for fractional scaling (10^9) */
private const val FRAC_LIMIT = 1_000_000_000L
/** Maximum decimal precision supported (9 decimal places) */
private const val MAX_DECIMALS = 9
/**
* Formats and writes the given [Double] value directly into the [scratch] buffer starting at [offset].
*
* @param value The double value to write.
* @param scratch The destination byte array.
* @param offset The starting position in the destination buffer.
* @param fallback The fallback formatter used if the value cannot be processed by the fast-path.
* @return The number of bytes written to the buffer.
*/
fun writeDoubleDirect(
value: Double,
scratch: ByteArray,
offset: Int,
fallback: (Double) -> Int
): Int {
if (!value.isFinite()) return fallback(value)
var pos = offset
var localValue = value
if (value.toRawBits() < 0) {
scratch[pos++] = C.MINUS
localValue = -localValue
}
// Fast path for small whole numbers
// (very common in metrics/coordinates)
if (
localValue <= SMALL_WHOLE_THRESHOLD &&
localValue % C.WHOLE_NUMBER_CHECK == C.ZERO_DOUBLE
) {
return writeLongDirect(
localValue.toLong(),
scratch,
pos,
scratchEnd = pos + 32,
writeDecimalZero = true
) - offset
}
// If number is massive or microscopic, delegate to native system
if (
localValue > MASSIVE_DOUBLE_THRESHOLD ||
(localValue > 0.0 && localValue < MICROSCOPIC_DOUBLE_THRESHOLD)
) {
return fallback(value)
}
val intPart = localValue.toLong()
val fracPart = localValue - intPart
// roundToInt avoids the Double intermediate that round() returns
var fracInt = (fracPart * PRECISION_MULTIPLIER).roundToInt()
if (fracInt >= FRAC_LIMIT) {
return writeLongDirect(
intPart + 1,
scratch,
pos,
scratchEnd = pos + 32,
writeDecimalZero = true
) - offset
}
pos = writeLongDirect(
intPart,
scratch,
pos,
scratchEnd = pos + 32,
writeDecimalZero = false
)
scratch[pos++] = C.DOT
if (fracInt == 0) {
scratch[pos++] = C.ZERO
return pos - offset
}
var decimalsToPrint = MAX_DECIMALS
// Trim trailing zeros: % instead of multiply-subtract
while (decimalsToPrint > 1 && fracInt % 10 == 0) {
fracInt /= 10
decimalsToPrint--
}
pos += decimalsToPrint
var writePos = pos - 1
while (decimalsToPrint >= 2) {
val q = fracInt / 100
val r = (fracInt - (q * 100)) * 2
C.DOUBLE_DIGIT_LUT.copyInto(
scratch,
writePos - 1,
r,
r + 2
)
writePos -= 2
fracInt = q
decimalsToPrint -= 2
}
if (decimalsToPrint == 1) {
scratch[writePos] = (C.ZERO_INT + fracInt % 10).toByte()
}
return pos - offset
}
/**
* Formats and writes the given [Float] value directly into the [scratch] buffer starting at [offset].
* Uses 7 decimal places of precision suitable for the single-precision Float type to avoid representation noise.
*/
fun writeFloatDirect(
value: Float,
scratch: ByteArray,
offset: Int,
fallback: (Float) -> Int
): Int {
if (!value.isFinite()) return fallback(value)
var pos = offset
var localValue = value
if (value.toRawBits() < 0) {
scratch[pos++] = C.MINUS
localValue = -localValue
}
val doubleVal = localValue.toDouble()
// Fast path for small whole numbers
if (
doubleVal <= SMALL_WHOLE_THRESHOLD &&
doubleVal % C.WHOLE_NUMBER_CHECK == C.ZERO_DOUBLE
) {
return writeLongDirect(
doubleVal.toLong(),
scratch,
pos,
scratchEnd = pos + 32,
writeDecimalZero = true
) - offset
}
// If number is massive or microscopic, delegate to native system
if (
doubleVal > MASSIVE_DOUBLE_THRESHOLD ||
(localValue > 0.0f && doubleVal < MICROSCOPIC_DOUBLE_THRESHOLD)
) {
return fallback(value)
}
val intPart = doubleVal.toLong()
val fracPart = doubleVal - intPart
// Float precision limit is 7 decimals (10^7)
var fracInt = (fracPart * 10_000_000.0).roundToInt()
if (fracInt >= 10_000_000L) {
return writeLongDirect(
intPart + 1,
scratch,
pos,
scratchEnd = pos + 32,
writeDecimalZero = true
) - offset
}
pos = writeLongDirect(
intPart,
scratch,
pos,
scratchEnd = pos + 32,
writeDecimalZero = false
)
scratch[pos++] = C.DOT
if (fracInt == 0) {
scratch[pos++] = C.ZERO
return pos - offset
}
var decimalsToPrint = 7
while (decimalsToPrint > 1 && fracInt % 10 == 0) {
fracInt /= 10
decimalsToPrint--
}
pos += decimalsToPrint
var writePos = pos - 1
while (decimalsToPrint >= 2) {
val q = fracInt / 100
val r = (fracInt - (q * 100)) * 2
C.DOUBLE_DIGIT_LUT.copyInto(
scratch,
writePos - 1,
r,
r + 2
)
writePos -= 2
fracInt = q
decimalsToPrint -= 2
}
if (decimalsToPrint == 1) {
scratch[writePos] = (C.ZERO_INT + fracInt % 10).toByte()
}
return pos - offset
}
/**
* Converts a [Long] integer value into its ASCII bytes and writes it directly to [scratch].
*
* Digits are extracted from right to left using division and base-100 modulo arithmetic.
* Digit values are translated via pre-calculated ones and tens lookup tables to minimize computation.
* The compiled instruction utilizes an optimized `System.arraycopy` (via [ByteArray.copyInto])
* block operation to transfer the written string to its final offset.
*
* @param value The Long integer to write.
* @param scratch The destination byte array.
* @param offset The starting position in the destination buffer.
* @param scratchEnd The boundary of the scratch segment reserved for backward digit writing.
* @param writeDecimalZero If true, appends a `.0` suffix to the formatted integer.
* @return The next write index in the scratch buffer.
*/
private fun writeLongDirect(
value: Long,
scratch: ByteArray,
offset: Int,
scratchEnd: Int,
writeDecimalZero: Boolean
): Int {
if (value == 0L) {
scratch[offset] = C.ZERO
if (writeDecimalZero) {
scratch[offset + 1] = C.DOT
scratch[offset + 2] = C.ZERO
return offset + 3
}
return offset + 1
}
var localValue = value
// Write digits backward into the scratch zone at the end of our reserved area.
// scratchEnd is always offset + 32, safely within FAST_BUF_SCRATCH_ZONE.
var end = scratchEnd
while (localValue >= C.BASE_HUNDRED) {
val q = localValue / C.BASE_HUNDRED
val r = (localValue - (q * C.BASE_HUNDRED)).toInt()
localValue = q
scratch[--end] = C.FormatUtils.DIGIT_ONES[r]
scratch[--end] = C.FormatUtils.DIGIT_TENS[r]
}
if (localValue >= C.BASE_TEN) {
val r = localValue.toInt()
scratch[--end] = C.FormatUtils.DIGIT_ONES[r]
scratch[--end] = C.FormatUtils.DIGIT_TENS[r]
} else {
scratch[--end] = (localValue.toInt() + C.ASCII_OFFSET).toByte()
}
val length = scratchEnd - end
// Single System.arraycopy — JVM intrinsic, no per-byte loop
scratch.copyInto(
scratch,
offset,
end,
end + length
)
var nextOffset = offset + length
if (writeDecimalZero) {
scratch[nextOffset++] = C.DOT
scratch[nextOffset++] = C.ZERO
}
return nextOffset
}
}