phonetics update

This commit is contained in:
adueck 2023-07-28 10:25:39 +04:00
parent ddf967ae63
commit c82fc04821
3 changed files with 262 additions and 212 deletions

View File

@ -1,14 +1,14 @@
import GameCore from "../GameCore"; import GameCore from "../GameCore";
import { import {
Types as T, Types as T,
getInflectionPattern, getInflectionPattern,
Examples, Examples,
defaultTextOptions as opts, defaultTextOptions as opts,
firstVariation, firstVariation,
inflectWord, inflectWord,
HumanReadableInflectionPattern, HumanReadableInflectionPattern,
isUnisexSet, isUnisexSet,
InflectionsTable, InflectionsTable,
} from "@lingdocs/ps-react"; } from "@lingdocs/ps-react";
import { makePool } from "../../lib/pool"; import { makePool } from "../../lib/pool";
import { nouns, adjectives } from "../../words/words"; import { nouns, adjectives } from "../../words/words";
@ -20,188 +20,240 @@ const amount = 8;
const timeLimit = 300; const timeLimit = 300;
type Question = { type Question = {
entry: T.NounEntry | T.AdjectiveEntry, entry: T.NounEntry | T.AdjectiveEntry;
inflections: T.Inflections, inflections: T.Inflections;
}; };
type InfFormContent = { type InfFormContent = {
masc: [string, string, string], masc: [string, string, string];
fem: [string, string, string], fem: [string, string, string];
}; };
export default function InflectionsWriting({ inChapter, id, link, level }: { export default function InflectionsWriting({
inChapter: boolean, inChapter,
id: string, id,
link: string, link,
level: T.InflectionPattern, level,
}: {
inChapter: boolean;
id: string;
link: string;
level: T.InflectionPattern;
}) { }) {
const wordPool = makePool( const wordPool = makePool(
[...nouns, ...adjectives] [...nouns, ...adjectives].filter((x) => {
.filter(x => { if (isAdverbEntry(x)) return false;
if (isAdverbEntry(x)) return false; const infs = inflectWord(x);
const infs = inflectWord(x); if (!infs || !infs.inflections) return false;
if (!infs || !infs.inflections) return false; return getInflectionPattern(x) === level;
return (getInflectionPattern(x) === level); })
}) );
);
function getQuestion(): Question { function getQuestion(): Question {
const word = wordPool(); const word = wordPool();
const r = inflectWord(word); const r = inflectWord(word);
if (!r || !r.inflections) { if (!r || !r.inflections) {
throw new Error(`error getting inflections for ${word.f}`); throw new Error(`error getting inflections for ${word.f}`);
} }
return { return {
entry: word, entry: word,
inflections: r.inflections, inflections: r.inflections,
};
}; };
}
function Display({ question, callback }: QuestionDisplayProps<Question>) {
function handleAnswer(inf: InfFormContent) {
callback(infAnswerCorrect(inf, question.inflections));
}
return <div>
<div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
<Examples opts={opts}>{[
{
p: firstVariation(question.entry.p),
f: firstVariation(question.entry.f),
e: `${firstVariation(question.entry.e)} - ${question.entry.c}`,
}
]}</Examples>
</div>
<InflectionTableForm
onSubmit={handleAnswer}
question={question}
genders={isUnisexSet(question.inflections)
? "unisex"
: "masc" in question.inflections
? "masc"
: "fem"}
/>
</div>
}
function Instructions() {
return <div>
<p className="lead">Complete the inflections for the <strong>{HumanReadableInflectionPattern(level, opts)}</strong> pattern word</p>
</div>
}
return <GameCore function Display({ question, callback }: QuestionDisplayProps<Question>) {
inChapter={inChapter} function handleAnswer(inf: InfFormContent) {
studyLink={link} callback(infAnswerCorrect(inf, question.inflections));
getQuestion={getQuestion} }
id={id} return (
Display={Display} <div>
DisplayCorrectAnswer={DisplayCorrectAnswer} <div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
timeLimit={timeLimit} <Examples opts={opts}>
amount={amount} {[
Instructions={Instructions} {
p: firstVariation(question.entry.p),
f: firstVariation(question.entry.f),
e: `${firstVariation(question.entry.e)} - ${question.entry.c}`,
},
]}
</Examples>
</div>
<InflectionTableForm
onSubmit={handleAnswer}
question={question}
genders={
isUnisexSet(question.inflections)
? "unisex"
: "masc" in question.inflections
? "masc"
: "fem"
}
/>
</div>
);
}
function Instructions() {
return (
<div>
<p className="lead">
Complete the inflections for the{" "}
<strong>{HumanReadableInflectionPattern(level, opts)}</strong> pattern
word
</p>
</div>
);
}
return (
<GameCore
inChapter={inChapter}
studyLink={link}
getQuestion={getQuestion}
id={id}
Display={Display}
DisplayCorrectAnswer={DisplayCorrectAnswer}
timeLimit={timeLimit}
amount={amount}
Instructions={Instructions}
/> />
}; );
function InflectionTableForm({ onSubmit, genders, question }: {
onSubmit: (i: InfFormContent) => void,
genders: T.Gender | "unisex",
question: Question,
}) {
const [inf, setInf] = useState<InfFormContent>({ fem: ["", "", ""], masc: ["", "", ""] });
const mascInputRef = useRef<HTMLInputElement>(null);
const femInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setInf({ fem: ["", "", ""], masc: ["", "", ""] });
femInputRef.current && femInputRef.current.focus();
mascInputRef.current && mascInputRef.current.focus();
}, [question, setInf]);
function handleClearInf() {
setInf({ fem: ["", "", ""], masc: ["", "", ""] });
}
function handleInfInput(gender: T.Gender, inflection: number) {
return ({ target: { value }}: ChangeEvent<HTMLInputElement>) => {
inf[gender][inflection] = value;
setInf({...inf});
};
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
onSubmit(inf);
}
return <form onSubmit={handleSubmit}>
<table className="table" style={{ tableLayout: "fixed" }}>
<thead>
<tr>
<th scope="col" style={{ width: "3.5rem" }}></th>
{genders !== "fem" && <th scope="col" style={{ maxWidth: "10rem", textAlign: "left" }}>Masculine</th>}
{genders !== "masc" && <th scope="col" style={{ maxWidth: "10rem", textAlign: "left" }}>Feminine</th>}
</tr>
</thead>
<tbody>
{["Plain", "1st", "2nd"].map((title, i) => (
<tr key={title}>
<th scope="row">{title}</th>
{genders !== "fem" && <td>
<input
ref={i === 0 ? mascInputRef : undefined}
type="text"
className="form-control"
autoComplete="off"
autoCapitalize="off"
spellCheck="false"
dir="auto"
value={inf.masc[i]}
onChange={handleInfInput("masc", i)}
style={{ maxWidth: "12rem" }}
/>
</td>}
{genders !== "masc" && <td>
<input
ref={i === 0 ? femInputRef : undefined}
type="text"
className="form-control"
autoComplete="off"
autoCapitalize="off"
spellCheck="false"
dir="auto"
value={inf.fem[i]}
onChange={handleInfInput("fem", i)}
style={{ maxWidth: "12rem" }}
/>
</td>}
</tr>
))}
</tbody>
</table>
<div className="text-center">
<button type="button" className="btn btn-secondary mx-3" onClick={handleClearInf}>Clear</button>
<button type="submit" className="btn btn-primary mx-3">Submit</button>
</div>
</form>;
} }
function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element { function InflectionTableForm({
return <div> onSubmit,
<InflectionsTable genders,
inf={question.inflections} question,
textOptions={opts} }: {
hideTitle onSubmit: (i: InfFormContent) => void;
/> genders: T.Gender | "unisex";
</div>; question: Question;
}) {
const [inf, setInf] = useState<InfFormContent>({
fem: ["", "", ""],
masc: ["", "", ""],
});
const mascInputRef = useRef<HTMLInputElement>(null);
const femInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setInf({ fem: ["", "", ""], masc: ["", "", ""] });
femInputRef.current && femInputRef.current.focus();
mascInputRef.current && mascInputRef.current.focus();
}, [question, setInf]);
function handleClearInf() {
setInf({ fem: ["", "", ""], masc: ["", "", ""] });
}
function handleInfInput(gender: T.Gender, inflection: number) {
return ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
inf[gender][inflection] = value;
setInf({ ...inf });
};
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
onSubmit(inf);
}
return (
<form onSubmit={handleSubmit}>
<table className="table" style={{ tableLayout: "fixed" }}>
<thead>
<tr>
<th scope="col" style={{ width: "3.5rem" }}></th>
{genders !== "fem" && (
<th scope="col" style={{ maxWidth: "10rem", textAlign: "left" }}>
Masculine
</th>
)}
{genders !== "masc" && (
<th scope="col" style={{ maxWidth: "10rem", textAlign: "left" }}>
Feminine
</th>
)}
</tr>
</thead>
<tbody>
{["Plain", "1st", "2nd"].map((title, i) => (
<tr key={title}>
<th scope="row">{title}</th>
{genders !== "fem" && (
<td>
<input
ref={i === 0 ? mascInputRef : undefined}
type="text"
className="form-control"
autoComplete="off"
autoCapitalize="off"
spellCheck="false"
dir="auto"
value={inf.masc[i]}
onChange={handleInfInput("masc", i)}
style={{ maxWidth: "12rem" }}
/>
</td>
)}
{genders !== "masc" && (
<td>
<input
ref={i === 0 ? femInputRef : undefined}
type="text"
className="form-control"
autoComplete="off"
autoCapitalize="off"
spellCheck="false"
dir="auto"
value={inf.fem[i]}
onChange={handleInfInput("fem", i)}
style={{ maxWidth: "12rem" }}
/>
</td>
)}
</tr>
))}
</tbody>
</table>
<div className="text-center">
<button
type="button"
className="btn btn-secondary mx-3"
onClick={handleClearInf}
>
Clear
</button>
<button type="submit" className="btn btn-primary mx-3">
Submit
</button>
</div>
</form>
);
}
function DisplayCorrectAnswer({
question,
}: {
question: Question;
}): JSX.Element {
return (
<div>
<InflectionsTable
inf={question.inflections}
textOptions={opts}
hideTitle
/>
</div>
);
} }
function infAnswerCorrect(answer: InfFormContent, inf: T.Inflections): boolean { function infAnswerCorrect(answer: InfFormContent, inf: T.Inflections): boolean {
function genInfCorrect(gender: T.Gender): boolean { function genInfCorrect(gender: T.Gender): boolean {
// @ts-ignore // @ts-ignore
const genInf = inf[gender] as T.InflectionSet; const genInf = inf[gender] as T.InflectionSet;
return genInf.every((x, i) => ( return genInf.every((x, i) =>
x.some(ps => comparePs(answer[gender][i], ps)) x.some((ps) => comparePs(answer[gender][i], [ps]))
)); );
} }
if (isUnisexSet(inf)) { if (isUnisexSet(inf)) {
return genInfCorrect("masc") && genInfCorrect("fem"); return genInfCorrect("masc") && genInfCorrect("fem");
} }
return genInfCorrect("masc" in inf ? "masc": "fem"); return genInfCorrect("masc" in inf ? "masc" : "fem");
} }

View File

@ -414,8 +414,8 @@ function addUserAnswer(
function addBa(x: T.PsString) { function addBa(x: T.PsString) {
if (!a.withBa) return x; if (!a.withBa) return x;
return { return {
p: x.p.replace(kidsBlank.p, baParticle.p), p: x.p.replace(kidsBlank.p, baParticle.p + " "),
f: x.f.replace(kidsBlank.f, baParticle.f), f: x.f.replace(kidsBlank.f, baParticle.f + " "),
}; };
} }
function addAnswer(x: T.PsString): T.PsString { function addAnswer(x: T.PsString): T.PsString {

View File

@ -1,46 +1,44 @@
import { import {
removeAccents, removeAccents,
hasAccents, hasAccents,
Types as T, Types as T,
standardizePashto, standardizePashto,
standardizePhonetics, standardizePhonetics,
flattenLengths, flattenLengths,
} from "@lingdocs/ps-react"; } from "@lingdocs/ps-react";
import { removeAShort } from "./misc-helpers"; import { removeAShort } from "./misc-helpers";
export function getPercentageDone(current: number, total: number): number { export function getPercentageDone(current: number, total: number): number {
return Math.round( return Math.round((current / (total + 1)) * 100);
(current / (total + 1)) * 100
);
} }
/** /**
* Says if an input written in phonetics by the user is correct/the same as a given answer * Says if an input written in phonetics by the user is correct/the same as a given answer
* *
* The user is allowed to leave out the accents, but if they include them they must be the same as the answer * The user is allowed to leave out the accents, but if they include them they must be the same as the answer
* *
* @param input - the answer given by the user in phonetics * @param input - the answer given by the user in phonetics
* @param answer - the correct answer in phonetics * @param answer - the correct answer in phonetics
*/ */
export function compareF(input: string, answer: string): boolean { export function compareF(input: string, answer: string): boolean {
const inp = removeAShort(input); const inp = removeAShort(input);
const ans = removeAShort(answer); const ans = removeAShort(answer);
return inp === (hasAccents(inp) ? ans : removeAccents(ans)); return inp === (hasAccents(inp) ? ans : removeAccents(ans));
} }
export function comparePs(inputRaw: string, answer: T.SingleOrLengthOpts<T.PsString | T.PsString[]>): boolean { export function comparePs(
function cleanSpaces(s: string): string { inputRaw: string,
return s.replace(/\s+/g, " "); answer: T.SingleOrLengthOpts<T.PsString[]>
} ): boolean {
const input = cleanSpaces(inputRaw); function cleanSpaces(s: string): string {
if ("long" in answer) { return s.replace(/\s+/g, " ");
return comparePs(input, flattenLengths(answer)); }
} const input = cleanSpaces(inputRaw);
if (Array.isArray(answer)) { if ("long" in answer) {
return answer.some(a => comparePs(input, a)); return comparePs(input, flattenLengths(answer));
} }
const stand = standardizePhonetics( return answer.some((a) => {
standardizePashto(input) const stand = standardizePhonetics(standardizePashto(input)).trim();
).trim(); return stand === cleanSpaces(a.p) || compareF(stand, cleanSpaces(a.f));
return stand === cleanSpaces(answer.p) || compareF(stand, cleanSpaces(answer.f)); });
} }