UNPKG

@coorpacademy/progression-engine

Version:

151 lines (131 loc) 5.36 kB
import every from 'lodash/fp/every'; import filter from 'lodash/fp/filter'; import get from 'lodash/fp/get'; import includes from 'lodash/fp/includes'; import join from 'lodash/fp/join'; import map from 'lodash/fp/map'; import maxBy from 'lodash/fp/maxBy'; import pipe from 'lodash/fp/pipe'; import reverse from 'lodash/fp/reverse'; import some from 'lodash/fp/some'; import split from 'lodash/fp/split'; import toLower from 'lodash/fp/toLower'; import trim from 'lodash/fp/trim'; import zip from 'lodash/fp/zip'; import FuzzyMatching from 'fuzzy-matching'; const reverseString = pipe(split(''), reverse, join('')); function checkFuzzyAnswer(maxTypos, fm, userAnswer) { if (!userAnswer || userAnswer.length === 0) { return false; } // Find a valid answer resembling userAnswer return !!fm.get(userAnswer, { maxChanges: maxTypos }).value; } function containsAnswer(config, allowedAnswer, givenAnswer) { // Find the allowed answer in the given answer if (!includes(allowedAnswer, givenAnswer)) { // If not present return false; } // Get the non-space characters surrounding the answer and make sure that there are not too many. const limit = config.answerBoundaryLimit; const [first = '', second = ''] = givenAnswer.split(allowedAnswer); const indexOfSpaceInFirst = reverseString(first).indexOf(' '); const indexOfSpaceInSecond = second.indexOf(' '); return (first.length <= limit || indexOfSpaceInFirst !== -1 && indexOfSpaceInFirst <= limit) && (second.length <= limit || indexOfSpaceInSecond !== -1 && indexOfSpaceInSecond <= limit); } function isTextCorrect(config, allowedAnswers, answerWithCase, _maxTypos) { const fm = new FuzzyMatching(allowedAnswers); const maxTypos = _maxTypos === 0 ? _maxTypos : _maxTypos || config.maxTypos; const answer = toLower(answerWithCase); return checkFuzzyAnswer(maxTypos, fm, answer) || maxTypos !== 0 && some(allowedAnswer => containsAnswer(config, toLower(allowedAnswer), answer), allowedAnswers); } function matchAnswerForBasic(config, question, givenAnswer) { if (question.content.answers.length === 0) { return []; } const isCorrect = isTextCorrect(config, question.content.answers.map(answers => answers[0]), givenAnswer[0], question.content.maxTypos); return [[{ answer: givenAnswer[0], isCorrect }]]; } function matchAnswerForTemplate(config, question, givenAnswer) { if (question.content.answers.length === 0) { return []; } const result = givenAnswer.map((answer, index) => ({ answer, isCorrect: question.content.answers.some(allowedAnswer => isTextCorrect(config, [allowedAnswer[index]], toLower(answer), get(['content', 'choices', `${index}`, 'type'], question) === 'text' ? question.content.maxTypos : 0)) })); const missingAnswers = question.content.answers[0].slice(result.length).map(() => ({ answer: undefined, isCorrect: false })); return [result.concat(missingAnswers)]; } function matchAnswerForUnorderedItems(allowedAnswers, givenAnswer) { const lowerGivenAnswer = map(toLower, givenAnswer); return allowedAnswers.map(allowedAnswer => { const lowerAllowedAnswer = map(toLower, allowedAnswer); const givenAnswersMap = map(answer => ({ answer, isCorrect: includes(toLower(answer), lowerAllowedAnswer) }), givenAnswer); if (lowerAllowedAnswer.some(answer => !includes(answer, lowerGivenAnswer))) { return givenAnswersMap.concat([{ answer: undefined, isCorrect: false }]); } return givenAnswersMap; }); } function matchAnswerForOrderedItems(allowedAnswers, givenAnswer) { return map(allowedAnswer => { return map(([givenAnswerPart, allowedAnswerPart]) => { return { answer: givenAnswerPart, isCorrect: toLower(givenAnswerPart) === toLower(allowedAnswerPart) }; }, zip(givenAnswer, allowedAnswer)); }, allowedAnswers); } const findBestMatch = maxBy(correction => filter('isCorrect', correction).length); function matchGivenAnswerToQuestion(config, question, givenAnswer) { const allowedAnswers = question.content.answers; switch (question.type) { case 'basic': { return matchAnswerForBasic(config, question, givenAnswer); } case 'template': { return matchAnswerForTemplate(config, question, givenAnswer); } case 'qcm': { return matchAnswerForUnorderedItems(allowedAnswers, givenAnswer); } case 'qcmGraphic': { return matchAnswerForUnorderedItems(allowedAnswers, givenAnswer); } case 'qcmDrag': { return question.content.matchOrder ? matchAnswerForOrderedItems(allowedAnswers, givenAnswer) : matchAnswerForUnorderedItems(allowedAnswers, givenAnswer); } case 'slider': { return matchAnswerForOrderedItems(allowedAnswers, givenAnswer); } default: return [[]]; } } export default function checkAnswerCorrectness(config, question, givenAnswer) { const matches = matchGivenAnswerToQuestion(config, question, givenAnswer.map(trim)); if (matches.length === 0) { return { isCorrect: false, corrections: [] }; } const bestMatch = findBestMatch(matches); return { isCorrect: every('isCorrect', bestMatch), corrections: filter(item => item.answer !== undefined, bestMatch) }; } //# sourceMappingURL=check-answer-correctness.js.map