리펙토링: 코드 최적화
This commit is contained in:
@@ -51,6 +51,7 @@ components/
|
||||
lib/
|
||||
engine/ # 문제 생성 알고리즘 엔진
|
||||
parenthesis-sign.ts # 숫자 괄호 엔진
|
||||
parenthesis-latex.ts # 괄호 식 KaTeX 포맷 및 현재 항 강조 유틸
|
||||
variable-parenthesis.ts # 미지수 괄호 엔진
|
||||
types.ts # 숫자 괄호 타입
|
||||
variable-types.ts # 미지수 괄호 타입
|
||||
@@ -82,6 +83,7 @@ prisma/
|
||||
- 모든 수학 수식은 KaTeX로 렌더링한다. 일반 텍스트나 Unicode 수식 문자를 직접 사용하지 않는다.
|
||||
- KaTeX 컴포넌트는 `components/math/` 하위에 위치한다.
|
||||
- 현재 수식 컴포넌트는 `katex.renderToString()`을 직접 사용한다. 별도 래퍼 라이브러리를 추가하기 전에 Next.js App Router 클라이언트 번들에서 런타임 문제가 없는지 확인한다.
|
||||
- 괄호 식 수식 조립과 풀이 중 현재 항 강조는 `lib/engine/parenthesis-latex.ts`의 공통 유틸을 사용한다. 숫자/미지수 풀이 화면에 같은 포맷 로직을 중복 구현하지 않는다.
|
||||
|
||||
### 상태 관리
|
||||
|
||||
@@ -109,8 +111,8 @@ prisma/
|
||||
- 모든 항을 하나의 공유 input으로 입력한다.
|
||||
- input 위 우상단에는 각 항의 진행 상태를 progress 스타일로 표시한다.
|
||||
- 진행 상태는 값 입력 전에는 괄호 안 원래 항 값을 표시하고, 값 입력 중이거나 입력 후에는 사용자가 입력한 값을 표시한다.
|
||||
- 첫째 항에서 Enter를 누르면 같은 input이 둘째 항 입력으로 전환된다.
|
||||
- 둘째 항에서 Enter를 누르면 제출 동작을 실행한다. 별도 제출 버튼이 없어도 form submit 흐름이 유지되어야 한다.
|
||||
- Enter를 누르면 다음 항 입력으로 전환되고, 마지막 항에서 Enter를 누르면 제출 동작을 실행한다. 별도 제출 버튼이 없어도 form submit 흐름이 유지되어야 한다.
|
||||
- 풀이 중인 항의 수, 곱해지는 수, 해당 괄호는 KaTeX 색상으로 강조하고 나머지 항은 강조하지 않는다.
|
||||
- 터치 기반 입력 환경에서는 input focus 시 커스텀 숫자 키패드를 표시한다. 키패드는 숫자, `+/-`, `.`, `⌫`, `↵` 버튼을 제공한다.
|
||||
- 마우스/트랙패드처럼 hover 가능한 정밀 포인터가 있는 환경은 물리 키보드 사용 환경으로 간주하고 커스텀 숫자 키패드를 숨긴다. 웹 표준 API로 물리 키보드 연결 여부 자체를 안정적으로 판별할 수 없으므로 해상도 기준으로 분기하지 않는다. (`lib/input-device.ts`의 `DESKTOP_INPUT_DEVICE_QUERY` 사용)
|
||||
- 입력 중에는 `-`, `1.` 같은 임시 값을 허용할 수 있지만 제출 시에는 정상적인 양의 실수 또는 음의 실수 표현만 통과시킨다.
|
||||
@@ -120,10 +122,11 @@ prisma/
|
||||
#### 미지수 괄호 풀이 화면 (`/variable-parentheses/play`)
|
||||
|
||||
- 답을 직접 입력하지 않는다. 각 항의 보기 버튼 중에서 선택한다.
|
||||
- 첫째 항 선택 후 자동으로 둘째 항 보기로 focus가 이동한다.
|
||||
- 둘째 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 제출되어 피드백 화면으로 전환된다.
|
||||
- 항을 선택하면 자동으로 다음 항 보기로 focus가 이동한다.
|
||||
- 마지막 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 제출되어 피드백 화면으로 전환된다.
|
||||
- 입력 항 진행 상태는 progress 스타일로 표시한다. 선택 전에는 괄호 안 원래 항 값을 표시하고, 선택 후에는 고른 보기 값을 표시한다.
|
||||
- 선택된 보기 버튼은 `aria-pressed`로 표시한다.
|
||||
- 풀이 중인 항의 수, 곱해지는 수, 해당 괄호는 KaTeX 색상으로 강조하고 나머지 항은 강조하지 않는다.
|
||||
|
||||
### API / 백엔드
|
||||
|
||||
@@ -140,8 +143,11 @@ prisma/
|
||||
- 문제 세션에서 중복을 방지할 때는 랜덤 id가 아니라 수학적 문제 조건을 나타내는 안정적인 key를 기준으로 한다.
|
||||
- 모든 엔진은 전체 가능한 시드 목록을 모듈 로드 시 한 번 생성(`createAllProblemSeeds`)하고, 세션 생성 시 셔플 후 슬라이스하는 방식으로 중복 없이 출제한다.
|
||||
- 숫자 괄호 엔진: `lib/engine/parenthesis-sign.ts` / 타입: `lib/engine/types.ts`
|
||||
- 문제 구조는 `terms`, `operators`, `distributedTerms` 배열을 기준으로 한다. `a`, `b`, `distributedA`, `distributedB` 같은 2항 전용 필드를 추가하지 않는다.
|
||||
- 채점 입력은 `{ terms: number[] }` 형태만 사용한다.
|
||||
- 미지수 괄호 엔진: `lib/engine/variable-parenthesis.ts` / 타입: `lib/engine/variable-types.ts`
|
||||
- 괄호 안 두 항 중 정확히 하나가 `정수 × x` 항이다.
|
||||
- 괄호 안 항 중 정확히 하나가 `정수 × x` 항이다.
|
||||
- 문제 구조는 `terms`, `operators`, `correctTerms`, `choices` 배열을 기준으로 한다. `firstTerm`, `secondTerm`, `operator` 같은 2항 전용 필드를 추가하지 않는다.
|
||||
- 각 항의 보기는 정답 계수의 절대값 기준 음수/양수 두 개를 셔플해 제공한다.
|
||||
- 채점은 choice id 기반으로 수행한다.
|
||||
|
||||
@@ -194,9 +200,10 @@ NEXTAUTH_SECRET=your-secret
|
||||
```
|
||||
|
||||
- 시작 화면(`/`)은 학습 내용 선택 화면이다. `숫자 괄호 풀기`, `미지수 괄호 풀기`가 활성화되어 있고 분수/실수 괄호는 disabled 상태다.
|
||||
- 준비 화면은 문제 수 `[5] [10]` 버튼 그룹과 괄호 종류 버튼 그룹을 제공한다. 기본값은 `10`이며, 현재는 `()`만 선택 가능하다.
|
||||
- 준비 화면은 문제 수 `[5] [10]`, 괄호 종류, 괄호 안 최대 항 수 `[2] [3] [4]` 버튼 그룹을 제공한다. 기본 문제 수는 `10`, 기본 최대 항 수는 `2`이며, 괄호 종류는 현재 `()`만 선택 가능하다.
|
||||
- 준비 화면의 `연습 시작`은 Zustand 세션을 초기화하고 풀이 화면으로 이동한다.
|
||||
- 풀이 화면은 URL의 `count` 쿼리와 Zustand의 선택 문제 수를 동기화한다. 허용 문제 수는 `5`, `10`이며 유효하지 않은 값은 `10`으로 처리한다.
|
||||
- 풀이 화면은 URL의 `count`, `terms` 쿼리와 Zustand의 선택 문제 수·최대 항 수를 동기화한다. 허용 문제 수는 `5`, `10`이며 유효하지 않은 값은 `10`으로 처리한다. 허용 최대 항 수는 `2`, `3`, `4`이며 유효하지 않은 값은 `2`로 처리한다.
|
||||
- 풀이 화면의 문제 수식은 현재 풀고 있는 항의 수, 곱해지는 수, 해당 괄호를 강조 색상으로 표시하고 나머지 항은 기본 색상으로 표시한다.
|
||||
- 정답·오답 피드백은 `계산식 = 내가 입력한(고른) 답` 형식으로 표시한다. 정답은 `○`, 오답은 붉은색 답과 `×`, 굵은 정답값을 함께 보여준다.
|
||||
- 최종 결과 화면은 지금까지 푼 문제를 `문제 = 내가 입력한(고른) 답` 형식으로 나열하고, 피드백 화면과 같은 정답·오답 색상 및 기호 규칙을 사용한다.
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
- 시작 화면(`/`)에서 학습 내용을 선택합니다. `숫자 괄호 풀기`, `미지수 괄호 풀기`가 활성 상태이고 분수·실수 괄호는 준비 중으로 비활성화되어 있습니다.
|
||||
- 문제와 정답 수식은 KaTeX로 렌더링합니다.
|
||||
- 풀이 중인 문제 수식은 현재 항, 곱해지는 수, 해당 괄호를 강조 색상으로 표시하고 나머지 항은 기본 색상으로 표시합니다.
|
||||
- 진행 단계는 현재 문제, 완료된 정답, 완료된 오답, 남은 문제를 구분해서 표시합니다.
|
||||
- 정답·오답 피드백은 `계산식 = 내가 입력한(고른) 답` 형식으로 표시합니다.
|
||||
- 정답은 정답 색상 박스와 `○` 표시, 오답은 오답 색상 박스, 붉은색 답, `×`, 굵은 정답값을 표시합니다.
|
||||
@@ -55,20 +56,20 @@
|
||||
|
||||
### 숫자 괄호 풀기 (`/number-parentheses` → `/number-parentheses/play`)
|
||||
|
||||
- 준비 화면에서 문제 수 5 / 10을 선택하고 연습을 시작합니다. 기본값은 10.
|
||||
- 준비 화면에서 문제 수 5 / 10과 괄호 안 최대 항 수 2 / 3 / 4를 선택하고 연습을 시작합니다. 기본값은 문제 수 10, 최대 항 수 2입니다.
|
||||
- 괄호 종류는 현재 `()`만 선택 가능하고 `{}`, `[]`는 비활성화되어 있습니다.
|
||||
- 풀이 화면에서 두 항을 직접 숫자로 입력합니다.
|
||||
- 모든 항은 하나의 input에서 순서대로 입력하며, 첫째 항 Enter → 둘째 항 전환, 둘째 항 Enter → 제출.
|
||||
- 풀이 화면에서 각 항을 직접 숫자로 입력합니다.
|
||||
- 모든 항은 하나의 input에서 순서대로 입력하며, Enter로 다음 항으로 이동하고 마지막 항에서 Enter를 누르면 제출합니다.
|
||||
- input 위 우상단의 progress 표시에서 각 항의 진행 상태를 보여줍니다. 입력 전에는 괄호 안 원래 항 값, 입력 중/입력 후에는 사용자가 입력한 값을 표시합니다.
|
||||
- 터치 환경에서는 커스텀 숫자 키패드(숫자, `+/-`, `.`, `⌫`, `↵`)가 표시되며, 정밀 포인터가 있는 환경(마우스/트랙패드)에서는 숨깁니다.
|
||||
|
||||
### 미지수 괄호 풀기 (`/variable-parentheses` → `/variable-parentheses/play`)
|
||||
|
||||
- 준비 화면 구성은 숫자 괄호와 동일합니다.
|
||||
- 풀이 화면에서 두 항을 보기 중에서 선택합니다.
|
||||
- 풀이 화면에서 각 항을 보기 중에서 선택합니다.
|
||||
- 각 항의 보기는 정답 계수의 절대값을 기준으로 음수/양수 두 개를 제공합니다.
|
||||
- 첫째 항 선택 후 자동으로 둘째 항 보기로 focus가 이동합니다.
|
||||
- 둘째 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 피드백 화면으로 전환됩니다.
|
||||
- 항 선택 후 자동으로 다음 항 보기로 focus가 이동합니다.
|
||||
- 마지막 항을 선택하면 별도 `정답 확인` 버튼 없이 즉시 피드백 화면으로 전환됩니다.
|
||||
- 입력 항 progress 표시는 선택 전에는 괄호 안 원래 항 값, 선택 후에는 고른 보기 값을 표시합니다.
|
||||
|
||||
---
|
||||
@@ -141,7 +142,10 @@ components/
|
||||
lib/
|
||||
engine/ # 문제 생성 알고리즘 엔진
|
||||
parenthesis-sign.ts # 숫자 괄호 엔진
|
||||
parenthesis-latex.ts # 괄호 식 KaTeX 포맷 및 현재 항 강조 유틸
|
||||
variable-parenthesis.ts # 미지수 괄호 엔진
|
||||
types.ts # 숫자 괄호 타입
|
||||
variable-types.ts # 미지수 괄호 타입
|
||||
input-device.ts # 입력 장치 감지 미디어 쿼리
|
||||
prisma.ts # Prisma Client 싱글턴
|
||||
redis.ts # Redis Client 싱글턴
|
||||
|
||||
@@ -17,34 +17,9 @@ import { DESKTOP_INPUT_DEVICE_QUERY } from "@/lib/input-device";
|
||||
import KatexRenderer from "@/components/math/KatexRenderer";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import type { MaxTermCount, ProblemCount } from "@/store/gameStore";
|
||||
import type { ParenthesisSignProblem } from "@/lib/engine/types";
|
||||
import { formatParenthesisExpressionLatex } from "@/lib/engine/parenthesis-latex";
|
||||
|
||||
const REAL_NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/;
|
||||
const ACTIVE_TERM_COLOR = "#f97316";
|
||||
|
||||
function highlightLatex(latex: string) {
|
||||
return `\\textcolor{${ACTIVE_TERM_COLOR}}{${latex}}`;
|
||||
}
|
||||
|
||||
function formatActiveTermProblemLatex(
|
||||
problem: ParenthesisSignProblem,
|
||||
activeTermIndex: number,
|
||||
) {
|
||||
const termLatex = problem.terms
|
||||
.map((term, index) => {
|
||||
const displayTerm =
|
||||
index === activeTermIndex ? highlightLatex(`${term}`) : `${term}`;
|
||||
|
||||
if (index === 0) return displayTerm;
|
||||
|
||||
return `${problem.operators[index - 1]} ${displayTerm}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return `${highlightLatex(`${problem.multiplier}`)}${highlightLatex(
|
||||
"(",
|
||||
)}${termLatex}${highlightLatex(")")}`;
|
||||
}
|
||||
|
||||
function parseAnswer(value: string) {
|
||||
const trimmed = value.trim();
|
||||
@@ -233,7 +208,13 @@ export default function NumberPlayClient({
|
||||
const isLastProblem = session.currentIndex === session.problems.length - 1;
|
||||
const problemLatex = submitted
|
||||
? currentProblem.latexBefore
|
||||
: formatActiveTermProblemLatex(currentProblem, activeInput);
|
||||
: formatParenthesisExpressionLatex({
|
||||
multiplier: currentProblem.multiplier,
|
||||
terms: currentProblem.terms,
|
||||
operators: currentProblem.operators,
|
||||
formatTermLatex: (term) => `${term}`,
|
||||
activeTermIndex: activeInput,
|
||||
});
|
||||
const pendingAnswerLabels = currentProblem.terms.map((term, index) => {
|
||||
if (index === 0) return String(term);
|
||||
|
||||
|
||||
@@ -10,41 +10,13 @@ import VariableFeedbackPanel from "@/components/game/VariableFeedbackPanel";
|
||||
import VariableScoreSummary from "@/components/game/VariableScoreSummary";
|
||||
import KatexRenderer from "@/components/math/KatexRenderer";
|
||||
import { formatVariableTermLatex } from "@/lib/engine/variable-parenthesis";
|
||||
import type { VariableParenthesisProblem } from "@/lib/engine/variable-types";
|
||||
import { formatParenthesisExpressionLatex } from "@/lib/engine/parenthesis-latex";
|
||||
import {
|
||||
useVariableGameStore,
|
||||
type VariableMaxTermCount,
|
||||
type VariableProblemCount,
|
||||
} from "@/store/variableGameStore";
|
||||
|
||||
const ACTIVE_TERM_COLOR = "#f97316";
|
||||
|
||||
function highlightLatex(latex: string) {
|
||||
return `\\textcolor{${ACTIVE_TERM_COLOR}}{${latex}}`;
|
||||
}
|
||||
|
||||
function formatActiveTermProblemLatex(
|
||||
problem: VariableParenthesisProblem,
|
||||
activeTermIndex: number,
|
||||
) {
|
||||
const termLatex = problem.terms
|
||||
.map((term, index) => {
|
||||
const displayTerm =
|
||||
index === activeTermIndex
|
||||
? highlightLatex(formatVariableTermLatex(term))
|
||||
: formatVariableTermLatex(term);
|
||||
|
||||
if (index === 0) return displayTerm;
|
||||
|
||||
return `${problem.operators[index - 1]} ${displayTerm}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return `${highlightLatex(`${problem.multiplier}`)}${highlightLatex(
|
||||
"(",
|
||||
)}${termLatex}${highlightLatex(")")}`;
|
||||
}
|
||||
|
||||
interface VariablePlayClientProps {
|
||||
problemCount: VariableProblemCount;
|
||||
maxTermCount: VariableMaxTermCount;
|
||||
@@ -187,7 +159,13 @@ export default function VariablePlayClient({
|
||||
const isLastProblem = session.currentIndex === session.problems.length - 1;
|
||||
const problemLatex = submitted
|
||||
? currentProblem.latexBefore
|
||||
: formatActiveTermProblemLatex(currentProblem, activeTermIndex);
|
||||
: formatParenthesisExpressionLatex({
|
||||
multiplier: currentProblem.multiplier,
|
||||
terms: currentProblem.terms,
|
||||
operators: currentProblem.operators,
|
||||
formatTermLatex: formatVariableTermLatex,
|
||||
activeTermIndex,
|
||||
});
|
||||
const pendingChoiceLabels = currentProblem.terms.map((term, index) =>
|
||||
formatVariableTermLatex({
|
||||
...term,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
export const ACTIVE_DISTRIBUTION_COLOR = "#f97316";
|
||||
|
||||
function colorLatex(latex: string, color: string) {
|
||||
return `\\textcolor{${color}}{${latex}}`;
|
||||
}
|
||||
|
||||
interface FormatParenthesisExpressionLatexParams<T> {
|
||||
multiplier: number;
|
||||
terms: T[];
|
||||
operators: ("+" | "-")[];
|
||||
formatTermLatex: (term: T) => string;
|
||||
activeTermIndex?: number;
|
||||
activeColor?: string;
|
||||
}
|
||||
|
||||
export function formatParenthesisExpressionLatex<T>({
|
||||
multiplier,
|
||||
terms,
|
||||
operators,
|
||||
formatTermLatex,
|
||||
activeTermIndex,
|
||||
activeColor = ACTIVE_DISTRIBUTION_COLOR,
|
||||
}: FormatParenthesisExpressionLatexParams<T>) {
|
||||
const hasActiveTerm = activeTermIndex !== undefined;
|
||||
const maybeColor = (latex: string, active: boolean) =>
|
||||
hasActiveTerm && active ? colorLatex(latex, activeColor) : latex;
|
||||
const termLatex = terms
|
||||
.map((term, index) => {
|
||||
const displayTerm = maybeColor(
|
||||
formatTermLatex(term),
|
||||
index === activeTermIndex,
|
||||
);
|
||||
|
||||
if (index === 0) return displayTerm;
|
||||
|
||||
return `${operators[index - 1]} ${displayTerm}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
if (hasActiveTerm) {
|
||||
return `${colorLatex(`${multiplier}`, activeColor)}${colorLatex(
|
||||
"(",
|
||||
activeColor,
|
||||
)}${termLatex}${colorLatex(")", activeColor)}`;
|
||||
}
|
||||
|
||||
return `${multiplier}\\left(${termLatex}\\right)`;
|
||||
}
|
||||
@@ -14,22 +14,26 @@ describe("parenthesis sign problem engine", () => {
|
||||
expect(Number.isInteger(problem.multiplier)).toBe(true);
|
||||
expect(problem.multiplier).toBeGreaterThanOrEqual(-9);
|
||||
expect(problem.multiplier).toBeLessThanOrEqual(-1);
|
||||
expect(problem.a).toBeGreaterThanOrEqual(1);
|
||||
expect(problem.a).toBeLessThanOrEqual(9);
|
||||
expect(problem.b).toBeGreaterThanOrEqual(1);
|
||||
expect(problem.b).toBeLessThanOrEqual(9);
|
||||
expect(problem.a).not.toBe(problem.b);
|
||||
expect(["+", "-"]).toContain(problem.operator);
|
||||
expect(problem.terms).toHaveLength(2);
|
||||
expect(problem.operators).toHaveLength(1);
|
||||
expect(problem.terms.every((term) => term >= 1 && term <= 9)).toBe(true);
|
||||
expect(new Set(problem.terms).size).toBe(problem.terms.length);
|
||||
expect(["+", "-"]).toContain(problem.operators[0]);
|
||||
}
|
||||
});
|
||||
|
||||
it("distributes the multiplier correctly for both operators", () => {
|
||||
for (let index = 0; index < 100; index += 1) {
|
||||
const problem = generateProblem();
|
||||
const signedB = problem.operator === "-" ? -problem.b : problem.b;
|
||||
const signedTerms = problem.terms.map((term, termIndex) => {
|
||||
if (termIndex === 0) return term;
|
||||
|
||||
expect(problem.distributedA).toBe(problem.multiplier * problem.a);
|
||||
expect(problem.distributedB).toBe(problem.multiplier * signedB);
|
||||
return problem.operators[termIndex - 1] === "-" ? -term : term;
|
||||
});
|
||||
|
||||
expect(problem.distributedTerms).toEqual(
|
||||
signedTerms.map((term) => problem.multiplier * term),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,20 +41,15 @@ describe("parenthesis sign problem engine", () => {
|
||||
const problem = generateProblem();
|
||||
|
||||
const correctResult = gradeAnswer(problem, {
|
||||
a: problem.distributedA,
|
||||
b: problem.distributedB,
|
||||
terms: problem.distributedTerms,
|
||||
});
|
||||
|
||||
expect(correctResult.correct).toBe(true);
|
||||
expect(correctResult.userAnswer).toEqual([
|
||||
problem.distributedA,
|
||||
problem.distributedB,
|
||||
]);
|
||||
expect(correctResult.userAnswer).toEqual(problem.distributedTerms);
|
||||
|
||||
expect(
|
||||
gradeAnswer(problem, {
|
||||
a: problem.distributedB,
|
||||
b: problem.distributedA,
|
||||
terms: [...problem.distributedTerms].reverse(),
|
||||
}).correct,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ParenthesisSignProblem,
|
||||
UserAnswer,
|
||||
} from "./types";
|
||||
import { formatParenthesisExpressionLatex } from "./parenthesis-latex";
|
||||
|
||||
const MIN_MULTIPLIER = -9;
|
||||
const MAX_MULTIPLIER = -1;
|
||||
@@ -33,19 +34,6 @@ function formatLatexTerms(terms: number[]) {
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function formatParenthesisTerms(
|
||||
terms: number[],
|
||||
operators: ("+" | "-")[],
|
||||
) {
|
||||
return terms
|
||||
.map((term, index) => {
|
||||
if (index === 0) return `${term}`;
|
||||
|
||||
return `${operators[index - 1]} ${term}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function createProblem({
|
||||
multiplier,
|
||||
terms,
|
||||
@@ -57,9 +45,6 @@ function createProblem({
|
||||
return operators[index - 1] === "-" ? -term : term;
|
||||
});
|
||||
const distributedTerms = signedTerms.map((term) => multiplier * term);
|
||||
const [a, b] = terms;
|
||||
const [operator] = operators;
|
||||
const [distributedA, distributedB] = distributedTerms;
|
||||
const key = [
|
||||
multiplier,
|
||||
...terms.flatMap((term, index) =>
|
||||
@@ -73,16 +58,13 @@ function createProblem({
|
||||
multiplier,
|
||||
terms,
|
||||
operators,
|
||||
a,
|
||||
operator,
|
||||
b,
|
||||
distributedTerms,
|
||||
distributedA,
|
||||
distributedB,
|
||||
latexBefore: `${multiplier}\\left(${formatParenthesisTerms(
|
||||
latexBefore: formatParenthesisExpressionLatex({
|
||||
multiplier,
|
||||
terms,
|
||||
operators,
|
||||
)}\\right)`,
|
||||
formatTermLatex: (term) => `${term}`,
|
||||
}),
|
||||
latexAfter: formatLatexTerms(distributedTerms),
|
||||
};
|
||||
}
|
||||
@@ -222,16 +204,11 @@ export function gradeAnswer(
|
||||
problem: ParenthesisSignProblem,
|
||||
answer: UserAnswer,
|
||||
): GradeResult {
|
||||
const userAnswer =
|
||||
answer.terms ?? (answer.a !== undefined && answer.b !== undefined
|
||||
? [answer.a, answer.b]
|
||||
: []);
|
||||
|
||||
return {
|
||||
correct:
|
||||
userAnswer.length === problem.distributedTerms.length &&
|
||||
userAnswer.every((value, index) => value === problem.distributedTerms[index]),
|
||||
userAnswer,
|
||||
answer.terms.length === problem.distributedTerms.length &&
|
||||
answer.terms.every((value, index) => value === problem.distributedTerms[index]),
|
||||
userAnswer: answer.terms,
|
||||
correctAnswer: problem.distributedTerms,
|
||||
};
|
||||
}
|
||||
|
||||
+1
-8
@@ -4,20 +4,13 @@ export interface ParenthesisSignProblem {
|
||||
multiplier: number;
|
||||
terms: number[];
|
||||
operators: ("+" | "-")[];
|
||||
a: number;
|
||||
operator: "+" | "-";
|
||||
b: number;
|
||||
distributedTerms: number[];
|
||||
distributedA: number;
|
||||
distributedB: number;
|
||||
latexBefore: string;
|
||||
latexAfter: string;
|
||||
}
|
||||
|
||||
export interface UserAnswer {
|
||||
terms?: number[];
|
||||
a?: number;
|
||||
b?: number;
|
||||
terms: number[];
|
||||
}
|
||||
|
||||
export interface GradeResult {
|
||||
|
||||
@@ -25,27 +25,26 @@ function findCorrectChoiceIds(problem: VariableParenthesisProblem) {
|
||||
describe("variable parenthesis engine", () => {
|
||||
it("generates a problem with exactly one variable term inside parentheses", () => {
|
||||
const problem = generateVariableParenthesisProblem();
|
||||
const terms = [problem.firstTerm, problem.secondTerm];
|
||||
|
||||
expect(terms.filter((term) => term.kind === "variable")).toHaveLength(1);
|
||||
expect(terms.filter((term) => term.kind === "constant")).toHaveLength(1);
|
||||
expect(problem.terms.filter((term) => term.kind === "variable")).toHaveLength(1);
|
||||
expect(problem.terms.filter((term) => term.kind === "constant")).toHaveLength(1);
|
||||
expect(problem.multiplier).toBeLessThan(0);
|
||||
expect(Number.isInteger(problem.multiplier)).toBe(true);
|
||||
expect(problem.firstTerm.coefficient).toBeGreaterThanOrEqual(1);
|
||||
expect(problem.secondTerm.coefficient).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
problem.terms.every((term) => term.coefficient >= 1 && term.coefficient <= 9),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("distributes signs correctly for addition and subtraction", () => {
|
||||
const problems = generateVariableParenthesisSession(50);
|
||||
|
||||
for (const problem of problems) {
|
||||
const secondSign = problem.operator === "-" ? -1 : 1;
|
||||
expect(problem.correctTerms.map((term) => term.coefficient)).toEqual(
|
||||
problem.terms.map((term, index) => {
|
||||
const sign = index === 0 || problem.operators[index - 1] === "+" ? 1 : -1;
|
||||
|
||||
expect(problem.correctTerms[0].coefficient).toBe(
|
||||
problem.multiplier * problem.firstTerm.coefficient,
|
||||
);
|
||||
expect(problem.correctTerms[1].coefficient).toBe(
|
||||
problem.multiplier * secondSign * problem.secondTerm.coefficient,
|
||||
return problem.multiplier * sign * term.coefficient;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
VariableUserAnswer,
|
||||
VariableGradeResult,
|
||||
} from "./variable-types";
|
||||
import { formatParenthesisExpressionLatex } from "./parenthesis-latex";
|
||||
|
||||
const MIN_MULTIPLIER = -9;
|
||||
const MAX_MULTIPLIER = -1;
|
||||
@@ -95,10 +96,6 @@ export function formatVariableTermLatex(
|
||||
return `${term.coefficient}x`;
|
||||
}
|
||||
|
||||
function formatParenthesisTermLatex(term: VariableParenthesisTerm) {
|
||||
return formatVariableTermLatex(term);
|
||||
}
|
||||
|
||||
export function formatVariableExpressionLatex(
|
||||
terms: DistributedVariableTerm[],
|
||||
) {
|
||||
@@ -164,8 +161,6 @@ function createChoices(
|
||||
|
||||
function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem {
|
||||
const { multiplier, terms, operators } = seed;
|
||||
const [firstTerm, secondTerm] = terms;
|
||||
const [operator] = operators;
|
||||
const key = [
|
||||
multiplier,
|
||||
...terms.flatMap((term, index) =>
|
||||
@@ -189,18 +184,14 @@ function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem {
|
||||
multiplier,
|
||||
terms,
|
||||
operators,
|
||||
operator,
|
||||
firstTerm,
|
||||
secondTerm,
|
||||
correctTerms,
|
||||
choices,
|
||||
latexBefore: `${multiplier}\\left(${terms
|
||||
.map((term, index) => {
|
||||
if (index === 0) return formatParenthesisTermLatex(term);
|
||||
|
||||
return `${operators[index - 1]} ${formatParenthesisTermLatex(term)}`;
|
||||
})
|
||||
.join(" ")}\\right)`,
|
||||
latexBefore: formatParenthesisExpressionLatex({
|
||||
multiplier,
|
||||
terms,
|
||||
operators,
|
||||
formatTermLatex: formatVariableTermLatex,
|
||||
}),
|
||||
latexAfter: formatVariableExpressionLatex(correctTerms),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ export interface VariableParenthesisProblem {
|
||||
multiplier: number;
|
||||
terms: VariableParenthesisTerm[];
|
||||
operators: ("+" | "-")[];
|
||||
operator: "+" | "-";
|
||||
firstTerm: VariableParenthesisTerm;
|
||||
secondTerm: VariableParenthesisTerm;
|
||||
correctTerms: DistributedVariableTerm[];
|
||||
choices: VariableTermChoice[][];
|
||||
latexBefore: string;
|
||||
|
||||
Reference in New Issue
Block a user