Skip to content

ULID

Universally Unique Lexicographically Sortable Identifier

ulid.mjs

js
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32
const TIME_LEN = 10;
const RANDOM_LEN = 16;

/**
 * Convert a number to base32 encoded string
 *
 * @param {number} value
 * @param {number} length
 * @returns {string}
 */
function encodeBase32(value, length) {
  let encoded = "";

  while (length--) {
    encoded = ENCODING[value % 32] + encoded;
    value = Math.floor(value / 32);
  }
  return encoded;
}

/**
 * Get current time in milliseconds
 *
 * @returns {string} ULID time part
 */
function getTimePart() {
  const time = Date.now();
  return encodeBase32(time, TIME_LEN);
}

/**
 * Generate randomness
 *
 * @returns {string} ULID random part
 */
function getRandomPart() {
  let randomPart = "";

  for (let i = 0; i < RANDOM_LEN; i++) {
    randomPart += ENCODING[Math.floor(Math.random() * 32)];
  }
  return randomPart;
}

/**
 * Combine time part and random part
 *
 * @returns {string} ULID
 */
export const ulid = () => getTimePart() + getRandomPart();

/**
 * Convert base32 encoded string to a number
 *
 * @param {string} encoded
 * @returns {number}
 */
function decodeBase32(encoded) {
  let value = 0;

  for (let i = 0; i < encoded.length; i++) {
    value = value * 32 + ENCODING.indexOf(encoded[i]);
  }
  return value;
}

/**
 * Convert the time part back to milliseconds
 *
 * @param {string} ulid
 * @returns {number}
 */
export const getTimeFromUlid = ulid => {
  // Extract the time part...
  const timePart = ulid.substring(0, TIME_LEN);
  return decodeBase32(timePart);
};

ulid.spec.mjs

js
import { test, expect } from "vitest";
import { ulid, getTimeFromUlid } from "./index.mjs";

test("it should combine time part and random part", () => {
  const id = ulid();
  expect(id.substring(0, 2)).toEqual("01");
  expect(id).toHaveLength(26);
});

test("it should convert the time part back to milliseconds", () => {
  const time = new Date(getTimeFromUlid("01J9H6RFH20B3MSN0CE487E5EG"));
  expect(time.getDate()).toEqual(6);
  expect(time.getMonth()).toEqual(9);
  expect(time.getFullYear()).toEqual(2024);
});

Released under the MIT License.