(* Module Generate defines the objects and operations related to generating tests. *) (* module Generate; from CommonObjects import Name, Date, Time, Difficulty, Type, QuestionItem, Points; *) object Test components: name:Name and date:Date and testcriteria:TestCriteria and question_list:QuestionItem* and filename:FileName; description: (* Test object. *); end Test; object MixQuestionDistribution components: num_tf:integer and num_mc:integer and num_short:integer and num_long:integer and num_code:integer and num_tc_pts:number and num_mc_pts:number and num_short_pts:number and num_long_pts:number and num_code_pts:number; end MixQuestionDistribution; object TestCriteria components: num_questions:integer and num_points:integer and time:Time and diff:Difficulty and test_type:TQuestionType and qdistribution:MixQuestionDistribution and qdatabase_list:TSelectedQDB*; description: (* Contains details about a test. *); end TestCriteria; object TQuestionType has Type or TQTypeMixed; object TQTypeMixed; object TSelectedQDB components: qdatabase:QDatabase and include_references:TSDBIncludeReferences* and num_questions:integer; description: (* TSelectedQDB contains a question database, a filenmae pointing to the database, the number of question to be select from the database, and the chapters that the system will select question from the database. *); end TSelectedQDB; object TSQDBIncludeRef components: reference:string; description: (* *); end TSDBIncludeChapters; object TestQuestionItem inherits from QuestionItem components: qnumber:integer and qpoints:number; description: (* TestQuestionItem specializes QuestionItem by adding a question number component. ---More info might be added later--- *); end TestQuestionItem; function QMatchTypeNDiff(type:TQuestionType, diff:Difficulty, qi:QuestionItem)->boolean = ( ( if( type != TQTypeMixed ) then (qi.type = type) ) and (qi.diff = diff) ); function IsInSomeQDatabase(tsdblist:TSelectedQDB*, qi:QuestionItem)->boolean = ( exists (tsqdb in tsdblist) (exists(qi' in tsqdb.qdatabase.question_list) (qi' = qi)) ); function MatchQuestionListSingleQDB inputs: type :TQuestionType, diff :Difficulty, tsqdb :TSelectedQDB; outputs: qilist' :QuestionItem*; precondition: ; postcondition: forall (qi in tqilist') ( (qi in tsqdb.qdatabase.question_list) and QMatchTypeNDiff(type, diff, qi) and if( #(tsqdb.include_references) != 0 ) then qi.reference in tsqdb.include_references ) ; end MatchQuestionListSingleQDB; object QuestionItems has QuestionItem*; function MatchQuestionListList inputs: type :TQuestionType, diff :Difficulty, tsqdblist :TSelectedQDB*; outputs: qilistlist :QuestionItems*; postcondition: #tsqdblist = #qilistlist and (forall (i:integer | (i >= 1) and (i <= #qilistlist)) (qilistlist[i] = MatchQuestionListSingleQDB(type, diff, tsqdblist[i]))) ; end MatchQuestionListList; function SelectQuestions inputs: qilist :QuestionItem*; outputs: qilist' :QuestionItem*; postcondition: #qilist' <= #qilist and (forall (i:integer | (i >= 1) and (i <= #qilist')) ( qilist'[i] = qilist[i] )) ; end SelectQuestions; function SelectedQuestionListList inputs: qilistlist :QuestionItems*; outputs: selqilistlist :QuestionItems*; postcondition: #qilistlist = #selqilistlist and (forall (i:integer | (i >= 1) and (i <= #selqilistlist)) (selqilistlist[i] = SelectQuestions(qilistlist[i]))) ; end SelectedQuestionListList; function Int2Num(i:integer)->number = ( i * 1.0 ); function Ratio(num:integer, dem:integer)->number = ( (Int2Num(num) / Int2Num(dem)) ); function Decimal(num:number)->number = ( if( num >= 1.0 ) then Decimal(num - 1.0) else num ); function Round(num:integer, dem:integer)->integer = ( (* positive numbers only, negative?, maybe next version *) if( Decimal(Ratio(num, dem)) >= 0.5 ) then (num / dem) + 1 else (num / dem) ); function NewTSQDBList inputs: tsqdblist:TSelectedQDB*, num_points:integer, tot_points:integer; outputs: tsqdblist':TSelectedQDB*; description: (* This function creates a new selected question database for each of the old ones. All fields are same except the num_questions field. The new number of questions to select from the question database is the ratio of (number of points for that type and the total number of questions for the test) times the old number of questions for the question database. This works if SumQuestionDistribution() = total number questions for the test. If SumQuestionDistribution() is less than the total, meaning the user wants n number of questions and m* number for each question type, the rest dont care, this stills works since the user doesn't care about the type of the extra questions selected. SumQuestionDistribution() never greater than the total. *); postcondition: #tsqdblist' <= #tsqdblist and (forall (i:integer | (i >= 1) and (i <= #tsqdblist')) ( tsqdblist'[i].qdatabase = tsqdblist[i].qdatabase and tsqdblist'[i].include_references = tsqdblist[i].include_references and tsqdblist'[i].num_questions = Round((tsqdblist[i].num_questions * num_points), tot_points) )) ; end NewTSQDBList; function ValidTimeRange(mins:integer, expectedmins:integer, acceptable_err:integer)->boolean = ( (mins >= (expectedmins - acceptable_err)) and (mins <= (expectedmins + acceptable_err)) ); function GenerateQuestionList inputs: type :TQuestionType, diff :Difficulty, tsqdblist :TSelectedQDB*, time :Time, points :integer; outputs: qilist' :QuestionItem*; description: (* Selects questions from the give selected question databases that matches the type, diff, tsqdb[i].reference. The type can not be mix. The SumTimeMins() must be within +- 5 mins of time. *); precondition: type != TQTypeMixed and type != nil and ValidTimeRange(SumTimeMins(qilist'), Time2Mins(time), 5) and ... ; end GenerateQuestionList; function NoDuplicatesInList(qilist:QuestionItem*)->boolean = ( ); function IsInSomeMatchList(tc:TestCriteria, qi:QuestionItem)->boolean = ( exists (tsqdb in tc.qdatabase_list) qi in MatchQuestionList(tc, tsqdb); ); function IsInSomeSelectedQuestionList(tc:TestCriteria, qi:QuestionItem)->boolean = ( exists (tsqdb in tc.qdatabase_list) qi in SelectedQuestionListSingleQDB(tc, tsqdb) ); function GenerateSimpleQuestionList inputs: tc :TestCriteria; outputs: qilist :QuestionItem*; description: (* have to use this function to compare questions from question databases because of the question number and question points fields in the test question item object. this list is then converted to the test question item object list. *); postcondition: ( forall (qi in qilist) IsInSomeSelectedQuestionList(tc, qi) ) and NoDuplicatesInList(qilist) ; end GenerateSimpleQuestionList; function ConvertDatabaseQuestionListToTestQuestionList inputs: qilist:QuestionItem*; outputs: tqilist:TestQuestionItem*; postcondition: #qilist = #tqilist and (forall (i:integer | (i >= 1) and (i <= #qilist)) ( tqilist[i].qnumber = i and tqilist[i].questiontext = qilist[i].questiontext and tqilist[i].answer = qilist[i].answer and tqilist[i].type = qilist[i].type and tqilist[i].diff = qilist[i].diff and tqilist[i].time = qilist[i].time )) ; end ConvertDatabaseQuestionListToTestQuestionList; function Time2Mins(time:Time)->integer = ( (time.hours * 60) + time.minutes ); function Mins2Time(minutes:integer) -> (time:Time) = ( (time.hours = minutes / 60) and (time.minutes = minutes mod 60) ); function SumTimeMins(tqilist:TestQuestionItem*) = if (#tqilist = 0) then 0 else Time2Mins(tqilist[1].time) + SumTimeMins(tqilist[2:#tqilist]); function SumPoints(tqilist:TestQuestionItem*) = if (#tqilist = 0) then 0 else tqilist[1].qpoints + SumPoints(tqilist[2:#tqilist]); function TotalQuestions(tqilistlist:TestQuestionItems*) = if (#tqilistlist = 0) then 0 else #(tqilistlist[1]) + TotalQuestions(tqilistlist[2:#tqilistlist]); function GenerateSimpleTest inputs: tc:TestCriteria; outputs: tst':Test; description: (* Generates a simple test. Type != Mix *); postcondition: tst'.question_list = ConvertDatabaseQuestionListToTestQuestionList( GenerateSimpleQuestionList(tc) ) and ( (forall (tqi in tst'.question_list) tqi.points = tst'.testcriteria.num_points / #tst'.question_list (* distributes points evenly *) )) ; end GenerateSimpleTest; function CreateCriteriaList inputs: tc:TestCriteria; outputs: tclist:TestCriteria*; precondition: ; postcondition: forall(tc' in tclist) ( not ( exists(tc'' in tclist) (tc'.test_type = tc''.test_type) and (tc' != tc'') ) ) (* and (forall (i:integer | (i >= 1) and (i <= #tstlist)) ( ) *) ; end CreateCriteriaList; function CreateSimpleTestList inputs: tclist:TestCriteria*; outputs: tstlist:Test*; postcondition: #tstlist = #tclist and (forall (i:integer | (i >= 1) and (i <= #tstlist)) tstlist[i] = GenerateSimpleTest( tclist[i] )) ; end CreateSimpleTestList; function CombineTestList inputs: tstlist:Test*; outputs: tst:Test; postcondition: ( forall (tst' in tstlist) ( forall (tqi' in tst'.question_list) (tqi' in tst.question_list) ) and forall (tqi' in tst.question_list) ( exists(tst' in tstlist) (tqi' in tst'.question_list) ) ) ; end CombineTestList; function GenerateMixTypeTest inputs: tc:TestCriteria; outputs: tst':Test; description: (* Generates test. Type = Mix *); postcondition: tst' = CombineTestList( CreateSimpleTestList( CreateCriteriaList(tc) ) ) ; end GenerateMixTypeTest; operation GenerateTest inputs: tc:TestCriteria; outputs: tst':Test; description: (* Generates a test using info in TestCriteria. *); precondition: ; postcondition: ( if tc.test_type != mix then tst' = GenerateSimpleTest(tc) else tst' = GenerateMixTypeTest(tc) ) and tst'.testcriteria = tc and tst'.filename = nil and tst'.name = nil and tst'.date = nil ; end GenerateTest; function IsValidQuestion(tqi:TestQuestionItem)->boolean = ( tqi.question_text != nil and tqi.answer != nil ); function QuestionsEqual(tqi:TestQuestionItem, qi:QuestionItem)->boolean = ( tqi.questiontext = qi.questiontext and tqi.answer = qi.answer and tqi.type = qi.type and tqi.diff = qi.diff and tqi.time = qi.time ); function AllSameExceptQuestionList(tst1:Test, tst2:Test)->boolean = ( tst1.name = tst2.name and tst1.date = tst2.date and tst1.testcriteria = tst2.testcriteria and tst1.filename = tst2.filename ); operation AddQuestion inputs: tst :Test, tqi :TestQuestionItem; outputs: tst' :Test; description: (* Add the given TestQuestionItem to the given Test. The given question must not be in the test. *); precondition: (not (tqi in tst.question_list)) and IsValidQuestion(tqi) ; postcondition: AllSameExceptQuestionList(tst, tst') and forall (tqi':TestQuestionItem) (tqi' in tst'.question_list) iff ( (tqi' = tqi) or (tqi' in tst.question_list) ) ; end AddQuestion; operation ModifyQuestion inputs: tst :Test, old_tqi :TestQuestionItem, new_tqi :TestQuestionItem; outputs: tst' :Test; description: (* This operation removes the question oldq from the test and add the question newq to the test. The output is the updated test. *); precondition: old_tqi != new_tqi and old_tqi in tst.question_list and (not (exists (new_tqi' in tst.question_list) new_tqi' = new_tqi)) and IsValidQuestion(tqi) ; postcondition: AllSameExceptQuestionList(tst, tst') and forall (tqi':TestQuestionItem) (tqi' in tst') iff ( ( (tqi' = new_tqi) or (tqi' in tst.question_list) ) and (tqi' != old_tqi) ) ; end ModifyQuestion; operation DeleteQuestion inputs: tst :Test, tqi :TestQuestionItem; outputs: tst' :Test; description: (* This operation removes the given question from the test. Given question must be in the test. *); precondition: tqi in tst.question_list ; postcondition: AllSameExceptQuestionList(tst, tst') and forall (tqi':TestQuestionItem) (tqi' in tst'.question_list) iff ( (tqi' != tqi) and (tqi' in tst.question_list) ) ; end DeleteQuestion; function TQI2QI inputs: tqi :TestQuestionItem; outputs: qi :QuestionItem; postcondition: tqi.questiontext = qi.questiontext and tqi.answer = qi.answer and tqi.type = qi.type and tqi.diff = qi.diff and tqi.time = qi.time ; end TQI2QI; function SimiliarQuestion inputs: tst :Test, tqi :TestQuestionItem; outputs: tqi' :TestQuestionItem; description: (* return a similiar question from the given test *); precondition: ; precondition: IsInSomeSelectedQuestionList(tst.testcriteria, TQI2QI(tqi')) and (not (tqi' in tst.question_list)) and tqi'.type = tqi.type and tqi'.diff = tqi.diff and tqi' != tqi ; end SimiliarQuestion; operation ReplaceQuestion inputs: tst :Test, tqi :TestQuestionItem; outputs: tst' :Test; description: (* Replace a question in a test with a similiar question from a database. The difficulty and type of the question is the same. If there are more than one such questions, the system choose the first one. If there is none, the operation displays a warning message and exits. Given question must be in the test. If there are no similiar questions, the system displays a warning and tst = tst'; *); precondition: tqi in tst.question_list; postcondition: AllSameExceptQuestionList(tst, tst') and ( forall (tqi':TestQuestionItem) (tqi' in tst'.question_list) iff ( (tqi' != tqi) and (tqi' in tst.question_list) ) (* delete question *) ) and ( forall (tqi':TestQuestionItem) (tqi' in tst'.question_list) iff ( ( tqi' = SimiliarQuestion(tst, tqi) and tqi' != nil (* exists a similiar question *) ) or tqi' in tst.question_list ) (* add similiar question *) ) or ( SimiliarQuestion(tst, tqi) = nil and tst = tst' (* no similiar question *) ) ; end ReplaceQuestion; (* end Generate; *)