Add mixed denominator fractions worksheet option
This commit is contained in:
parent
9159608dcd
commit
2f841f2435
|
|
@ -1,7 +1,7 @@
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
|
|
||||||
export interface OperatorIconProps {
|
export interface OperatorIconProps {
|
||||||
operator: 'addition' | 'subtraction' | 'mixed'
|
operator: 'addition' | 'subtraction' | 'mixed' | 'fractions'
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||||
isDark?: boolean
|
isDark?: boolean
|
||||||
color?: 'gray' | 'green'
|
color?: 'gray' | 'green'
|
||||||
|
|
@ -15,7 +15,8 @@ const sizeMap = {
|
||||||
xl: 'xl',
|
xl: 'xl',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
function getOperatorSymbol(operator: 'addition' | 'subtraction' | 'mixed'): string {
|
function getOperatorSymbol(operator: 'addition' | 'subtraction' | 'mixed' | 'fractions'): string {
|
||||||
|
if (operator === 'fractions') return '⅟'
|
||||||
if (operator === 'mixed') return '±'
|
if (operator === 'mixed') return '±'
|
||||||
if (operator === 'subtraction') return '−'
|
if (operator === 'subtraction') return '−'
|
||||||
return '+'
|
return '+'
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { css } from '@styled/css'
|
||||||
import { OperatorIcon } from './OperatorIcon'
|
import { OperatorIcon } from './OperatorIcon'
|
||||||
|
|
||||||
export interface OperatorSectionProps {
|
export interface OperatorSectionProps {
|
||||||
operator: 'addition' | 'subtraction' | 'mixed' | undefined
|
operator: 'addition' | 'subtraction' | 'mixed' | 'fractions' | undefined
|
||||||
onChange: (operator: 'addition' | 'subtraction' | 'mixed') => void
|
onChange: (operator: 'addition' | 'subtraction' | 'mixed' | 'fractions') => void
|
||||||
isDark?: boolean
|
isDark?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ export function OperatorSection({ operator, onChange, isDark = false }: Operator
|
||||||
// Derive checkbox states from operator value
|
// Derive checkbox states from operator value
|
||||||
const additionChecked = operator === 'addition' || operator === 'mixed' || !operator
|
const additionChecked = operator === 'addition' || operator === 'mixed' || !operator
|
||||||
const subtractionChecked = operator === 'subtraction' || operator === 'mixed'
|
const subtractionChecked = operator === 'subtraction' || operator === 'mixed'
|
||||||
|
const fractionsChecked = operator === 'fractions'
|
||||||
|
|
||||||
const handleAdditionChange = (checked: boolean) => {
|
const handleAdditionChange = (checked: boolean) => {
|
||||||
if (!checked && !subtractionChecked) {
|
if (!checked && !subtractionChecked) {
|
||||||
|
|
@ -40,6 +41,10 @@ export function OperatorSection({ operator, onChange, isDark = false }: Operator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleFractionsSelect = () => {
|
||||||
|
onChange('fractions')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-section="operator-selection">
|
<div data-section="operator-selection">
|
||||||
<label
|
<label
|
||||||
|
|
@ -187,6 +192,85 @@ export function OperatorSection({ operator, onChange, isDark = false }: Operator
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Mixed-Denominator Fractions */}
|
||||||
|
<label
|
||||||
|
data-action="toggle-fractions"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '3',
|
||||||
|
cursor: 'pointer',
|
||||||
|
px: '3',
|
||||||
|
py: '2.5',
|
||||||
|
rounded: 'lg',
|
||||||
|
border: '2px solid',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
bg: fractionsChecked
|
||||||
|
? isDark
|
||||||
|
? 'brand.900'
|
||||||
|
: 'brand.50'
|
||||||
|
: isDark
|
||||||
|
? 'gray.700'
|
||||||
|
: 'white',
|
||||||
|
borderColor: fractionsChecked ? 'brand.500' : isDark ? 'gray.600' : 'gray.300',
|
||||||
|
_hover: {
|
||||||
|
borderColor: 'brand.400',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={fractionsChecked}
|
||||||
|
onChange={handleFractionsSelect}
|
||||||
|
className={css({
|
||||||
|
width: '5',
|
||||||
|
height: '5',
|
||||||
|
cursor: 'pointer',
|
||||||
|
accentColor: 'brand.600',
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '2',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<OperatorIcon operator="fractions" isDark={isDark} />
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5',
|
||||||
|
color: isDark ? 'gray.200' : 'gray.700',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
fontSize: 'sm',
|
||||||
|
fontWeight: 'semibold',
|
||||||
|
'@media (max-width: 200px)': {
|
||||||
|
fontSize: 'xs',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Mixed Denominator Fractions
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
fontSize: 'xs',
|
||||||
|
color: isDark ? 'gray.400' : 'gray.600',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Build fluency adding unlike denominators.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
|
|
@ -196,11 +280,13 @@ export function OperatorSection({ operator, onChange, isDark = false }: Operator
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{additionChecked && subtractionChecked
|
{fractionsChecked
|
||||||
? 'Problems will randomly use addition or subtraction'
|
? 'All problems will be mixed-denominator fraction addition'
|
||||||
: subtractionChecked
|
: additionChecked && subtractionChecked
|
||||||
? 'All problems will be subtraction'
|
? 'Problems will randomly use addition or subtraction'
|
||||||
: 'All problems will be addition'}
|
: subtractionChecked
|
||||||
|
? 'All problems will be subtraction'
|
||||||
|
: 'All problems will be addition'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ProblemPreview } from './ProblemPreview'
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
icon: string | ((operator?: 'addition' | 'subtraction' | 'mixed') => string) | 'preview'
|
icon: string | ((operator?: 'addition' | 'subtraction' | 'mixed' | 'fractions') => string) | 'preview'
|
||||||
subtitle?: (props: {
|
subtitle?: (props: {
|
||||||
mode?: 'custom' | 'manual' | 'mastery'
|
mode?: 'custom' | 'manual' | 'mastery'
|
||||||
difficultyProfile?: string
|
difficultyProfile?: string
|
||||||
|
|
@ -29,6 +29,7 @@ export const TABS: Tab[] = [
|
||||||
id: 'operator',
|
id: 'operator',
|
||||||
label: 'Operator',
|
label: 'Operator',
|
||||||
icon: (operator) => {
|
icon: (operator) => {
|
||||||
|
if (operator === 'fractions') return '⅟'
|
||||||
if (operator === 'mixed') return '±'
|
if (operator === 'mixed') return '±'
|
||||||
if (operator === 'subtraction') return '−'
|
if (operator === 'subtraction') return '−'
|
||||||
return '+'
|
return '+'
|
||||||
|
|
@ -115,7 +116,7 @@ export const TABS: Tab[] = [
|
||||||
interface TabNavigationProps {
|
interface TabNavigationProps {
|
||||||
activeTab: string
|
activeTab: string
|
||||||
onChange: (tabId: string) => void
|
onChange: (tabId: string) => void
|
||||||
operator?: 'addition' | 'subtraction' | 'mixed'
|
operator?: 'addition' | 'subtraction' | 'mixed' | 'fractions'
|
||||||
mode?: 'custom' | 'manual' | 'mastery'
|
mode?: 'custom' | 'manual' | 'mastery'
|
||||||
difficultyProfile?: string
|
difficultyProfile?: string
|
||||||
interpolate?: boolean
|
interpolate?: boolean
|
||||||
|
|
|
||||||
|
|
@ -342,8 +342,8 @@ const additionConfigV4BaseSchema = z.object({
|
||||||
message: 'min must be less than or equal to max',
|
message: 'min must be less than or equal to max',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// V4: Operator selection (addition, subtraction, or mixed)
|
// V4: Operator selection (addition, subtraction, mixed, or fractions)
|
||||||
operator: z.enum(['addition', 'subtraction', 'mixed']).default('addition'),
|
operator: z.enum(['addition', 'subtraction', 'mixed', 'fractions']).default('addition'),
|
||||||
|
|
||||||
// Regrouping probabilities (shared between modes)
|
// Regrouping probabilities (shared between modes)
|
||||||
pAnyStart: z.number().min(0).max(1),
|
pAnyStart: z.number().min(0).max(1),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
import type { WorksheetFormState } from '@/app/create/worksheets/types'
|
import type { WorksheetFormState } from '@/app/create/worksheets/types'
|
||||||
import {
|
import {
|
||||||
|
createPRNG,
|
||||||
generateMasteryMixedProblems,
|
generateMasteryMixedProblems,
|
||||||
|
generateFractionProblems,
|
||||||
generateMixedProblems,
|
generateMixedProblems,
|
||||||
generateProblems,
|
generateProblems,
|
||||||
generateSubtractionProblems,
|
generateSubtractionProblems,
|
||||||
|
|
@ -13,6 +15,48 @@ import { generateTypstSource } from './typstGenerator'
|
||||||
import { validateProblemSpace } from './utils/validateProblemSpace'
|
import { validateProblemSpace } from './utils/validateProblemSpace'
|
||||||
import { validateWorksheetConfig } from './validation'
|
import { validateWorksheetConfig } from './validation'
|
||||||
|
|
||||||
|
function renderFractionPageSvg(
|
||||||
|
pageProblems: ReturnType<typeof generateFractionProblems>,
|
||||||
|
config: WorksheetFormState,
|
||||||
|
pageIndex: number
|
||||||
|
): string {
|
||||||
|
const width = config.orientation === 'portrait' ? 850 : 1100
|
||||||
|
const height = config.orientation === 'portrait' ? 1100 : 850
|
||||||
|
const margin = 48
|
||||||
|
const cols = config.cols ?? 4
|
||||||
|
const rows = Math.max(1, Math.ceil(pageProblems.length / cols))
|
||||||
|
|
||||||
|
const cellWidth = (width - margin * 2) / cols
|
||||||
|
const cellHeight = (height - margin * 2) / rows
|
||||||
|
|
||||||
|
const problemTexts = pageProblems
|
||||||
|
.map((problem, index) => {
|
||||||
|
const col = index % cols
|
||||||
|
const row = Math.floor(index / cols)
|
||||||
|
const x = margin + col * cellWidth + cellWidth / 2
|
||||||
|
const y = margin + row * cellHeight + cellHeight / 2
|
||||||
|
|
||||||
|
const label = problem
|
||||||
|
? `${problem.numerator1}/${problem.denominator1} + ${problem.numerator2}/${problem.denominator2} =`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return `<g transform="translate(${x}, ${y})">
|
||||||
|
<text text-anchor="middle" dominant-baseline="middle" font-size="22" font-family="Inter, Arial" fill="#111827">${
|
||||||
|
(pageIndex * (config.problemsPerPage ?? 20)) + index + 1
|
||||||
|
}.) ${label}</text>
|
||||||
|
</g>`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||||
|
<rect x="0" y="0" width="${width}" height="${height}" fill="white"/>
|
||||||
|
<text x="${margin}" y="${margin - 16}" font-size="18" font-family="Inter, Arial" fill="#374151">
|
||||||
|
${config.name || 'Fraction Practice'} • Page ${pageIndex + 1}
|
||||||
|
</text>
|
||||||
|
${problemTexts}
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
export interface PreviewResult {
|
export interface PreviewResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
pages?: string[]
|
pages?: string[]
|
||||||
|
|
@ -64,13 +108,16 @@ export async function generateWorksheetPreview(
|
||||||
|
|
||||||
// Validate problem space for duplicate risk
|
// Validate problem space for duplicate risk
|
||||||
const operator = validatedConfig.operator ?? 'addition'
|
const operator = validatedConfig.operator ?? 'addition'
|
||||||
const spaceValidation = validateProblemSpace(
|
const spaceValidation =
|
||||||
validatedConfig.problemsPerPage,
|
operator === 'fractions'
|
||||||
validatedConfig.pages,
|
? { warnings: [] }
|
||||||
validatedConfig.digitRange,
|
: validateProblemSpace(
|
||||||
validatedConfig.pAnyStart,
|
validatedConfig.problemsPerPage,
|
||||||
operator
|
validatedConfig.pages,
|
||||||
)
|
validatedConfig.digitRange,
|
||||||
|
validatedConfig.pAnyStart,
|
||||||
|
operator
|
||||||
|
)
|
||||||
|
|
||||||
if (spaceValidation.warnings.length > 0) {
|
if (spaceValidation.warnings.length > 0) {
|
||||||
console.log('[PREVIEW] Problem space warnings:', spaceValidation.warnings)
|
console.log('[PREVIEW] Problem space warnings:', spaceValidation.warnings)
|
||||||
|
|
@ -78,6 +125,7 @@ export async function generateWorksheetPreview(
|
||||||
|
|
||||||
// Generate all problems for full preview based on operator
|
// Generate all problems for full preview based on operator
|
||||||
const mode = config.mode ?? 'custom'
|
const mode = config.mode ?? 'custom'
|
||||||
|
const rand = createPRNG(validatedConfig.seed ?? Date.now() % 2147483647)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[PREVIEW] Step 2: Generating ${validatedConfig.total} problems (mode: ${mode}, operator: ${operator})...`
|
`[PREVIEW] Step 2: Generating ${validatedConfig.total} problems (mode: ${mode}, operator: ${operator})...`
|
||||||
|
|
@ -146,18 +194,52 @@ export async function generateWorksheetPreview(
|
||||||
validatedConfig.interpolate,
|
validatedConfig.interpolate,
|
||||||
validatedConfig.seed
|
validatedConfig.seed
|
||||||
)
|
)
|
||||||
: generateMixedProblems(
|
: operator === 'fractions'
|
||||||
validatedConfig.total,
|
? generateFractionProblems(validatedConfig.total, rand)
|
||||||
validatedConfig.digitRange,
|
: generateMixedProblems(
|
||||||
validatedConfig.pAnyStart,
|
validatedConfig.total,
|
||||||
validatedConfig.pAllStart,
|
validatedConfig.digitRange,
|
||||||
validatedConfig.interpolate,
|
validatedConfig.pAnyStart,
|
||||||
validatedConfig.seed
|
validatedConfig.pAllStart,
|
||||||
)
|
validatedConfig.interpolate,
|
||||||
|
validatedConfig.seed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[PREVIEW] Step 2: ✓ Generated ${problems.length} problems`)
|
console.log(`[PREVIEW] Step 2: ✓ Generated ${problems.length} problems`)
|
||||||
|
|
||||||
|
if (operator === 'fractions') {
|
||||||
|
const problemsPerPage = validatedConfig.problemsPerPage ?? 20
|
||||||
|
const totalPages = Math.ceil(problems.length / problemsPerPage)
|
||||||
|
const start = startPage !== undefined ? Math.max(0, startPage) : 0
|
||||||
|
const end = endPage !== undefined ? Math.min(endPage, totalPages - 1) : totalPages - 1
|
||||||
|
|
||||||
|
if (start > end || start >= totalPages) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Invalid page range: start=${start}, end=${end}, totalPages=${totalPages}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages: string[] = []
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
const startIndex = i * problemsPerPage
|
||||||
|
const pageProblems = problems.slice(startIndex, startIndex + problemsPerPage) as ReturnType<
|
||||||
|
typeof generateFractionProblems
|
||||||
|
>
|
||||||
|
pages.push(renderFractionPageSvg(pageProblems, validatedConfig, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
pages,
|
||||||
|
totalPages,
|
||||||
|
startPage: start,
|
||||||
|
endPage: end,
|
||||||
|
warnings: spaceValidation.warnings.length > 0 ? spaceValidation.warnings : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate Typst sources (one per page)
|
// Generate Typst sources (one per page)
|
||||||
// Use placeholder URL for QR code in preview (actual URL will be generated when PDF is created)
|
// Use placeholder URL for QR code in preview (actual URL will be generated when PDF is created)
|
||||||
const previewShareUrl = validatedConfig.includeQRCode
|
const previewShareUrl = validatedConfig.includeQRCode
|
||||||
|
|
@ -280,6 +362,7 @@ export async function generateSinglePage(
|
||||||
// This is unavoidable because problems are distributed across pages
|
// This is unavoidable because problems are distributed across pages
|
||||||
const operator = validatedConfig.operator ?? 'addition'
|
const operator = validatedConfig.operator ?? 'addition'
|
||||||
const mode = config.mode ?? 'custom'
|
const mode = config.mode ?? 'custom'
|
||||||
|
const rand = createPRNG(validatedConfig.seed ?? Date.now() % 2147483647)
|
||||||
|
|
||||||
let problems
|
let problems
|
||||||
|
|
||||||
|
|
@ -328,6 +411,8 @@ export async function generateSinglePage(
|
||||||
validatedConfig.interpolate,
|
validatedConfig.interpolate,
|
||||||
validatedConfig.seed
|
validatedConfig.seed
|
||||||
)
|
)
|
||||||
|
} else if (operator === 'fractions') {
|
||||||
|
problems = generateFractionProblems(validatedConfig.total, rand)
|
||||||
} else if (operator === 'subtraction') {
|
} else if (operator === 'subtraction') {
|
||||||
problems = generateSubtractionProblems(
|
problems = generateSubtractionProblems(
|
||||||
validatedConfig.total,
|
validatedConfig.total,
|
||||||
|
|
@ -349,6 +434,20 @@ export async function generateSinglePage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (operator === 'fractions') {
|
||||||
|
const problemsPerPage = validatedConfig.problemsPerPage ?? 20
|
||||||
|
const startIndex = pageNumber * problemsPerPage
|
||||||
|
const pageProblems = problems.slice(startIndex, startIndex + problemsPerPage) as ReturnType<
|
||||||
|
typeof generateFractionProblems
|
||||||
|
>
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
page: renderFractionPageSvg(pageProblems, validatedConfig, pageNumber),
|
||||||
|
totalPages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate Typst source for ALL pages (lightweight operation)
|
// Generate Typst source for ALL pages (lightweight operation)
|
||||||
// Use placeholder URL for QR code in preview
|
// Use placeholder URL for QR code in preview
|
||||||
const previewShareUrl = validatedConfig.includeQRCode
|
const previewShareUrl = validatedConfig.includeQRCode
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AdditionProblem,
|
AdditionProblem,
|
||||||
|
FractionProblem,
|
||||||
ProblemCategory,
|
ProblemCategory,
|
||||||
SubtractionProblem,
|
SubtractionProblem,
|
||||||
WorksheetProblem,
|
WorksheetProblem,
|
||||||
|
|
@ -40,6 +41,42 @@ function shuffleArray<T>(arr: T[], rand: () => number): T[] {
|
||||||
return shuffled
|
return shuffled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate mixed-denominator fraction addition problems
|
||||||
|
*/
|
||||||
|
export function generateFractionProblems(
|
||||||
|
total: number,
|
||||||
|
rand: () => number
|
||||||
|
): FractionProblem[] {
|
||||||
|
const problems: FractionProblem[] = []
|
||||||
|
|
||||||
|
const pickDenominator = () => randint(2, 12, rand)
|
||||||
|
const pickNumerator = (denominator: number) => randint(1, denominator - 1, rand)
|
||||||
|
|
||||||
|
while (problems.length < total) {
|
||||||
|
const denominator1 = pickDenominator()
|
||||||
|
let denominator2 = pickDenominator()
|
||||||
|
|
||||||
|
// Ensure denominators are different to enforce mixed denominators
|
||||||
|
if (denominator2 === denominator1) {
|
||||||
|
denominator2 = ((denominator2 + 1 - 2) % 11) + 2 // rotate within 2-12
|
||||||
|
}
|
||||||
|
|
||||||
|
const numerator1 = pickNumerator(denominator1)
|
||||||
|
const numerator2 = pickNumerator(denominator2)
|
||||||
|
|
||||||
|
problems.push({
|
||||||
|
numerator1,
|
||||||
|
denominator1,
|
||||||
|
numerator2,
|
||||||
|
denominator2,
|
||||||
|
operator: 'fraction',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate random integer between min and max (inclusive)
|
* Generate random integer between min and max (inclusive)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export type WorksheetFormState = Partial<Omit<AdditionConfigV4Custom, 'version'>
|
||||||
/**
|
/**
|
||||||
* Worksheet operator type
|
* Worksheet operator type
|
||||||
*/
|
*/
|
||||||
export type WorksheetOperator = 'addition' | 'subtraction' | 'mixed'
|
export type WorksheetOperator = 'addition' | 'subtraction' | 'mixed' | 'fractions'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single addition problem
|
* A single addition problem
|
||||||
|
|
@ -136,10 +136,21 @@ export interface SubtractionProblem {
|
||||||
operator: 'sub'
|
operator: 'sub'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single fraction addition problem with different denominators
|
||||||
|
*/
|
||||||
|
export interface FractionProblem {
|
||||||
|
numerator1: number
|
||||||
|
denominator1: number
|
||||||
|
numerator2: number
|
||||||
|
denominator2: number
|
||||||
|
operator: 'fraction'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified problem type (addition or subtraction)
|
* Unified problem type (addition or subtraction)
|
||||||
*/
|
*/
|
||||||
export type WorksheetProblem = AdditionProblem | SubtractionProblem
|
export type WorksheetProblem = AdditionProblem | SubtractionProblem | FractionProblem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation result
|
* Validation result
|
||||||
|
|
|
||||||
|
|
@ -165,10 +165,10 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||||
errors.push('Digit range min cannot be greater than max')
|
errors.push('Digit range min cannot be greater than max')
|
||||||
}
|
}
|
||||||
|
|
||||||
// V4: Validate operator (addition, subtraction, or mixed)
|
// V4: Validate operator (addition, subtraction, mixed, or fractions)
|
||||||
const operator = formState.operator ?? 'addition'
|
const operator = formState.operator ?? 'addition'
|
||||||
if (!['addition', 'subtraction', 'mixed'].includes(operator)) {
|
if (!['addition', 'subtraction', 'mixed', 'fractions'].includes(operator)) {
|
||||||
errors.push('Operator must be "addition", "subtraction", or "mixed"')
|
errors.push('Operator must be "addition", "subtraction", "mixed", or "fractions"')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate seed (must be positive integer)
|
// Validate seed (must be positive integer)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue