refactor and improve equative explorer, using the NP centric data format

This commit is contained in:
lingdocs 2021-11-01 13:50:17 -04:00
parent 8843a7f106
commit 1d9ff2b9e1
34 changed files with 826 additions and 855 deletions

View File

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@lingdocs/lingdocs-main": "^0.2.0", "@lingdocs/lingdocs-main": "^0.2.0",
"@lingdocs/pashto-inflector": "^1.3.2", "@lingdocs/pashto-inflector": "^1.3.5",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",

View File

@ -4,10 +4,14 @@ const { readDictionary } = require("@lingdocs/pashto-inflector");
const path = require("path"); const path = require("path");
const wordsPath = path.join(".", "src", "words"); const wordsPath = path.join(".", "src", "words");
const wordsFile = "raw-words.ts"; const wordsFile = "raw-words.ts";
const verbCollectionPath = path.join(wordsPath, "verb-categories"); const verbCollectionPath = path.join(wordsPath, "verb-categories");
const nounAdjCollectionPath = path.join(wordsPath, "noun-adj-categories"); const nounAdjCollectionPath = path.join(wordsPath, "noun-adj-categories");
const adverbCollectionPath = path.join(wordsPath, "adverbs");
const verbTsFiles = fs.readdirSync(verbCollectionPath); const verbTsFiles = fs.readdirSync(verbCollectionPath);
const nounAdjTsFiles = fs.readdirSync(nounAdjCollectionPath); const nounAdjTsFiles = fs.readdirSync(nounAdjCollectionPath);
const adverbTsFiles = fs.readdirSync(adverbCollectionPath);
const allVerbTsS = verbTsFiles.flatMap(fileName => [ const allVerbTsS = verbTsFiles.flatMap(fileName => [
...require(path.join("..", verbCollectionPath, fileName)).map(x => x.ts) ...require(path.join("..", verbCollectionPath, fileName)).map(x => x.ts)
@ -17,6 +21,10 @@ const allNounAdjTsS = nounAdjTsFiles.flatMap(fileName => [
...require(path.join("..", nounAdjCollectionPath, fileName)).map(x => x.ts) ...require(path.join("..", nounAdjCollectionPath, fileName)).map(x => x.ts)
]).filter((v, i, a) => a.findIndex(x => x === v) === i); ]).filter((v, i, a) => a.findIndex(x => x === v) === i);
const allAdverbTsS = adverbTsFiles.flatMap(fileName => [
...require(path.join("..", adverbCollectionPath, fileName)).map(x => x.ts)
]).filter((v, i, a) => a.findIndex(x => x === v) === i);
console.log("getting words from dictionary..."); console.log("getting words from dictionary...");
fetch(process.env.LINGDOCS_DICTIONARY_URL).then(res => res.arrayBuffer()).then(buffer => { fetch(process.env.LINGDOCS_DICTIONARY_URL).then(res => res.arrayBuffer()).then(buffer => {
@ -61,7 +69,7 @@ function getVerbsFromTsS(entries) {
} }
function getNounsAdjsFromTsS(entries) { function getNounsAdjsFromTsS(entries) {
const b = allNounAdjTsS.map(ts => { const b = [...allNounAdjTsS, ...allAdverbTsS].map(ts => {
const entry = entries.find(x => ts === x.ts); const entry = entries.find(x => ts === x.ts);
if (!entry) { if (!entry) {
console.log("couldn't find ts", ts); console.log("couldn't find ts", ts);

View File

@ -3,20 +3,11 @@ const fetch = require("node-fetch");
const path = require("path"); const path = require("path");
const { readDictionary } = require("@lingdocs/pashto-inflector"); const { readDictionary } = require("@lingdocs/pashto-inflector");
const verbsPath = path.join(".", "src", "words"); const verbsPath = path.join(".", "src", "words");
const collectionPath = path.join(verbsPath, "verb-categories");
const verbTsFiles = fs.readdirSync(collectionPath);
const pConsonants = ["ب", "پ", "ت", "ټ", "ث", "ج", "چ", "ح", "خ", "څ", "ځ", "د", "ډ", "ذ", "ر", "ړ", "ز", "ژ", "ږ", "س", "ش", "ښ", "ص", "ض", "ط", "ظ", "غ", "ف", "ق", "ک", "ګ", "گ", "ل", "ل", "م", "ن", "ڼ"];
// const allTsS = [...new Set(verbTsFiles.reduce((arr, fileName) => {
// const TsS = require(path.join("..", collectionPath, fileName));
// return [...arr, ...TsS];
// }, []))];
fetch(process.env.LINGDOCS_DICTIONARY_URL).then(res => res.arrayBuffer()).then(data => { fetch(process.env.LINGDOCS_DICTIONARY_URL).then(res => res.arrayBuffer()).then(data => {
const { entries } = readDictionary(data); const { entries } = readDictionary(data);
const filtered = entries.filter(e => ( const filtered = entries.filter(e => (
e.c?.includes("n. m. unisex") e.c?.includes("loc. adv.")
)); ));
const content = `module.exports = [ const content = `module.exports = [
${filtered.reduce((text, entry) => ( ${filtered.reduce((text, entry) => (
@ -25,21 +16,3 @@ ${filtered.reduce((text, entry) => (
];`; ];`;
fs.writeFileSync(path.join(verbsPath, "query-results.js"), content); fs.writeFileSync(path.join(verbsPath, "query-results.js"), content);
}); });
// function getFromTsS(entries) {
// return allTsS.map(item => {
// const entry = entries.find(x => item.ts === x.ts);
// if (!entry) {
// console.log("couldn't find ts", ts);
// return undefined;
// }
// if (entry.c && entry.c.includes("comp.")) {
// const complement = entries.find(x => entry.l === x.ts);
// return {
// entry,
// complement,
// };
// }
// return { entry, def: item.e };
// }).filter(x => x);
// }

View File

@ -8,7 +8,7 @@ import {
getEnglishWord, getEnglishWord,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
function InflectionCarousel({ items }: { items: (Noun | Adjective)[] }) { function InflectionCarousel({ items }: { items: (NounEntry | AdjectiveEntry)[] }) {
if (!items.length) { if (!items.length) {
return "no items for carousel"; return "no items for carousel";
} }

View File

@ -3,25 +3,31 @@ import {
defaultTextOptions as opts, defaultTextOptions as opts,
ButtonSelect, ButtonSelect,
Types as T, Types as T,
personGender,
personIsPlural,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { import {
ExplorerState, ExplorerState,
ExplorerReducerAction, ExplorerReducerAction,
} from "./explorer-types"; } from "./explorer-types";
import { // import {
makeBlockWPronouns, // makeBlockWPronouns,
} from "./explorer-helpers"; // } from "./explorer-helpers";
import { import {
equativeMachine, equativeMachine,
assembleEquativeOutput, assembleEquativeOutput,
SubjectInput,
PredicateInput,
isParticipleInput,
ParticipleInput,
} from "../../lib/equative-machine"; } from "../../lib/equative-machine";
import { isPluralEntry, isUnisexNoun, isAdjective, isSingularEntry } from "../../lib/type-predicates"; import {
isPluralNounEntry,
isUnisexNounEntry,
isAdjectiveEntry,
isSingularEntry,
isVerbEntry,
isLocativeAdverbEntry,
isNounEntry,
} from "../../lib/type-predicates";
export function chooseLength<O>(o: T.SingleOrLengthOpts<O>, length: "short" | "long"): O { function chooseLength<O>(o: T.SingleOrLengthOpts<O>, length: "short" | "long"): O {
return ("long" in o) ? o[length] : o; return ("long" in o) ? o[length] : o;
} }
@ -30,10 +36,18 @@ function SingleItemDisplay({ state }: { state: ExplorerState }) {
return <div>ERROR: Wrong display being used</div>; return <div>ERROR: Wrong display being used</div>;
} }
try { try {
const subjInput = makeSubjectInput(state.subject[state.subject.type], state); const se = state.subject[state.subject.type];
const predInput = makePredicateInput(state.predicate[state.predicate.type], state); const pe = state.predicate[state.predicate.type];
const subject = makeNounPhrase(se, state, "subject");
const predicate = (isAdjectiveEntry(pe) || isLocativeAdverbEntry(pe))
? makeComplement(pe)
: makeNounPhrase(pe, state, "predicate");
const block = assembleEquativeOutput( const block = assembleEquativeOutput(
equativeMachine(subjInput, predInput, state.tense) equativeMachine({
subject,
predicate,
tense: state.tense,
})
); );
return <div> return <div>
<VerbTable textOptions={opts} block={chooseLength(block, state.length)} /> <VerbTable textOptions={opts} block={chooseLength(block, state.length)} />
@ -44,84 +58,99 @@ function SingleItemDisplay({ state }: { state: ExplorerState }) {
} }
} }
function makeSubjectInput(entry: Noun | ParticipleInput | UnisexNoun, state: ExplorerState): SubjectInput { function makeComplement(entry: AdjectiveEntry | LocativeAdverbEntry): Compliment {
if (isParticipleInput(entry)) { return {
return entry; type: "compliment",
} entry,
const isUnisex = isUnisexNoun(entry); };
if (isUnisex && isSingularEntry(entry)) { }
function makeNounPhrase(entry: NounEntry | UnisexNounEntry | VerbEntry, state: ExplorerState, entity: "subject" | "predicate"): NounPhrase {
if (isVerbEntry(entry)) {
return { return {
...state.subject.info, type: "participle",
entry, entry,
}; };
} }
if (isUnisex && isPluralEntry(entry)) { const isUnisex = isUnisexNounEntry(entry);
if (isUnisex && isSingularEntry(entry)) {
return { return {
...state.subject.info, type: "unisex noun",
plural: true, number: state[entity].info.number,
gender: state[entity].info.gender,
entry,
};
}
if (isUnisex && isPluralNounEntry(entry)) {
return {
type: "unisex noun",
number: state[entity].info.number,
gender: state[entity].info.gender,
entry, entry,
}; };
} }
if (isUnisex) { if (isUnisex) {
throw new Error("improper unisex noun"); throw new Error("improper unisex noun");
} }
if (isPluralEntry(entry)) { if (isPluralNounEntry(entry)) {
const e = entry as PluralNounEntry<MascNounEntry | FemNounEntry>;
return { return {
plural: true, type: "plural noun",
entry, entry: e,
} };
} }
if (isSingularEntry(entry)) { if (isSingularEntry(entry)) {
const e = entry as SingularEntry<MascNounEntry | FemNounEntry>;
return { return {
entry, type: "singular noun",
plural: state.subject.info.plural, entry: e,
number: state[entity].info.number,
}; };
} }
throw new Error("unable to make subject input from entry"); throw new Error("unable to make subject input from entry");
} }
function makePredicateInput(entry: Noun | ParticipleInput | UnisexNoun | Adjective, state: ExplorerState): PredicateInput { export function makeBlockWPronouns(e: AdjectiveEntry | UnisexNounEntry | LocativeAdverbEntry, tense: EquativeTense, length?: "short" | "long"): T.SingleOrLengthOpts<T.VerbBlock> {
if (isParticipleInput(entry) || isAdjective(entry)) { // if the output's gonna have long / short forms (if it's past or wouldBe) then recursive call to make the long and short versions
return entry; if (!length && "long" in assembleEquativeOutput(equativeMachine({
} subject: { type: "pronoun", pronounType: "near", person: 0 },
const isUnisex = isUnisexNoun(entry); predicate: (isAdjectiveEntry(e) || isLocativeAdverbEntry(e))
if (isUnisex && state.subject.type === "pronouns") { ? { type: "compliment", entry: e }
return entry; : { type: "unisex noun", gender: "masc", number: "singular", entry: e },
} tense,
if (isUnisex && isSingularEntry(entry)) { }))) {
return { return {
...state.predicate.info, short: makeBlockWPronouns(e, tense, "short") as T.VerbBlock,
entry, long: makeBlockWPronouns(e, tense, "long") as T.VerbBlock,
}; };
} }
if (isUnisex && isPluralEntry(entry)) { const makeP = (p: T.Person): T.ArrayOneOrMore<T.PsString> => {
return { const b = assembleEquativeOutput(equativeMachine({
...state.predicate.info, subject: { type: "pronoun", pronounType: "far", person: p },
plural: true, predicate: (isAdjectiveEntry(e) || isLocativeAdverbEntry(e))
entry, ? { type: "compliment", entry: e }
}; : { type: "unisex noun", gender: personGender(p), number: personIsPlural(p) ? "plural" : "singular", entry: e },
} tense,
if (isUnisex) { }));
throw new Error("improper unisex noun"); if ("long" in b) {
} if (!length) throw new Error("bad length processing");
if (isPluralEntry(entry)) { return b[length];
return { }
plural: true, return b;
entry, };
} return [
} [makeP(0), makeP(6)],
if (isSingularEntry(entry)) { [makeP(1), makeP(7)],
return { [makeP(2), makeP(8)],
entry, [makeP(3), makeP(9)],
plural: state.predicate.info.plural, [makeP(4), makeP(10)],
}; [makeP(5), makeP(11)],
} ];
throw new Error("unable to make predicate input from entry");
} }
function PronounBlockDisplay({ state }: { state: ExplorerState }) { function PronounBlockDisplay({ state }: { state: ExplorerState }) {
const pred = state.predicate[state.predicate.type]; const pred = state.predicate[state.predicate.type];
if (!isParticipleInput(pred) && (isAdjective(pred) || isUnisexNoun(pred))) { if (!isVerbEntry(pred) && (isAdjectiveEntry(pred) || isLocativeAdverbEntry(pred) || (isNounEntry(pred) && isUnisexNounEntry(pred)))) {
const block = makeBlockWPronouns(pred, state.tense); const block = makeBlockWPronouns(pred, state.tense);
return <VerbTable return <VerbTable
textOptions={opts} textOptions={opts}
@ -133,7 +162,7 @@ function PronounBlockDisplay({ state }: { state: ExplorerState }) {
function EquativeDisplay({ state, dispatch }: { state: ExplorerState, dispatch: (action: ExplorerReducerAction) => void }) { function EquativeDisplay({ state, dispatch }: { state: ExplorerState, dispatch: (action: ExplorerReducerAction) => void }) {
return <> return <>
{state.tense === "past" && <div className="text-center"> {(state.tense === "past" || state.tense === "wouldBe") && <div className="text-center">
<ButtonSelect <ButtonSelect
small small
options={[ options={[
@ -145,9 +174,12 @@ function EquativeDisplay({ state, dispatch }: { state: ExplorerState, dispatch:
/> />
</div>} </div>}
{state.subject.type === "pronouns" {state.subject.type === "pronouns"
? <PronounBlockDisplay state={state} /> ? <PronounBlockDisplay state={state} />
: <SingleItemDisplay state={state} /> : <SingleItemDisplay state={state} />
} }
{state.predicate.type === "participle" && <div className="mt-2 small text-muted text-center">
Note: This means that the subject <em>is</em> the act of the participle/verb, not that the subject is currently doing the verb!
</div>}
</>; </>;
} }

View File

@ -3,8 +3,7 @@ import {
reducer, reducer,
} from "./explorer-reducer"; } from "./explorer-reducer";
import { import {
PredicateSelector, InputSelector,
SubjectSelector,
TenseSelector, TenseSelector,
} from "./explorer-selectors"; } from "./explorer-selectors";
import { import {
@ -16,6 +15,7 @@ import {
defaultAdjective, defaultAdjective,
defaultNoun, defaultNoun,
defaultParticiple, defaultParticiple,
defaultAdverb,
} from "./explorer-inputs"; } from "./explorer-inputs";
import EquativeDisplay from "./EquativeDisplay"; import EquativeDisplay from "./EquativeDisplay";
@ -25,11 +25,12 @@ const defaultState: ExplorerState = {
predicate: { predicate: {
type: "adjective", type: "adjective",
adjective: defaultAdjective, adjective: defaultAdjective,
adverb: defaultAdverb,
unisexNoun: defaultUnisexNoun, unisexNoun: defaultUnisexNoun,
participle: defaultParticiple, participle: defaultParticiple,
noun: defaultNoun, noun: defaultNoun,
info: { info: {
plural: false, number: "singular",
gender: "masc", gender: "masc",
}, },
}, },
@ -39,7 +40,7 @@ const defaultState: ExplorerState = {
participle: defaultParticiple, participle: defaultParticiple,
unisexNoun: defaultUnisexNoun, unisexNoun: defaultUnisexNoun,
info: { info: {
plural: false, number: "singular",
gender: "masc", gender: "masc",
}, },
}, },
@ -52,15 +53,13 @@ function EquativeExplorer() {
unsafeSetState(newState); unsafeSetState(newState);
} }
return <> return <>
<TenseSelector state={state} dispatch={dispatch} />
<div className="row"> <div className="row">
<div className="col-sm"> <div className="col">
<TenseSelector state={state} dispatch={dispatch} /> <InputSelector entity="subject" state={state} dispatch={dispatch} />
</div> </div>
<div className="col"> <div className="col">
<SubjectSelector state={state} dispatch={dispatch} /> <InputSelector entity="predicate" state={state} dispatch={dispatch} />
</div>
<div className="col">
<PredicateSelector state={state} dispatch={dispatch} />
</div> </div>
</div> </div>
<EquativeDisplay state={state} dispatch={dispatch} /> <EquativeDisplay state={state} dispatch={dispatch} />

View File

@ -1,54 +0,0 @@
import {
Types as T,
removeFVarients,
getEnglishWord,
} from "@lingdocs/pashto-inflector";
import {
equativeMachine,
assembleEquativeOutput,
ParticipleInput,
isParticipleInput,
getEnglishParticiple,
} from "../../lib/equative-machine";
export function sort<T extends (Adjective | Noun | ParticipleInput)>(arr: Readonly<T[]>): T[] {
return [...arr].sort((a, b) => a.p.localeCompare(b.p));
}
export function makeBlockWPronouns(e: Adjective | UnisexNoun, tense: EquativeTense, length?: "short" | "long"): T.SingleOrLengthOpts<T.VerbBlock> {
// if the output's gonna have long / short forms (if it's past or wouldBe) then recursive call to make the long and short versions
if (!length && "long" in assembleEquativeOutput(equativeMachine(0, e, tense))) {
return {
short: makeBlockWPronouns(e, tense, "short") as T.VerbBlock,
long: makeBlockWPronouns(e, tense, "long") as T.VerbBlock,
};
}
const makeP = (p: T.Person): T.ArrayOneOrMore<T.PsString> => {
const b = assembleEquativeOutput(equativeMachine(p, e, tense));
if ("long" in b) {
if (!length) throw new Error("bad length processing");
return b[length];
}
return b;
};
return [
[makeP(0), makeP(6)],
[makeP(1), makeP(7)],
[makeP(2), makeP(8)],
[makeP(3), makeP(9)],
[makeP(4), makeP(10)],
[makeP(5), makeP(11)],
];
}
export function makeOptionLabel(e: T.DictionaryEntry): string {
const eng = (isParticipleInput(e)) ? getEnglishParticiple(e) : getEnglishWord(e);
const english = typeof eng === "string"
? eng
: !eng
? ""
: ("singular" in eng && eng.singular !== undefined)
? eng.singular
: eng.plural;
return `${e.p} - ${removeFVarients(e.f)} (${english})`;
}

View File

@ -1,27 +1,37 @@
import { nouns, adjectives, verbs } from "../../words/words"; import { nouns, adjectives, verbs, adverbs } from "../../words/words";
import { import {
isUnisexNoun, isLocativeAdverbEntry,
isUnisexNounEntry,
} from "../../lib/type-predicates"; } from "../../lib/type-predicates";
import { sort } from "./explorer-helpers";
import {
ParticipleInput,
} from "../../lib/equative-machine";
const unisexNouns = sort(nouns.filter(x => isUnisexNoun(x)) as UnisexNoun[]); function sort<T extends (AdjectiveEntry | NounEntry | VerbEntry | AdverbEntry)>(arr: Readonly<T[]>): T[] {
const nonUnisexNouns = sort(nouns.filter(x => !isUnisexNoun(x)) as (MascNoun | FemNoun)[]); if ("entry" in arr[0]) {
return [...arr].sort((a, b) => (
// @ts-ignore
a.entry.p.localeCompare(b.entry.p)
));
}
return [...arr].sort((a, b) => (
// @ts-ignore
a.p.localeCompare(b.p)
));
}
const unisexNouns = sort(nouns.filter(x => isUnisexNounEntry(x)) as UnisexNounEntry[]);
const nonUnisexNouns = sort(nouns.filter(x => !isUnisexNounEntry(x)) as (MascNounEntry | FemNounEntry)[]);
const inputs = { const inputs = {
adjective: sort(adjectives), adjective: sort(adjectives),
unisexNoun: unisexNouns, unisexNoun: unisexNouns,
noun: nonUnisexNouns, noun: nonUnisexNouns,
// @ts-ignore participle: sort(verbs),
participle: sort(verbs.map(e => e.entry) as ParticipleInput[]), adverb: sort(adverbs.filter(isLocativeAdverbEntry)),
}; };
export const defaultAdjective = inputs.adjective.find(ps => ps.p === "زوړ") || inputs.adjective[0]; export const defaultAdjective = inputs.adjective.find(ps => ps.p === "زوړ") || inputs.adjective[0];
export const defaultAdverb = inputs.adverb.find(ps => ps.p === "دلته") || inputs.adverb[0];
export const defaultUnisexNoun = inputs.unisexNoun.find(ps => ps.p === "پښتون") || inputs.unisexNoun[0]; export const defaultUnisexNoun = inputs.unisexNoun.find(ps => ps.p === "پښتون") || inputs.unisexNoun[0];
export const defaultNoun = inputs.noun.find(ps => ps.p === "کتاب") || inputs.noun[0]; export const defaultNoun = inputs.noun.find(ps => ps.p === "کتاب") || inputs.noun[0];
export const defaultParticiple = inputs.participle.find(ps => ps.p === "لیکل") || inputs.participle[0]; export const defaultParticiple = inputs.participle.find(ps => ps.entry.p === "لیکل") || inputs.participle[0];
export default inputs; export default inputs;

View File

@ -3,7 +3,7 @@ import { ExplorerState, ExplorerReducerAction } from "./explorer-types";
export function reducer(state: ExplorerState, action: ExplorerReducerAction): ExplorerState { export function reducer(state: ExplorerState, action: ExplorerReducerAction): ExplorerState {
if (action.type === "setPredicate") { if (action.type === "setPredicate") {
const pile = inputs[state.predicate.type] as (UnisexNoun | Adjective)[]; const pile = inputs[state.predicate.type] as (UnisexNounEntry | AdjectiveEntry)[];
const predicate = (pile.find(p => p.ts === action.payload) || pile[0]); const predicate = (pile.find(p => p.ts === action.payload) || pile[0]);
return { return {
...state, ...state,
@ -19,7 +19,7 @@ export function reducer(state: ExplorerState, action: ExplorerReducerAction): Ex
...state, ...state,
predicate: { predicate: {
...state.predicate, ...state.predicate,
type: (predicateType === "unisexNoun" && state.subject.type === "noun") ? "adjective" : predicateType, type: predicateType,
}, },
}; };
} }
@ -27,14 +27,17 @@ export function reducer(state: ExplorerState, action: ExplorerReducerAction): Ex
const subjectType = action.payload; const subjectType = action.payload;
return { return {
...state, ...state,
predicate: {
...state.predicate,
type: state.predicate.type === "unisexNoun" ? "adjective" : state.predicate.type,
},
subject: { subject: {
...state.subject, ...state.subject,
type: subjectType, type: subjectType,
} },
predicate: {
...state.predicate,
type: (
subjectType === "pronouns" &&
!["adjective", "adverb", "unisexNoun"].includes(state.predicate.type)
) ? "adjective" : state.predicate.type,
},
}; };
} }
if (action.type === "setSubject") { if (action.type === "setSubject") {
@ -50,26 +53,28 @@ export function reducer(state: ExplorerState, action: ExplorerReducerAction): Ex
}, },
}; };
} }
if (action.type === "setSubjectPlural") { if (action.type === "setNumber") {
const entity = action.payload.entity;
return { return {
...state, ...state,
subject: { [entity]: {
...state.subject, ...state[entity],
info: { info: {
...state.subject.info, ...state[entity].info,
plural: action.payload, number: action.payload.number,
}, },
}, },
}; };
} }
if (action.type === "setSubjectGender") { if (action.type === "setGender") {
const entity = action.payload.entity;
return { return {
...state, ...state,
subject: { [entity]: {
...state.subject, ...state[entity],
info: { info: {
...state.subject.info, ...state[entity].info,
gender: action.payload, gender: action.payload.gender,
}, },
}, },
}; };
@ -80,6 +85,15 @@ export function reducer(state: ExplorerState, action: ExplorerReducerAction): Ex
tense: action.payload, tense: action.payload,
}; };
} }
// if (action.type === "setPredicateEntity") {
// return {
// ...state,
// predicate: {
// ...state.predicate,
// entity: action.payload,
// },
// };
// }
return { return {
...state, ...state,
length: action.payload, length: action.payload,

View File

@ -1,16 +1,21 @@
import { makeOptionLabel } from "./explorer-helpers"; // import { makeOptionLabel } from "./explorer-helpers";
import inputs from "./explorer-inputs"; import inputs from "./explorer-inputs";
import { import {
ExplorerReducerAction, ExplorerReducerAction,
ExplorerState, ExplorerState,
PredicateType,
SubjectType, SubjectType,
PredicateType,
} from "./explorer-types"; } from "./explorer-types";
import {
getEnglishParticiple,
} from "../../lib/np-tools";
import { import {
ButtonSelect, ButtonSelect,
getEnglishWord,
Types as T, Types as T,
removeFVarients,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { isFemNoun, isMascNoun, isPluralEntry } from "../../lib/type-predicates"; import { isAdjectiveEntry, isAdverbEntry, isFemNounEntry, isMascNounEntry, isNounEntry, isPluralNounEntry } from "../../lib/type-predicates";
import Select from "react-select"; import Select from "react-select";
const zIndexProps = { const zIndexProps = {
@ -18,194 +23,166 @@ const zIndexProps = {
styles: { menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) }, styles: { menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) },
}; };
export function SubjectSelector({ state, dispatch }: { const npTypeOptions: { type: SubjectType, label: string }[] = [
{ type: "unisexNoun", label: "Unisex Noun"},
{ type: "noun", label: "Noun" },
{ type: "participle", label: "Participle" },
];
const subjectTypeOptions: { type: SubjectType, label: string }[] = [
{ type: "pronouns" as SubjectType, label: "Pronouns" }
];
const compTypeOptions: { type: PredicateType, label: string }[] = [
{ type: "adjective", label: "Adjective" },
{ type: "adverb", label: "Loc. Adverb" },
];
export function InputSelector({ state, dispatch, entity }: {
state: ExplorerState, state: ExplorerState,
dispatch: (action: ExplorerReducerAction) => void, dispatch: (action: ExplorerReducerAction) => void,
entity: "subject" | "predicate",
}) { }) {
const typeOptions = [
...entity === "subject"
? subjectTypeOptions
: compTypeOptions,
...npTypeOptions.filter(o => !(entity === "predicate" && (
(state.subject.type === "pronouns" && ["noun", "participle"].includes(o.type))
// || (state.subject.type === "unisexNoun" && o.type === "unisexNoun")
))),
];
function onTypeSelect(e: React.ChangeEvent<HTMLInputElement>) { function onTypeSelect(e: React.ChangeEvent<HTMLInputElement>) {
const t = e.target.value as SubjectType; if (entity === "subject") {
dispatch({ type: "setSubjectType", payload: t }); const t = e.target.value as SubjectType;
} dispatch({ type: "setSubjectType", payload: t });
function onSubjectSelect({ value }: any) { } else {
dispatch({ type: "setSubject", payload: parseInt(value) }); const t = e.target.value as PredicateType;
} dispatch({ type: "setPredicateType", payload: t });
const pluralNounSelected = (
state.subject.type === "noun" && isPluralEntry(state.subject.noun)
);
const options = state.subject.type === "pronouns"
? []
: inputs[state.subject.type].map(e => ({
value: e.ts.toString(),
label: makeOptionLabel(e),
}));
const subject = state.subject.type === "pronouns"
? undefined
: state.subject[state.subject.type];
return <div className="form-group">
<label htmlFor="predicate-select"><h5 className="mb-0">Subject:</h5></label>
<div className="form-check">
<input
className="form-check-input"
type="radio"
name="pronounsSubjectRadio"
id="pronounsSubjectRadio"
value="pronouns"
checked={state.subject.type === "pronouns"}
onChange={onTypeSelect}
/>
<label className="form-check-label" htmlFor="adjectivesPredicateRadio">
Pronouns
</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="radio"
name="nounsSubjectRadio"
id="nounsSubjectRadio"
value="noun"
checked={state.subject.type === "noun"}
onChange={onTypeSelect}
/>
<label className="form-check-label" htmlFor="unisexNounsPredicateRadio">
Nouns
</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="radio"
name="unisexNounsSubjectRadio"
id="unisexNounsSubjectRadio"
value="unisexNoun"
checked={state.subject.type === "unisexNoun"}
onChange={onTypeSelect}
/>
<label className="form-check-label" htmlFor="unisexNounsPredicateRadio">
Unisex Nouns
</label>
</div>
<div className="form-check mb-2">
<input
className="form-check-input"
type="radio"
name="participlesSubjectRadio"
id="participlesSubjectRadio"
value="participle"
checked={state.subject.type === "participle"}
onChange={onTypeSelect}
/>
<label className="form-check-label" htmlFor="unisexNounsPredicateRadio">
Participles
</label>
</div>
{state.subject.type !== "pronouns" &&
<>
<Select
value={subject?.ts.toString()}
onChange={onSubjectSelect}
className="mb-2"
// @ts-ignore
options={options}
isSearchable
placeholder={options.find(o => o.value === subject?.ts.toString())?.label}
{...zIndexProps}
/>
<div className="d-flex flex-row justify-content-center mt-3">
<div className="mr-2">
<ButtonSelect
small
options={[
...(state.subject.type === "unisexNoun" || (state.subject.type === "participle") || (isMascNoun(state.subject[state.subject.type])))
? [{ label: "Masc.", value: "masc" }] : [],
...(state.subject.type === "unisexNoun" || ((state.subject.type !== "participle") && isFemNoun(state.subject[state.subject.type])))
? [{ label: "Fem.", value: "fem" }] : [],
]}
value={state.subject.type === "noun"
? (isMascNoun(state.subject[state.subject.type]) ? "masc" : "fem")
: state.subject.type === "participle"
? "masc"
: state.subject.info.gender}
handleChange={state.subject.type === "noun" ? p => null : (p) => dispatch({ type: "setSubjectGender", payload: p as T.Gender })}
/>
</div>
<div className="ml-2">
<ButtonSelect
small
options={[
...(!pluralNounSelected && state.subject.type !== "participle") ? [{ label: "Singular", value: "singular" }] : [],
{ label: "Plural", value: "plural" },
]}
value={(state.subject.info.plural || pluralNounSelected || state.subject.type === "participle") ? "plural" : "singular"}
handleChange={(p) => dispatch({ type: "setSubjectPlural", payload: p === "plural" ? true : false })}
/>
</div>
</div>
</>
} }
}
function onEntrySelect({ value }: any) {
dispatch({ type: entity === "subject" ? "setSubject" : "setPredicate", payload: parseInt(value) });
}
function CheckboxItem({ type, label }: { type: string, label: string }) {
const id = `${entity}-${type}-radio`;
return <div className="form-check">
<input
className="form-check-input"
type="radio"
id={id}
value={type}
checked={state[entity].type === type}
onChange={onTypeSelect}
/>
<label className="form-check-label" htmlFor={id}>
{label}
</label>
</div>
}
const type = state[entity].type;
const entry: NounEntry | VerbEntry | AdjectiveEntry | LocativeAdverbEntry | undefined = type === "pronouns"
? undefined
// @ts-ignore
: state[entity][type];
const options = type === "pronouns"
? []
: inputs[type].map(makeOption);
return <div className="form-group">
<h5 className="mb-2">{entity === "subject" ? "Subject:" : "Predicate:"}</h5>
<div className="mb-2">
{typeOptions.map(({ type, label }) => (
<CheckboxItem type={type} label={label} key={`${entity}-${type}-radio`} />
))}
</div>
{type !== "pronouns" && <>
<Select
value={entry && ("entry" in entry ? entry.entry : entry).ts.toString()}
onChange={onEntrySelect}
className="mb-2"
// @ts-ignore
options={options}
isSearchable
// @ts-ignore
placeholder={options.find(o => o.value === ("entry" in entry ? entry.entry : entry).ts.toString())?.label}
{...zIndexProps}
/>
{!["adjective", "adverb"].includes(type) && !(state.subject.type === "pronouns" && state.predicate.type === "unisexNoun") &&
<GenderAndNumberSelect state={state} dispatch={dispatch} entity={entity} />
}
</>}
</div>; </div>;
} }
export function PredicateSelector({ state, dispatch }: { function GenderAndNumberSelect({ state, dispatch, entity }: {
state: ExplorerState, state: ExplorerState,
dispatch: (action: ExplorerReducerAction) => void, dispatch: (action: ExplorerReducerAction) => void,
entity: "subject" | "predicate",
}) { }) {
function onTypeSelect(e: React.ChangeEvent<HTMLInputElement>) { const type = state[entity].type;
const t = e.target.value as PredicateType; if (type === "pronouns") {
dispatch({ type: "setPredicateType", payload: t }); return <div>ERROR: Should not display with pronouns</div>;
} }
function onPredicateSelect({ value }: any) { // @ts-ignore
dispatch({ type: "setPredicate", payload: parseInt(value) }); const entry: NounEntry | VerbEntry | AdverbEntry | AdjectiveEntry = state[entity][type];
} const gender = type === "noun"
const options = inputs[state.predicate.type].map(e => ({ ? (isNounEntry(entry) && isMascNounEntry(entry) ? "masc" : "fem")
value: `${e.ts}`, : type === "participle"
label: makeOptionLabel(e), ? "masc"
})); : state[entity].info.gender;
const predicate = state.predicate[state.predicate.type]; const pluralNounSelected = (
return <div> type === "noun" && isPluralNounEntry(state[entity][type])
<label htmlFor="predicate-select"><h5 className="mb-0">Predicate:</h5></label> );
<div className="form-check"> return <div className="d-flex flex-row justify-content-center mt-3">
<input <div className="mr-2">
className="form-check-input" <ButtonSelect
type="radio" small
name="adjectivesPredicateRadio" options={[
id="adjectivesPredicateRadio" ...(type === "unisexNoun" || (type === "participle") || (isNounEntry(entry) && isMascNounEntry(entry)))
value="adjective" ? [{ label: "Masc.", value: "masc" }] : [],
checked={state.predicate.type === "adjective"} ...(type === "unisexNoun" || ((type !== "participle") && (isNounEntry(entry) && isFemNounEntry(entry))))
onChange={onTypeSelect} ? [{ label: "Fem.", value: "fem" }] : [],
]}
value={gender}
handleChange={type === "noun" ? p => null : (p) => dispatch({ type: "setGender", payload: { gender: p as T.Gender, entity }})}
/> />
<label className="form-check-label" htmlFor="adjectivesPredicateRadio">
Adjectives
</label>
</div> </div>
<div className="form-check mb-2"> <div className="ml-2">
<input <ButtonSelect
className="form-check-input" small
type="radio" options={[
name="unisexNounsPredicateRadio" ...(!pluralNounSelected && type !== "participle") ? [{ label: "Singular", value: "singular" }] : [],
id="unisexNounsPredicateRadio" { label: "Plural", value: "plural" },
value="unisexNoun" ]}
checked={state.predicate.type === "unisexNoun"} value={(state[entity].info.number === "plural" || pluralNounSelected || type === "participle") ? "plural" : "singular"}
onChange={onTypeSelect} handleChange={(p) => dispatch({ type: "setNumber", payload: { number: p as NounNumber, entity }})}
disabled={state.subject.type !== "pronouns"}
/> />
<label className="form-check-label" htmlFor="unisexNounsPredicateRadio">
Unisex Nouns
</label>
</div> </div>
<Select
value={predicate.ts.toString()}
onChange={onPredicateSelect}
className="mb-2"
// @ts-ignore
options={options}
isSearchable
placeholder={options.find(o => o.value === predicate.ts.toString())?.label}
{...zIndexProps}
/>
</div>; </div>;
} }
function makeOption(e: VerbEntry | NounEntry | AdjectiveEntry | LocativeAdverbEntry): { value: string, label: string } {
const entry = "entry" in e ? e.entry : e;
// TODO: THIS IS SUUUPER SKETCH
const eng = (isNounEntry(e) || isAdjectiveEntry(e) || isAdverbEntry(e))
? getEnglishWord(e)
: getEnglishParticiple(entry);
const english = typeof eng === "string"
? eng
: !eng
? ""
: ("singular" in eng && eng.singular !== undefined)
? eng.singular
: eng.plural;
return {
label: `${entry.p} - ${removeFVarients(entry.f)} (${english})`,
value: entry.ts.toString(),
};
}
export function TenseSelector({ state, dispatch }: { export function TenseSelector({ state, dispatch }: {
state: ExplorerState, state: ExplorerState,
dispatch: (action: ExplorerReducerAction) => void, dispatch: (action: ExplorerReducerAction) => void,
@ -222,7 +199,7 @@ export function TenseSelector({ state, dispatch }: {
dispatch({ type: "setTense", payload: value }); dispatch({ type: "setTense", payload: value });
} }
return <div> return <div>
<h5>Tense:</h5> <h5>Equative:</h5>
<Select <Select
value={state.tense} value={state.tense}
onChange={onTenseSelect} onChange={onTenseSelect}

View File

@ -1,7 +1,8 @@
import { Types as T } from "@lingdocs/pashto-inflector"; import { Types as T } from "@lingdocs/pashto-inflector";
import { ParticipleInput } from "../../lib/equative-machine";
export type PredicateType = "adjective" | "noun" | "unisexNoun" | "participle"; export type PredicateNPType = "noun" | "unisexNoun" | "participle";
export type PredicateCompType = "adjective" | "adverb";
export type PredicateType = PredicateNPType | PredicateCompType;
export type SubjectType = "pronouns" | "noun" | "unisexNoun" | "participle"; export type SubjectType = "pronouns" | "noun" | "unisexNoun" | "participle";
export type ExplorerState = { export type ExplorerState = {
@ -13,14 +14,18 @@ export type ExplorerState = {
export type SubjectEntityInfo = EntitiyInfo & { type: SubjectType }; export type SubjectEntityInfo = EntitiyInfo & { type: SubjectType };
export type PredicateEntityInfo = EntitiyInfo & { type: PredicateType, adjective: Adjective }; export type PredicateEntityInfo = EntitiyInfo & {
type: PredicateType,
adjective: AdjectiveEntry,
adverb: LocativeAdverbEntry,
}
type EntitiyInfo = { type EntitiyInfo = {
noun: Noun, noun: NounEntry,
participle: ParticipleInput, participle: VerbEntry,
unisexNoun: UnisexNoun, unisexNoun: UnisexNounEntry,
info: { info: {
plural: boolean, number: NounNumber,
gender: T.Gender, gender: T.Gender,
}, },
}; };
@ -34,9 +39,9 @@ export type ExplorerReducerAction = {
} | { } | {
type: "setSubject", payload: number, type: "setSubject", payload: number,
} | { } | {
type: "setSubjectPlural", payload: boolean, type: "setNumber", payload: { entity: "subject" | "predicate", number: NounNumber },
} | { } | {
type: "setSubjectGender", payload: T.Gender, type: "setGender", payload: { entity: "subject" | "predicate", gender: T.Gender },
} | { } | {
type: "setTense", payload: EquativeTense, type: "setTense", payload: EquativeTense,
} | { } | {

View File

@ -4,4 +4,14 @@ title: Equative Explorer 🌎
import EquativeExplorer from "../../components/equative-explorer/EquativeExplorer"; import EquativeExplorer from "../../components/equative-explorer/EquativeExplorer";
<EquativeExplorer /> You can use this tool to explore how to make different equative sentences. Everything that comes out of this will be **gramatically correct**, but the sentences might not always make sense! 🤪
<div className="mb-4">
<EquativeExplorer />
</div>
### Equative Rules: 👨‍🏫
- The **subject** has to be a **noun phrase**
- The **predicate** can be a **noun phrase** or **compliment**
- In Pashto, if the predicate is a noun phrase, the equative agrees with the predicate. Otherwise, if the predicate is a compliment, the equative agrees with the subject. In English, the equative always agrees with the subject.

View File

@ -30,12 +30,11 @@ import Link from "../../components/Link";
import Table from "../../components/Table"; import Table from "../../components/Table";
import { startingWord } from "../../lib/starting-word"; import { startingWord } from "../../lib/starting-word";
import { import {
isFemNoun, isFemNounEntry,
isPattern6FemNoun, isPattern6FemEntry,
isPattern7FemNoun,
} from "../../lib/type-predicates"; } from "../../lib/type-predicates";
export const femNouns = nouns.filter(isFemNoun); export const femNouns = nouns.filter(isFemNounEntry);
The <Link to="/inflection/inflection-patterns/">5 basic patterns in the last chapter</Link> work with both masculine and feminine words (nouns and adjectives). The <Link to="/inflection/inflection-patterns/">5 basic patterns in the last chapter</Link> work with both masculine and feminine words (nouns and adjectives).
@ -43,6 +42,6 @@ There are also a few more patterns that are only for **feminine nouns**.
## Feminine Nouns Ending in <InlinePs opts={opts} ps={{ p: "ي", f: "ee" }} /> ## Feminine Nouns Ending in <InlinePs opts={opts} ps={{ p: "ي", f: "ee" }} />
<InflectionCarousel items={startingWord(femNouns.filter(isPattern6FemNoun), "آزادي")} /> <InflectionCarousel items={startingWord(femNouns.filter(isPattern6FemEntry), "آزادي")} />
**Note:** This only works with **inanimate nouns**. (ie. words for people like <InlinePs opts={opts} ps={{ p: "باجي", f: "baajée", e: "older sister" }} /> will not inflect like this.) **Note:** This only works with **inanimate nouns**. (ie. words for people like <InlinePs opts={opts} ps={{ p: "باجي", f: "baajée", e: "older sister" }} /> will not inflect like this.)

View File

@ -24,12 +24,12 @@ import {
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { import {
isNounOrVerb, isNounOrVerb,
isPattern1Word, isPattern1Entry,
isPattern2Word, isPattern2Entry,
isPattern3Word, isPattern3Entry,
isPattern4Word, isPattern4Entry,
isPattern5Word, isPattern5Entry,
isUnisexNoun, isUnisexNounEntry,
} from "../../lib/type-predicates"; } from "../../lib/type-predicates";
import InflectionCarousel from "../../components/InflectionCarousel"; import InflectionCarousel from "../../components/InflectionCarousel";
import { nouns, adjectives } from "../../words/words"; import { nouns, adjectives } from "../../words/words";
@ -37,7 +37,7 @@ import { startingWord } from "../../lib/starting-word";
import Link from "../../components/Link"; import Link from "../../components/Link";
export const words = [ export const words = [
...nouns.filter(isUnisexNoun), ...nouns.filter(isUnisexNounEntry),
...adjectives, ...adjectives,
]; ];
@ -57,7 +57,7 @@ These words always end in:
- **Feminine:** - <InlinePs opts={opts} ps={{ p: "ـه", f: "-a" }} /> - **Feminine:** - <InlinePs opts={opts} ps={{ p: "ـه", f: "-a" }} />
<InflectionCarousel items={startingWord( <InflectionCarousel items={startingWord(
words.filter(isPattern1Word), words.filter(isPattern1Entry),
"غټ", "غټ",
)} /> )} />
@ -92,7 +92,7 @@ Notice how it does not use the first feminine inflection <InlinePs opts={opts} p
## 2. Words ending in an unstressed <InlinePs opts={opts} ps={{ p: "ی", f: "ey" }} /> ## 2. Words ending in an unstressed <InlinePs opts={opts} ps={{ p: "ی", f: "ey" }} />
<InflectionCarousel items={startingWord( <InflectionCarousel items={startingWord(
words.filter(isPattern2Word), words.filter(isPattern2Entry),
"ستړی", "ستړی",
)} /> )} />
@ -101,7 +101,7 @@ Notice how it does not use the first feminine inflection <InlinePs opts={opts} p
This is very similar to pattern #2, but with the stress on the last syllable the feminine inflection changes. This is very similar to pattern #2, but with the stress on the last syllable the feminine inflection changes.
<InflectionCarousel items={startingWord( <InflectionCarousel items={startingWord(
words.filter(isPattern3Word), words.filter(isPattern3Entry),
"لومړی", "لومړی",
)} /> )} />
@ -113,7 +113,7 @@ These words are a little irregular but you can see a common patten based around:
- shortening the other forms and adding the <InlinePs opts={opts} ps={{ p: "ـه", f: "-a" }} />, <InlinePs opts={opts} ps={{ p: "ـې", f: "-e" }} />, <InlinePs opts={opts} ps={{ p: "ـو", f: "-o" }} /> endings - shortening the other forms and adding the <InlinePs opts={opts} ps={{ p: "ـه", f: "-a" }} />, <InlinePs opts={opts} ps={{ p: "ـې", f: "-e" }} />, <InlinePs opts={opts} ps={{ p: "ـو", f: "-o" }} /> endings
<InflectionCarousel items={startingWord( <InflectionCarousel items={startingWord(
words.filter(isPattern4Word), words.filter(isPattern4Entry),
"پښتون", "پښتون",
)} /> )} />
@ -124,7 +124,7 @@ These words are a little irregular but you can see a common patten based around:
These are also a little irregular but instead of lengthening the 1st masculine inflection they compress it as well and take just an <InlinePs opts={opts} ps={{ p: "ـه", f: "-u" }} /> on the end. These are also a little irregular but instead of lengthening the 1st masculine inflection they compress it as well and take just an <InlinePs opts={opts} ps={{ p: "ـه", f: "-u" }} /> on the end.
<InflectionCarousel items={startingWord( <InflectionCarousel items={startingWord(
words.filter(isPattern5Word), words.filter(isPattern5Entry),
"غل", "غل",
)} /> )} />

View File

@ -18,38 +18,38 @@ import {
firstVariation, firstVariation,
} from "../../lib/text-tools"; } from "../../lib/text-tools";
import { import {
isMascNoun, isMascNounEntry,
isFemNoun, isFemNounEntry,
isUnisexNoun, isUnisexNounEntry,
} from "../../lib/type-predicates"; } from "../../lib/type-predicates";
import { categorize } from "../../lib/categorize"; import { categorize } from "../../lib/categorize";
const genders: T.Gender[] = ["masc", "fem"]; const genders: T.Gender[] = ["masc", "fem"];
const mascNouns = nouns.filter(isMascNoun); const mascNouns = nouns.filter(isMascNounEntry);
const femNouns = [ const femNouns = [
...nouns.filter(isFemNoun), ...nouns.filter(isFemNounEntry),
...getFemVersions(mascNouns.filter(isUnisexNoun)), ...getFemVersions(mascNouns.filter(isUnisexNounEntry)),
]; ];
const types = { const types = {
masc: categorize<MascNoun, { masc: categorize<MascNounEntry, {
consonantMasc: MascNoun[], consonantMasc: MascNounEntry[],
eyMasc: MascNoun[], eyMasc: MascNounEntry[],
uMasc: MascNoun[], uMasc: MascNounEntry[],
yMasc: MascNoun[], yMasc: MascNounEntry[],
}>(mascNouns, { }>(mascNouns, {
consonantMasc: endsWith([{ p: pashtoConsonants }, { p: "و", f: "w" }]), consonantMasc: endsWith([{ p: pashtoConsonants }, { p: "و", f: "w" }]),
eyMasc: endsWith({ p: "ی", f: "ey" }), eyMasc: endsWith({ p: "ی", f: "ey" }),
uMasc: endsWith({ p: "ه", f: "u" }), uMasc: endsWith({ p: "ه", f: "u" }),
yMasc: endsWith([{ p: "ای", f: "aay" }, { p: "وی", f: "ooy" }]), yMasc: endsWith([{ p: "ای", f: "aay" }, { p: "وی", f: "ooy" }]),
}), }),
fem: categorize<FemNoun, { fem: categorize<FemNounEntry, {
aaFem: FemNoun[], aaFem: FemNounEntry[],
eeFem: FemNoun[], eeFem: FemNounEntry[],
uyFem: FemNoun[], uyFem: FemNounEntry[],
aFem: FemNoun[], aFem: FemNounEntry[],
eFem: FemNoun[], eFem: FemNounEntry[],
}>(femNouns, { }>(femNouns, {
aaFem: endsWith({ p: "ا", f: "aa" }), aaFem: endsWith({ p: "ا", f: "aa" }),
eeFem: endsWith({ p: "ي", f: "ee" }), eeFem: endsWith({ p: "ي", f: "ee" }),
@ -59,7 +59,7 @@ const types = {
}), }),
}; };
function getFemVersions(uns: UnisexNoun[]): FemNoun[] { function getFemVersions(uns: UnisexNounEntry[]): FemNounEntry[] {
return uns.map((n) => { return uns.map((n) => {
const infs = inflectWord(n); const infs = inflectWord(n);
if (!infs || !infs.inflections) return undefined; if (!infs || !infs.inflections) return undefined;
@ -68,18 +68,18 @@ function getFemVersions(uns: UnisexNoun[]): FemNoun[] {
e: n.e, e: n.e,
...infs.inflections.fem[0][0], ...infs.inflections.fem[0][0],
} as T.DictionaryEntry; } as T.DictionaryEntry;
}).filter(n => !!n) as FemNoun[]; }).filter(n => !!n) as FemNounEntry[];
} }
function flatten<T>(o: Record<string, T[]>): T[] { function flatten<T>(o: Record<string, T[]>): T[] {
return Object.values(o).flat(); return Object.values(o).flat();
} }
function nounNotIn(st: Noun[]): (n: Noun | T.DictionaryEntry) => boolean { function nounNotIn(st: NounEntry[]): (n: NounEntry | T.DictionaryEntry) => boolean {
return (n: T.DictionaryEntry) => !st.find(x => x.ts === n.ts); return (n: T.DictionaryEntry) => !st.find(x => x.ts === n.ts);
} }
type CategorySet = Record<string, Noun[]>; type CategorySet = Record<string, NounEntry[]>;
// for some reason we need to use this CategorySet type here... 🤷‍♂️ // for some reason we need to use this CategorySet type here... 🤷‍♂️
const exceptions: Record<string, CategorySet> = { const exceptions: Record<string, CategorySet> = {
masc: { masc: {

View File

@ -19,10 +19,10 @@ import {
firstVariation, firstVariation,
} from "../../lib/text-tools"; } from "../../lib/text-tools";
import { import {
isUnisexNoun, isUnisexNounEntry,
} from "../../lib/type-predicates"; } from "../../lib/type-predicates";
const unisexNouns = nouns.filter(isUnisexNoun); const unisexNouns = nouns.filter(isUnisexNounEntry);
type NType = "pattern1" | "pattern2" | "pattern3" | "pattern4" | "pattern5" | "other"; type NType = "pattern1" | "pattern2" | "pattern3" | "pattern4" | "pattern5" | "other";
// TODO: make pattern types as overlay types // TODO: make pattern types as overlay types
const types = intoPatterns(unisexNouns); const types = intoPatterns(unisexNouns);
@ -41,7 +41,7 @@ export default function UnisexNounGame({ id, link }: { id: string, link: string
do { do {
type = getRandomFromList(keys); type = getRandomFromList(keys);
} while (!pool[type].length); } while (!pool[type].length);
const entry = getRandomFromList<UnisexNoun>( const entry = getRandomFromList<UnisexNounEntry>(
// @ts-ignore // @ts-ignore
pool[type] pool[type]
); );

View File

@ -1,9 +1,9 @@
import { import {
isPattern1Word, isPattern1Entry,
isPattern2Word, isPattern2Entry,
isPattern3Word, isPattern3Entry,
isPattern4Word, isPattern4Entry,
isPattern5Word, isPattern5Entry,
} from "./type-predicates"; } from "./type-predicates";
/** /**
@ -75,31 +75,31 @@ export function categorize<I, X extends Record<string, I[]>>(
// TODO: uncategorizable words like ایرې - n. f. pl. -- could be pattern 1 or 2 🤷‍♂️ // TODO: uncategorizable words like ایرې - n. f. pl. -- could be pattern 1 or 2 🤷‍♂️
export function intoPatterns<T extends (Noun | Adjective)>(words: T[]): { export function intoPatterns<T extends (NounEntry | AdjectiveEntry)>(words: T[]): {
"pattern1": Pattern1Word<T>[], "pattern1": Pattern1Entry<T>[],
"pattern2": Pattern2Word<T>[], "pattern2": Pattern2Entry<T>[],
"pattern3": Pattern3Word<T>[], "pattern3": Pattern3Entry<T>[],
"pattern4": Pattern4Word<T>[], "pattern4": Pattern4Entry<T>[],
"pattern5": Pattern5Word<T>[], "pattern5": Pattern5Entry<T>[],
"other": NonInflecting<T>[], "other": NonInflecting<T>[],
// "pattern6fem": Pattern6FemNoun<T>[], // "pattern6fem": Pattern6FemNoun<T>[],
} { } {
return categorize<(Noun | Adjective), { return categorize<(NounEntry | AdjectiveEntry), {
"pattern1": Pattern1Word<T>[], "pattern1": Pattern1Entry<T>[],
"pattern2": Pattern2Word<T>[], "pattern2": Pattern2Entry<T>[],
"pattern3": Pattern3Word<T>[], "pattern3": Pattern3Entry<T>[],
"pattern4": Pattern4Word<T>[], "pattern4": Pattern4Entry<T>[],
"pattern5": Pattern5Word<T>[], "pattern5": Pattern5Entry<T>[],
"other": NonInflecting<T>[], "other": NonInflecting<T>[],
// "pattern6fem": Pattern6FemNoun<T>[], // "pattern6fem": Pattern6FemNoun<T>[],
}>( }>(
words, words,
{ {
"pattern1": isPattern1Word, "pattern1": isPattern1Entry,
"pattern2": isPattern2Word, "pattern2": isPattern2Entry,
"pattern3": isPattern3Word, "pattern3": isPattern3Entry,
"pattern4": isPattern4Word, "pattern4": isPattern4Entry,
"pattern5": isPattern5Word, "pattern5": isPattern5Entry,
// "pattern6fem": (n) => (isNoun(n) && isPattern6FemNoun(n)), // "pattern6fem": (n) => (isNoun(n) && isPattern6FemNoun(n)),
"other": "leftovers", "other": "leftovers",
}, },

View File

@ -0,0 +1,45 @@
import {
Types as T,
getEnglishWord,
inflectWord,
isUnisexSet,
personGender,
personIsPlural,
} from "@lingdocs/pashto-inflector";
import { isAdjectiveEntry, isLocativeAdverbEntry } from "./type-predicates";
import {
psStringFromEntry,
} from "./text-tools";
export function evaluateCompliment(c: Compliment, person: T.Person): { ps: T.PsString[], e: string } {
const e = getEnglishWord(c.entry);
if (!e || typeof e !== "string") {
console.log(e);
throw new Error("error getting english for compliment");
}
if (isLocativeAdverbEntry(c.entry)) {
return {
ps: [psStringFromEntry(c.entry)],
e,
};
}
if (isAdjectiveEntry(c.entry)) {
const infs = inflectWord(c.entry);
if (!infs) return {
ps: [psStringFromEntry(c.entry)],
e,
}
if (!infs.inflections || !isUnisexSet(infs.inflections)) {
throw new Error("error getting inflections for adjective, looks like a noun's inflections");
}
return {
ps: chooseInflection(infs.inflections, person),
e,
};
}
throw new Error("noun complements not yet implemented");
}
function chooseInflection(inflections: T.UnisexSet<T.InflectionSet>, pers: T.Person): T.ArrayOneOrMore<T.PsString> {
return inflections[personGender(pers)][personIsPlural(pers) ? 1 : 0];
}

View File

@ -1,356 +1,89 @@
import { import {
getPersonFromVerbForm,
Types as T, Types as T,
getEnglishWord,
grammarUnits, grammarUnits,
inflectWord,
personIsPlural,
removeFVarients,
isUnisexSet,
personGender,
getVerbBlockPosFromPerson, getVerbBlockPosFromPerson,
addEnglish, getPersonFromVerbForm,
parseEc,
concatPsString, concatPsString,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { import {
isArrayOneOrMore, isPluralEntry, personFromNP,
} from "./type-predicates"; evaluateNP,
} from "./np-tools";
import {
evaluateCompliment,
} from "./compliment-tools";
export type EquativeMachineOutput = { // Equative Rules
subject: T.PsString[], // - An equative equates a SUBJECT: Noun Phrase and a PREDICATE: Noun Phrase | Compliment
predicate: T.PsString[], // - In Pashto, the equative agrees with the predicate when the predicate is a Noun Phrase,
equative: T.SingleOrLengthOpts<T.ArrayOneOrMore<T.PsString>>, // otherwise it agrees with the subject
ba: boolean, // - If the subject is a pronoun, always agree with the subject
}; // - In English, the equative agrees with the subject
export type NounInput = { export function equativeMachine(e: EquativeClause): EquativeClauseOutput {
entry: SingularEntry<Noun>, const ba = (e.tense === "future" || e.tense === "wouldBe");
plural: boolean, const subject = evaluateNP(e.subject);
} | { const predicate = ("type" in e.predicate && e.predicate.type === "compliment")
entry: PluralEntry<Noun>, ? evaluateCompliment(e.predicate, personFromNP(e.subject))
plural: true, : evaluateNP(e.predicate);
}; const equative = makeEquative(e);
export type ParticipleInput = T.DictionaryEntry & { __brand: "a participle" };
export type SpecifiedUnisexNounInput = NounInput & { gender: T.Gender };
export type PersonInput = T.Person;
export type EntityInput = SubjectInput | PredicateInput;
export type SubjectInput = PersonInput | NounInput | ParticipleInput | SpecifiedUnisexNounInput;
export type PredicateInput = PersonInput | NounInput | Adjective | SpecifiedUnisexNounInput | UnisexNoun | ParticipleInput;
export function equativeMachine(sub: SubjectInput, pred: PredicateInput, tense: EquativeTense): EquativeMachineOutput {
// - english equative always agrees with subject
// - pashto equative agrees with predicate, unless it's an adjective, in which case the
// agreement reverts to the subject
const subjPerson = getInputPerson(sub, "subject");
const predPerson = getInputPerson(pred, "predicate") || subjPerson;
const subject = makeEntity(sub);
const predicate = makeEntity(pred, subjPerson);
const equative = makeEquative(subjPerson, predPerson, sub, tense);
const ba = determineBa(tense);
return { return {
ba,
subject, subject,
predicate, predicate,
equative, equative,
ba,
}; };
} }
export function assembleEquativeOutput(o: EquativeMachineOutput): T.SingleOrLengthOpts<T.ArrayOneOrMore<T.PsString>> { export function assembleEquativeOutput(o: EquativeClauseOutput): T.SingleOrLengthOpts<T.ArrayOneOrMore<T.PsString>> {
if ("long" in o.equative) { if ("long" in o.equative.ps) {
return { return {
long: assembleEquativeOutput({ ...o, equative: o.equative.long }) as T.ArrayOneOrMore<T.PsString>, long: assembleEquativeOutput({ ...o, equative: { ...o.equative, ps: o.equative.ps.long }}) as T.ArrayOneOrMore<T.PsString>,
short: assembleEquativeOutput({ ...o, equative: o.equative.short }) as T.ArrayOneOrMore<T.PsString>, short: assembleEquativeOutput({ ...o, equative: { ...o.equative, ps:o.equative.ps.short }}) as T.ArrayOneOrMore<T.PsString>,
} }
} }
// get all possible combinations of subject, predicate, and equative // get all possible combinations of subject, predicate, and equative
// soooo cool how this works 🤓 // soooo cool how this works 🤓
const equatives = o.equative; const equatives = o.equative.ps;
const predicates = o.predicate; const predicates = o.predicate.ps;
const ba = o.ba ? { p: " به", f: " ba" } : ""; const ba = o.ba ? { p: " به", f: " ba" } : "";
const ps = o.subject.flatMap(subj => ( const ps = o.subject.ps.flatMap(subj => (
predicates.flatMap(pred => ( predicates.flatMap(pred => (
equatives.map(eq => ( equatives.map(eq => (
concatPsString(subj, ba, " ", pred, " ", eq)) concatPsString(subj, ba, " ", pred, " ", eq))
) )
)) ))
)); ));
const e = `${o.subject[0].e} ${o.equative[0].e} ${o.predicate[0].e}`; const e = `${o.subject.e} ${o.equative.e[0]} ${o.predicate.e}`;
return ps.map(x => ({ ...x, e })) as T.ArrayOneOrMore<T.PsString>; return ps.map(x => ({ ...x, e })) as T.ArrayOneOrMore<T.PsString>;
} }
// LEVEL 2 FUNCTIONS function makeEquative(e: EquativeClause) {
function getEngEq(row: number, col: number): string[] {
function determineBa(tense: EquativeTense): boolean { const t = grammarUnits.englishEquative[e.tense === "subjunctive" ? "present" : e.tense];
return (tense === "future" || tense === "wouldBe");
}
function getInputPerson(e: SubjectInput, part: "subject"): T.Person;
function getInputPerson(e: PredicateInput, part: "predicate"): T.Person | undefined;
function getInputPerson(e: EntityInput, part: "subject" | "predicate"): T.Person | undefined {
function nounPerson(gender: T.Gender, plural: boolean): T.Person {
return plural
? ((gender === "masc") ? T.Person.ThirdPlurMale : T.Person.ThirdPlurFemale)
: ((gender === "masc") ? T.Person.ThirdSingMale : T.Person.ThirdSingFemale);
}
if (isPersonInput(e)) return e;
if (isNounInput(e)) {
const gender = e.entry.c?.includes("n. m.") ? "masc" : "fem"
return nounPerson(gender, e.plural);
}
if (isSpecifiedUnisexNounInput(e)) {
return nounPerson(e.gender, e.plural);
}
if (isParticipleInput(e)) return T.Person.ThirdPlurMale;
if (isUnisexNounInput(e)) return undefined;
if (isAdjectiveInput(e)) return undefined;
}
function makeEntity(e: EntityInput, subjPerson?: T.Person): T.PsString[] {
const isSubject = subjPerson === undefined;
if (typeof e === "number") return makePronoun(e);
if ("entry" in e) {
return makeNoun(e, isSubject ? "subject" : "predicate");
}
if (isAdjectiveInput(e) && subjPerson !== undefined) {
return makeAdjective(e, subjPerson);
}
if (isUnisexNounInput(e)) {
if (subjPerson === undefined) throw new Error("unspecified unisex noun must be in the predicate");
return makeUnisexNoun(e, subjPerson);
}
if (isParticipleInput(e)) {
return makeParticiple(e);
}
throw new Error(`invalid entity in ${subjPerson ? "predicate" : "subject"}`);
}
function makeEquative(subj: T.Person, pred: T.Person, subjectInput: SubjectInput, tense: EquativeTense): T.SentenceForm {
function getEngEq(row: number, col: number): string {
const t = grammarUnits.englishEquative[tense === "subjunctive" ? "present" : tense];
return typeof t === "string" return typeof t === "string"
? t ? [t]
: t[row][col]; : [t[row][col]];
} }
const baseTense = (e.tense === "future")
const isPluralNoun = isNounInput(subjectInput) && isPluralEntry(subjectInput.entry);
// The subject's person information, for the English equative
const [eeRow, eeCol] = getVerbBlockPosFromPerson(
(isParticipleInput(subjectInput) || isPluralNoun)
? T.Person.ThirdSingMale
: subj
);
const baseTense = (tense === "future")
? "subjunctive" ? "subjunctive"
: tense === "wouldBe" : e.tense === "wouldBe"
? "past" ? "past"
: tense; : e.tense;
return addEnglish( const subjP = personFromNP(e.subject);
getEngEq(eeRow, eeCol), const englishPerson = (e.subject.type === "plural noun" || e.subject.type === "participle")
// pashto agrees with predicate (if possible) ? T.Person.ThirdSingMale
getPersonFromVerbForm(grammarUnits.equativeEndings[baseTense], pred), : subjP
); const pashtoPerson = (e.subject.type === "pronoun")
} ? e.subject.person
: ("type" in e.predicate && e.predicate.type === "compliment")
// LEVEL 3 FUNCTIONS ? subjP
: personFromNP(e.predicate);
function makePronoun(sub: T.Person): T.PsString[] {
const [row, col] = getVerbBlockPosFromPerson(sub);
return addEnglish(
grammarUnits.persons[sub].label.subject,
grammarUnits.pronouns.far.plain[row][col],
);
}
function makeUnisexNoun(e: UnisexNoun, subjPerson: T.Person): T.PsString[] {
// reuse english from make noun - do the a / an sensitivity
// if it's the predicate - get the inflection according to the subjPerson
const inf = inflectWord(e);
const english = getEnglishFromNoun(e, personIsPlural(subjPerson), "predicate");
const gender = personGender(subjPerson);
if (!inf) {
return [psStringFromEntry(e, english)];
}
// if (!inf.inflections && (!("plural" in inf) || (!inf.inflections || !isUnisexSet(inf.inflections)))) {
// throw Error("improper unisex noun");
// }
// if plural // anim // chose that
// otherwise just chose inflection (or add both)
const pashto = ((): T.ArrayOneOrMore<T.PsString> => {
const plural = personIsPlural(subjPerson);
function getPlural() {
const plural = getInf(inf, "plural", gender, true);
const arabicPlural = getInf(inf, "arabicPlural", gender, true);
const inflections = getInf(inf, "inflections", gender, true)
return [
...plural,
...arabicPlural,
// avoid useless non-inflecting masculine inflection "plural"
...(plural.length && inflections[0].p === e.p) ? [] : inflections,
];
}
const ps = plural
? getPlural()
: getInf(inf, "inflections", gender, plural);
return isArrayOneOrMore(ps)
? ps
: [psStringFromEntry(e, english)];
})();
return addEnglish(english, pashto);
}
function getInf(infs: T.InflectorOutput, t: "plural" | "arabicPlural" | "inflections", gender: T.Gender, plural: boolean): T.PsString[] {
// @ts-ignore
if (infs && t in infs && infs[t] !== undefined && gender in infs[t] && infs[t][gender] !== undefined) {
// @ts-ignore
const iset = infs[t][gender] as T.InflectionSet;
const ipick = iset[(t === "inflections" && plural) ? 1 : 0];
return ipick;
}
return [];
}
function makeNoun(n: NounInput | SpecifiedUnisexNounInput, entity: "subject" | "predicate"): T.PsString[] {
const english = getEnglishFromNoun(n.entry, n.plural, entity);
const pashto = ((): T.ArrayOneOrMore<T.PsString> => {
const infs = inflectWord(n.entry);
const gender = "gender" in n
? n.gender
: n.entry.c?.includes("n. f.") ? "fem" : "masc";
const ps = !n.plural
? getInf(infs, "inflections", gender, false)
: [
...getInf(infs, "plural", gender, true),
...getInf(infs, "arabicPlural", gender, true),
...getInf(infs, "inflections", gender, true),
];
return isArrayOneOrMore(ps)
? ps
: [psStringFromEntry(n.entry, english)];
})();
return addEnglish(english, pashto);
}
function makeAdjective(e: Adjective, pers: T.Person): T.PsString[] {
const inf = inflectWord(e);
const english = getEnglishWord(e);
if (!english) throw new Error("no english available for adjective");
if (typeof english !== "string") throw new Error("error getting english for adjective, looks like a noun");
// non-inflecting adjective
if (!inf) return [psStringFromEntry(e, english)];
if (!inf.inflections) throw new Error("error getting inflections, looks like a noun")
if (!isUnisexSet(inf.inflections)) throw new Error("inflections for adjective were not unisex, looks like a noun");
// inflecting adjective - inflected based on the subject person
return addEnglish(
english,
chooseInflection(inf.inflections, pers),
);
}
function makeParticiple(e: T.DictionaryEntry): T.PsString[] {
return [psStringFromEntry(e, getEnglishParticiple(e))];
}
// LEVEL 4 FUNCTIONS
function chooseInflection(inflections: T.UnisexSet<T.InflectionSet>, pers: T.Person): T.ArrayOneOrMore<T.PsString> {
return inflections[personGender(pers)][personIsPlural(pers) ? 1 : 0];
}
function getEnglishFromNoun(entry: T.DictionaryEntry, plural: boolean, entity: "subject" | "predicate"): string {
const articles = {
singular: "(A/The)",
plural: "(The)",
};
const article = articles[plural ? "plural" : "singular"];
function addArticle(s: string) {
return `${entity === "subject" ? article : article.toLowerCase()} ${s}`;
}
const e = getEnglishWord(entry);
if (!e) throw new Error(`unable to get english from subject ${entry.f} - ${entry.ts}`);
if (typeof e === "string") return ` ${e}`;
if (plural) return addArticle(e.plural);
if (!e.singular || e.singular === undefined) {
throw new Error(`unable to get english from subject ${entry.f} - ${entry.ts}`);
}
return addArticle(e.singular);
}
// function getEnglishForUnisexNoun(pred: UnisexNounInput, pers: T.Person): string | undefined {
// const english = getEnglishWord(pred);
// const plurSing = personIsPlural(pers) ? "plural" : "singular";
// return typeof english === "string"
// ? english
// : english === undefined
// ? undefined
// : english[plurSing]
// ? english[plurSing]
// : undefined;
// }
function psStringFromEntry(entry: T.DictionaryEntry, e: string): T.PsString {
return { return {
p: entry.p, ps: getPersonFromVerbForm(
f: removeFVarients(entry.f), grammarUnits.equativeEndings[baseTense],
e, pashtoPerson,
),
e: getEngEq(...getVerbBlockPosFromPerson(englishPerson)),
}; };
} }
export function getEnglishParticiple(entry: T.DictionaryEntry): string {
if (!entry.ec) throw new Error("no english information for participle");
const ec = parseEc(entry.ec);
const participle = ec[2];
return (entry.ep)
? `${participle} ${entry.ep}`
: participle;
}
export function isPersonInput(e: EntityInput | T.DictionaryEntry): e is PersonInput {
return typeof e === "number";
}
export function isNounInput(e: EntityInput | T.DictionaryEntry): e is NounInput {
if (isPersonInput(e)) return false;
if ("entry" in e && !("gender" in e)) {
// e
return true;
}
return false;
}
export function isParticipleInput(e: EntityInput | T.DictionaryEntry): e is ParticipleInput {
if (isPersonInput(e)) return false;
if ("entry" in e) return false;
return !!e.c?.startsWith("v.");
}
export function isSpecifiedUnisexNounInput(e: EntityInput): e is SpecifiedUnisexNounInput {
if (isPersonInput(e)) return false;
if ("entry" in e && "gender" in e) {
// e
return true;
}
return false;
}
export function isUnisexNounInput(e: EntityInput): e is UnisexNoun {
if (isPersonInput(e)) return false;
if ("entry" in e) return false;
return !!e.c?.includes("unisex");
}
export function isAdjectiveInput(e: EntityInput): e is Adjective {
if (isPersonInput(e)) return false;
if ("entry" in e) return false;
if (isNounInput(e)) return false;
if (isUnisexNounInput(e)) return false;
if (isSpecifiedUnisexNounInput(e)) return false;
return !!(e.c?.includes("adj.") && !(e.c.includes("n. m.") || e.c.includes("n. f.")));
}

124
src/lib/np-tools.ts Normal file
View File

@ -0,0 +1,124 @@
import { isMascNounEntry } from "./type-predicates";
import {
Types as T,
getEnglishWord,
parseEc,
getVerbBlockPosFromPerson,
grammarUnits,
inflectWord,
} from "@lingdocs/pashto-inflector";
import {
psStringFromEntry,
} from "./text-tools";
export function personFromNP(np: NounPhrase): T.Person {
if (np.type === "participle") {
return T.Person.ThirdPlurMale;
}
if (np.type === "pronoun") {
return np.person;
}
const gender: T.Gender = "gender" in np
? np.gender
: isMascNounEntry(np.entry)
? "masc"
: "fem";
const number: NounNumber = np.type === "plural noun"
? "plural"
: np.number;
return number === "plural"
? (gender === "masc" ? T.Person.ThirdPlurMale : T.Person.ThirdPlurFemale)
: (gender === "masc" ? T.Person.ThirdSingMale : T.Person.ThirdSingFemale);
}
export function evaluateNP(np: NounPhrase): { ps: T.PsString[], e: string } {
if (np.type === "participle") {
return evaluateParticiple(np);
}
if (np.type === "pronoun") {
return evaluatePronoun(np);
}
return evaluateNoun(np);
}
function evaluatePronoun(p: Pronoun): { ps: T.PsString[], e: string } {
// TODO: Will need to handle inflecting and inflecting english pronouns etc.
const [row, col] = getVerbBlockPosFromPerson(p.person);
return {
ps: grammarUnits.pronouns[p.pronounType].plain[row][col],
e: grammarUnits.persons[p.person].label.subject,
};
}
function evaluateNoun(n: Noun): { ps: T.PsString[], e: string } {
const number: NounNumber = "number" in n ? n.number : "plural";
const english = getEnglishFromNoun(n.entry, number);
const pashto = ((): T.PsString[] => {
const infs = inflectWord(n.entry);
const gender: T.Gender = "gender" in n ? n.gender :
(isMascNounEntry(n.entry) ? "masc" : "fem");
const ps = number === "singular"
? getInf(infs, "inflections", gender, false)
: [
...getInf(infs, "plural", gender, true),
...getInf(infs, "arabicPlural", gender, true),
...getInf(infs, "inflections", gender, true),
];
return ps.length > 0
? ps
: [psStringFromEntry(n.entry)];
})();
return { ps: pashto, e: english };
}
function getInf(infs: T.InflectorOutput, t: "plural" | "arabicPlural" | "inflections", gender: T.Gender, plural: boolean): T.PsString[] {
// @ts-ignore
if (infs && t in infs && infs[t] !== undefined && gender in infs[t] && infs[t][gender] !== undefined) {
// @ts-ignore
const iset = infs[t][gender] as T.InflectionSet;
const ipick = iset[(t === "inflections" && plural) ? 1 : 0];
return ipick;
}
return [];
}
export function getEnglishParticiple(entry: T.DictionaryEntry): string {
if (!entry.ec) {
console.log("errored participle");
console.log(entry);
throw new Error("no english information for participle");
}
const ec = parseEc(entry.ec);
const participle = ec[2];
return (entry.ep)
? `${participle} ${entry.ep}`
: participle;
}
function evaluateParticiple({ entry: { entry }}: Participle): { ps: T.PsString[], e: string } {
return {
ps: [psStringFromEntry(entry)],
e: getEnglishParticiple(entry),
};
}
function getEnglishFromNoun(entry: T.DictionaryEntry, number: NounNumber): string {
const articles = {
singular: "(a/the)",
plural: "(the)",
};
const article = articles[number];
function addArticle(s: string) {
return `${article} ${s}`;
}
const e = getEnglishWord(entry);
if (!e) throw new Error(`unable to get english from subject ${entry.f} - ${entry.ts}`);
if (typeof e === "string") return ` ${e}`;
if (number === "plural") return addArticle(e.plural);
if (!e.singular || e.singular === undefined) {
throw new Error(`unable to get english from subject ${entry.f} - ${entry.ts}`);
}
return addArticle(e.singular);
}

View File

@ -248,7 +248,7 @@ const abilities: {
{ {
in: { in: {
subject: { subject: {
entry: {"ts":1527815008,"i":8433,"p":"شودې","f":"shoodé","g":"shoode","e":"milk","c":"n. f. pl."} as PluralEntry<Noun>, entry: {"ts":1527815008,"i":8433,"p":"شودې","f":"shoodé","g":"shoode","e":"milk","c":"n. f. pl."} as PluralNounEntry<Noun>,
plural: true, plural: true,
}, },
predicate: {"ts":1527812796,"i":8578,"p":"ښه","f":"xu","g":"xu","e":"good","c":"adj."} as Adjective, predicate: {"ts":1527812796,"i":8578,"p":"ښه","f":"xu","g":"xu","e":"good","c":"adj."} as Adjective,
@ -264,7 +264,7 @@ const abilities: {
{ {
in: { in: {
subject: { subject: {
entry: {"ts":1527817330,"i":9204,"p":"غنم","f":"ghanúm","g":"ghanum","e":"wheat","c":"n. m. pl."} as PluralEntry<Noun>, entry: {"ts":1527817330,"i":9204,"p":"غنم","f":"ghanúm","g":"ghanum","e":"wheat","c":"n. m. pl."} as PluralNounEntry<Noun>,
plural: true, plural: true,
}, },
predicate: {"ts":1527815451,"i":7192,"p":"زوړ","f":"zoR","g":"zoR","e":"old","c":"adj. irreg.","infap":"زاړه","infaf":"zaaRu","infbp":"زړ","infbf":"zaR"} as Adjective, predicate: {"ts":1527815451,"i":7192,"p":"زوړ","f":"zoR","g":"zoR","e":"old","c":"adj. irreg.","infap":"زاړه","infaf":"zaaRu","infbp":"زړ","infbf":"zaR"} as Adjective,

View File

@ -1,6 +1,6 @@
import shuffle from "./shuffle-array"; import shuffle from "./shuffle-array";
export const startingWord = (words: Readonly<(Noun | Adjective)[]>, p: string) => { export const startingWord = (words: Readonly<(NounEntry | AdjectiveEntry)[]>, p: string) => {
const firstWord = words.find(w => w.p === p); const firstWord = words.find(w => w.p === p);
return [ return [
...firstWord ? [firstWord] : [], ...firstWord ? [firstWord] : [],

View File

@ -1,3 +1,15 @@
import {
Types as T,
removeFVarients,
} from "@lingdocs/pashto-inflector";
export function firstVariation(s: string): string { export function firstVariation(s: string): string {
return s.split(/[,|;]/)[0].trim(); return s.split(/[,|;]/)[0].trim();
}
export function psStringFromEntry(entry: T.PsString): T.PsString {
return {
p: entry.p,
f: removeFVarients(entry.f),
};
} }

View File

@ -5,39 +5,48 @@ import {
Types as T, Types as T,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
export function isNoun(e: Word): e is Noun { export function isNounEntry(e: Entry): e is NounEntry {
if ("entry" in e) return false; if ("entry" in e) return false;
return !!(e.c && (e.c.includes("n. m.") || e.c.includes("n. f."))); return !!(e.c && (e.c.includes("n. m.") || e.c.includes("n. f.")));
} }
export function isAdjective(e: Word): e is Adjective { export function isAdjectiveEntry(e: Entry): e is AdjectiveEntry {
if ("entry" in e) return false; if ("entry" in e) return false;
return !!e.c?.includes("adj.") && !isNoun(e); return !!e.c?.includes("adj.") && !isNounEntry(e);
} }
export function isNounOrAdj(e: Word): e is (Noun | Adjective) { export function isAdverbEntry(e: Entry): e is AdverbEntry {
return isNoun(e) || isAdjective(e); if ("entry" in e) return false;
return !!e.c?.includes("adv.");
} }
export function isVerb(e: Word): e is Verb { export function isLocativeAdverbEntry(e: Entry): e is LocativeAdverbEntry {
return isAdverbEntry(e) && e.c.includes("loc. adv.");
}
export function isNounOrAdjEntry(e: Entry): e is (NounEntry | AdjectiveEntry) {
return isNounEntry(e) || isAdjectiveEntry(e);
}
export function isVerbEntry(e: Entry): e is VerbEntry {
return "entry" in e && !!e.entry.c?.startsWith("v."); return "entry" in e && !!e.entry.c?.startsWith("v.");
} }
export function isMascNoun(e: Noun | Adjective): e is MascNoun { export function isMascNounEntry(e: NounEntry | AdjectiveEntry): e is MascNounEntry {
return !!e.c && e.c.includes("n. m."); return !!e.c && e.c.includes("n. m.");
} }
export function isFemNoun(e: Noun | Adjective): e is FemNoun { export function isFemNounEntry(e: NounEntry | AdjectiveEntry): e is FemNounEntry {
return !!e.c && e.c.includes("n. f."); return !!e.c && e.c.includes("n. f.");
} }
export function isUnisexNoun(e: Noun | Adjective): e is UnisexNoun { export function isUnisexNounEntry(e: NounEntry | AdjectiveEntry): e is UnisexNounEntry {
return isNoun(e) && e.c.includes("unisex"); return isNounEntry(e) && e.c.includes("unisex");
} }
export function isAdjOrUnisexNoun(e: Word): e is (Adjective | UnisexNoun) { export function isAdjOrUnisexNounEntry(e: Entry): e is (AdjectiveEntry | UnisexNounEntry) {
return isAdjective(e) || ( return isAdjectiveEntry(e) || (
isNoun(e) && isUnisexNoun(e) isNounEntry(e) && isUnisexNounEntry(e)
); );
} }
@ -47,10 +56,10 @@ export function isAdjOrUnisexNoun(e: Word): e is (Adjective | UnisexNoun) {
* @param e * @param e
* @returns * @returns
*/ */
export function isPattern1Word<T extends (Noun | Adjective)>(e: T): e is Pattern1Word<T> { export function isPattern1Entry<T extends (NounEntry | AdjectiveEntry)>(e: T): e is Pattern1Entry<T> {
if (e.noInf) return false; if (e.noInf) return false;
if (e.infap) return false; if (e.infap) return false;
if (isFemNoun(e)) { if (isFemNounEntry(e)) {
return ( return (
endsWith([{ p: "ه", f: "a" }, { p: "ح", f: "a" }], e) || endsWith([{ p: "ه", f: "a" }, { p: "ح", f: "a" }], e) ||
(endsWith({ p: pashtoConsonants }, e) && !e.c.includes("anim.")) (endsWith({ p: pashtoConsonants }, e) && !e.c.includes("anim."))
@ -69,10 +78,10 @@ export function isPattern1Word<T extends (Noun | Adjective)>(e: T): e is Pattern
* @param e * @param e
* @returns * @returns
*/ */
export function isPattern2Word<T extends (Noun | Adjective)>(e: T): e is Pattern2Word<T> { export function isPattern2Entry<T extends (NounEntry | AdjectiveEntry)>(e: T): e is Pattern2Entry<T> {
if (e.noInf) return false; if (e.noInf) return false;
if (e.infap) return false; if (e.infap) return false;
if (isFemNoun(e)) { if (isFemNounEntry(e)) {
return !e.c.includes("pl.") && endsWith({ p: "ې", f: "e" }, e, true); return !e.c.includes("pl.") && endsWith({ p: "ې", f: "e" }, e, true);
} }
// TODO: check if it's a single syllable word, in which case it would be pattern 1 // TODO: check if it's a single syllable word, in which case it would be pattern 1
@ -85,10 +94,10 @@ export function isPattern2Word<T extends (Noun | Adjective)>(e: T): e is Pattern
* @param e * @param e
* @returns * @returns
*/ */
export function isPattern3Word<T extends (Noun | Adjective)>(e: T): e is Pattern3Word<T> { export function isPattern3Entry<T extends (NounEntry | AdjectiveEntry)>(e: T): e is Pattern3Entry<T> {
if (e.noInf) return false; if (e.noInf) return false;
if (e.infap) return false; if (e.infap) return false;
if (isFemNoun(e)) { if (isFemNounEntry(e)) {
return endsWith({ p: "ۍ" }, e); return endsWith({ p: "ۍ" }, e);
} }
return (countSyllables(e.f) > 1) return (countSyllables(e.f) > 1)
@ -102,7 +111,7 @@ export function isPattern3Word<T extends (Noun | Adjective)>(e: T): e is Pattern
* @param e * @param e
* @returns * @returns
*/ */
export function isPattern4Word<T extends (Noun | Adjective)>(e: T): e is Pattern4Word<T> { export function isPattern4Entry<T extends (NounEntry | AdjectiveEntry)>(e: T): e is Pattern4Entry<T> {
if (e.noInf) return false; if (e.noInf) return false;
return ( return (
!!(e.infap && e.infaf && e.infbp && e.infbf) !!(e.infap && e.infaf && e.infbp && e.infbf)
@ -117,7 +126,7 @@ export function isPattern4Word<T extends (Noun | Adjective)>(e: T): e is Pattern
* @param e * @param e
* @returns * @returns
*/ */
export function isPattern5Word<T extends (Noun | Adjective)>(e: T): e is Pattern5Word<T> { export function isPattern5Entry<T extends (NounEntry | AdjectiveEntry)>(e: T): e is Pattern5Entry<T> {
if (e.noInf) return false; if (e.noInf) return false;
return ( return (
!!(e.infap && e.infaf && e.infbp && e.infbf) !!(e.infap && e.infaf && e.infbp && e.infbf)
@ -128,18 +137,18 @@ export function isPattern5Word<T extends (Noun | Adjective)>(e: T): e is Pattern
); );
} }
export function isPattern6FemNoun(e: FemNoun): e is Pattern6FemNoun<FemNoun> { export function isPattern6FemEntry(e: FemNounEntry): e is Pattern6FemEntry<FemNounEntry> {
if (!isFemNoun(e)) return false; if (!isFemNounEntry(e)) return false;
if (e.c.includes("anim.")) return false; if (e.c.includes("anim.")) return false;
return e.p.slice(-1) === "ي"; return e.p.slice(-1) === "ي";
} }
export function isPluralEntry<U extends Noun>(e: U): e is PluralEntry<U> { export function isPluralNounEntry<U extends NounEntry>(e: U): e is PluralNounEntry<U> {
return e.c.includes("pl."); return e.c.includes("pl.");
} }
export function isSingularEntry<U extends Noun>(e: U): e is SingularEntry<U> { export function isSingularEntry<U extends NounEntry>(e: U): e is SingularEntry<U> {
return !isPluralEntry(e); return !isPluralNounEntry(e);
} }
export function isArrayOneOrMore<U>(a: U[]): a is T.ArrayOneOrMore<U> { export function isArrayOneOrMore<U>(a: U[]): a is T.ArrayOneOrMore<U> {

61
src/types.d.ts vendored
View File

@ -1,61 +0,0 @@
type Progress = {
total: number,
current: number,
};
type Current<T> = {
progress: Progress,
question: T,
};
type QuestionGenerator<T> = Generator<Current<T>, void, unknown>;
type QuestionDisplayProps<T> = {
question: T,
callback: (correct: boolean) => void,
};
type GameRecord = {
title: string,
id: string,
studyLink: string,
Game: () => JSX.Element,
};
type Noun = import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { c: string } & { __brand: "a noun entry" };
type MascNoun = Noun & { __brand2: "a masc noun entry" };
type FemNoun = Noun & { __brand2: "a fem noun entry" };
type UnisexNoun = MascNoun & { __brand3: "a unisex noun entry" };
type Adjective = import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { c: string } & { __brand: "an adjective entry" };
type Verb = {
entry: import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { __brand: "a verb entry" },
complement?: import("@lingdocs/pashto-inflector").Types.DictionaryEntry,
};
type SingularEntry<T extends Noun> = T & { __brand7: "a singular noun - as opposed to an always plural noun" };
type PluralEntry<T extends Noun> = T & { __brand7: "a noun that is always plural" };
type RawWord = T.DictionaryEntry | {
entry: T.DictionaryEntry,
complement?: T.DictionaryEntry,
};
// TODO: Write type predicates for these
type Pattern1Word<T> = T & { __brand3: "basic inflection pattern" };
type Pattern2Word<T> = T & { __brand3: "ending in unstressed ی pattern" };
type Pattern3Word<T> = T & { __brand3: "ending in stressed ی pattern" };
type Pattern4Word<T> = T & { __brand3: "Pashtoon pattern" };
type Pattern5Word<T> = T & { __brand3: "short squish pattern" };
type Pattern6FemNoun<T extends FemNoun> = FemNoun & { __brand3: "non anim. ending in ي" };
type NonInflecting<T> = T & { __brand3: "non-inflecting" };
// PLUS FEM INFLECTING
type Word = Noun | Adjective | Verb;
type Words = {
nouns: Noun[],
adjectives: Adjective[],
verbs: Verb[],
}
type EquativeTense = "present" | "subjunctive" | "past" | "future" | "wouldBe" | "pastSubjunctive";

32
src/types/entry-types.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
type NounEntry = import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { c: string } & { __brand: "a noun entry" };
type MascNounEntry = NounEntry & { __brand2: "a masc noun entry" };
type FemNounEntry = NounEntry & { __brand2: "a fem noun entry" };
type UnisexNounEntry = MascNounEntry & { __brand3: "a unisex noun entry" };
type AdverbEntry = import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { c: string } & { __brand: "an adverb entry" };
type LocativeAdverbEntry = AdverbEntry & { __brand2: "a locative adverb entry" };
type AdjectiveEntry = import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { c: string } & { __brand: "an adjective entry" };
type VerbEntry = {
entry: import("@lingdocs/pashto-inflector").Types.DictionaryEntry & { __brand: "a verb entry" },
// TODO: the compliment could also be typed? Maybe?
complement?: import("@lingdocs/pashto-inflector").Types.DictionaryEntry,
};
type SingularEntry<T extends NounEntry> = T & { __brand7: "a singular noun - as opposed to an always plural noun" };
type PluralNounEntry<T extends NounEntry> = T & { __brand7: "a noun that is always plural" };
type Pattern1Entry<T> = T & { __brand3: "basic inflection pattern" };
type Pattern2Entry<T> = T & { __brand3: "ending in unstressed ی pattern" };
type Pattern3Entry<T> = T & { __brand3: "ending in stressed ی pattern" };
type Pattern4Entry<T> = T & { __brand3: "Pashtoon pattern" };
type Pattern5Entry<T> = T & { __brand3: "short squish pattern" };
type Pattern6FemEntry<T extends FemNounEntry> = T & { __brand3: "non anim. ending in ي" };
type NonInflecting<T> = T & { __brand3: "non-inflecting" };
type Entry = NounEntry | AdjectiveEntry | AdverbEntry | VerbEntry;
type Words = {
nouns: NounEntry[],
adjectives: AdjectiveEntry[],
verbs: VerbEntry[],
adverbs: AdverbEntry[],
}

23
src/types/game-types.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
type Progress = {
total: number,
current: number,
};
type Current<T> = {
progress: Progress,
question: T,
};
type QuestionGenerator<T> = Generator<Current<T>, void, unknown>;
type QuestionDisplayProps<T> = {
question: T,
callback: (correct: boolean) => void,
};
type GameRecord = {
title: string,
id: string,
studyLink: string,
Game: () => JSX.Element,
};

63
src/types/gramm-types.d.ts vendored Normal file
View File

@ -0,0 +1,63 @@
type EquativeTense = "present" | "subjunctive" | "past" | "future" | "wouldBe" | "pastSubjunctive";
type NounNumber = "singular" | "plural";
type EquativeClause = {
subject: NounPhrase,
predicate: NounPhrase | Compliment,
tense: EquativeTense,
};
type EquativeClauseOutput = {
subject: {
ps: (import("@lingdocs/pashto-inflector").Types.PsString)[],
e: string,
},
predicate: {
ps: (import("@lingdocs/pashto-inflector").Types.PsString)[],
e: string,
},
ba: boolean,
equative: {
ps: import("@lingdocs/pashto-inflector").Types.SentenceForm,
e: string[],
},
};
type NounPhrase = Pronoun | Noun | Participle;
// TODO: better, simpler type here
type Noun = {
type: "unisex noun",
number: NounNumber,
gender: import("@lingdocs/pashto-inflector").Types.Gender,
entry: UnisexNounEntry,
possesor?: Noun,
adjectives?: AdjectiveEntry[],
} | {
type: "plural noun",
entry: PluralNounEntry<MascNounEntry | FemNounEntry>,
possesor?: Noun,
adjectives?: AdjectiveEntry[],
} | {
type: "singular noun",
number: NounNumber,
entry: SingularEntry<MascNounEntry | FemNounEntry>,
possesor?: Noun,
adjectives?: AdjectiveEntry[],
};
type Compliment = {
type: "compliment",
entry: AdjectiveEntry | LocativeAdverbEntry | NounEntry,
};
type Participle = {
type: "participle",
entry: VerbEntry,
}
type Pronoun = {
type: "pronoun",
pronounType: "near" | "far",
person: import("@lingdocs/pashto-inflector").Types.Person,
};

View File

@ -0,0 +1,11 @@
module.exports = [
{ ts: 1527811221, e: `above, overhead` }, // پورته - porta
{ ts: 1578080952673, e: `outside, outside of, beyond` }, // دباندې - dubaande
{ ts: 1527812558, e: `here` }, // دلته - dălta
{ ts: 1527812449, e: `there` }, // هلته - halta
{ ts: 1527813122, e: `inside, within, interior` }, // دننه - dununa
{ ts: 1527812780, e: `down, beneath` }, // ښکته - xkuta
{ ts: 1527814605, e: `far, distant` }, // لرې - lure
{ ts: 1527814708, e: `close, near, soon, almost` }, // نژدې - nijzde, najzde
{ ts: 1527814911, e: `near, close to, almost` }, // نږدې - naGde
];

View File

@ -0,0 +1,5 @@
module.exports = [
{"ts":1527812798,"i":5593,"p":"خفه","f":"khúfa","g":"khufa","e":"sad, upset, angry; choked, suffocated","c":"adj."},
{"ts":1527812792,"i":5773,"p":"خوشاله","f":"khoshaala","g":"khoshaala","e":"happy, glad","c":"adj."},
{"ts":1527812761,"i":8534,"p":"ښایسته","f":"xáaysta","g":"xaayusta","e":"beautiful","c":"adj."},
];

File diff suppressed because one or more lines are too long

View File

@ -1,20 +1,22 @@
import rawWords from "./raw-words"; import rawWords from "./raw-words";
import { import {
isAdjective, isAdjectiveEntry,
isNoun, isNounEntry,
isVerb, isVerbEntry,
isAdverbEntry,
} from "../lib/type-predicates"; } from "../lib/type-predicates";
import { categorize } from "../lib/categorize"; import { categorize } from "../lib/categorize";
const words = categorize<Word, Words>(rawWords, { const words = categorize<Entry, Words>(rawWords, {
"nouns": isNoun, "nouns": isNounEntry,
"adjectives": isAdjective, "adjectives": isAdjectiveEntry,
"verbs": isVerb, "verbs": isVerbEntry,
"adverbs": isAdverbEntry,
}); });
export default words; export default words;
export const { nouns, adjectives, verbs } = words; export const { nouns, adjectives, verbs, adverbs } = words;
// console.log( // console.log(
// Object.entries( // Object.entries(

View File

@ -1684,10 +1684,10 @@
pbf "^3.2.1" pbf "^3.2.1"
rambda "^6.7.0" rambda "^6.7.0"
"@lingdocs/pashto-inflector@^1.3.2": "@lingdocs/pashto-inflector@^1.3.5":
version "1.3.2" version "1.3.5"
resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-1.3.2.tgz#a5501d9a56c8590eee052a88ffdbe38a07594f29" resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-1.3.5.tgz#cfc311462c686e5591f12ee68b333a2ec479f428"
integrity sha512-crZlvjLssb+Q6kh6sAHlg4A4BerJnurSFUMz0fyYl3PlULsZyVHGvKepHYhP5AXyWt34zGiTzHTWapzIWsWtiQ== integrity sha512-Asxu773Z9+0Gxi8wkSD6x3JYcSeJmXqgoHQu1Hvph1oOAac3t8Bn0uXC/OGO5p1leilEHePJT+hkMFmURTArcw==
dependencies: dependencies:
classnames "^2.2.6" classnames "^2.2.6"
pbf "^3.2.1" pbf "^3.2.1"