From 77653789d39423670aca040b28e41b589b2c523c Mon Sep 17 00:00:00 2001 From: Spencer Harston Date: Sun, 12 Oct 2025 23:02:21 -0600 Subject: [PATCH 1/4] feat: allow custom selection of operators --- index.html | 56 +++++++++++++++++++++++------------------------ src/interfaces.ts | 2 +- src/main.ts | 45 ++++++++++++++++++++++++------------- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/index.html b/index.html index db83272..8cf3312 100644 --- a/index.html +++ b/index.html @@ -62,7 +62,7 @@
-
Generator Options
+
Generate Problems
@@ -78,25 +78,21 @@
Operator
- +
- +
- +
- +
-
- - -
@@ -165,7 +161,7 @@
- Options + Generator Options
@@ -183,24 +179,41 @@
- +
- +
- +
+ +
+ Print Options +
+ + +
+
+ + +
+
+ + +
+
- + +
@@ -221,20 +234,6 @@ Due to differences in HTML vs PDF rendering, the PDF will not look quite the same as what you see below. But it'll be pretty close.

-
-
- - -
-
- - -
-
- - -
-
@@ -249,7 +248,6 @@
-
diff --git a/src/interfaces.ts b/src/interfaces.ts index a2d912b..6fedcb2 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -17,7 +17,7 @@ export interface Problem { export interface GeneratorOptions { seed: number; - operator: string; + operators: string[]; upperMin: number; upperMax: number; lowerMin: number; diff --git a/src/main.ts b/src/main.ts index b06884a..d8c367f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -130,7 +130,7 @@ inputForm.addEventListener("submit", (e) => { const options: GeneratorOptions = { seed: inputData.get("seed") as unknown as number, // TODO: generate a new seed if not present - operator: inputData.get("operator") as string, + operators: inputData.getAll("operator") as string[], upperMin: inputData.get("upper-min") as unknown as number, upperMax: inputData.get("upper-max") as unknown as number, lowerMin: inputData.get("lower-min") as unknown as number, @@ -221,6 +221,16 @@ function setFormValues(options: GeneratorOptions) { break; } } + + // mark checkboxes checked for selected operators + if (key === "operators") { + const operatorCheckboxes = inputForm.elements.namedItem("operator") as RadioNodeList; + value.forEach((operator: string) => { + operatorCheckboxes.forEach((opCB) => { + if (opCB.value === operator) opCB.checked = true; + }); + }); + } } } @@ -275,6 +285,11 @@ function updatePagesNote() { function validateForm(options: GeneratorOptions): boolean { let isFormValid = true; + if (!options.operators || options.operators.length < 1) { + console.error("At least one operator must be selected"); + isFormValid = false; + } + // minimum operands have to be less than maximum if (parseInt(options.upperMin.toString()) > parseInt(options.upperMax.toString())) { console.error("Minimum upper operand values must be greater than maximum upper operand values"); @@ -286,7 +301,7 @@ function validateForm(options: GeneratorOptions): boolean { } // for division problems - if (options.operator === "/") { + if (options.operators.includes("/")) { // integer answers are only possible if upper/dividend is greater than lower/divisor if (options.intsOnly) { if (parseInt(options.upperMax.toString()) < parseInt(options.lowerMin.toString())) { @@ -329,7 +344,7 @@ function getAnswer(left: number, right: number, operator: string) { return answer; } -function getOperands(options: GeneratorOptions, rng: SeededRNG) { +function getOperands(options: GeneratorOptions, rng: SeededRNG, currOperator: string) { const upperOperand = rng.nextInt(options.upperMin, options.upperMax); const lowerOperand = rng.nextInt(options.lowerMin, options.lowerMax); @@ -341,7 +356,7 @@ function getOperands(options: GeneratorOptions, rng: SeededRNG) { } // avoid divide by zero - if (options.operator === "/" && operands[1] === 0) { + if (currOperator === "/" && operands[1] === 0) { operands[1] = rng.nextInt(1, options.lowerMax); } @@ -352,26 +367,26 @@ function generateMathProblems(options: GeneratorOptions): Problem[] { const rng = new SeededRNG(options.seed); for (let i = 0; i < options.numProblems; i++) { - // copy the form options - const optionsCopy = JSON.parse(JSON.stringify(options)) as GeneratorOptions; - - // if operator = mix, need to randomize which operator to use - optionsCopy.operator = options.operator === "mix" ? operators[rng.nextInt(0, 3)] : options.operator; + // if operators has more than one selection, randomize which one to use + const currOperator = + options.operators.length > 1 + ? options.operators[rng.nextInt(0, options.operators.length - 1)] + : options.operators[0]; - let operands = getOperands(optionsCopy, rng); - let answer = getAnswer(operands[0], operands[1], optionsCopy.operator); + let operands = getOperands(options, rng, currOperator); + let answer = getAnswer(operands[0], operands[1], currOperator); if (options.intsOnly) { do { - operands = getOperands(options, rng); - answer = getAnswer(operands[0], operands[1], optionsCopy.operator); + operands = getOperands(options, rng, currOperator); + answer = getAnswer(operands[0], operands[1], currOperator); } while (!Number.isInteger(answer)); } generatedProblems.push({ left: operands[0], right: operands[1], - operator: optionsCopy.operator, + operator: currOperator, answer: answer }); } @@ -571,7 +586,7 @@ function getOptionsFromURL(): GeneratorOptions { // return a GeneratorOptions object with either the values from the params or default values return { seed: parseInt(params.get("seed") || generateRandomSeed().toString(), 10), - operator: params.get("operator") || "+", + operators: params.getAll("operator").length > 0 ? params.getAll("operator") : ["+"], upperMin: parseInt(params.get("upper-min") || "0"), upperMax: parseInt(params.get("lef-max") || "100"), lowerMin: parseInt(params.get("lower-min") || "0"), From c08e7d8203a8294cf2c444f0985897a8d580bfd5 Mon Sep 17 00:00:00 2001 From: Spencer Harston Date: Sun, 12 Oct 2025 23:07:14 -0600 Subject: [PATCH 2/4] chore: update version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7b07a4..4343542 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "math-sheets", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "math-sheets", - "version": "0.7.0", + "version": "0.8.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e0f8a91..107fe31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "math-sheets", - "version": "0.7.0", + "version": "0.8.0", "private": true, "description": "Generate PDF worksheets of simple math problems in the browser.", "scripts": { From 7ee44a5146668fe367d6f5c6f5760d019c3ed188 Mon Sep 17 00:00:00 2001 From: Spencer Harston Date: Sun, 12 Oct 2025 23:30:18 -0600 Subject: [PATCH 3/4] fix: use the right array type --- src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index d8c367f..2dc700d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -224,7 +224,9 @@ function setFormValues(options: GeneratorOptions) { // mark checkboxes checked for selected operators if (key === "operators") { - const operatorCheckboxes = inputForm.elements.namedItem("operator") as RadioNodeList; + const operatorCheckboxes = Array.from( + inputForm.elements.namedItem("operator") as RadioNodeList + ) as HTMLInputElement[]; value.forEach((operator: string) => { operatorCheckboxes.forEach((opCB) => { if (opCB.value === operator) opCB.checked = true; From 7ea8bb7415350e8dd94e996a15c9a0ed79b1e5f0 Mon Sep 17 00:00:00 2001 From: Spencer Harston Date: Mon, 13 Oct 2025 22:03:13 -0600 Subject: [PATCH 4/4] docs: update readme and credits --- README.md | 18 +++++++----------- index.html | 7 +++++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0c62e6b..5d8b8fa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Generate PDF worksheets of simple math problems in the browser. -Inspired by the worksheets my dad printed off for my siblings and I during the summer breaks. I'm recreating this purely by memory; all I remember is that it was a grid of math problems, printed on a dot matrix printer. Not sure what he created his sheets in (likely QBasic or VisualBasic as this was late 1990s), so my attempt is to recreate this as close as possible, using modern web technologies. +Inspired by the worksheets my dad printed off for my siblings and I during the summer breaks. I'm recreating this purely by memory; all I remember is that it was a grid of math problems, printed on a dot matrix printer. Not sure how he created those sheets (likely Basic/QBasic or VisualBasic as this was late 1990s), so my attempt is to recreate this as close as possible, using modern web technologies. Built with HTML, CSS and Typescript and Vite in (more than) a few hours. I'm also using this as an exercise to learn newer web technologies (like TS and Vite). Is this overkill? Yes. Was it worth it? TBD (but likely yes). @@ -15,7 +15,7 @@ For generating the problems, there's several configuration options: - The two operand min/max values can be set independently - For division problems, the second operand is regenerated if it's 0 to avoid divide by zero problems - Set the number of problems to generate - - Currently I'm creating the PDF so that it fits 24 problems (4x6) per page, which may change + - Currently I'm creating the PDF so that it fits 20 problems (4x5) per page, which may change - Set the operand order (highest or lowest first) - Option to not have negative answers, which overrides operand order to highest first (helpful with subtraction problems) - Option to use long division notation in displaying problems @@ -23,13 +23,13 @@ For generating the problems, there's several configuration options: - If unselected, then answers are rounded to 3 decimal places where needed - Configuration options and resulting problems can be saved by using the same URL - When the problems are generated, you can click the `Save Config` button to rewrite the current URL with parameters that specify which options are set and their respective values - - When returning to the site with the same URL, the problems generated will be the same, handled by the `seed` value in the form. A different seed will generate different problems + - When returning to the site with the same URL and parameters, the problems generated will be the same, handled by the `seed` value in the form. A different seed will generate different problems - Throwback wallpaper! - Wallpaper is randomly chosen on first visit and reloads - The Select menu below the windows can change the background, saving the choice in localStorage for future visits -For generating the PDFs, there's a couple options to change: +When generating the PDFs, there's a couple options to change: - Set the font used in the generated PDF - If something fails, this will fall back to using the default Courier font @@ -39,17 +39,13 @@ For generating the PDFs, there's a couple options to change: ## Status -It's at the MVP stage right now with basic functionality available. +The web app is at the MVP stage right now, with basic functionality available. -There are a couple features still in-progress: - -- Allow multiple operators, instead of one or all - -See [ISSUES](https://github.com/sphars/math-sheets/issues) for details about upcoming features. +See [ISSUES](https://github.com/sphars/math-sheets/issues) for details about potential features. ## Development -This site is built with Typescript and Vite. Only requirement is Node v22+. To run locally, clone the repo, then +This site is built with Typescript and Vite. Node v22+ is required. To run locally, clone the repo, then ``` cd math-sheets diff --git a/index.html b/index.html index 8cf3312..dd8a9a6 100644 --- a/index.html +++ b/index.html @@ -41,8 +41,10 @@ in the late 90s/early 00s (hence the Windows 98 theme). Thanks Dad!

- This website is open-source and is licensed under the MIT license. Have any comments or bugs to report? File - an issue on GitHub. + This website is open-source and is licensed under the MIT license. + Source code. Have any comments or bugs + to report? File an issue on + GitHub.