155 lines
6.2 KiB
TypeScript
155 lines
6.2 KiB
TypeScript
import { Exercise, FillBlanksExercise, FillBlanksMCOption, MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap, Shuffles, UserSolution } from "@/interfaces/exam";
|
|
|
|
export default function shuffleExamExercise(
|
|
shuffle: boolean | undefined,
|
|
exercise: Exercise,
|
|
showSolutions: boolean,
|
|
userSolutions: UserSolution[],
|
|
shuffles: Shuffles[],
|
|
setShuffles: (maps: Shuffles[]) => void
|
|
): Exercise {
|
|
if (!shuffle) {
|
|
return exercise;
|
|
}
|
|
const userSolution = userSolutions.find((x) => x.exercise === exercise.id)!;
|
|
|
|
if (exercise.type === "multipleChoice") {
|
|
return shuffleMultipleChoice(exercise, userSolution, shuffles, setShuffles, showSolutions);
|
|
} else if (exercise.type === "fillBlanks") {
|
|
return shuffleFillBlanks(exercise, userSolution, shuffles, setShuffles, showSolutions);
|
|
}
|
|
|
|
return exercise;
|
|
}
|
|
|
|
function shuffleMultipleChoice(
|
|
exercise: MultipleChoiceExercise,
|
|
userSolution: UserSolution,
|
|
shuffles: Shuffles[],
|
|
setShuffles: (maps: Shuffles[]) => void,
|
|
showSolutions: boolean,
|
|
): MultipleChoiceExercise {
|
|
|
|
if (typeof userSolution.shuffleMaps === "undefined" || (userSolution.shuffleMaps && userSolution.shuffleMaps.length === 0) && !showSolutions) {
|
|
const newShuffleMaps: ShuffleMap[] = [];
|
|
exercise.questions = exercise.questions.map(shuffleQuestion(newShuffleMaps));
|
|
userSolution!.shuffleMaps = newShuffleMaps;
|
|
setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), {exerciseID: exercise.id, shuffles: newShuffleMaps}]);
|
|
} else {
|
|
exercise.questions = exercise.questions.map(retrieveShuffledQuestion(userSolution.shuffleMaps));
|
|
}
|
|
|
|
return exercise;
|
|
}
|
|
|
|
function shuffleQuestion(newShuffleMaps: ShuffleMap[]) {
|
|
return (question: MultipleChoiceQuestion): MultipleChoiceQuestion => {
|
|
const options = [...question.options];
|
|
const shuffledOptions = fisherYatesShuffle(options);
|
|
|
|
const optionMapping: Record<string, string> = {};
|
|
const newOptions = shuffledOptions.map((option, index) => {
|
|
const newId = String.fromCharCode(65 + index);
|
|
optionMapping[option.id] = newId;
|
|
return { ...option, id: newId };
|
|
});
|
|
|
|
newShuffleMaps.push({ questionID: question.id, map: optionMapping });
|
|
|
|
return { ...question, options: newOptions, shuffleMap: optionMapping };
|
|
};
|
|
}
|
|
|
|
function retrieveShuffledQuestion(shuffleMaps: ShuffleMap[]) {
|
|
return (question: MultipleChoiceQuestion): MultipleChoiceQuestion => {
|
|
const questionShuffleMap = shuffleMaps.find(map => map.questionID === question.id);
|
|
if (questionShuffleMap) {
|
|
const shuffledOptions = Object.entries(questionShuffleMap.map)
|
|
.sort(([, a], [, b]) => a.localeCompare(b))
|
|
.map(([originalId, newId]) => {
|
|
const originalOption = question.options.find(opt => opt.id === originalId);
|
|
return { ...originalOption, id: newId };
|
|
});
|
|
|
|
return { ...question, options: shuffledOptions, shuffleMap: questionShuffleMap.map };
|
|
}
|
|
return question;
|
|
};
|
|
}
|
|
function shuffleFillBlanks(
|
|
exercise: FillBlanksExercise,
|
|
userSolution: UserSolution,
|
|
shuffles: Shuffles[],
|
|
setShuffles: (maps: Shuffles[]) => void,
|
|
showSolutions: boolean
|
|
): FillBlanksExercise {
|
|
if (typeof userSolution.shuffleMaps === "undefined" || (userSolution.shuffleMaps && userSolution.shuffleMaps.length === 0) && !showSolutions) {
|
|
const newShuffleMaps: ShuffleMap[] = [];
|
|
exercise.words = exercise.words.map(shuffleWord(newShuffleMaps));
|
|
userSolution.shuffleMaps = newShuffleMaps;
|
|
setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), {exerciseID: exercise.id, shuffles: newShuffleMaps}]);
|
|
} else {
|
|
exercise.words = exercise.words.map(retrieveShuffledWord(userSolution.shuffleMaps!));
|
|
}
|
|
|
|
return exercise;
|
|
}
|
|
|
|
function shuffleWord(newShuffleMaps: ShuffleMap[]) {
|
|
return (word: string | { letter: string; word: string } | FillBlanksMCOption): typeof word => {
|
|
if (typeof word === 'object' && 'options' in word) {
|
|
const options = word.options;
|
|
const originalKeys = Object.keys(options);
|
|
const shuffledKeys = fisherYatesShuffle(originalKeys);
|
|
|
|
const newOptions = shuffledKeys.reduce<typeof options>((acc, key, index) => {
|
|
acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
|
|
return acc;
|
|
}, {} as typeof options);
|
|
|
|
const optionMapping = originalKeys.reduce<Record<string, string>>((acc, key, index) => {
|
|
acc[key] = shuffledKeys[index];
|
|
return acc;
|
|
}, {});
|
|
|
|
newShuffleMaps.push({ questionID: word.id, map: optionMapping });
|
|
|
|
return { ...word, options: newOptions };
|
|
}
|
|
return word;
|
|
};
|
|
}
|
|
|
|
function retrieveShuffledWord(shuffleMaps: ShuffleMap[]) {
|
|
return (word: string | { letter: string; word: string } | FillBlanksMCOption): typeof word => {
|
|
if (typeof word === 'object' && 'options' in word) {
|
|
const shuffleMap = shuffleMaps.find(map => map.questionID === word.id);
|
|
if (shuffleMap) {
|
|
const options = word.options;
|
|
const shuffledOptions = Object.keys(options).reduce<typeof options>((acc, key) => {
|
|
const shuffledKey = shuffleMap.map[key as keyof typeof options];
|
|
acc[shuffledKey as keyof typeof options] = options[key as keyof typeof options];
|
|
return acc;
|
|
}, {} as typeof options);
|
|
|
|
return { ...word, options: shuffledOptions };
|
|
}
|
|
}
|
|
return word;
|
|
};
|
|
}
|
|
|
|
function fisherYatesShuffle<T>(array: T[]): T[] {
|
|
const shuffled = [...array];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
return shuffled;
|
|
}
|
|
|
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
|
return Array.isArray(words) && words.every(
|
|
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
|
);
|
|
} |