base60-codec 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # ๐Ÿ“ฆ base60-codec
2
+
3
+ A tiny, fast, and deterministic Base60 encoder/decoder for TypeScript.
4
+
5
+ - Fixed-length Base60 IDs for UUID / ULID / Int64
6
+ - BigInt-based (no precision loss)
7
+ - Stable alphabet (0โ€“9 Aโ€“Z aโ€“z without ambiguous characters)
8
+ - Brand types for type-safe Base60 strings
9
+ - Zero dependencies
10
+ - ESM ready (NodeNext)
11
+
12
+ Ideal for generating compact, URL-safe IDs with predictable ordering.
13
+
14
+ ## ๐Ÿš€ Installation
15
+
16
+ ```
17
+ npm install base60-codec
18
+ ```
19
+
20
+ ## ๐Ÿงฉ Quick Usage
21
+
22
+ ```
23
+ import { base60 } from "base60-codec";
24
+
25
+ const uuid = "550e8400-e29b-41d4-a716-446655440000";
26
+
27
+ // Encode as 22-char Base60
28
+ const encoded = base60.encodeUUID(uuid);
29
+ console.log(encoded); // e.g. "09EzBRW... (22 chars)"
30
+
31
+ // Decode back to UUID
32
+ console.log(base60.decodeUUID(encoded));
33
+ // โ†’ "550e8400-e29b-41d4-a716-446655440000"
34
+ ```
35
+
36
+ ## โœจ Features
37
+
38
+ ### โœ… UUID (128-bit) โ†’ 22 chars
39
+
40
+ ```
41
+ encodeUUID(uuid: string): string
42
+ decodeUUID(id: Base60String): string
43
+ ```
44
+
45
+ ### โœ… ULID (26 chars Base32) โ†’ 22 chars
46
+
47
+ ```
48
+ encodeULID(ulid: string): Base60String
49
+ decodeULID(id: Base60String): string
50
+ ```
51
+
52
+ ### โœ… Int64 โ†’ 11 chars
53
+
54
+ ```
55
+ encodeInt64(num: number | bigint): string
56
+ decodeInt64(id: Base60String): bigint
57
+ ```
58
+
59
+ ### โœ… BigInt encoding
60
+
61
+ ```
62
+ encodeBigInt(value: bigint, padLength?: number): string
63
+ decodeToBigInt(text: Base60String): bigint
64
+ ```
65
+
66
+ ### โœ… Safe comparison
67
+
68
+ ```
69
+ compareAsBigInt(a: Base60String, b: Base60String): -1 | 0 | 1
70
+ ```
71
+
72
+ ### โœ… Type-safe Base60 string guard
73
+
74
+ ```
75
+ if (base60.isValidBase60(text)) {
76
+ // text is now typed as Base60String
77
+ }
78
+ ```
79
+
80
+ ## ๐Ÿ“ Alphabet
81
+
82
+ ```
83
+ 0123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
84
+ ```
85
+
86
+ - No visually ambiguous characters (0/O, I/l, etc.)
87
+ - Stable ordering
88
+ - URL-safe
89
+
90
+ ## โš ๏ธ Notes
91
+
92
+ 1. `encodeBytes()` / `decodeToBytes()` **is not fully reversible**
93
+
94
+ Leading zero bytes are dropped:
95
+
96
+ ```
97
+ encodeBytes(Uint8Array([0,1,2]))
98
+ โ†“
99
+ decodeToBytes(...) โ†’ [1,2]
100
+ ```
101
+
102
+ This is expected: Base60 โ†’ BigInt โ†’ bytes produces the minimal byte length.
103
+
104
+ UUID / ULID / Int64 are unaffected because they use fixed 16-byte / 8-byte decoding internally.
105
+
106
+ ## ๐Ÿงช Testing
107
+
108
+ ```
109
+ npm test
110
+ ```
111
+
112
+ Uses Vitest.
113
+
114
+ ## ๐Ÿ“œ License
115
+
116
+ MIT
117
+
118
+ ## ๐ŸŽ‰ Summary
119
+
120
+ base60-codec gives you:
121
+
122
+ - deterministic, compact, comparable Base60 identifiers
123
+ - 22-char identifiers for both UUID & ULID
124
+ - 11-char identifiers for Int64
125
+ - safe brand-typed Base60 strings
126
+ - pure TypeScript implementation (no deps)
127
+
128
+ Perfect for generating short IDs in databases, URLs, logs, or distributed systems.
package/dist/index.cjs ADDED
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ base60: () => base60,
24
+ createBase60Codec: () => createBase60Codec
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var FIXED_LEN_UUID = 22;
28
+ var FIXED_LEN_ULID = 22;
29
+ var FIXED_LEN_INT64 = 11;
30
+ var alphabet = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
31
+ var crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
32
+ function createBase60Codec() {
33
+ const base = BigInt(alphabet.length);
34
+ const charToValue = /* @__PURE__ */ new Map();
35
+ for (let i = 0; i < alphabet.length; i++) {
36
+ charToValue.set(alphabet[i], BigInt(i));
37
+ }
38
+ function leftPad(text, length) {
39
+ if (text.length > length) {
40
+ throw new Error(
41
+ `Encoded value length (${text.length}) exceeds fixed length ${length}`
42
+ );
43
+ }
44
+ return text.padStart(length, alphabet[0]);
45
+ }
46
+ function bytesToBigInt(bytes) {
47
+ let result = 0n;
48
+ for (const b of bytes) {
49
+ result = (result << 8n) + BigInt(b);
50
+ }
51
+ return result;
52
+ }
53
+ function bigIntToBytes(value, fixedLength) {
54
+ if (value < 0n) {
55
+ throw new Error("negative bigint is not supported");
56
+ }
57
+ let hex = value.toString(16);
58
+ if (hex.length % 2 === 1) hex = "0" + hex;
59
+ const bytes = new Uint8Array(hex.length / 2);
60
+ for (let i = 0; i < bytes.length; i++) {
61
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
62
+ }
63
+ if (fixedLength !== void 0) {
64
+ if (bytes.length > fixedLength) {
65
+ throw new Error("decoded bytes exceed fixed length");
66
+ }
67
+ const padded = new Uint8Array(fixedLength);
68
+ padded.set(bytes, fixedLength - bytes.length);
69
+ return padded;
70
+ }
71
+ return bytes;
72
+ }
73
+ function encodeBigInt(value, padLength) {
74
+ if (value === 0n) {
75
+ return padLength != null ? leftPad(alphabet[0], padLength) : alphabet[0];
76
+ }
77
+ let v = value;
78
+ let out = "";
79
+ while (v > 0n) {
80
+ const mod = v % base;
81
+ v = v / base;
82
+ out = alphabet[Number(mod)] + out;
83
+ }
84
+ return padLength != null ? leftPad(out, padLength) : out;
85
+ }
86
+ function decodeToBigInt(text) {
87
+ let v = 0n;
88
+ for (const ch of text) {
89
+ const d = charToValue.get(ch);
90
+ if (d === void 0) {
91
+ throw new Error(`invalid character "${ch}"`);
92
+ }
93
+ v = v * base + d;
94
+ }
95
+ return v;
96
+ }
97
+ return {
98
+ alphabet,
99
+ encodeBytes(bytes) {
100
+ return encodeBigInt(bytesToBigInt(bytes));
101
+ },
102
+ decodeToBytes(text) {
103
+ return bigIntToBytes(decodeToBigInt(text));
104
+ },
105
+ encodeBigInt,
106
+ decodeToBigInt,
107
+ encodeInt64(value) {
108
+ return leftPad(
109
+ encodeBigInt(BigInt(value)),
110
+ FIXED_LEN_INT64
111
+ );
112
+ },
113
+ decodeInt64(text) {
114
+ if (text.length !== FIXED_LEN_INT64) {
115
+ throw new Error(`Expected ${FIXED_LEN_INT64} chars for Base60 Int64`);
116
+ }
117
+ return decodeToBigInt(text);
118
+ },
119
+ encodeUUID(uuid) {
120
+ const hex = uuid.replace(/-/g, "");
121
+ if (hex.length !== 32) throw new Error("invalid UUID");
122
+ const bytes = new Uint8Array(16);
123
+ for (let i = 0; i < 16; i++) {
124
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
125
+ }
126
+ return leftPad(
127
+ encodeBigInt(bytesToBigInt(bytes)),
128
+ FIXED_LEN_UUID
129
+ );
130
+ },
131
+ decodeUUID(text) {
132
+ const bytes = bigIntToBytes(decodeToBigInt(text), 16);
133
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
134
+ return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20);
135
+ },
136
+ encodeULID(ulid) {
137
+ if (!/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(ulid)) {
138
+ throw new Error("Invalid ULID format");
139
+ }
140
+ const map32 = /* @__PURE__ */ new Map();
141
+ for (let i = 0; i < crockford.length; i++) {
142
+ map32.set(crockford[i], i);
143
+ }
144
+ let value = 0n;
145
+ for (const ch of ulid.toUpperCase()) {
146
+ const v = map32.get(ch);
147
+ if (v === void 0) throw new Error(`Invalid ULID char: ${ch}`);
148
+ value = (value << 5n) + BigInt(v);
149
+ }
150
+ const raw = this.encodeBigInt(value);
151
+ return leftPad(raw, FIXED_LEN_ULID);
152
+ },
153
+ decodeULID(text) {
154
+ if (text.length !== 22) {
155
+ throw new Error("Expected 22-char Base60 ULID");
156
+ }
157
+ const value = this.decodeToBigInt(text);
158
+ const bytes = bigIntToBytes(value, 16);
159
+ let bits = "";
160
+ for (const b of bytes) {
161
+ bits += b.toString(2).padStart(8, "0");
162
+ }
163
+ const padded = bits.padStart(130, "0");
164
+ let ulid = "";
165
+ for (let i = 0; i < 26; i++) {
166
+ const chunk = padded.slice(i * 5, i * 5 + 5);
167
+ ulid += crockford[parseInt(chunk, 2)];
168
+ }
169
+ return ulid;
170
+ },
171
+ compareAsBigInt(a, b) {
172
+ const ai = decodeToBigInt(a);
173
+ const bi = decodeToBigInt(b);
174
+ if (ai < bi) return -1;
175
+ if (ai > bi) return 1;
176
+ return 0;
177
+ },
178
+ isValidBase60: (text) => {
179
+ for (const ch of text) {
180
+ if (!alphabet.includes(ch)) return false;
181
+ }
182
+ return true;
183
+ }
184
+ };
185
+ }
186
+ var base60 = createBase60Codec();
187
+ // Annotate the CommonJS export names for ESM import in node:
188
+ 0 && (module.exports = {
189
+ base60,
190
+ createBase60Codec
191
+ });
@@ -0,0 +1,22 @@
1
+ type Base60String = string & {
2
+ __brand_base60: true;
3
+ };
4
+ interface Base60Codec {
5
+ alphabet: Base60String;
6
+ encodeBytes(bytes: Uint8Array): string;
7
+ decodeToBytes(text: Base60String): Uint8Array;
8
+ encodeBigInt(value: bigint, padLength?: number): Base60String;
9
+ decodeToBigInt(text: Base60String): bigint;
10
+ encodeInt64(value: number | bigint): Base60String;
11
+ decodeInt64(text: Base60String): bigint;
12
+ encodeUUID(uuid: string): Base60String;
13
+ decodeUUID(text: Base60String): string;
14
+ encodeULID(ulid: string): Base60String;
15
+ decodeULID(text: Base60String): string;
16
+ compareAsBigInt(a: Base60String, b: Base60String): number;
17
+ isValidBase60(text: string): text is Base60String;
18
+ }
19
+ declare function createBase60Codec(): Base60Codec;
20
+ declare const base60: Base60Codec;
21
+
22
+ export { type Base60Codec, type Base60String, base60, createBase60Codec };
@@ -0,0 +1,22 @@
1
+ type Base60String = string & {
2
+ __brand_base60: true;
3
+ };
4
+ interface Base60Codec {
5
+ alphabet: Base60String;
6
+ encodeBytes(bytes: Uint8Array): string;
7
+ decodeToBytes(text: Base60String): Uint8Array;
8
+ encodeBigInt(value: bigint, padLength?: number): Base60String;
9
+ decodeToBigInt(text: Base60String): bigint;
10
+ encodeInt64(value: number | bigint): Base60String;
11
+ decodeInt64(text: Base60String): bigint;
12
+ encodeUUID(uuid: string): Base60String;
13
+ decodeUUID(text: Base60String): string;
14
+ encodeULID(ulid: string): Base60String;
15
+ decodeULID(text: Base60String): string;
16
+ compareAsBigInt(a: Base60String, b: Base60String): number;
17
+ isValidBase60(text: string): text is Base60String;
18
+ }
19
+ declare function createBase60Codec(): Base60Codec;
20
+ declare const base60: Base60Codec;
21
+
22
+ export { type Base60Codec, type Base60String, base60, createBase60Codec };
package/dist/index.js ADDED
@@ -0,0 +1,165 @@
1
+ // src/index.ts
2
+ var FIXED_LEN_UUID = 22;
3
+ var FIXED_LEN_ULID = 22;
4
+ var FIXED_LEN_INT64 = 11;
5
+ var alphabet = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
6
+ var crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
7
+ function createBase60Codec() {
8
+ const base = BigInt(alphabet.length);
9
+ const charToValue = /* @__PURE__ */ new Map();
10
+ for (let i = 0; i < alphabet.length; i++) {
11
+ charToValue.set(alphabet[i], BigInt(i));
12
+ }
13
+ function leftPad(text, length) {
14
+ if (text.length > length) {
15
+ throw new Error(
16
+ `Encoded value length (${text.length}) exceeds fixed length ${length}`
17
+ );
18
+ }
19
+ return text.padStart(length, alphabet[0]);
20
+ }
21
+ function bytesToBigInt(bytes) {
22
+ let result = 0n;
23
+ for (const b of bytes) {
24
+ result = (result << 8n) + BigInt(b);
25
+ }
26
+ return result;
27
+ }
28
+ function bigIntToBytes(value, fixedLength) {
29
+ if (value < 0n) {
30
+ throw new Error("negative bigint is not supported");
31
+ }
32
+ let hex = value.toString(16);
33
+ if (hex.length % 2 === 1) hex = "0" + hex;
34
+ const bytes = new Uint8Array(hex.length / 2);
35
+ for (let i = 0; i < bytes.length; i++) {
36
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
37
+ }
38
+ if (fixedLength !== void 0) {
39
+ if (bytes.length > fixedLength) {
40
+ throw new Error("decoded bytes exceed fixed length");
41
+ }
42
+ const padded = new Uint8Array(fixedLength);
43
+ padded.set(bytes, fixedLength - bytes.length);
44
+ return padded;
45
+ }
46
+ return bytes;
47
+ }
48
+ function encodeBigInt(value, padLength) {
49
+ if (value === 0n) {
50
+ return padLength != null ? leftPad(alphabet[0], padLength) : alphabet[0];
51
+ }
52
+ let v = value;
53
+ let out = "";
54
+ while (v > 0n) {
55
+ const mod = v % base;
56
+ v = v / base;
57
+ out = alphabet[Number(mod)] + out;
58
+ }
59
+ return padLength != null ? leftPad(out, padLength) : out;
60
+ }
61
+ function decodeToBigInt(text) {
62
+ let v = 0n;
63
+ for (const ch of text) {
64
+ const d = charToValue.get(ch);
65
+ if (d === void 0) {
66
+ throw new Error(`invalid character "${ch}"`);
67
+ }
68
+ v = v * base + d;
69
+ }
70
+ return v;
71
+ }
72
+ return {
73
+ alphabet,
74
+ encodeBytes(bytes) {
75
+ return encodeBigInt(bytesToBigInt(bytes));
76
+ },
77
+ decodeToBytes(text) {
78
+ return bigIntToBytes(decodeToBigInt(text));
79
+ },
80
+ encodeBigInt,
81
+ decodeToBigInt,
82
+ encodeInt64(value) {
83
+ return leftPad(
84
+ encodeBigInt(BigInt(value)),
85
+ FIXED_LEN_INT64
86
+ );
87
+ },
88
+ decodeInt64(text) {
89
+ if (text.length !== FIXED_LEN_INT64) {
90
+ throw new Error(`Expected ${FIXED_LEN_INT64} chars for Base60 Int64`);
91
+ }
92
+ return decodeToBigInt(text);
93
+ },
94
+ encodeUUID(uuid) {
95
+ const hex = uuid.replace(/-/g, "");
96
+ if (hex.length !== 32) throw new Error("invalid UUID");
97
+ const bytes = new Uint8Array(16);
98
+ for (let i = 0; i < 16; i++) {
99
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
100
+ }
101
+ return leftPad(
102
+ encodeBigInt(bytesToBigInt(bytes)),
103
+ FIXED_LEN_UUID
104
+ );
105
+ },
106
+ decodeUUID(text) {
107
+ const bytes = bigIntToBytes(decodeToBigInt(text), 16);
108
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
109
+ return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20);
110
+ },
111
+ encodeULID(ulid) {
112
+ if (!/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(ulid)) {
113
+ throw new Error("Invalid ULID format");
114
+ }
115
+ const map32 = /* @__PURE__ */ new Map();
116
+ for (let i = 0; i < crockford.length; i++) {
117
+ map32.set(crockford[i], i);
118
+ }
119
+ let value = 0n;
120
+ for (const ch of ulid.toUpperCase()) {
121
+ const v = map32.get(ch);
122
+ if (v === void 0) throw new Error(`Invalid ULID char: ${ch}`);
123
+ value = (value << 5n) + BigInt(v);
124
+ }
125
+ const raw = this.encodeBigInt(value);
126
+ return leftPad(raw, FIXED_LEN_ULID);
127
+ },
128
+ decodeULID(text) {
129
+ if (text.length !== 22) {
130
+ throw new Error("Expected 22-char Base60 ULID");
131
+ }
132
+ const value = this.decodeToBigInt(text);
133
+ const bytes = bigIntToBytes(value, 16);
134
+ let bits = "";
135
+ for (const b of bytes) {
136
+ bits += b.toString(2).padStart(8, "0");
137
+ }
138
+ const padded = bits.padStart(130, "0");
139
+ let ulid = "";
140
+ for (let i = 0; i < 26; i++) {
141
+ const chunk = padded.slice(i * 5, i * 5 + 5);
142
+ ulid += crockford[parseInt(chunk, 2)];
143
+ }
144
+ return ulid;
145
+ },
146
+ compareAsBigInt(a, b) {
147
+ const ai = decodeToBigInt(a);
148
+ const bi = decodeToBigInt(b);
149
+ if (ai < bi) return -1;
150
+ if (ai > bi) return 1;
151
+ return 0;
152
+ },
153
+ isValidBase60: (text) => {
154
+ for (const ch of text) {
155
+ if (!alphabet.includes(ch)) return false;
156
+ }
157
+ return true;
158
+ }
159
+ };
160
+ }
161
+ var base60 = createBase60Codec();
162
+ export {
163
+ base60,
164
+ createBase60Codec
165
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "base60-codec",
3
+ "version": "0.1.1",
4
+ "description": "A tiny, deterministic Base60 encoder/decoder for UUID, ULID, Int64, and BigInt.",
5
+ "author": "Shinnosuke Hagiwara <hagiwara000@gmail.com>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm,cjs --dts",
23
+ "test": "vitest",
24
+ "clean": "rm -rf dist"
25
+ },
26
+ "devDependencies": {
27
+ "tsup": "^8.0.1",
28
+ "typescript": "^5.7.0",
29
+ "vitest": "^1.5.0"
30
+ }
31
+ }