fix: correct segment expression formatting and rule detection
- Fix formatSegmentExpression to show "5 + 3" for direct 6-9 adds instead of "(5 - 3)" - Fix determineSegmentDecisions to only classify complement rules when negative terms exist - Prevent mislabeling direct 6-9 additions as FiveComplement rules - Add comprehensive test suite validating expression format and rule classification - Direct additions now properly show addition expressions without parentheses - Complement operations maintain parentheses format: "(pos - neg1 - neg2)" 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { generateUnifiedInstructionSequence } from '../unifiedStepGenerator'
|
||||
|
||||
describe('segment expression & rule detection', () => {
|
||||
it('direct 8 at ones: expression is "5 + 3", rule is Direct', () => {
|
||||
const seq = generateUnifiedInstructionSequence(0, 8) // +8
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.expression.replace(/\s+/g, '')).toBe('5+3')
|
||||
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
|
||||
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
|
||||
expect(seg.plan.some(p => p.rule === 'TenComplement')).toBe(false)
|
||||
})
|
||||
|
||||
it('direct 6 at ones: expression is "5 + 1", rule is Direct', () => {
|
||||
const seq = generateUnifiedInstructionSequence(0, 6) // +6
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.expression.replace(/\s+/g, '')).toBe('5+1')
|
||||
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
|
||||
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
|
||||
})
|
||||
|
||||
it('direct 9 at ones: expression is "5 + 4", rule is Direct', () => {
|
||||
const seq = generateUnifiedInstructionSequence(0, 9) // +9
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.expression.replace(/\s+/g, '')).toBe('5+4')
|
||||
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
|
||||
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
|
||||
})
|
||||
|
||||
it('five-complement at ones (start 2, +3): expression is "(5 - 2)"', () => {
|
||||
const seq = generateUnifiedInstructionSequence(2, 5)
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.expression.replace(/\s+/g, '')).toBe('(5-2)')
|
||||
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(true)
|
||||
})
|
||||
|
||||
it('five-complement at ones (start 1, +4): expression is "(5 - 1)"', () => {
|
||||
const seq = generateUnifiedInstructionSequence(1, 5)
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.expression.replace(/\s+/g, '')).toBe('(5-1)')
|
||||
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(true)
|
||||
})
|
||||
|
||||
it('ten-complement no cascade (19 + 1 = 20): has TenComplement but not Cascade', () => {
|
||||
const seq = generateUnifiedInstructionSequence(19, 20)
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.plan.some(p => p.rule === 'TenComplement')).toBe(true)
|
||||
expect(seg.plan.some(p => p.rule === 'Cascade')).toBe(false)
|
||||
})
|
||||
|
||||
it('ten-complement with cascade (199 + 1 = 200): has Cascade', () => {
|
||||
const seq = generateUnifiedInstructionSequence(199, 200)
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
expect(seg.plan.some(p => p.rule === 'Cascade')).toBe(true)
|
||||
})
|
||||
|
||||
it('ranges: segment.termRange encloses all step.termPosition', () => {
|
||||
const seq = generateUnifiedInstructionSequence(3478, 3500) // +22
|
||||
const tensSeg = seq.segments.find(s => s.place === 1)!
|
||||
expect(tensSeg).toBeDefined()
|
||||
|
||||
tensSeg.stepIndices.forEach(i => {
|
||||
const r = seq.steps[i].termPosition
|
||||
expect(r.startIndex >= tensSeg.termRange.startIndex).toBe(true)
|
||||
expect(r.endIndex <= tensSeg.termRange.endIndex).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('direct addition with multiple places maintains correct expressions', () => {
|
||||
const seq = generateUnifiedInstructionSequence(0, 68) // +68
|
||||
|
||||
// Tens place: +6 should be "5 + 1" (direct)
|
||||
const tensSeg = seq.segments.find(s => s.place === 1)
|
||||
if (tensSeg) {
|
||||
expect(tensSeg.expression.replace(/\s+/g, '')).toBe('50+10')
|
||||
expect(tensSeg.plan.some(p => p.rule === 'Direct')).toBe(true)
|
||||
}
|
||||
|
||||
// Ones place: +8 should be "5 + 3" (direct)
|
||||
const onesSeg = seq.segments.find(s => s.place === 0)
|
||||
if (onesSeg) {
|
||||
expect(onesSeg.expression.replace(/\s+/g, '')).toBe('5+3')
|
||||
expect(onesSeg.plan.some(p => p.rule === 'Direct')).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('ensures no false FiveComplement classification for direct 6-9 adds', () => {
|
||||
// Test all direct 6-9 additions to ensure they're never classified as FiveComplement
|
||||
for (let digit = 6; digit <= 9; digit++) {
|
||||
const seq = generateUnifiedInstructionSequence(0, digit)
|
||||
const seg = seq.segments.find(s => s.place === 0)!
|
||||
|
||||
expect(seg.plan.some(p => p.rule === 'Direct')).toBe(true)
|
||||
expect(seg.plan.some(p => p.rule === 'FiveComplement')).toBe(false)
|
||||
expect(seg.expression).toMatch(/^5 \+ \d+$/)
|
||||
}
|
||||
})
|
||||
|
||||
it('validates complement expressions use parentheses', () => {
|
||||
// Five complement
|
||||
const fiveCompSeq = generateUnifiedInstructionSequence(2, 5)
|
||||
const fiveCompSeg = fiveCompSeq.segments.find(s => s.place === 0)!
|
||||
expect(fiveCompSeg.expression).toMatch(/^\(.+\)$/) // Should be wrapped in parentheses
|
||||
|
||||
// Ten complement
|
||||
const tenCompSeq = generateUnifiedInstructionSequence(19, 20)
|
||||
const tenCompSeg = tenCompSeq.segments.find(s => s.place === 0)!
|
||||
expect(tenCompSeg.expression).toMatch(/^\(.+\)$/) // Should be wrapped in parentheses
|
||||
})
|
||||
})
|
||||
@@ -191,13 +191,18 @@ function decisionForTenComplement(a: number, d: number, nextIs9: boolean): Segme
|
||||
}
|
||||
|
||||
function formatSegmentExpression(terms: string[]): string {
|
||||
// single term -> "40"
|
||||
if (terms.length === 1 && !terms[0].startsWith('-')) return terms[0]
|
||||
if (terms.length === 0) return ''
|
||||
|
||||
// complement group -> "(pos - n1 - n2 - ...)"
|
||||
const pos = terms[0]
|
||||
const negs = terms.slice(1).map(t => t.replace(/^-/, ''))
|
||||
return `(${pos} - ${negs.join(' - ')})`
|
||||
const positives = terms.filter(t => !t.startsWith('-'))
|
||||
const negatives = terms.filter(t => t.startsWith('-')).map(t => t.slice(1))
|
||||
|
||||
// All positive → join with pluses (no parentheses)
|
||||
if (negatives.length === 0) {
|
||||
return positives.join(' + ')
|
||||
}
|
||||
|
||||
// Complement group → (pos - n1 - n2 - …)
|
||||
return `(${positives[0]} - ${negatives.join(' - ')})`
|
||||
}
|
||||
|
||||
function formatSegmentGoal(digit: number, placeValue: number): string {
|
||||
@@ -260,34 +265,44 @@ function determineSegmentDecisions(
|
||||
): SegmentDecision[] {
|
||||
const sum = currentDigit + digit
|
||||
|
||||
if (steps.length === 1) {
|
||||
// If there is exactly one step and it's positive, it's direct.
|
||||
if (steps.length === 1 && !steps[0].operation.startsWith('-')) {
|
||||
return [{
|
||||
rule: 'Direct',
|
||||
conditions: [`a+d=${currentDigit}+${digit}=${sum} ≤ 9`],
|
||||
explanation: ['Fits in this place; add earth beads directly.']
|
||||
explanation: ['Fits in this place; add beads directly.']
|
||||
}]
|
||||
}
|
||||
|
||||
const positives = steps.filter(s => !s.operation.startsWith('-')).map(s => parseInt(s.operation, 10))
|
||||
const negatives = steps.filter(s => s.operation.startsWith('-')).map(s => Math.abs(parseInt(s.operation, 10)))
|
||||
|
||||
// No negatives → it's a direct (possibly 5+earth remainder) entry, not complement
|
||||
if (negatives.length === 0) {
|
||||
return [{
|
||||
rule: 'Direct',
|
||||
conditions: [`a+d=${currentDigit}+${digit}=${sum} ≤ 9`],
|
||||
explanation: ['Heaven bead (5) plus lower beads: still direct addition.']
|
||||
}]
|
||||
}
|
||||
|
||||
// There are negatives → complement family
|
||||
const hasFiveAdd = positives.some(v => Number.isInteger(v / 5) && isPowerOfTen(v / 5))
|
||||
const tenAdd = positives.find(v => isPowerOfTenGE10(v))
|
||||
const hasTenAdd = tenAdd !== undefined
|
||||
|
||||
if (hasFiveAdd && !hasTenAdd) {
|
||||
return decisionForFiveComplement(currentDigit, digit)
|
||||
}
|
||||
|
||||
if (hasTenAdd) {
|
||||
const tenAddPlace = Math.round(Math.log10(tenAdd!))
|
||||
// If the +10^k lands above the immediate next place, we must have rippled through 9s.
|
||||
// Alternatively, multiple distinct higher places in negatives indicates cascade.
|
||||
const negPlaces = new Set(negatives.map(v => Math.floor(Math.log10(v))))
|
||||
const cascades = tenAddPlace > place + 1 || negPlaces.size >= 2
|
||||
return decisionForTenComplement(currentDigit, digit, cascades)
|
||||
}
|
||||
|
||||
if (hasFiveAdd) {
|
||||
return decisionForFiveComplement(currentDigit, digit)
|
||||
}
|
||||
|
||||
// Fallback (unlikely with current generators)
|
||||
return [{
|
||||
rule: 'Direct',
|
||||
conditions: [`processing digit ${digit} at ${getPlaceName(place)}`],
|
||||
|
||||
Reference in New Issue
Block a user