diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml deleted file mode 100644 index 6021cda..0000000 --- a/.github/workflows/size.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: size -on: [pull_request] -jobs: - size: - runs-on: ubuntu-latest - env: - CI_JOB_NUMBER: 1 - steps: - - uses: actions/checkout@v1 - - uses: andresz1/size-limit-action@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 4277faf..aa8763b 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,50 @@ # `@urbit/aura` -This NPM package is intended to ease the flow of developing FE applications for urbit, by adding parsing and formatting functions for the various urbit auras +Hoon literal syntax parsing and rendering. Approaching two nines of parity with hoon stdlib. -## API +Supports all standard auras (except `@dr` and `@uc`). -```typescript -// @da manipulation -function parseDa(da: string): bigint; -function formatDa(da: bigint): string; -// Given a bigint representing an urbit date, returns a unix timestamp. -function daToUnix(da: bigint): number; -// Given a unix timestamp, returns a bigint representing an urbit date -function unixToDa(unix: number): bigint; +## API overview + +Atoms are native `bigint`s. Top-level library functions reflect the hoon stdlib. Aliases are provided for your comfort. Summary of exports below. -// @p manipulation -// Convert a number to a @p-encoded string. -function patp(arg: string | number | bigint): string; -function hex2patp(hex: string): string; -function patp2hex(name: string): string; -function patp2bn(name: string): bigint; -function patp2dec(name: string): string; -// Determine the ship class of a @p value. -function clan(who: string): string; -// Determine the parent of a @p value. -function sein(name: string): strin; -// Validate a @p string. -function isValidPatp(str: string): boolean; -// Ensure @p is sigged. -function preSig(ship: string): string; -// Remove sig from @p -function deSig(ship: string): string; -// Trim @p to short form -function cite(ship: string): string | null; +### Parsing + +```typescript +const parse = slav; +const tryParse = slaw; +function valid(:aura, :string): boolean; +function slav(:aura, :string): bigint; // throws on failure +function slaw(:aura, :string): bigint | null; +function nuck(:string): coin | null; +``` -// @q manipulation -// Convert a number to a @q-encoded string. -function patq(arg: string | number | bigint): string; -function hex2patq(arg: string): string; -function patq2hex(name: string): string; -function patq2bn(name: string): bigint; -function patq2dec(name: string): string; -// Validate a @q string. -function isValidPatq(str: string): boolean; -// Equality comparison on @q values. -function eqPatq(p: string, q: string): boolean; +### Rendering -// @ud manipulation -function parseUd(ud: string): bigint; -function formatUd(ud: bigint): string; +```typescript +const render = scot; +function scot(:aura, :bigint): string; +function rend(:coin): string; +``` -// @uv manipulation -function parseUv(x: string): bigint; -function formatUv(x: bigint | string): string; +### Utilities -// @uw manipulation -function parseUw(x: string): bigint; -function formatUw(x: bigint | string): string; +We provide some utilities for desirable operations on specific auras. These too generally match their hoon stdlib equivalents. -// @ux manipulation -function parseUx(ux: string): string; -function formatUx(hex: string): string; -``; +```typescript +const da = { + function toUnix(:bigint): number, + function fromUnix(:number): bigint, +}; +const p = { + type rank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn', + type size = 'galaxy' | 'star' | 'planet' | 'moon' | 'comet', + function cite(:bigint | string): string, + function sein(:bigint): bigint, + function sein(:string): string, // throws on bad input + function clan(:bigint | string): rank, // throws on bad input + function kind(:bigint | string): size, // throws on bad input + function rankToSize(:rank): size, + function sizeToRank(:size): rank, +}; ``` diff --git a/package-lock.json b/package-lock.json index a206b04..92c03c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@tsconfig/recommended": "^1.0.2", "dts-cli": "^2.0.0", "husky": "^7.0.4", - "jsverify": "^0.8.4", "tslib": "^2.5.0", "typescript": "^5.0.4" }, @@ -7909,21 +7908,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsverify": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/jsverify/-/jsverify-0.8.4.tgz", - "integrity": "sha512-nUG73Sfi8L4eOkc7pv9sflgAm43v+z6XMuePGVdRoBUxBLJiVcMcf3Xgc4h19eHHF3JwsaagOkUu825UnPBLJw==", - "dev": true, - "dependencies": { - "lazy-seq": "^1.0.0", - "rc4": "~0.1.5", - "trampa": "^1.0.0", - "typify-parser": "^1.1.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -7961,15 +7945,6 @@ "language-subtag-registry": "~0.3.2" } }, - "node_modules/lazy-seq": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazy-seq/-/lazy-seq-1.0.0.tgz", - "integrity": "sha512-AQ4vRcnULa7FX6R6YTAjKQAE1MuEThidVQm0TEtTpedaBpnOwid5k6go16E5NDkafel1xAsZL73WkwdG03IzhA==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9102,15 +9077,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/rc4": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz", - "integrity": "sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10182,12 +10148,6 @@ "node": ">=12" } }, - "node_modules/trampa": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trampa/-/trampa-1.0.1.tgz", - "integrity": "sha512-93WeyHNuRggPEsfCe+yHxCgM2s6H3Q8Wmlt6b6ObJL8qc7eZlRaFjQxwTrB+zbvGtlDRnAkMoYYo3+2uH/fEwA==", - "dev": true - }, "node_modules/ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -10394,15 +10354,6 @@ "node": ">=12.20" } }, - "node_modules/typify-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/typify-parser/-/typify-parser-1.1.0.tgz", - "integrity": "sha512-p5+L1sc6Al3bcStMwiZNxDh4ii4JxL+famEbSIUuOUMVoNn9Nz27AT1jL3x7poMHxqKK0UQIUAp5lGkKbyKkFA==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -16529,18 +16480,6 @@ "universalify": "^2.0.0" } }, - "jsverify": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/jsverify/-/jsverify-0.8.4.tgz", - "integrity": "sha512-nUG73Sfi8L4eOkc7pv9sflgAm43v+z6XMuePGVdRoBUxBLJiVcMcf3Xgc4h19eHHF3JwsaagOkUu825UnPBLJw==", - "dev": true, - "requires": { - "lazy-seq": "^1.0.0", - "rc4": "~0.1.5", - "trampa": "^1.0.0", - "typify-parser": "^1.1.0" - } - }, "jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -16572,12 +16511,6 @@ "language-subtag-registry": "~0.3.2" } }, - "lazy-seq": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazy-seq/-/lazy-seq-1.0.0.tgz", - "integrity": "sha512-AQ4vRcnULa7FX6R6YTAjKQAE1MuEThidVQm0TEtTpedaBpnOwid5k6go16E5NDkafel1xAsZL73WkwdG03IzhA==", - "dev": true - }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -17418,12 +17351,6 @@ "safe-buffer": "^5.1.0" } }, - "rc4": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz", - "integrity": "sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==", - "dev": true - }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -18238,12 +18165,6 @@ "punycode": "^2.1.1" } }, - "trampa": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trampa/-/trampa-1.0.1.tgz", - "integrity": "sha512-93WeyHNuRggPEsfCe+yHxCgM2s6H3Q8Wmlt6b6ObJL8qc7eZlRaFjQxwTrB+zbvGtlDRnAkMoYYo3+2uH/fEwA==", - "dev": true - }, "ts-jest": { "version": "29.1.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", @@ -18371,12 +18292,6 @@ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, - "typify-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/typify-parser/-/typify-parser-1.1.0.tgz", - "integrity": "sha512-p5+L1sc6Al3bcStMwiZNxDh4ii4JxL+famEbSIUuOUMVoNn9Nz27AT1jL3x7poMHxqKK0UQIUAp5lGkKbyKkFA==", - "dev": true - }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 51a7438..6293dd3 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@tsconfig/recommended": "^1.0.2", "dts-cli": "^2.0.0", "husky": "^7.0.4", - "jsverify": "^0.8.4", "tslib": "^2.5.0", "typescript": "^5.0.4" }, diff --git a/src/da.ts b/src/d.ts similarity index 54% rename from src/da.ts rename to src/d.ts index c337aa8..3ebfb26 100644 --- a/src/da.ts +++ b/src/d.ts @@ -1,3 +1,134 @@ +/** + * Given a string formatted as a `@da`, returns a bigint representing the urbit date. + * + * @param {string} x The formatted `@da` + * @return {bigint} The urbit date as bigint + */ +export function parseDa(x: string): bigint { + let pos = true; + let [date, time, ms] = x.split('..'); + time = time || '0.0.0'; + ms = ms || '0000'; + let [yer, month, day] = date.slice(1).split('.'); + if (yer.at(-1) === '-') { + yer = yer.slice(0, -1); + pos = false; + } + const [hour, minute, sec] = time.split('.'); + const millis = ms.split('.').map((m) => BigInt('0x' + m)); + + return year({ + pos: pos, + year: BigInt(yer), + month: BigInt(month), + time: { + day: BigInt(day), + hour: BigInt(hour), + minute: BigInt(minute), + second: BigInt(sec), + ms: millis, + }, + }); +} + +export function parseDr(x: string): bigint { + const rop: Tarp = { day: 0n, hour: 0n, minute: 0n, second: 0n, ms: [] }; + x = x.slice(1); // strip ~ + let [time, ms] = x.split('..'); + ms = ms || '0000'; + rop.ms = ms.split('.').map((m) => BigInt('0x' + m)); + time.split('.').forEach((a) => { + switch (a[0]) { + case 'd': rop.day += BigInt(a.slice(1)); break; + case 'h': rop.hour += BigInt(a.slice(1)); break; + case 'm': rop.minute += BigInt(a.slice(1)); break; + case 's': rop.second += BigInt(a.slice(1)); break; + default: throw new Error('bad dr: ' + x); + } + }); + ms = ms || '0000'; + return yule(rop); +} + +/** + * Given a bigint representing an urbit date, returns a string formatted as a proper `@da`. + * + * @param {bigint} x The urbit date as bigint + * @return {string} The formatted `@da` + */ +export function renderDa(x: bigint): string { + const { pos, year, month, time } = yore(x); + let out = `~${year}${pos ? '' : '-'}.${month}.${time.day}`; + if (time.hour !== 0n || time.minute !== 0n || time.second !== 0n || time.ms.length !== 0) { + out = out + `..${time.hour.toString().padStart(2, '0')}.${time.minute.toString().padStart(2, '0')}.${time.second.toString().padStart(2, '0')}` + if (time.ms.length !== 0) { + out = out + `..${time.ms.map((x) => x.toString(16).padStart(4, '0')).join('.')}`; + } + } + return out; +} + +export function renderDr(x: bigint): string { + if (x === 0n) return '~s0'; + const { day, hour, minute, second, ms } = yell(x); + let out: string[] = []; + if (day !== 0n) out.push('d' + day.toString()); + if (hour !== 0n) out.push('h' + hour.toString()); + if (minute !== 0n) out.push('m' + minute.toString()); + if (second !== 0n) out.push('s' + second.toString()); + if (ms.length !== 0) { + if (out.length === 0) out.push('s0'); + out.push('.' + ms.map((x) => x.toString(16).padStart(4, '0')).join('.')); + } + return '~' + out.join('.'); +} + +/** + * Given a bigint representing an urbit date, returns a unix timestamp. + * + * @param {bigint} da The urbit date + * @return {number} The unix timestamp + */ +export function toUnix(da: bigint): number { + // ported from +time:enjs:format in hoon.hoon + const offset = DA_SECOND / 2000n; + const epochAdjusted = offset + (da - DA_UNIX_EPOCH); + + return Math.round( + Number(epochAdjusted * 1000n / DA_SECOND) + ); +} + +/** + * Given a unix timestamp, returns a bigint representing an urbit date + * + * @param {number} unix The unix timestamp + * @return {bigint} The urbit date + */ +export function fromUnix(unix: number): bigint { + const timeSinceEpoch = BigInt(unix) * DA_SECOND / 1000n; + return DA_UNIX_EPOCH + timeSinceEpoch; +} + +/** + * Given a number of seconds, return a bigint representing its `@dr` + */ +export function fromSeconds(seconds: bigint): bigint { + return yule({ day: 0n, hour: 0n, minute: 0n, second: seconds, ms: [] }); +} + +/** + * Convert a `@dr` to the amount of seconds it represents, dropping sub- + * second precision + */ +export function toSeconds(dr: bigint): bigint { + const { day, hour, minute, second } = yell(dr); + return (((((day * 24n) + hour) * 60n) + minute) * 60n) + second; +} + +// +// internals +// interface Dat { pos: boolean; @@ -33,7 +164,7 @@ const MIT_YO = 60n; const ERA_YO = 146097n; const CET_YO = 36524n; -export function year(det: Dat) { +function year(det: Dat) { const yer = det.pos ? EPOCH + (BigInt(det.year)) : EPOCH - (BigInt(det.year) - 1n); @@ -68,59 +199,40 @@ export function year(det: Dat) { return d; })(); - let sec = BigInt(det.time.second) - + (DAY_YO * day) - + (HOR_YO * BigInt(det.time.hour)) - + (MIT_YO * BigInt(det.time.minute)); + det.time.day = day; + return yule(det.time); +} + +function yule(rip: Tarp): bigint { + let sec = rip.second + + (DAY_YO * rip.day) + + (HOR_YO * rip.hour) + + (MIT_YO * rip.minute); - let ms = det.time.ms; + let ms = rip.ms; let fac = 0n; - let muc = 3; + let muc = 3n; while (ms.length !== 0) { const [first, ...rest] = ms; - fac = fac + (first << BigInt(16 * muc)); + fac = fac + (first << (16n * muc)); ms = rest; - muc -= 1; + muc -= 1n; } return fac | (sec << 64n); } -/** - * Given a string formatted as a @da, returns a bigint representing the urbit date. - * - * @return {string} x The formatted @da - * @return {bigint} x The urbit date as bigint - */ -export function parseDa(x: string): bigint { - const [date, time, ms] = x.split('..'); - const [yer, month, day] = date.slice(1).split('.'); - const [hour, minute, sec] = time.split('.'); - const millis = ms.split('.').map((m) => BigInt('0x'+m)); - - return year({ - pos: true, - year: BigInt(yer), - month: BigInt(month), - time: { - day: BigInt(day), - hour: BigInt(hour), - minute: BigInt(minute), - second: BigInt(sec), - ms: millis, - }, - }); -} - function yell(x: bigint): Tarp { let sec = x >> 64n; const milliMask = BigInt('0xffffffffffffffff'); const millis = milliMask & x; const ms = millis - .toString(16) - .match(/.{1,4}/g)! - .filter((x) => x !== '0000') + .toString(16).padStart(16, '0') + .match(/.{4}/g)! .map((x) => BigInt('0x'+x)); + while (ms.at(-1) === 0n) { + ms.pop(); + } let day = sec / DAY_YO; sec = sec % DAY_YO; let hor = sec / HOR_YO; @@ -192,47 +304,3 @@ function yore(x: bigint): Dat { time, }; } - -/** - * Given a bigint representing an urbit date, returns a string formatted as a proper @da. - * - * @param {bigint} x The urbit date as bigint - * @return {string} The formatted @da - */ -export function formatDa(x: bigint | string) { - if (typeof x === 'string') { - x = BigInt(x); - } - const { year, month, time } = yore(x); - - return `~${year}.${month}.${time.day}..${time.hour}.${time.minute}.${ - time.second - }..${time.ms.map((x) => x.toString(16).padStart(4, '0')).join('.')}`; -} - -/** - * Given a bigint representing an urbit date, returns a unix timestamp. - * - * @param {bigint} da The urbit date - * @return {number} The unix timestamp - */ -export function daToUnix(da: bigint): number { - // ported from +time:enjs:format in hoon.hoon - const offset = DA_SECOND / 2000n; - const epochAdjusted = offset + (da - DA_UNIX_EPOCH); - - return Math.round( - Number(epochAdjusted * 1000n / DA_SECOND) - ); -} - -/** - * Given a unix timestamp, returns a bigint representing an urbit date - * - * @param {number} unix The unix timestamp - * @return {bigint} The urbit date - */ -export function unixToDa(unix: number): bigint { - const timeSinceEpoch = BigInt(unix) * DA_SECOND / 1000n; - return DA_UNIX_EPOCH + timeSinceEpoch; -} diff --git a/src/hoon/index.ts b/src/hoon/index.ts deleted file mode 100644 index 2bb0359..0000000 --- a/src/hoon/index.ts +++ /dev/null @@ -1,92 +0,0 @@ - -export const pre = ` -dozmarbinwansamlitsighidfidlissogdirwacsabwissib\ -rigsoldopmodfoglidhopdardorlorhodfolrintogsilmir\ -holpaslacrovlivdalsatlibtabhanticpidtorbolfosdot\ -losdilforpilramtirwintadbicdifrocwidbisdasmidlop\ -rilnardapmolsanlocnovsitnidtipsicropwitnatpanmin\ -ritpodmottamtolsavposnapnopsomfinfonbanmorworsip\ -ronnorbotwicsocwatdolmagpicdavbidbaltimtasmallig\ -sivtagpadsaldivdactansidfabtarmonranniswolmispal\ -lasdismaprabtobrollatlonnodnavfignomnibpagsopral\ -bilhaddocridmocpacravripfaltodtiltinhapmicfanpat\ -taclabmogsimsonpinlomrictapfirhasbosbatpochactid\ -havsaplindibhosdabbitbarracparloddosbortochilmac\ -tomdigfilfasmithobharmighinradmashalraglagfadtop\ -mophabnilnosmilfopfamdatnoldinhatnacrisfotribhoc\ -nimlarfitwalrapsarnalmoslandondanladdovrivbacpol\ -laptalpitnambonrostonfodponsovnocsorlavmatmipfip\ -`; - -export const suf = ` -zodnecbudwessevpersutletfulpensytdurwepserwylsun\ -rypsyxdyrnuphebpeglupdepdysputlughecryttyvsydnex\ -lunmeplutseppesdelsulpedtemledtulmetwenbynhexfeb\ -pyldulhetmevruttylwydtepbesdexsefwycburderneppur\ -rysrebdennutsubpetrulsynregtydsupsemwynrecmegnet\ -secmulnymtevwebsummutnyxrextebfushepbenmuswyxsym\ -selrucdecwexsyrwetdylmynmesdetbetbeltuxtugmyrpel\ -syptermebsetdutdegtexsurfeltudnuxruxrenwytnubmed\ -lytdusnebrumtynseglyxpunresredfunrevrefmectedrus\ -bexlebduxrynnumpyxrygryxfeptyrtustyclegnemfermer\ -tenlusnussyltecmexpubrymtucfyllepdebbermughuttun\ -bylsudpemdevlurdefbusbeprunmelpexdytbyttyplevmyl\ -wedducfurfexnulluclennerlexrupnedlecrydlydfenwel\ -nydhusrelrudneshesfetdesretdunlernyrsebhulryllud\ -remlysfynwerrycsugnysnyllyndyndemluxfedsedbecmun\ -lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\ -`; - -export const prefixes = pre.match(/.{1,3}/g) as RegExpMatchArray; -export const suffixes = suf.match(/.{1,3}/g) as RegExpMatchArray; - -export const bex = (n: bigint) => 2n ** n; - -export const rsh = (a: bigint, b: bigint, c: bigint) => - c / bex(bex(a) * b); - -export const met = (a: bigint, b: bigint, c = 0n): bigint => - (b === 0n) ? c : met(a, rsh(a, 1n, b), c + 1n); - -export const end = (a: bigint, b: bigint, c: bigint) => - c % bex(bex(a) * b); - -export const patp2syls = (name: string): string[] => - name.replace(/[\^~-]/g, '').match(/.{1,3}/g) || []; - -/** - * Weakly check if a string is a valid @p or @q value. - * - * This is, at present, a pretty weak sanity check. It doesn't confirm the - * structure precisely (e.g. dashes), and for @q, it's required that q values - * of (greater than one) odd bytelength have been zero-padded. So, for - * example, '~doznec-binwod' will be considered a valid @q, but '~nec-binwod' - * will not. - * - * @param {String} name a @p or @q value - * @return {boolean} - */ -export function isValidPat(name: string): boolean { - if (typeof name !== 'string') { - throw new Error('isValidPat: non-string input'); - } - - const leadingTilde = name.slice(0, 1) === '~'; - - if (leadingTilde === false || name.length < 4) { - return false; - } else { - const syls = patp2syls(name); - const wrongLength = syls.length % 2 !== 0 && syls.length !== 1; - const sylsExist = syls.reduce( - (acc, syl, index) => - acc && - (index % 2 !== 0 || syls.length === 1 - ? suffixes.includes(syl) - : prefixes.includes(syl)), - true - ); - - return !wrongLength && sylsExist; - } -} diff --git a/src/index.ts b/src/index.ts index 2759f22..97058fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,19 @@ -export * from './da'; -export * from './p'; -export * from './q'; -export * from './ud'; -export * from './uv'; -export * from './uw'; -export * from './ux'; +// main + +export * from './types'; +export { parse, tryParse, valid, slav, slaw, nuck } from './parse'; +export { render, scot, rend } from './render'; //TODO expose encodeString() ? + +// atom utils + +import { toUnix, fromUnix, fromSeconds, toSeconds } from './d'; +export const da = { toUnix, fromUnix }; +export const dr = { toSeconds, fromSeconds }; + +import type * as pt from './p'; +import { cite, sein, clan, kind, rankToSize, sizeToRank } from './p'; +export const p = { cite, sein, clan, kind, rankToSize, sizeToRank }; +export namespace p { + export type rank = pt.rank; + export type size = pt.size; +} diff --git a/src/p.ts b/src/p.ts index 93934cb..29818e3 100644 --- a/src/p.ts +++ b/src/p.ts @@ -1,40 +1,26 @@ -import { - isValidPat, - patp2syls, - suffixes, - prefixes, - met, - end, - rsh, -} from './hoon'; import ob from './hoon/ob'; -/** - * Convert a hex-encoded string to a @p-encoded string. - * - * @param {String} hex - * @return {String} - */ -export function hex2patp(hex: string): string { - if (hex === null) { - throw new Error('hex2patp: null input'); - } - return patp(BigInt('0x'+hex)); -} +export type rank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn'; +export type size = 'galaxy' | 'star' | 'planet' | 'moon' | 'comet'; + +// +// main parsing & rendering +// + +//NOTE matches for shape, not syllables +export const regexP = /^~([a-z]{3}|([a-z]{6}(\-[a-z]{6}){0,3}(\-(\-[a-z]{6}){4})*))$/; /** - * Convert a @p-encoded string to a hex-encoded string. - * - * @param {String} name @p - * @return {String} + * Convert a valid `@p` literal string to a bigint. + * Throws on malformed input. + * @param {String} str certified-sane `@p` literal string */ -export function patp2hex(name: string): string { - if (isValidPat(name) === false) { - throw new Error('patp2hex: not a valid @p'); - } - const syls = patp2syls(name); +export function parseP(str: string): bigint { + const syls = patp2syls(str); - const syl2bin = (idx: number) => idx.toString(2).padStart(8, '0'); + const syl2bin = (idx: number) => { + return idx.toString(2).padStart(8, '0'); //NOTE base16 isn't any faster + } const addr = syls.reduce( (acc, syl, idx) => @@ -44,194 +30,220 @@ export function patp2hex(name: string): string { '' ); - const bn = BigInt('0b'+addr); - const hex = ob.fynd(bn).toString(16); - return hex.length % 2 !== 0 ? hex.padStart(hex.length + 1, '0') : hex; -} - -/** - * Convert a @p-encoded string to a bignum. - * - * @param {String} name @p - * @return {bigint} - */ -export function patp2bn(name: string): bigint { - return BigInt('0x'+patp2hex(name)); + const num = BigInt('0b' + addr); + return ob.fynd(num); } /** - * Convert a @p-encoded string to a decimal-encoded string. - * - * @param {String} name @p - * @return {String} + * Convert a valid `@p` literal string to a bigint. + * Returns null on malformed input. + * @param {String} str `@p` literal string */ -export function patp2dec(name: string): string { - let bn: bigint; - try { - bn = patp2bn(name); - } catch (_) { - throw new Error('patp2dec: not a valid @p'); - } - return bn.toString(); +export function parseValidP(str: string): bigint | null { + if (!regexP.test(str) || !validSyllables(str)) return null; + const res = parseP(str); + return (str === renderP(res)) ? res : null; } /** - * Determine the ship class of a @p value. - * - * @param {String} @p - * @return {String} + * Convert a number to a @p-encoded string. + * @param {bigint} num */ -export function clan(who: string): string { - let name: bigint; - try { - name = patp2bn(who); - } catch (_) { - throw new Error('clan: not a valid @p'); - } +export function renderP(num: bigint): string { + const sxz = ob.fein(num); + const dyx = Math.ceil(sxz.toString(16).length / 2); + const dyy = Math.ceil(sxz.toString(16).length / 4); - const wid = met(3n, name); - return wid <= 1n - ? 'galaxy' - : wid === 2n - ? 'star' - : wid <= 4n - ? 'planet' - : wid <= 8n - ? 'moon' - : 'comet'; -} + function loop(tsxz: bigint, timp: number, trep: string): string { + const log = tsxz & 0xFFFFn; + const pre = prefixes[Number(log >> 8n)]; + const suf = suffixes[Number(log & 0xFFn)]; + const etc = (timp & 0b11) ? '-' : ((timp === 0) ? '' : '--'); -/** - * Determine the parent of a @p value. - * - * @param {String} @p - * @return {String} - */ -export function sein(name: string): string { - let who: bigint; - try { - who = patp2bn(name); - } catch (_) { - throw new Error('sein: not a valid @p'); - } + const res = pre + suf + etc + trep; - let mir: string; - try { - mir = clan(name); - } catch (_) { - throw new Error('sein: not a valid @p'); + return timp === dyy ? trep : loop(tsxz >> 16n, timp + 1, res); } - const res = - mir === 'galaxy' - ? who - : mir === 'star' - ? end(3n, 1n, who) - : mir === 'planet' - ? end(4n, 1n, who) - : mir === 'moon' - ? end(5n, 1n, who) - : 0n; - return patp(res); + return ( + '~' + (dyx <= 1 ? suffixes[Number(sxz)] : loop(sxz, 0, '')) + ); } +// +// utilities +// + /** * Validate a @p string. * * @param {String} str a string * @return {boolean} */ -export function isValidPatp(str: string): boolean { - return isValidPat(str) && str === patp(patp2dec(str)); +export function isValidP(str: string): boolean { + return regexP.test(str) // general structure + && validSyllables(str) // valid syllables + && str === renderP(parseP(str)); // no leading zeroes } /** - * Convert a number to a @p-encoded string. - * - * @param {String, Number, bigint} arg - * @return {String} + * Determine the `$rank` of a `@p` value or literal. + * Throws on malformed input string. + * @param {String} who `@p` value or literal string */ -export function patp(arg: string | number | bigint) { - if (arg === null) { - throw new Error('patp: null input'); - } - const n = BigInt(arg); - - const sxz = ob.fein(n); - const dyy = met(4n, sxz); - - function loop(tsxz: bigint, timp: bigint, trep: string): string { - const log = end(4n, 1n, tsxz); - const pre = prefixes[Number(rsh(3n, 1n, log))]; - const suf = suffixes[Number(end(3n, 1n, log))]; - const etc = (timp % 4n) === 0n ? ((timp === 0n) ? '' : '--') : '-'; - - const res = pre + suf + etc + trep; - - return timp === dyy ? trep : loop(BigInt(rsh(4n, 1n, tsxz).toString()), timp + 1n, res); - } - - const dyx = BigInt(met(3n, sxz).toString()); - - return ( - '~' + (dyx <= 1n ? suffixes[Number(sxz)] : loop(BigInt(sxz.toString()), 0n, '')) - ); +export function clan(who: bigint | string): rank { + let num: bigint; + if (typeof who === 'bigint') num = who; + else num = checkedParseP(who); + + return num <= 0xFFn + ? 'czar' + : num <= 0xFFFFn + ? 'king' + : num <= 0xFFFFFFFFn + ? 'duke' + : num <= 0xFFFFFFFFFFFFFFFFn + ? 'earl' + : 'pawn'; } /** - * Ensure @p is sigged. - * - * @param {String} str a string - * @return {String} + * Determine the "size" of a `@p` value or literal. + * Throws on malformed input string. + * @param {String} who `@p` value or literal string */ -export function preSig(ship: string): string { - if (!ship) { - return ''; - } +export function kind(who: bigint | string): size { + return rankToSize(clan(who)); +} - if (ship.trim().startsWith('~')) { - return ship.trim(); +export function rankToSize(rank: rank): size { + switch (rank) { + case 'czar': return 'galaxy'; + case 'king': return 'star'; + case 'duke': return 'planet'; + case 'earl': return 'moon'; + case 'pawn': return 'comet'; + } +} +export function sizeToRank(size: size): rank { + switch (size) { + case 'galaxy': return 'czar'; + case 'star': return 'king'; + case 'planet': return 'duke'; + case 'moon': return 'earl'; + case 'comet': return 'pawn'; } - - return '~'.concat(ship.trim()); } /** - * Remove sig from @p - * - * @param {String} str a string - * @return {String} + * Determine the parent of a `@p` value. + * Throws on malformed input string. + * @param {String | number} who `@p` value or literal string */ -export function deSig(ship: string): string { - if (!ship) { - return ''; - } +export function sein(who: bigint): bigint; +export function sein(who: string): string; +export function sein(who: bigint | string): typeof who { + let num: bigint; + if (typeof who === 'bigint') num = who; + else num = checkedParseP(who); - return ship.replace('~', ''); + let mir = clan(num); + + const res = + mir === 'czar' + ? num + : mir === 'king' + ? num & 0xFFn + : mir === 'duke' + ? num & 0xFFFFn + : mir === 'earl' + ? num & 0xFFFFFFFFn + : num & 0xFFFFn; + + if (typeof who === 'bigint') return res; + else return renderP(res); } /** - * Trim @p to short form - * - * @param {String} str a string - * @return {String} + * Render short-form ship name. + * Throws on malformed input string. + * @param {String | number} who `@p` value or literal string */ -export function cite(ship: string): string | null { - if (!ship) { - return null; +export function cite(who: bigint | string): string { + let num: bigint; + if (typeof who === 'bigint') num = who; + else num = checkedParseP(who); + + if (num <= 0xFFFFFFFFn) { + return renderP(num); + } else if (num <= 0xFFFFFFFFFFFFFFFFn) { + return renderP(num & 0xFFFFFFFFn).replace('-', '^'); + } else { + return renderP(BigInt('0x'+num.toString(16).slice(0,4))) + '_' + renderP(num & 0xFFFFn).slice(1); } +} - const patp = deSig(ship); +// +// internals +// - // comet - if (patp.length === 56) { - return preSig(patp.slice(0, 6) + '_' + patp.slice(50, 56)); - } +function checkedParseP(str: string): bigint { + if (!isValidP(str)) throw new Error('invalid @p literal: ' + str); + return parseP(str); +} - // moon - if (patp.length === 27) { - return preSig(patp.slice(14, 20) + '^' + patp.slice(21, 27)); - } +const pre = ` +dozmarbinwansamlitsighidfidlissogdirwacsabwissib\ +rigsoldopmodfoglidhopdardorlorhodfolrintogsilmir\ +holpaslacrovlivdalsatlibtabhanticpidtorbolfosdot\ +losdilforpilramtirwintadbicdifrocwidbisdasmidlop\ +rilnardapmolsanlocnovsitnidtipsicropwitnatpanmin\ +ritpodmottamtolsavposnapnopsomfinfonbanmorworsip\ +ronnorbotwicsocwatdolmagpicdavbidbaltimtasmallig\ +sivtagpadsaldivdactansidfabtarmonranniswolmispal\ +lasdismaprabtobrollatlonnodnavfignomnibpagsopral\ +bilhaddocridmocpacravripfaltodtiltinhapmicfanpat\ +taclabmogsimsonpinlomrictapfirhasbosbatpochactid\ +havsaplindibhosdabbitbarracparloddosbortochilmac\ +tomdigfilfasmithobharmighinradmashalraglagfadtop\ +mophabnilnosmilfopfamdatnoldinhatnacrisfotribhoc\ +nimlarfitwalrapsarnalmoslandondanladdovrivbacpol\ +laptalpitnambonrostonfodponsovnocsorlavmatmipfip\ +`; + +const suf = ` +zodnecbudwessevpersutletfulpensytdurwepserwylsun\ +rypsyxdyrnuphebpeglupdepdysputlughecryttyvsydnex\ +lunmeplutseppesdelsulpedtemledtulmetwenbynhexfeb\ +pyldulhetmevruttylwydtepbesdexsefwycburderneppur\ +rysrebdennutsubpetrulsynregtydsupsemwynrecmegnet\ +secmulnymtevwebsummutnyxrextebfushepbenmuswyxsym\ +selrucdecwexsyrwetdylmynmesdetbetbeltuxtugmyrpel\ +syptermebsetdutdegtexsurfeltudnuxruxrenwytnubmed\ +lytdusnebrumtynseglyxpunresredfunrevrefmectedrus\ +bexlebduxrynnumpyxrygryxfeptyrtustyclegnemfermer\ +tenlusnussyltecmexpubrymtucfyllepdebbermughuttun\ +bylsudpemdevlurdefbusbeprunmelpexdytbyttyplevmyl\ +wedducfurfexnulluclennerlexrupnedlecrydlydfenwel\ +nydhusrelrudneshesfetdesretdunlernyrsebhulryllud\ +remlysfynwerrycsugnysnyllyndyndemluxfedsedbecmun\ +lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\ +`; + +export const prefixes = pre.match(/.{1,3}/g) as RegExpMatchArray; +export const suffixes = suf.match(/.{1,3}/g) as RegExpMatchArray; + +function patp2syls(name: string): string[] { + return name.replace(/[\^~-]/g, '').match(/.{1,3}/g) || []; +} - return preSig(patp); +// check if string contains valid syllables +function validSyllables(name: string): boolean { + const syls = patp2syls(name); + return !(syls.length % 2 !== 0 && syls.length !== 1) // wrong length + && syls.every((syl, index) => // invalid syllables + index % 2 !== 0 || syls.length === 1 + ? suffixes.includes(syl) + : prefixes.includes(syl) + ); } diff --git a/src/parse.ts b/src/parse.ts new file mode 100644 index 0000000..b548088 --- /dev/null +++ b/src/parse.ts @@ -0,0 +1,366 @@ +// parse: deserialize from atom literal strings +// +// atom literal parsing from hoon 137 (and earlier). +// stdlib arm names are included for ease of cross-referencing. +// +//TODO unsupported auras: @dr, @uc + +import { aura, dime, coin } from './types'; + +import { parseDa, parseDr } from './d'; +import { parseValidP, regexP } from './p'; +import { parseValidQ } from './q'; +import { parseR, precision } from './r'; + +function integerRegex(a: string, b: string, c: string, d: number, e: boolean = false): RegExp { + const pre = d === 0 ? b : `${b}${c}{0,${d-1}}`; + const aft = d === 0 ? `${c}*` : `(\\.${c}{${d}})*`; + return new RegExp(`^${e ? '\\-\\-?' : ''}${a}(0|${pre}${aft})$`); +} + +function floatRegex(a: number): RegExp { + return new RegExp(`^\\.~{${a}}(nan|\\-?(inf|(0|[1-9][0-9]*)(\\.[0-9]+)?(e\\-?(0|[1-9][0-9]*))?))$`); +} + +//TODO rewrite with eye towards capturing groups? +export const regex: { [key in aura]: RegExp } = { + 'c': /^~\-((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, + 'da': /^~(0|[1-9][0-9]*)\-?\.0*([1-9]|1[0-2])\.0*[1-9][0-9]*(\.\.([0-9]+)\.([0-9]+)\.([0-9]+)(\.(\.[0-9a-f]{4})+)?)?$/, + 'dr': /^~((d|h|m|s)(0|[1-9][0-9]*))(\.(d|h|m|s)(0|[1-9][0-9]*))*(\.(\.[0-9a-f]{4})+)?$/, //TODO first ? to * mb + 'f': /^\.(y|n)$/, + 'if': /^(\.(0|[1-9][0-9]{0,2})){4}$/, + 'is': /^(\.(0|[1-9a-fA-F][0-9a-fA-F]{0,3})){8}$/, + 'n': /^~$/, + 'p': regexP, //NOTE matches shape but not syllables + 'q': /^\.~(([a-z]{3}|[a-z]{6})(\-[a-z]{6})*)$/, //NOTE matches shape but not syllables + 'rd': floatRegex(1), + 'rh': floatRegex(2), + 'rq': floatRegex(3), + 'rs': floatRegex(0), + 'sb': integerRegex('0b', '1', '[01]', 4, true), + 'sd': integerRegex('', '[1-9]', '[0-9]', 3, true), + 'si': integerRegex('0i', '[1-9]', '[0-9]', 0, true), + 'sv': integerRegex('0v', '[1-9a-v]', '[0-9a-v]', 5, true), + 'sw': integerRegex('0w', '[1-9a-zA-Z~-]', '[0-9a-zA-Z~-]', 5, true), + 'sx': integerRegex('0x', '[1-9a-f]', '[0-9a-f]', 4, true), + 't': /^~~((~[0-9a-fA-F]+\.)|(~[~\.])|[0-9a-z\-\._])*$/, + 'ta': /^~\.[0-9a-z\-\.~_]*$/, + 'tas': /^[a-z][a-z0-9\-]*$/, + 'ub': integerRegex('0b', '1', '[01]', 4), + 'ud': integerRegex('', '[1-9]', '[0-9]', 3), + 'ui': integerRegex('0i', '[1-9]', '[0-9]', 0), + 'uv': integerRegex('0v', '[1-9a-v]', '[0-9a-v]', 5), + 'uw': integerRegex('0w', '[1-9a-zA-Z~-]', '[0-9a-zA-Z~-]', 5), + 'ux': integerRegex('0x', '[1-9a-f]', '[0-9a-f]', 4), +}; + +// parse(): slav() +// slav(): slaw() but throwing on failure +// +export const parse = slav; +export default parse; +export function slav(aura: aura, str: string): bigint { + const out = slaw(aura, str); + if (!out) { + throw new Error('slav: failed to parse @' + aura + ' from string: ' + str); + } + return out; +} + +// tryParse(): slaw() +// slaw(): parse string as specific aura, null if that fails +// +export const tryParse = slaw; +export function slaw(aura: aura, str: string): bigint | null { + // if the aura has a regex, test with that first + //TODO does double work with checks in nuck? + // + if (aura in regex && !regex[aura as aura].test(str)) { + return null; + } + // proceed into parsing the string into a coin, + // producing a result if the aura matches + // + //TODO further short-circuit based on aura? + const coin = nuck(str); + if (coin && coin.type === 'dime' && coin.aura === aura) { + return coin.atom; + } else { + return null; + } +} + +export function valid(aura: aura, str: string): boolean { + return slaw(aura, str) !== null; +} + +// nuck(): parse string into coin, or null if that fails +// +export function nuck(str: string): coin | null { + if (str === '') return null; + + // narrow options down by the first character, before doing regex tests + // and trying to parse for real + // + const c = str[0]; + if (c >= 'a' && c <= 'z') { // "sym" + if (regex['tas'].test(str)) { + return { type: 'dime', aura: 'tas', atom: stringToCord(str) }; + } else { + return null; + } + } else + if (c >= '0' && c <= '9') { // "bisk" + const dim = bisk(str); + if (!dim) { + return null; + } else { + return { type: 'dime', ...dim }; + } + } else + if (c === '-') { // "tash" + let pos = true; + if (str[1] == '-') { + str = str.slice(2); + } else { + str = str.slice(1); + pos = false; + } + const dim = bisk(str); + if (dim) { + // `@s`?:(pos (mul 2 b) ?:(=(0 b) 0 +((mul 2 (dec b))))) + if (pos) { + dim.atom = 2n * dim.atom; + } else if (dim.atom !== 0n) { + dim.atom = 1n + (2n * (dim.atom - 1n)); + } + //NOTE assumes bisk always returns u* auras + return { type: 'dime', aura: dim.aura.replace('u', 's') as aura, atom: dim.atom }; + } else { + return null; + } + } else + if (c === '.') { // "perd", "zust" + //NOTE doesn't match stdlib parsing order, but they're easy early-outs + if (str === '.y') { + return { type: 'dime', aura: 'f', atom: 0n }; + } else + if (str === '.n') { + return { type: 'dime', aura: 'f', atom: 1n }; + } else + //REVIEW entering the branch this way assumes regexes for sequentially-tested auras don't overlap... + // going down the list of options this way matches hoon parser behavior the closest, but is slow for the "miss" case. + // could be optimized by hard-returning if the regex fails for cases where the lead char is unique. + // should probably run some perf tests + if (regex['is'].test(str)) { + const value = str.slice(1).split('.').reduce((a, v) => a + v.padStart(4, '0'), ''); + return { type: 'dime', aura: 'is', atom: BigInt('0x'+value) }; + } else + if (regex['if'].test(str)) { + const value = str.slice(1).split('.').reduce((a, v, i) => (a + (BigInt(v) << BigInt(8 * (3 - i)))), 0n); + return { type: 'dime', aura: 'if', atom: value }; + } else + if ( ( str[1] === '~' && + (regex['rd'].test(str) || regex['rh'].test(str) || regex['rq'].test(str)) ) + || regex['rs'].test(str) ) { // "royl" + let precision = 0; + while (str[precision+1] === '~') precision++; + let aura: aura; + switch (precision) { + case 0: aura = 'rs'; break; + case 1: aura = 'rd'; break; + case 2: aura = 'rh'; break; + case 3: aura = 'rq'; break; + default: throw new Error('parsing invalid @r*'); + } + return { type: 'dime', aura, atom: parseR(aura[1] as precision, str) }; + } else + if (str[1] === '~' && regex['q'].test(str)) { + const num = parseValidQ(str); + if (num === null) return null; + return { type: 'dime', aura: 'q', atom: num }; + } else + if (str[1] === '_' && /^\.(_([0-9a-zA-Z\-\.]|~\-|~~)+)*__$/.test(str)) { // "nusk" + const coins = str.slice(1, -2).split('_').slice(1).map((s): coin | null => { + //NOTE real +wick produces null for strings w/ other ~ chars, + // but the regex above already excludes those + s = s.replaceAll('~-', '_').replaceAll('~~', '~'); // "wick" + return nuck(s); + }); + if (coins.some(c => c === null)) { + return null; + } else { + return { type: 'many', list: coins as coin[] }; + } + } + return null; + } else + if (c === '~') { + if (str === '~') { + return { type: 'dime', aura: 'n', atom: 0n } + } else { // "twid" + if (regex['da'].test(str)) { + return { type: 'dime', aura: 'da', atom: parseDa(str) }; + } else + if (regex['dr'].test(str)) { + return { type: 'dime', aura: 'dr', atom: parseDr(str) }; + } else + if (regex['p'].test(str)) { + //NOTE this still does the regex check twice... + const res = parseValidP(str); + if (res === null) return null; + return { type: 'dime', aura: 'p', atom: res }; + } else + //TODO test if these single-character checks affect performance or no, + // or if we want to move them further up, etc. + if (str[1] === '.' && regex['ta'].test(str)) { + return { type: 'dime', aura: 'ta', atom: stringToCord(str.slice(2)) }; + } else + if (str[1] === '~' && regex['t'].test(str)) { + return { type: 'dime', aura: 't', atom: stringToCord(decodeString(str.slice(2))) }; + } else + if (str[1] === '-' && regex['c'].test(str)) { + //TODO cheeky! this doesn't support the full range of inputs that the + // hoon stdlib supports, but should work for all sane inputs. + // no guarantees about behavior for insane inputs... + if (/^~\-~[0-9a-f]+\.$/.test(str)) { + return { type: 'dime', aura: 'c', atom: BigInt('0x' + str.slice(3, -1)) }; + } + return { type: 'dime', aura: 'c', atom: stringToCord(decodeString(str.slice(2))) }; + } + } + if ((str[1] === '0') && /^~0[0-9a-v]+$/.test(str)) { + return { type: 'blob', jam: slurp(5, UV_ALPHABET, str.slice(2)) }; + } + return null; + } + return null; +} + +// bisk(): parse string into dime of integer aura, or null if that fails +// +function bisk(str: string): dime | null { + switch (str.slice(0, 2)) { + case '0b': // "bay" + if (regex['ub'].test(str)) { + return { aura: 'ub', atom: BigInt(str.replaceAll('.', '')) }; + } else { + return null; + } + + case '0c': // "fim" + //TODO support base58check + console.log('aura-js: @uc parsing unsupported (bisk)'); + return null; + + case '0i': // "dim" + if (regex['ui'].test(str)) { + return { aura: 'ui', atom: BigInt(str.slice(2)) } + } else { + return null; + } + + case '0x': // "hex" + if (regex['ux'].test(str)) { + return { aura: 'ux', atom: BigInt(str.replaceAll('.', '')) }; + } else { + return null; + } + + case '0v': // "viz" + if (regex['uv'].test(str)) { + return { aura: 'uv', atom: slurp(5, UV_ALPHABET, str.slice(2)) }; + } else { + return null; + } + + case '0w': // "wiz" + if (regex['uw'].test(str)) { + return { aura: 'uw', atom: slurp(6, UW_ALPHABET, str.slice(2)) }; + } else { + return null; + } + + default: // "dem" + if (regex['ud'].test(str)) { + return { aura: 'ud', atom: BigInt(str.replaceAll('.', '')) } + } else { + return null; + } + } +} + +// decodeString(): decode string from @ta-safe format +// +// using logic from +woad. +// for example, '~.some.~43.hars~21.' becomes 'some Chars!' +// assumes +// +export function decodeString(str: string): string { + let out = ''; + let i = 0; + while (i < str.length) { + switch (str[i]) { + case '.': + out = out + ' '; + i++; continue; + case '~': + switch (str[++i]) { + case '~': + out = out + '~'; + i++; continue; + case '.': + out = out + '.'; + i++; continue; + default: + let char: number = 0; + do { + char = (char << 4) | Number.parseInt(str[i++], 16); + } while (str[i] !== '.') + out = out + String.fromCodePoint(char); + i++; continue; + } + default: + out = out + str[i++]; + continue; + } + } + return out; +} + +function stringToCord(str: string): bigint { + return bytesToBigint(new TextEncoder().encode(str)); +} + +const UW_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; +const UV_ALPHABET = '0123456789abcdefghijklmnopqrstuv'; +function slurp(bits: number, alphabet: string, str: string): bigint { + let out = 0n; + const bbits = BigInt(bits); + while (str !== '') { + if (str[0] !== '.') { + out = (out << bbits) + BigInt(alphabet.indexOf(str[0])); + } + str = str.slice(1); + } + return out; +} + +//REVIEW should the reversal happen here or at callsites? depends on what endianness is idiomatic to js? +function bytesToBigint(bytes: Uint8Array): bigint { + if (bytes.length === 0) return 0n; + // if we have node's Buffer available, use it, it's wicked fast. + // otherwise, constructing the hex string "by hand" and instantiating + // a bigint from that is the fastest thing we can do. + // + if (typeof Buffer !== 'undefined') + return BigInt('0x' + Buffer.from(bytes.reverse()).toString('hex')); + let byt: number, + parts: string[] = []; + for (var i = bytes.length - 1; i >= 0; --i) { + byt = bytes[i]; + parts.push(byt < 16 ? "0" + byt.toString(16) : byt.toString(16)); + } + const num = BigInt('0x' + parts.join('')); + return num; +} diff --git a/src/q.ts b/src/q.ts index 53d2d0c..b4d8324 100644 --- a/src/q.ts +++ b/src/q.ts @@ -1,31 +1,18 @@ -import { isValidPat, prefixes, suffixes } from './hoon'; -import { chunk, splitAt } from './utils'; +import { prefixes, suffixes } from './p'; //TODO investigate whether native UintArrays are more portable // than node Buffers /** - * Convert a number to a @q-encoded string. - * - * @param {String, Number, bigint} arg + * Convert a number to a `@q`-encoded string. + * @param {bigint} num * @return {String} */ -export function patq(arg: string | number | bigint) { - const bn = BigInt(arg); +export function renderQ(num: bigint): string { //NOTE stupid hack to work around bad node Buffer spec - const hex = bn.toString(16); + const hex = num.toString(16); const lex = hex.length; const buf = Buffer.from(hex.padStart(lex+lex%2, '0'), 'hex'); - return buf2patq(buf); -} - -/** - * Convert a Buffer into a @q-encoded string. - * - * @param {Buffer} buf - * @return {String} - */ -function buf2patq(buf: Buffer): string { const chunked = buf.length % 2 !== 0 && buf.length > 1 ? [[buf[0]]].concat(chunk(Array.from(buf.slice(1)), 2)) @@ -33,8 +20,8 @@ function buf2patq(buf: Buffer): string { const prefixName = (byts: number[]) => byts[1] === undefined - ? prefixes[0] + suffixes[byts[0]] - : prefixes[byts[0]] + suffixes[byts[1]]; + ? suffixes[byts[0]] + : prefixes[byts[0]] + suffixes[byts[1]]; //TODO this branch unused const name = (byts: number[]) => byts[1] === undefined @@ -45,121 +32,78 @@ function buf2patq(buf: Buffer): string { pair.length % 2 !== 0 && chunked.length > 1 ? prefixName(pair) : name(pair); return chunked.reduce( - (acc, elem) => acc + (acc === '~' ? '' : '-') + alg(elem), - '~' + (acc, elem) => acc + (acc === '.~' ? '' : '-') + alg(elem), + '.~' ); } /** - * Convert a hex-encoded string to a @q-encoded string. - * - * Note that this preserves leading zero bytes. - * - * @param {String} hex + * Convert a `@q`-encoded string to a bigint. + * Throws on malformed input. + * @param {String} str `@q` string with leading .~ * @return {String} */ -export function hex2patq(arg: string): string { - const hex = arg.length % 2 !== 0 ? arg.padStart(arg.length + 1, '0') : arg; - - const buf = Buffer.from(hex, 'hex'); - return buf2patq(buf); -} - -/** - * Convert a @q-encoded string to a hex-encoded string. - * - * Note that this preserves leading zero bytes. - * - * @param {String} name @q - * @return {String} - */ -export function patq2hex(name: string): string { - if (isValidPat(name) === false) { - throw new Error('patq2hex: not a valid @q'); +export function parseQ(str: string): bigint { + const chunks = str.slice(2).split('-'); + const dec2hex = (dec: number) => { + if (dec < 0) throw new Error('malformed @q'); + return dec.toString(16).padStart(2, '0'); } - const chunks = name.slice(1).split('-'); - const dec2hex = (dec: number) => dec.toString(16).padStart(2, '0'); - const splat = chunks.map((chunk) => { + const splat = chunks.map((chunk, i) => { let syls = splitAt(3, chunk); - return syls[1] === '' + return (syls[1] === '' && i === 0) // singles only at the start ? dec2hex(suffixes.indexOf(syls[0])) : dec2hex(prefixes.indexOf(syls[0])) + dec2hex(suffixes.indexOf(syls[1])); }); - return name.length === 0 ? '00' : splat.join(''); + return BigInt('0x' + (str.length === 0 ? '00' : splat.join(''))); } -/** - * Convert a @q-encoded string to a bignum. - * - * @param {String} name @q - * @return {bigint} - */ -export const patq2bn = (name: string): bigint => BigInt('0x'+patq2hex(name)); - -/** - * Convert a @q-encoded string to a decimal-encoded string. - * - * @param {String} name @q - * @return {String} - */ -export function patq2dec(name: string): string { - let bn: bigint; +export function parseValidQ(str: string): bigint | null { try { - bn = patq2bn(name); - } catch (_) { - throw new Error('patq2dec: not a valid @q'); + const num = parseQ(str); + return num; + } catch (e) { + return null; } - return bn.toString(); } /** - * Validate a @q string. - * - * @param {String} str a string + * Validate a `@q` string. + * @param {String} str a string * @return {boolean} */ -export const isValidPatq = (str: string): boolean => - isValidPat(str) && eqPatq(str, patq(patq2dec(str))); - -/** - * Remove all leading zero bytes from a sliceable value. - * @param {String} - * @return {String} - */ -const removeLeadingZeroBytes = (str: string): string => - str.slice(0, 2) === '00' ? removeLeadingZeroBytes(str.slice(2)) : str; - -/** - * Equality comparison, modulo leading zero bytes. - * @param {String} - * @param {String} - * @return {Bool} - */ -const eqModLeadingZeroBytes = (s: string, t: string): boolean => - removeLeadingZeroBytes(s) === removeLeadingZeroBytes(t); - -/** - * Equality comparison on @q values. - * @param {String} p a @q-encoded string - * @param {String} q a @q-encoded string - * @return {Bool} - */ -export function eqPatq(p: string, q: string): boolean { - let phex; +export function isValidQ(str: string): boolean { + if (str === '') return false; try { - phex = patq2hex(p); - } catch (_) { - throw new Error('eqPatq: not a valid @q'); + parseQ(str); + return true; + } catch (e) { + return false; } - - let qhex; - try { - qhex = patq2hex(q); - } catch (_) { - throw new Error('eqPatq: not a valid @q'); +}; + +// +// internals +// + +function chunk(arr: T[], size: number): T[][] { + let chunk: T[] = []; + let newArray = [chunk]; + + for (let i = 0; i < arr.length; i++) { + if (chunk.length < size) { + chunk.push(arr[i]); + } else { + chunk = [arr[i]]; + newArray.push(chunk); + } } - return eqModLeadingZeroBytes(phex, qhex); + return newArray; +} + +function splitAt(index: number, str: string) { + return [str.slice(0, index), str.slice(index)]; } diff --git a/src/r.ts b/src/r.ts new file mode 100644 index 0000000..6db7933 --- /dev/null +++ b/src/r.ts @@ -0,0 +1,530 @@ +export type precision = 'h' | 's' | 'd' | 'q' | precisionBits; +type precisionBits = { w: number, p: number, l: string }; + +// str: @r* format string including its leading . and ~s +export function parseR(per: precision, str: string): bigint { + per = getPrecision(per); + return parse(str.slice(per.l.length), per.w, per.p); +} + +export function renderR(per: precision, r: bigint): string { + per = getPrecision(per); + return per.l + rCo(deconstruct(r, BigInt(per.w), BigInt(per.p))); +} + +// +// helpers +// + +function getPrecision(per: precision): precisionBits { + if (per === 'h') return { w: 5, p: 10, l: '.~~' }; else + if (per === 's') return { w: 8, p: 23, l: '.' }; else + if (per === 'd') return { w: 11, p: 52, l: '.~' }; else + if (per === 'q') return { w: 15, p: 112, l: '.~~~' }; else + return per; +} + +function bitMask(bits: bigint): bigint { + return (2n ** bits) - 1n; +} + +// +// parsing and construction +// + +// str: @r* format string with its leading . and ~ stripped off +// w: exponent bits +// p: mantissa bits +function parse(str: string, w: number, p: number): bigint { + if (str === 'nan') return makeNaN(w, p); + if (str === 'inf') return makeInf(true, w, p); + if (str === '-inf') return makeInf(false, w, p); + let i = 0; + let sign = true; + if (str[i] === '-') { + sign = false; + i++; + } + let int = ''; + while (str[i] !== '.' && str[i] !== 'e' && str[i] !== undefined) { + int += str[i++]; + } + if (str[i] === '.') i++; + let fra = ''; + while (str[i] !== 'e' && str[i] !== undefined) { + fra += str[i++]; + } + if (str[i] === 'e') i++; + let expSign = true; + if (str[i] === '-') { + expSign = false; + i++; + } + let exp = ''; + while (str[i] !== undefined) { + exp += str[i++]; + } + return BigInt('0b' + makeFloat(w, p, sign, int, fra, expSign, Number(exp))); +} + +function makeNaN(w: number, p: number): bigint { + return bitMask(BigInt(w + 1)) << BigInt(p - 1); +} + +function makeInf(s: boolean, w: number, p: number): bigint { + return bitMask(BigInt(s ? w : w + 1)) << BigInt(p); +} + +// turn into representation without exponent +function makeFloat(w: number, p: number, sign: boolean, intPart: string, floatPart: string, expSign: boolean, exp: number) { + if (exp !== 0) { + if (expSign) { + intPart = intPart + floatPart.padEnd(exp, '0').slice(0, exp); + floatPart = floatPart.slice(exp); + } else { + floatPart = intPart.padStart(exp, '0').slice(-exp) + floatPart; + intPart = intPart.slice(0, -exp); + } + } + return construct(p, w, sign, BigInt(intPart), BigInt(floatPart.length), BigInt(floatPart)); +} + +//NOTE modified from an encodeFloat() written by by Jonas Raoni Soares Silva, +// made to operate on (big)integers, without using js's float logic. +// http://jsfromhell.com/classes/binary-parser +// (yes, this code is vaguely deranged. but it works!) +function construct(precisionBits: number, exponentBits: number, + sign: boolean, intPart: bigint, floatDits: bigint, floatPart: bigint) { + //REVIEW when do we trigger this? + // inputs representing nrs too large for exponentBits? + // inputs with precision we can't match? + // add tests for those cases! should match stdlib result. + function exceed(x: string) { + console.warn(x); + return 1; + } + + const bias = 2**(exponentBits - 1) - 1, + minExp = -bias + 1, + maxExp = bias, + minUnnormExp = minExp - precisionBits, + len = 2 * bias + 1 + precisionBits + 3, + bin = new Array(len), + denom = 10n ** floatDits; + var exp = 0, + signal = !sign, + i, lastBit, rounded, j, result, n; + // zero-initialize the bit-array + for (i = len; i; bin[--i] = 0); + // integral into bits + for (i = bias + 2; intPart && i; bin[--i] = intPart & 1n, intPart = intPart >> 1n); + // fractional into bits + for (i = bias + 1; floatPart > 0n && (i < len); (bin[++i] = (((floatPart *= 2n) >= denom) ? 1 : 0)) && (floatPart = floatPart - denom)); + // walk cursor (i) to first 1-bit. + for (i = -1; ++i < len && !bin[i];); + // round if needed + if (bin[(lastBit = precisionBits - 1 + (i = (exp = bias + 1 - i) >= minExp && exp <= maxExp ? i + 1 : bias + 1 - (exp = minExp - 1))) + 1]) { + if (!(rounded = bin[lastBit])) + for (j = lastBit + 2; !rounded && j < len; rounded = bin[j++]); + for (j = lastBit + 1; rounded && --j >= 0; (bin[j] = (!bin[j] ? 1 : 0) - 0) && (rounded = 0)); + } + // walk cursor (i) to first/next(??) 1-bit + for (i = i - 2 < 0 ? -1 : i - 3; ++i < len && !bin[i];); + + // set exponent, throwing on under- and overflows + (exp = bias + 1 - i) >= minExp && exp <= maxExp ? ++i : exp < minExp && + (exp != bias + 1 - len && exp < minUnnormExp && exceed('r.construct underflow'), i = bias + 1 - (exp = minExp - 1)); + intPart && (exceed(intPart ? 'r.construct overflow' : 'r.construct'), + exp = maxExp + 1, i = bias + 2); + // exponent into bits + for (n = Math.abs(exp + bias), j = exponentBits + 1, result = ''; --j; result = (n & 1) + result, n = n >>= 1); + // final serialization: sign + exponent + mantissa + return (signal ? '1' : '0') + result + bin.slice(i, i + precisionBits).join(''); +}; + +// +// deconstruction and rendering +// + +type dn = { t: 'd', s: boolean, e: number, a: string } + | { t: 'i', s: boolean } + | { t: 'n' }; + +//NOTE not _exactly_ like +r-co due to dragon4() outExponent semantics. +// if we copy +r-co logic exactly we off-by-one all over the place. +function rCo(a: dn): string { + if (a.t === 'n') return 'nan'; + if (a.t === 'i') return a.s ? 'inf' : '-inf'; + let e: number; + if ((a.e - 4) > 0) { // 12000 -> 12e3 e>+2 + e = 1; + } else + if ((a.e + 2) < 0) { // 0.001 -> 1e-3 e<-2 + e = 1; + } else { // 1.234e2 -> '.'@3 -> 123 .4 + e = a.e + 1; + a.e = 0; + } + return (a.s ? '' : '-') + + edCo(e, a.a) + + ((a.e === 0) ? '' : ('e' + a.e.toString())); +} + +function edCo(exp: number, int: string): string { + const dig: number = Math.abs(exp); + if (exp <= 0) { + return '0.' + (''.padEnd(dig, '0')) + int; + } else { + const len = int.length; + if (dig >= len) return int + ''.padEnd((dig - len), '0'); + return int.slice(0, dig) + '.' + int.slice(dig); + } +} + +//NOTE the deconstruct() and dragon4() below are ported from Ryan Juckett's +// PrintFloat32() and Dragon4() respectively. its general structure is +// copied one-to-one and comments are preserved, but we got to drop some +// logic due to having access to native bigints. see his post series for +// a good walkthrough of the underlying algorithm and its implementation, +// as well as pointers to additional references. +// https://www.ryanjuckett.com/printing-floating-point-numbers/ +// we only use one of the cutoff modes, but have maintained support for +// the others for completeness' sake. + +// deconstruct(): binary float to $dn structure (+drg:ff) +function deconstruct(float: bigint, exponentBits: bigint, precisionBits: bigint): dn { + // deconstruct the value into its components + const mantissaMask = bitMask(precisionBits); + const exponentMask = bitMask(exponentBits); + const floatMantissa: bigint = float & mantissaMask; + const floatExponent: bigint = (float >> BigInt(precisionBits)) & exponentMask; + const sign: boolean = ((float >> BigInt(exponentBits + precisionBits)) & 1n) === 0n; + + // transform the components into the values they represent + let mantissa: bigint, exponent: bigint, mantissaHighBitIdx: number, unequalMargins: boolean; + if (floatExponent === exponentMask) { // specials + if (floatMantissa === 0n) + return { t: 'i', s: sign }; // infinity + return { t: 'n' }; // nan + } else + if (floatExponent !== 0n) { // normalized + // the floating point equation is: + // value = (1 + mantissa/2^23) * 2 ^ (exponent-127) + // we convert the integer equation by factoring a 2^23 out of the exponent + // value = (1 + mantissa/2^23) * 2^23 * 2 ^ (exponent-127-23) + // value = (2^23 + mantissa) * 2 ^ (exponent-127-23) + // because of the implied 1 in front of the mantissa we have 24 bits of precision + // m = (2^23 + mantissa) + // e = (exponent-127-23) + mantissa = (1n << BigInt(precisionBits)) | floatMantissa; + exponent = floatExponent - ((2n**(exponentBits-1n))-1n) - precisionBits; + mantissaHighBitIdx = Number(precisionBits); + unequalMargins = (floatExponent !== 1n) && (floatMantissa === 0n); + } else { // denormalized + // the floating point equation is: + // value = (mantissa/2^23) * 2 ^ (1-127) + // we convert the integer equation by factoring a 2^23 out of the exponent + // value = (mantissa/2^23) * 2^23 * 2 ^ (1-127-23) + // value = mantissa * 2 ^ (1-127-23) + // we have up to 23 bits of precision + // m = (mantissa) + // e = (1-127-23) + mantissa = floatMantissa; + exponent = 1n - ((2n**(exponentBits-1n))-1n) - precisionBits; + mantissaHighBitIdx = mantissa.toString(2).length - 1; // poor man's log2 + unequalMargins = false; + } + + const buf = (2n**precisionBits).toString(10).length + 1; + const res = dragon4(mantissa, Number(exponent), mantissaHighBitIdx, unequalMargins, 'unique', 0, buf); + return { t: 'd', s: sign, e: res.outExponent, a: res.digits }; +} + +// dragon4(): binary float to decimal digits +// +// like +drg:fl (but with slightly different outExponent semantics) +// +// mantissa: value significand +// exponent: value exponent in base 2 +// mantissaHighBitIdx: highest set mantissa bit index +// hasUnequalMargins: is the high margin twice the low margin +// cutoffMode: 'unique' | 'totalLength' | 'fractionLength' +// cutoffNumber: cutoff parameter for the selected mode +// bufferSize: max output digits +// +// digits: printed digits +// outExponent: exponent of the first digit printed +// +function dragon4( + mantissa: bigint, + exponent: number, + mantissaHighBitIdx: number, + hasUnequalMargins: boolean, + cutoffMode: 'unique' | 'totalLength' | 'fractionLength', + cutoffNumber: number, + bufferSize: number +): { digits: string, outExponent: number } { + const bexponent = BigInt(exponent); + let pCurDigit = 0; // pointer into output buffer (digit string index) + let outBuffer = new Array(bufferSize).fill('0'); + let outExponent = 0; + + // if mantissa is zero, output "0" + if (mantissa === 0n) { + outBuffer[0] = '0'; + outExponent = 0; + return { digits: outBuffer.slice(0, 1).join(''), outExponent }; + } + + // compute the initial state in integral form such that: + // value = scaledValue / scale + // marginLow = scaledMarginLow / scale + + let scale: bigint; // positive scale applied to value and margin such + // that they can be represented as whole numbers + let scaledValue: bigint; // scale * mantissa + let scaledMarginLow: bigint; // scale * 0.5 * (distance between this floating- + // point number and its immediate lower value) + + // for normalized IEEE floating point values, each time the exponent is + // incremented the margin also doubles. That creates a subset of transition + // numbers where the high margin is twice the size of the low margin. + let scaledMarginHigh: bigint; + + if (hasUnequalMargins) { + if (exponent > 0) { // no fractional component + // 1. expand the input value by multiplying out the mantissa and exponent. + // this represents the input value in its whole number representation. + // 2. apply an additional scale of 2 such that later comparisons against + // the margin values are simplified. + // 3. set the margin value to the lowest mantissa bit's scale. + + scaledValue = 4n * mantissa; + scaledValue <<= bexponent; // 2 * 2 * mantissa*2^exponent + scale = 4n; // 2 * 2 * 1 + scaledMarginLow = 1n << bexponent; // 2 * 2^(exponent-1) + scaledMarginHigh = 1n << bexponent+1n; // 2 * 2 * 2^(exponent-1) + } else { // fractional component + // in order to track the mantissa data as an integer, we store it as is + // with a large scale + + scaledValue = 4n * mantissa; // 2 * 2 * mantissa + scale = 1n << -bexponent+2n; // 2 * 2 * 2^(-exponent) + scaledMarginLow = 1n; // 2 * 2^(-1) + scaledMarginHigh = 2n; // 2 * 2 * 2^(-1) + } + } else { + if (exponent > 0) { // no fractional component + // 1. expand the input value by multiplying out the mantissa and exponent. + // this represents the input value in its whole number representation. + // 2. apply an additional scale of 2 such that later comparisons against + // the margin values are simplified. + // 3. set the margin value to the lowest mantissa bit's scale. + + scaledValue = 2n * mantissa; + scaledValue <<= bexponent; // 2 * mantissa*2^exponent + scale = 2n; // 2 * 1 + scaledMarginLow = 1n << bexponent; // 2 * 2^(exponent-1) + scaledMarginHigh = scaledMarginLow; + } else { // fractional component + // in order to track the mantissa data as an integer, we store it as is + // with a large scale + + scaledValue = 2n * mantissa; // 2 * mantissa + scale = 1n << BigInt(-exponent + 1); // 2 * 2^(-exponent) + scaledMarginLow = 1n; // 2 * 2^(-1) + scaledMarginHigh = scaledMarginLow; + } + } + + // compute an estimate for digitExponent that will be correct or undershoot + // by one. this optimization is based on the paper "Printing Floating-Point + // Numbers Quickly and Accurately" by Burger and Dybvig. + // http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.4656&rep=rep1&type=pdf + // we perform an additional subtraction of 0.69 to increase the frequency of + // a failed estimate because that lets us take a faster branch in the code. + // 0.69 is chosen because 0.69 + log10(2) is less than one by a reasonable + // epsilon that will account for any floating point error. + + // we want to set digitExponent to floor(log10(v)) + 1 + // v = mantissa*2^exponent + // log2(v) = log2(mantissa) + exponent; + // log10(v) = log2(v) * log10(2) + // floor(log2(v)) = mantissaHighBitIdx + exponent; + // log10(v) - log10(2) < (mantissaHighBitIdx + exponent) * log10(2) <= log10(v) + // log10(v) < (mantissaHighBitIdx + exponent) * log10(2) + log10(2) <= log10(v) + log10(2) + // floor( log10(v) ) < ceil( (mantissaHighBitIdx + exponent) * log10(2) ) <= floor( log10(v) ) + 1 + //NOTE loses precision! wants a 64-bit float. but seems precise enough... + const log10_2 = 0.30102999566398119521373889472449; + let digitExponent = Math.ceil((mantissaHighBitIdx + exponent) * log10_2 - 0.69); + + // if the digit exponent is smaller than the smallest desired digit for + // fractional cutoff, pull the digit back into legal range at which point we + // will round to the appropriate value. + // note that while our value for digitExponent is still an estimate, this is + // safe because it only increases the number. this will either correct + // digitExponent to an accurate value or it will clamp it above the accurate + // value. + if (cutoffMode === 'fractionLength' && digitExponent <= -cutoffNumber) { + digitExponent = -cutoffNumber + 1; + } + + // scale adjustment for digit exponent, divide value by 10^digitExponent + if (digitExponent > 0) { + // the exponent is positive creating a division so we multiply up the scale + scale *= BigInt(10) ** BigInt(digitExponent); + } else if (digitExponent < 0) { + // the exponent is negative creating a multiplication so we multiply + // up the scaledValue, scaledMarginLow and scaledMarginHigh + const pow10 = BigInt(10) ** BigInt(-digitExponent); + scaledValue *= pow10; + scaledMarginLow *= pow10; + if (scaledMarginHigh !== scaledMarginLow) { + scaledMarginHigh *= scaledMarginLow; + } + } + + // if (value >= 1), our estimate for digitExponent was too low + if (scaledValue >= scale) { + // the exponent estimate was incorrect. increment the exponent and don't + // perform the premultiply needed for the first loop iteration. + digitExponent += 1; + } else { + // the exponent estimate was correct. multiply larger by the output base + // to prepare for the first loop iteration. + scaledValue *= 10n; + scaledMarginLow *= 10n; + if (scaledMarginHigh !== scaledMarginLow) scaledMarginHigh *= 10n; + } + + // compute the cutoff exponent (the exponent of the final digit to print). + // default to the maximum size of the output buffer. + let cutoffExponent = digitExponent - bufferSize; + if (cutoffMode === 'totalLength') { + let desired = digitExponent - cutoffNumber; + if (desired > cutoffExponent) cutoffExponent = desired; + } else if (cutoffMode === 'fractionLength') { + let desired = -cutoffNumber; + if (desired > cutoffExponent) cutoffExponent = desired; + } + + // output the exponent of the first digit we will print + outExponent = digitExponent - 1; + + //NOTE thanks to native bigints, no bit block normalization needed + + // these values are used to inspect why the print loop terminated so we can properly + // round the final digit. + let low = false; // did the value get within marginLow distance from zero + let high = false; // did the value get within marginHigh distance from one + let outputDigit = 0; // current digit being output + + if (cutoffMode === 'unique') { + // for the unique cutoff mode, we will try to print until we have reached + // a level of precision that uniquely distinguishes this value from its + // neighbors. if we run out of space in the output buffer, we exit early. + + while (true) { + digitExponent -= 1; + + // extract the digit + outputDigit = Number(scaledValue / scale); + scaledValue = scaledValue % scale; + + // update the high end of the value + let scaledValueHigh = scaledValue + scaledMarginHigh; + + // stop looping if we are far enough away from our neighboring values + // or if we have reached the cutoff digit + low = scaledValue < scaledMarginLow; + high = scaledValueHigh > scale; + if (low || high || (digitExponent === cutoffExponent)) break; + + // store the output digit + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit); + pCurDigit += 1; + + // mulitply larger by the output base + scaledValue *= 10n; + scaledMarginLow *= 10n; + if (scaledMarginHigh !== scaledMarginLow) scaledMarginHigh *= 10n; + } + } else { + // for length based cutoff modes, we will try to print until we have + // exhausted all precision (i.e. all remaining digits are zeros) or until + // we reach the desired cutoff digit. + + low = false; + high = false; + while (true) { + digitExponent -= 1; + + // extract the digit + outputDigit = Number(scaledValue / scale); + scaledValue = scaledValue % scale; + + if (scaledValue === 0n || digitExponent === cutoffExponent) break; + + // store the output digit + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit); + pCurDigit += 1; + + // multiply larger by the output base + scaledValue *= 10n; + } + } + + // round off the final digit. + // default to rounding down if value got too close to 0 + let roundDown = low; + + if (low === high) { // legal to round up and down + // round to the closest digit by comparing value with 0.5. to do this we + // need to convert the inequality to large integer values. + // compare( value, 0.5 ) + // compare( scale * value, scale * 0.5 ) + // compare( 2 * scale * value, scale ) + scaledValue *= 2n; + let compare = scaledValue < scale ? -1 + : (scaledValue > scale ? 1 : 0); + roundDown = compare < 0; + // if we are directly in the middle, round towards the even digit + // (i.e. IEEE rouding rules) + if (compare === 0) roundDown = (outputDigit & 1) === 0; + } + + // print the rounded digit + if (roundDown) { + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit); + pCurDigit += 1; + } else { + // handle rounding up + if (outputDigit === 9) { + // find the first non-nine prior digit + while (true) { + if (pCurDigit === 0) { // first digit + // output 1 at the next highest exponent + outBuffer[pCurDigit] = '1'; + pCurDigit += 1; + outExponent += 1; + break; + } + pCurDigit -= 1; + if (outBuffer[pCurDigit] !== '9') { + // increment the digit + outBuffer[pCurDigit] = String.fromCharCode(outBuffer[pCurDigit].charCodeAt(0) + 1); + pCurDigit += 1; + break; + } + } + } else { + // values in the range [0,8] can perform a simple round up + outBuffer[pCurDigit] = String.fromCharCode('0'.charCodeAt(0) + outputDigit + 1); + pCurDigit += 1; + } + } + + // trim trailing zeroes, produce output + const digits = outBuffer.slice(0, pCurDigit).join(''); + return { digits, outExponent }; +} diff --git a/src/render.ts b/src/render.ts new file mode 100644 index 0000000..4676bbb --- /dev/null +++ b/src/render.ts @@ -0,0 +1,224 @@ +// render: serialize into atom literal strings +// +// atom literal rendering from hoon 137 (and earlier). +// stdlib arm names are included for ease of cross-referencing. +// +//TODO unsupported auras: @uc + +import { aura, coin } from './types'; + +import { renderDa, renderDr } from './d'; +import { renderP } from './p'; +import { renderQ } from './q'; +import { renderR } from './r'; + +// render(): scot() +// scot(): render atom as specific aura +// +export const render = scot; +export default render; +export function scot(aura: aura, atom: bigint): string { + return rend({ type: 'dime', aura, atom }); +} + +// rend(): render coin into string +// +export function rend(coin: coin): string { + switch (coin.type) { + case 'blob': + return '~0' + coin.jam.toString(32); + + case 'many': + return '.' + coin.list.reduce((acc: string, item: coin) => { + return acc + '_' + wack(rend(item)); + }, '') + '__'; + + case 'dime': + switch(coin.aura[0]) { + case 'c': + // this short-circuits the (wood (tuft atom)) calls that + // hoon.hoon does, leaning on wood only for ascii characters, + // and going straight to encoded representation for anything else. + // (otherwise we'd need to stringify the utf-32 bytes, ouch.) + if (coin.atom < 0x7fn) + return '~-' + encodeString(String.fromCharCode(Number(coin.atom))); + else + return '~-~' + coin.atom.toString(16) + '.'; + case 'd': + switch(coin.aura[1]) { + case 'a': + return renderDa(coin.atom); + case 'r': + return renderDr(coin.atom); + default: + return zco(coin.atom); + } + case 'f': + switch(coin.atom) { + case 0n: return '.y'; + case 1n: return '.n'; + default: return zco(coin.atom); + } + case 'n': + return '~'; + case 'i': + switch(coin.aura[1]) { + case 'f': return '.' + spite(coin.atom, 1, 4, 10); + case 's': return '.' + spite(coin.atom, 2, 8, 16); + default: return zco(coin.atom); + } + case 'p': + return renderP(coin.atom); + case 'q': + return renderQ(coin.atom); + case 'r': + switch(coin.aura[1]) { + case 'd': return renderR('d', coin.atom); + case 'h': return renderR('h', coin.atom); + case 'q': return renderR('q', coin.atom); + case 's': return renderR('s', coin.atom); + default: return zco(coin.atom); + } + case 'u': + switch(coin.aura[1]) { + case 'c': throw new Error('aura-js: @uc rendering unsupported'); //TODO; + case 'b': return '0b' + split(coin.atom.toString(2), 4); + case 'i': return '0i' + dco(1, coin.atom); + case 'x': return '0x' + split(coin.atom.toString(16), 4); + case 'v': return '0v' + split(coin.atom.toString(32), 5); + case 'w': return '0w' + split(blend(6, UW_ALPHABET, coin.atom), 5); + default: return split(coin.atom.toString(10), 3); + } + case 's': + const end = (coin.atom & 1n); + coin.atom = end + (coin.atom >> 1n); + coin.aura = coin.aura.replace('s', 'u') as aura; + return ((end === 0n) ? '--' : '-') + rend(coin); + case 't': + if (coin.aura[1] === 'a') { + if (coin.aura[2] === 's') { + return cordToString(coin.atom); + } else { + return '~.' + cordToString(coin.atom); + } + } else { + return '~~' + encodeString(cordToString(coin.atom)); + } + default: + return zco(coin.atom); + } + } +} + +function dco(lent: number, atom: bigint): string { + return atom.toString(10).padStart(lent, '0'); +} + +function xco(lent: number, atom: bigint): string { + return atom.toString(16).padStart(lent, '0'); +} + +function zco(atom: bigint): string { + return '0x' + xco(1, atom); +} + +function wack(str: string) { + return str.replaceAll('~', '~~').replaceAll('_', '~-'); +} + +// encodeString(): encode string into @ta-safe format +// +// using logic from +wood. +// for example, 'some Chars!' becomes '~.some.~43.hars~21.' +// this is url-safe encoding for arbitrary strings. +// +export function encodeString(string: string) { + let out = ''; + for (let i = 0; i < string.length; i += 1) { + const char = string[i]; + let add = ''; + switch (char) { + case ' ': + add = '.'; + break; + case '.': + add = '~.'; + break; + case '~': + add = '~~'; + break; + default: { + const codePoint = string.codePointAt(i); + if (!codePoint) break; + // js strings are encoded in UTF-16, so 16 bits per character. + // codePointAt() reads a _codepoint_ at a character index, and may + // consume up to two js string characters to do so, in the case of + // 16 bit surrogate pseudo-characters. here we detect that case, so + // we can advance the cursor to skip past the additional character. + if (codePoint > 0xffff) i += 1; + if ( + (codePoint >= 97 && codePoint <= 122) || // a-z + (codePoint >= 48 && codePoint <= 57) || // 0-9 + char === '-' + ) { + add = char; + } else { + add = `~${codePoint.toString(16)}.`; + } + } + } + out += add; + } + return out; +} + +const UW_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; +function blend(bits: number, alphabet: string, atom: bigint): string { + if (atom === 0n) return alphabet[0]; + let out = ''; + const bbits = BigInt(bits); + while (atom !== 0n) { + out = alphabet[Number(BigInt.asUintN(bits, atom))] + out; + atom = atom >> bbits; + } + return out; +} + +function split(str: string, group: number): string { + // insert '.' every $group characters counting from the end, + // while avoiding putting a leading dot at the start + return str.replace(new RegExp(`(?=(?:.{${group}})+$)(?!^)`, 'g'), '.'); +} + +// byte-level split() +function spite(atom: bigint, bytes: number, groups: number, base: number = 10): string { + let out = ''; + const size = 8n * BigInt(bytes); + const mask = (1n << size) - 1n; + while (groups-- > 0) { + if (out !== '') out = '.' + out; + out = (atom & mask).toString(base) + out; + atom = atom >> size; + } + return out; +} + +function cordToString(atom: bigint): string { + return new TextDecoder('utf-8').decode(atomToByteArray(atom).reverse()); +}; + +//NOTE from nockjs' bigIntToByteArray +//REVIEW original produced [0] for 0n... probably not correct in our contexts! +function atomToByteArray(atom: bigint): Uint8Array { + if (atom === 0n) return new Uint8Array(0); + const hexString = atom.toString(16); + const paddedHexString = hexString.length % 2 === 0 ? hexString : '0' + hexString; + const arrayLength = paddedHexString.length / 2; + const int8Array = new Uint8Array(arrayLength); + for (let i = 0; i < paddedHexString.length; i += 2) { + const hexSubstring = paddedHexString.slice(i, i + 2); + const signedInt = (parseInt(hexSubstring, 16) << 24) >> 24; + int8Array[(i / 2)] = signedInt; + } + return int8Array; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2c7a86e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,34 @@ +export type aura = 'c' + | 'da' + | 'dr' + | 'f' + | 'if' + | 'is' + | 'n' + | 'p' + | 'q' + | 'rd' + | 'rh' + | 'rq' + | 'rs' + | 'sb' + | 'sd' + | 'si' + | 'sv' + | 'sw' + | 'sx' + | 't' + | 'ta' + | 'tas' + | 'ub' + | 'ud' + | 'ui' + | 'uv' + | 'uw' + | 'ux'; + +export type dime = { aura: aura, atom: bigint } + +export type coin = ({ type: 'dime' } & dime) + | { type: 'blob', jam: bigint } //NOTE nockjs for full noun + | { type: 'many', list: coin[] } diff --git a/src/ud.ts b/src/ud.ts deleted file mode 100644 index 6d06166..0000000 --- a/src/ud.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { chunk } from './utils'; - -/** - * Given a string representing a @ud, returns a bigint - * - * @param {string} ud the number as @ud - * @return {bigint} the number as bigint - */ -export function parseUd(ud: string): bigint { - return BigInt(ud.replace(/\./g, '')); -} - -/** - * Given a bigint representing a @ud, returns a proper @ud as string - * - * @param {bigint} ud the number as bigint - * @return {string} the number as @ud - */ -export function formatUd(ud: bigint): string { - const transform = chunk(ud.toString().split('').reverse(), 3) - .map((group) => group.reverse().join('')) - .reverse() - .join('.'); - return transform.replace(/^[0\.]+/g, ''); -} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 72f9e2b..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -export function chunkFromRight(str: string, size: number) { - const numChunks = Math.ceil(str.length / size); - const chunks = new Array(numChunks); - - for (let i = numChunks - 1, o = str.length; i >= 0; --i, o -= size) { - let start = o - size; - let len = size; - if (start < 0) { - start = 0; - len = o; - } - chunks[i] = str.substr(start, len); - } - - return chunks; -} - -export function chunk(arr: T[], size: number): T[][] { - let chunk: T[] = []; - let newArray = [chunk]; - - for (let i = 0; i < arr.length; i++) { - if (chunk.length < size) { - chunk.push(arr[i]); - } else { - chunk = [arr[i]]; - newArray.push(chunk); - } - } - - return newArray; -} - -export function dropWhile(arr: T[], pred: (x: T) => boolean): T[] { - const newArray = arr.slice(); - - for (const item of arr) { - if (pred(item)) { - newArray.shift(); - } else { - return newArray; - } - } - - return newArray; -} - -export function splitAt(index: number, str: string) { - return [str.slice(0, index), str.slice(index)]; -} diff --git a/src/uv.ts b/src/uv.ts deleted file mode 100644 index c0d38db..0000000 --- a/src/uv.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { chunkFromRight } from './utils'; - -const uvAlphabet = '0123456789abcdefghijklmnopqrstuv'; - -export function parseUv(x: string) { - let res = 0n; - x = x.slice(2); - while (x !== '') { - if (x[0] !== '.') { - res = (res << 5n) + BigInt(uvAlphabet.indexOf(x[0])); - } - x = x.slice(1); - } - return res; -} - -export function formatUv(x: bigint | string) { - if (typeof x === 'string') { - x = BigInt(x); - } - let res = ''; - while (x !== 0n) { - let nextFive = Number(BigInt.asUintN(5, x)); - res = uvAlphabet[nextFive] + res; - x = x >> 5n; - } - return `0v${chunkFromRight(res, 5).join('.')}`; -} diff --git a/src/uw.ts b/src/uw.ts deleted file mode 100644 index 6a5017a..0000000 --- a/src/uw.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { chunkFromRight } from './utils'; - -const uwAlphabet = - '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~'; - -export function parseUw(x: string) { - let res = 0n; - x = x.slice(2); - while (x !== '') { - if (x[0] !== '.') { - res = (res << 6n) + BigInt(uwAlphabet.indexOf(x[0])); - } - x = x.slice(1); - } - return res; -} - -export function formatUw(x: bigint | string) { - if (typeof x === 'string') { - x = BigInt(x); - } - let res = ''; - while (x !== 0n) { - let nextSix = Number(BigInt.asUintN(6, x)); - res = uwAlphabet[nextSix] + res; - x = x >> 6n; - } - return `0w${chunkFromRight(res, 5).join('.')}`; -} diff --git a/src/ux.ts b/src/ux.ts deleted file mode 100644 index d4d1b58..0000000 --- a/src/ux.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { dropWhile, chunk } from './utils'; - -/** - * Given a string representing a @ux, returns a string in hex - * - * @param {string} ux the number as @ux - * @return {string} the number as hex - */ -export function parseUx(ux: string) { - return ux.replace('0x', '').replace('.', ''); -} - -/** - * Given a string representing hex, returns a proper @ux - * - * @param {string} hex the number as hex - * @return {string} the number as hex - */ -export const formatUx = (hex: string): string => { - const nonZeroChars = dropWhile(hex.split(''), (y) => y === '0'); - const ux = - chunk(nonZeroChars.reverse(), 4) - .map((x) => { - return x.reverse().join(''); - }) - .reverse() - .join('.') || '0'; - - return `0x${ux}`; -}; diff --git a/test/aura.test.ts b/test/aura.test.ts deleted file mode 100644 index 4d976d7..0000000 --- a/test/aura.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - formatDa, - parseDa, - formatUw, - parseUw, - formatUv, - parseUv, - parseUd, - formatUd, - parseUx, - formatUx, -} from '../src'; - -const DA_PAIRS: [string, bigint][] = [ - [ - '~2022.5.2..15.50.20..b4cb', - BigInt('170141184505617087925707667943685357568'), - ], - [ - '~2022.5.2..18.52.34..8166.240c.0635.b423', - BigInt('170141184505617289618704043249403016227'), - ], -]; -describe('@da', () => { - DA_PAIRS.map(([da, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseDa(da); - const diff = integer - res; - expect(diff == 0n).toBe(true); - }); - it('formats', () => { - const res = formatDa(integer); - expect(res).toEqual(da); - }); - }); - }); -}); - -const UD_PAIRS: [string, bigint][] = [ - ['123', 123n], - ['7.827.527.286', 7827527286n], - [ - '927.570.172.527.456.683.282.759.587.841.913.712.910.138.850.310.449.267.827.527.286', - BigInt('927570172527456683282759587841913712910138850310449267827527286'), - ], -]; - -describe('@ud', () => { - UD_PAIRS.map(([ud, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUd(ud); - const diff = integer - res; - expect(diff === 0n).toBe(true); - }); - it('formats', () => { - const res = formatUd(integer); - expect(res).toEqual(ud); - }); - }); - }); -}); - -const UW_PAIRS: [string, bigint][] = [ - ['0wji', 1234n], - [ - '0w2.VNFPq.zLWXr.mHG98.cOSaU.jD-HK.WOAEW.icKX-.-UOti.RrLxM.BEdKI.U8j~T.rgqLe.HuVVm.m5aDi.FcUj0.z-9H9.PWYVS', - BigInt( - '9729869760580312915057700420931106632029212932045019789366559593013069886734510969807231346927570172527456683282759587841913712910138850310449267827527286' - ), - ], - [ - '0w8.~wwXK.5Jbvq.EPFfs.mWqAa.G6VLL.Hp5RZ.1ztU0.OdjK6.rwC4f.IUflm.bew2G.q2V58.Yvb-y.8D7JP.mAX5-.tTUnZ.4PIzy.fU8eX.xriTS.GcWjT.5KCF2.GxKrX.WShtv.goTu0.czkXx.CU9x3.Xe3Rl.yPE0G.CwKhi.f7O~E.y9NXs.RFeNv.Dt-5~.hcX8U.z-23K.UmQJZ.GzeAZ.NrFGg.GErC-.-JAnn.Q6dTw.38ReU.pK2og.-PwZl.oIW0a.FEbAk.zNYLW.8ysuT.dqjIn.VTvxv.QjeOe', - BigInt( - '338660688809191135992117047766620650717811482934943979674885003948246397791915632356127816874957444994283298782534439422236465196123969501940528462017413072176474702992911473379692926314882846435461316330442229390384286920909868601208813714735355172837223931275587957994082972971545840145432819726749971121524031459169847685770572005049993814978529576884322644499161452167351136603982630270130940863597682766057587354154988711969349941809951888309135835193470094' - ), - ], -]; -describe('@uw', () => { - UW_PAIRS.map(([uw, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUw(uw); - const diff = integer - res; - expect(diff === 0n).toBe(true); - }); - it('formats', () => { - const res = formatUw(integer); - expect(res).toEqual(uw); - }); - }); - }); -}); - -const UV_PAIRS: [string, bigint][] = [ - ['0v16i', 1234n], - [ - '0v1d0.l2h7n.mo1ro.s3r8e.4f6gd.dfsp1.hc5en.a0k8j.1v7vk.16jqd.oog39.5ool7.mrkdp.vvofi.gd2d6.vnmi9.a1dlt.7lbbm.iq76k.u5ivc.pp8qa', - BigInt( - '4715838753694475992579249794985609354876653107513376869107585916141874120351297535898666953377988719385257642282348313095587079274499396365843215360500554' - ), - ], - [ - '0v1q7.2j1o2.gsrac.v0lr2.4qq3l.dl4dl.geimi.ti4kn.nerpk.io8e9.fb6u8.qdo3a.f6jnl.4t0ro.mnphj.45eu3.aasog.tgnop.mgknj.vrf7c.qh8uk.uhoko.e0k76.qj7o5.eoh6m.gtbd9.3dc3k.lknch.55trm.ud4m2.3ibqp.ni6je.0qjpk.tt978.6u5lu.ccp1b.ngqin.647c5.u6dk5.5svur.pr6ka.7l7ke.563g5.1pmkp.u1bm4.9lk7a.ra8rb.0t5d5.r499f.etnj9.5ggsi.umdsh.krg6k.ud7fa.9q1nh.dfj36.8ats6.klph1.r1fhj.d19f7.vmuep.l2ht9', - BigInt( - '2192679466494434890472543084060582766548642415978526232232529756549132081077716901494847003622252677433111645469887112954835308752404322485369993198040597565692723588585723772692969275396046341198068016409658069930178326315327541379152850899800598824712189194725563892210423915200062671509436137248472305920263160462934628062386175666117414052493363024883656571948762124184585291750029792031534226654202512820124560651712985859227347538529179923696933418921183145' - ), - ], -]; -describe('@uv', () => { - UV_PAIRS.map(([uv, integer], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUv(uv); - const diff = integer - res; - expect(diff === 0n).toBe(true); - }); - it('formats', () => { - const res = formatUv(integer); - expect(res).toEqual(uv); - }); - }); - }); -}); - -const UX_PAIRS: [string, string][] = [ - ['0x0', '0'], - ['0xff.a0e2', 'ffa0e2'], -]; - -describe('@ux', () => { - UX_PAIRS.map(([ux, hex], idx) => { - describe(`case ${idx}`, () => { - it('parses', () => { - const res = parseUx(ux); - expect(res).toEqual(hex); - }); - it('formats', () => { - const res = formatUx(hex); - expect(res).toEqual(ux); - }); - }); - }); - - it('trims leading zeroes', () => { - const res = formatUx('00ffa0e2'); - expect(res).toEqual('0xff.a0e2'); - }); -}); diff --git a/test/data/atoms.ts b/test/data/atoms.ts new file mode 100644 index 0000000..b92e3c7 --- /dev/null +++ b/test/data/atoms.ts @@ -0,0 +1,778 @@ +import { aura } from '../../src/types'; + +// most test cases generated from snippets similar to the following: +// +// =| n=@ud +// ^- (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q]) +// %- zing +// |- ^- (list (list [n=@ui =@ub =@ud =@ui =@uv =@uw =@ux =@p =@q])) +// ?: (gth n 5) ~ +// :_ $(n +(n), eny (shaz +(eny))) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// :- [. . . . . . .]:(end 2^(bex n) eny) +// =. eny (shaz eny) +// [. . . . . . .]~:(end 2^(bex n) eny) +// + +export const INTEGER_AURAS: aura[] = [ + 'ub', 'ud', 'ui', 'uv', 'uw', 'ux', + 'sb', 'sd', 'si', 'sv', 'sw', 'sx', +]; +export const INTEGER_TESTS: { + n: bigint, + ub: string, + ud: string, + ui: string, + uv: string, + uw: string, + ux: string, + sb: string, + sd: string, + si: string, + sv: string, + sw: string, + sx: string, +}[] = [ + { 'n': 0n, + 'ub': '0b0', + 'ud': '0', + 'ui': '0i0', + 'uv': '0v0', + 'uw': '0w0', + 'ux': '0x0', + 'sb': '--0b0', + 'sd': '--0', + 'si': '--0i0', + 'sv': '--0v0', + 'sw': '--0w0', + 'sx': '--0x0', + }, + { 'n': 7n, + 'ub': '0b111', + 'ud': '7', + 'ui': '0i7', + 'uv': '0v7', + 'uw': '0w7', + 'ux': '0x7', + 'sb': '-0b100', + 'sd': '-4', + 'si': '-0i4', + 'sv': '-0v4', + 'sw': '-0w4', + 'sx': '-0x4', + }, + { 'n': 5n, + 'ub': '0b101', + 'ud': '5', + 'ui': '0i5', + 'uv': '0v5', + 'uw': '0w5', + 'ux': '0x5', + 'sb': '-0b11', + 'sd': '-3', + 'si': '-0i3', + 'sv': '-0v3', + 'sw': '-0w3', + 'sx': '-0x3', + }, + { 'n': 4n, + 'ub': '0b100', + 'ud': '4', + 'ui': '0i4', + 'uv': '0v4', + 'uw': '0w4', + 'ux': '0x4', + 'sb': '--0b10', + 'sd': '--2', + 'si': '--0i2', + 'sv': '--0v2', + 'sw': '--0w2', + 'sx': '--0x2', + }, + { 'n': 171n, + 'ub': '0b1010.1011', + 'ud': '171', + 'ui': '0i171', + 'uv': '0v5b', + 'uw': '0w2H', + 'ux': '0xab', + 'sb': '-0b101.0110', + 'sd': '-86', + 'si': '-0i86', + 'sv': '-0v2m', + 'sw': '-0w1m', + 'sx': '-0x56', + }, + { 'n': 53n, + 'ub': '0b11.0101', + 'ud': '53', + 'ui': '0i53', + 'uv': '0v1l', + 'uw': '0wR', + 'ux': '0x35', + 'sb': '-0b1.1011', + 'sd': '-27', + 'si': '-0i27', + 'sv': '-0vr', + 'sw': '-0wr', + 'sx': '-0x1b', + }, + { 'n': 77n, + 'ub': '0b100.1101', + 'ud': '77', + 'ui': '0i77', + 'uv': '0v2d', + 'uw': '0w1d', + 'ux': '0x4d', + 'sb': '-0b10.0111', + 'sd': '-39', + 'si': '-0i39', + 'sv': '-0v17', + 'sw': '-0wD', + 'sx': '-0x27', + }, + { 'n': 64491n, + 'ub': '0b1111.1011.1110.1011', + 'ud': '64.491', + 'ui': '0i64491', + 'uv': '0v1uvb', + 'uw': '0wfLH', + 'ux': '0xfbeb', + 'sb': '-0b111.1101.1111.0110', + 'sd': '-32.246', + 'si': '-0i32246', + 'sv': '-0vvfm', + 'sw': '-0w7TS', + 'sx': '-0x7df6', + }, + { 'n': 51765n, + 'ub': '0b1100.1010.0011.0101', + 'ud': '51.765', + 'ui': '0i51765', + 'uv': '0v1ihl', + 'uw': '0wcER', + 'ux': '0xca35', + 'sb': '-0b110.0101.0001.1011', + 'sd': '-25.883', + 'si': '-0i25883', + 'sv': '-0vp8r', + 'sw': '-0w6kr', + 'sx': '-0x651b', + }, + { 'n': 46444n, + 'ub': '0b1011.0101.0110.1100', + 'ud': '46.444', + 'ui': '0i46444', + 'uv': '0v1dbc', + 'uw': '0wblI', + 'ux': '0xb56c', + 'sb': '--0b101.1010.1011.0110', + 'sd': '--23.222', + 'si': '--0i23222', + 'sv': '--0vmlm', + 'sw': '--0w5GS', + 'sx': '--0x5ab6', + }, + { 'n': 384265565n, + 'ub': '0b1.0110.1110.0111.0110.1101.0101.1101', + 'ud': '384.265.565', + 'ui': '0i384265565', + 'uv': '0vb.eerat', + 'uw': '0wmVSRt', + 'ux': '0x16e7.6d5d', + 'sb': '-0b1011.0111.0011.1011.0110.1010.1111', + 'sd': '-192.132.783', + 'si': '-0i192132783', + 'sv': '-0v5.n7dlf', + 'sw': '-0wbsXqL', + 'sx': '-0xb73.b6af', + }, + { 'n': 2456897374n, + 'ub': '0b1001.0010.0111.0001.0100.0111.0101.1110', + 'ud': '2.456.897.374', + 'ui': '0i2456897374', + 'uv': '0v29.72hqu', + 'uw': '0w2.isktu', + 'ux': '0x9271.475e', + 'sb': '--0b100.1001.0011.1000.1010.0011.1010.1111', + 'sd': '--1.228.448.687', + 'si': '--0i1228448687', + 'sv': '--0v14.jh8tf', + 'sw': '--0w1.9eaeL', + 'sx': '--0x4938.a3af', + }, + { 'n': 38583115n, + 'ub': '0b10.0100.1100.1011.1011.0100.1011', + 'ud': '38.583.115', + 'ui': '0i38583115', + 'uv': '0v1.4peqb', + 'uw': '0w2jbJb', + 'ux': '0x24c.bb4b', + 'sb': '-0b1.0010.0110.0101.1101.1010.0110', + 'sd': '-19.291.558', + 'si': '-0i19291558', + 'sv': '-0vicnd6', + 'sw': '-0w19BSC', + 'sx': '-0x126.5da6', + }, + { 'n': 13604104043154737885n, + 'ub': '0b1011.1100.1100.1011.0111.1100.1000.1100.1011.0011.1011.0001.1000.1010.1101.1101', + 'ud': '13.604.104.043.154.737.885', + 'ui': '0i13604104043154737885', + 'uv': '0vbpi.rship.r32mt', + 'uw': '0wb.Pbv8O.PIoHt', + 'ux': '0xbccb.7c8c.b3b1.8add', + 'sb': '-0b101.1110.0110.0101.1011.1110.0100.0110.0101.1001.1101.1000.1100.0101.0110.1111', + 'sd': '-6.802.052.021.577.368.943', + 'si': '-0i6802052021577368943', + 'sv': '-0v5sp.du8pc.thhbf', + 'sw': '-0w5.VBLAp.pSclL', + 'sx': '-0x5e65.be46.59d8.c56f', + }, + { 'n': 18441444580797368868n, + 'ub': '0b1111.1111.1110.1101.0010.1100.0010.0011.1010.0111.0111.1010.1100.1010.0010.0100', + 'ud': '18.441.444.580.797.368.868', + 'ui': '0i18441444580797368868', + 'uv': '0vfvr.9c4ej.nlih4', + 'uw': '0wf.~Jb2e.DuIEA', + 'ux': '0xffed.2c23.a77a.ca24', + 'sb': '--0b111.1111.1111.0110.1001.0110.0001.0001.1101.0011.1011.1101.0110.0101.0001.0010', + 'sd': '--9.220.722.290.398.684.434', + 'si': '--0i9220722290398684434', + 'sv': '--0v7vt.km279.rqp8i', + 'sw': '--0w7.~SBx7.jLmki', + 'sx': '--0x7ff6.9611.d3bd.6512', + }, + { 'n': 7643844662312245512n, + 'ub': '0b110.1010.0001.0100.0110.0100.0011.1000.1011.0111.0110.0011.0001.1001.0000.1000', + 'ud': '7.643.844.662.312.245.512', + 'ui': '0i7643844662312245512', + 'uv': '0v6k5.3472r.m6688', + 'uw': '0w6.Ekp3y.ToNA8', + 'ux': '0x6a14.6438.b763.1908', + 'sb': '--0b11.0101.0000.1010.0011.0010.0001.1100.0101.1011.1011.0001.1000.1100.1000.0100', + 'sd': '--3.821.922.331.156.122.756', + 'si': '--0i3821922331156122756', + 'sv': '--0v3a2.hi3hd.r3344', + 'sw': '--0w3.kacxN.rIoO4', + 'sx': '--0x350a.321c.5bb1.8c84', + }, + { 'n': 293389376720547819362821033486028091527n, + 'ub': '0b1101.1100.1011.1000.1011.1101.0001.0100.1101.0100.1111.1101.1011.1110.0011.0111.1001.0011.1100.0000.1000.1110.0100.1000.0111.0011.1010.1111.1011.0000.1000.0111', + 'ud': '293.389.376.720.547.819.362.821.033.486.028.091.527', + 'ui': '0i293389376720547819362821033486028091527', + 'uv': '0v6.sn2uh.9l7tn.orp7g.4e91p.qvc47', + 'uw': '0w3s.KbQkR.fS-dV.f0zAx.PHX27', + 'ux': '0xdcb8.bd14.d4fd.be37.93c0.8e48.73af.b087', + 'sb': '-0b110.1110.0101.1100.0101.1110.1000.1010.0110.1010.0111.1110.1101.1111.0001.1011.1100.1001.1110.0000.0100.0111.0010.0100.0011.1001.1101.0111.1101.1000.0100.0100', + 'sd': '-146.694.688.360.273.909.681.410.516.743.014.045.764', + 'si': '-0i146694688360273909681410516743014045764', + 'sv': '-0v3.ebhf8.kqjur.sdsjo.274gs.tfm24', + 'sw': '-0w1K.n5Waq.DXv6Y.DwhOg.VRZx4', + 'sx': '-0x6e5c.5e8a.6a7e.df1b.c9e0.4724.39d7.d844', + }, + { 'n': 11826418988767709295206418976840492314n, + 'ub': '0b1000.1110.0101.1010.1111.0111.1001.0110.1100.1000.1101.1010.0001.1100.0011.1111.0000.1011.0001.0001.1010.0111.0011.1111.0001.1101.0110.0010.0101.0001.1010', + 'ud': '11.826.418.988.767.709.295.206.418.976.840.492.314', + 'ui': '0i11826418988767709295206418976840492314', + 'uv': '0v8smnn.ir4dk.71v1c.8qefo.tc98q', + 'uw': '0w8.VqZVr.8SxM~.2N6Df.NRykq', + 'ux': '0x8e5.af79.6c8d.a1c3.f0b1.1a73.f1d6.251a', + 'sb': '--0b100.0111.0010.1101.0111.1011.1100.1011.0110.0100.0110.1101.0000.1110.0001.1111.1000.0101.1000.1000.1101.0011.1001.1111.1000.1110.1011.0001.0010.1000.1101', + 'sd': '--5.913.209.494.383.854.647.603.209.488.420.246.157', + 'si': '--0i5913209494383854647603209488420246157', + 'sv': '--0v4ebbr.pdi6q.3gvgm.4d77s.em4kd', + 'sw': '--0w4.sJuYJ.ArgUv.xozjD.UWNad', + 'sx': '--0x472.d7bc.b646.d0e1.f858.8d39.f8eb.128d', + }, + { 'n': 75341289328899252391918368331716799250n, + 'ub': '0b11.1000.1010.1110.0011.0100.0101.1011.0011.0101.0100.1101.1100.0101.0101.1100.0100.0110.1001.0001.0011.0110.0100.0111.0111.1011.0010.1110.1010.1011.0001.0010', + 'ud': '75.341.289.328.899.252.391.918.368.331.716.799.250', + 'ui': '0i75341289328899252391918368331716799250', + 'uv': '0v1.oloq5.mdado.le4d4.9m8tt.itaoi', + 'uw': '0wU.Hzhrd.kT5n4.qhdAt.XbGIi', + 'ux': '0x38ae.345b.354d.c55c.4691.3647.7b2e.ab12', + 'sb': '--0b1.1100.0101.0111.0001.1010.0010.1101.1001.1010.1010.0110.1110.0010.1010.1110.0010.0011.0100.1000.1001.1011.0010.0011.1011.1101.1001.0111.0101.0101.1000.1001', + 'sd': '--37.670.644.664.449.626.195.959.184.165.858.399.625', + 'si': '--0i37670644664449626195959184165858399625', + 'sv': '--0vsasd2.r6l6s.an26i.4r4eu.pelc9', + 'sw': '--0ws.lNEJC.GryHy.d8COe.ZBRm9', + 'sx': '--0x1c57.1a2d.9aa6.e2ae.2348.9b23.bd97.5589', + }, +]; + +export const IPV4_TESTS: { + n: bigint, + if: string +}[] = [ + { 'n': 807321889n, + 'if': '.48.30.193.33' + }, + { 'n': 3804317590n, + 'if': '.226.193.71.150' + }, + { 'n': 895708593n, + 'if': '.53.99.109.177' + }, + { 'n': 133621386n, + 'if': '.7.246.230.138' + }, + { 'n': 1110053789n, + 'if': '.66.42.19.157' + }, + { 'n': 1738320401n, + 'if': '.103.156.170.17' + }, + { 'n': 4231920335n, + 'if': '.252.61.250.207' + }, + { 'n': 2681308946n, + 'if': '.159.209.135.18' + }, + { 'n': 5963504n, + 'if': '.0.90.254.240' + }, + { 'n': 691080007n, + 'if': '.41.49.11.71' + }, + { 'n': 3896744529n, + 'if': '.232.67.154.81' + }, + { 'n': 4172959856n, + 'if': '.248.186.80.112' + }, + { 'n': 282345860n, + 'if': '.16.212.65.132' + }, + { 'n': 1400904024n, + 'if': '.83.128.25.88' + }, + { 'n': 3171770264n, + 'if': '.189.13.95.152' + }, + { 'n': 959932782n, + 'if': '.57.55.105.110' + }, + { 'n': 890947430n, + 'if': '.53.26.199.102' + }, + { 'n': 2083634832n, + 'if': '.124.49.190.144' + }, + { 'n': 2135483961n, + 'if': '.127.72.230.57' + }, + { 'n': 3903494749n, + 'if': '.232.170.154.93' + }, + { 'n': 588679618n, + 'if': '.35.22.137.194' + }, + // zero bytes + { 'n': 0n, + 'if': '.0.0.0.0' + }, + { 'n': 257n, + 'if': '.0.0.1.1' + }, + { 'n': 4278190335n, + 'if': '.255.0.0.255' + }, +]; + +export const IPV6_TESTS: { + n: bigint, + is: string +}[] = [ + { 'n': 337589486868456293016024144360375510656n, + 'is': '.fdf9.5ec3.442f.f80c.32eb.706e.22e2.e680' + }, + { 'n': 235657577517117307780314982160714186767n, + 'is': '.b149.ff9e.c1eb.b281.4faf.a336.9384.400f' + }, + { 'n': 325357659134260222628146827587324816556n, + 'is': '.f4c5.9b18.1578.8d04.baff.1cb5.dc9.2cac' + }, + { 'n': 226735022798529839177410190715794164949n, + 'is': '.aa93.93b5.4850.9b87.d240.94d8.1c97.cd5' + }, + { 'n': 57252987712168015925131514820989714772n, + 'is': '.2b12.863b.6962.1b1e.7831.5a7.f37e.8154' + }, + { 'n': 88629668840893973117284629411124587232n, + 'is': '.42ad.740c.a906.aac0.1de5.fb30.8289.aae0' + }, + { 'n': 248290127112293020329330966732261351214n, + 'is': '.baca.f066.e65f.a51c.bd5a.1b7d.55ec.df2e' + }, + { 'n': 37045722501599219774186597438855573164n, + 'is': '.1bde.bf22.feae.52c8.6925.6cc3.9533.eeac' + }, + { 'n': 67997883586901786122445670637270469610n, + 'is': '.3327.ea7f.fdda.e2d6.75bd.8c18.173c.bea' + }, + { 'n': 326647343852133419583482125089361985308n, + 'is': '.f5bd.fd75.bf6b.2e3b.2888.3de.fa55.7f1c' + }, + { 'n': 195717022160085584065700179326430314983n, + 'is': '.933d.ba34.4b3a.15c8.f807.1faf.9157.81e7' + }, + { 'n': 211000578906934658167753232415210617987n, + 'is': '.9ebd.3bba.dbe1.a940.4026.a247.6d6c.3c83' + }, + { 'n': 154849127606874136259068650818300161140n, + 'is': '.747e.db6f.a054.18af.e6c6.e4e3.6f7e.2c74' + }, + { 'n': 14767302673435791657373783029827412506n, + 'is': '.b1c.143c.1300.94d3.2ff7.3b36.def2.a21a' + }, + { 'n': 123971713052710237067818008482194269471n, + 'is': '.5d44.155c.7d2d.7c6.b52a.9c0b.3bbe.251f' + }, + { 'n': 262699680397970860118004915307903938281n, + 'is': '.c5a2.1e3a.e156.2210.e905.21b9.292b.6ee9' + }, + { 'n': 257209633456024567583396352939592994981n, + 'is': '.c180.c604.7996.1f58.949e.27e.eb3b.50a5' + }, + { 'n': 258801164939755730885841989240059520853n, + 'is': '.c2b3.4a93.f230.4b8c.569.ccc3.266.e755' + }, + { 'n': 270767742644940472562032354717108516079n, + 'is': '.cbb3.f869.14a2.69f8.2372.9a37.d897.b4ef' + }, + { 'n': 269795612802750900437183701002939522985n, + 'is': '.caf8.beb5.719a.f72e.5ae.2e41.a74b.dfa9' + }, + { 'n': 47520821311211709795896162188995433788n, + 'is': '.23c0.2d61.dccf.12ea.5736.4606.77e1.953c' + }, + // zero bytes + { 'n': 0n, + 'is': '.0.0.0.0.0.0.0.0' + }, + { 'n': 340277174624079928635746076935439056895n, + 'is': '.ffff.0.0.0.0.0.0.ffff' + } +]; + +// float tests generated like so (swapping out 32 for each size) +// +// =| n=@ud +// |- ^- (list [n=@ui =@rs]) +// ?: (gth n 20) ~ +// :_ $(n +(n), eny (shaz +(eny))) +// [. .]:(end 0^32 eny) + +export const FLOAT_16_TESTS: { + n: bigint, + rh: string +}[] = [ + { 'n': 2794n, 'rh': '.~~2.11e-4' }, + { 'n': 56111n, 'rh': '.~~-229.9' }, + { 'n': 52968n, 'rh': '.~~-27.62' }, + { 'n': 5299n, 'rh': '.~~1.147e-3' }, + { 'n': 16599n, 'rh': '.~~2.42' }, + { 'n': 20880n, 'rh': '.~~44.5' }, + { 'n': 32827n, 'rh': '.~~-3.5e-6' }, + { 'n': 42018n, 'rh': '.~~-0.01614' }, + { 'n': 6518n, 'rh': '.~~2.666e-3' }, + { 'n': 5290n, 'rh': '.~~1.139e-3' }, + { 'n': 13267n, 'rh': '.~~0.2445' }, + { 'n': 55960n, 'rh': '.~~-211' }, + { 'n': 5180n, 'rh': '.~~1.034e-3' }, + { 'n': 57898n, 'rh': '.~~-789' }, + { 'n': 64269n, 'rh': '.~~-57760' }, + { 'n': 27075n, 'rh': '.~~2950' }, + { 'n': 52518n, 'rh': '.~~-20.6' }, + { 'n': 2871n, 'rh': '.~~2.202e-4' }, + { 'n': 14390n, 'rh': '.~~0.5264' }, + { 'n': 22479n, 'rh': '.~~124.94' }, + { 'n': 53512n, 'rh': '.~~-40.25' }, + // specials + { 'n': 32256n, 'rh': '.~~nan' }, + { 'n': 31744n, 'rh': '.~~inf' }, + { 'n': 64512n, 'rh': '.~~-inf' }, +]; +export const FLOAT_32_TESTS: { + n: bigint, + rs: string +}[] = [ + { 'n': 3801684100n, 'rs': '.-1.4120592e21' }, + { 'n': 3198448935n, 'rs': '.-0.3212063' }, + { 'n': 1493470325n, 'rs': '.2.3318207e15' }, + { 'n': 3753392125n, 'rs': '.-2.6548713e19' }, + { 'n': 2367701358n, 'rs': '.-4.9382565e-31' }, + { 'n': 3955998488n, 'rs': '.-4.926287e26' }, + { 'n': 3770285654n, 'rs': '.-1.0721795e20' }, + { 'n': 774658779n, 'rs': '.3.9188968e-11' }, + { 'n': 1839384350n, 'rs': '.6.297161e27' }, + { 'n': 3200958240n, 'rs': '.-0.39598942' }, + { 'n': 1531710513n, 'rs': '.5.74343e16' }, + { 'n': 76604734n, 'rs': '.3.4064763e-36' }, + { 'n': 3849641890n, 'rs': '.-7.227392e22' }, + { 'n': 449588378n, 'rs': '.8.444448e-23' }, + { 'n': 2200559883n, 'rs': '.-4.9922973e-37' }, + { 'n': 3233902359n, 'rs': '.-6.044811' }, + { 'n': 1826097792n, 'rs': '.2.0894205e27' }, + { 'n': 2038589591n, 'rs': '.8.463999e34' }, + { 'n': 374924072n, 'rs': '.1.7520019e-25' }, + { 'n': 2891424105n, 'rs': '.-3.0642938e-12' }, + { 'n': 1504861925n, 'rs': '.6.2758604e15' }, + // specials + { 'n': 2143289344n, 'rs': '.nan' }, + { 'n': 2139095040n, 'rs': '.inf' }, + { 'n': 4286578688n, 'rs': '.-inf' }, +]; +export const FLOAT_64_TESTS: { + n: bigint, + rd: string +}[] = [ + { 'n': 4161079059275835235n, 'rd': '.~1.5344573483999128e-30' }, + { 'n': 9946657041402327650n, 'rd': '.~-2.6040520237797157e-260' }, + { 'n': 10945498080022120923n, 'rd': '.~-1.5219889995240713e-193' }, + { 'n': 12777619852430681908n, 'rd': '.~-4.352347778495045e-71' }, + { 'n': 10832826134033707969n, 'rd': '.~-4.476445969104017e-201' }, + { 'n': 5759527968158150641n, 'rd': '.~1.083878940297261e77' }, + { 'n': 14210857183243570667n, 'rd': '.~-2.7934289125168545e25' }, + { 'n': 802481108141386491n, 'rd': '.~5.057760190438517e-255' }, + { 'n': 1275779858016506694n, 'rd': '.~2.2132215938493265e-223' }, + { 'n': 13095741331018331181n, 'rd': '.~-7.863102918443266e-50' }, + { 'n': 1888824041075399563n, 'rd': '.~2.1134816073641755e-182' }, + { 'n': 1084036463832721993n, 'rd': '.~3.350404450282384e-236' }, + { 'n': 5378671490392695147n, 'rd': '.~3.906026148423093e51' }, + { 'n': 6949924683075251658n, 'rd': '.~4.095814633904063e156' }, + { 'n': 11922669081154321283n, 'rd': '.~-3.149243846108954e-128' }, + { 'n': 3348628534449979563n, 'rd': '.~7.952461579092363e-85' }, + { 'n': 16740885817600377897n, 'rd': '.~-3.5722157011559137e194' }, + { 'n': 10947731899891474740n, 'rd': '.~-2.0655661021482118e-193' }, + { 'n': 5258441678658489674n, 'rd': '.~3.5873091546780243e43' }, + { 'n': 13356339406634822690n, 'rd': '.~-2.0992411948796013e-32' }, + { 'n': 5682954223851386361n, 'rd': '.~8.25703042630886e71' }, + // specials + { 'n': 9221120237041090560n, 'rd': '.~nan' }, + { 'n': 9218868437227405312n, 'rd': '.~inf' }, + { 'n': 18442240474082181120n, 'rd': '.~-inf' }, +]; +export const FLOAT_128_TESTS: { + n: bigint, + rq: string +}[] = [ + { 'n': 68021193693829353317549889929563141156n, 'rq': '.~~~7.350512667020030909706173370706225e-989' }, + { 'n': 302103353625220804986267666991697023063n, 'rq': '.~~~-7.9549197455275592244500492866747605e2718' }, + { 'n': 246898801142481942446083786230981830385n, 'rq': '.~~~-2.2259646667707415960533566169084728e-482' }, + { 'n': 266347262120684140485161040153719834628n, 'rq': '.~~~-8.279222993489825223543556036825673e645' }, + { 'n': 221468995426755939807482572792716215704n, 'rq': '.~~~-1.111792035542746634059251896548544e-1956' }, + { 'n': 21301800639651122378660853950525964078n, 'rq': '.~~~1.7726070616810188982779118724916924e-3697' }, + { 'n': 14097152844847057880436990765628471568n, 'rq': '.~~~3.369589719043255367636184354903706e-4115' }, + { 'n': 295177098639635213272177198060962589505n, 'rq': '.~~~-2.2206978503537075954826511134765822e2317' }, + { 'n': 110688872525300968280230769566478882214n, 'rq': '.~~~3.6397750043610061937155700733186537e1485' }, + { 'n': 61599026415237650695220681488049467053n, 'rq': '.~~~3.4057177992615989177915966678096206e-1361' }, + { 'n': 172863816977031872621427160265490779418n, 'rq': '.~~~-1.2556914150614657185267070080341778e-4774' }, + { 'n': 302266284239331446242693035229785100109n, 'rq': '.~~~-2.350663948127252855536381243313653e2728' }, + { 'n': 156268960646491515609436753671493095692n, 'rq': '.~~~1.38238485325181967793853192964508e4128' }, + { 'n': 108198906770347465279793905792970007495n, 'rq': '.~~~1.6574125869800981414994144332269465e1341' }, + { 'n': 267024674209818006533062290780699457496n, 'rq': '.~~~-1.50776910344360132341073895837773e685' }, + { 'n': 90468551431184434281606009464311982071n, 'rq': '.~~~1.8958834323964283970820423084762307e313' }, + { 'n': 142729305176167457500565258995430273841n, 'rq': '.~~~1.4432040214189287854740316305289527e3343' }, + { 'n': 130858994578269367391522528661145525849n, 'rq': '.~~~9.267801652640158022832285899427825e2654' }, + { 'n': 9047595725142263795917295497258429378n, 'rq': '.~~~6.26545010819892388144698108939643e-4408' }, + { 'n': 262829608976719100907507105384827825295n, 'rq': '.~~~-9.321356263350323349343422220817277e441' }, + { 'n': 197646596458388884873804679655774313010n, 'rq': '.~~~-8.161207090132586143046770776173326e-3338' }, + // specials + { 'n': 170138587312039964317873038467719495680n, 'rq': '.~~~nan' }, + { 'n': 170135991163610696904058773219554885632n, 'rq': '.~~~inf' }, + { 'n': 340277174624079928635746076935438991360n, 'rq': '.~~~-inf' }, +]; + +export const PHONETIC_AURAS: aura[] = [ 'p', 'q' ]; +export const PHONETIC_TESTS: { + n: bigint, + p: string, + q: string +}[] = [ + { 'n': 7n, 'p': '~let', 'q': '.~let' }, + { 'n': 0n, 'p': '~zod', 'q': '.~zod' }, + { 'n': 8n, 'p': '~ful', 'q': '.~ful' }, + { 'n': 117n, 'p': '~deg', 'q': '.~deg' }, + { 'n': 83n, 'p': '~tev', 'q': '.~tev' }, + { 'n': 223n, 'p': '~lud', 'q': '.~lud' }, + { 'n': 39995n, 'p': '~hapwyc', 'q': '.~hapwyc' }, + { 'n': 50426n, 'p': '~mitrep', 'q': '.~mitrep' }, + { 'n': 11415n, 'p': '~torryx', 'q': '.~torryx' }, + { 'n': 1863930458n, 'p': '~mogteg-botfex', 'q': '.~ligput-motfus' }, + { 'n': 3284934632n, 'p': '~loplet-nosnyx', 'q': '.~fasryd-mirlyn' }, + { 'n': 3833668n, 'p': '~nidmes-samrut', 'q': '.~sef-palsub' }, + { 'n': 9260427482306755094n, + 'p': '~lasrum-pindyt-nimnym-fotmeg', + 'q': '.~lasrum-pindyt-tadtem-lodlup', + }, + { 'n': 6363574354411289343n, + 'p': '~nopnet-rostem-navteb-fodbep', + 'q': '.~nopnet-rostem-nimfel-monfes', + }, + { 'n': 17571387016818844998n, + 'p': '~namler-folwet-bictes-wormec', + 'q': '.~namler-folwet-samwet-sarrul', + }, + { 'n': 241760151623976361741451001031931477015n, + 'p': '~dablys-minwed-mosreb-mictyn--nostyv-nimdul-hanbyl-bisdep', + 'q': '.~dablys-minwed-mosreb-mictyn-nostyv-nimdul-hanbyl-bisdep', + }, + { 'n': 148310954517291502180858368907816435627n, + 'p': '~ligryn-lomnem-fintes-davsyr--pacdel-wolpex-ripdev-paldeb', + 'q': '.~ligryn-lomnem-fintes-davsyr-pacdel-wolpex-ripdev-paldeb', + }, + { 'n': 97100713129464593177912155425728457718n, + 'p': '~tipwep-danner-minlyx-posned--mapmun-matlud-sitreb-balweg', + 'q': '.~tipwep-danner-minlyx-posned-mapmun-matlud-sitreb-balweg', + }, + // with zero bytes + { 'n': 3833668n, + 'p': '~nidmes-samrut', + 'q': '.~sef-palsub' + }, + { 'n': 773182725n, + 'p': '~dozreg-palfun', + 'q': '.~fospeg-fopper', + }, + { 'n': 319478973361751151n, + 'p': '~sampel-sampel-lacwyl-tirder', + 'q': '.~sampel-sampel-dozpel-sampel', + }, + { 'n': 319478973354476655n, + 'p': '~sampel-sampel-dozzod-sampel', + 'q': '.~sampel-sampel-dozzod-sampel', + }, + // absurdly long + { 'n': 0x7468697320697320736f6d6520766572792068696768207175616c69747920656e74726f7079n, + 'p': '~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud', + 'q': '.~divmes-davset-holdet-sallun-salpel-taswet-holtex-watmeb-tarlun-picdet-magmes-holter-dacruc-timdet-divtud-holwet-maldut-padpel-sivtud' + }, +]; + +export const DATE_AURAS: aura[] = [ 'da' ]; +export const DATE_TESTS: { + n: bigint, + da: string +}[] = [ + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.1.1' + }, + { 'n': 170141182164706681340023325049697075200n, + 'da': '~2000-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~1-.1.1' + }, + { 'n': 170213050367437966468743593413155225600n, + 'da': '~123456789.12.12' + }, + { 'n': 170141184507170056208381036660470579200n, + 'da': '~2025.1.1..01.00.00' + }, + { 'n': 170141184507169989800102652781061472256n, + 'da': '~2025.1.1..00.00.00..0001' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..07.07.07' + }, + { 'n': 170141184492616163050404573632566132736n, + 'da': '~2000.1.1..11.11.11' + }, + { 'n': 170141184492616163050404761352701739008n, + 'da': '~2000.1.1..11.11.11..0000.aabb' + }, + { 'n': 170141184492616163062707000439658774528n, + 'da': '~2000.1.1..11.11.11..aabb' + }, + { 'n': 170141184492615487727406687186543706111n, + 'da': '~2000.1.1..01.01.01..aabb.ccdd.eeff.ffff' + } +]; + +export const TIME_AURAS: aura[] = ['dr']; +export const TIME_TESTS: { + n: bigint, + dr: string +}[] = [ + { 'n': 0n, + 'dr': '~s0' + }, + { 'n': 281474976710656n, + 'dr': '~s0..0001' + }, + { 'n': 18446744073709551616n, + 'dr': '~s1' + }, + { 'n': 1661332218022355928088576n, + 'dr': '~d1.h1.m1.s1' + }, + { 'n': 1968341379640822520656953344n, + 'dr': '~d1234.h23.m59.s59..ffff' + }, + { 'n': 1968341361194641392605855744n, + 'dr': '~d1234.h23.m59.s59..0000.ffff' + }, +] + +export const TEXT_AURAS: aura[] = [ 't' ]; +export const TEXT_TESTS: { + n: bigint, + t: string, + ta: string, + tas: string, +}[] = [ + { n: 0n, + tas: '', + ta: '~.', + t: '~~' + }, + { n: 97n, + tas: 'a', + ta: '~.a', + t: '~~a' + }, + { n: 121404708502375659064812904n, + tas: 'hello-world', + ta: '~.hello-world', + t: '~~hello-world' + }, + { n: 10334410032597741434076685640n, + tas: 'Hello World!', + ta: '~.Hello World!', + t: '~~~48.ello.~57.orld~21.' + }, + { n: 19287354765356410753152687274314809137704681783870144927392762099049413935995106n, + tas: 'โ˜…๐Ÿค yeeุŒhaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + ta: '~.โ˜…๐Ÿค yeeุŒhaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + t: '~~~2605.~1f920.yee~60c.haw~1f468.~200d.~1f467.~200d.~1f466.' + } +]; + +export const CHAR_AURAS: aura[] = [ 'c' ]; +export const CHAR_TESTS: { n: bigint, c: string }[] = [ + { n: 129312n, c: '~-~1f920.' }, + { n: 128104n, c: '~-~1f468.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128103n, c: '~-~1f467.' }, + { n: 8205n, c: '~-~200d.' }, + { n: 128102n, c: '~-~1f466.' }, + { n: 97n, c: '~-a' }, + { n: 33n, c: '~-~21.' }, + { n: 32n, c: '~-.' }, + { n: 126n, c: '~-~~' }, + { n: 46n, c: '~-~.' }, + { n: 1548n, c: '~-~60c.' }, + // the cases below are deranged, because the input is deranged. + // @c represents utf-32 codepoints, so if you give it not-utf-32 + // it will render bogus, drop bytes in the rendering, etc. + // we include them (disabled) here to indicate that we don't have 100% + // exact stdlib parity, but in practice that shouldn't matter. + // { n: 478560413032n, c: '~-~c6568.o' }, // 'hello' + // { n: 36762444129640n, c: '~-~c6568.~216f.' } // 'hello!' +]; diff --git a/test/fuzz.test.ts b/test/fuzz.test.ts new file mode 100644 index 0000000..c252ea7 --- /dev/null +++ b/test/fuzz.test.ts @@ -0,0 +1,97 @@ +import { aura } from '../src/types'; +import { tryParse as parse } from "../src/parse"; +import render from '../src/render'; +import { webcrypto } from 'crypto'; + +// tests per bit-size +const testCount = 50; + +const simpleAuras: aura[] = [ + 'c', + 'da', + 'dr', + // 'f', // limited legitimate values + // 'if', // won't round-trip above "native" size limit + // 'is', // won't round-trip above "native" size limit + // 'n', // limited legitimate values + 'p', + 'q', + // 'rh', // strict size limits, and NaN usually won't round-trip + // 'rd', // + // 'rq', // + // 'rs', // + 'sb', + 'sd', + 'si', + 'sv', + 'sw', + 'sx', + // 't', // stdlib also crashes on rendering arbitrary bytes + // 'ta', // + // 'tas', // + 'ub', + // 'uc', //TODO unsupported + 'ud', + 'ui', + 'uv', + 'uw', + 'ux' +] + +function fuzz(minBits: number, maxBits: number, auras: aura[], f: (n:bigint)=>bigint = (n)=>n) { + describe(minBits + 'โ€”' + maxBits + '-bit values', () => { + const tests: { [bits: number]: bigint[] } = {}; + for (let bits = minBits; bits <= maxBits; bits++) { + const parts = Math.ceil(bits / 64); + const src = bits <= 8 ? new Uint8Array(testCount) + : bits <= 16 ? new Uint16Array(testCount) + : bits <= 32 ? new Uint32Array(testCount) + : bits <= 64 ? new BigUint64Array(testCount) + : new BigUint64Array(testCount * parts); + webcrypto.getRandomValues(src); + const arr: bigint[] = []; + if (bits <= 64) { + src.forEach((n) => arr.push(f(BigInt(n)))); + } else { + const mask = (1n << BigInt(bits % 64)) - 1n; + for (let t = 0; t < testCount; t+=parts) { + let num = (src[t] as bigint) & mask; + for (let p = 1; p < parts; p++) { + num = (num << 64n) | (src[t+p] as bigint); + } + arr.push(f(num)); + } + } + tests[bits] = arr; + } + auras.forEach((a) => { + it(a + ' round-trips losslessly', () => { + for (let bits = minBits; bits <= maxBits; bits++) { + tests[bits].forEach((n) => { + n = BigInt(n); + expect(parse(a, render(a, n))).toEqual(n); + }); + } + }); + }); + }); +} + +fuzz( 1, 32, [...simpleAuras, 'if', 'is']); +fuzz(33, 64, [...simpleAuras, 'is']); +fuzz(65, 128, simpleAuras); + +// for floats, avoid NaNs, whose payload gets discarded during rendering, +// and as such usually won't round-trip cleanly. +// NaNs are encoded as "exponent bits all 1, non-zero significand", +// so we hard-set one pseudo-random exponent bit to 0 +function safeFloat(size: bigint, w: bigint, p: bigint) { + const full = (1n << size) - 1n; + const makemask = (n: bigint) => full ^ (1n << (p + (n % w))); + return (n: bigint) => n & makemask(n); +} +//TODO tests fail, off-by-one... +// fuzz(4, 16, ['rh'], safeFloat( 16n, 5n, 10n)); +// fuzz(4, 32, ['rs'], safeFloat( 32n, 8n, 23n)); +// fuzz(4, 64, ['rd'], safeFloat( 64n, 11n, 52n)); +// fuzz(4, 128, ['rq'], safeFloat(128n, 15n, 112n)); diff --git a/test/p.test.ts b/test/p.test.ts index b901ef9..8295a43 100644 --- a/test/p.test.ts +++ b/test/p.test.ts @@ -1,122 +1,31 @@ -import jsc from 'jsverify'; import { - patp, - patp2hex, - hex2patp, - patp2dec, - clan, + kind, sein, - isValidPatp, - preSig, - deSig, + isValidP, cite, -} from '../src'; - -const patps = jsc.uint32.smap( - (num) => patp(num), - (pp) => parseInt(patp2dec(pp)) -); +} from '../src/p'; describe('@p', () => { - it('patp2dec matches expected reference values', () => { - expect(patp2dec('~zod')).toEqual('0'); - expect(patp2dec('~lex')).toEqual('200'); - expect(patp2dec('~binzod')).toEqual('512'); - expect(patp2dec('~samzod')).toEqual('1024'); - expect(patp2dec('~poldec-tonteg')).toEqual('9896704'); - expect(patp2dec('~nidsut-tomdun')).toEqual('15663360'); - expect(patp2dec('~morlyd-mogmev')).toEqual('3108299008'); - expect(patp2dec('~fipfes-morlyd')).toEqual('479733505'); - }); - - it('patp matches expected reference values', () => { - expect(patp('0')).toEqual('~zod'); - expect(patp('200')).toEqual('~lex'); - expect(patp('512')).toEqual('~binzod'); - expect(patp('1024')).toEqual('~samzod'); - expect(patp('9896704')).toEqual('~poldec-tonteg'); - expect(patp('15663360')).toEqual('~nidsut-tomdun'); - expect(patp('3108299008')).toEqual('~morlyd-mogmev'); - expect(patp('479733505')).toEqual('~fipfes-morlyd'); - }); - - it('large patp values match expected reference values', () => { - expect( - hex2patp( - '7468697320697320736f6d6520766572792068696768207175616c69747920656e74726f7079' - ) - ).toEqual( - '~divmes-davset-holdet--sallun-salpel-taswet-holtex--watmeb-tarlun-picdet-magmes--holter-dacruc-timdet-divtud--holwet-maldut-padpel-sivtud' - ); - }); - - it('patp throws on null input', () => { - let input = () => patp(null as any); - expect(input).toThrow(); - }); - - it('hex2patp throws on null input', () => { - let input = () => hex2patp(null as any); - expect(input).toThrow(); - }); - - it('patp2hex throws on invalid patp', () => { - let input = () => patp2hex('nidsut-tomdun'); - expect(input).toThrow(); - input = () => patp2hex('~nidsut-tomdzn'); - expect(input).toThrow(); - input = () => patp2hex('~sut-tomdun'); - expect(input).toThrow(); - input = () => patp2hex('~nidsut-dun'); - expect(input).toThrow(); - input = () => patp2hex(null as any); - expect(input).toThrow(); - }); - - it('patp and patp2dec are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patp2dec(patp(num))) === num - ); - - let iso1 = jsc.forall(patps, (pp) => patp(patp2dec(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - - it('patp2hex and hex2patp are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patp2hex(hex2patp(num.toString(16))), 16) === num - ); - - let iso1 = jsc.forall(patps, (pp) => hex2patp(patp2hex(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - describe('clan/sein', () => { - it('clan works as expected', () => { - expect(clan('~zod')).toEqual('galaxy'); - expect(clan('~fes')).toEqual('galaxy'); - expect(clan('~marzod')).toEqual('star'); - expect(clan('~fassec')).toEqual('star'); - expect(clan('~dacsem-fipwex')).toEqual('planet'); - expect(clan('~fidnum-rosbyt')).toEqual('planet'); - expect(clan('~doznec-bannux-nopfen')).toEqual('moon'); - expect(clan('~dozryt--wolmep-racmyl-padpeg-mocryp')).toEqual('comet'); + it('clan/kind works as expected', () => { + expect(kind('~zod')).toEqual('galaxy'); + expect(kind('~fes')).toEqual('galaxy'); + expect(kind('~marzod')).toEqual('star'); + expect(kind('~fassec')).toEqual('star'); + expect(kind('~dacsem-fipwex')).toEqual('planet'); + expect(kind('~fidnum-rosbyt')).toEqual('planet'); + expect(kind('~doznec-bannux-nopfen')).toEqual('moon'); + expect(kind('~dozryt--wolmep-racmyl-padpeg-mocryp')).toEqual('comet'); }); it('clan throws on invalid input', () => { - let input = () => clan('~zord'); + let input = () => kind('~zord'); expect(input).toThrow(); - input = () => clan('zod'); + input = () => kind('zod'); expect(input).toThrow(); - input = () => clan('~nid-tomdun'); + input = () => kind('~nid-tomdun'); expect(input).toThrow(); - input = () => clan(null as any); + input = () => kind(null as any); expect(input).toThrow(); }); @@ -129,6 +38,7 @@ describe('@p', () => { expect(sein('~fassec')).toEqual('~sec'); expect(sein('~nidsut-tomdun')).toEqual('~marzod'); expect(sein('~sansym-ribnux')).toEqual('~marnec'); + expect(sein('~sampel-sampel-sampel-sampel--sampel-sampel-sansym-ribnux')).toEqual('~ribnux'); }); it('sein throws on invalid input', () => { @@ -138,65 +48,29 @@ describe('@p', () => { expect(input).toThrow(); input = () => sein('~nid-tomdun'); expect(input).toThrow(); - input = () => sein(null as any); - expect(input).toThrow(); }); }); - describe('isValidPatp', () => { - it('isValidPatp returns true for valid @p values', () => { - expect(isValidPatp('~zod')).toEqual(true); - expect(isValidPatp('~marzod')).toEqual(true); - expect(isValidPatp('~nidsut-tomdun')).toEqual(true); + describe('isValidP', () => { + it('isValidP returns true for valid @p values', () => { + expect(isValidP('~zod')).toEqual(true); + expect(isValidP('~marzod')).toEqual(true); + expect(isValidP('~nidsut-tomdun')).toEqual(true); }); - it('isValidPatp returns false for invalid @p values', () => { - expect(isValidPatp('')).toEqual(false); - expect(isValidPatp('~')).toEqual(false); - expect(isValidPatp('~hu')).toEqual(false); - expect(isValidPatp('~what')).toEqual(false); - expect(isValidPatp('sudnit-duntom')).toEqual(false); - }); - }); - - describe('preSig', () => { - it('preSig adds a sig if missing', () => { - expect(preSig('nocsyx-lassul')).toEqual('~nocsyx-lassul'); - }); - - it('preSig ignores sig if there already', () => { - expect(preSig('~nocsyx-lassul')).toEqual('~nocsyx-lassul'); - }); - - it('preSig ignores whitespace', () => { - expect(preSig(' nocsyx-lassul')).toEqual('~nocsyx-lassul'); - }); - - it('preSig ignores empty values safely', () => { - expect(preSig(null as any)).toEqual(''); - expect(() => preSig(null as any)).not.toThrow(); - }); - }); - - describe('deSig', () => { - it('deSig removes the sig if present', () => { - expect(deSig('~nocsyx-lassul')).toEqual('nocsyx-lassul'); - }); - - it('deSig ignores if no sig', () => { - expect(deSig('nocsyx-lassul')).toEqual('nocsyx-lassul'); - }); - - it('deSig ignores empty values safely', () => { - expect(deSig(null as any)).toEqual(''); - expect(() => deSig(null as any)).not.toThrow(); + it('isValidP returns false for invalid @p values', () => { + expect(isValidP('')).toEqual(false); + expect(isValidP('~')).toEqual(false); + expect(isValidP('~hu')).toEqual(false); + expect(isValidP('~what')).toEqual(false); + expect(isValidP('sudnit-duntom')).toEqual(false); }); }); describe('cite', () => { it('cite shortens moons', () => { expect(cite('~mister-mister-nocsyx-lassul')).toEqual('~nocsyx^lassul'); - expect(cite('~dozzod-dozzod-nocsyx-lassul')).toEqual('~nocsyx^lassul'); + expect(cite('~binzod-dozzod-nocsyx-lassul')).toEqual('~nocsyx^lassul'); }); it('cite shortens comets', () => { @@ -212,25 +86,11 @@ describe('@p', () => { expect(cite('~zod')).toEqual('~zod'); expect(cite('~marzod')).toEqual('~marzod'); expect(cite('~marzod-marzod')).toEqual('~marzod-marzod'); - expect(cite('~marzod-marzod-marzod')).toEqual('~marzod-marzod-marzod'); }); it('cite always returns sigged values', () => { expect(cite('~zod')?.at(0)).toEqual('~'); expect(cite('~zod')).toEqual('~zod'); }); - - it('cite accepts unsigged values', () => { - expect(cite('zod')).toEqual('~zod'); - expect(cite('mister-mister-nocsyx-lassul')).toEqual('~nocsyx^lassul'); - expect( - cite('roldex-navmev-biltyp-dozzod--dozzod-lapled-binnum-binzod') - ).toEqual('~roldex_binzod'); - }); - - it('cite ignores empty values safely', () => { - expect(cite(null as any)).toEqual(null); - expect(() => cite(null as any)).not.toThrow(); - }); }); }); diff --git a/test/parse.test.ts b/test/parse.test.ts new file mode 100644 index 0000000..e880a73 --- /dev/null +++ b/test/parse.test.ts @@ -0,0 +1,206 @@ +import { aura } from '../src/types'; +import { tryParse as parse, decodeString, nuck, regex } from '../src/parse'; +import { INTEGER_AURAS, INTEGER_TESTS, + IPV4_TESTS, IPV6_TESTS, + FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, + PHONETIC_AURAS, PHONETIC_TESTS, + DATE_AURAS, DATE_TESTS, + TIME_AURAS, TIME_TESTS, + TEXT_AURAS, TEXT_TESTS, + CHAR_AURAS, CHAR_TESTS, + } from './data/atoms'; + +//TODO test for parse failures: leading zeroes, date out of range, etc +//TODO test for non-standard-but-accepted cases: leading 0 in hex chars, weird dates, etc. + +describe('limited auras', () => { + describe(`@n parsing`, () => { + it('parses', () => { + const res = parse('n', '~'); + expect(res).toEqual(0n); + }); + }); + describe(`@f parsing`, () => { + it('parses', () => { + const yea = parse('f', '.y'); + expect(yea).toEqual(0n); + const nay = parse('f', '.n'); + expect(nay).toEqual(1n); + }); + }); +}); + +function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { + describe(`${desc} auras`, () => { + auras.map((a) => { + describe(`@${a} parsing`, () => { + tests.map((test) => { + // @ts-ignore we know this is sane/safe + const str = test[a]; + describe(str, () => { + it('matches regex', () => { + expect(regex[a].test(str)).toEqual(true); + }); + it('parses', () => { + const res = parse(a, str); + expect(res).toEqual(test.n); + }); + }); + }); + }); + }); + }); +} + +//NOTE we add additional tests to the data: +// parsing must be liberal and accept formats +// which the renderer would never produce +const OUR_DATE_TESTS: { + n: bigint, + da: string +}[] = [ + ...DATE_TESTS, + { 'n': 170141183328369385600900416699944140800n, + 'da': '~0.1.1' + }, + { 'n': 170141184492712641901540060096049971200n, + 'da': '~2000.2.31' + }, + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.01.01' + }, + { 'n': 170141184492615420181573981275213004800n, + 'da': '~2000.001.001' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..7.7.7' + }, + { 'n': 170141184492615892916284358229892268032n, + 'da': '~2000.1.1..007.007.007' + }, + { 'n': 170141184492622106013428863442102517760n, + 'da': '~2000.1.1..99.99.99..abcd' + }, + { 'n': 170141184492616163050404573632566132736n, + 'da': '~2000.1.1..11.11.11..0000' + }, + { 'n': 170141184492616163062707000439658774528n, + 'da': '~2000.1.1..11.11.11..aabb.0000' + } +]; +const OUR_TIME_TESTS: { + n: bigint, + dr: string +}[] = [ + ...TIME_TESTS, + { 'n': 9979688543876867424256n, + 'dr': '~m1.m5.s1.m3' + }, + { 'n': 18446744073709551616n, + 'dr': '~s1..0000' + }, + { 'n': 18446744073709551616n, + 'dr': '~s1..0000.0000' + }, + { 'n': 18446744073709551616n, + 'dr': '~d0.h0.s1.m0' + }, + { 'n': 69059795211765323057332224n, + 'dr': '~d1.h999.m999.s999' + }, +] + +testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('ipv4', ['if'], IPV4_TESTS); +testAuras('ipv6', ['is'], IPV6_TESTS); +testAuras('float16', ['rh'], FLOAT_16_TESTS); +testAuras('float32', ['rs'], FLOAT_32_TESTS); +testAuras('float64', ['rd'], FLOAT_64_TESTS); +testAuras('float128', ['rq'], FLOAT_128_TESTS); +testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); +testAuras('date', DATE_AURAS, OUR_DATE_TESTS); +testAuras('time', TIME_AURAS, OUR_TIME_TESTS); +testAuras('text', [ 't' ], TEXT_TESTS); +testAuras('chars', CHAR_AURAS, CHAR_TESTS); + +describe('string decoding', () => { + it('decodes', () => { + expect(decodeString('a~62.c')).toEqual('abc'); + expect(decodeString('a~0a.c')).toEqual('a\nc'); + expect(decodeString('~2605.~1f920.yeehaw~1f468.~200d.~1f467.~200d.~1f466.')).toEqual('โ˜…๐Ÿค yeehaw๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'); + }); +}); + +describe('blob parsing', () => { + it('parses', () => { + expect(nuck('~02')).toEqual({ type: 'blob', jam: 2n }); + expect(nuck('~097su1g7hk1')).toEqual({ type: 'blob', jam: 325350265702017n }); + }); +}); + +describe('many parsing', () => { + it('parses', () => { + expect(nuck('.__')).toEqual({ type: 'many', list: [] }); + expect( + nuck('._123__') + ).toEqual( + { type: 'many', list: [ { type: 'dime', aura: 'ud', atom: 123n }, ] } + ); + expect( + nuck('._~~~~a~~42.c_123_~~01hu32s3gu1_.~-1~-2~-~-__') + ).toEqual({ type: 'many', list: [ + { type: 'dime', aura: 't', atom: 6505057n }, + { type: 'dime', aura: 'ud', atom: 123n }, + { type: 'blob', jam: 54910179722177n }, + { type: 'many', list: [ + { type: 'dime', aura: 'ud', atom: 1n }, + { type: 'dime', aura: 'ud', atom: 2n } + ] } + ] }); + }); +}); + +describe('invalid syntax', () => { + it('fails leading zeroes', () => { + expect(nuck('00')).toEqual(null); + expect(nuck('01')).toEqual(null); + expect(nuck('0b01')).toEqual(null); + }); + it('fails incomplete atoms', () => { + expect(nuck('~0')).toEqual(null); + expect(nuck('~2000.1')).toEqual(null); + }); + it('fails unescaped characters', () =>{ + expect(nuck('~.aBc')).toEqual(null); + expect(nuck('._~zod__')).toEqual(null); + expect(nuck('.123__')).toEqual(null); + }); + it('fails bogus dates', () => { + expect(nuck('~2025.1.0')).toEqual(null); + expect(nuck('~2025.0.1')).toEqual(null); + expect(nuck('~2025.13.1')).toEqual(null); + }); + it('fails bogus @p or @q', () => { + expect(nuck('~zad')).toEqual(null); + expect(nuck('.~zad')).toEqual(null); + expect(nuck('~zodbin')).toEqual(null); + expect(nuck('~dozzod')).toEqual(null); + expect(nuck('.~zodbin')).toEqual(null); + expect(nuck('~funpal')).toEqual(null); + expect(nuck('.~funpal')).toEqual(null); + expect(nuck('~nidsut-dun')).toEqual(null); + expect(nuck('.~nidsut-dun')).toEqual(null); + expect(nuck('~mister--dister')).toEqual(null); + expect(nuck('.~mister--dister')).toEqual(null); + }); +}); + +describe('oversized inputs', () => { + it('parses oversized @if', () => { + expect(parse('if', '.255.0.0.999')).toEqual(0xff0003e7n); + expect(parse('if', '.255.0.1.999')).toEqual(0xff0004e7n); + expect(parse('if', '.256.0.0.255')).toEqual(0x1000000ffn); + }); +}); + +//TODO oversized floats diff --git a/test/q.test.ts b/test/q.test.ts index 33cd8e3..ad62214 100644 --- a/test/q.test.ts +++ b/test/q.test.ts @@ -1,107 +1,20 @@ -import jsc from 'jsverify'; -import { - patq, - patq2hex, - hex2patq, - patq2dec, - eqPatq, - isValidPatq, -} from '../src'; - -const patqs = jsc.uint32.smap( - (num) => patq(num), - (pq) => parseInt(patq2dec(pq)) -); +import { isValidQ } from '../src/q'; describe('patq, etc.', () => { - it('patq2dec matches expected reference values', () => { - expect(patq2dec('~zod')).toEqual('0'); - expect(patq2dec('~binzod')).toEqual('512'); - expect(patq2dec('~samzod')).toEqual('1024'); - expect(patq2dec('~poldec-tonteg')).toEqual('4016240379'); - expect(patq2dec('~nidsut-tomdun')).toEqual('1208402137'); - }); - - it('patq matches expected reference values', () => { - expect(patq('0')).toEqual('~zod'); - expect(patq('512')).toEqual('~binzod'); - expect(patq('1024')).toEqual('~samzod'); - expect(patq('4016240379')).toEqual('~poldec-tonteg'); - expect(patq('1208402137')).toEqual('~nidsut-tomdun'); - }); - - it('large patq values match expected reference values', () => { - expect(hex2patq('01010101010101010102')).toEqual( - '~marnec-marnec-marnec-marnec-marbud' - ); - expect( - hex2patq( - '6d7920617765736f6d65207572626974207469636b65742c206920616d20736f206c75636b79' - ) - ).toEqual( - '~tastud-holruc-sidwet-salpel-taswet-holdeg-paddec-davdut-holdut-davwex-balwet-divwen-holdet-holruc-taslun-salpel-holtux-dacwex-baltud' - ); - }); - - it('patq2hex throws on invalid patp', () => { - let input = () => patq2hex('nidsut-tomdun'); - expect(input).toThrow(); - input = () => patq2hex('~nidsut-tomdzn'); - expect(input).toThrow(); - input = () => patq2hex('~sut-tomdun'); - expect(input).toThrow(); - input = () => patq2hex('~nidsut-dun'); - expect(input).toThrow(); - input = () => patq2hex(null as any); - expect(input).toThrow(); - }); - - it('patq and patq2dec are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patq2dec(patq(num))) === num - ); - - let iso1 = jsc.forall(patqs, (pp) => patq(patq2dec(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - - it('patq2hex and hex2patq are inverses', () => { - let iso0 = jsc.forall( - jsc.uint32, - (num) => parseInt(patq2hex(hex2patq(num.toString(16))), 16) === num - ); - - let iso1 = jsc.forall(patqs, (pp) => hex2patq(patq2hex(pp)) === pp); - - jsc.assert(iso0); - jsc.assert(iso1); - }); - - describe('eqPatq', () => { - it('works as expected', () => { - expect(eqPatq('~dozzod-dozzod', '~zod')).toEqual(true); - expect(eqPatq('~dozzod-mardun', '~mardun')).toEqual(true); - expect(eqPatq('~dozzod-mardun', '~mardun-dozzod')).toEqual(false); - }); - }); - - describe('isValidPatq', () => { - it('isValidPatq returns true for valid @p values', () => { - expect(isValidPatq('~zod')).toEqual(true); - expect(isValidPatq('~marzod')).toEqual(true); - expect(isValidPatq('~nidsut-tomdun')).toEqual(true); - expect(isValidPatq('~dozzod-binwes-nidsut-tomdun')).toEqual(true); + describe('isValidQ', () => { + it('isValidQ returns true for valid @p values', () => { + expect(isValidQ('.~zod')).toEqual(true); + expect(isValidQ('.~marzod')).toEqual(true); + expect(isValidQ('.~nidsut-tomdun')).toEqual(true); + expect(isValidQ('.~dozzod-binwes-nidsut-tomdun')).toEqual(true); }); - it('isValidPatq returns false for invalid @p values', () => { - expect(isValidPatq('')).toEqual(false); - expect(isValidPatq('~')).toEqual(false); - expect(isValidPatq('~hu')).toEqual(false); - expect(isValidPatq('~what')).toEqual(false); - expect(isValidPatq('sudnit-duntom')).toEqual(false); + it('isValidQ returns false for invalid @p values', () => { + expect(isValidQ('')).toEqual(false); + expect(isValidQ('.~')).toEqual(false); + expect(isValidQ('.~hu')).toEqual(false); + expect(isValidQ('.~what')).toEqual(false); + expect(isValidQ('sudnit-duntom')).toEqual(false); }); }); }); diff --git a/test/render.test.ts b/test/render.test.ts new file mode 100644 index 0000000..c72a4c0 --- /dev/null +++ b/test/render.test.ts @@ -0,0 +1,105 @@ +import { aura, coin } from '../src/types'; +import { render, rend } from '../src/render'; +import { INTEGER_AURAS, INTEGER_TESTS, + IPV4_TESTS, IPV6_TESTS, + FLOAT_16_TESTS, FLOAT_32_TESTS, FLOAT_64_TESTS, FLOAT_128_TESTS, + PHONETIC_AURAS, PHONETIC_TESTS, + DATE_AURAS, DATE_TESTS, + TIME_AURAS, TIME_TESTS, + TEXT_AURAS, TEXT_TESTS, + CHAR_AURAS, CHAR_TESTS, + } from './data/atoms'; + +describe('limited auras', () => { + describe(`@n rendering`, () => { + it('renders', () => { + const gud = render('n', 0n); + expect(gud).toEqual('~'); + const bad = render('n', 1n); + expect(bad).toEqual('~'); + }); + }); + describe(`@f rendering`, () => { + it('renders', () => { + const yea = render('f', 0n); + expect(yea).toEqual('.y'); + const nay = render('f', 1n); + expect(nay).toEqual('.n'); + const bad = render('f', 2n); + expect(bad).toEqual('0x2'); + }); + }); +}); + +function testAuras(desc: string, auras: aura[], tests: { n: bigint }[]) { + describe(`${desc} auras`, () => { + auras.map((a) => { + describe(`@${a} rendering`, () => { + tests.map((test) => { + // @ts-ignore we know this is sane/safe + describe(test[a], () => { + it('renders', () => { + const res = render(a, test.n); + // @ts-ignore we know this is sane/safe + expect(res).toEqual(test[a]); + }); + }); + }); + }); + }); + }); +} + +testAuras('integer', INTEGER_AURAS, INTEGER_TESTS); +testAuras('ipv4', ['if'], IPV4_TESTS); +testAuras('ipv6', ['is'], IPV6_TESTS); +testAuras('float16', ['rh'], FLOAT_16_TESTS); +testAuras('float32', ['rs'], FLOAT_32_TESTS); +testAuras('float64', ['rd'], FLOAT_64_TESTS); +testAuras('float128', ['rq'], FLOAT_128_TESTS); +testAuras('phonetic', PHONETIC_AURAS, PHONETIC_TESTS); +testAuras('date', DATE_AURAS, DATE_TESTS); +testAuras('time', TIME_AURAS, TIME_TESTS); +testAuras('text', TEXT_AURAS, TEXT_TESTS); +testAuras('chars', CHAR_AURAS, CHAR_TESTS); + +const MANY_COINS: { + coin: coin, + out: string +}[] = [ + { coin: { type: 'many', list: [] }, + out: '.__' + }, + { coin: { type: 'many', list: [ { type: 'many', list: [] } ] }, + out: '._.~-~-__' + }, + { coin: { type: 'many', list: [ { type: 'dime', aura: 'p', atom: 0n }, { type: 'dime', aura: 'ux', atom: 0x1234abcdn } ] }, + out: '._~~zod_0x1234.abcd__' + }, +] +describe('%many coin rendering', () => { + MANY_COINS.map((test) => { + describe(test.out, () => { + it('renders', () => { + const res = rend(test.coin); + expect(res).toEqual(test.out); + }); + }); + }); +}); + +describe('blob rendering', () => { + it('parses', () => { + expect(rend({ type: 'blob', jam: 2n })).toEqual('~02'); + expect(rend({ type: 'blob', jam: 325350265702017n })).toEqual('~097su1g7hk1'); + }); +}); + +describe('oversized inputs', () => { + it('truncates oversized @if', () => { + expect(render('if', 0x1000000ffn)).toEqual('.0.0.0.255'); + }); + it('truncates oversized @is', () => { + expect(render('is', 0xaaaaffff000000000000000000000000ffffn)).toEqual('.ffff.0.0.0.0.0.0.ffff'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0b99bf5..8978696 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,6 @@ "compilerOptions": { "module": "esnext", "moduleResolution": "nodenext", - "target": "es2020" + "target": "es2021" } }