Last active
July 18, 2021 17:09
-
-
Save jean-merelis/aaa7913ee0f994170d12b317ae179039 to your computer and use it in GitHub Desktop.
ulid - Kotlin implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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