main
Wisellama 2024-02-17 20:21:36 -08:00
commit 0c255850f2
5 changed files with 217 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*~

15
crockford_test.go Normal file
View File

@ -0,0 +1,15 @@
package main
import (
"testing"
)
func TestCrockfordEncode(t *testing.T) {
input := []byte{1, 141, 178, 57, 150, 88, 1, 148, 253, 194, 250, 47, 252, 192, 65, 211}
expected := "01HPS3K5JR06AFVGQT5ZYC0GEK"
output := CrockfordEncode(input)
if expected != output {
t.Errorf("expected %v, got %v", expected, output)
}
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module example.com/ulid
go 1.21.5
require github.com/oklog/ulid v1.3.1

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=

194
main.go Normal file
View File

@ -0,0 +1,194 @@
package main
import (
"encoding/binary"
"log"
"math/rand"
"time"
"github.com/oklog/ulid"
)
// ULID spec is mirrored here:
// https://git.wisellama.rocks/Mirrors/ulid-spec
func main() {
t := time.Date(2024, 02, 16, 14, 02, 15, 17, time.UTC)
ms := uint64(t.UnixMilli())
msBytes := GetMSBytes(t)
log.Printf("%X%X%X%X%X%X", msBytes[0], msBytes[1], msBytes[2], msBytes[3], msBytes[4], msBytes[5])
seed := int64(0)
entropy := rand.New(rand.NewSource(seed))
u, err := ulid.New(ms, entropy)
if err != nil {
log.Fatal(err)
}
log.Printf("%v", u)
log.Printf("%016X", u.Time())
log.Printf("%016X", ms)
log.Printf("%016X", ms)
// ULID is a 128-bit (16-byte) value similar to a UUID.
// The first 48-bits (6 bytes) are based on a timestamp.
// The remaining 80-bits (10 bytes) are random.
// I'm not implementing the monotonicity part of the ULID spec
// because I don't need it.
entropy = rand.New(rand.NewSource(seed))
randomBytes := make([]byte, 10)
_, err = entropy.Read(randomBytes)
if err != nil {
log.Fatalf("failed to read bytes from entropy source")
}
if len(msBytes) != 6 {
log.Fatalf("timestamp bytes are wrong")
}
if len(randomBytes) != 10 {
log.Fatalf("random bytes are wrong")
}
ulidBytes := make([]byte, 0, 16)
for _, b := range msBytes {
ulidBytes = append(ulidBytes, b)
}
for _, b := range randomBytes {
ulidBytes = append(ulidBytes, b)
}
ulidString := CrockfordEncode(ulidBytes)
log.Printf("ULID string: %v", ulidString)
}
// GetMSBytes returns the given Unix time in milliseconds as a 6-byte
// array. It truncates the 64-bit Unix epoch time down to 48-bits (6
// bytes) and returns that 6 byte array. According to the ULID spec,
// 48-bits is enough room that we won't run out of space until 10889
// AD.
func GetMSBytes(t time.Time) []byte {
ms := uint64(t.UnixMilli())
// Put the 64-bit int into a byte array
bytes := make([]byte, 8)
binary.BigEndian.PutUint64(bytes, ms)
// Chop off the first 2 bytes (16 bits) to get the 6 byte (48-bit)
// output.
return bytes[2:]
}
var (
crockfordEncodeMap = map[uint64]rune{
0: '0',
1: '1',
2: '2',
3: '3',
4: '4',
5: '5',
6: '6',
7: '7',
8: '8',
9: '9',
10: 'A',
11: 'B',
12: 'C',
13: 'D',
14: 'E',
15: 'F',
16: 'G',
17: 'H',
18: 'J',
19: 'K',
20: 'M',
21: 'N',
22: 'P',
23: 'Q',
24: 'R',
25: 'S',
26: 'T',
27: 'V',
28: 'W',
29: 'X',
30: 'Y',
31: 'Z',
}
crockfordDecodeMap = map[rune]uint64{
'0': 0,
'O': 0,
'o': 0,
'1': 1,
'I': 1,
'i': 1,
}
)
// CrockfordEncode takes a byte array and encodes every 5-bits as a
// character string according to Crockford's base 32 encoding.
//
// https://www.crockford.com/base32.html
func CrockfordEncode(bytes []byte) string {
// Crockford is a base 32 encoding.
// 2^5 = 32, so every 5 bits will give us a character.
// Each byte is 8 bits, so we'll have to smoosh a few bytes together.
// For ULIDs, we have 128 bits which doesn't evenly divide by 5.
//
// Technically we'll be encoding 130 bits of information
// (divisible by 5), but the timestamp will should always start
// with zeros.
if len(bytes) < 16 {
log.Printf("failed to encode, expected a 16 byte ULID")
return ""
}
log.Printf("bytes: %v", bytes)
// Split our bytes up into groups 40 bits each = 120 out of our
// 130 bits. Put these into byte arrays that are 8 bytes long so
// that we can convert them into uint64s.
last := append([]byte{0, 0, 0}, bytes[11:]...) // 11 12 13 14 15
log.Printf("last: %b", last)
third := append([]byte{0, 0, 0}, bytes[6:11]...) // 6 7 8 9 10
second := append([]byte{0, 0, 0}, bytes[1:7]...) // 1 2 3 4 5
// Plus the last 8 bits and 2 padding zeros to give us the remaining 10.
first := append([]byte{0, 0, 0, 0, 0, 0, 0}, bytes[0:1]...) // 0
// Convert each of those into integers so we have all the bits in one place.
lastInt := binary.BigEndian.Uint64(last)
thirdInt := binary.BigEndian.Uint64(third)
secondInt := binary.BigEndian.Uint64(second)
firstInt := binary.BigEndian.Uint64(first)
// Encode those ints into strings 5-bits at a time.
output := make([]rune, 0, 26)
shiftedInt := uint64(0)
for i := 1; i >= 0; i-- {
shiftedInt = firstInt >> (i * 5)
lookup := shiftedInt & 0b11111
output = append(output, crockfordEncodeMap[lookup])
}
for i := 7; i >= 0; i-- {
shiftedInt = secondInt >> (i * 5)
lookup := shiftedInt & 0b11111
output = append(output, crockfordEncodeMap[lookup])
}
for i := 7; i >= 0; i-- {
shiftedInt = thirdInt >> (i * 5)
lookup := shiftedInt & 0b11111
output = append(output, crockfordEncodeMap[lookup])
}
for i := 7; i >= 0; i-- {
shiftedInt = lastInt >> (i * 5)
lookup := shiftedInt & 0b11111
output = append(output, crockfordEncodeMap[lookup])
}
return string(output)
}