Skip to content

Commit

Permalink
feat: number to words multiple choice
Browse files Browse the repository at this point in the history
Closes #92.
  • Loading branch information
asartalo committed Jul 7, 2022
1 parent 1433d89 commit c12c803
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 52 deletions.
2 changes: 2 additions & 0 deletions cypress/e2e/worksheet_numbers_to_words.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ it('can create numbers to words worksheet', () => {
});
});
});

cy.findByLabelText(/problem.+type/i).select('Multiple Choice');
});
6 changes: 6 additions & 0 deletions src/lib/formatNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// TODO: Use locale on computer's machine
const numberFormatter = new Intl.NumberFormat('en-US');

export default function formatNumber(num: number): string {
return numberFormatter.format(num);
}
53 changes: 53 additions & 0 deletions src/lib/mutateNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import randomElement from './randomElement';
import { randomGenerator } from './RandomNumberGenerator';
import shuffle from './shuffle';

// TODO: Make these work for numbers with decimal places
function toDigits(n: number): string[] {
return n.toString().split('');
}

function insertDigit(n: number): number {
const nDigits = toDigits(n);
const insertAt = randomGenerator.integer(nDigits.length - 1);
nDigits.splice(insertAt, 0, randomGenerator.integer(9).toString());
return parseFloat(nDigits.join(''));
}

function removeDigitOrRandom(n: number): number {
const nDigits = toDigits(n);
if (nDigits.length > 1) {
const removeAt = randomGenerator.integer(nDigits.length - 1);
nDigits.splice(removeAt, 1);
return parseFloat(nDigits.join(''));
}
return randomGenerator.integer(n);
}

function replaceDigit(n: number): number {
const nDigits = toDigits(n);
const replaceAt = randomGenerator.integer(nDigits.length - 1);
nDigits.splice(replaceAt, 1, randomGenerator.integer(9).toString());
return parseFloat(nDigits.join(''));
}

function shuffleDigits(n: number): number {
return parseFloat(shuffle(toDigits(n)).join(''));
}

function numberInRange(n: number): number {
const digitLength = (n.toString()).replace('.', '').length;
return randomGenerator.integer((10 ** (digitLength)) - 1);
}

const mangleFunctions = [
insertDigit,
removeDigitOrRandom,
replaceDigit,
shuffleDigits,
numberInRange,
];

export default function mutateNumber(n: number): number {
return randomElement(mangleFunctions)(n);
}
22 changes: 22 additions & 0 deletions src/lib/shuffle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* eslint-disable no-param-reassign */

const { floor, random } = Math;

export default function shuffle<T>(inputArray: T[]) {
const array = inputArray.slice(0);
let currentIndex = array.length;
let randomIndex: number;

// While there remain elements to shuffle.
while (currentIndex !== 0) {
// Pick a remaining element.
randomIndex = floor(random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}

return array;
}
13 changes: 13 additions & 0 deletions src/lib/tryByKey.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import tryByKey from './tryByKey';

describe('tryByKey()', () => {
it('retries on unique numbers', () => {
const keys = [1, 8, 3, 1, 2, 4, 8];
const got: number[] = [];
const limitedRetries = tryByKey();
for (let i = 0; i < keys.length; i++) {
limitedRetries(keys[i], () => got.push(keys[i]));
}
expect(got).toEqual([1, 8, 3, 2, 4]);
});
});
2 changes: 1 addition & 1 deletion src/lib/tryByKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type RetryFunction = (key: RetryKey, callback: VoidFunction) => void;
export default function tryByKey(maximumTries = 6): RetryFunction {
const found: Map<string, number> = new Map([]);
return function wrapper(rawKey, fn) {
const key = rawKey.toString();
const key = rawKey.toString().trim();
const set = found.get(key);
if (set === undefined) {
fn();
Expand Down
3 changes: 3 additions & 0 deletions src/lib/uniqueElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function uniqueElements<T>(array: T[]): T[] {
return Array.from(new Set(array));
}
10 changes: 9 additions & 1 deletion src/pages/numbersToWords/CustomizeNumbersToWordsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import arrayToOptions from '../../components/forms/arrayToOptions';
import CustomizeForm from '../../components/forms/CustomizeForm';
import NumberField from '../../components/forms/NumberField';
import SelectField from '../../components/forms/SelectField';
import stringMapToOptions from '../../components/forms/stringMapToOptions';
import { Magnitude, magnitudes } from '../../lib/math/magnitude';
import NumbersToWordsData from './NumbersToWordsData';
import NumbersToWordsData, { NumbersToWordsProblemType, problemTypes } from './NumbersToWordsData';

interface CustomizeNumbersToWordsFormProps {
onChange: (data: NumbersToWordsData) => void;
Expand All @@ -16,6 +17,13 @@ function CustomizeNumbersToWordsForm({
}: CustomizeNumbersToWordsFormProps): JSX.Element {
return (
<CustomizeForm name="Worksheet">
<SelectField<NumbersToWordsProblemType>
name="problemType"
value={data.problemType}
onChange={(problemType) => onChange({ ...data, problemType })}
>
{stringMapToOptions(problemTypes)}
</SelectField>
<NumberField
name="count"
label="Number of Problems"
Expand Down
11 changes: 8 additions & 3 deletions src/pages/numbersToWords/NumbersToWordsData.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
export type NumbersToWordsMagnitude = 'tens' | 'hundreds' | 'thousands';
export const magnitudes: NumbersToWordsMagnitude[] = ['tens', 'hundreds', 'thousands'];
import { Magnitude } from '../../lib/math/magnitude';

export type NumbersToWordsProblemType = 'blanks' | 'choice';
export const problemTypes = new Map<NumbersToWordsProblemType, string>([
['blanks', 'Fill in the Blanks'],
['choice', 'Multiple Choice'],
]);
interface NumbersToWordsData {
magnitude: NumbersToWordsMagnitude;
problemType: NumbersToWordsProblemType;
magnitude: Magnitude;
count: number;
}

Expand Down
1 change: 1 addition & 0 deletions src/pages/numbersToWords/NumbersToWordsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import NumbersToWordsData from './NumbersToWordsData';
import PreviewNumbersToWords from './PreviewNumbersToWords';

const defaultData: NumbersToWordsData = {
problemType: 'blanks',
magnitude: 'tens',
count: 10,
};
Expand Down
62 changes: 37 additions & 25 deletions src/pages/numbersToWords/PreviewNumbersToWords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,63 @@ import WorksheetFooter from '../../components/printElements/WorksheetFooter';
import WorksheetHeader from '../../components/printElements/WorksheetHeader';
import PageTitle from '../../elements/PageTitle';
import numberToWords from '../../lib/numberToWords';
import { randomGenerator } from '../../lib/RandomNumberGenerator';
import NumbersToWordsData from './NumbersToWordsData';
import tryByKey from '../../lib/tryByKey';
import { magNFromMagnitude, maxFromMagnitude } from '../../lib/math/magnitude';
import MultipleChoiceProblem from '../placeValues/MultipleChoiceProblem';
import formatNumber from '../../lib/formatNumber';
import generateProblems, { NumToWordsProblem } from './generateProblems';

function generateProblems({ count, magnitude }: NumbersToWordsData): Array<number> {
const max = maxFromMagnitude(magnitude);
const magNumber = magNFromMagnitude(magnitude);

const problems: Array<number> = [];
const limitedRetries = tryByKey(max);

while (problems.length < count) {
const number = randomGenerator.stepMagnitude(magNumber);
limitedRetries(number, () => {
problems.push(number);
});
}
interface FillInTheBlanksProps {
number: number;
showAnswer: boolean;
}

return problems;
function FillInTheBlanks({ number, showAnswer }: FillInTheBlanksProps) {
return (
<>
{
numberToWords(number)
}
{': '}
<Blank answer={formatNumber(number)} width="wide" showAnswer={showAnswer} />
</>
);
}

interface PreviewNumbersToWordsProps {
customData: NumbersToWordsData;
}

// TODO: Use locale on computer's machine
const numberFormatter = new Intl.NumberFormat('en-US');

function PreviewNumbersToWords({ customData }: PreviewNumbersToWordsProps): JSX.Element {
const { problemType } = customData;
const problems = generateProblems(customData);
const instructions = problemType === 'blanks'
? 'Write the number that is written in words.'
: 'Circle the number that the words represent.';

const itemBuilder = (showAnswer: boolean) => {
function fn(number: number, indexNumber: number) {
function fn(problem: NumToWordsProblem, indexNumber: number) {
const { number } = problem;
return (
<ProblemListItem
key={`problem-${indexNumber}`}
className="numbers-to-words-problem-item"
label={`Numbers To Words ${showAnswer ? 'Answer' : 'Problem'}`}
>
{
numberToWords(number)
problemType === 'blanks'
? (
<FillInTheBlanks number={number} showAnswer={showAnswer} />
)
: (
<MultipleChoiceProblem
choices={problem.choices}
answer={number}
showAnswer={showAnswer}
>
{numberToWords(number)}
</MultipleChoiceProblem>
)
}
{': '}
<Blank answer={numberFormatter.format(number)} width="wide" showAnswer={showAnswer} />
</ProblemListItem>
);
}
Expand All @@ -62,7 +74,7 @@ function PreviewNumbersToWords({ customData }: PreviewNumbersToWordsProps): JSX.
<MultiPaperPage
header={(
<WorksheetHeader>
<p>Write the number that is written in words.</p>
<p>{instructions}</p>
</WorksheetHeader>
)}
footer={(<WorksheetFooter itemCount={problems.length} />)}
Expand Down
50 changes: 50 additions & 0 deletions src/pages/numbersToWords/generateProblems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { magNFromMagnitude, maxFromMagnitude } from '../../lib/math/magnitude';
import mutateNumber from '../../lib/mutateNumber';
import { randomGenerator } from '../../lib/RandomNumberGenerator';
import shuffle from '../../lib/shuffle';
import tryByKey from '../../lib/tryByKey';
import NumbersToWordsData from './NumbersToWordsData';

export interface NumToWordsProblem {
number: number;
choices: number[];
}

function generateChoices(number: number): number[] {
const choices: Set<number> = new Set([number]);
while (choices.size < 4) {
choices.add(mutateNumber(number));
}
const shuffled = shuffle(Array.from(choices));
return shuffled;
}

function choiceProblem(number: number): NumToWordsProblem {
const choices: number[] = generateChoices(number);
return { number, choices };
}

function fillInTheBlankProblem(number: number): NumToWordsProblem {
return { number, choices: [] };
}

export default function generateProblems({
count, magnitude, problemType,
}: NumbersToWordsData): NumToWordsProblem[] {
const max = maxFromMagnitude(magnitude);
const magNumber = magNFromMagnitude(magnitude);

const problems: Array<NumToWordsProblem> = [];
const limitedRetries = tryByKey(max);

while (problems.length < count) {
const number = randomGenerator.stepMagnitude(magNumber);
limitedRetries(number, () => {
problems.push(
(problemType === 'blanks' ? fillInTheBlankProblem : choiceProblem)(number),
);
});
}

return problems;
}
Loading

0 comments on commit c12c803

Please sign in to comment.