fix: correct pedagogical algorithm specification and tests

Key corrections based on feedback:
- Fix 5's complement definition: d = (5 - (5-d)) when can't push d lowers
- Use a + d ≥ 10 as strict trigger for 10's complement
- Add ripple-carry logic for cascading through 9s
- Include borrowing rules for subtraction during complements
- Update test cases with mathematically correct expectations
- Add worked examples showing proper bead state tracking

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-24 17:14:39 -05:00
parent e5450f6aea
commit 9e87d3ac37
2 changed files with 472 additions and 51 deletions

View File

@@ -1,77 +1,110 @@
# Soroban Pedagogical Expansion Algorithm
## Overview
This algorithm generates pedagogical expansions that show how to perform arithmetic operations on a soroban (Japanese abacus) by analyzing physical bead movement constraints.
This algorithm generates pedagogical expansions that show how to perform arithmetic operations on a soroban (Japanese abacus) by analyzing physical bead movement constraints and current abacus state.
## Key Principle
The LHS (starting value) is ALREADY on the abacus. The pedagogical expansion only shows how to perform the operation (RHS) to reach the target value.
The LHS (starting value) is ALREADY on the abacus. The pedagogical expansion only shows how to perform the operation (addend/RHS) to reach the target value.
## Addition Algorithm
### Input
- Current abacus state = LHS value (already displayed)
- Operation to perform = RHS value (the number to add)
### Setup
- Abacus shows LHS value (already displayed)
- RHS is the addend (number to add)
- Leave an extra blank rod on the left to absorb carries
- Target = LHS + RHS
### Process
1. **Parse RHS digit by digit from highest place value to lowest**
2. **For each digit D at place value P:**
### Process - Left to Right Processing
**Step A: Try Direct Entry**
- Attempt to add digit D directly by moving beads at place P
- If successful, continue to next digit
For each digit at place P from most-significant to least-significant:
**Step B: Try 5's Complement**
- If direct entry fails, try using heaven bead
- Replace D with `(5 + (D-5))` if heaven bead available
- If successful, continue to next digit
1. **Setup for Place P**
- Let `d` = RHS digit at place P. If `d = 0`, continue to next place.
- Let `a` = current digit showing at place P (09)
**Step C: Try 10's Complement**
- If heaven bead already active OR digit still won't fit
- Add 10 to place value P+1 (next highest place)
- Subtract `(10 - D)` from place value P
- Expression: `D = (10 - (10-D))`
2. **Decision: Direct Addition vs 10's Complement**
- **If `a + d ≤ 9` (no carry needed):** Add within place P
- **If `a + d ≥ 10` (carry needed):** Use 10's complement
**Step D: Try 100's Complement**
- If can't add 10 to P+1 (because it shows 9)
- Add 100 to place value P+2
- Subtract 90 from place value P+1
- Subtract `(10 - D)` from place value P
3. **Case A: Direct Addition at Place P (a + d ≤ 9)**
**Step E: Try 1000's Complement**
- If can't add 100 to P+2 (because it shows 9)
- Add 1000 to place value P+3
- Subtract 900 from place value P+2
- Subtract 90 from place value P+1
- Subtract `(10 - D)` from place value P
**For d ≤ 4:**
- If you can push `d` lower beads: do it directly
- Else (not enough lower capacity, upper bead is up): Use 5's complement:
- Add 5 (activate upper bead)
- Subtract `(5 - d)` (remove lower beads)
- Expression: `d = (5 - (5-d))`
**Step F: Continue Pattern**
- Keep cascading up place values as needed
- Each level adds the next power of 10 and subtracts appropriate complements
**For d ≥ 5:**
- If possible: activate upper bead (if not already) and push `d - 5` lower beads
- If that won't fit: fall back to Case B (10's complement)
3. **Generate Parenthesized Expressions**
- Each complement operation becomes a parenthesized replacement
- Example: `7 = (10 - 3)` for ten's complement
- Example: `6 = (5 + 1)` for five's complement
4. **Case B: 10's Complement (a + d ≥ 10)**
4. **Process Left to Right**
- Continue until entire RHS is processed
- Each digit gets handled with appropriate complement strategy
**Ripple-Carry Process:**
- Find the nearest higher non-9 place value
- Increment that place by 1
- Set any intervening 9s to 0
## Examples
**Subtraction at Place P:**
- Subtract `(10 - d)` at place P
- **If can't subtract at P (borrowing needed):**
- Borrow 10 from the place just incremented (decrement by 1, add 10 to P)
- Then subtract `(10 - d)` at place P
### Direct Entry
- `4 + 3 = 7` → No complement needed, direct bead movement
**Expression:** `d = (10 - (10-d))`
### Five's Complement
- `0 + 6 = 0 + (5 + 1) = 6` → Replace 6 with (5 + 1)
5. **Continue to Next Place**
- Move to next place value to the right
- Repeat process
### Ten's Complement
- `4 + 7 = 4 + (10 - 3) = 11` → Replace 7 with (10 - 3)
### Invariant
After finishing each place, every rod shows a single decimal digit (09), and the abacus equals LHS + the processed prefix of RHS.
### Multi-Place Processing
- `89 + 25` → Process as `80 + 9 + 20 + 5`
- Handle 20 first (add to tens place), then 5 (add to ones place)
## Worked Examples
### Example 1: 268 + 795 = 1063
```
Start: 2|6|8
Hundreds (7): a=2, d=7, a+d=9 ≤ 9 → Direct addition (5+2 lowers)
Result: 9|6|8
Tens (9): a=6, d=9, a+d=15 ≥ 10 → 10's complement
Ripple-carry: hundreds=9 → set to 0, increment thousands → 1|0|6|8
Subtract (10-9)=1 from tens: 6-1=5 → 1|0|5|8
Ones (5): a=8, d=5, a+d=13 ≥ 10 → 10's complement
Carry: tens 5→6
Subtract (10-5)=5 from ones: 8-5=3
Result: 1|0|6|3 = 1063
```
### Example 2: 999 + 1 = 1000
```
Start: 9|9|9
Ones (1): a=9, d=1, a+d=10 ≥ 10 → 10's complement
Ripple-carry across 9s: increment thousands to 1, clear hundreds and tens to 0
Subtract (10-1)=9 from ones: 9-9=0
Result: 1|0|0|0 = 1000
```
### Example 3: 4 + 3 = 7 (Direct)
```
Start: 4 (heaven up, 4 lowers down)
Ones (3): a=4, d=3, a+d=7 ≤ 9 → But can't push 3 more lowers (would be 7 lowers)
Use 5's complement: 3 = (5 - 2)
Add 5 (second heaven bead), subtract 2 lowers
Result: 7 (both heaven beads, 2 lowers) = 7
```
### Example 4: 7 + 8 = 15 (5's complement impossible)
```
Start: 7 (heaven up, 2 lowers down)
Ones (8): a=7, d=8, a+d=15 ≥ 10 → 10's complement
Add 10 to tens place
Subtract (10-8)=2: but need to borrow first
Borrow 10 from tens (decrement tens, add 10 to ones): 7+10=17
Subtract 2: 17-2=15, but 15 > 9 so carry: tens+1, ones=5
Result: 1|5 = 15
```
## Subtraction Algorithm
**TODO: Implement in separate sprint**

View File

@@ -0,0 +1,388 @@
import { describe, it, expect } from 'vitest'
import { generateUnifiedInstructionSequence } from '../unifiedStepGenerator'
describe('Pedagogical Expansion Algorithm - Addition Only', () => {
describe('Level 1: Direct Entry (No Complements)', () => {
const directEntryTests = [
{
start: 0, target: 1,
expected: {
decomposition: '0 + 1 = 1',
meaningful: false,
steps: [{ term: '1', value: 1, instruction: /add.*1.*earth/i }]
}
},
{
start: 1, target: 3,
expected: {
decomposition: '1 + 2 = 3',
meaningful: false,
steps: [{ term: '2', value: 3, instruction: /add.*2.*earth/i }]
}
},
{
start: 0, target: 4,
expected: {
decomposition: '0 + 4 = 4',
meaningful: false,
steps: [{ term: '4', value: 4, instruction: /add.*4.*earth/i }]
}
},
{
start: 0, target: 5,
expected: {
decomposition: '0 + 5 = 5',
meaningful: false,
steps: [{ term: '5', value: 5, instruction: /activate.*heaven/i }]
}
},
{
start: 5, target: 7,
expected: {
decomposition: '5 + 2 = 7',
meaningful: false,
steps: [{ term: '2', value: 7, instruction: /add.*2.*earth/i }]
}
},
{
start: 0, target: 10,
expected: {
decomposition: '0 + 10 = 10',
meaningful: false,
steps: [{ term: '10', value: 10, instruction: /add.*1.*tens/i }]
}
}
]
directEntryTests.forEach(test => {
it(`should handle direct entry: ${test.start} + ${test.target - test.start} = ${test.target}`, () => {
const result = generateUnifiedInstructionSequence(test.start, test.target)
expect(result.fullDecomposition).toContain(test.expected.decomposition)
expect(result.isMeaningfulDecomposition).toBe(test.expected.meaningful)
expect(result.steps).toHaveLength(test.expected.steps.length)
test.expected.steps.forEach((expectedStep, i) => {
expect(result.steps[i].mathematicalTerm).toBe(expectedStep.term)
expect(result.steps[i].expectedValue).toBe(expectedStep.value)
expect(result.steps[i].englishInstruction).toMatch(expectedStep.instruction)
})
})
})
})
describe('Level 2: Five-Complements Required', () => {
const fiveComplementTests = [
{
start: 4, target: 7, // 4 + 3, but can't add 3 lowers (4+3=7 lowers, max is 4)
expected: {
decomposition: '4 + 3 = 4 + (5 - 2) = 7',
meaningful: true,
steps: [
{ term: '5', value: 9, instruction: /add.*5/i }, // Add upper bead
{ term: '-2', value: 7, instruction: /remove.*2.*earth/i } // Remove 2 lowers
]
}
},
{
start: 3, target: 5, // 3 + 2, but can't add 2 lowers (3+2=5 lowers, max is 4)
expected: {
decomposition: '3 + 2 = 3 + (5 - 3) = 5',
meaningful: true,
steps: [
{ term: '5', value: 8, instruction: /add.*5/i },
{ term: '-3', value: 5, instruction: /remove.*3.*earth/i }
]
}
},
{
start: 2, target: 3, // 2 + 1, but can't add 1 lower (2+1=3 lowers, this should actually be direct)
expected: {
decomposition: '2 + 1 = 3',
meaningful: false, // This is direct addition
steps: [{ term: '1', value: 3, instruction: /add.*1.*earth/i }]
}
},
{
start: 0, target: 6, // 0 + 6 = 5 + 1 (direct decomposition, not complement)
expected: {
decomposition: '0 + 6 = 0 + 5 + 1 = 6',
meaningful: false, // Direct: activate heaven, add 1 lower
steps: [
{ term: '5', value: 5, instruction: /activate.*heaven/i },
{ term: '1', value: 6, instruction: /add.*1.*earth/i }
]
}
},
{
start: 1, target: 5, // 1 + 4, but can't add 4 lowers (1+4=5 lowers, max is 4)
expected: {
decomposition: '1 + 4 = 1 + (5 - 1) = 5',
meaningful: true,
steps: [
{ term: '5', value: 6, instruction: /add.*5/i },
{ term: '-1', value: 5, instruction: /remove.*1.*earth/i }
]
}
}
]
fiveComplementTests.forEach(test => {
it(`should handle five-complement: ${test.start}${test.target}`, () => {
const result = generateUnifiedInstructionSequence(test.start, test.target)
expect(result.fullDecomposition).toContain(test.expected.decomposition)
expect(result.isMeaningfulDecomposition).toBe(test.expected.meaningful)
expect(result.steps).toHaveLength(test.expected.steps.length)
test.expected.steps.forEach((expectedStep, i) => {
expect(result.steps[i].mathematicalTerm).toBe(expectedStep.term)
expect(result.steps[i].expectedValue).toBe(expectedStep.value)
expect(result.steps[i].englishInstruction).toMatch(expectedStep.instruction)
})
})
})
})
describe('Level 3: Ten-Complements Required', () => {
const tenComplementTests = [
{
start: 4, target: 11, // 4 + 7, a+d = 11 ≥ 10
expected: {
decomposition: '4 + 7 = 4 + (10 - 3) = 11',
meaningful: true,
steps: [
{ term: '10', value: 14, instruction: /add.*1.*tens/i },
{ term: '-3', value: 11, instruction: /remove.*3.*earth/i }
]
}
},
{
start: 6, target: 15, // 6 + 9, a+d = 15 ≥ 10
expected: {
decomposition: '6 + 9 = 6 + (10 - 1) = 15',
meaningful: true,
steps: [
{ term: '10', value: 16, instruction: /add.*1.*tens/i },
{ term: '-1', value: 15, instruction: /remove.*1.*earth/i }
]
}
},
{
start: 7, target: 15, // 7 + 8, a+d = 15 ≥ 10
expected: {
decomposition: '7 + 8 = 7 + (10 - 2) = 15',
meaningful: true,
steps: [
{ term: '10', value: 17, instruction: /add.*1.*tens/i },
{ term: '-2', value: 15, instruction: /remove.*2.*earth/i }
]
}
},
{
start: 5, target: 9, // 5 + 4, a+d = 9 ≤ 9 (should be direct)
expected: {
decomposition: '5 + 4 = 9',
meaningful: false,
steps: [{ term: '4', value: 9, instruction: /add.*4.*earth/i }]
}
},
{
start: 9, target: 18, // 9 + 9, a+d = 18 ≥ 10
expected: {
decomposition: '9 + 9 = 9 + (10 - 1) = 18',
meaningful: true,
steps: [
{ term: '10', value: 19, instruction: /add.*1.*tens/i },
{ term: '-1', value: 18, instruction: /remove.*1.*earth/i }
]
}
}
]
tenComplementTests.forEach(test => {
it(`should handle ten-complement: ${test.start}${test.target}`, () => {
const result = generateUnifiedInstructionSequence(test.start, test.target)
expect(result.fullDecomposition).toContain(test.expected.decomposition)
expect(result.isMeaningfulDecomposition).toBe(test.expected.meaningful)
expect(result.steps).toHaveLength(test.expected.steps.length)
test.expected.steps.forEach((expectedStep, i) => {
expect(result.steps[i].mathematicalTerm).toBe(expectedStep.term)
expect(result.steps[i].expectedValue).toBe(expectedStep.value)
expect(result.steps[i].englishInstruction).toMatch(expectedStep.instruction)
})
})
})
})
describe('Level 4: Multi-Place Value Operations', () => {
const multiPlaceTests = [
{
start: 12, target: 34,
expected: {
decomposition: '12 + 22 = 12 + 20 + 2 = 34',
meaningful: true,
steps: [
{ term: '20', value: 32, instruction: /add.*2.*tens/i },
{ term: '2', value: 34, instruction: /add.*2.*earth/i }
]
}
},
{
start: 23, target: 47,
expected: {
decomposition: '23 + 24 = 23 + 20 + 4 = 47',
meaningful: true,
steps: [
{ term: '20', value: 43, instruction: /add.*2.*tens/i },
{ term: '4', value: 47, instruction: /add.*4.*earth/i }
]
}
},
{
start: 34, target: 78,
expected: {
decomposition: '34 + 44 = 34 + 40 + 4 = 78',
meaningful: true,
steps: [
{ term: '40', value: 74, instruction: /add.*4.*tens/i },
{ term: '4', value: 78, instruction: /add.*4.*earth/i }
]
}
}
]
multiPlaceTests.forEach(test => {
it(`should handle multi-place: ${test.start}${test.target}`, () => {
const result = generateUnifiedInstructionSequence(test.start, test.target)
expect(result.fullDecomposition).toContain(test.expected.decomposition)
expect(result.isMeaningfulDecomposition).toBe(test.expected.meaningful)
expect(result.steps).toHaveLength(test.expected.steps.length)
test.expected.steps.forEach((expectedStep, i) => {
expect(result.steps[i].mathematicalTerm).toBe(expectedStep.term)
expect(result.steps[i].expectedValue).toBe(expectedStep.value)
expect(result.steps[i].englishInstruction).toMatch(expectedStep.instruction)
})
})
})
})
describe('Level 5: Complex Cases with Cascading Complements', () => {
const complexTests = [
{
start: 89, target: 97,
expected: {
decomposition: '89 + 8 = 89 + (10 - 2) = 97',
meaningful: true,
steps: [
{ term: '10', value: 99, instruction: /add.*1.*tens/i },
{ term: '-2', value: 97, instruction: /remove.*2.*earth/i }
]
}
},
{
start: 99, target: 107,
expected: {
decomposition: '99 + 8 = 99 + (100 - 90) + (10 - 2) = 107',
meaningful: true,
steps: [
{ term: '100', value: 199, instruction: /add.*1.*hundreds/i },
{ term: '-90', value: 109, instruction: /remove.*9.*tens/i },
{ term: '-2', value: 107, instruction: /remove.*2.*earth/i }
]
}
}
]
complexTests.forEach(test => {
it(`should handle cascading complements: ${test.start}${test.target}`, () => {
const result = generateUnifiedInstructionSequence(test.start, test.target)
expect(result.fullDecomposition).toContain(test.expected.decomposition)
expect(result.isMeaningfulDecomposition).toBe(test.expected.meaningful)
expect(result.steps).toHaveLength(test.expected.steps.length)
test.expected.steps.forEach((expectedStep, i) => {
expect(result.steps[i].mathematicalTerm).toBe(expectedStep.term)
expect(result.steps[i].expectedValue).toBe(expectedStep.value)
expect(result.steps[i].englishInstruction).toMatch(expectedStep.instruction)
})
})
})
})
describe('Edge Cases and Validation', () => {
it('should handle zero difference (no operation)', () => {
const result = generateUnifiedInstructionSequence(5, 5)
expect(result.isMeaningfulDecomposition).toBe(false)
expect(result.steps).toHaveLength(0)
expect(result.fullDecomposition).toBe('5 + 0 = 5')
})
it('should maintain abacus state consistency throughout', () => {
const result = generateUnifiedInstructionSequence(0, 17)
// Each step should have valid expected state
result.steps.forEach(step => {
expect(step.expectedState).toBeDefined()
expect(step.expectedValue).toBeGreaterThan(0)
expect(step.isValid).toBe(true)
})
// Final step should reach target
const finalStep = result.steps[result.steps.length - 1]
expect(finalStep.expectedValue).toBe(17)
})
it('should generate proper bead movement data', () => {
const result = generateUnifiedInstructionSequence(0, 6)
result.steps.forEach(step => {
expect(Array.isArray(step.beadMovements)).toBe(true)
step.beadMovements.forEach(movement => {
expect(movement).toHaveProperty('placeValue')
expect(movement).toHaveProperty('beadType')
expect(movement).toHaveProperty('direction')
})
})
})
})
describe('Algorithm Bookkeeping Verification', () => {
it('should track abacus state after every operation', () => {
const result = generateUnifiedInstructionSequence(34, 89)
let currentValue = 34
result.steps.forEach(step => {
// Each step should progress toward target
expect(step.expectedValue).not.toBe(currentValue)
currentValue = step.expectedValue
// State should be valid for abacus constraints
Object.values(step.expectedState).forEach(beadState => {
expect(beadState.earthActive).toBeGreaterThanOrEqual(0)
expect(beadState.earthActive).toBeLessThanOrEqual(4)
expect(typeof beadState.heavenActive).toBe('boolean')
})
})
// Final value should match target
expect(currentValue).toBe(89)
})
it('should make correct complement decisions based on current state', () => {
// Test case where heaven bead is already active
const result = generateUnifiedInstructionSequence(7, 13) // 7 + 6
// Should NOT use five-complement for 6 since heaven already active
// Should either use ten-complement or break down 6 differently
const hasInvalidFiveComplement = result.fullDecomposition.includes('(5 + 1)')
expect(hasInvalidFiveComplement).toBe(false)
})
})
})