(* 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;