Skip to content

Instantly share code, notes, and snippets.

@jean-merelis
Last active July 18, 2021 17:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jean-merelis/aaa7913ee0f994170d12b317ae179039 to your computer and use it in GitHub Desktop.
Save jean-merelis/aaa7913ee0f994170d12b317ae179039 to your computer and use it in GitHub Desktop.
ulid - Kotlin implementation
package com.github.jeanmerelis.id
import java.security.SecureRandom
import java.util.*
import kotlin.Comparable as KotlinComparable
class ID : KotlinComparable<ID> {
val mostSignificantBits: Long
val leastSignificantBits: Long
constructor() {
mostSignificantBits = random.nextLong()
leastSignificantBits = random.nextLong()
}
constructor(mostSignificantBits: Long, leastSignificantBits: Long) {
this.mostSignificantBits = mostSignificantBits
this.leastSignificantBits = leastSignificantBits
}
constructor(uuid: UUID) {
this.mostSignificantBits = uuid.mostSignificantBits
this.leastSignificantBits = uuid.leastSignificantBits
}
constructor(fromString: String) {
if (fromString.length == 36) {
val _id = UUID.fromString(fromString)
this.mostSignificantBits = _id.mostSignificantBits
this.leastSignificantBits = _id.leastSignificantBits
return
}
require(fromString.length == 26) { "ulidString must be exactly 26 chars long." }
val timeString = fromString.substring(0, 10)
val time: Long = internalParseCrockford(timeString)
require(time and TIMESTAMP_OVERFLOW_MASK == 0L) { "ulidString must not exceed '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'!" }
val part1String = fromString.substring(10, 18)
val part2String = fromString.substring(18)
val part1: Long = internalParseCrockford(part1String)
val part2: Long = internalParseCrockford(part2String)
this.mostSignificantBits = time shl 16 or (part1 ushr 24)
this.leastSignificantBits = part2 or (part1 shl 40)
}
fun toUUID() = UUID(mostSignificantBits, leastSignificantBits)
fun isEqualTo(other: Any?): Boolean {
if (this === other) return true
if (javaClass == other?.javaClass) return equals(other)
if (UUID::class.java != other?.javaClass) return false
other as UUID
if (mostSignificantBits != other.mostSignificantBits) return false
if (leastSignificantBits != other.leastSignificantBits) return false
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ID
if (mostSignificantBits != other.mostSignificantBits) return false
if (leastSignificantBits != other.leastSignificantBits) return false
return true
}
override fun hashCode(): Int {
var result = mostSignificantBits.hashCode()
result = 31 * result + leastSignificantBits.hashCode()
return result
}
override fun compareTo(other: ID): Int {
return COMPARATOR.compare(this, other)
}
override fun toString(): String {
val buffer = CharArray(26)
internalWriteCrockford(
buffer,
timestamp(this),
10,
0
)
var value = mostSignificantBits and 0xFFFFL shl 24
val interim = leastSignificantBits ushr 40
value = value or interim
internalWriteCrockford(buffer, value, 8, 10)
internalWriteCrockford(buffer, leastSignificantBits, 8, 18)
return String(buffer)
}
companion object {
private val COMPARATOR =
Comparator.comparingLong<ID> { it.mostSignificantBits }
.thenComparingLong { it.leastSignificantBits }
fun getUUIDFromString(s: String): UUID {
if (s.length == 36) {
return UUID.fromString(s)
}
return ID(s).toUUID()
}
}
}
private val random: Random = SecureRandom()
private val ENCODING_CHARS = charArrayOf(
'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 = byteArrayOf( // 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
-1, -1, -1, -1, -1, -1, -1, -1, // 48
0, 1, 2, 3, 4, 5, 6, 7, // 56
8, 9, -1, -1, -1, -1, -1, -1, // 64
-1, 10, 11, 12, 13, 14, 15, 16, // 72
17, 1, 18, 19, 1, 20, 21, 0, // 80
22, 23, 24, 25, 26, -1, 27, 28, // 88
29, 30, 31, -1, -1, -1, -1, -1, // 96
-1, 10, 11, 12, 13, 14, 15, 16, // 104
17, 1, 18, 19, 1, 20, 21, 0, // 112
22, 23, 24, 25, 26, -1, 27, 28, // 120
29, 30, 31
)
private const val MASK = 0x1FL
private const val MASK_INT = 0x1F
private const val MASK_BITS = 5
private const val TIMESTAMP_OVERFLOW_MASK = -0x1000000000000L
private fun timestamp(uuid: ID): Long {
return uuid.mostSignificantBits ushr 16
}
private fun internalParseCrockford(input: String): Long {
Objects.requireNonNull(input, "input must not be null!")
val length = input.length
require(length <= 12) { "input length must not exceed 12 but was $length!" }
var result: Long = 0
for (i in 0 until length) {
val current = input[i]
var value: Byte = -1
if (current.toInt() < DECODING_CHARS.size) {
value = DECODING_CHARS.get(current.toInt())
}
require(value >= 0) { "Illegal character '$current'!" }
result = result or (value.toLong() shl (length - 1 - i) * MASK_BITS)
}
return result
}
/* http://crockford.com/wrmg/base32.html */
private fun internalWriteCrockford(buffer: CharArray, value: Long, count: Int, offset: Int) {
for (i in 0 until count) {
val index = (value ushr (count - i - 1) * MASK_BITS and MASK).toInt()
buffer[offset + i] = ENCODING_CHARS[index]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment