Go back to Richel Bilderbeek's homepage.

Go back to Richel Bilderbeek's C++ page.

 

 

 

 

 

(C++) MultipleChoiceQuestion

 

STLQt CreatorLubuntu

 

MultipleChoiceQuestion is a class for a multiple choice question.

Technical facts

 

 

 

 

 

 

./CppMultipleChoiceQuestion/CppMultipleChoiceQuestion.pri

 

INCLUDEPATH += \
    ../../Classes/CppMultipleChoiceQuestion

SOURCES += \
    ../../Classes/CppMultipleChoiceQuestion/multiplechoicequestion.cpp

HEADERS  += \
    ../../Classes/CppMultipleChoiceQuestion/multiplechoicequestion.h

OTHER_FILES += \
    ../../Classes/CppMultipleChoiceQuestion/Licence.txt

 

 

 

 

 

./CppMultipleChoiceQuestion/multiplechoicequestion.h

 

//---------------------------------------------------------------------------
/*
MultipleChoiceQuestion, class for a multiple choice question
Copyright (C) 2011-2015 Richel Bilderbeek

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
//---------------------------------------------------------------------------
// From http://www.richelbilderbeek.nl/CppMultipleChoiceQuestion
//---------------------------------------------------------------------------
#ifndef MULTIPLECHOICEQUESTION_H
#define MULTIPLECHOICEQUESTION_H

#include <string>
#include <vector>

#include "question.h"

namespace ribi {

///Class for a multiple choice question
struct MultipleChoiceQuestion : public Question
{
  ///Throws nothing or std::out_of_range or std::runtime_error
  explicit MultipleChoiceQuestion(const std::string& question);

  explicit MultipleChoiceQuestion(
    const std::string& filename,
    const std::string& question,
    const std::string& answer,
    const std::vector<std::string>& wrong_answers);

  ///Create a copy of the Question, depending on the derived class its type
  Question * Clone() const noexcept;

  ///Obtain the only correct answer
  const std::string& GetAnswer() const noexcept;

  ///Obtain an example multiple choice question
  static std::string GetExampleMultipleChoiceQuestion() noexcept { return "-,1+1=,2,0,4"; }

  ///Obtain valid multiple choice question
  static std::vector<std::string> GetInvalidMultipleChoiceQuestions() noexcept;

  ///Obtain the possible options to be chosen in a random order
  const std::vector<std::string>& GetOptions() const noexcept;

  ///Obtain valid multiple choice question
  static std::vector<std::string> GetValidMultipleChoiceQuestions() noexcept;

  ///Obtain the version
  static std::string GetVersion() noexcept;

  ///Obtain the version history
  static std::vector<std::string> GetVersionHistory() noexcept;

  ///Obtain the wrong answers
  const std::vector<std::string>& GetWrongAnswers() const noexcept { return m_wrong_answers; }

  ///How to display the question as multiple lines
  std::vector<std::string> ToLines() const noexcept;

  ///Convert to std::string line
  std::string ToStr() const noexcept;

  private:
  friend void boost::checked_delete<>(MultipleChoiceQuestion *);
  ~MultipleChoiceQuestion() noexcept {}

  ///All the wrong answers
  const std::vector<std::string> m_wrong_answers;

  ///All the options the user can choose from, which equals
  ///all wrong answers and the correct answer, shuffled randomly
  const std::vector<std::string> m_options;

  ///Create the possible options to be chosen in a random order
  static std::vector<std::string> CreateOptions(
    const std::vector<std::string>& wrong_answers,
    const std::string& answer) noexcept;

  ///The options (correct + wrong answers) are at indices 2 to SeperateString(input,',').size()
  static std::vector<std::string> ExtractOptions(
    const std::string& input);

  ///The wrong answers are at indices 3 to SeperateString(input,',').size()
  static std::vector<std::string> ExtractWrongAnswers(
    const std::string& input);

  #ifndef NDEBUG
  static void Test() noexcept;
  #endif

};

} //~namespace ribi

#endif // MULTIPLECHOICEQUESTION_H

 

 

 

 

 

./CppMultipleChoiceQuestion/multiplechoicequestion.cpp

 

//---------------------------------------------------------------------------
/*
MultipleChoiceQuestion, class for a multiple choice question
Copyright (C) 2011-2015 Richel Bilderbeek

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
//---------------------------------------------------------------------------
// From http://www.richelbilderbeek.nl/CppMultipleChoiceQuestion
//---------------------------------------------------------------------------
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Weffc++"
#pragma GCC diagnostic ignored "-Wunused-local-typedefs"
#include "multiplechoicequestion.h"

#include <algorithm>
#include <cassert>
#include <sstream>
#include <boost/algorithm/string/split.hpp>
#include <boost/scoped_ptr.hpp>

#include "container.h"
#include "fileio.h"
#include "imagecanvas.h"
#include "testtimer.h"
#include "trace.h"
#pragma GCC diagnostic pop

ribi::MultipleChoiceQuestion::MultipleChoiceQuestion(const std::string& question)
  : Question(
      Container().SeperateString(question,',').at(0),
      Container().SeperateString(question,',').at(1),
      { Container().SeperateString(question,',').at(2) }),
    m_wrong_answers(ExtractWrongAnswers(question)),
    m_options(ExtractOptions(question))
{
  #ifndef NDEBUG
  Test();
  #endif
  if (question.empty())
  {
    throw std::logic_error("A multiple choice question must contain text");
  }
  if (question[0] == ',')
  {
    throw std::logic_error("A multiple choice question must not start with a comma");
  }
  if (question[question.size() - 1] == ',')
  {
    throw std::logic_error("A multiple choice question must not end with a comma");
  }
  if (question.find(",,") != std::string::npos)
  {
    throw std::logic_error("A multiple choice question cannot contain two consecutive commas");
  }
}

ribi::MultipleChoiceQuestion::MultipleChoiceQuestion(
  const std::string& filename,
  const std::string& question,
  const std::string& answer,
  const std::vector<std::string>& wrong_answers)
: Question(filename,question, {answer} ),
  m_wrong_answers(wrong_answers),
  m_options(CreateOptions(wrong_answers,answer))
{
  #ifndef NDEBUG
  Test();
  #endif
  //assert(!filename.empty() && "Filename must not be empty");
  //assert(FileExists(filename) == true && "File must exists");
  //assert(!question.empty() && "MultipleChoiceQuestion must not be empty");
  //assert(!correct_answer.empty() && "Correct answer must not be empty");
  if (this->GetWrongAnswers().empty())
  {
    throw std::logic_error("a MultipleChoiceQuestion must have at least one incorrect option");
  }

  assert(!GetCorrectAnswers().empty() && "There must be a correct answer");
  assert(!GetWrongAnswers().empty() && "Wrong answers must not be empty");

  //Why did I put this max here? I think this is something that might be constrained by a GUI only
  //assert(wrong_answers.size() <= 5 && "There is a maximum of five wrong answers");
}

ribi::Question * ribi::MultipleChoiceQuestion::Clone() const noexcept
{
  return new MultipleChoiceQuestion(
    this->GetFilename(),
    this->GetQuestion(),
    this->GetAnswer(),
    this->GetWrongAnswers());
}

const std::string& ribi::MultipleChoiceQuestion::GetAnswer() const noexcept
{
  assert(!GetCorrectAnswers().empty());
  return GetCorrectAnswers()[0];
}

std::vector<std::string> ribi::MultipleChoiceQuestion::CreateOptions(
  const std::vector<std::string>& wrong_answers,
  const std::string& answer) noexcept
{
  std::vector<std::string> v = wrong_answers;
  v.push_back(answer);
  std::random_shuffle(std::begin(v),std::end(v));
  return v;
}

std::vector<std::string> ribi::MultipleChoiceQuestion::ExtractOptions(const std::string& input)
{
  const auto v = Container().SeperateString(input,',');
  std::vector<std::string> w;
  std::copy(v.begin() + 2,v.end(),std::back_inserter(w));
  std::random_shuffle(w.begin(),w.end());
  return w;
}

std::vector<std::string> ribi::MultipleChoiceQuestion::ExtractWrongAnswers(const std::string& input)
{
  const auto v = Container().SeperateString(input,',');
  if (v.size() < 4)
  {
    throw std::logic_error(
      "A multiple choice question must contain at least four elements:"
      "[0] image,"
      "[1] question,"
      "[2] answer,"
      "[3-x] incorrect answers");
  }
  std::vector<std::string> w;
  std::copy(v.begin() + 3,v.end(),std::back_inserter(w));
  return w;
}

std::vector<std::string> ribi::MultipleChoiceQuestion::GetInvalidMultipleChoiceQuestions() noexcept
{
  return {
    "-,1+1=,2", //No incorrect options
    "-,1+1=",   //No answer
    "-",        //No question
    "tmp.png",  //No question
    "",         //Nothing
    ",tmp.png,1+1=,2,1", //Start with comma
    "tmp.png,,1+1=,2,1", //Two consecutive comma's
    "tmp.png,1+1=,,2,1", //Two consecutive comma's
    "tmp.png,1+1=,2,,1", //Two consecutive comma's
    "tmp.png,1+1=,2,1,", //End with comma
    ",tmp.png,1+1=,2,1,", //Start and end with comma
    ",,tmp.png,1+1=,2,1,",
    ",tmp.png,,1+1=,2,1,",
    ",tmp.png,1+1=,,2,1,",
    ",tmp.png,1+1=,2,,1,",
    ",tmp.png,1+1=,2,1,,",
    ",",
    ",,",
    ",,,",
    ",,,,",
    ",,,,,",
    ",,,,,,"
  };
}

const std::vector<std::string>& ribi::MultipleChoiceQuestion::GetOptions() const noexcept
{
  return m_options;
}

std::vector<std::string> ribi::MultipleChoiceQuestion::GetValidMultipleChoiceQuestions() noexcept
{
  return {
    "-,1+1=,2,1",
    "tmp.png,1+1=,2,1",
    "-,1+1=,2,1,0,3,4,5,6,7,8,9,0"
  };
}

std::string ribi::MultipleChoiceQuestion::GetVersion() noexcept
{
  return "1.2";
}

std::vector<std::string> ribi::MultipleChoiceQuestion::GetVersionHistory() noexcept
{
  return {
    "2011-06-27: version 1.0: initial version",
    "2011-09-16: version 1.1: allow parsing from std::string"
    "2013-09-26: version 1.2: added testing"
  };
}

#ifndef NDEBUG
void ribi::MultipleChoiceQuestion::Test() noexcept
{
  {
    static bool is_tested{false};
    if (is_tested) return;
    is_tested = true;
  }
  {
    Container();
    ribi::fileio::FileIo();
    ImageCanvas::Test();
  }
  const TestTimer test_timer(__func__,__FILE__,1.0);
  try
  {
    const boost::scoped_ptr<MultipleChoiceQuestion> q {
      new MultipleChoiceQuestion(
        MultipleChoiceQuestion::GetExampleMultipleChoiceQuestion()
      )
    };
    assert(q);
  }
  catch (std::exception& e)
  {
    assert("MultipleChoiceQuestion::GetExampleMultipleChoiceQuestion()"
        && "must yield a valid MultipleChoiceQuestion");
  }
  //Test valid multiple choice questions for validity
  {
    const std::vector<std::string> valid { GetValidMultipleChoiceQuestions() };
    for (const std::string& s: valid)
    {
      try
      {
        boost::scoped_ptr<MultipleChoiceQuestion> q { new MultipleChoiceQuestion(s) };
        assert(q); //To make the compiler happy
        //OK
      }
      catch (std::exception& e)
      {
        TRACE("ERROR");
        TRACE(s);
        assert(!"Valid questions must be accepted");
      }
    }
  }
  //Test invalid multiple choice questions for invalidity
  {
    const std::vector<std::string> invalid { GetInvalidMultipleChoiceQuestions()  };
    for (const std::string& s: invalid)
    {
      try
      {
        const boost::scoped_ptr<MultipleChoiceQuestion> q { new MultipleChoiceQuestion(s) };
        TRACE("ERROR");
        TRACE(s);
        assert(!"Invalid questions must be rejected");
      }
      catch (std::exception& e)
      {
        //OK
      }
    }
  }
  //Test simple get/set
  {
    const std::string filename = "-";
    const std::string question = "1+1=";
    const std::string answer = "2";
    const std::vector<std::string> wrong_answers { "chicken", "cow" };
    const boost::scoped_ptr<MultipleChoiceQuestion> q { new MultipleChoiceQuestion(filename,question,answer,wrong_answers) };
    assert(q->GetFilename() == filename);
    assert(q->GetQuestion() == question);
    assert(q->GetAnswer() == answer);
    assert(q->GetWrongAnswers() == wrong_answers);
    assert(q->GetOptions().size() == wrong_answers.size() + 1); //The options are the incorrect answers and the correct answer(s)
    assert(q->IsCorrect(answer));
    assert(!q->IsCorrect(wrong_answers.at(0)));
    assert(!q->IsCorrect(wrong_answers.at(1)));
  }
  //Test conversion std::string to MultipleChoiceQuestion and back
  {
    const std::vector<std::string> valid { GetValidMultipleChoiceQuestions() };
    for (const std::string& s: valid)
    {
      const boost::scoped_ptr<MultipleChoiceQuestion> q { new MultipleChoiceQuestion(s) };
      assert(s == q->ToStr());
    }
  }
  //Test that ToLines always yields the same result
  {
    const std::vector<std::string> valid { GetValidMultipleChoiceQuestions() };
    for (const std::string& s: valid)
    {
      const boost::scoped_ptr<MultipleChoiceQuestion> q { new MultipleChoiceQuestion(s) };
      const std::vector<std::string> v { q->ToLines() };
      const std::vector<std::string> w { q->ToLines() };
      assert(v == w);
    }
  }
}
#endif

std::vector<std::string> ribi::MultipleChoiceQuestion::ToLines() const noexcept
{
  std::vector<std::string> question_lines;
  question_lines.push_back(this->GetQuestion());
  question_lines.push_back("");
  const std::vector<std::string> w { this->GetOptions() };

  int i=0;
  for (const std::string& s: w)
  {
    std::stringstream t;
    t << '[' << i << "] " << s;
    question_lines.push_back(t.str());
    ++i;
  }

  const int screen_rows { 23 };
  const int question_rows { static_cast<int>(question_lines.size()) };
  const int n_rows { screen_rows - question_rows };

  std::vector<std::string> lines;

  if (!GetFilename().empty())
  {
    int n_cols = 78;

    while (1)
    {
      const boost::shared_ptr<ImageCanvas> canvas {
        new ImageCanvas(GetFilename(),n_cols)
      };
      if (canvas->GetHeight() > n_rows)
      {
        --n_cols;
      }
      else
      {
        lines = canvas->ToStrings();
        break;
      }
      if (n_cols == 5) break;
    }
  }
  std::copy(question_lines.begin(),question_lines.end(),std::back_inserter(lines));
  return lines;
}

std::string ribi::MultipleChoiceQuestion::ToStr() const noexcept
{
  //Concatenate the correct answer
  assert(!this->GetCorrectAnswers().empty());
  std::string correct_answers_str;
  for (const std::string s: this->GetCorrectAnswers()) { correct_answers_str += s + "/"; }
  assert(!correct_answers_str.empty());
  correct_answers_str.resize(correct_answers_str.size() - 1);

  std::string s
    = this->GetFilename()
    + "," + this->GetQuestion()
    + "," + correct_answers_str
    + ",";
  for (const std::string t: this->GetWrongAnswers()) { s += t + ","; }
  //Remove tailing comma
  assert(!s.empty());
  s.resize(s.size() - 1);
  return s;
}

 

 

 

 

 

Go back to Richel Bilderbeek's C++ page.

Go back to Richel Bilderbeek's homepage.

 

Valid XHTML 1.0 Strict

This page has been created by the tool CodeToHtml