Coverage Summary for Class: ProtoTimestampKt (com.ghost.protobuf.wkt)

Class Class, % Method, % Branch, % Line, % Instruction, %
ProtoTimestampKt 100% (1/1) 100% (6/6) 69.5% (57/82) 91.3% (137/150) 89.7% (712/794)


 @file:OptIn(InternalGhostApi::class)
 @file:Suppress("NOTHING_TO_INLINE")
 
 package com.ghost.protobuf.wkt
 
 import com.ghost.serialization.InternalGhostApi
 import com.ghost.serialization.contract.GhostSerializer
 import com.ghost.serialization.parser.GhostJsonFlatReader
 import com.ghost.serialization.parser.GhostJsonReader
 import com.ghost.serialization.parser.GhostJsonStringReader
 import com.ghost.serialization.parser.nextString
 import com.ghost.serialization.writer.GhostJsonFlatWriter
 import com.ghost.serialization.writer.GhostJsonStringWriter
 import com.ghost.serialization.writer.GhostJsonWriter
 import com.ghost.serialization.parser.GhostJsonConstants as C
 
 /**
  * A Timestamp represents a point in time independent of any time zone or local
  * calendar, encoded as a count of seconds and fractions of seconds at
  * nanosecond resolution.
  */
 data class ProtoTimestamp(val seconds: Long, val nanos: Int)
 
 /**
  * Serializer for [ProtoTimestamp].
  */
 object ProtoTimestampSerializer : GhostSerializer<ProtoTimestamp> {
     override val typeName: String get() = C.WKT_TIMESTAMP_TYPE
 
     override fun serialize(writer: GhostJsonWriter, value: ProtoTimestamp) {
         writer.value(formatTimestamp(value))
     }
 
     override fun serialize(writer: GhostJsonFlatWriter, value: ProtoTimestamp) {
         writer.value(formatTimestamp(value))
     }
 
     override fun serialize(writer: GhostJsonStringWriter, value: ProtoTimestamp) {
         writer.value(formatTimestamp(value))
     }
 
     override fun deserialize(reader: GhostJsonReader): ProtoTimestamp {
         return parseTimestamp(reader.nextString())
     }
 
     override fun deserialize(reader: GhostJsonFlatReader): ProtoTimestamp {
         return parseTimestamp(reader.nextString())
     }
 
     override fun deserialize(reader: GhostJsonStringReader): ProtoTimestamp {
         return parseTimestamp(reader.nextString())
     }
 }
 
 internal inline fun String.parseDecimalAt(start: Int, end: Int): Int {
     var result = 0
     var index = start
     while (index < end) {
         val code = this[index].code
         if ((code - C.ZERO_INT) !in 0..9) throw IllegalArgumentException(C.ERR_MALFORMED_DIGIT)
         result = result * C.BASE_TEN + (code - C.ZERO_INT)
         index++
     }
     return result
 }
 
 // Writes `value` zero-padded to `width` digits directly into the ByteArray.
 internal fun writePaddedInt(buffer: ByteArray, startOffset: Int, value: Int, width: Int): Int {
     var position = startOffset
     var digitCount = 1
     var threshold = C.BASE_TEN
     while (threshold <= value && digitCount < width) {
         digitCount++
         threshold *= C.BASE_TEN
     }
     var remainingValue = value
     var actualDigits = digitCount
     while (threshold <= remainingValue) {
         actualDigits++
         threshold *= C.BASE_TEN
     }
     var paddingCount = width - actualDigits
     while (paddingCount > 0) {
         buffer[position++] = C.CHAR_ZERO.code.toByte()
         paddingCount--
     }
     var divisor = 1
     var digitIndex = actualDigits - 1
     while (digitIndex > 0) {
         divisor *= C.BASE_TEN
         digitIndex--
     }
     remainingValue = value
     while (divisor > 0) {
         val digit = remainingValue / divisor
         buffer[position++] = (digit + C.ZERO_INT).toByte()
         remainingValue %= divisor
         divisor /= C.BASE_TEN
     }
     return position
 }
 
 // Appends nanos as fractional digits. Proto3 JSON mandates exactly 0, 3, 6, or 9 fractional
 // digits (never an arbitrary trim) — e.g. 450_000_000 ns must render as ".450", not ".45".
 internal fun writeNanosFraction(buffer: ByteArray, startOffset: Int, nanos: Int): Int {
     val width: Int
     val scale: Int
     if (nanos % C.NANOS_PER_MILLI == 0) {
         width = 3
         scale = C.NANOS_PER_MILLI
     } else if (nanos % C.NANOS_PER_MICRO == 0) {
         width = 6
         scale = C.NANOS_PER_MICRO
     } else {
         width = 9
         scale = 1
     }
     return writePaddedInt(buffer, startOffset, nanos / scale, width)
 }
 
 // Zero-allocation calendar converter using Hatcher/Richards algorithm
 // Ranges validated: 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z
 internal fun parseTimestamp(timestampString: String): ProtoTimestamp {
     if (timestampString.length < C.TS_MIN_LENGTH) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_SHORT)
     }
     val year = timestampString.parseDecimalAt(C.TS_YEAR_START, C.TS_YEAR_END)
     if (timestampString[C.TS_YEAR_END] != C.CHAR_HYPHEN) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_YEAR_HYPHEN)
     }
     val month = timestampString.parseDecimalAt(C.TS_MONTH_START, C.TS_MONTH_END)
     if (timestampString[C.TS_MONTH_END] != C.CHAR_HYPHEN) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_MONTH_HYPHEN)
     }
     val day = timestampString.parseDecimalAt(C.TS_DAY_START, C.TS_DAY_END)
     if (timestampString[C.TS_DAY_END] != C.CHAR_T_UPPER && timestampString[C.TS_DAY_END] != C.CHAR_T) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_T)
     }
     val hour = timestampString.parseDecimalAt(C.TS_HOUR_START, C.TS_HOUR_END)
     if (timestampString[C.TS_HOUR_END] != C.CHAR_COLON) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_HOUR_COLON)
     }
     val minute = timestampString.parseDecimalAt(C.TS_MIN_START, C.TS_MIN_END)
     if (timestampString[C.TS_MIN_END] != C.CHAR_COLON) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_MINUTE_COLON)
     }
     val second = timestampString.parseDecimalAt(C.TS_SEC_START, C.TS_SEC_END)
 
     var nanos = 0
     var nextIndex = C.TS_SEC_END
     if (timestampString[C.TS_SEC_END] == C.CHAR_DOT) {
         var endIndex = C.TS_SEC_END + 1
         val len = timestampString.length
         while (endIndex < len) {
             val code = timestampString[endIndex].code
             if ((code - C.ZERO_INT) !in 0..9) {
                 break
             }
             endIndex++
         }
         val fracDigits = endIndex - (C.TS_SEC_END + 1)
         var fractionValue = 0
         var fractionIndex = C.TS_SEC_END + 1
         while (fractionIndex < endIndex) {
             fractionValue =
                 fractionValue * C.BASE_TEN + (timestampString[fractionIndex].code - C.ZERO_INT)
             fractionIndex++
         }
         var multiplier = 1
         var multiplierIndex = 0
         while (multiplierIndex < C.NANOS_DIGITS - fracDigits) {
             multiplier *= C.BASE_TEN
             multiplierIndex++
         }
         nanos = fractionValue * multiplier
         nextIndex = endIndex
     }
 
     if (nextIndex >= timestampString.length) {
         throw IllegalArgumentException(C.ERR_TIMESTAMP_TZ)
     }
 
     var offsetSec = 0
     if (timestampString[nextIndex] != C.CHAR_Z_UPPER && timestampString[nextIndex] != C.CHAR_Z_LOWER) {
         if (
             nextIndex + C.TS_TZ_OFFSET_LEN != timestampString.length ||
             (timestampString[nextIndex] != C.CHAR_PLUS && timestampString[nextIndex] != C.CHAR_HYPHEN)
         ) {
             throw IllegalArgumentException(C.ERR_TIMESTAMP_TZ_SUPPORT)
         }
         val tzSign = if (timestampString[nextIndex] == C.CHAR_HYPHEN) -1 else 1
         val tzHour = timestampString.parseDecimalAt(nextIndex + 1, nextIndex + 3)
         val tzMin = timestampString.parseDecimalAt(nextIndex + 4, nextIndex + 6)
         offsetSec = tzSign * (tzHour * 3600 + tzMin * 60)
     }
 
     val epochSeconds = dateToEpochSeconds(year, month, day, hour, minute, second) - offsetSec
     return ProtoTimestamp(epochSeconds, nanos)
 }
 
 // Convert YYYY-MM-DD HH:MM:SS to Epoch Seconds (UTC)
 internal fun dateToEpochSeconds(
     year: Int,
     month: Int,
     day: Int,
     hour: Int,
     minute: Int,
     second: Int
 ): Long {
     val yearAdjustment = (if (month <= 2) year - 1 else year).toLong()
     val monthAdjustment = (if (month <= 2) month + 12 else month).toLong()
     val era =
         (if (yearAdjustment >= 0) yearAdjustment else yearAdjustment - (C.HINNANT_ERA_YEARS - 1)) / C.HINNANT_ERA_YEARS
     val yearOfEra = yearAdjustment - era * C.HINNANT_ERA_YEARS
     val dayOfYear = (153 * (monthAdjustment - 3) + 2) / 5 + day - 1
     val dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear
     val days = era * C.HINNANT_DAYS_PER_ERA + dayOfEra - C.HINNANT_EPOCH_OFFSET
     return days * C.SECONDS_PER_DAY + hour * C.SECONDS_PER_HOUR + minute * C.SECONDS_PER_MINUTE + second
 }
 
 internal fun formatTimestamp(timestamp: ProtoTimestamp): String {
     val seconds = timestamp.seconds
     var days = seconds / C.SECONDS_PER_DAY
     var remSeconds = (seconds % C.SECONDS_PER_DAY).toInt()
     if (remSeconds < 0) {
         days -= 1
         remSeconds += C.SECONDS_PER_DAY.toInt()
     }
 
     val hour = remSeconds / C.SECONDS_PER_HOUR.toInt()
     val remMinutes = remSeconds % C.SECONDS_PER_HOUR.toInt()
     val minute = remMinutes / C.SECONDS_PER_MINUTE.toInt()
     val second = remMinutes % C.SECONDS_PER_MINUTE.toInt()
 
     val zeroDay = days + C.HINNANT_EPOCH_OFFSET
     val era =
         (if (zeroDay >= 0) zeroDay else zeroDay - C.HINNANT_DAYS_CYCLE_ERA) / C.HINNANT_DAYS_PER_ERA
     val dayOfEra = (zeroDay - era * C.HINNANT_DAYS_PER_ERA).toInt()
     val yearOfEra =
         (dayOfEra - dayOfEra / C.HINNANT_DAYS_CYCLE_4 + dayOfEra / C.HINNANT_DAYS_CYCLE_100 - dayOfEra / C.HINNANT_DAYS_CYCLE_ERA) / 365
     val y = yearOfEra + era * C.HINNANT_ERA_YEARS
     val dayOfYear = dayOfEra - (365 * yearOfEra + yearOfEra / 4 - yearOfEra / 100)
     val monthPosition = (5 * dayOfYear + 2) / 153
     val day = dayOfYear - (153 * monthPosition + 2) / 5 + 1
     val month = if (monthPosition < 10) monthPosition + 3 else monthPosition - 9
     val year = (if (month <= 2) y + 1 else y).toInt()
 
     val bytes = ByteArray(C.TS_BUFFER_SIZE)
     var pos = 0
     pos = writePaddedInt(bytes, pos, year, 4)
     bytes[pos++] = C.CHAR_HYPHEN.code.toByte()
     pos = writePaddedInt(bytes, pos, month, 2)
     bytes[pos++] = C.CHAR_HYPHEN.code.toByte()
     pos = writePaddedInt(bytes, pos, day, 2)
     bytes[pos++] = C.CHAR_T_UPPER.code.toByte()
     pos = writePaddedInt(bytes, pos, hour, 2)
     bytes[pos++] = C.CHAR_COLON.code.toByte()
     pos = writePaddedInt(bytes, pos, minute, 2)
     bytes[pos++] = C.CHAR_COLON.code.toByte()
     pos = writePaddedInt(bytes, pos, second, 2)
 
     if (timestamp.nanos > 0) {
         bytes[pos++] = C.CHAR_DOT.code.toByte()
         pos = writeNanosFraction(bytes, pos, timestamp.nanos)
     }
     bytes[pos++] = C.CHAR_Z_UPPER.code.toByte()
     return bytes.decodeToString(0, pos)
 }