├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── base.d.ts ├── base.js ├── index.d.ts ├── index.js ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /base.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Encodes a string to a Base62 string. 3 | 4 | @param string - The string to encode. 5 | @returns The Base62 encoded string. 6 | 7 | @example 8 | ``` 9 | import base62 from '@sindresorhus/base62'; 10 | 11 | const encodedString = base62.encodeString('Hello world!'); 12 | console.log(encodedString); 13 | //=> '8nlogx6nlNdJhVT24v' 14 | 15 | console.log(base62.decodeString(encodedString)); 16 | //=> 'Hello world!' 17 | 18 | console.log(base62.encodeString('🦄')); 19 | //=> '95s3vg' 20 | ``` 21 | */ 22 | export function encodeString(string: string): string; 23 | 24 | /** 25 | Decodes a Base62 encoded string created with `encodeString()` back to the original string. 26 | 27 | @param encodedString - The Base62 encoded string to decode. 28 | @returns The decoded string. 29 | 30 | @example 31 | ``` 32 | import base62 from '@sindresorhus/base62'; 33 | 34 | const encodedString = base62.encodeString('Hello world!'); 35 | console.log(encodedString); 36 | //=> '8nlogx6nlNdJhVT24v' 37 | 38 | console.log(base62.decodeString(encodedString)); 39 | //=> 'Hello world!' 40 | 41 | console.log(base62.encodeString('🦄')); 42 | //=> '95s3vg' 43 | ``` 44 | */ 45 | export function decodeString(encodedString: string): string; 46 | 47 | /** 48 | Encodes bytes to a Base62 string. 49 | 50 | @param bytes - The bytes to encode. 51 | @returns The Base62 encoded string. 52 | */ 53 | export function encodeBytes(bytes: Uint8Array): string; 54 | 55 | /** 56 | Decodes a Base62 string created with `encodeBytes()` back to bytes. 57 | 58 | @param encodedString - The Base62 encoded string to decode. 59 | @returns The decoded bytes as Uint8Array. 60 | */ 61 | export function decodeBytes(encodedString: string): Uint8Array; 62 | 63 | /** 64 | Encodes a non-negative integer to a Base62 string. 65 | 66 | @param integer - The integer to encode. 67 | @returns The Base62 encoded string. 68 | 69 | @example 70 | ``` 71 | import base62 from '@sindresorhus/base62'; 72 | 73 | console.log(base62.encodeInteger(1337)); 74 | //=> 'LZ' 75 | ``` 76 | */ 77 | export function encodeInteger(integer: number): string; 78 | 79 | /** 80 | Decodes a Base62 encoded string to an integer. 81 | 82 | @param encodedString - The Base62 string to decode. 83 | @returns The decoded integer. 84 | */ 85 | export function decodeInteger(encodedString: string): number; 86 | 87 | /** 88 | Encodes a non-negative bigint to a Base62 string. 89 | 90 | @param bigint - The bigint to encode. 91 | @returns The Base62 encoded string. 92 | */ 93 | export function encodeBigInt(bigint: bigint): string; 94 | 95 | /** 96 | Decodes a Base62 encoded string to a bigint. 97 | 98 | @param encodedString - The Base62 string to decode. 99 | @returns The decoded bigint. 100 | */ 101 | export function decodeBigInt(encodedString: string): bigint; 102 | -------------------------------------------------------------------------------- /base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 3 | const BASE = 62; 4 | const BASE_BIGINT = 62n; 5 | const ALPHABET = [...'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz']; 6 | const INDICES = new Map(ALPHABET.map((character, index) => [character, index])); 7 | 8 | const cachedEncoder = new globalThis.TextEncoder(); 9 | const cachedDecoder = new globalThis.TextDecoder(); 10 | 11 | function assertString(value, label) { 12 | if (typeof value !== 'string') { 13 | throw new TypeError(`The \`${label}\` parameter must be a string, got \`${value}\` (${typeof value}).`); 14 | } 15 | } 16 | 17 | function getIndex(character) { 18 | const index = INDICES.get(character); 19 | 20 | if (index === undefined) { 21 | throw new TypeError(`Unexpected character for Base62 encoding: \`${character}\`.`); 22 | } 23 | 24 | return index; 25 | } 26 | 27 | export function encodeString(string) { 28 | assertString(string, 'string'); 29 | return encodeBytes(cachedEncoder.encode(string)); 30 | } 31 | 32 | export function decodeString(encodedString) { 33 | assertString(encodedString, 'encodedString'); 34 | return cachedDecoder.decode(decodeBytes(encodedString)); 35 | } 36 | 37 | export function encodeBytes(bytes) { 38 | if (!(bytes instanceof Uint8Array)) { 39 | throw new TypeError('The `bytes` parameter must be an instance of Uint8Array.'); 40 | } 41 | 42 | if (bytes.length === 0) { 43 | return ''; 44 | } 45 | 46 | // Prepend 0x01 to the byte array before encoding to ensure the BigInt conversion 47 | // does not strip any leading zeros and to prevent any byte sequence from being 48 | // interpreted as a numerically zero value. 49 | let value = 1n; 50 | 51 | for (const byte of bytes) { 52 | value = (value << 8n) | BigInt(byte); 53 | } 54 | 55 | return encodeBigInt(value); 56 | } 57 | 58 | export function decodeBytes(encodedString) { 59 | assertString(encodedString, 'encodedString'); 60 | 61 | if (encodedString.length === 0) { 62 | return new Uint8Array(); 63 | } 64 | 65 | let value = decodeBigInt(encodedString); 66 | 67 | const byteArray = []; 68 | while (value > 0n) { 69 | byteArray.push(Number(value & 0xFFn)); 70 | value >>= 8n; 71 | } 72 | 73 | // Remove the 0x01 that was prepended during encoding. 74 | return Uint8Array.from(byteArray.reverse().slice(1)); 75 | } 76 | 77 | export function encodeInteger(integer) { 78 | if (!Number.isInteger(integer)) { 79 | throw new TypeError(`Expected an integer, got \`${integer}\` (${typeof integer}).`); 80 | } 81 | 82 | if (integer < 0) { 83 | throw new TypeError('The integer must be non-negative.'); 84 | } 85 | 86 | if (integer === 0) { 87 | return ALPHABET[0]; 88 | } 89 | 90 | let encodedString = ''; 91 | while (integer > 0) { 92 | encodedString = ALPHABET[integer % BASE] + encodedString; 93 | integer = Math.floor(integer / BASE); 94 | } 95 | 96 | return encodedString; 97 | } 98 | 99 | export function decodeInteger(encodedString) { 100 | assertString(encodedString, 'encodedString'); 101 | 102 | let integer = 0; 103 | for (const character of encodedString) { 104 | integer = (integer * BASE) + getIndex(character); 105 | } 106 | 107 | return integer; 108 | } 109 | 110 | export function encodeBigInt(bigint) { 111 | if (typeof bigint !== 'bigint') { 112 | throw new TypeError(`Expected a bigint, got \`${bigint}\` (${typeof bigint}).`); 113 | } 114 | 115 | if (bigint < 0) { 116 | throw new TypeError('The bigint must be non-negative.'); 117 | } 118 | 119 | if (bigint === 0n) { 120 | return ALPHABET[0]; 121 | } 122 | 123 | let encodedString = ''; 124 | while (bigint > 0n) { 125 | encodedString = ALPHABET[Number(bigint % BASE_BIGINT)] + encodedString; 126 | bigint /= BigInt(BASE); 127 | } 128 | 129 | return encodedString; 130 | } 131 | 132 | export function decodeBigInt(encodedString) { 133 | assertString(encodedString, 'encodedString'); 134 | 135 | let bigint = 0n; 136 | for (const character of encodedString) { 137 | bigint = (bigint * BASE_BIGINT) + BigInt(getIndex(character)); 138 | } 139 | 140 | return bigint; 141 | } 142 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * as default from './base.js'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * as default from './base.js'; 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sindresorhus/base62", 3 | "version": "0.1.0", 4 | "description": "Encode & decode strings, bytes, and integers to Base62", 5 | "license": "MIT", 6 | "repository": "sindresorhus/base62", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts", 28 | "base.js", 29 | "base.d.ts" 30 | ], 31 | "keywords": [ 32 | "base62", 33 | "base", 34 | "base64", 35 | "base-62", 36 | "encode", 37 | "decode", 38 | "shorten", 39 | "compress", 40 | "compact", 41 | "alphanumeric", 42 | "serialization", 43 | "url", 44 | "safe", 45 | "text", 46 | "string", 47 | "number", 48 | "integer", 49 | "bigint", 50 | "bytes", 51 | "uint8array", 52 | "algorithm", 53 | "transformation", 54 | "encoder", 55 | "decoder", 56 | "encoding", 57 | "decoding", 58 | "url-friendly", 59 | "url-safe" 60 | ], 61 | "devDependencies": { 62 | "ava": "^6.1.2", 63 | "xo": "^0.58.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # base62 2 | 3 | > Encode & decode strings, bytes, and integers to [Base62](https://en.wikipedia.org/wiki/Base62) 4 | 5 | Base62 is ideal for URL shortening, creating readable codes, and compact data representation, because it compresses large values into shorter, alphanumeric strings, maximizing space efficiency and readability. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install @sindresorhus/base62 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import base62 from '@sindresorhus/base62'; 17 | 18 | const encodedString = base62.encodeString('Hello world!'); 19 | console.log(encodedString); 20 | //=> '8nlogx6nlNdJhVT24v' 21 | 22 | console.log(base62.decodeString(encodedString)); 23 | //=> 'Hello world!' 24 | 25 | console.log(base62.encodeString('🦄')); 26 | //=> '95s3vg' 27 | 28 | console.log(base62.encodeInteger(1337)); 29 | //=> 'LZ' 30 | ``` 31 | 32 | > [!NOTE] 33 | > The output may differ from other Base62 encoders due to variations in alphabet order and byte encoding. 34 | 35 | ## API 36 | 37 | It uses the most common alphabet for Base62: `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` 38 | 39 | ### `encodeString(string: string): string` 40 | 41 | Encodes a string to a Base62 string. 42 | 43 | > [!CAUTION] 44 | > The result format is not yet guaranteed to be stable across package versions. Avoid using it for persistent storage. 45 | 46 | ### `decodeString(encodedString: string): string` 47 | 48 | Decodes a Base62 encoded string created with `encodeString()` back to the original string. 49 | 50 | ### `encodeBytes(bytes: Uint8Array): string` 51 | 52 | Encodes bytes to a Base62 string. 53 | 54 | > [!CAUTION] 55 | > The result format is not yet guaranteed to be stable across package versions. Avoid using it for persistent storage. 56 | 57 | ### `decodeBytes(encodedString: string): Uint8Array` 58 | 59 | Decodes a Base62 string created with `encodeBytes()` back to bytes. 60 | 61 | ### `encodeInteger(integer: number): string` 62 | 63 | Encodes a non-negative integer to a Base62 string. 64 | 65 | ### `decodeInteger(encodedString: string): number` 66 | 67 | Decodes a Base62 string to an integer. 68 | 69 | ### `encodeBigInt(integer: bigint): string` 70 | 71 | Encodes a non-negative bigint to a Base62 string. 72 | 73 | ### `decodeBigInt(encodedString: string): bigint` 74 | 75 | Decodes a Base62 string to a bigint. 76 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import base62 from './index.js'; 3 | 4 | test('encodeBytes / decodeBytes - round-trip', t => { 5 | const input = new Uint8Array([10, 20, 30, 40, 255]); 6 | const encoded = base62.encodeBytes(input); 7 | const decoded = base62.decodeBytes(encoded); 8 | t.deepEqual(decoded, input); 9 | }); 10 | 11 | const testStringRoundtrip = (t, input, expected) => { 12 | const encoded = base62.encodeString(input); 13 | const decoded = base62.decodeString(encoded); 14 | t.is(encoded, expected); 15 | t.is(decoded, input); 16 | }; 17 | 18 | const testStrings = new Map([ 19 | ['Hello, World!', '8nlogx6nlNdJhVT24v'], 20 | ['Another example🏴', 'B6f8m2TtOhNkJuVfeJVXhKTPAi'], 21 | ['1234567890', '7CipUfMcknk2uu'], 22 | ['🦄', '95s3vg'], 23 | ['😊🚀🌟💥', 'F7782ZaAxP6MFPZUluW18H'], 24 | ['Special characters ~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\', 'BU3kOcSFw49tim4FMGq22KElf62dgQ6YAeCoc2XVcK32JQ5LoO2TfOn2cwCL5PPeRU9BE'], 25 | ['', ''], 26 | ['0', '4u'], 27 | ['000x', '5ZNTk8'], 28 | ]); 29 | 30 | for (const [string, expected] of testStrings) { 31 | test(`encodeString / decodeString - round-trip for "${string}"`, t => { 32 | testStringRoundtrip(t, string, expected); 33 | }); 34 | } 35 | 36 | test('encodeInteger / decodeInteger - round-trip', t => { 37 | const input = 1_234_567_890; 38 | const encoded = base62.encodeInteger(input); 39 | const decoded = base62.decodeInteger(encoded); 40 | t.is(decoded, input); 41 | }); 42 | 43 | test('encodeBigInt / decodeBigInt - round-trip', t => { 44 | const input = 123_456_789_012_345_678_901_234_567_890n; 45 | const encoded = base62.encodeBigInt(input); 46 | const decoded = base62.decodeBigInt(encoded); 47 | t.is(decoded, input); 48 | }); 49 | 50 | test('encodeBytes - handles single byte edge cases', t => { 51 | const input = new Uint8Array([0, 0, 0, 1, 9, 10, 35, 61, 62, 255]); 52 | const encoded = base62.encodeBytes(input); 53 | const decoded = base62.decodeBytes(encoded); 54 | t.deepEqual(decoded, input); 55 | }); 56 | 57 | test('encodeString - handles Unicode characters', t => { 58 | const input = '😊🚀🌟💥'; 59 | const encoded = base62.encodeString(input); 60 | const decoded = base62.decodeString(encoded); 61 | t.is(decoded, input); 62 | }); 63 | 64 | test('encodeInteger - handles small and large numbers', t => { 65 | const inputs = [ 66 | 0, 67 | 1, 68 | 12, 69 | 123, 70 | 1234, 71 | 12_345, 72 | 123_456, 73 | 1_234_567, 74 | 12_345_678, 75 | Number.MAX_SAFE_INTEGER, 76 | ]; 77 | 78 | for (const input of inputs) { 79 | const encoded = base62.encodeInteger(input); 80 | const decoded = base62.decodeInteger(encoded); 81 | t.is(decoded, input); 82 | } 83 | }); 84 | 85 | test('encodeBigInt - handles very large BigInts', t => { 86 | const inputs = [ 87 | 0n, 88 | 1n, 89 | 12_345_678_901_234_567_890n, 90 | 98_765_432_109_876_543_210n, 91 | 0x1F_FF_FF_FF_FF_FF_FFn, // Close to Number.MAX_SAFE_INTEGER 92 | 0xFF_FF_FF_FF_FF_FF_FF_FF_FF_FF_FF_FF_FF_FF_FF_FFn, 93 | ]; 94 | 95 | for (const input of inputs) { 96 | const encoded = base62.encodeBigInt(input); 97 | const decoded = base62.decodeBigInt(encoded); 98 | t.is(decoded, input); 99 | } 100 | }); 101 | --------------------------------------------------------------------------------