@sindresorhus/base62
Version:
Encode & decode strings, bytes, and integers to Base62
178 lines (137 loc) • 4.94 kB
JavaScript
/* eslint-disable no-bitwise */
const BASE = 62;
const BASE_BIGINT = 62n;
const DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const cachedEncoder = new globalThis.TextEncoder();
const cachedDecoder = new globalThis.TextDecoder();
function assertString(value, label) {
if (typeof value !== 'string') {
throw new TypeError(`The \`${label}\` parameter must be a string, got \`${value}\` (${typeof value}).`);
}
}
function validateAlphabet(alphabet) {
if (typeof alphabet !== 'string') {
throw new TypeError(`The alphabet must be a string, got \`${alphabet}\` (${typeof alphabet}).`);
}
if (alphabet.length !== BASE) {
throw new TypeError(`The alphabet must be exactly ${BASE} characters long, got ${alphabet.length}.`);
}
const uniqueCharacters = new Set(alphabet);
if (uniqueCharacters.size !== BASE) {
throw new TypeError(`The alphabet must contain ${BASE} unique characters, got ${uniqueCharacters.size}.`);
}
}
export class Base62 {
constructor(options = {}) {
const alphabet = options.alphabet ?? DEFAULT_ALPHABET;
validateAlphabet(alphabet);
this.alphabet = [...alphabet];
this.indices = new Map(this.alphabet.map((character, index) => [character, index]));
}
#getIndex(character) {
const index = this.indices.get(character);
if (index === undefined) {
throw new TypeError(`Unexpected character for Base62 encoding: \`${character}\`.`);
}
return index;
}
encodeString(string) {
assertString(string, 'string');
return this.encodeBytes(cachedEncoder.encode(string));
}
decodeString(encodedString) {
assertString(encodedString, 'encodedString');
return cachedDecoder.decode(this.decodeBytes(encodedString));
}
encodeBytes(bytes) {
if (!(bytes instanceof Uint8Array)) {
throw new TypeError('The `bytes` parameter must be an instance of Uint8Array.');
}
if (bytes.length === 0) {
return '';
}
// Prepend 0x01 to the byte array before encoding to ensure the BigInt conversion
// does not strip any leading zeros and to prevent any byte sequence from being
// interpreted as a numerically zero value.
let value = 1n;
for (const byte of bytes) {
value = (value << 8n) | BigInt(byte);
}
return this.encodeBigInt(value);
}
decodeBytes(encodedString) {
assertString(encodedString, 'encodedString');
if (encodedString.length === 0) {
return new Uint8Array();
}
let value = this.decodeBigInt(encodedString);
const byteArray = [];
while (value > 0n) {
byteArray.push(Number(value & 0xFFn));
value >>= 8n;
}
// Remove the 0x01 that was prepended during encoding.
return Uint8Array.from(byteArray.reverse().slice(1));
}
encodeInteger(integer) {
if (!Number.isInteger(integer)) {
throw new TypeError(`Expected an integer, got \`${integer}\` (${typeof integer}).`);
}
if (integer < 0) {
throw new TypeError('The integer must be non-negative.');
}
if (integer === 0) {
return this.alphabet[0];
}
let encodedString = '';
while (integer > 0) {
encodedString = this.alphabet[integer % BASE] + encodedString;
integer = Math.floor(integer / BASE);
}
return encodedString;
}
decodeInteger(encodedString) {
assertString(encodedString, 'encodedString');
let integer = 0;
for (const character of encodedString) {
integer = (integer * BASE) + this.#getIndex(character);
}
return integer;
}
encodeBigInt(bigint) {
if (typeof bigint !== 'bigint') {
throw new TypeError(`Expected a bigint, got \`${bigint}\` (${typeof bigint}).`);
}
if (bigint < 0) {
throw new TypeError('The bigint must be non-negative.');
}
if (bigint === 0n) {
return this.alphabet[0];
}
let encodedString = '';
while (bigint > 0n) {
encodedString = this.alphabet[Number(bigint % BASE_BIGINT)] + encodedString;
bigint /= BASE_BIGINT;
}
return encodedString;
}
decodeBigInt(encodedString) {
assertString(encodedString, 'encodedString');
let bigint = 0n;
for (const character of encodedString) {
bigint = (bigint * BASE_BIGINT) + BigInt(this.#getIndex(character));
}
return bigint;
}
}
// Create default instance with standard alphabet
const defaultBase62 = new Base62();
// Export convenience functions using the default alphabet for backward compatibility
export const encodeString = string => defaultBase62.encodeString(string);
export const decodeString = encodedString => defaultBase62.decodeString(encodedString);
export const encodeBytes = bytes => defaultBase62.encodeBytes(bytes);
export const decodeBytes = encodedString => defaultBase62.decodeBytes(encodedString);
export const encodeInteger = integer => defaultBase62.encodeInteger(integer);
export const decodeInteger = encodedString => defaultBase62.decodeInteger(encodedString);
export const encodeBigInt = bigint => defaultBase62.encodeBigInt(bigint);
export const decodeBigInt = encodedString => defaultBase62.decodeBigInt(encodedString);