How would one convert a UUID type to ULID type?

2.9k Views Asked by At

Theres a bit documentation out there how to convert ULID to UUID but not so much when you need to convert UUID to ULID.

I'm looking at this UUID / ULID generator / converter https://www.ulidtools.com/

but I'm not quite sure how I would replicate the UUID to ULID conversion, the source is too obfuscated for me to understand.

I'm not sure where to even begin, is this conversion even safe ? will it guarantee a unique conversion ?

4

There are 4 best solutions below

3
sanukj On

Pure Javascript Method for conversion:

Converting a UUID to a ULID guarantee a unique code. ULIDs are unique 128-bit identifiers that are generated based on a combination of the current timestamp.

function convertUUIDtoULID(uuid) {
  const uuidBinary = uuid.split("-").map((hex) => parseInt(hex, 16));
  return uuidBinary
    .slice(0, 8)
    .map((byte) => byte.toString(32))
    .concat(uuidBinary.slice(8).map((byte) => byte.toString(32)))
    .join("");
}

const uuid = "454391df-b950-42ea-a2c0-92d62c215d67";
const ulid = convertUUIDtoULID(uuid);
console.log(ulid);
  • The function takes in a string of UUID format as an input
  • It splits the UUID string at each "-" character and converts each resulting hexadecimal string into a decimal number using the parseInt() function with base 16.
  • It creates a new array of the first 8 decimal numbers from the UUID and converts each number to a base-32 string using the toString() function with base 32.
  • It concatenates this array with a new array of the remaining decimal numbers from the UUID, also converted to base-32 strings.
  • The resulting array of base-32 strings is joined together into a single string, which is returned as the ULID.
  • The example UUID string is passed into the function and the resulting ULID string is logged to the console.

You can also use [https://www.npmjs.com/package/ulid] for converting UUID to ULID

1
Iman Hosseini Pour On

I've had the same issue and I took a look at ulidtools.com then after digging into its source code for a while I found that it uses this package behind the seen.

import pkg from "id128";
const { Ulid, Uuid4 } = pkg;

const ulid = Ulid.generate();
const ulidToUuid = Uuid4.fromRaw(ulid.toRaw());
const uuidToUlid = Ulid.fromRaw(ulidToUuid.toRaw());

console.table([
  {
    generated_ulid: ulid.toCanonical(),
    converted_to_uuid: ulidToUuid.toCanonical(),
    converted_back_to_ulid: uuidToUlid.toCanonical(),
  },
]);

0
Renato Gama On

Based on @ImanHosseiniPour answer I came up with the following:

TIMESTAMP + UUID = ULID

import { Ulid, Uuid4 } from "id128";
import { factory, decodeTime } from 'ulid'

const genUlid = factory();

function convertUuidToUlid(
  timestamp: Date, 
  canonicalUuid: string,
): Ulid {
  const uuid = Uuid4.fromCanonical(canonicalUuid);
  const convertedUlid = Ulid.fromRaw(uuid.toRaw())
  const ulidTimestamp = genUlid(timestamp.valueOf()).slice(0, 10)
  const ulidRandom = convertedUlid.toCanonical().slice(10);
  
  return Ulid.fromCanonical(ulidTimestamp + ulidRandom)
}

const timestamp = new Date()
const uuid = 'f0df59ea-bfe2-43a8-98d4-8213348daeb6'
const ulid = convertUuidToUlid(timestamp, uuid)
const originalUuid = Uuid4.fromRaw(ulid.toRaw());

console.table({
  timestamp: timestamp.valueOf(),
  uuid,
  ulid: ulid.toCanonical(),
  decodedTime: decodeTime(ulid.toCanonical()),
  originalUuid: originalUuid.toCanonical(),
});

Keep in mind that reverse engineering the ULID into a UUID will make the two first chunks different from the original since we incorporated the timestamp

0
popestr On

The other answers to this question didn't satisfy my needs (no external dependencies). Here is a version which works with vanilla ECMAScript2018 JS:

/**
 * Decodes a hexadecimal string (case-insensitive) into an equivalent Uint8Array.
 * 
 * @param {string} hexString The string to decode
 * @returns {Uint8Array} The string decoded into binary
 */
function decodeHex(hexString) {
    if (typeof hexString !== 'string' || hexString.length % 2 !== 0) {
        throw new Error('Invalid hex string');
    }

    const decoded = new Uint8Array(hexString.length / 2);

    for (let i = 0; i < hexString.length; i += 2) {
        const byte = parseInt(hexString.substring(i, i + 2), 16);
        decoded[i / 2] = byte;
    }

    return decoded;
}


/**
 * The ULID encoding lookup. Notably excludes I, L, U, and O.
 */
const ULID_ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

/**
 * Converts a UUID to an equivalent ULID.
 * 
 * @param {string} uuid The UUID, encoded as a 36-character hex-with-dashes string.
 * @returns {string} The equivalent ULID, encoded as a 26-character base32 string.
 */
function uuidToUlid(uuid) {
    if (!/^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$/i.test(uuid)) {
        throw new Error('Invalid UUID.');
    }

    // Break into sections, excluding dashes.
    // Using 0179e73f-ff38-a5e0-e633-48fae1c0bd35 as example...

    const section1 = uuid.substring(0, 8);      // is 0179e73f
    const section2 = uuid.substring(9, 13);     // is ff38
    const section3 = uuid.substring(14, 18);    // is a5e0
    const section4 = uuid.substring(19, 23);    // is e633
    const section5 = uuid.substring(24);        // is 48fae1c0bd35

    // concatenate the parts, decoded into Uint8Array types. This will have length 16.
    const decodedArray = [
        ...decodeHex(section1),
        ...decodeHex(section2),
        ...decodeHex(section3),
        ...decodeHex(section4),
        ...decodeHex(section5),
    ];

    // optimized unrolled loop for converting 16 bytes into 26 characters, using 
    // the ULID lookup to translate from integers [0-25] to valid ULID characters.
    // ref. https://github.com/RobThree/NUlid
    const ulid = [
        ULID_ENCODING[(decodedArray[0] & 224) >> 5],
        ULID_ENCODING[decodedArray[0] & 31],
        ULID_ENCODING[(decodedArray[1] & 248) >> 3],
        ULID_ENCODING[((decodedArray[1] & 7) << 2) | ((decodedArray[2] & 192) >> 6)],
        ULID_ENCODING[(decodedArray[2] & 62) >> 1],
        ULID_ENCODING[((decodedArray[2] & 1) << 4) | ((decodedArray[3] & 240) >> 4)],
        ULID_ENCODING[((decodedArray[3] & 15) << 1) | ((decodedArray[4] & 128) >> 7)],
        ULID_ENCODING[(decodedArray[4] & 124) >> 2],
        ULID_ENCODING[((decodedArray[4] & 3) << 3) | ((decodedArray[5] & 224) >> 5)],
        ULID_ENCODING[decodedArray[5] & 31],
        ULID_ENCODING[(decodedArray[6] & 248) >> 3],
        ULID_ENCODING[((decodedArray[6] & 7) << 2) | ((decodedArray[7] & 192) >> 6)],
        ULID_ENCODING[(decodedArray[7] & 62) >> 1],
        ULID_ENCODING[((decodedArray[7] & 1) << 4) | ((decodedArray[8] & 240) >> 4)],
        ULID_ENCODING[((decodedArray[8] & 15) << 1) | ((decodedArray[9] & 128) >> 7)],
        ULID_ENCODING[(decodedArray[9] & 124) >> 2],
        ULID_ENCODING[((decodedArray[9] & 3) << 3) | ((decodedArray[10] & 224) >> 5)],
        ULID_ENCODING[decodedArray[10] & 31],
        ULID_ENCODING[(decodedArray[11] & 248) >> 3],
        ULID_ENCODING[((decodedArray[11] & 7) << 2) | ((decodedArray[12] & 192) >> 6)],
        ULID_ENCODING[(decodedArray[12] & 62) >> 1],
        ULID_ENCODING[((decodedArray[12] & 1) << 4) | ((decodedArray[13] & 240) >> 4)],
        ULID_ENCODING[((decodedArray[13] & 15) << 1) | ((decodedArray[14] & 128) >> 7)],
        ULID_ENCODING[(decodedArray[14] & 124) >> 2],
        ULID_ENCODING[((decodedArray[14] & 3) << 3) | ((decodedArray[15] & 224) >> 5)],
        ULID_ENCODING[decodedArray[15] & 31]
    ].join('');

    return ulid;
}

const uuid = '0179e73f-ff38-a5e0-e633-48fae1c0bd35';
const ulid = uuidToUlid(uuid);
console.log(ulid); // 01F7KKZZSRMQGECCT8ZBGW1F9N

Here is a JSFiddle if you want to verify functionality.