Skip to content

Commit

Permalink
Merge pull request #2 from fajarnugraha37/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
fajarnugraha37 authored Oct 19, 2024
2 parents 61d0a3d + 58832d1 commit bf5fa03
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 153 deletions.
98 changes: 98 additions & 0 deletions lib/encode-decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { PRNG } from "../types/index.d.ts";
import { GLOBAL } from "./const.ts";
import { randomChar } from "./util.ts";

/**
* Extracts the timestamp given a valid ULID.
*
* @example encode the time to ULID
* ```ts
* import { encodeTime } from "@std/ulid";
* import { assertEquals } from "@std/assert";
*
* assertEquals("01ARYZ6S41", encodeTime(1469918176385, 10));
* ```
*
* @param now The number of milliseconds since the Unix epoch.
* @param len length of the generated string.
* @returns The ULID to extract the timestamp from.
*/
export function encodeTime(now: number, len: number = GLOBAL.TIME_LEN): string {
if (now > GLOBAL.TIME_MAX) {
throw new Deno.errors.InvalidData(
"cannot encode time greater than " + GLOBAL.TIME_MAX,
);
}
if (now < 0) {
throw new Deno.errors.InvalidData("time must be positive");
}
if (Number.isInteger(now) === false) {
throw new Deno.errors.InvalidData("time must be an integer");
}
let str = "";
for (; len > 0; len--) {
const mod = now % GLOBAL.ENCODING_LEN;
str = GLOBAL.ENCODING[mod] + str;
now = (now - mod) / GLOBAL.ENCODING_LEN;
}

return str;
}

/**
* Extracts the number of milliseconds since the Unix epoch that had passed when
* the ULID was generated. If the ULID is malformed, an error will be thrown.
*
* @example Decode the time from a ULID
* ```ts
* import { decodeTime, ulid } from "@std/ulid";
* import { assertEquals } from "@std/assert";
*
* const timestamp = 150_000;
* const ulidString = ulid(timestamp);
*
* assertEquals(decodeTime(ulidString), timestamp);
* ```
*
* @param ulid The ULID to extract the timestamp from.
* @returns The number of milliseconds since the Unix epoch that had passed when the ULID was generated.
*/
export function decodeTime(id: string): number {
if (id.length !== GLOBAL.TIME_LEN + GLOBAL.RANDOM_LEN) {
throw new Deno.errors.InvalidData("malformed ulid");
}
const time = id
.substring(0, GLOBAL.TIME_LEN)
.split("")
.reverse()
.reduce((carry, char, index) => {
const encodingIndex = GLOBAL.ENCODING.indexOf(char);
if (encodingIndex === -1) {
throw new Deno.errors.InvalidData("invalid character found: " + char);
}
return (carry += encodingIndex * Math.pow(GLOBAL.ENCODING_LEN, index));
}, 0);
if (time > GLOBAL.TIME_MAX) {
throw new Deno.errors.InvalidData("malformed ulid, timestamp too large");
}

return time;
}

/**
* Encodes a random string of specified length using the provided PRNG.
*
* This function iterates for the specified length, calling the `randomChar` function with the PRNG
* to generate a random character and prepend it to the string being built.
*
* @param {number} len - The desired length of the random string.
* @param {PRNG} prng - The PRNG to use for generating random characters.
* @returns {string} A random string of the specified length.
*/
export function encodeRandom(len: number, prng: PRNG): string {
let str = "";
for (; len > 0; len--) {
str = randomChar(prng) + str;
}
return str;
}
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./ulid-converter.ts";
export * from "./ulid-monotonic.ts";
export * from "./ulid.ts";
export * from "./util.ts";
66 changes: 66 additions & 0 deletions lib/ulid-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { GLOBAL } from "./const.ts";
import { crockford } from "./crockford.ts";

/**
* Converts a ULID string to a UUID string.
*
* This function validates the ULID string using a pre-defined regular expression (`GLOBAL.ULID_REGEX`).
* If invalid, it throws an `InvalidData` error. Otherwise, it decodes the ULID string using the `crockford.decode`
* function (assumed to be an external library) and converts the resulting Uint8Array to a UUID string
* in the standard format (e.g., "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
*
* @param {string} ulid - The ULID string to convert.
* @returns {string} The corresponding UUID string.
* @throws {Deno.errors.InvalidData} If the provided ULID is invalid.
*/
export function ulidToUUID(ulid: string): string {
const isValid = GLOBAL.ULID_REGEX.test(ulid);
if (!isValid) {
throw new Deno.errors.InvalidData("Invalid ULID");
}

const uint8Array = crockford.decode(ulid);
const uuid = Array.from(uint8Array)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");

return (
uuid.substring(0, 8) +
"-" +
uuid.substring(8, 12) +
"-" +
uuid.substring(12, 16) +
"-" +
uuid.substring(16, 20) +
"-" +
uuid.substring(20)
);
}

/**
* Converts a UUID string to a ULID string.
*
* This function validates the UUID string using a pre-defined regular expression (`GLOBAL.UUID_REGEX`).
* If invalid, it throws an `InvalidData` error. Otherwise, it removes hyphens from the UUID string and splits
* it into an array of byte pairs. It then converts each byte pair back to a number using hexadecimal parsing
* and creates a new Uint8Array. Finally, it uses the `crockford.encode` function (assumed to be an external library)
* to encode the Uint8Array into a ULID string.
*
* @param {string} uuid - The UUID string to convert.
* @returns {string} The corresponding ULID string.
* @throws {Deno.errors.InvalidData} If the provided UUID is invalid.
*/
export function uuidToULID(uuid: string): string {
const isValid = GLOBAL.UUID_REGEX.test(uuid);
if (!isValid) {
throw new Deno.errors.InvalidData("Invalid UUID");
}
const clean = uuid.replace(/-/g, "")
.match(/.{1,2}/g);
if (!clean) {
throw new Deno.errors.InvalidData("Invalid UUID");
}
const uint8Array = new Uint8Array(clean.map((byte) => parseInt(byte, 16)));

return crockford.encode(uint8Array);
}
68 changes: 68 additions & 0 deletions lib/ulid-monotonic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { PRNG, ULID } from "../types/index.d.ts";
import { GLOBAL } from "./const.ts";
import { encodeRandom, encodeTime } from "./encode-decode.ts";
import { detectPrng, incrementBase32 } from "./util.ts";

/**
* Creates a ULID generation factory function that ensures monotonic generation.
*
* This factory function generates ULIDs where the timestamp is always increasing or remains the same,
* and the random part is incremented only if the timestamp doesn't change. This ensures lexicographic sorting
* based on the ULID string.
*
* The default PRNG is chosen by the `detectPrng` function.
*
* @param {PRNG} [prng=detectPrng()] - The PRNG to use for generating random parts of the ULID.
* @returns {ULID} A function that generates monotonic ULIDs.
*/
export function monotonicFactory(prng: PRNG = detectPrng()): ULID {
let lastTime = 0;
let lastRandom: string;
return function ulid(seedTime: number = Date.now()): string {
if (seedTime <= lastTime) {
const incrementedRandom = (lastRandom = incrementBase32(lastRandom));
return encodeTime(lastTime, GLOBAL.TIME_LEN) + incrementedRandom;
}
lastTime = seedTime;
const newRandom = (lastRandom = encodeRandom(GLOBAL.RANDOM_LEN, prng));
return encodeTime(seedTime, GLOBAL.TIME_LEN) + newRandom;
};
}

/**
* Generate a ULID that monotonically increases even for the same millisecond,
* optionally passing the current time. If the current time is not passed, it
* will default to `Date.now()`.
*
* Unlike the {@linkcode ulid} function, this function is guaranteed to return
* strictly increasing ULIDs, even for the same seed time, but only if the seed
* time only ever increases. If the seed time ever goes backwards, the ULID will
* still be generated, but it will not be guaranteed to be monotonic with
* previous ULIDs for that same seed time.
*
* @example Generate a monotonic ULID
* ```ts no-assert
* import { monotonicUlid } from "@std/ulid";
*
* monotonicUlid(); // 01HYFKHG5F8RHM2PM3D7NSTDAS
* ```
*
* @example Generate a monotonic ULID with a seed time
* ```ts no-assert
* import { monotonicUlid } from "@std/ulid";
*
* // Strict ordering for the same timestamp, by incrementing the least-significant random bit by 1
* monotonicUlid(150000); // 0000004JFHJJ2Z7X64FN2B4F1Q
*
* // A different timestamp will reset the random bits
* monotonicUlid(150001); // 0000004JFHJJ2Z7X64FN2B4F1P
*
* // A previous seed time will not guarantee ordering, and may result in a
* // ULID lower than one with the same seed time generated previously
* monotonicUlid(150000); // 0000004JFJ7XF6D76ES95SZR0X
* ```
*
* @param seedTime The time to base the ULID on, in milliseconds since the Unix epoch. Defaults to `Date.now()`.
* @returns A ULID that is guaranteed to be strictly increasing for the same seed time.
*/
export const monotonicUlid: ULID = monotonicFactory();
Loading

0 comments on commit bf5fa03

Please sign in to comment.