UNPKG

@zsnout/ithkuil

Version:

A set of tools which can generate and parse romanized Ithkuil text and which can generate Ithkuil script from text and JSON data.

439 lines (438 loc) 16.3 kB
import { ALL_MOOD_OR_CASE_SCOPES } from "../generate/formative/slot-8/mood-or-case-scope.js"; import { ALL_AFFILIATIONS, ALL_AFFIXUAL_ADJUNCT_SCOPES, ALL_ASPECTS, ALL_BIAS_ADJUNCTS, ALL_CASES, ALL_CASE_SCOPES, ALL_CONFIGURATIONS, ALL_CONTEXTS, ALL_EFFECTS, ALL_ESSENCES, ALL_EXTENSIONS, ALL_FUNCTIONS, ALL_ILLOCUTION_OR_VALIDATIONS, ALL_LEVELS, ALL_MODULAR_ADJUNCT_SCOPES, ALL_MODULAR_ADJUNCT_TYPES, ALL_MOODS, ALL_PARSING_ADJUNCTS, ALL_PERSPECTIVES, ALL_PHASES, ALL_SINGLE_REGISTER_ADJUNCTS, ALL_SPECIFICATIONS, ALL_SUPPLETIVE_ADJUNCT_TYPES, ALL_VALENCES, wordToIthkuil, } from "../generate/index.js"; import { ALL_REFERENTS } from "../generate/referential/referent/referent.js"; import { testNeo } from "../parse/formative/indexneo.js"; import { parseWord, transformWord } from "../parse/index.js"; function genTests(numberOfTestCases, mode) { function randomItem(object) { if (object.length == 0) { throw new Error("Not enough items to pick a value from."); } const value = object[Math.floor(Math.random() * object.length)]; return value; } function biasedRandomItem(defaultValue, object) { if (mode == "full") { return randomItem([defaultValue, ...object]); } if (Math.random() < 0.9) { return defaultValue; } return randomItem(object); } function randomCA() { return { affiliation: biasedRandomItem("CSL", ALL_AFFILIATIONS), configuration: biasedRandomItem("UPX", ALL_CONFIGURATIONS), extension: biasedRandomItem("DEL", ALL_EXTENSIONS), perspective: biasedRandomItem("M", ALL_PERSPECTIVES), essence: biasedRandomItem("NRM", ALL_ESSENCES), }; } function randomAffix() { if (Math.random() < 0.4) { return { cs: randomLetterSeries(3), type: randomItem([1, 2]), degree: randomItem([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), }; } if (Math.random() < 0.4) { return { case: randomItem(ALL_CASES), }; } if (Math.random() < 0.5) { return { case: randomItem(ALL_CASES), isInverse: randomItem([true, false]), type: randomItem([1, 2]), }; } return { ca: randomCA() }; } function randomAffixList(force = false) { const output = []; while (true) { if ((output.length == 0 ? !force : true) && Math.random() < 0.5) { return output; } output.push(randomAffix()); } } function randomLetterSeries(maxLength) { let output = ""; for (let index = 0; index < maxLength; index++) { const char = randomItem("pbtdkgfvţḑszšžxcżčjmnňrlř"); if (char == output.slice(-1)) { index--; continue; } output += char; if (Math.random() < 0.75) { break; } } return output; } const randomVN = () => biasedRandomItem("MNO", randomItem([ ALL_VALENCES, ALL_PHASES, ALL_EFFECTS, ALL_LEVELS, ALL_ASPECTS, ])); const randomNonAspectualVN = () => biasedRandomItem("MNO", randomItem([ALL_VALENCES, ALL_PHASES, ALL_EFFECTS, ALL_LEVELS])); function randomFormative() { const root = Math.random() < 0.1 ? Math.random() < 0.5 ? BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) : Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) : Math.random() < 0.1 ? [randomItem(ALL_REFERENTS)] : Math.random() < 0.1 ? { cs: Math.random() < 0.1 ? Math.random() < 0.5 ? BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) : Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) : randomLetterSeries(5), degree: randomItem([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), } : randomLetterSeries(5); if (Math.random() < 30 / numberOfTestCases) { return { type: randomItem(["UNF/C", "UNF/K", "FRM"]), root, }; } const ca = randomCA(); return { type: biasedRandomItem("UNF/C", ["UNF/K", "FRM"]), concatenationType: biasedRandomItem("none", [1, 2]), shortcut: true, version: biasedRandomItem("PRC", ["CPT"]), stem: biasedRandomItem(1, [2, 3, 0]), root, get rootType() { return (Array.isArray(this.root) ? "referential" : typeof this.root == "object" ? "affix" : "standard"); }, function: biasedRandomItem("STA", ALL_FUNCTIONS), specification: biasedRandomItem("BSC", ALL_SPECIFICATIONS), context: biasedRandomItem("EXS", ALL_CONTEXTS), slotVAffixes: mode == "short" ? undefined : randomAffixList(), ca, ...ca, slotVIIAffixes: mode == "short" ? undefined : randomAffixList(), vn: mode == "short" ? biasedRandomItem("MNO", randomItem([ ALL_VALENCES, ALL_PHASES, ALL_EFFECTS, ALL_LEVELS, ALL_ASPECTS, ])) : randomItem(randomItem([ ALL_VALENCES, ALL_PHASES, ALL_EFFECTS, ALL_LEVELS, ALL_ASPECTS, ])), caseScope: biasedRandomItem("CCN", ALL_CASE_SCOPES), mood: biasedRandomItem("FAC", ALL_MOODS), case: biasedRandomItem("THM", ALL_CASES), illocutionValidation: biasedRandomItem("OBS", ALL_ILLOCUTION_OR_VALIDATIONS), }; } function randomReferentList() { return [randomItem(ALL_REFERENTS)]; } function randomReferential() { const core = Math.random() < 0.2 ? { type: randomItem(["CAR", "QUO", "NAM", "PHR"]) } : { referents: randomReferentList(), perspective: biasedRandomItem("M", ["G", "N", "A"]), }; if (Math.random() < 0.33) { return { ...core, specification: biasedRandomItem("BSC", ["CTE", "CSV", "OBJ"]), affixes: randomAffixList(), case: biasedRandomItem("THM", ALL_CASES), case2: biasedRandomItem("THM", ALL_CASES), essence: biasedRandomItem("NRM", ["RPV"]), }; } if (Math.random() < 0.5) { const second = Math.random() < 0.5 ? undefined : { referents2: randomReferentList(), perspective2: biasedRandomItem("M", ["G", "N", "A"]), }; return { ...core, ...second, case: biasedRandomItem("THM", ALL_CASES), case2: biasedRandomItem("THM", ALL_CASES), essence: biasedRandomItem("NRM", ["RPV"]), }; } return { ...core, case: biasedRandomItem("THM", ALL_CASES), essence: biasedRandomItem("NRM", ["RPV"]), }; } function randomAdjunct() { switch (randomItem([1, 2, 3, 4, 5, 6])) { case 1: return randomItem(ALL_BIAS_ADJUNCTS); case 2: return randomItem(ALL_PARSING_ADJUNCTS); case 3: return randomItem(ALL_SINGLE_REGISTER_ADJUNCTS); case 4: return { type: randomItem(ALL_SUPPLETIVE_ADJUNCT_TYPES), case: biasedRandomItem("THM", ALL_CASES), }; case 5: return { affixes: randomAffixList(true), scope: biasedRandomItem(undefined, ALL_AFFIXUAL_ADJUNCT_SCOPES), scope2: biasedRandomItem(undefined, ALL_AFFIXUAL_ADJUNCT_SCOPES), }; } const type = biasedRandomItem("WHOLE", ALL_MODULAR_ADJUNCT_TYPES); switch (randomItem([1, 2, 3])) { case 1: return { type, vn1: randomItem(ALL_ASPECTS), }; case 2: return { type, cn: randomItem(ALL_MOOD_OR_CASE_SCOPES), vn1: randomVN(), vn2: Math.random() < 0.5 ? randomVN() : undefined, vn3: randomNonAspectualVN(), }; case 3: return { type, cn: randomItem(ALL_MOOD_OR_CASE_SCOPES), vn1: randomVN(), vn2: Math.random() < 0.5 ? randomVN() : undefined, scope: randomItem(ALL_MODULAR_ADJUNCT_SCOPES), }; } } console.time("creating words"); const testCases = Array.from({ length: numberOfTestCases }, () => { const word = Math.random() < 0.3333 ? randomAdjunct() : Math.random() < 0.5 ? randomReferential() : randomFormative(); if (typeof word != "string") Object.defineProperty(word, "wordType", { get() { return ("root" in this ? "formative" : "referents" in this ? "referential" : "adjunct"); }, }); const source = wordToIthkuil(word); return [word, source]; }); console.timeEnd("creating words"); return testCases; } function runTests(numberOfTestCases, mode) { const testCases = genTests(numberOfTestCases, mode); function test(mark, f) { let index = 0; console.time(mark); for (const [word, source] of testCases) { index++; try { f(source, word); } catch (error) { console.error(`Failed on input #${index} '${source}':`); if (error instanceof Error) { console.error(error.message); } else { console.error("Error: " + String(error)); } console.error(word); return false; } } console.timeEnd(mark); return true; } function findFailures(f) { const failures = testCases.filter(([word, source]) => { try { return f(source, word); } catch (err) { console.log(err instanceof Error ? err.message : err); return true; } }); if (failures.length == 0) { console.log("No benchmark failures found!"); return; } Object.keys(failures[0][0]) .map((key) => { const value = failures[0][0][key]; const percentageThatHadThisKey = failures.filter((x) => x[0][key] == value).length / failures.length; return [value, percentageThatHadThisKey, key]; }) .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) .map(([value, percentageThatHadThisKey, key]) => { console.log((Math.round(percentageThatHadThisKey * 1000) / 10 + "%").padEnd(5) + " of failures had " + key + " = " + value); }); console.error("\nHere are some failed words:\n" + failures .slice(0, 10) .map((x) => x[1]) .join("\n")); console.log("Total failures: " + failures.length + " of " + testCases.length + "."); } function benchmark() { return test("benchmarking", (source) => { const result = parseWord(source); if (result == null) { throw new Error("Failed to tokenize."); } }); } function checkValidity() { return test("checking validity", (source, parsed) => { const result = parseWord(source); if (result == null) { throw new Error("Failed to tokenize."); } const output = wordToIthkuil(result); if (source != output) { throw new Error(`Output '${output}' is different from input '${source}'.`); } { const { word } = transformWord(source); const neo = testNeo(word); if (typeof parsed == "object" && "root" in parsed) { if (neo != word) { throw new Error(`Neo output '${neo}' is different from input '${word}'.`); } } } }); } function findAllBenchmarkFailures() { return findFailures((source) => { const result = parseWord(source); return result == null; }); } function findAllValidityFailures() { const failures = testCases .map(([word, source]) => { try { const result = parseWord(source); if (result == null) { return [word, source, null]; } const output = wordToIthkuil(result); return [word, source, output]; } catch (err) { return [word, source, false]; } }) .filter(([, source, output]) => output == null || output == false || source != output); if (failures.length == 0) { console.log("No validity failures found!"); return; } Object.keys(failures[0][0]) .map((key) => { const value = failures[0][0][key]; const percentageThatHadThisKey = failures.filter((x) => x[0][key] == value).length / failures.length; return [value, percentageThatHadThisKey, key]; }) .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) .map(([value, percentageThatHadThisKey, key]) => { console.log((Math.round(percentageThatHadThisKey * 1000) / 10 + "%").padEnd(5) + " of failures had " + key + " = " + value); }); console.error("\nHere are some failed words:\n\n" + failures .slice(0, 10) .map((x) => { return x[1] + "\n" + x[2]; }) .join("\n\n")); console.error("\nTotal validity failures: " + failures.length + " of " + testCases.length); } if (!benchmark()) { findAllBenchmarkFailures(); return false; } else if (!checkValidity()) { findAllValidityFailures(); return false; } else { console.log("All tests passed!"); return true; } } function containedRunTests(numberOfTestCases, mode) { console.log(); try { console.group(`Testing in ${mode} mode ${numberOfTestCases} times...`); return runTests(numberOfTestCases, mode); } catch (error) { console.error("An error occurred."); console.error(error); return false; } finally { console.groupEnd(); } } if (containedRunTests(1e5, "short") && containedRunTests(1e5, "full") && containedRunTests(1e6, "short") && containedRunTests(1e6, "full")) { console.log(); console.log("All tests passed!"); console.log(); } else { console.log(); throw new Error("Tests failed."); }