Skip to content

Instantly share code, notes, and snippets.

@ccampo133
Last active August 24, 2022 19:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ccampo133/5e19679b6bd882ac6170921cc72413aa to your computer and use it in GitHub Desktop.
Save ccampo133/5e19679b6bd882ac6170921cc72413aa to your computer and use it in GitHub Desktop.
ULID in Kotlin
/**
* Adopted from https://github.com/JonasSchubert/kULID
*
* MIT License
*
* Copyright (c) 2021 GuepardoApps (Jonas Schubert), Christopher J. Campo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import kotlin.experimental.and
import kotlin.random.Random
class ULID {
companion object {
private const val ULID_LENGTH = 26
private const val DEFAULT_ENTROPY_SIZE = 10
// Min and max allowed timestamp values.
private const val MIN_TIMESTAMP = 0x0L
private const val MAX_TIMESTAMP = 0x0000ffffffffffffL
// Base32 characters mapping
private val charMapping = 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'
)
/**
* Generate random ULID string using [kotlin.random.Random] instance.
* @return ULID string
*/
fun random(): String = generate(System.currentTimeMillis(), Random.nextBytes(DEFAULT_ENTROPY_SIZE))
/**
* Generate ULID from Unix epoch timestamp in millisecond and entropy bytes.
* Throws [java.lang.IllegalArgumentException] if timestamp is less than {@value #MIN_TIME},
* is more than {@value #MAX_TIME}, or entropy bytes is null or less than 10 bytes.
* @param time Unix epoch timestamp in millisecond
* @param entropy Entropy bytes
* @return ULID string
*/
fun generate(time: Long, entropy: ByteArray): String {
if (time < MIN_TIMESTAMP || time > MAX_TIMESTAMP || entropy.size < DEFAULT_ENTROPY_SIZE) {
throw IllegalArgumentException("Time is too long, or entropy is less than 10 bytes or null")
}
val chars = CharArray(ULID_LENGTH)
// time
chars[0] = charMapping[time.ushr(45).toInt() and 0x1f]
chars[1] = charMapping[time.ushr(40).toInt() and 0x1f]
chars[2] = charMapping[time.ushr(35).toInt() and 0x1f]
chars[3] = charMapping[time.ushr(30).toInt() and 0x1f]
chars[4] = charMapping[time.ushr(25).toInt() and 0x1f]
chars[5] = charMapping[time.ushr(20).toInt() and 0x1f]
chars[6] = charMapping[time.ushr(15).toInt() and 0x1f]
chars[7] = charMapping[time.ushr(10).toInt() and 0x1f]
chars[8] = charMapping[time.ushr(5).toInt() and 0x1f]
chars[9] = charMapping[time.toInt() and 0x1f]
// entropy
chars[10] = charMapping[(entropy[0].toShort() and 0xff).toInt().ushr(3)]
chars[11] =
charMapping[(entropy[0].toInt() shl 2 or (entropy[1].toShort() and 0xff).toInt().ushr(6) and 0x1f)]
chars[12] = charMapping[((entropy[1].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
chars[13] =
charMapping[(entropy[1].toInt() shl 4 or (entropy[2].toShort() and 0xff).toInt().ushr(4) and 0x1f)]
chars[14] =
charMapping[(entropy[2].toInt() shl 5 or (entropy[3].toShort() and 0xff).toInt().ushr(7) and 0x1f)]
chars[15] = charMapping[((entropy[3].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
chars[16] =
charMapping[(entropy[3].toInt() shl 3 or (entropy[4].toShort() and 0xff).toInt().ushr(5) and 0x1f)]
chars[17] = charMapping[(entropy[4].toInt() and 0x1f)]
chars[18] = charMapping[(entropy[5].toShort() and 0xff).toInt().ushr(3)]
chars[19] =
charMapping[(entropy[5].toInt() shl 2 or (entropy[6].toShort() and 0xff).toInt().ushr(6) and 0x1f)]
chars[20] = charMapping[((entropy[6].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
chars[21] =
charMapping[(entropy[6].toInt() shl 4 or (entropy[7].toShort() and 0xff).toInt().ushr(4) and 0x1f)]
chars[22] =
charMapping[(entropy[7].toInt() shl 5 or (entropy[8].toShort() and 0xff).toInt().ushr(7) and 0x1f)]
chars[23] = charMapping[((entropy[8].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
chars[24] =
charMapping[(entropy[8].toInt() shl 3 or (entropy[9].toShort() and 0xff).toInt().ushr(5) and 0x1f)]
chars[25] = charMapping[(entropy[9].toInt() and 0x1f)]
return String(chars).toLowerCase()
}
}
}
@petered
Copy link

petered commented Aug 24, 2022

Awesome, very useful, thanks.

@ccampo133
Copy link
Author

@petered you bet, glad you found it useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment