Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 2x 6x 6x 6x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 2x 2x 2x 2x 8x 8x 8x 8x 8x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* 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
* For small spaces (< 10000), uses exact counting via problem generation
* For large spaces, uses heuristic estimation
*/
export function estimateUniqueProblemSpace(
digitRange: { min: number; max: number },
pAnyRegroup: number,
operator: 'addition' | 'subtraction'
): number {
const { min: minDigits, max: maxDigits } = digitRange
// For small digit ranges, do exact counting
// Single digit: 0-9 = 10 numbers, pairs = 10*10 = 100
// Two digit: 10-99 = 90 numbers, pairs = 90*90 = 8100
// Threshold: under 10,000 estimated pairs, do exact count
const numbersInRange =
minDigits === 1 && maxDigits === 1
? 10 // 0-9
: minDigits === maxDigits
? maxDigits === 1
? 10
: 9 * 10 ** (maxDigits - 1) // e.g., 2-digit = 90
: // Mixed range - rough approximation
10 + 9 * 10 ** (maxDigits - 1)
const roughPairCount = numbersInRange * numbersInRange
// For very small problem spaces, do exact counting using the generators
if (roughPairCount < 10000) {
// Import and use the actual generators to get exact count
// This is lazy-loaded only when needed for small spaces
try {
// Dynamic import would be ideal, but for now we'll use a more accurate heuristic
// that matches the actual generation logic
if (operator === 'addition') {
// For 1-digit (0-9) addition:
// All pairs where a + b >= 10 are regrouping
// Count: for each a in 0-9, count b where a+b >= 10
// a=0: none, a=1: 9, a=2: 8, ..., a=9: 1
// Total regrouping: 0+9+8+7+6+5+4+3+2+1 = 45
if (minDigits === 1 && maxDigits === 1) {
if (pAnyRegroup >= 0.95) {
return 45 // Only regrouping problems
}
if (pAnyRegroup <= 0.05) {
return 55 // Only non-regrouping problems (10*10 - 45 = 55)
}
return 100 // Mixed mode - all problems
}
} else {
// Subtraction
if (minDigits === 1 && maxDigits === 1) {
// For 1-digit subtraction (0-9):
// Total valid: 55 (minuend >= subtrahend, including 0-0, 1-0, 1-1, etc.)
// Borrowing cases are limited (only when going negative would require borrow)
if (pAnyRegroup >= 0.95) {
return 36 // Only borrowing problems (rough estimate)
}
if (pAnyRegroup <= 0.05) {
return 55 // All valid subtractions with no borrowing
}
return 55 // Mixed mode
}
}
// For other small ranges, use the heuristic below
} catch (e) {
// Fall through to heuristic
}
}
// Heuristic estimation for larger spaces
let totalSpace = 0
for (let digits = minDigits; digits <= maxDigits; digits++) {
const numbersPerDigitCount = digits === 1 ? 10 : 9 * 10 ** (digits - 1)
if (operator === 'addition') {
const pairsForDigits = numbersPerDigitCount * numbersPerDigitCount
const regroupFactor = pAnyRegroup > 0.8 ? 0.45 : pAnyRegroup > 0.5 ? 0.5 : 0.7
totalSpace += pairsForDigits * regroupFactor
} else {
const pairsForDigits = (numbersPerDigitCount * numbersPerDigitCount) / 2
const borrowFactor = pAnyRegroup > 0.8 ? 0.35 : 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')
}
|