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