[system] / trunk / webwork / system / courseScripts / List.pm Repository:
ViewVC logotype

Annotation of /trunk/webwork/system/courseScripts/List.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2 - (view) (download) (as text)

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9