All Downloads are FREE. Search and download functionalities are using the official Maven repository.

smithy4s.Timestamp.scala Maven / Gradle / Ivy

There is a newer version: 0.19.0-41-91762fb
Show newest version
/*
 *  Copyright 2021-2024 Disney Streaming
 *
 *  Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     https://disneystreaming.github.io/TOST-1.0.txt
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package smithy4s

import smithy.api.TimestampFormat
import scala.util.control.{NoStackTrace, NonFatal}

case class Timestamp private (epochSecond: Long, nano: Int)
    extends TimestampPlatform {

  def epochMilli: Long = epochSecond * 1000 + nano / 1000000

  def isAfter(other: Timestamp): Boolean = {
    val diff = epochSecond - other.epochSecond
    diff > 0 || diff == 0 && nano > other.nano
  }

  def format(format: TimestampFormat): String = format match {
    case TimestampFormat.DATE_TIME     => formatToString(0)
    case TimestampFormat.EPOCH_SECONDS => formatEpochSeconds
    case TimestampFormat.HTTP_DATE     => formatToString(1)
  }

  def conciseDateTime: String = formatToString(3)

  def conciseDate: String = formatToString(2)

  override def toString: String = format(TimestampFormat.DATE_TIME)

  private[this] def formatToString(internalFormat: Int): String = {
    val s = new java.lang.StringBuilder(32)
    val epochDay =
      (if (epochSecond >= 0) epochSecond
       else epochSecond - 86399) / 86400 // 86400 == seconds per day
    val secsOfDay = (epochSecond - epochDay * 86400).toInt
    var marchZeroDay =
      epochDay + 719468 // 719468 == 719528 - 60 == days 0000 to 1970 - days 1st Jan to 1st Mar
    var adjustYear = 0
    if (marchZeroDay < 0) { // adjust negative years to positive for calculation
      val adjust400YearCycles = to400YearCycle(marchZeroDay + 1) - 1
      adjustYear = adjust400YearCycles * 400
      marchZeroDay -= adjust400YearCycles * 146097L
    }
    var year = to400YearCycle(marchZeroDay * 400 + 591)
    var marchDayOfYear = toMarchDayOfYear(marchZeroDay, year)
    if (marchDayOfYear < 0) { // fix year estimate
      year -= 1
      marchDayOfYear = toMarchDayOfYear(marchZeroDay, year)
    }
    val marchMonth =
      (marchDayOfYear * 17135 + 6854) >> 19 // (marchDayOfYear * 5 + 2) / 153
    year += (marchMonth * 3277 >> 15) + adjustYear // year += marchMonth / 10 + adjustYear (reset any negative year and convert march-based values back to january-based)
    val month = marchMonth +
      (if (marchMonth < 10) 3
       else -9)
    val day =
      marchDayOfYear - ((marchMonth * 1002762 - 16383) >> 15) // marchDayOfYear - (marchMonth * 306 + 5) / 10 + 1
    internalFormat match {
      case 1 =>
        s.append(Timestamp.daysOfWeek(((epochDay + 700000003) % 7).toInt))
          .append(',')
        append2Digits(day, s.append(' '))
        s.append(' ').append(Timestamp.months(month - 1))
        append4Digits(year, s.append(' '))
        appendTime(secsOfDay, s.append(' '), addSeparator = true)
        appendNano(nano, s)
        s.append(" GMT").toString
      case 2 =>
        append4Digits(year, s)
        append2Digits(month, s)
        append2Digits(day, s)
        s.toString
      case 3 =>
        append4Digits(year, s)
        append2Digits(month, s)
        append2Digits(day, s)
        appendTime(secsOfDay, s.append('T'), addSeparator = false)
        s.append('Z').toString
      case _ =>
        append4Digits(year, s)
        append2Digits(month, s.append('-'))
        append2Digits(day, s.append('-'))
        appendTime(secsOfDay, s.append('T'), addSeparator = true)
        appendNano(nano, s)
        s.append('Z').toString
    }
  }

  private[this] def formatEpochSeconds: String = {
    val s = new java.lang.StringBuilder(32)
    s.append(epochSecond)
    appendNano(nano, s)
    s.toString
  }

  private[this] def appendTime(
      secsOfDay: Int,
      s: java.lang.StringBuilder,
      addSeparator: Boolean
  ): Unit = {
    val y1 =
      secsOfDay * 1193047L // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/
    val y2 = (y1 & 0xffffffffL) * 60
    val y3 = (y2 & 0xffffffffL) * 60

    if (addSeparator) {
      append2Digits((y1 >> 32).toInt, s)
      append2Digits((y2 >> 32).toInt, s.append(':'))
      append2Digits((y3 >> 32).toInt, s.append(':'))
    } else {
      append2Digits((y1 >> 32).toInt, s)
      append2Digits((y2 >> 32).toInt, s)
      append2Digits((y3 >> 32).toInt, s)
    }
  }

  private[this] def appendNano(nano: Int, s: java.lang.StringBuilder): Unit =
    if (nano != 0) {
      val y1 =
        nano * 1441151881L // Based on James Anhalt's algorithm for 9 digits: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/
      val y2 = (y1 & 0x1ffffffffffffffL) * 100
      s.append('.').append(((y1 >>> 57).toInt + '0').toChar)
      append2Digits((y2 >>> 57).toInt, s)
      if ((y2 & 0x1fffff800000000L) != 0) { // check if nano is divisible by 1000000
        val y3 = (y2 & 0x1ffffffffffffffL) * 100
        val y4 = (y3 & 0x1ffffffffffffffL) * 100
        append2Digits((y3 >>> 57).toInt, s)
        val d = Timestamp.digits((y4 >>> 57).toInt)
        s.append((d & 0xff).toChar)
        if ((y4 & 0x1ff000000000000L) != 0 || d > 0x3039) { // check if nano is divisible by 1000
          append2Digits(
            ((y4 & 0x1ffffffffffffffL) * 100 >>> 57).toInt,
            s.append((d >> 8).toChar)
          )
        }
      }
    }

  private[this] def append4Digits(x: Int, s: java.lang.StringBuilder): Unit = {
    val q = x * 5243 >> 19 // divide a 4-digit positive int by 100
    append2Digits(q, s)
    append2Digits(x - q * 100, s)
  }

  private[this] def append2Digits(x: Int, s: java.lang.StringBuilder): Unit = {
    val d = Timestamp.digits(x)
    val _ = s.append((d & 0xff).toChar).append((d >> 8).toChar)
  }

  private[this] def to400YearCycle(day: Long): Int =
    (day / 146097).toInt // 146097 == number of days in a 400 year cycle

  private[this] def toMarchDayOfYear(marchZeroDay: Long, year: Int): Int = {
    val century =
      (year * 1374389535L >> 37).toInt // divide an int by 100 (the sign correction is not needed)
    (marchZeroDay - year * 365L).toInt - (year >> 2) + century - (century >> 2)
  }
}

object Timestamp extends TimestampCompanionPlatform {

  val epoch = Timestamp(0, 0)

  def fromEpochMilli(epochMilli: Long): Timestamp = {
    val secs = java.lang.Math.floorDiv(epochMilli, 1000)
    val mos = java.lang.Math.floorMod(epochMilli, 1000)
    Timestamp(secs, (mos * 1000000).toInt)
  }

  private val digits: Array[Short] = Array(
    0x3030, 0x3130, 0x3230, 0x3330, 0x3430, 0x3530, 0x3630, 0x3730, 0x3830,
    0x3930, 0x3031, 0x3131, 0x3231, 0x3331, 0x3431, 0x3531, 0x3631, 0x3731,
    0x3831, 0x3931, 0x3032, 0x3132, 0x3232, 0x3332, 0x3432, 0x3532, 0x3632,
    0x3732, 0x3832, 0x3932, 0x3033, 0x3133, 0x3233, 0x3333, 0x3433, 0x3533,
    0x3633, 0x3733, 0x3833, 0x3933, 0x3034, 0x3134, 0x3234, 0x3334, 0x3434,
    0x3534, 0x3634, 0x3734, 0x3834, 0x3934, 0x3035, 0x3135, 0x3235, 0x3335,
    0x3435, 0x3535, 0x3635, 0x3735, 0x3835, 0x3935, 0x3036, 0x3136, 0x3236,
    0x3336, 0x3436, 0x3536, 0x3636, 0x3736, 0x3836, 0x3936, 0x3037, 0x3137,
    0x3237, 0x3337, 0x3437, 0x3537, 0x3637, 0x3737, 0x3837, 0x3937, 0x3038,
    0x3138, 0x3238, 0x3338, 0x3438, 0x3538, 0x3638, 0x3738, 0x3838, 0x3938,
    0x3039, 0x3139, 0x3239, 0x3339, 0x3439, 0x3539, 0x3639, 0x3739, 0x3839,
    0x3939
  )
  private val daysOfWeek: Array[String] =
    Array("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
  private val months: Array[String] =
    Array(
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec"
    )

  def apply(epochSecond: Long, nano: Int): Timestamp = {
    require(
      epochSecond >= -62167219200L && epochSecond <= 253402300799L,
      "illegal epochSecond"
    )
    require(nano >= 0 && nano <= 999999999, "illegal nano")
    new Timestamp(epochSecond, nano)
  }

  def apply(
      year: Int,
      month: Int,
      day: Int,
      hour: Int = 0,
      minute: Int = 0,
      second: Int = 0,
      nano: Int = 0
  ): Timestamp = {
    require(year >= 0 && year <= 9999, "illegal year")
    require(month >= 1 && month <= 12, "illegal month")
    require(
      day >= 1 && (day <= 28 || day <= maxDayForYearMonth(year, month)),
      "illegal year, month, day combination"
    )
    require(hour >= 0 && hour <= 23, "illegal hour")
    require(minute >= 0 && minute <= 59, "illegal minute")
    require(second >= 0 && second <= 59, "illegal second")
    require(nano >= 0 && nano <= 999999999, "illegal nano")
    new Timestamp(
      toEpochDay(
        year,
        month,
        day
      ) * 86400 + (hour * 3600 + minute * 60 + second),
      nano
    )
  }

  def fromEpochSecond(epochSecond: Long): Timestamp = Timestamp(epochSecond, 0)

  def parse(string: String, format: TimestampFormat): Option[Timestamp] = try {
    new Some(format match {
      case TimestampFormat.DATE_TIME     => parseDateTime(string)
      case TimestampFormat.EPOCH_SECONDS => parseEpochSeconds(string)
      case TimestampFormat.HTTP_DATE     => parseHTTPDate(string)
    })
  } catch {
    case NonFatal(_) => None
  }

  def showFormat(format: TimestampFormat): String = format match {
    case TimestampFormat.DATE_TIME =>
      "date-time timestamp (YYYY-MM-ddThh:mm:ss.sssZ)"
    case TimestampFormat.EPOCH_SECONDS => "epoch-second timestamp"
    case TimestampFormat.HTTP_DATE =>
      "http-date timestamp (EEE, dd MMM yyyy hh:mm:ss.sss z)"
  }

  private[this] def parseDateTime(s: String): Timestamp = {
    val len = s.length
    if (len < 19) error()
    var pos = 0
    val year = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val ch3 = s.charAt(pos + 3)
      val ch4 = s.charAt(pos + 4)
      if (
        ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9'
        || ch4 != '-'
      ) error()
      pos += 5
      ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111
    }
    val month = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      if (
        ch0 < '0' || ch0 > '1' || ch1 < '0' || ch1 > '9' || month < 1 || month > 12 || ch2 != '-'
      ) error()
      pos += 3
      month
    }
    val day = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      if (
        ch0 < '0' || ch0 > '3' || ch1 < '0' || ch1 > '9' || day == 0 ||
        (day > 28 && day > maxDayForYearMonth(year, month)) || ch2 != 'T'
      ) error()
      pos += 3
      day
    }
    val hour = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      if (
        ch0 < '0' || ch0 > '2' || ch1 < '0' || ch1 > '9' || hour > 23 || ch2 != ':'
      ) error()
      pos += 3
      hour
    }
    val minute = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9' || ch2 != ':')
        error()
      pos += 3
      ch0 * 10 + ch1 - 528 // 528 == '0' * 11
    }
    val second = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
      pos += 2
      ch0 * 10 + ch1 - 528 // 528 == '0' * 11
    }
    var epochSecond = toEpochDay(
      year,
      month,
      day
    ) * 86400 + (hour * 3600 + minute * 60 + second)
    var nano = 0
    var ch = (0: Char)
    if (pos < len) {
      ch = s.charAt(pos)
      pos += 1
      if (ch == '.') {
        var nanoDigitWeight = 100000000
        while (
          pos < len && {
            ch = s.charAt(pos)
            pos += 1
            (ch >= '0' && ch <= '9') && nanoDigitWeight != 0
          }
        ) {
          nano += (ch - '0') * nanoDigitWeight
          nanoDigitWeight =
            (nanoDigitWeight * 429496730L >> 32).toInt // divide a small positive int by 10
        }
      }
    }
    if (ch != 'Z') {
      val isNeg = ch == '-' || (ch != '+' && {
        error()
        true
      })
      if (pos + 2 > len) error()
      var offsetTotal = {
        val ch0 = s.charAt(pos)
        val ch1 = s.charAt(pos + 1)
        if (ch0 < '0' || ch0 > '1' || ch1 < '0' || ch1 > '9') error()
        pos += 2
        ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      } * 3600
      if (
        pos + 3 <= len && {
          ch = s.charAt(pos)
          pos += 1
          ch == ':'
        } && {
          offsetTotal += {
            val ch0 = s.charAt(pos)
            val ch1 = s.charAt(pos + 1)
            if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
            pos += 2
            ch0 * 10 + ch1 - 528 // 528 == '0' * 11
          } * 60
          pos + 3 <= len
        } && {
          ch = s.charAt(pos)
          pos += 1
          ch == ':'
        }
      ) offsetTotal += {
        val ch0 = s.charAt(pos)
        val ch1 = s.charAt(pos + 1)
        if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
        pos += 2
        ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      }
      if (offsetTotal > 64800) error() // 64800 == 18 * 60 * 60
      if (isNeg) offsetTotal = -offsetTotal
      epochSecond -= offsetTotal
    }
    if (pos != len) error()
    new Timestamp(epochSecond, nano)
  }

  private[this] def parseEpochSeconds(s: String): Timestamp = {
    val len = s.length
    if (len == 0) error()
    var pos = 0
    var ch = s.charAt(pos)
    pos += 1
    val isNeg = ch == '-'
    if (isNeg) {
      ch = s.charAt(pos)
      pos += 1
    }
    if (ch < '0' || ch > '9') error()
    var epochSecond: Long = (ch - '0').toLong
    while (
      pos < len && {
        ch = s.charAt(pos)
        ch >= '0' && ch <= '9'
      }
    ) {
      epochSecond = epochSecond * 10 + (ch - '0')
      if (epochSecond > 377705116800L) error()
      pos += 1
    }
    if (isNeg) epochSecond = -epochSecond
    if (epochSecond > 253402300799L) error()
    var nano = 0
    if (pos < len) {
      ch = s.charAt(pos)
      pos += 1
      if (ch == '.') {
        var nanoDigitWeight = 100000000
        while (
          pos < len && {
            ch = s.charAt(pos)
            pos += 1
            (ch >= '0' && ch <= '9') && nanoDigitWeight != 0
          }
        ) {
          nano += (ch - '0') * nanoDigitWeight
          nanoDigitWeight =
            (nanoDigitWeight * 429496730L >> 32).toInt // divide a small positive int by 10
        }
      }
    }
    if (pos != len) error()
    new Timestamp(epochSecond, nano)

  }

  private[this] def parseHTTPDate(s: String): Timestamp = {
    val len = s.length
    if (len < 29) error()
    var pos = 0
    val dayOfWeek = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val ch3 = s.charAt(pos + 3)
      val ch4 = s.charAt(pos + 4)
      var i = 0
      while (
        i < 7 && {
          val d = daysOfWeek(i)
          d.charAt(0) != ch0 || d.charAt(1) != ch1 || d.charAt(2) != ch2
        }
      ) i += 1
      val dayOfWeek = i + 1
      if (dayOfWeek > 7 || ch3 != ',' || ch4 != ' ') error()
      pos += 5
      dayOfWeek
    }
    val day = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      if (
        ch0 < '0' || ch0 > '3' || ch1 < '0' || ch1 > '9' || day == 0 || day > 31 || ch2 != ' '
      ) error()
      pos += 3
      day
    }
    val month = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val ch3 = s.charAt(pos + 3)
      var i = 0
      while (
        i < 12 && {
          val m = months(i)
          m.charAt(0) != ch0 || m.charAt(1) != ch1 || m.charAt(2) != ch2
        }
      ) i += 1
      val month = i + 1
      if (month > 12 || ch3 != ' ') error()
      pos += 4
      month
    }
    val year = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val ch3 = s.charAt(pos + 3)
      val ch4 = s.charAt(pos + 4)
      if (
        ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9'
        || ch4 != ' '
      ) error()
      pos += 5
      ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111
    }
    if (day > 28 && day > maxDayForYearMonth(year, month)) error()
    val epochDay = toEpochDay(year, month, day)
    if (dayOfWeek != (epochDay + 700000003) % 7 + 1) error()
    val hour = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11
      if (
        ch0 < '0' || ch0 > '2' || ch1 < '0' || ch1 > '9' || hour > 23 || ch2 != ':'
      ) error()
      pos += 3
      hour
    }
    val minute = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      val ch2 = s.charAt(pos + 2)
      if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9' || ch2 != ':')
        error()
      pos += 3
      ch0 * 10 + ch1 - 528 // 528 == '0' * 11
    }
    val second = {
      val ch0 = s.charAt(pos)
      val ch1 = s.charAt(pos + 1)
      if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
      pos += 2
      ch0 * 10 + ch1 - 528 // 528 == '0' * 11
    }
    var nano = 0
    var ch = (0: Char)
    if (pos < len) {
      ch = s.charAt(pos)
      pos += 1
      if (ch == '.') {
        var nanoDigitWeight = 100000000
        while (
          pos < len && {
            ch = s.charAt(pos)
            pos += 1
            (ch >= '0' && ch <= '9') && nanoDigitWeight != 0
          }
        ) {
          nano += (ch - '0') * nanoDigitWeight
          nanoDigitWeight =
            (nanoDigitWeight * 429496730L >> 32).toInt // divide a small positive int by 10
        }
      }
    }
    if (
      ch != ' ' || pos + 2 >= len || {
        val ch0 = s.charAt(pos)
        val ch1 = s.charAt(pos + 1)
        val ch2 = s.charAt(pos + 2)
        pos += 3
        ch0 != 'G' || ch1 != 'M' || ch2 != 'T' || pos != len
      }
    ) error()
    new Timestamp(epochDay * 86400 + (hour * 3600 + minute * 60 + second), nano)
  }

  private[this] def toEpochDay(year: Int, month: Int, day: Int): Long =
    year * 365L + ((year + 3 >> 2) - {
      val cp = year * 1374389535L
      if (year < 0) (cp >> 37) - (cp >> 39) // year / 100 - year / 400
      else
        (cp + 136064563965L >> 37) - (cp + 548381424465L >> 39) // (year + 99) / 100 - (year + 399) / 400
    }.toInt + (month * 1002277 - 988622 >> 15) - // (month * 367 - 362) / 12
      (if (month <= 2) 0
       else if (isLeap(year)) 1
       else 2) + day - 719529) // 719528 == days 0000 to 1970

  private[this] def maxDayForYearMonth(year: Int, month: Int): Int =
    if (month != 2) ((month >> 3) ^ (month & 0x1)) + 30
    else if (isLeap(year)) 29
    else 28

  private[this] def isLeap(year: Int): Boolean =
    (year & 0x3) == 0 && { // (year % 100 != 0 || year % 400 == 0)
      val cp = year * 1374389535L
      val cc = year >> 31
      ((cp ^ cc) & 0x1fc0000000L) != 0 || (((cp >> 37) - cc) & 0x3) == 0
    }

  private[this] def error(): Throwable = throw new RuntimeException
    with NoStackTrace
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy