Skip to content

Instantly share code, notes, and snippets.

@anonymous1184
Last active February 16, 2024 11:55
Show Gist options
  • Save anonymous1184/a3a21d9f7b1e08df4dc90d4ac64ba38a to your computer and use it in GitHub Desktop.
Save anonymous1184/a3a21d9f7b1e08df4dc90d4ac64ba38a to your computer and use it in GitHub Desktop.
ULID

Universally Unique Lexicographically Sortable Identifier

Canonical Spec: https://github.com/ulid/spec

ULID()

  • By default operates in monotonic mode.
  • ULID() is an alias of ULID.Monotonic().
  • Use ULID.Random() for non-monotonic mode.

ULID.DecodeTime()

  • Decodes the date component of an ID.

Examples

ULID.Monotonic()   ;           ├─ Sequential ─┤
ULID(946684799000) ; 00VHNCZA0RDNAV6EEZZ39ZJJ30
ULID(946684799000) ; 00VHNCZA0RDNAV6EEZZ39ZJJ31
ULID(946684799000) ; 00VHNCZA0RDNAV6EEZZ39ZJJ32
                   ; ├─ Date ─┤

ULID.Random()             ;           ├─── Random ───┤
ULID.Random(946684799000) ; 00VHNCZA0R0C2EZT1HJF682WGX
ULID.Random(946684799000) ; 00VHNCZA0R8KTKPFTW2DRTV8GD
ULID.Random(946684799000) ; 00VHNCZA0RMBY4YRRTATJH3WPR
                          ; ├─ Date ─┤

ts := ULID.DecodeTime("00VHNCZA0RCQXAP2B8G3DF054X")
; > 946684799000 > Fri, Dec 31, 1999 23:59:59 (GMT)
#Requires AutoHotkey v2.0
; Version: 20230.09.12.1
; https://gist.github.com/a3a21d9f7b1e08df4dc90d4ac64ba38a
; "Transpiled" from:
; https://github.com/ulid/javascript/blob/master/dist/index.js
; January 2, 2018 revision:
; https://github.com/ulid/javascript/blob/a5831206a11636c94d4657b9e1a1354c529ee4e9/dist/index.js
class ULID {
static ENCODING := "0123456789ABCDEFGHJKMNPQRSTVWXYZ" ; Crockford's B32
, ENCODING_LEN := 32 ; ENCODING.length
, TIME_MAX := 281474976710655
, TIME_LEN := 10
, RANDOM_LEN := 16
static Call(SeedTime := 0) {
return this.Monotonic(SeedTime)
}
static Monotonic(SeedTime := 0) {
static lastTime := 0, lastRandom := ""
if (!SeedTime) {
SeedTime := this._DateNow()
}
if (SeedTime <= lastTime) {
lastRandom := this.IncrementBase32(lastRandom)
incrementRandom := lastRandom
return this.EncodeTime(lastTime, this.TIME_LEN) incrementRandom
}
lastTime := SeedTime
lastRandom := this.EncodeRandom(this.RANDOM_LEN)
newRandom := lastRandom
return this.EncodeTime(SeedTime, this.TIME_LEN) newRandom
}
static Random(SeedTime := 0) {
if (!SeedTime) {
SeedTime := this._DateNow()
}
return this.EncodeTime(SeedTime, this.TIME_LEN) this.EncodeRandom(this.RANDOM_LEN)
}
static ReplaceCharAt(String, Index, Char) {
if (Index > StrLen(String)) {
return String
}
return SubStr(String, 1, Index) Char SubStr(String, Index + 2)
}
static IncrementBase32(Str) {
char := ""
charIndex := ""
index := StrLen(Str)
done := false
while (!done && index--) {
char := SubStr(Str, index + 1, 1)
charIndex := InStr(this.ENCODING, char)
if (charIndex = 0) {
throw Error("incorrectly encoded string", -1)
}
if (charIndex = this.ENCODING_LEN) {
len := SubStr(this.ENCODING, 1, 1)
Str := this.ReplaceCharAt(Str, index, len)
continue
}
len := SubStr(this.ENCODING, charIndex + 1, 1)
done := this.ReplaceCharAt(Str, index, len)
}
if (!done) {
throw Error("cannot increment this string", -1)
}
return done
}
static RandomChar() {
rand := Random(1, this.ENCODING_LEN)
return SubStr(this.ENCODING, rand, 1)
}
static EncodeTime(Now, Len) {
if !(Now is Number) {
throw Error(Now " must be a number", -1)
} else if (Now > this.TIME_MAX) {
throw Error("cannot encode time greater than " this.TIME_MAX, -1)
} else if (Now < 0) {
throw Error("time must be positive", -1)
} else if !(Now is Integer) {
throw Error("time must be an integer", -1)
}
str := ""
while (Len--) {
mdl := Mod(Now, this.ENCODING_LEN)
str := SubStr(this.ENCODING, mdl + 1, 1) str
Now := (Now - mdl) / this.ENCODING_LEN
}
return str
}
static EncodeRandom(Len) {
str := ""
while (Len--) {
str .= this.RandomChar()
}
return str
}
static DecodeTime(Id) {
timeLen := this.TIME_LEN
if (StrLen(Id) != this.TIME_LEN + this.RANDOM_LEN) {
throw Error("malformed ulid", -1)
}
time := ""
while (timeLen--) {
time .= SubStr(Id, timeLen + 1, 1)
}
if (time > this.TIME_MAX) {
throw Error("malformed ulid, timestamp too large", -1)
}
carry := 0
for (char in StrSplit(time)) {
encodingIndex := InStr(this.ENCODING, char) - 1
if (encodingIndex = -1) {
throw Error("invalid character found: " char, -1)
}
carry += encodingIndex * this.ENCODING_LEN ** (A_Index - 1)
}
return carry
}
static _DateNow() { ; JS' Date.now()
DllCall("GetSystemTimeAsFileTime", "Int64*", &ft := 0)
return (ft - 116444736000000000) // 10000
}
}
; spell:ignore 0123456789ABCDEFGHJKMNPQRSTVWXYZ Crockford's
#Requires AutoHotkey v1.1
; Version: 20230.09.12.1
; https://gist.github.com/a3a21d9f7b1e08df4dc90d4ac64ba38a
; "Transpiled" from:
; https://github.com/ulid/javascript/blob/master/dist/index.js
; January 2, 2018 revision:
; https://github.com/ulid/javascript/blob/a5831206a11636c94d4657b9e1a1354c529ee4e9/dist/index.js
ULID(SeedTime := 0) {
return ULID.Monotonic(SeedTime)
}
class ULID {
static ENCODING := "0123456789ABCDEFGHJKMNPQRSTVWXYZ" ; Crockford's B32
, ENCODING_LEN := 32 ; ENCODING.length
, TIME_MAX := 281474976710655
, TIME_LEN := 10
, RANDOM_LEN := 16
Monotonic(SeedTime := 0) {
static lastTime := 0, lastRandom := ""
if (!SeedTime) {
SeedTime := this._DateNow()
}
if (SeedTime <= lastTime) {
lastRandom := this.IncrementBase32(lastRandom)
incrementRandom := lastRandom
return this.EncodeTime(lastTime, this.TIME_LEN) incrementRandom
}
lastTime := SeedTime
lastRandom := this.EncodeRandom(this.RANDOM_LEN)
newRandom := lastRandom
return this.EncodeTime(SeedTime, this.TIME_LEN) newRandom
}
Random(SeedTime := 0) {
if (!SeedTime) {
SeedTime := this._DateNow()
}
return this.EncodeTime(SeedTime, this.TIME_LEN) this.EncodeRandom(this.RANDOM_LEN)
}
ReplaceCharAt(String, Index, Char) {
if (Index > StrLen(String)) {
return String
}
return SubStr(String, 1, Index) Char SubStr(String, Index + 2)
}
IncrementBase32(Str) {
char := ""
charIndex := ""
index := StrLen(Str)
done := false
while (!done && index--) {
char := SubStr(Str, index + 1, 1)
charIndex := InStr(this.ENCODING, char)
if (charIndex = 0) {
throw Exception("incorrectly encoded string", -1)
}
if (charIndex = this.ENCODING_LEN) {
len := SubStr(this.ENCODING, 1, 1)
Str := this.ReplaceCharAt(Str, index, len)
continue
}
len := SubStr(this.ENCODING, charIndex + 1, 1)
done := this.ReplaceCharAt(Str, index, len)
}
if (!done) {
throw Exception("cannot increment this string", -1)
}
return done
}
RandomChar() {
Random rand, 1, this.ENCODING_LEN
return SubStr(this.ENCODING, rand, 1)
}
EncodeTime(Now, Len) {
if !(Now + 0) {
throw Exception(Now " must be a number", -1)
} else if (Now > this.TIME_MAX) {
throw Exception("cannot encode time greater than " this.TIME_MAX, -1)
} else if (Now < 0) {
throw Exception("time must be positive", -1)
} else if (InStr(Now, ".")) {
throw Exception("time must be an integer", -1)
}
str := ""
while (Len--) {
mdl := Mod(Now, this.ENCODING_LEN)
str := SubStr(this.ENCODING, mdl + 1, 1) str
Now := (Now - mdl) / this.ENCODING_LEN
}
return str
}
EncodeRandom(Len) {
str := ""
while (Len--) {
str .= this.RandomChar()
}
return str
}
DecodeTime(Id) {
timeLen := this.TIME_LEN
if (StrLen(Id) != this.TIME_LEN + this.RANDOM_LEN) {
throw Exception("malformed ulid", -1)
}
time := ""
while (timeLen--) {
time .= SubStr(Id, timeLen + 1, 1)
}
if (time > this.TIME_MAX) {
throw Exception("malformed ulid, timestamp too large", -1)
}
carry := 0
for _, char in StrSplit(time) {
encodingIndex := InStr(this.ENCODING, char) - 1
if (encodingIndex = -1) {
throw Exception("invalid character found: " char, -1)
}
carry += encodingIndex * this.ENCODING_LEN ** (A_Index - 1)
}
return carry
}
_DateNow() { ; JS' Date.now()
DllCall("GetSystemTimeAsFileTime", "Int64*", ft := 0)
return (ft - 116444736000000000) // 10000
}
}
; spell:ignore 0123456789ABCDEFGHJKMNPQRSTVWXYZ Crockford's
--- ULID1.ahk
+++ ULID.ahk
@@ -1 +1 @@
-#Requires AutoHotkey v1.1
+#Requires AutoHotkey v2.0
@@ -12,4 +11,0 @@
-ULID(SeedTime := 0) {
- return ULID.Monotonic(SeedTime)
-}
-
@@ -24 +20,5 @@
- Monotonic(SeedTime := 0) {
+ static Call(SeedTime := 0) {
+ return this.Monotonic(SeedTime)
+ }
+
+ static Monotonic(SeedTime := 0) {
@@ -40 +40 @@
- Random(SeedTime := 0) {
+ static Random(SeedTime := 0) {
@@ -47 +47 @@
- ReplaceCharAt(String, Index, Char) {
+ static ReplaceCharAt(String, Index, Char) {
@@ -54 +54 @@
- IncrementBase32(Str) {
+ static IncrementBase32(Str) {
@@ -63 +63 @@
- throw Exception("incorrectly encoded string", -1)
+ throw Error("incorrectly encoded string", -1)
@@ -74 +74 @@
- throw Exception("cannot increment this string", -1)
+ throw Error("cannot increment this string", -1)
@@ -79,2 +79,2 @@
- RandomChar() {
- Random rand, 1, this.ENCODING_LEN
+ static RandomChar() {
+ rand := Random(1, this.ENCODING_LEN)
@@ -84,3 +84,3 @@
- EncodeTime(Now, Len) {
- if !(Now + 0) {
- throw Exception(Now " must be a number", -1)
+ static EncodeTime(Now, Len) {
+ if !(Now is Number) {
+ throw Error(Now " must be a number", -1)
@@ -88 +88 @@
- throw Exception("cannot encode time greater than " this.TIME_MAX, -1)
+ throw Error("cannot encode time greater than " this.TIME_MAX, -1)
@@ -90,3 +90,3 @@
- throw Exception("time must be positive", -1)
- } else if (InStr(Now, ".")) {
- throw Exception("time must be an integer", -1)
+ throw Error("time must be positive", -1)
+ } else if !(Now is Integer) {
+ throw Error("time must be an integer", -1)
@@ -103 +103 @@
- EncodeRandom(Len) {
+ static EncodeRandom(Len) {
@@ -111 +111 @@
- DecodeTime(Id) {
+ static DecodeTime(Id) {
@@ -114 +114 @@
- throw Exception("malformed ulid", -1)
+ throw Error("malformed ulid", -1)
@@ -121 +121 @@
- throw Exception("malformed ulid, timestamp too large", -1)
+ throw Error("malformed ulid, timestamp too large", -1)
@@ -124 +124 @@
- for _, char in StrSplit(time) {
+ for (char in StrSplit(time)) {
@@ -127 +127 @@
- throw Exception("invalid character found: " char, -1)
+ throw Error("invalid character found: " char, -1)
@@ -134,2 +134,2 @@
- _DateNow() { ; JS' Date.now()
- DllCall("GetSystemTimeAsFileTime", "Int64*", ft := 0)
+ static _DateNow() { ; JS' Date.now()
+ DllCall("GetSystemTimeAsFileTime", "Int64*", &ft := 0)
@mdelgadoschwartz
Copy link

Very nice code. Could you provide a few more instructions? (I am not very familiar with Autohotkey scripting btw.)
At the moment I run the following script to make the ULID, and it works, but I am wondering whether I need to call first the current date or something. 🙂

::ulid-::
{
SendInput ULID()
}

Thanks in advance for any help!

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