ULID is not strictly compatible with UUID
Suppose I generate a ULID, then convert it to a UUID.
Question
With which version of UUID is the result compatible?
Answer
Possibly none! Every UUID requires a variant and a version. This is also true of the original OSF DCE specification (page 586 gives the format, indicating version and variant).
A correctly-generated ULID will have random values in those fields. Thus, converting a ULID can result in a UUID that thinks its version number is anything from 0 to 15.
Why is this a problem?
I prefer to use ULID's internally, but to interact with other systems I need to generate UUID's that satisfies the requirements of a certain version. That means I'm basically out of luck, unless I want to bit-meddle. ...which I can do for the time being, but it seems like a suboptimal solution.
Proposals
- Add a caveat to the ULID spec that indicates this is a potential problem.
- Add a method that generates ULIDs compatible with a given UUID version. This probably shouldn't be allowed for UUIDs that have specific requirements.
This is an issue I'm coming across when porting the javascript implementation of parsing from ULID to UUID. This is not UUID compatible
I think it may be prudent to create ULID V2 which complies with the UUIDv8 spec to allow for interoperability with other systems.
Simply generate a UUIDv7 and encode it to Crockford base32. UUIDv7 ~can be considered~ is a subset of the ULID keyspace, but not vice versa. No need to use UUIDv8 spec.
Since UUIDv7 is ordered, it can be used instead of ULID. Because UUID is compatible with most of the systems, while ULID is not widely supported. If we need character efficient way to encode UUID to base32, below java snippet can be used.
import java.nio.ByteBuffer;
import java.util.UUID;
public class UUIDBase32Converter {
private static final char[] BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();
private static final int[] BASE32_LOOKUP = new int[128];
static {
for (int i = 0; i < BASE32_LOOKUP.length; i++) BASE32_LOOKUP[i] = -1;
for (int i = 0; i < BASE32_CHARS.length; i++) BASE32_LOOKUP[BASE32_CHARS[i]] = i;
}
// UUID to Base32 (no padding)
public static String uuidToBase32(UUID uuid) {
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
byte[] data = buffer.array();
StringBuilder base32 = new StringBuilder();
int index = 0, digit = 0;
int currByte, nextByte;
for (int i = 0; i < data.length;) {
currByte = data[i] & 0xFF;
if (index > 3) {
if (i + 1 < data.length) {
nextByte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256);
} else {
nextByte = 0;
}
digit = currByte & (0xFF >> index);
index = (index + 5) % 8;
digit <<= index;
digit |= nextByte >> (8 - index);
i++;
} else {
digit = (currByte >> (8 - (index + 5))) & 0x1F;
index = (index + 5) % 8;
if (index == 0) i++;
}
base32.append(BASE32_CHARS[digit]);
}
return base32.toString();
}
// Base32 to UUID
public static UUID base32ToUUID(String base32Str) {
String s = base32Str.toUpperCase().replace("=", "");
byte[] bytes = new byte[16];
int buffer = 0, bitsLeft = 0, byteIndex = 0;
for (char c : s.toCharArray()) {
if (c >= BASE32_LOOKUP.length || BASE32_LOOKUP[c] == -1) {
throw new IllegalArgumentException("Invalid Base32 character: " + c);
}
buffer <<= 5;
buffer |= BASE32_LOOKUP[c];
bitsLeft += 5;
if (bitsLeft >= 8) {
bytes[byteIndex++] = (byte) ((buffer >> (bitsLeft - 8)) & 0xFF);
bitsLeft -= 8;
if (byteIndex == 16) break; // only take 16 bytes
}
}
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
long mostSigBits = byteBuffer.getLong();
long leastSigBits = byteBuffer.getLong();
return new UUID(mostSigBits, leastSigBits);
}
public static void main(String[] args) {
UUID originalUUID = UUID.randomUUID();
System.out.println("Original UUID: " + originalUUID);
String base32 = uuidToBase32(originalUUID);
System.out.println("Base32: " + base32);
UUID decodedUUID = base32ToUUID(base32);
System.out.println("Decoded UUID: " + decodedUUID);
System.out.println("Match: " + originalUUID.equals(decodedUUID));
}
}
Example uses
UUIDBase32Converter.uuidToBase32(UUID.randomUUID());
UUUIDBase32Converter.base32ToUUID("JW4XO3YS4VANBFPIAG2R3D6U4Y");