diff --git a/cypress/e2e/navigation.cy.ts b/cypress/e2e/navigation.cy.ts index 5247cab..23c96ea 100644 --- a/cypress/e2e/navigation.cy.ts +++ b/cypress/e2e/navigation.cy.ts @@ -20,7 +20,12 @@ it('can visit all subpages', () => { cy.contains(/Printable Materials for Education/i); + // Compare Numbers Worksheets + clickWorksheetLink(/compare.+numbers/i); + cy.hasCustomizeFormHeading(/compare.+numbers/i); + // Addition Worksheets + goBackHome(); clickWorksheetLink(/addition.+fill.+blank/i); cy.hasCustomizeFormHeading(/addition.+fill.+blank/i); diff --git a/cypress/e2e/worksheet_compare_numbers.cy.ts b/cypress/e2e/worksheet_compare_numbers.cy.ts new file mode 100644 index 0000000..0a10c5b --- /dev/null +++ b/cypress/e2e/worksheet_compare_numbers.cy.ts @@ -0,0 +1,24 @@ +it('can create compare numbers worksheet', () => { + cy.visitWorksheetCompareNumbers(); + + cy.findByLabelText(/count/i).clearType('8'); + cy.findByLabelText(/magnitude/i).select('Hundreds'); + cy.findByLabelText(/columns/i).clearType('2'); + cy.withinPreview(() => { + cy.findByRole('list', { name: 'Problems' }).within((subject) => { + cy.wrap(subject).findAllByRole('listitem') + .should('have.length', 8) + .each(($li) => { + cy.wrap($li).contains(/\d+\s+_+\s+\d+/); + }); + }); + + cy.findByRole('list', { name: 'Answers' }).within((subject) => { + cy.wrap(subject).findAllByRole('listitem') + .should('have.length', 8) + .each(($li) => { + cy.wrap($li).contains(/\d+\s+(<|>|=)\s+\d+/); + }); + }); + }); +}); diff --git a/cypress/index.d.ts b/cypress/index.d.ts index b4539f3..9f0aacf 100644 --- a/cypress/index.d.ts +++ b/cypress/index.d.ts @@ -16,6 +16,7 @@ declare namespace Cypress { visitWorksheetTellingTime(): Chainable; visitWorksheetNumberGrid(): Chainable; visitWorksheetSkipCounting(): Chainable; + visitWorksheetCompareNumbers(): Chainable; } interface Chainable { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 671ed6e..affe3cf 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -41,6 +41,7 @@ const paths = { worksheetTellingTime: '/worksheet-telling-time', worksheetNumberGrid: '/worksheet-number-grid', worksheetSkipCounting: '/worksheet-skip-counting', + worksheetCompareNumbers: 'worksheet-compare-numbers', }; const pathNames = Object.keys(paths); diff --git a/src/lib/RandomNumberGenerator.ts b/src/lib/RandomNumberGenerator.ts index ee39cd8..c52c118 100644 --- a/src/lib/RandomNumberGenerator.ts +++ b/src/lib/RandomNumberGenerator.ts @@ -4,9 +4,9 @@ import Range from './Range'; export type MathRandom = () => number; -type ScalingMap = Map; +export type ScalingMap = Map; -function scaledRoulette(scales: ScalingMap): T { +export function scaledRoulette(scales: ScalingMap): T { const r = Math.random(); const total = Array.from(scales.values()) .reduce((sum, n) => sum + n, 0); diff --git a/src/lib/linkMap.ts b/src/lib/linkMap.ts index 38c0f07..252e62e 100644 --- a/src/lib/linkMap.ts +++ b/src/lib/linkMap.ts @@ -14,6 +14,7 @@ const NumbersToWordsPage = lazy(() => import('../pages/numbersToWords/NumbersToW const TellingTimePage = lazy(() => import('../pages/tellingTime/TellingTimePage')); const NumberGridPage = lazy(() => import('../pages/numberGrid/NumberGridPage')); const SkipCountingPage = lazy(() => import('../pages/skipCounting/SkipCountingPage')); +const CompareNumbersPage = lazy(() => import('../pages/compareNumbers/CompareNumbersPage')); const ExperimentsPage = lazy(() => import('../pages/experiments/ExperimentsPage')); const SettingsPage = lazy(() => import('../pages/settings/SettingsPage')); @@ -43,6 +44,10 @@ if (!isProduction) { } export const mathLinks: SectionLinks = new Map([ + ['/worksheet-compare-numbers', { + text: 'Compare Numbers', + loader: CompareNumbersPage, + }], ['/addition-fill-the-blanks', { text: 'Addition: Fill in the Blanks', loader: AdditionFillTheBlanksPage, diff --git a/src/pages/compareNumbers/CompareNumbersData.ts b/src/pages/compareNumbers/CompareNumbersData.ts new file mode 100644 index 0000000..64e70e5 --- /dev/null +++ b/src/pages/compareNumbers/CompareNumbersData.ts @@ -0,0 +1,14 @@ +import { Magnitude } from '../../lib/math/magnitude'; + +export type NumberComparison = '>' | '<' | '='; +export const numberComparisons: NumberComparison[] = [ + '>', + '<', + '=', +]; + +export default interface CompareNumbersData { + count: number; + magnitude: Magnitude; + columns: number; +} diff --git a/src/pages/compareNumbers/CompareNumbersPage.tsx b/src/pages/compareNumbers/CompareNumbersPage.tsx new file mode 100644 index 0000000..b03d563 --- /dev/null +++ b/src/pages/compareNumbers/CompareNumbersPage.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PrintableUI from '../../components/PrintableUI'; +import usePageState from '../usePageState'; +import CustomizeCompareNumbersForm from './CustomizeCompareNumbersForm'; +import CompareNumbersData from './CompareNumbersData'; +import PreviewCompareNumbers from './PreviewCompareNumbers'; + +const defaultData: CompareNumbersData = { + count: 10, + magnitude: 'tens', + columns: 1, +}; +const key = 'compareNumbers'; + +function CompareNumbersPage(): JSX.Element { + const { data, onChange } = usePageState({ + key, defaultData, + }); + return ( + + )} + > + + + ); +} + +export default CompareNumbersPage; diff --git a/src/pages/compareNumbers/CompareNumbersProblem.ts b/src/pages/compareNumbers/CompareNumbersProblem.ts new file mode 100644 index 0000000..0543294 --- /dev/null +++ b/src/pages/compareNumbers/CompareNumbersProblem.ts @@ -0,0 +1,26 @@ +import { NumberComparison } from './CompareNumbersData'; + +class CompareNumbersProblem { + left: number; + + right: number; + + constructor(left: number, right: number) { + this.left = left; + this.right = right; + } + + symbol(): NumberComparison { + if (this.left === this.right) { + return '='; + } + return this.left > this.right ? '>' : '<'; + } + + toString() { + const symbol = this.symbol() as string; + return `${this.left} ${symbol} ${this.right}`; + } +} + +export default CompareNumbersProblem; diff --git a/src/pages/compareNumbers/CustomizeCompareNumbersForm.tsx b/src/pages/compareNumbers/CustomizeCompareNumbersForm.tsx new file mode 100644 index 0000000..d4e3b24 --- /dev/null +++ b/src/pages/compareNumbers/CustomizeCompareNumbersForm.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Divider } from '@material-ui/core'; +import CustomizeForm from '../../components/forms/CustomizeForm'; +import CompareNumbersData from './CompareNumbersData'; +import NumberField from '../../components/forms/NumberField'; +import { Magnitude, magnitudes } from '../../lib/math/magnitude'; +import SelectField from '../../components/forms/SelectField'; +import arrayToOptions from '../../components/forms/arrayToOptions'; + +interface CustomizeCompareNumbersFormProps { + onChange: (data: CompareNumbersData) => void; + data: CompareNumbersData; +} + +function CustomizeCompareNumbersForm({ + data, onChange, +}: CustomizeCompareNumbersFormProps): JSX.Element { + return ( + + onChange({ ...data, count })} + /> + + name="magnitude" + value={data.magnitude} + onChange={(magnitude) => onChange({ ...data, magnitude })} + > + {arrayToOptions(magnitudes, true)} + + + + onChange({ ...data, columns })} + min={1} + max={10} + /> + + + ); +} + +export default CustomizeCompareNumbersForm; diff --git a/src/pages/compareNumbers/PreviewCompareNumbers.tsx b/src/pages/compareNumbers/PreviewCompareNumbers.tsx new file mode 100644 index 0000000..a7061b3 --- /dev/null +++ b/src/pages/compareNumbers/PreviewCompareNumbers.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import MultiPaperPage from '../../components/MultiPaperPage'; +import PageTitle from '../../elements/PageTitle'; +import ProblemList from '../../components/ProblemList'; +import ProblemListItem from '../../components/ProblemListItem'; +import WorksheetFooter from '../../components/printElements/WorksheetFooter'; +import WorksheetHeader from '../../components/printElements/WorksheetHeader'; +import CompareNumbersData from './CompareNumbersData'; +import generateProblems from './generateProblems'; +import CompareNumbersProblem from './CompareNumbersProblem'; +import Blank from '../../components/Blank'; + +function itemBuilder( + showAnswer: boolean, +) { + function fn(problem: CompareNumbersProblem, indexNumber: number) { + return ( + + {problem.left} + {' '} + + {' '} + {problem.right} + + ); + } + return fn; +} + +interface PreviewCompareNumbersProps { + data: CompareNumbersData; +} + +function PreviewCompareNumbers({ data }: PreviewCompareNumbersProps): JSX.Element { + const problems = generateProblems(data); + const instructions = 'Write <, >, = if the number on the left is less than, greater than, or equal to the number on the right.'; + + return ( + <> + +

{instructions}

+ + )} + wrapper={ProblemList} + footer={()} + wrapperProps={{ + className: 'problems', + columns: data.columns, + }} + data={problems} + itemSelector=".compare-numbers-problem-item" + renderItems={itemBuilder(false)} + /> + Answer Key + )} + wrapper={ProblemList} + wrapperProps={{ + className: 'answers', + label: 'Answers', + columns: data.columns, + }} + data={problems} + itemSelector=".compare-numbers-problem-item" + renderItems={itemBuilder(true)} + /> + + ); +} + +export default PreviewCompareNumbers; diff --git a/src/pages/compareNumbers/generateProblems.ts b/src/pages/compareNumbers/generateProblems.ts new file mode 100644 index 0000000..724bf87 --- /dev/null +++ b/src/pages/compareNumbers/generateProblems.ts @@ -0,0 +1,35 @@ +import { maxFromMagnitude } from '../../lib/math/magnitude'; +import { randomGenerator, scaledRoulette, ScalingMap } from '../../lib/RandomNumberGenerator'; +import CompareNumbersData, { NumberComparison } from './CompareNumbersData'; +import CompareNumbersProblem from './CompareNumbersProblem'; + +const comparisonWeights: ScalingMap = new Map([ + ['<', 2], + ['>', 2], + ['=', 1], +]); + +export default function generateProblems({ + magnitude, count, +}: CompareNumbersData): CompareNumbersProblem[] { + const problems: CompareNumbersProblem[] = []; + const max = maxFromMagnitude(magnitude); + for (let i = 0; i < count; i++) { + const comparison = scaledRoulette(comparisonWeights); + let left: number; + let right: number; + if (comparison === '>') { + left = randomGenerator.integer(max, 1); + right = randomGenerator.integer(left - 1); + } else if (comparison === '<') { + left = randomGenerator.integer(max - 1); + right = randomGenerator.integer(max, left + 1); + } else { + left = randomGenerator.integer(max); + right = left; + } + problems.push(new CompareNumbersProblem(left, right)); + } + + return problems; +}