Skip to content

Commit

Permalink
Feat/decode (#2)
Browse files Browse the repository at this point in the history
* feat: Added decode logic

* Added comparatoin with old implementation

* Added test for illegal arguments

* Adds comment for referenced libraries
  • Loading branch information
jkugiya committed Sep 5, 2019
1 parent 4bd3ecb commit fe7ed80
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 85 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -37,3 +37,9 @@ val binary: Array[Byte] = generator.binary()

## LICENSE
[LICENSE](https://github.com/jkugiya/ulid-scala/blob/master/LICENSE)

## Prior Arts
- [azam/ulidj](https://github.com/azam/ulidj)
- [sulky-ulid](https://github.com/huxi/sulky/tree/master/sulky-ulid)
- [ulid4s](https://github.com/petitviolet/ulid4s)

2 changes: 1 addition & 1 deletion build.sbt
Expand Up @@ -2,7 +2,7 @@ import Dependencies._

ThisBuild / scalaVersion := "2.13.0"
ThisBuild / crossScalaVersions := Seq("2.13.0", "2.12.9", "2.11.11")
ThisBuild / version := "0.2.0-SNAPSHOT"
ThisBuild / version := "1.0.0-SNAPSHOT"
ThisBuild / organization := "com.github.jkugiya"
ThisBuild / organizationName := "jkugiya"

Expand Down
86 changes: 86 additions & 0 deletions src/main/scala/jkugiya/ulid/Base32Codec.scala
@@ -0,0 +1,86 @@
package jkugiya.ulid

import java.nio.ByteBuffer

private[ulid] object Base32Codec {

private val toBase32 = 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'
)

def encode(ulid: ULID): String = {
val chars = new Array[Char](26)
// timestamp(10byte)
// take last 5bit
val timestamp = ulid.time
chars(9) = toBase32((timestamp & 0x1F).toInt)
chars(8) = toBase32((timestamp >>> 5 & 0x1F).toInt)
chars(7) = toBase32((timestamp >>> 10 & 0x1F).toInt)
chars(6) = toBase32((timestamp >>> 15 & 0x1F).toInt)
chars(5) = toBase32((timestamp >>> 20 & 0x1F).toInt)
chars(4) = toBase32((timestamp >>> 25 & 0x1F).toInt)
chars(3) = toBase32((timestamp >>> 30 & 0x1F).toInt)
chars(2) = toBase32((timestamp >>> 35 & 0x1F).toInt)
chars(1) = toBase32((timestamp >>> 40 & 0x1F).toInt)
chars(0) = toBase32((timestamp >>> 45 & 0x1F).toInt)
// randomness(16byte / 80bits)
// take 5bit
val randomness = ulid.originalRandomness
chars(10) = toBase32(((randomness(0) & 0xFF) >>> 3) & 0x1F)
chars(11) = toBase32(((randomness(0) << 2) | ((randomness(1) & 0xFF) >>> 6)) & 0x1F)
chars(12) = toBase32(((randomness(1) & 0xFF) >>> 1) & 0x1F)
chars(13) = toBase32(((randomness(1) << 4) | ((randomness(2) & 0xFF) >>> 4)) & 0x1F)
chars(14) = toBase32(((randomness(2) << 1) | ((randomness(3) & 0xFF) >>> 7)) & 0x1F)
chars(15) = toBase32(((randomness(3) & 0xFF) >>> 2) & 0x1F)
chars(16) = toBase32(((randomness(3) << 3) | ((randomness(4) & 0xFF) >>> 5)) & 0x1F)
chars(17) = toBase32(randomness(4) & 0x1F)
chars(18) = toBase32((randomness(5) & 0xFF) >>> 3 & 0x1F)
chars(19) = toBase32(((randomness(5) << 2) | ((randomness(6) & 0xFF) >>> 6)) & 0x1F)
chars(20) = toBase32(((randomness(6) & 0xFF) >>> 1) & 0x1F)
chars(21) = toBase32(((randomness(6) << 4) | ((randomness(7) & 0xFF) >>> 4)) & 0x1F)
chars(22) = toBase32(((randomness(7) << 1) | ((randomness(8) & 0xFF) >>> 7)) & 0x1F)
chars(23) = toBase32(((randomness(8) & 0xFF) >>> 2) & 0x1F)
chars(24) = toBase32(((randomness(8) << 3) | ((randomness(9) & 0xFF) >>> 5)) & 0x1F)
chars(25) = toBase32(randomness(9) & 0x1F)
new String(chars)
}

def decode(base32: String): ULID = {
if (base32.length != 26) {
throw new IllegalArgumentException("The length of Base32 string should be 26")
}
val chars = base32.toCharArray
var i = 0
var time = 0L
var next40 = 0L
var last40 = 0L
def bit = {
val bit = toBase32.indexOf(chars(i))
if (bit < 0) {
throw new IllegalArgumentException(s"Given string contains invalid character. (${chars(i)})")
}
bit
}
while(i < 10) {
time = (time << 5) | bit
i += 1
}
while (i < 18) {
next40 = (next40 << 5) | bit
i += 1
}
while(i < 26) {
last40 = (last40 << 5) | bit
i += 1
}
val first64 = next40 << 24 | (last40 >>> 16)
val last16: Short = (last40 & 0xFFFF).toShort
val buffer = ByteBuffer.allocate(10)
buffer.putLong(first64).putShort(last16)
val ulid = new ULID(time, buffer.array())
ulid
}
}
48 changes: 0 additions & 48 deletions src/main/scala/jkugiya/ulid/Base32Encoder.scala

This file was deleted.

32 changes: 32 additions & 0 deletions src/main/scala/jkugiya/ulid/BinaryCodec.scala
@@ -0,0 +1,32 @@
package jkugiya.ulid

import java.nio.ByteBuffer

import jkugiya.ulid.ULID.ByteLengthOfULID

private[ulid] object BinaryCodec {

def encode(ulid: ULID): Array[Byte] = {
val buffer = ByteBuffer.allocate(ByteLengthOfULID)
buffer.putLong(ulid.time << 16) // takes 48bit only
buffer.position(6)
buffer.put(ulid.originalRandomness)
buffer.array()
}

def decode(binary: Array[Byte]): ULID = {
if (binary.length != 16) {
throw new IllegalArgumentException("Binary length should be 16.")
}
val binaryBuffer = ByteBuffer.wrap(binary)
val m = binaryBuffer.getLong
val l = binaryBuffer.getLong
val time = m >>> 16
val next64 = (m << 48 | l >>> 16)
val last16 = (l & 0xFFFF).toShort
val buffer = ByteBuffer.allocate(10)
buffer.putLong(next64).putShort(last16)
new ULID(time, buffer.array())
}

}
39 changes: 19 additions & 20 deletions src/main/scala/jkugiya/ulid/ULID.scala
@@ -1,6 +1,5 @@
package jkugiya.ulid

import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.{ UUID, Random => JRandom }

Expand Down Expand Up @@ -41,36 +40,42 @@ object ULID {
def getGenerator(random: JRandom = secureGenerator): ULIDGenerator =
new StatefulGenerator(random)

def fromBase32(base32: String): ULID =
Base32Codec.decode(base32)

def fromUUID(uuid: UUID): ULID =
UUIDCodec.decode(uuid)

def fromBinary(binary: Array[Byte]): ULID =
BinaryCodec.decode(binary)

}

private[ulid] trait ULIDGenerator {

def generate(): ULID

final def binary(): Array[Byte] = generate().binary
final def binary(): Array[Byte] =
BinaryCodec.encode(generate())

final def base32(): String =
Base32Encoder.encode(generate())
Base32Codec.encode(generate())

final def uuid(): UUID =
UUIDEncoder.encode(generate())
UUIDCodec.encode(generate())

def algorithm(): String

}

private[ulid] trait ULIDEncoder[A] {
def encode(ulid: ULID): A
}

private[ulid] class ULID(val time: Long, private[ulid] val originalRandomness: Array[Byte]) {
if (time > MaxTimestamp || time < MinTimestamp) {
throw new IllegalArgumentException(s"Invalid timestamp is given.(${time}, should be between ${MinTimestamp} to ${MaxTimestamp}.")
}
if (originalRandomness.length != 10) {
val byteLengh = originalRandomness.length
val bitLength = byteLengh * 8
throw new IllegalArgumentException(s"randomness should be 80bits(10 byte), but ${bitLength} bits(${byteLengh} byte) randomness is given.")
val byteLength = originalRandomness.length
val bitLength = byteLength * 8
throw new IllegalArgumentException(s"randomness should be 80bits(10 byte), but ${bitLength} bits(${byteLength} byte) randomness is given.")
}

def this(time: Long, random: JRandom) = {
Expand All @@ -82,17 +87,11 @@ private[ulid] class ULID(val time: Long, private[ulid] val originalRandomness: A
})
}

def binary: Array[Byte] = {
val buffer = ByteBuffer.allocate(ByteLengthOfULID)
buffer.putLong(time << 16) // takes 48bit only
buffer.position(6)
buffer.put(originalRandomness)
buffer.array()
}
def binary: Array[Byte] = BinaryCodec.encode(this)

def base32: String = Base32Encoder.encode(this)
def base32: String = Base32Codec.encode(this)

def uuid: UUID = UUIDEncoder.encode(this)
def uuid: UUID = UUIDCodec.encode(this)

def randomness: Array[Byte] = {
val value = new Array[Byte](RandomnessSize)
Expand Down
26 changes: 26 additions & 0 deletions src/main/scala/jkugiya/ulid/UUIDCodec.scala
@@ -0,0 +1,26 @@
package jkugiya.ulid

import java.nio.ByteBuffer
import java.util.UUID

private[ulid] object UUIDCodec {

def encode(ulid: ULID): UUID = {
val binary = ulid.binary
val buffer = ByteBuffer.wrap(binary)
new UUID(buffer.getLong(), buffer.getLong)
}

def decode(uuid: UUID): ULID = {
val m = uuid.getMostSignificantBits
val l = uuid.getLeastSignificantBits
val time = m >>> 16
val next64 = (m << 48 | l >>> 16)
val last16 = (l & 0xFFFF).toShort
val buffer = ByteBuffer.allocate(10)
buffer.putLong(next64).putShort(last16)
new ULID(time, buffer.array())
}

}

15 changes: 0 additions & 15 deletions src/main/scala/jkugiya/ulid/UUIDEncoder.scala

This file was deleted.

64 changes: 64 additions & 0 deletions src/test/scala/jkugiya/ulid/Base32Logics.scala
@@ -0,0 +1,64 @@
package jkugiya.ulid

import java.nio.ByteBuffer

object Base32Logics {
private val toBase32 = 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'
)

// old implementation
def logic1(time: Long, randomness: Array[Byte]): String = {
val ulid = new ULID(time, randomness)
val binary = ulid.binary
(Stream(0, 0) ++ (0 to 127).toStream.map { i =>
val bitValueOfI = (binary(i / 8) << (i % 8)) & 0x80
if (bitValueOfI == 0) 0
else 1
}).grouped(5).map { buffer =>
val bit5 = buffer.toArray
val index =
(bit5(0) << 4) |
(bit5(1) << 3) |
(bit5(2) << 2) |
(bit5(3) << 1) |
bit5(4)
toBase32(index)
}.mkString
}

// old implementation
val MaskForTake5 = 0xf800000000000000L
def logic2(time: Long, randomness: Array[Byte]): String = {
val ulid = new ULID(time, randomness)
val binary = ulid.binary // len = 16
val chars = new Array[Char](26)
val mostSigBits = ByteBuffer.wrap(binary.slice(0, 8)).getLong
val leastSigBits = ByteBuffer.wrap(binary.slice(8, 16)).getLong
var i = 0
// first 60bits(empty 2bits + 58bits)
val firstBits = mostSigBits >>> 6 << 4
while(i < 12) {
chars(i) = toBase32(((firstBits << (i * 5) & MaskForTake5) >>> 59).toInt)
i += 1
}
// second 60bits
val secondBits = ((mostSigBits << 58) | (leastSigBits >>> 10 << 4))
i = 12
while(i < 24) {
chars(i) = toBase32(((secondBits << (i - 12) * 5 & MaskForTake5) >>> 59).toInt)
i += 1
}
// third 10 bits
i = 24
val thirdBits = leastSigBits << 54
while(i < 26) {
chars(i) = toBase32(((thirdBits << (i - 24) * 5 & MaskForTake5) >>> 59).toInt)
i += 1
}
chars.mkString
}
}

0 comments on commit fe7ed80

Please sign in to comment.