diff --git a/apps/web/src/app/create/worksheets/addition/SUBTRACTION_AND_OPERATOR_PLAN.md b/apps/web/src/app/create/worksheets/addition/SUBTRACTION_AND_OPERATOR_PLAN.md
new file mode 100644
index 00000000..9f600ed5
--- /dev/null
+++ b/apps/web/src/app/create/worksheets/addition/SUBTRACTION_AND_OPERATOR_PLAN.md
@@ -0,0 +1,878 @@
+# Subtraction Support and Operator Selection Plan
+
+## Overview
+
+Add support for subtraction problems and allow users to choose between addition, subtraction, or mixed operations on worksheets.
+
+## Phase 1: Operator Selection UI
+
+### UI Component Location
+`src/app/create/worksheets/addition/components/ConfigPanel.tsx`
+
+### New Setting: `operator`
+
+Add operator selector control in the Basic Settings section, right after the digit range slider.
+
+**Type Definition:**
+```typescript
+// types.ts
+export type WorksheetOperator = 'addition' | 'subtraction' | 'mixed'
+
+export interface WorksheetFormState {
+ // ... existing fields
+ operator: WorksheetOperator // NEW
+}
+```
+
+**Default Value:** `'addition'` (backward compatible)
+
+**UI Design:**
+
+```tsx
+{/* Operator Selection */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {formState.operator === 'mixed'
+ ? 'Problems will randomly use addition or subtraction'
+ : formState.operator === 'addition'
+ ? 'All problems will be addition'
+ : 'All problems will be subtraction'}
+
+
+```
+
+**Considerations:**
+- When operator is 'subtraction' or 'mixed', ensure minuend ≥ subtrahend (no negative answers)
+- Update `difficultyProfiles.ts` if needed to account for subtraction difficulty
+- Mixed mode: Should alternate or randomize? → **Randomize** for variety
+
+---
+
+## Phase 2: Subtraction Problem Generation
+
+### File: `problemGenerator.ts`
+
+**New Function:**
+```typescript
+export interface SubtractionProblem {
+ minuend: number
+ subtrahend: number
+ operator: '-'
+}
+
+/**
+ * Generate subtraction problems ensuring minuend ≥ subtrahend (no negatives)
+ */
+export function generateSubtractionProblems(
+ count: number,
+ digitRange: { min: number; max: number },
+ pAnyBorrow: number, // Probability any place needs borrowing
+ pAllBorrow: number, // Probability all places need borrowing
+ interpolate: boolean,
+ seed: number
+): SubtractionProblem[]
+```
+
+**Key Constraints:**
+1. `minuend ≥ subtrahend` (prevent negative results)
+2. `minuend > 0` (no zero minuends)
+3. Both numbers within digit range
+4. Control borrowing probability similar to carry probability for addition
+
+**Borrowing Detection:**
+```typescript
+function requiresBorrowing(minuend: number, subtrahend: number): boolean {
+ let m = minuend
+ let s = subtrahend
+
+ while (m > 0 || s > 0) {
+ const mDigit = m % 10
+ const sDigit = s % 10
+
+ if (mDigit < sDigit) return true
+
+ m = Math.floor(m / 10)
+ s = Math.floor(s / 10)
+ }
+
+ return false
+}
+```
+
+**Mixed Mode Generation:**
+```typescript
+export function generateMixedProblems(
+ count: number,
+ digitRange: { min: number; max: number },
+ pAnyRegroup: number, // Probability any place needs regrouping (carry OR borrow)
+ pAllRegroup: number, // Probability all places need regrouping
+ interpolate: boolean,
+ seed: number
+): (AdditionProblem | SubtractionProblem)[] {
+ const rng = seedrandom(seed.toString())
+ const problems: (AdditionProblem | SubtractionProblem)[] = []
+
+ for (let i = 0; i < count; i++) {
+ const useAddition = rng() < 0.5 // 50/50 mix
+
+ if (useAddition) {
+ // Generate addition problem
+ } else {
+ // Generate subtraction problem
+ }
+ }
+
+ return problems
+}
+```
+
+---
+
+## Phase 3: Subtraction Typst Rendering
+
+### File: `typstHelpers.ts`
+
+**New Function:**
+```typescript
+export function generateSubtractionProblemStackFunction(
+ cellSize: number,
+ maxDigits: number = 3
+): string
+```
+
+**Typst Function Signature:**
+```typst
+#let subtraction-problem-stack(
+ minuend, // e.g., 52
+ subtrahend, // e.g., 17
+ index-or-none, // Problem number or none
+ show-borrows, // Show borrow boxes
+ show-answers, // Show answer boxes
+ show-colors, // Show place value colors
+ show-ten-frames, // Show ten-frame visualization
+ show-numbers // Show problem numbers
+) = {
+ // Implementation
+}
+```
+
+### Key Differences from Addition
+
+**1. Borrow Boxes (instead of Carry Boxes)**
+
+Position: Above the minuend row
+
+Visual:
+- Top triangle: Source place value color (giving the 10)
+- Bottom triangle: Destination place value color (receiving the 10)
+- Direction: RIGHT to LEFT (opposite of addition)
+
+Example for 52 - 17:
+```
+[Borrow boxes] [ ] [B1→0]
+[Minuend] [ 5] [ 2]
+[Subtrahend] − [ 1] [ 7]
+[Line] ----------
+[Answer boxes] [A1] [A0]
+```
+
+**2. Actual Digits Calculation**
+
+```typst
+// Extract minuend and subtrahend digits
+let minuend-digits = ()
+let subtrahend-digits = ()
+// ... extraction loop (same as addition)
+
+// Find highest non-zero positions
+let minuend-highest = 0
+let subtrahend-highest = 0
+// ... detection loop
+
+// Calculate difference
+let difference = minuend - subtrahend
+
+// Find highest non-zero digit in difference
+let diff-highest = 0
+let temp-diff = difference
+for i in range(0, max-extraction) {
+ if calc.rem(temp-diff, 10) > 0 { diff-highest = i }
+ temp-diff = calc.floor(temp-diff / 10)
+}
+
+// Grid size based on MINUEND (not difference)
+// But answer boxes only show up to diff-highest
+let grid-digits = calc.max(minuend-highest, subtrahend-highest) + 1
+let answer-digits = diff-highest + 1 // Can be less than grid-digits!
+```
+
+**3. Borrow Detection**
+
+```typst
+let borrow-places = ()
+let needs-borrow-from = () // Track which place borrowed FROM
+
+for i in range(0, grid-digits) {
+ let m-digit = minuend-digits.at(i)
+ let s-digit = subtrahend-digits.at(i)
+
+ // Check if this place needs a borrow
+ if m-digit < s-digit {
+ borrow-places.push(i)
+
+ // Mark that we borrowed FROM i+1
+ if i + 1 < grid-digits {
+ needs-borrow-from.push(i + 1)
+ }
+ }
+}
+```
+
+**4. Borrow Boxes Row**
+
+```typst
+// Borrow boxes row (shows borrows FROM higher TO lower)
+[], // Empty cell for operator column
+..for i in range(0, grid-digits).rev() {
+ let shows-borrow = show-borrows and (i in needs-borrow-from)
+
+ if shows-borrow {
+ // This place borrowed FROM to give to i-1
+ let source-color = place-colors.at(i) // This place (giving)
+ let dest-color = place-colors.at(i - 1) // Lower place (receiving)
+
+ if show-colors {
+ (box(width: cellSizeIn, height: cellSizeIn)[
+ #diagonal-split-box(cellSizeIn, source-color, dest-color)
+ ],)
+ } else {
+ (box(width: cellSizeIn, height: cellSizeIn, stroke: 0.5pt)[],)
+ }
+ } else {
+ (box(width: cellSizeIn, height: cellSizeIn)[
+ #v(cellSizeIn)
+ ],)
+ }
+},
+```
+
+**5. Ten-Frames for Borrowing**
+
+Show ten-frames for places where borrowing occurs.
+
+Visual concept for ones place of 52 - 17:
+- Need to compute: (2 + 10) - 7 = 5
+- Top frame: Show 10 (borrowed from tens)
+- Bottom frame: Show the 5 filled dots (result after borrowing)
+
+```typst
+..if show-ten-frames {
+ let regrouping-places = borrow-places // Places that needed borrowing
+
+ if regrouping-places.len() > 0 {
+ (
+ [], // Empty cell for operator column
+ ..for i in range(0, grid-digits).rev() {
+ let shows-frame = show-ten-frames-for-all or (i in regrouping-places)
+
+ if shows-frame {
+ // Show borrowed amount and result
+ let m-digit = minuend-digits.at(i)
+ let s-digit = subtrahend-digits.at(i)
+ let borrowed-value = m-digit + 10
+ let result = borrowed-value - s-digit
+
+ // Top frame: 10 (borrowed)
+ // Bottom frame: result
+ let top-color = if i + 1 < grid-digits { place-colors.at(i + 1) } else { color-none }
+ let bottom-color = place-colors.at(i)
+
+ (box(width: cellSizeIn, height: cellSizeIn * 0.8)[
+ #align(center + top)[
+ #ten-frames-stacked(
+ cellSizeIn * 0.90,
+ if show-colors { top-color } else { color-none },
+ if show-colors { bottom-color } else { color-none }
+ )
+ ]
+ #place(top, line(length: cellSizeIn * 0.90, stroke: heavy-stroke))
+ ],)
+ } else {
+ (v(cellSizeIn * 0.8),)
+ }
+ },
+ )
+ } else {
+ ()
+ }
+}
+```
+
+**6. Answer Boxes (hide leading zeros in difference)**
+
+```typst
+// Answer boxes row
+[], // Empty cell for operator column
+..for i in range(0, grid-digits).rev() {
+ let place-color = place-colors.at(i)
+ let fill-color = if show-colors { place-color } else { color-none }
+
+ // Only show answer box if within actual answer digits
+ let shows-answer = show-answers and i < answer-digits
+
+ if shows-answer {
+ (box(width: cellSizeIn, height: cellSizeIn, stroke: 0.5pt, fill: fill-color)[],)
+ } else {
+ // No answer box for leading zero positions
+ (box(width: cellSizeIn, height: cellSizeIn)[
+ #v(cellSizeIn)
+ ],)
+ }
+},
+```
+
+**7. Operator Symbol**
+
+Change the operator cell from "+" to "−":
+
+```typst
+// Subtrahend row with − sign
+box(width: cellSizeIn, height: cellSizeIn)[
+ #align(center + horizon)[
+ #text(size: cellSizePt * 0.8)[−] // Use minus sign, not hyphen
+ ]
+],
+```
+
+---
+
+## Phase 4: Update typstGenerator.ts
+
+### Unified Problem Type
+
+```typescript
+// types.ts
+export type WorksheetProblem = AdditionProblem | SubtractionProblem
+
+export interface AdditionProblem {
+ a: number
+ b: number
+ operator: '+'
+ // Display flags added by enrichment
+ showCarryBoxes?: boolean
+ showAnswerBoxes?: boolean
+ showPlaceValueColors?: boolean
+ showTenFrames?: boolean
+ showProblemNumbers?: boolean
+ showCellBorder?: boolean
+}
+
+export interface SubtractionProblem {
+ minuend: number
+ subtrahend: number
+ operator: '−' // Use proper minus sign
+ // Display flags added by enrichment
+ showBorrowBoxes?: boolean
+ showAnswerBoxes?: boolean
+ showPlaceValueColors?: boolean
+ showTenFrames?: boolean
+ showProblemNumbers?: boolean
+ showCellBorder?: boolean
+}
+```
+
+### Update generatePageTypst
+
+```typescript
+function generatePageTypst(
+ config: WorksheetConfig,
+ pageProblems: WorksheetProblem[], // Can be addition or subtraction
+ problemOffset: number,
+ rowsPerPage: number
+): string {
+ // Enrich problems based on operator type
+ const enrichedProblems = pageProblems.map((p, index) => {
+ if (p.operator === '+') {
+ // Addition enrichment (existing logic)
+ if (config.mode === 'smart') {
+ const meta = analyzeProblem(p.a, p.b)
+ const displayOptions = resolveDisplayForProblem(config.displayRules, meta)
+ return { ...p, ...displayOptions }
+ } else {
+ return {
+ ...p,
+ showCarryBoxes: config.showCarryBoxes,
+ showAnswerBoxes: config.showAnswerBoxes,
+ // ... etc
+ }
+ }
+ } else {
+ // Subtraction enrichment (NEW)
+ if (config.mode === 'smart') {
+ const meta = analyzeSubtractionProblem(p.minuend, p.subtrahend)
+ const displayOptions = resolveDisplayForProblem(config.displayRules, meta)
+ return {
+ ...p,
+ showBorrowBoxes: displayOptions.showCarryBoxes, // Map carry → borrow
+ showAnswerBoxes: displayOptions.showAnswerBoxes,
+ // ... etc
+ }
+ } else {
+ return {
+ ...p,
+ showBorrowBoxes: config.showCarryBoxes, // Use same setting
+ showAnswerBoxes: config.showAnswerBoxes,
+ // ... etc
+ }
+ }
+ }
+ })
+
+ // Generate Typst with correct function calls
+ const problemsTypst = enrichedProblems.map((p) => {
+ if (p.operator === '+') {
+ return ` (operator: "+", a: ${p.a}, b: ${p.b}, showCarryBoxes: ${p.showCarryBoxes}, ...),`
+ } else {
+ return ` (operator: "-", minuend: ${p.minuend}, subtrahend: ${p.subtrahend}, showBorrowBoxes: ${p.showBorrowBoxes}, ...),`
+ }
+ }).join('\n')
+
+ // In Typst template:
+ return String.raw`
+ // ... setup
+
+ ${generateTypstHelpers(cellSize)}
+ ${generateProblemStackFunction(cellSize, maxDigits)}
+ ${generateSubtractionProblemStackFunction(cellSize, maxDigits)}
+
+ #let problem-box(problem, index) = {
+ if problem.operator == "+" {
+ problem-stack(
+ problem.a,
+ problem.b,
+ index,
+ problem.showCarryBoxes,
+ problem.showAnswerBoxes,
+ // ... etc
+ )
+ } else {
+ subtraction-problem-stack(
+ problem.minuend,
+ problem.subtrahend,
+ index,
+ problem.showBorrowBoxes,
+ problem.showAnswerBoxes,
+ // ... etc
+ )
+ }
+ }
+
+ // ... rest of template
+ `
+}
+```
+
+---
+
+## Phase 5: Problem Analysis for Subtraction
+
+### File: `problemAnalysis.ts`
+
+**New Function:**
+```typescript
+export interface SubtractionProblemMeta {
+ minuend: number
+ subtrahend: number
+ digitsMinuend: number
+ digitsSubtrahend: number
+ maxDigits: number
+ difference: number
+ digitsDifference: number
+ requiresBorrowing: boolean
+ borrowCount: number
+ borrowPlaces: Array<'ones' | 'tens' | 'hundreds' | 'thousands' | 'ten-thousands'>
+}
+
+export function analyzeSubtractionProblem(
+ minuend: number,
+ subtrahend: number
+): SubtractionProblemMeta {
+ const digitsMinuend = Math.floor(Math.log10(minuend)) + 1
+ const digitsSubtrahend = Math.floor(Math.log10(subtrahend)) + 1
+ const maxDigits = Math.max(digitsMinuend, digitsSubtrahend)
+ const difference = minuend - subtrahend
+ const digitsDifference = difference === 0 ? 1 : Math.floor(Math.log10(difference)) + 1
+
+ // Detect borrowing
+ const borrowPlaces: SubtractionProblemMeta['borrowPlaces'] = []
+ let m = minuend
+ let s = subtrahend
+ let placeNames: SubtractionProblemMeta['borrowPlaces'][number][] = [
+ 'ones',
+ 'tens',
+ 'hundreds',
+ 'thousands',
+ 'ten-thousands',
+ ]
+
+ let placeIndex = 0
+ while (m > 0 || s > 0) {
+ const mDigit = m % 10
+ const sDigit = s % 10
+
+ if (mDigit < sDigit) {
+ borrowPlaces.push(placeNames[placeIndex])
+ }
+
+ m = Math.floor(m / 10)
+ s = Math.floor(s / 10)
+ placeIndex++
+ }
+
+ return {
+ minuend,
+ subtrahend,
+ digitsMinuend,
+ digitsSubtrahend,
+ maxDigits,
+ difference,
+ digitsDifference,
+ requiresBorrowing: borrowPlaces.length > 0,
+ borrowCount: borrowPlaces.length,
+ borrowPlaces,
+ }
+}
+```
+
+---
+
+## Phase 6: Display Rules for Subtraction
+
+### File: `displayRules.ts`
+
+**Update resolveDisplayForProblem:**
+
+```typescript
+export function resolveDisplayForProblem(
+ rules: DisplayRules,
+ meta: ProblemMeta | SubtractionProblemMeta
+): DisplayOptions {
+ // Detect problem type
+ const isSubtraction = 'minuend' in meta
+ const requiresRegrouping = isSubtraction ? meta.requiresBorrowing : meta.requiresRegrouping
+
+ // Borrow boxes / Carry boxes
+ const showCarryBoxes =
+ rules.carryBoxes === 'always' ||
+ (rules.carryBoxes === 'whenRegrouping' && requiresRegrouping)
+
+ // ... rest of resolution logic (same for both operations)
+
+ return {
+ showCarryBoxes, // For subtraction, this means "show borrow boxes"
+ showAnswerBoxes,
+ showPlaceValueColors,
+ showTenFrames,
+ showProblemNumbers,
+ showCellBorder,
+ }
+}
+```
+
+**Note:** We reuse `showCarryBoxes` flag for both operations. The rendering functions interpret it as "show carry boxes" for addition and "show borrow boxes" for subtraction.
+
+---
+
+## Phase 7: Auto-Save and Persistence
+
+### Update `useWorksheetAutoSave.ts`
+
+Add `operator` to persisted fields:
+
+```typescript
+const {
+ // ... existing fields
+ operator, // NEW
+} = formState
+
+const response = await fetch('/api/worksheets/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ type: worksheetType,
+ config: {
+ // ... existing fields
+ operator, // NEW
+ },
+ }),
+})
+```
+
+---
+
+## Phase 8: Preview and Example Routes
+
+### Update `/api/create/worksheets/addition/preview`
+
+```typescript
+// Support operator field in config
+const problems = config.operator === 'addition'
+ ? generateProblems(/* ... */)
+ : config.operator === 'subtraction'
+ ? generateSubtractionProblems(/* ... */)
+ : generateMixedProblems(/* ... */)
+```
+
+### Update `/api/create/worksheets/addition/example`
+
+Add operator selection for display options preview:
+
+```typescript
+interface ExampleRequest {
+ // ... existing fields
+ operator?: 'addition' | 'subtraction' // NEW
+}
+
+function generateExampleTypst(config: ExampleRequest): string {
+ const operator = config.operator || 'addition'
+
+ // Generate appropriate problem type
+ if (operator === 'addition') {
+ // ... existing addition logic
+ } else {
+ // Generate subtraction problem
+ const minuend = config.addend1 ?? 52
+ const subtrahend = config.addend2 ?? 17
+
+ return String.raw`
+ // ... setup
+ ${generateSubtractionProblemStackFunction(cellSize, 3)}
+
+ #let minuend = ${minuend}
+ #let subtrahend = ${subtrahend}
+
+ #align(center + horizon)[
+ #subtraction-problem-stack(
+ minuend,
+ subtrahend,
+ if show-numbers { 0 } else { none },
+ show-carries, // Interpreted as show-borrows for subtraction
+ show-answers,
+ show-colors,
+ show-ten-frames,
+ show-numbers
+ )
+ ]
+ `
+ }
+}
+```
+
+---
+
+## Phase 9: UI Updates for Display Options Preview
+
+### Update `DisplayOptionsPreview.tsx`
+
+Add operator selector to preview component:
+
+```tsx
+export function DisplayOptionsPreview({ formState }: DisplayOptionsPreviewProps) {
+ // Local state for operands
+ const [operands, setOperands] = useState([45, 27])
+ const [operator, setOperator] = useState<'addition' | 'subtraction'>(
+ formState.operator === 'subtraction' ? 'subtraction' : 'addition'
+ )
+
+ // Sync with formState.operator changes
+ useEffect(() => {
+ if (formState.operator === 'subtraction' || formState.operator === 'addition') {
+ setOperator(formState.operator)
+ }
+ }, [formState.operator])
+
+ const [debouncedOptions, setDebouncedOptions] = useState({
+ // ... existing fields
+ operator, // NEW
+ })
+
+ return (
+
+
+
Preview
+
+ {/* Operator toggle (only show if formState is mixed) */}
+ {formState.operator === 'mixed' && (
+
+
+
+
+ )}
+
+
+
+
+ {/* SVG preview */}
+
+ )
+}
+```
+
+---
+
+## Phase 10: Validation Updates
+
+### File: `validation.ts`
+
+Add operator validation:
+
+```typescript
+export const worksheetConfigSchema = z.object({
+ // ... existing fields
+ operator: z.enum(['addition', 'subtraction', 'mixed']).default('addition'),
+})
+```
+
+Ensure subtraction constraints:
+```typescript
+if (config.operator === 'subtraction' || config.operator === 'mixed') {
+ // Validate that we can generate valid subtraction problems
+ // (minuend ≥ subtrahend within digit range)
+}
+```
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+
+1. **Problem Generation:**
+ - `generateSubtractionProblems()` always produces minuend ≥ subtrahend
+ - Borrowing probability controls work correctly
+ - Mixed mode produces 50/50 distribution
+
+2. **Problem Analysis:**
+ - `analyzeSubtractionProblem()` correctly detects borrowing
+ - Handles edge cases (100-99=1, 1000-1=999, etc.)
+
+3. **Display Rules:**
+ - Smart mode correctly applies conditional scaffolding for subtraction
+ - Borrow detection works for all place values
+
+### Integration Tests
+
+1. **Typst Rendering:**
+ - Subtraction problems render with correct operator (−)
+ - Borrow boxes appear in correct positions
+ - Answer boxes hide leading zeros correctly
+ - Ten-frames show borrowing visualization
+
+2. **Preview:**
+ - Display options preview works for subtraction
+ - Changing operator updates preview correctly
+ - Mixed mode shows both addition and subtraction examples
+
+### Manual Testing Scenarios
+
+1. **Simple subtraction (no borrowing):** 85 - 32
+2. **Single borrow:** 52 - 17
+3. **Multiple borrows:** 534 - 178
+4. **Borrow from zero:** 301 - 89
+5. **Chain borrowing:** 1000 - 1
+6. **Result with leading zeros hidden:** 100 - 99 = 1
+7. **Large numbers:** 99999 - 12345
+8. **Mixed worksheet:** Alternating + and −
+
+---
+
+## Migration Path
+
+### Backward Compatibility
+
+1. Existing worksheets default to `operator: 'addition'`
+2. All existing UI works unchanged (addition only)
+3. New operator selector is opt-in
+
+### Database Migration
+
+If storing worksheet templates:
+```sql
+ALTER TABLE worksheet_settings
+ADD COLUMN operator VARCHAR(20) DEFAULT 'addition';
+```
+
+---
+
+## Implementation Order
+
+1. ✅ **Phase 1:** Add operator UI selector (ConfigPanel.tsx)
+2. ✅ **Phase 2:** Generate subtraction problems (problemGenerator.ts)
+3. ✅ **Phase 3:** Analyze subtraction problems (problemAnalysis.ts)
+4. ✅ **Phase 4:** Render subtraction in Typst (typstHelpers.ts)
+5. ✅ **Phase 5:** Update typstGenerator.ts for unified rendering
+6. ✅ **Phase 6:** Update display rules for subtraction
+7. ✅ **Phase 7:** Auto-save operator setting
+8. ✅ **Phase 8:** Update preview/example routes
+9. ✅ **Phase 9:** Update DisplayOptionsPreview component
+10. ✅ **Phase 10:** Validation and testing
+
+---
+
+## Open Questions
+
+1. **Negative results:** Should we allow worksheets with negative answers? (Probably not for elementary level)
+2. **Zero borrowing:** How to visualize 100-100=0? Show single 0 in answer row?
+3. **Ten-frame content for borrowing:** Show borrowed 10 + minuend digit, or show the result after subtraction?
+4. **Mixed mode distribution:** Should it be exactly 50/50, or allow user to specify the ratio?
+5. **Difficulty profiles:** Do we need separate difficulty profiles for subtraction vs addition?
+
+---
+
+## Notes
+
+- Use proper minus sign (−, U+2212) not hyphen (-) in UI and Typst
+- Reuse as much addition infrastructure as possible (smart mode, display rules, etc.)
+- Keep the same visual language (colors, ten-frames, scaffolding) for consistency
+- Consider renaming the folder from `addition/` to `arithmetic/` in the future
diff --git a/apps/web/src/app/create/worksheets/addition/problemAnalysis.ts b/apps/web/src/app/create/worksheets/addition/problemAnalysis.ts
index e0b20c60..88819971 100644
--- a/apps/web/src/app/create/worksheets/addition/problemAnalysis.ts
+++ b/apps/web/src/app/create/worksheets/addition/problemAnalysis.ts
@@ -1,5 +1,14 @@
// Problem analysis for conditional display rules
// Analyzes n-digit + p-digit addition problems to determine characteristics
+// Supports 1-5 digit problems (max sum: 99999 + 99999 = 199998)
+
+export type PlaceValue =
+ | 'ones'
+ | 'tens'
+ | 'hundreds'
+ | 'thousands'
+ | 'tenThousands'
+ | 'hundredThousands'
export interface ProblemMeta {
a: number
@@ -11,12 +20,13 @@ export interface ProblemMeta {
digitsSum: number
requiresRegrouping: boolean
regroupCount: number
- regroupPlaces: ('ones' | 'tens' | 'hundreds')[]
+ regroupPlaces: PlaceValue[]
}
/**
* Analyze an addition problem to determine its characteristics
- * Supports any positive integers a, b where a > 0 and b > 0
+ * Supports 1-5 digit problems (a and b can each be 1-99999)
+ * Maximum sum: 99999 + 99999 = 199998 (6 digits)
*/
export function analyzeProblem(a: number, b: number): ProblemMeta {
// Basic properties
@@ -27,17 +37,30 @@ export function analyzeProblem(a: number, b: number): ProblemMeta {
const digitsSum = sum.toString().length
// Analyze regrouping place by place
- // Pad to 3 digits for consistent indexing (supports up to 999 + 999)
- const aDigits = String(a).padStart(3, '0').split('').map(Number).reverse()
- const bDigits = String(b).padStart(3, '0').split('').map(Number).reverse()
+ // Pad to 6 digits for consistent indexing (supports up to 99999 + 99999 = 199998)
+ const aDigits = String(a).padStart(6, '0').split('').map(Number).reverse()
+ const bDigits = String(b).padStart(6, '0').split('').map(Number).reverse()
- const regroupPlaces: ('ones' | 'tens' | 'hundreds')[] = []
- const places = ['ones', 'tens', 'hundreds'] as const
+ const regroupPlaces: PlaceValue[] = []
+ const places: PlaceValue[] = [
+ 'ones',
+ 'tens',
+ 'hundreds',
+ 'thousands',
+ 'tenThousands',
+ 'hundredThousands',
+ ]
// Check each place value for carrying
- for (let i = 0; i < 3; i++) {
- if (aDigits[i] + bDigits[i] >= 10) {
+ // We need to track carries propagating through place values
+ let carry = 0
+ for (let i = 0; i < 6; i++) {
+ const digitSum = aDigits[i] + bDigits[i] + carry
+ if (digitSum >= 10) {
regroupPlaces.push(places[i])
+ carry = 1
+ } else {
+ carry = 0
}
}
@@ -54,3 +77,79 @@ export function analyzeProblem(a: number, b: number): ProblemMeta {
regroupPlaces,
}
}
+
+/**
+ * Metadata for a subtraction problem
+ */
+export interface SubtractionProblemMeta {
+ minuend: number
+ subtrahend: number
+ digitsMinuend: number
+ digitsSubtrahend: number
+ maxDigits: number
+ difference: number
+ digitsDifference: number
+ requiresBorrowing: boolean
+ borrowCount: number
+ borrowPlaces: PlaceValue[]
+}
+
+/**
+ * Analyze a subtraction problem to determine its characteristics
+ * Supports 1-5 digit problems (minuend and subtrahend can each be 1-99999)
+ * Assumes minuend >= subtrahend (no negative results)
+ */
+export function analyzeSubtractionProblem(
+ minuend: number,
+ subtrahend: number
+): SubtractionProblemMeta {
+ // Basic properties
+ const digitsMinuend = minuend.toString().length
+ const digitsSubtrahend = subtrahend.toString().length
+ const maxDigits = Math.max(digitsMinuend, digitsSubtrahend)
+ const difference = minuend - subtrahend
+ const digitsDifference = difference === 0 ? 1 : difference.toString().length
+
+ // Analyze borrowing place by place
+ // Pad to 6 digits for consistent indexing
+ const mDigits = String(minuend).padStart(6, '0').split('').map(Number).reverse()
+ const sDigits = String(subtrahend).padStart(6, '0').split('').map(Number).reverse()
+
+ const borrowPlaces: PlaceValue[] = []
+ const places: PlaceValue[] = [
+ 'ones',
+ 'tens',
+ 'hundreds',
+ 'thousands',
+ 'tenThousands',
+ 'hundredThousands',
+ ]
+
+ // Check each place value for borrowing
+ // We need to track borrows propagating through place values
+ let borrow = 0
+ for (let i = 0; i < 6; i++) {
+ const mDigit = mDigits[i] - borrow
+ const sDigit = sDigits[i]
+
+ if (mDigit < sDigit) {
+ borrowPlaces.push(places[i])
+ borrow = 1 // Need to borrow from next higher place
+ } else {
+ borrow = 0
+ }
+ }
+
+ return {
+ minuend,
+ subtrahend,
+ digitsMinuend,
+ digitsSubtrahend,
+ maxDigits,
+ difference,
+ digitsDifference,
+ requiresBorrowing: borrowPlaces.length > 0,
+ borrowCount: borrowPlaces.length,
+ borrowPlaces,
+ }
+}