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, + } +}