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

wvlet.airframe.ulid.CrockfordBase32.scala Maven / Gradle / Ivy

There is a newer version: 24.11.0
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 wvlet.airframe.ulid

/**
  * Base 32 encoding by Douglas Crockford: https://www.crockford.com/base32.html
  */
object CrockfordBase32 {
  private val ENCODING_CHARS: Array[Char] = Array(
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P',
    'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'
  )

  private val DECODING_CHARS: Array[Byte] = Array[Byte](
    -1, -1, -1, -1, -1, -1, -1, -1, // 0
    -1, -1, -1, -1, -1, -1, -1, -1, // 8
    -1, -1, -1, -1, -1, -1, -1, -1, // 16
    -1, -1, -1, -1, -1, -1, -1, -1, // 24
    -1, -1, -1, -1, -1, -1, -1, -1, // 32
    -1, -1, -1, -1, -1, -1, -1, -1, // 40
    0, 1, 2, 3, 4, 5, 6, 7,         // 48
    8, 9, -1, -1, -1, -1, -1, -1,   // 56
    -1, 10, 11, 12, 13, 14, 15, 16, // 64
    17, 1, 18, 19, 1, 20, 21, 0,    // 72
    22, 23, 24, 25, 26, -1, 27, 28, // 80
    29, 30, 31, -1, -1, -1, -1, -1, // 88
    -1, 10, 11, 12, 13, 14, 15, 16, // 96
    17, 1, 18, 19, 1, 20, 21, 0,    // 104
    22, 23, 24, 25, 26, -1, 27, 28, // 112
    29, 30, 31                      // 120
  )

  @inline def decode(ch: Char): Byte = DECODING_CHARS(ch & 0x7f)
  @inline def encode(i: Int): Char   = ENCODING_CHARS(i & 0x1f)

  /**
    * Decode a string representation of 128 bit value (26 characters) as a pair of (Long, Long) (128 bits)
    *
    * Note that technically 26 characters x 5 bite can represent 130-bit values. This method will discard the top 2 bits
    * from the string as ULID only uses 128 bits.
    */
  def decode128bits(s: String): (Long, Long) = {

    /**
      * `| hi (64-bits) | low (64-bits) |`
      */
    val len = s.length
    if (len != 26) {
      throw new IllegalArgumentException(s"String length must be 26: ${s} (length: ${len})")
    }
    var i         = 0
    var hi        = 0L
    var low       = 0L
    val carryMask = ~(~0L >>> 5)
    while (i < 26) {
      val v     = decode(s.charAt(i))
      val carry = (low & carryMask) >>> (64 - 5)
      low <<= 5
      low |= v
      hi <<= 5
      hi |= carry
      i += 1
    }
    (hi, low)
  }

  /**
    * Encode 128-bit values (Long, Long) with Crockford Base32
    */
  def encode128bits(hi: Long, low: Long): String = {
    val s = new StringBuilder(26)
    var i = 0
    var h = hi
    var l = low
    // encode from lower 5-bit
    while (i < 26) {
      s += encode((l & 0x1fL).toInt)
      val carry = (h & 0x1fL) << (64 - 5)
      l >>>= 5
      l |= carry
      h >>>= 5
      i += 1
    }
    // TODO: Use reverseInPlace when Scala 2.12 dropped
    s.reverseContents().toString()
  }

  /**
    * Decode 10-character Crockford Base32 as a 48-bit unsigned value. This is used for decoding ULID timestamp (48-bit
    * value)
    */
  def decode48bits(s: String): Long = {
    val len = s.length
    if (len != 10) {
      throw new IllegalArgumentException(s"String size must be 10: ${s} (length:${len})")
    }
    var l: Long = decode(s.charAt(0))
    var i       = 1
    while (i < len) {
      l <<= 5
      l |= decode(s.charAt(i))
      i += 1
    }
    l
  }

  def isValidBase32(s: String): Boolean = {
    s.forall { decode(_) != -1 }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy