5.6. Test Generation

(* Module Generate defines the objects and operations to automatically generate a test in the Test Tool. *)
(* Begin David Myers' section *)
module Generate;
    from Question import QuestionDB, TestQuestion, Type, Time, Difficulty;
    from File import File, RequiresSaving;
    from Test import Test, TestName, TestTime;

  object GenerateOptions is
    components: class:Class and title:Title and duration:Duration and
        num_questions:NumberOfQuestions and adv:Advanced and
        diff:OpDifficulty and keywords:Keywords and ques_types:QuestionTypes;
    description: (*
        GenerateOptions supply the needed fields for automatically generating a test.
        The Class briefly describes what class the test is for.  The Title component 
        is a brief description of what the test is for.  The Duration indicates how long 
        the test will last.  The NumberOfQuestions indicates the desired number of questions 
        on the test.  The Advanced field is a boolean toggling the advanced features.  
        The Difficulty indicates the desired difficulty of the test.  The Keywords indicate 
        which keywords to look for in questions.  The QuestionTypes 
        specify the number of each question type on the test.
    *);
  end GenerateOptions;

  object Class is string;
  object Title is string;

  object Duration is 
    components: integer;
    description: (*
          Specified in number of minutes.
    *);
  end Duration;

  object NumberOfQuestions is integer;
  object Advanced is boolean;

  object Range is
    components: lower:Lower and upper:Upper;
    description: (*
          Used to represent the Difficulty Range of a test.  All questions on the test must lie within 
          the range specified in GenerateOptions.
    *);
  end Range;

  object Lower is
    components: integer;
    description: (*
          The lower limit of the Range object.
    *);
  end Lower;

  object Upper is
    components: integer;
    description: (*
          The upper limit of the Range object.
    *);
  end Upper;

  object OpDifficulty is Range
    description: (*
        Difficulty values range from 1 to 5.
    *);
  end Difficulty;

  object QuestionTypes is
    components: ques_remaining:QuestionsRemaining and total:TotalQuestions and 
        num_tf:NumberOfTF and num_mc:NumberOfMC and num_fitb:NumberOfFITB and 
        num_match:NumberOfMatching and num_code:NumberOfCoding and num_short:NumberOfShort;
    description: (*
        QuestionTypes holds the desired number of each type of question. 
        QuestionsRemaining indicates how many questions do not have a specified 
        type.  TotalQuestions contains the number of questions on the test.
        Each "NumberOf" field contains the desired number of each type of question.
    *);
  end QuestionTypes;

  object QuestionsRemaining is integer;
  object TotalQuestions is integer;
  object NumberOfTF is integer;
  object NumberOfMC is integer;
  object NumberOfFITB is integer;
  object NumberOfMatching is integer;
  object NumberOfCoding is integer;
  object NumberOfShort is integer;

  object Keywords is string;



  operation GenerateTest is
    inputs: qdb:QuestionDB, options:GenerateOptions;
    outputs: test:Test;
    description: (*
        GenerateTest generates a test with options specified under GenerateOptions. 
        Questions are chosen from the QuestionDB to be included in a Test.
        The generation may proceed if Class, Title, Duration, and NumberOfQuestions 
        are all not null.  The RequiresSaving flag of the test is set to true.
    *);
    precondition:
        (*
         * The Class and Title fields are not empty.
         *)
        (options.class != nil) 
            and 
        (options.title != nil)

            and

        (*
         * The duration and number of questions are non-negative.
         *)
        (options.duration > 0)
            and
        (options.num_questions > 0);

    postcondition:
      (*
       * The outputed test has the same title, class, and duration as specified
       * in GenerateOptions.
       *)
      (test.class = options.class)
          and
      (test.testname = options.title)
          and
      (test.testtime = options.duration)

          and

    (*
     *  The test must include not only the valid types of questions, but also the specified number of 
     *  each type.  This portion of the postcondition is not included due to time restrictions.
     *)

    (*
     *  The difficulty and duration for the test must be within the range specified in GenerateOptions. 
     *  Specifically, the sum of times for all the questions must be less than the time of the test, and 
     *  the min/max difficulty of questions must be within the difficulty range of the test.
     *)
    DurationInRange (test)
        and
    InRange (options.diff, MinDifficulty (test.testquestionlist))
        and
    InRange (options.diff, MaxDifficulty (test.testquestionlist))

        and

    (* 
     *  Every question included in the test must also be in the local question database.
     *)
    forall (question:TestQuestion)
        ((question in test.testquestionlist) and (question in qdb))

        and

    (*
     *  The questions added to the test are the "best", where best means it is the closest 
     *  match to the test Difficulty and Duration specified in GenerateOptions. 
     *  Therefore, there exists no other question in the database with a better match.
     *)
    BestFit(qdb, test, options)

            and

    (*
     * Set the requires_saving flag for the test to true.
     *)
    (test.requires_saving = true)
    ;
  end GenerateTest;



  (****************
   * Auxiliary functions.
   *)
  function DurationInRange(test:Test) -> (boolean) = (
    (*
     * The sum of the question's times must be less than or equal to the test time.
     *)
    TimeTotalOfQuestions(test.testquestionlist) <= test.testtime
    )
  end DurationInRange;

  function InRange (range:Range, i:integer) -> (boolean) = (
    (*
     * Function to check if a given integer is within the range criteria.  Used to check if a given question 
     * is within the valid difficulty range of a test.
     *)
        ((i >= range.lower) and (i <= range.upper))
    )
  end InRange;

  function AverageRange (range:Range) -> (integer) = (
    (*
     * Function to return the average of a range of integers.
     *)
        (range.lower + range.upper) / 2
    )
  end AverageRange;

  function TimeTotalOfQuestions (testquestionlist:TestQuestion*) -> (real) = (
    (*
     * Function to return the total time included in a list of questions.  Returns a real number.
     *)
        if (#testquestionlist = 0) 
        then 0
        else (testquestionlist[1].time + TimeTotalOfQuestions (testquestionlist - testquestionlist[1]))
    )
  end TimeTotalOfQuestions;

  function DifficultyTotal (testquestionlist:TestQuestion*) -> (integer) = (
    (*
     * Function to return the total difficulty of a list of questions.  Useful for calculating the mean
     * difficulty for the exam in conjuction with the operation AverageDifficulty.
     *)
        if (#testquestionlist = 0)
        then 0 
        else (testquestionlist[1].difficulty + DifficultyTotal (testquestionlist - testquestionlist[1]))
    )
  end DifficultyTotal;

  function MinDifficulty (testquestionlist:TestQuestion*) -> (integer) = (
    (*
     * Function to return the lowest difficulty of a question in a list of questions.
     *)
        if (#testquestionlist = 1)
        then testquestionlist[1].difficulty
        else Min (testquestionlist[1].difficulty, MinDifficulty (testquestionlist - testquestionlist[1]))
    )
  end MinDifficulty;

  function MaxDifficulty (testquestionlist:TestQuestion*) -> (integer) = (
    (*
     * Function to return the highest difficulty of a question in a list of questions.
     *)
        if (#testquestionlist = 1)
        then testquestionlist[1].difficulty
        else Max (testquestionlist[1].difficulty, MaxDifficulty (testquestionlist - testquestionlist[1]))
    )
  end MaxDifficulty;

  function AverageDifficulty (testquestionlist:TestQuestion*) -> (real) = (
    (*
     * Function to return the average difficulty of a list of questions.  Used in conjuction with 
     * DifficultyTotal.
     *)
        DifficultyTotal(testquestionlist) / #testquestionlist
    )
  end AverageDifficulty;

    (*
     *  Helper functions to calculate the min or max of an integer.
     *)
  function Min (x:integer, y:integer) -> (integer) = if (x < y) then x else y;
  function Max (x:integer, y:integer) -> (integer) = if (x > y) then x else y;

  function BestFit(qdb:QuestionDB, test:Test, options:GenerateOptions) -> (boolean) = (
    (*
     * There is no other question in the database of similar type that has a closer difficulty.
     *)
    forall (question:TestQuestion | question in test.testquestionlist)
        not (exists(question':TestQuestion | question' in qdb) (
            (question.type = question'.type)
                and
            (AverageRange(options.diff) - question'.difficulty) < (AverageRange(options.diff) - question.difficulty)
            ))
    )
  end BestFit;

(* End David Myers' section *)

end Generate;



Prev: test.rsl | Next: grading.rsl | Up: formal specification | Top: index