PageRenderTime 58ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/List.pm

https://github.com/ghe3/pg
Perl | 608 lines | 263 code | 93 blank | 252 comment | 21 complexity | 01045bd922eb1d4c1f5107ea20a525d1 MD5 | raw file
  1. #New highly object-oriented list construct
  2. #This List.pm is the super class for all types of lists
  3. #As of 6/5/2000 the three list sub-classes are Match, Select, Multiple
  4. #RDV
  5. =head1 NAME
  6. List.pm -- super-class for all list structures
  7. =head1 SYNOPSIS
  8. =pod
  9. List.pm is not intended to be used as a stand alone object.
  10. It is a super-class designed to be inherited by sub-classes that,
  11. through small changes, can be used for a variety of different
  12. questions that involve some sort of list of questions and/or answers.
  13. List.pm has been used to construct Match.pm, Select.pm, and Multiple.pm.
  14. These three classes are objects that can be used to create the
  15. following question types:
  16. B<Matching list:>
  17. Given a list of questions and answers, match the correct answers to the
  18. questions. Some answers may be used more than once and some may not be used at
  19. all. The order of the answers is usually random but some answers can be
  20. appended to the end in a set order (i.e. 'None of the above'). Answers are
  21. given corresponding letters as shortcuts to typing in the full answer. (i.e.
  22. the answer to #1 is A).
  23. B<Select list:>
  24. Given a list of questions and (usually) implied answers, give the correct
  25. answer to each question. This is intended mainly for true/false questions or
  26. other types of questions where the answers are short and can therefore be typed
  27. in by the user easily. If a select list is desired but the answers are too long
  28. to really type in, a popup-list of the answers can be used.
  29. B<Multiple choice:>
  30. Given a single question and a list of answers, select the single correct answer.
  31. This structure creates a standard multiple choice question as would be seen on a
  32. standardize test. Extra answers are entered along with the question in a simple
  33. format and (as with Match.pm), if necessary, can be appended in order at the end
  34. (i.e. 'None of the above')
  35. =for html
  36. <P>See <a href="Match">Match.pm</a>, <a href="Select">Select.pm</a>, <a href="Multiple">Multiple.pm</a>, and <a href="PGchoicemacros">PGchoicemacros.pl</a>
  37. =head1 DESCRIPTION
  38. =head2 Variables and methods available to sub-classes
  39. =head3 Variables
  40. questions # array of questions as entered using qa()
  41. answers # array of answers as entered using qa()
  42. extras # array of extras as entered using extra()
  43. selected_q # randomly selected subset of "questions"
  44. selected_a # the answers for the selected questions
  45. selected_e # randomly selected subset of "extras"
  46. ans_rule_len # determines the length of the answer blanks
  47. # default is 4
  48. slice # index used to select specific questions
  49. shuffle # permutation array which can be applied to slice
  50. # to shuffle the answers
  51. inverted_shuffle # the inverse permutation array
  52. rf_print_q # reference to any subroutine which should
  53. # take ($self, @questions) as parameters and
  54. # output the questions in a formatted string.
  55. # If you want to change the way questions are
  56. # printed, write your own print method and set
  57. # this equal to a reference to to that method
  58. # (i.e. $sl->rf_print_q = ~~&printing_routine_q)
  59. rf_print_a # reference to any subroutine which should
  60. # take ($self, @answers) as parameters and
  61. # output the answers in a formatted string.
  62. # If you want to change the way answers are
  63. # printed, write your own print method and set
  64. # this equal to a reference to to that method
  65. # (i.e. $sl->rf_print_a = ~~&printing_routine_a)
  66. ra_pop_up_list # Field used in sub classes that use pop_up_list_print_q
  67. # to format the questions. (Placing a pop_up_list next to
  68. # each question instead of an answer blank.
  69. # It is initialized to
  70. # => [no_answer =>' ?', T => 'True', F => 'False']
  71. ans_rule_len # field which can be used in the question printing routines
  72. # to determine the length of the answer blanks before the questions.
  73. =head3 Methods
  74. qa( array ) # accepts an array of strings which can be used
  75. # for questions and answers
  76. extra( array ) # accepts an array of strings which can be used
  77. # as extra answers
  78. print_q # yields a formatted string of question to be
  79. # matched with answer blanks
  80. print_a # yields a formatted string of answers
  81. choose([3, 4], 1) # chooses questions indexed 3 and 4 and one other
  82. # randomly
  83. choose_extra([3, 4], 1) # choooses extra answers indexed 3 and 4 and one
  84. # other
  85. makeLast( array ) # accepts an array of strings (like qa) which will
  86. # be forced to the end of the list of answers.
  87. ra_correct_ans # outputs a reference to the array of correct answers
  88. correct_ans # outputs a concatenated string of correct answers (only for Multiple)
  89. =head2 Usage
  90. None -- see SYNOPSIS above
  91. =cut
  92. BEGIN {
  93. be_strict();
  94. }
  95. #use strict;
  96. package List;
  97. @List::ISA = qw( Exporter );
  98. my %fields = (
  99. questions => undef,
  100. answers => undef,
  101. extras => undef,
  102. selected_q => undef,
  103. selected_a => undef,
  104. selected_e => undef,
  105. ans_rule_len => undef,
  106. ra_pop_up_list => undef,
  107. rf_print_q => undef,
  108. rf_print_a => undef,
  109. slice => undef,
  110. shuffle => undef,
  111. inverted_shuffle => undef,
  112. rand_gen => undef,
  113. );
  114. #used to initialize variables and create an instance of the class
  115. sub new {
  116. my $class = shift;
  117. my $seed = shift;
  118. warn "List requires a random number: new List(random(1,2000,1)" unless defined $seed;
  119. my $self = { _permitted => \%fields,
  120. questions => [],
  121. answers => [],
  122. extras => [],
  123. selected_q => [],
  124. selected_a => [],
  125. selected_e => [],
  126. ans_rule_len => 4,
  127. ra_pop_up_list => [no_answer =>' ?', T => 'True', F => 'False'],
  128. rf_print_q => 0,
  129. rf_print_a => 0,
  130. slice => [],
  131. shuffle => [],
  132. inverted_shuffle => [],
  133. rand_gen => new PGrandom,
  134. };
  135. bless $self, $class;
  136. $self->{rand_gen}->srand($seed);
  137. $self->{rf_print_q} = shift;
  138. $self->{rf_print_a} = shift;
  139. return $self;
  140. }
  141. # AUTOLOAD allows variables to be set and accessed like methods
  142. # returning the value of the variable
  143. sub AUTOLOAD {
  144. my $self = shift;
  145. my $type = ref($self) or die "$self is not an object";
  146. # $AUTOLOAD is sent in by Perl and is the full name of the object (i.e. main::blah::blah_more)
  147. my $name = $List::AUTOLOAD;
  148. $name =~ s/.*://; #strips fully-qualified portion
  149. unless ( exists $self->{'_permitted'}->{$name} ) {
  150. die "Can't find '$name' field in object of class '$type'";
  151. }
  152. if (@_) {
  153. return $self->{$name} = shift; #set the variable to the first parameter
  154. } else {
  155. return $self->{$name}; #if no parameters just return the value
  156. }
  157. }
  158. sub DESTROY {
  159. # doing nothing about destruction, hope that isn't dangerous
  160. }
  161. # *** Utility methods ***
  162. #internal
  163. #choose k random numbers out of n
  164. sub NchooseK {
  165. my $self = shift;
  166. my ($n, $k) = @_;
  167. die "method NchooseK: n = $n cannot be less than k=$k\n
  168. You probably did a 'choose($k)' with only $n questions!" if $k > $n;
  169. my @array = 0..($n-1);
  170. my @out = ();
  171. while (@out < $k) {
  172. push(@out, splice(@array, $self->{rand_gen}->random(0, $#array, 1), 1) );
  173. }
  174. return @out;
  175. }
  176. #internal
  177. #return an array of random numbers
  178. sub shuffle {
  179. my $self = shift;
  180. my $i = @_;
  181. my @out = $self->NchooseK($i, $i);
  182. return @out;
  183. }
  184. # *** Utility subroutines ***
  185. #internal
  186. #swap subscripts with their respective values
  187. sub invert {
  188. my @array = @_;
  189. my @out = ();
  190. for (my $i=0; $i<@array; $i++) {
  191. $out[$array[$i]] = $i;
  192. }
  193. return @out;
  194. }
  195. #internal
  196. #slice of the alphabet
  197. sub ALPHABET {
  198. return ('A'..'ZZ')[@_];
  199. }
  200. #given a universe of subscripts and a subset of the universe,
  201. #return the complement of that set in the universe
  202. sub complement {
  203. my $ra_univ = shift;
  204. my $ra_set = shift;
  205. my @univ = @$ra_univ;
  206. my @set = @$ra_set;
  207. my %set = ();
  208. foreach my $i (@set) {
  209. $set{$i} = 1;
  210. }
  211. my @out = ();
  212. foreach my $i (@univ) {
  213. push(@out, $i) unless exists( $set{$i} );
  214. }
  215. return @out;
  216. }
  217. # *** Input and Output subroutines ***
  218. #From here down are the ones that should be overloaded by sub-classes
  219. #Input answers
  220. #defaults to inputting 'question', 'answer', 'question', etc (should be overloaded for other types of questions)
  221. =head3 qa
  222. Usage: $ml->qa( qw( question1 answer1 question2 answer2 ) );
  223. =cut
  224. sub qa {
  225. my $self = shift;
  226. my @questANDanswer = @_;
  227. while (@questANDanswer) {
  228. push (@{ $self->{questions} }, shift(@questANDanswer) );
  229. push (@{ $self->{answers} }, shift(@questANDanswer) );
  230. }
  231. }
  232. #Input extra answers
  233. sub extra {
  234. my $self = shift;
  235. push(@{ $self->{extras} }, @_); #pushing allows multiple calls without overwriting old "extras"
  236. }
  237. #Output questions
  238. #Doesn't do actual output, refers to method given in call to 'new' (rf_print_q)
  239. sub print_q {
  240. my $self = shift;
  241. &{ $self->{rf_print_q} }( $self, @{ $self->{selected_q} } );
  242. }
  243. #Output answers
  244. #Doesn't do actual output, refers to method given in call to 'new' (rf_print_a)
  245. sub print_a {
  246. my $self = shift;
  247. &{ $self->{rf_print_a} }( $self, @{ $self->{selected_a} } );
  248. }
  249. #return array of answers to be checked against the students answers
  250. #defaults to returning the actual selected answers (should be overloaded for other types of answers)
  251. sub ra_correct_ans {
  252. my $self = shift;
  253. return $self->{selected_a};
  254. }
  255. =head3 cmp
  256. Usage ANS($ml -> cmp);
  257. provides a MathObject like comparison method
  258. returns a string of comparison methods for checking the list object
  259. =cut
  260. sub cmp {
  261. my $self = shift;
  262. my @answers = @{$self->{selected_a}};
  263. @answers = map {Value::makeValue($_)} @answers; # make sure answers are all MathObjects
  264. @answers = map {$_->cmp} @answers; # replace the MathObjects by their AnswerEvaluators
  265. return @answers;
  266. }
  267. #Match and Select return references to arrays while Multiple justs returns a string
  268. #so Match and Select use ra_correct_ans while Multiple uses correct_ans
  269. sub correct_ans {
  270. warn "Match and/or Select do not use correct_ans.\nYou should use ra_correct_ans instead.";
  271. }
  272. # *** Question and Answer Manipulation Subroutines ***
  273. #calls methods that deal with list specific methods of picking random questions and answers
  274. #mainly exists for backward compatibility and to hide some of the activity from the naive user
  275. sub choose {
  276. my $self = shift;
  277. my @input = @_;
  278. $self->getRandoms(scalar(@{$self->{questions}}), @input); #pick random numbers
  279. $self->selectQA(); #select questions and answers
  280. $self->dumpExtra(); #dump extra answers into "extras"
  281. $self->condense(); #eliminate duplicate answers"
  282. }
  283. #randomly inserts the selected extra answers into selected_a and
  284. #updates inverted_shuffle accordingly
  285. sub choose_extra {
  286. my $self = shift;
  287. my @input = @_;
  288. $self->getRandoms(scalar(@{ $self->{extras} }), @input);
  289. $self->{selected_e} = [ @{ $self->{extras} }[ @{ $self->{slice} }[ @{ $self->{shuffle} } ] ] ];
  290. my $length = 0;
  291. my $random = 0;
  292. foreach my $extra_ans ( invert(@{ $self->{shuffle} }) ) {
  293. #warn "Selected Answers: @{ $self->{selected_a} }<BR>
  294. # Inverted Shuffle: @{ $self->{inverted_shuffle} }<BR>
  295. # Random: $random";
  296. $random = $self->{rand_gen}->random(0, scalar(@{ $self->{selected_a} }), 1);
  297. for (my $pos = 0; $pos < @{ $self->{inverted_shuffle} }; $pos++) {
  298. @{ $self->{inverted_shuffle} }[$pos]++ unless @{ $self->{inverted_shuffle} }[$pos] < $random;
  299. }
  300. my @temp = ( @{ $self->{selected_a} }[0..$random-1], @{ $self->{selected_e} }[$extra_ans], @{$self->{selected_a} }[$random..$#{ $self->{selected_a} } ] );
  301. @{ $self->{selected_a} } = @temp;
  302. }
  303. }
  304. #create random @slice and @shuffle to randomize questions and answers
  305. sub getRandoms {
  306. my $self = shift;
  307. my $N = shift;
  308. my @input = @_;
  309. my $K = 0;
  310. my @fixed_choices = (); # questions forced by the user
  311. foreach my $i (@input) { #input is of the form ([3, 5, 6], 3)
  312. if (ref($i) eq 'ARRAY') {
  313. push(@fixed_choices, @{$i});
  314. } else {
  315. $K += $i;
  316. }
  317. }
  318. # my $N = @{ $self->{questions} };
  319. my @remaining = complement( [0..$N-1], [@fixed_choices] );
  320. my @slice = @fixed_choices;
  321. push (@slice, @remaining[ $self->NchooseK(scalar(@remaining), $K) ] ); #slice of remaing choices
  322. @slice = @slice[ $self->NchooseK( scalar(@slice), scalar(@slice) ) ]; #randomize the slice (the questions)
  323. #shuffle will be used to randomize the answers a second time (so they don't coincide with the questions)
  324. my @shuffle = $self->NchooseK( scalar(@slice), scalar(@slice) );
  325. $self->{slice} = \@slice; #keep track of the slice and shuffle
  326. $self->{shuffle} = \@shuffle;
  327. }
  328. #select questions and answers according to slice and shuffle
  329. sub selectQA {
  330. my $self = shift;
  331. $self->{selected_q} = [ @{ $self->{questions} }[ @{ $self->{slice} } ] ];
  332. $self->{selected_a} = [ @{ $self->{answers} }[@{ $self->{slice} }[@{ $self->{shuffle} } ] ] ];
  333. $self->{inverted_shuffle} = [ invert(@{ $self->{shuffle} }) ];
  334. }
  335. #dump unused answers into list of extra answers
  336. sub dumpExtra {
  337. my $self = shift;
  338. my @more_extras = complement([0..scalar(@{ $self->{answers} })-1], [@{ $self->{slice} }]);
  339. push( @{ $self->{extras} }, @{ $self->{answers} }[@more_extras] );
  340. }
  341. #Allows answers to be added to the end of the selected answers
  342. #This can be used to force answers like "None of the above" and/or "All of the above" to still occur at the
  343. #end of the list instead of being randomized like the rest of the answers
  344. sub makeLast {
  345. my $self = shift;
  346. my @input = @_;
  347. push(@{ $self->{selected_a} }, @input);
  348. $self->condense(); #make sure that the user has not accidentally forced a duplicate answer
  349. #note: condense was changed to eliminate the first occurence of a duplicate
  350. #instead of the last occurence so that it could be used in this case and
  351. #would not negate the fact that one of the answers needs to be at the end
  352. }
  353. #Eliminates duplicates answers and rearranges inverted_shuffle so that all questions with the same answer
  354. #point to one and only one copy of that answer
  355. sub old_condense {
  356. my $self = shift;
  357. for (my $outer = 0; $outer < @{ $self->{selected_a} }; $outer++) {
  358. for (my $inner = $outer+1; $inner < @{ $self->{selected_a} }; $inner++) {
  359. if (@{ $self->{selected_a} }[$outer] eq @{ $self->{selected_a} }[$inner]) {
  360. #then delete the duplicate answer at subscript $outer
  361. @{ $self->{selected_a} } = ( @{ $self->{selected_a} }[0..$outer-1], @{ $self->{selected_a} }[$outer+1..$#{ $self->{selected_a} }] );
  362. #the values of inverted_shuffle point to the position elements in selected_a
  363. #so in order to delete something from selected_a, each element with a position
  364. #greater than $outer must have its position be decremented by one
  365. $inner--; #$inner must be greater than outer so decrement $inner first
  366. for (my $pos = 0; $pos < @{ $self->{inverted_shuffle} }; $pos++) {
  367. if ( @{ $self->{inverted_shuffle} }[$pos] == $outer ) {
  368. @{ $self->{inverted_shuffle} }[$pos] = $inner;
  369. } elsif ( @{ $self->{inverted_shuffle} }[$pos] > $outer ) {
  370. @{ $self->{inverted_shuffle} }[$pos]--;
  371. }
  372. }
  373. #we just changed a bunch of pointers so we need to go back over the same answers again
  374. #(so we decrement $inner (which we already did) and $outer to counter-act the for loop))
  375. #this could probably be done slightly less hackish with while loops instead of for loops
  376. #$outer--;
  377. }
  378. }
  379. }
  380. }
  381. #re-written RDV 10/4/2000
  382. #Eliminates duplicate answers and rearranges inverted_shuffle so that all questions with the same answer
  383. #point to one and only one copy of that answer
  384. sub condense {
  385. my $self = shift;
  386. my ($outer, $inner) = (0, 0);
  387. my $repeat = 0;
  388. while ($outer < @{ $self->{selected_a} }) {
  389. $inner = $outer + 1;
  390. $repeat = 0; #loop again if we find a match
  391. while ($inner < @{ $self->{selected_a}}) {
  392. $repeat = 0; #loop again if we find a match
  393. if (@{ $self->{selected_a} }[$outer] eq @{$self->{selected_a} }[$inner]) {
  394. #then delete the duplicate answer at subscript $outer by combining everything before and after it
  395. @{ $self->{selected_a} } = ( @{ $self->{selected_a} }[0..$outer-1], @{ $self->{selected_a} }[$outer+1..$#{ $self->{selected_a} }] );
  396. #the values of inverted_shuffle to point the _subscript_ of elements in selected_a
  397. #so in order to delete something from selected_a, each element with a subscript
  398. #greater than $outer (where the deletion occurred) must have its position decremented by one
  399. #This also means we need to "slide" $inner down so that it points to the new position
  400. #of the duplicate answer
  401. $inner--;
  402. for (my $pos = 0; $pos < @{ $self->{inverted_shuffle} }; $pos++) {
  403. if ( @{ $self->{inverted_shuffle} }[$pos] == $outer) {
  404. @{ $self->{inverted_shuffle} }[$pos] = $inner;
  405. } elsif ( @{ $self->{inverted_shuffle} }[$pos] > $outer ) {
  406. @{ $self->{inverted_shuffle} }[$pos]--;
  407. }
  408. }
  409. #because we just changed the element that $outer points to
  410. #we need to run throught the loop to make sure that the new value at $outer has
  411. #no duplicates as well
  412. #This means that we don't want to increment either counter (and we need to reset $inner)
  413. $repeat = 1;
  414. $inner = $outer + 1;
  415. }
  416. $inner++ unless $repeat;
  417. }
  418. $outer++ unless $repeat;
  419. }
  420. }
  421. # This condense didn't repeat the inner loop after deleting the element at $outer (so that $outer now pointed to a new value)
  422. # so if the new value at $outer also had a duplicate then it was just skipped.
  423. # This shouldn't work but i'll leave it in for a while just in case
  424. ##Eliminates duplicates answers and rearranges inverted_shuffle so that all questions with the same answer
  425. ##point to one and only one copy of that answer
  426. #sub old_condense {
  427. # my $self = shift;
  428. # for (my $outer = 0; $outer < @{ $self->{selected_a} }; $outer++) {
  429. # for (my $inner = $outer+1; $inner < @{ $self->{selected_a} }; $inner++) {
  430. # if (@{ $self->{selected_a} }[$outer] eq @{ $self->{selected_a} }[$inner]) {
  431. # #then delete the duplicate answer at subscript $outer
  432. # @{ $self->{selected_a} } = ( @{ $self->{selected_a} }[0..$outer-1], @{ $self->{selected_a} }[$outer
  433. #
  434. # #the values of inverted_shuffle point to the position elements in selected_a
  435. # #so in order to delete something from selected_a, each element with a position
  436. # #greater than $outer must have its position be decremented by one
  437. # $inner--; #$inner must be greater than outer so decrement $inner first
  438. # for (my $pos = 0; $pos < @{ $self->{inverted_shuffle} }; $pos++) {
  439. # if ( @{ $self->{inverted_shuffle} }[$pos] == $outer ) {
  440. # @{ $self->{inverted_shuffle} }[$pos] = $inner;
  441. # } elsif ( @{ $self->{inverted_shuffle} }[$pos] > $outer ) {
  442. # @{ $self->{inverted_shuffle} }[$pos]--;
  443. # }
  444. # }
  445. # }
  446. # }
  447. # }
  448. #}
  449. # sub pretty_print {
  450. # my $r_input = shift;
  451. # my $out = '';
  452. # if ( not ref($r_input) ) {
  453. # $out = $r_input; # not a reference
  454. # } elsif ("$r_input" =~/hash/i ) { # this will pick up objects whose '$self' is hash and so works better than ref($r_iput).
  455. # local($^W) = 0;
  456. # $out .= "$r_input " ."<TABLE border = \"2\" cellpadding = \"3\" BGCOLOR = \"#FFFFFF\">";
  457. # foreach my $key (sort keys %$r_input ) {
  458. # $out .= "<tr><TD> $key</TD><TD> =&gt; </td><td>".pretty_print($r_input->{$key}) . "</td></tr>";
  459. # }
  460. # $out .="</table>";
  461. # } elsif (ref($r_input) eq 'ARRAY' ) {
  462. # my @array = @$r_input;
  463. # $out .= "( " ;
  464. # while (@array) {
  465. # $out .= pretty_print(shift @array) . " , ";
  466. # }
  467. # $out .= " )";
  468. # } elsif (ref($r_input) eq 'CODE') {
  469. # $out = "$r_input";
  470. # # } elsif (ref($r_input) =~/list/i or ref($r_input) =~/match/i or ref($r_input) =~/multiple/i) {
  471. # # local($^W) = 0;
  472. # # $out .= ref($r_input) . " <BR>" ."<TABLE BGCOLOR = \"#FFFFFF\">";
  473. # # foreach my $key (sort keys %$r_input ) {
  474. # # $out .= "<tr><TD> $key</TD><TD> =&gt; </td><td>".pretty_print($r_input->{$key}) . "</td></tr>";
  475. # # }
  476. # # $out .="</table>";
  477. # } else {
  478. # $out = $r_input;
  479. # }
  480. # $out;
  481. # }
  482. 1;