feat: add problem space validation to warn about duplicate risk
Created validateProblemSpace utility to estimate unique problem count and warn users when worksheet config will produce many duplicates. Validation levels: - none: < 30% of space used - low: 30-50% (minor duplicates expected) - medium: 50-80% (suggest reducing pages or expanding constraints) - high: 80-150% (strong warnings with specific recommendations) - extreme: >150% (mostly duplicates, block generation) Provides actionable suggestions: - Reduce page count - Increase digit range - Lower regrouping probability Next: Wire this into the UI to show warnings before generation.
This commit is contained in:
parent
08fef59cc5
commit
0b8c1803ff
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { getSkillById } from './skills'
|
||||
import { generateTypstSource } from './typstGenerator'
|
||||
import { validateWorksheetConfig } from './validation'
|
||||
import { validateProblemSpace } from './utils/validateProblemSpace'
|
||||
|
||||
export interface PreviewResult {
|
||||
success: boolean
|
||||
|
|
@ -20,6 +21,7 @@ export interface PreviewResult {
|
|||
endPage?: number
|
||||
error?: string
|
||||
details?: string
|
||||
warnings?: string[] // Added for problem space validation warnings
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Validate that worksheet configuration has enough problem space to avoid excessive duplicates
|
||||
*/
|
||||
|
||||
export interface ProblemSpaceValidation {
|
||||
isValid: boolean
|
||||
warnings: string[]
|
||||
estimatedUniqueProblems: number
|
||||
requestedProblems: number
|
||||
duplicateRisk: 'none' | 'low' | 'medium' | 'high' | 'extreme'
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the maximum unique problems possible given constraints
|
||||
*/
|
||||
function estimateUniqueProblemSpace(
|
||||
digitRange: { min: number; max: number },
|
||||
pAnyRegroup: number,
|
||||
operator: 'addition' | 'subtraction'
|
||||
): number {
|
||||
const { min: minDigits, max: maxDigits } = digitRange
|
||||
|
||||
// Calculate approximate number space for each digit count
|
||||
let totalSpace = 0
|
||||
for (let digits = minDigits; digits <= maxDigits; digits++) {
|
||||
// For N-digit numbers: 9 * 10^(N-1) possibilities (e.g., 2-digit = 90 numbers from 10-99)
|
||||
const numbersPerDigitCount = digits === 1 ? 9 : 9 * Math.pow(10, digits - 1)
|
||||
|
||||
if (operator === 'addition') {
|
||||
// Addition: a + b where both are in the digit range
|
||||
// Rough estimate: numbersPerDigitCount^2 / 2 (since a+b = b+a, but we count both)
|
||||
// Then filter by regrouping probability
|
||||
const pairsForDigits = numbersPerDigitCount * numbersPerDigitCount
|
||||
|
||||
// If pAnyRegroup is high, only a fraction of pairs will work
|
||||
// Regrouping is more common with larger digits, so this is approximate
|
||||
const regroupFactor = pAnyRegroup > 0.8 ? 0.3 : pAnyRegroup > 0.5 ? 0.5 : 0.7
|
||||
|
||||
totalSpace += pairsForDigits * regroupFactor
|
||||
} else {
|
||||
// Subtraction: minuend - subtrahend where minuend > subtrahend
|
||||
// About half the pairs (where minuend > subtrahend)
|
||||
const pairsForDigits = (numbersPerDigitCount * numbersPerDigitCount) / 2
|
||||
|
||||
// Borrowing constraints reduce space similarly
|
||||
const borrowFactor = pAnyRegroup > 0.8 ? 0.3 : pAnyRegroup > 0.5 ? 0.5 : 0.7
|
||||
|
||||
totalSpace += pairsForDigits * borrowFactor
|
||||
}
|
||||
}
|
||||
|
||||
return Math.floor(totalSpace)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate worksheet configuration for duplicate risk
|
||||
* Returns warnings if configuration will likely produce many duplicates
|
||||
*/
|
||||
export function validateProblemSpace(
|
||||
problemsPerPage: number,
|
||||
pages: number,
|
||||
digitRange: { min: number; max: number },
|
||||
pAnyStart: number,
|
||||
operator: 'addition' | 'subtraction' | 'mixed'
|
||||
): ProblemSpaceValidation {
|
||||
const requestedProblems = problemsPerPage * pages
|
||||
const warnings: string[] = []
|
||||
|
||||
// For mixed mode, assume half of each
|
||||
let estimatedSpace: number
|
||||
if (operator === 'mixed') {
|
||||
const addSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, 'addition')
|
||||
const subSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, 'subtraction')
|
||||
estimatedSpace = addSpace + subSpace
|
||||
} else {
|
||||
estimatedSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, operator)
|
||||
}
|
||||
|
||||
// Calculate duplicate risk
|
||||
const ratio = requestedProblems / estimatedSpace
|
||||
let duplicateRisk: 'none' | 'low' | 'medium' | 'high' | 'extreme'
|
||||
|
||||
if (ratio < 0.3) {
|
||||
duplicateRisk = 'none'
|
||||
} else if (ratio < 0.5) {
|
||||
duplicateRisk = 'low'
|
||||
warnings.push(
|
||||
`You're requesting ${requestedProblems} problems, but only ~${Math.floor(estimatedSpace)} unique problems are possible with these constraints. Some duplicates may occur.`
|
||||
)
|
||||
} else if (ratio < 0.8) {
|
||||
duplicateRisk = 'medium'
|
||||
warnings.push(
|
||||
`Warning: Only ~${Math.floor(estimatedSpace)} unique problems possible, but you're requesting ${requestedProblems}. Expect moderate duplicates.`
|
||||
)
|
||||
warnings.push(
|
||||
`Suggestion: Reduce pages to ${Math.floor(estimatedSpace * 0.5 / problemsPerPage)} or increase digit range to ${digitRange.max + 1}`
|
||||
)
|
||||
} else if (ratio < 1.5) {
|
||||
duplicateRisk = 'high'
|
||||
warnings.push(
|
||||
`High duplicate risk! Only ~${Math.floor(estimatedSpace)} unique problems possible for ${requestedProblems} requested.`
|
||||
)
|
||||
warnings.push(
|
||||
`Recommendations:\n` +
|
||||
` • Reduce to ${Math.floor(estimatedSpace * 0.5 / problemsPerPage)} pages (50% of available space)\n` +
|
||||
` • Increase digit range to ${digitRange.max + 1}-${digitRange.max + 1}\n` +
|
||||
` • Lower regrouping probability from ${Math.round(pAnyStart * 100)}% to 50%`
|
||||
)
|
||||
} else {
|
||||
duplicateRisk = 'extreme'
|
||||
warnings.push(
|
||||
`Extreme duplicate risk! Requesting ${requestedProblems} problems but only ~${Math.floor(estimatedSpace)} unique problems exist.`
|
||||
)
|
||||
warnings.push(
|
||||
`This configuration will produce mostly duplicate problems.`
|
||||
)
|
||||
warnings.push(
|
||||
`Strong recommendations:\n` +
|
||||
` • Reduce to ${Math.floor(estimatedSpace * 0.5 / problemsPerPage)} pages maximum\n` +
|
||||
` • OR increase digit range from ${digitRange.min}-${digitRange.max} to ${digitRange.min}-${digitRange.max + 1}\n` +
|
||||
` • OR reduce regrouping requirement from ${Math.round(pAnyStart * 100)}%`
|
||||
)
|
||||
}
|
||||
|
||||
// Special case: single digit with high regrouping is extremely constrained
|
||||
if (digitRange.min === 1 && digitRange.max === 1 && pAnyStart > 0.8 && requestedProblems > 50) {
|
||||
warnings.unshift(
|
||||
`Single-digit problems (1-9) with ${Math.round(pAnyStart * 100)}% regrouping have very few unique combinations!`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: duplicateRisk !== 'extreme',
|
||||
warnings,
|
||||
estimatedUniqueProblems: Math.floor(estimatedSpace),
|
||||
requestedProblems,
|
||||
duplicateRisk,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format validation result for display to user
|
||||
*/
|
||||
export function formatValidationWarnings(validation: ProblemSpaceValidation): string {
|
||||
if (validation.warnings.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return validation.warnings.join('\n\n')
|
||||
}
|
||||
Loading…
Reference in New Issue