function normalizeText(text) { if (!text) return ''; return text .toLowerCase() .replace(/[äÄ]/g, 'ae') .replace(/[öÖ]/g, 'oe') .replace(/[üÜ]/g, 'ue') .replace(/ß/g, 'ss') .replace(/[^a-z0-9]/g, '') .trim(); } function extractFieldValue(field, fallback) { if (!field) return fallback || ''; if (typeof field === 'string') return field; if (typeof field === 'object' && field.value !== undefined) return field.value; return String(field); } function getTyp(question) { return extractFieldValue(question['Typ'], 'MC').toLowerCase(); } function checkFreitext(userAnswer, correctAnswer) { return normalizeText(userAnswer) === normalizeText(correctAnswer); } /** * Resolve correct answer(s) to keys (A/B/C/D). * Handles both: * - "A" or "A,C" (key-based) * - "Falsch" or "Stuttgart" (text-based → resolved to matching key) */ function parseCorrectAnswers(question) { const raw = (question['Richtige Antwort'] || '').trim(); const typ = getTyp(question); if (typ === 'freitext') return [raw]; const parts = raw.split(',').map((s) => s.trim()).filter(Boolean); const validKeys = new Set(['A', 'B', 'C', 'D']); // Check if all parts are valid keys already const allAreKeys = parts.length > 0 && parts.every((p) => validKeys.has(p.toUpperCase())); if (allAreKeys) { return parts.map((p) => p.toUpperCase()); } // Otherwise: try to match each part against the answer texts const resolved = []; for (const part of parts) { const partLower = part.toLowerCase().trim(); const keys = ['A', 'B', 'C', 'D']; let found = false; for (const k of keys) { const answerText = (question[`Antwort ${k}`] || '').trim(); if (answerText && answerText.toLowerCase() === partLower) { resolved.push(k); found = true; break; } } // If it looks like a key anyway (single char), use it if (!found && part.length === 1 && validKeys.has(part.toUpperCase())) { resolved.push(part.toUpperCase()); } } if (resolved.length > 0) return resolved; // Fallback: return as-is uppercase return parts.map((p) => p.toUpperCase()); } function countOptions(question) { let n = 0; if (question['Antwort A']) n++; if (question['Antwort B']) n++; if (question['Antwort C']) n++; if (question['Antwort D']) n++; return n; } function shuffleArray(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function calculateScore(question, selected, timeRemaining, totalTime, scoringMode) { if (!selected || selected.length === 0) return { points: 0, correct: false }; let basis; if (scoringMode === 'time' && totalTime > 0) { const ratio = Math.max(0, Math.min(1, timeRemaining / totalTime)); basis = 500 + Math.round(ratio * 500); } else { basis = 1000; } const typ = getTyp(question); const correctAnswers = parseCorrectAnswers(question); if (typ === 'freitext') { const ok = checkFreitext(selected[0], correctAnswers[0]); return { points: ok ? basis : 0, correct: ok }; } const correctSet = new Set(correctAnswers); const totalCorrect = correctSet.size; const totalIncorrect = countOptions(question) - totalCorrect; let rightPicked = 0; let wrongPicked = 0; for (const s of selected) { if (correctSet.has(s.toUpperCase())) rightPicked++; else wrongPicked++; } let points = (rightPicked / Math.max(totalCorrect, 1)) * basis; if (totalIncorrect > 0) { points -= (wrongPicked / totalIncorrect) * (basis / 2); } points = Math.max(0, Math.round(points)); const fullyCorrect = rightPicked === totalCorrect && wrongPicked === 0; return { points, correct: fullyCorrect }; } function sanitizeQuestion(question) { const typ = getTyp(question); const sanitized = { id: question.id, frage: question['Frage'] || '', typ: extractFieldValue(question['Typ'], 'MC'), bild: null, antworten: [], kategorie: extractFieldValue(question['Kategorie'], ''), }; const bild = question['Bild']; if (bild && Array.isArray(bild) && bild.length > 0 && bild[0].url) { sanitized.bild = `/api/img?url=${encodeURIComponent(bild[0].url)}`; } if (typ !== 'freitext') { const keys = ['A', 'B', 'C', 'D']; for (const k of keys) { const val = question[`Antwort ${k}`]; if (val) sanitized.antworten.push({ key: k, text: val }); } sanitized.antworten = shuffleArray(sanitized.antworten); } return sanitized; } module.exports = { calculateScore, sanitizeQuestion, parseCorrectAnswers, normalizeText, shuffleArray, extractFieldValue };