I have a Question
class:
class Question
{
int Id { get; set; };
string Text { get; set; }
QuestionDifficulty Difficulty { get; set; }
List<Answer> Answers { get; set; }
}
And an enumeration QuestionDifficulty
:
enum QuestionDifficulty { Easy, Medium, Hard, VeryHard }
I have to pick multiple question sets of the same size of each difficulty level from a container collection called Repository
for a test:
class Repository { List<Question> Questions { get; set; } /*other fields omitted for brevity*/ }
The number of questions to be picked for each difficulty level will be specified by a dictionary IDictionary<QuestionDifficulty, int>
. Since there are multiple ways it can be picked, I have written an interface IQuestionPicker
that defines this behavior. :
interface IQuestionPicker
{
public QResult Pick(Repository repo,
int setCount,
IDictionary<QuestionDifficulty, int> requiredQuestionCounts);
}
One of the implementations is called UniqueQuestionPicker
that picks a different collection of questions for each set. So no two sets will have any common questions. Hence, the Repository
container needs to have, for each difficulty level, at least requiredQuestionCounts[difficulty] * setCount
questions. So it performs the validation first. I wanted to move this validation to its own method HasEnoughQuestions()
returning a bool
. But I realized then I cannot include the data about which difficulty level has the problem in my exception.
class UniqueQuestionPicker : IQuestionPicker
{
public List<QuestionResult> Pick(Repository repo,
int setCount,
IDictionary<QuestionDifficulty, int> requiredQuestionCounts)
{
var questions = repo.Questions ??
throw new NullReferenceException($"{nameof(repo.Questions)} of argument {nameof(repo)} is null");
var groups = questions.GroupBy(question => question.Difficulty);
// Get actual number of questions for each difficulty level
Dictionary<QuestionDifficulty, int> actualQuestionCounts = groups
.ToDictionary(group => group.Key, group => group.Count());
// Validate if each difficulty level has enough questions
foreach (var difficulty in actualQuestionCounts.Keys)
{
int requiredQuestions = requiredQuestionCounts[difficulty] * setCount;
int actualQuestions = actualQuestionCounts[difficulty];
if (actualQuestions < requiredQuestions)
{
throw new InvalidOperationException(
$"Not enough questions of difficulty \"{difficulty}\"")
{
Data =
{
["RequiredQs"] = requiredQuestions,
["ActualQs"] = actualQuestions
}
};
}
Random random = new();
List<QuestionResult> results = new();
foreach(var group in groups)
{
var difficulty = group.Key;
QuestionResult result =
new(difficulty, setCount, questionsPerSet: requiredQuestionCounts[difficulty]);
var shuffledQuestions = group.OrderBy(q => random.Next());
for(int i = 0; !result.Full; i++)
{
result.Add(shuffledQuestions.ElementAt(i));
}
results.Add(result);
}
return results;
}
}
}
I'm a bit unsure about how my result data structure QResult
should look like. It should contain the specified number of question sets, each set containing the specified number of easy, medium, hard and very hard questions. I had first planned to use IDictionary<QuestionDifficulty, List<HashSet<Question>>>
, or IList<IDictionary<QuestionDifficulty, HashSet<Question>>>
but they both look a bit clumsy. I want to also allow combining two QResult
s picked from different repositories to be merged to a new QResult
.
I plan to include more picker implementations like ShuffledQuestionPicker
which would include the same questions in each set but they will be shuffled.
This is what the current QResult
I've written called QuestionResult
looks like. Once the SetCount
and QuestionsPerSet
are passed through its constructor, the Add()
method automatically creates a new HashSet<Question>
and advances an internal pointer called currentSet
when the QuestionSets[currentSet].Count
becomes equal to QuestionsPerSet
. I have used a HashSet
since I have to check before adding the question, that none of the previous sets contain it already and this makes it O(1). A QuestionResult
object will exist for each difficulty level, hence my picker returns List<QuestionResult>
.
public class QuestionResult
{
public IEnumerable<IEnumerable<Question>> QuestionSets { get => questionSets; }
public int QuestionsPerSet { get; }
public int SetCount { get; }
public bool Full { get; private set; }
private readonly List<HashSet<Question>> questionSets = new();
private int currentSet = 0;
public QuestionDifficulty Difficulty { get; }
public QuestionResult(QuestionDifficulty difficulty, int setCount, int questionsPerSet)
{
Difficulty = difficulty;
QuestionsPerSet = questionsPerSet;
SetCount = setCount;
}
public void Add(Question question)
{
if (questionSets[currentSet].Count == QuestionsPerSet)
{
questionSets.Add(new HashSet<Question>());
currentSet++;
}
if (questionSets.Any(set => set.Contains(question)))
{
throw new InvalidOperationException("Duplicate addition attempted");
}
if(questionSets.Count == SetCount && questionSets[currentSet].Count == QuestionsPerSet)
{
Full = true;
}
}
public bool TryAdd(Question question)
{
try
{
questionSets[currentSet].Add(question);
return true;
}
catch(InvalidOperationException)
{
return false;
}
}
}
Something about this approach does not feel right, it has gotten quite complex. Can someone recommend a better way to do this, a better data structure and design pattern?
Aucun commentaire:
Enregistrer un commentaire