[system] / trunk / pg / macros / contextOrdering.pl Repository:
ViewVC logotype

Annotation of /trunk/pg/macros/contextOrdering.pl

Parent Directory Parent Directory | Revision Log Revision Log


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

1 : dpvc 5959 =head1 NAME
2 :    
3 :     contextOrdering.pl - Parses ordered lists of letters like "B > A = C > D"
4 :    
5 :     =head1 DESCRIPTION
6 :    
7 :     This context provides a structured way to parse and check answers that
8 :     are ordered lists of letters, where the letters are separated by
9 :     greater-than signs or equal signs. The only operators allowed are >
10 :     and =, and the only letters allowed are the ones you specify explicitly.
11 :    
12 :     To access the context, you must include
13 :    
14 :     loadMacros("contextOrdering.pl");
15 :    
16 :     at the top of your problem file, and then specify the Ordering context:
17 :    
18 :     Context("Ordering");
19 :    
20 :     There are two main ways to use the Ordering context. The first is to
21 :     use the Ordering() command to generate your ordering. This command
22 :     creates a context in which the proper letters are defined, and returns
23 :     a MathObject that represents the ordering you have provided. For
24 :     example,
25 :    
26 :     $ans = Ordering("B > A > C");
27 :    
28 :     or
29 :    
30 :     $ans = Ordering(A => 2, B => 2.5, C => 1);
31 :    
32 :     would both produce the same ordering. The first form gives the
33 :     ordering as the student must type it, and the second gives the
34 :     ordering by specifying numeric values for the various letters that
35 :     determine the resulting order. Note that equality is determined using
36 :     the default tolerances for the Ordering context. You can change these
37 :     using commands like the following:
38 :    
39 :     Context("Ordering");
40 :     Context()->flags->set(tolerance => .01, tolType => 'absolute');
41 :    
42 :     If you want to allow lists of orderings, use the Ordering-List context:
43 :    
44 :     Context("Ordering-List");
45 :     $ans = Ordering("A > B , B = C");
46 :    
47 :     Note that each Ordering() call uses its own copy of the current
48 :     context. If you need to modify the actual context used, then use the
49 :     context() method of the resulting object.
50 :    
51 :     The second method of generating orderings is to declare the letters
52 :     you wish to use explicitly, and then build the Ordering objects using
53 :     the standard Compute() method:
54 :    
55 :     Context("Ordering");
56 :     Letters("A","B","C","D");
57 :     $a = Compute("A > B = C");
58 :     $b = Compute("C > D");
59 :    
60 :     Note that in this case, D is still a valid letter that students can
61 :     enter in response to an answer checker for $a, and similarly for A and
62 :     B with $b. Note also that both $a and $b use the same context, unlike
63 :     orderings produced by calls to the Ordering() function. Changes to
64 :     the current context WILL affect $a and $b.
65 :    
66 :     =cut
67 :    
68 :     loadMacros("MathObjects.pl");
69 :    
70 :     sub _contextOrdering_init {context::Ordering::Init()}
71 :    
72 :     ###########################################
73 :     #
74 :     # The main Ordering routines
75 :     #
76 :    
77 :     package context::Ordering;
78 :    
79 :     #
80 :     # Here we set up the prototype contexts and define the needed
81 :     # functions in the main:: namespace. Some error messages are
82 :     # modified to read better for these contexts.
83 :     #
84 :     sub Init {
85 :     my $context = $main::context{Ordering} = Parser::Context->getCopy("Numeric");
86 :     $context->{name} = "Ordering";
87 :     $context->parens->clear();
88 :     $context->variables->clear();
89 :     $context->constants->clear();
90 :     $context->operators->clear();
91 :     $context->functions->clear();
92 :     $context->strings->clear();
93 :     $context->operators->add(
94 : dpvc 5962 '>' => {precedence => 1.5, associativity => 'left', type => 'bin', class => 'context::Ordering::BOP::ordering'},
95 :     '=' => {precedence => 1.7, associativity => 'left', type => 'bin', class => 'context::Ordering::BOP::ordering'},
96 : dpvc 5959 );
97 : dpvc 5962 $context->{parser}{String} = "context::Ordering::Parser::String";
98 :     $context->{parser}{Value} = "context::Ordering::Parser::Value";
99 :     $context->{parser}{BOP} = "context::Ordering::Parser::BOP";
100 :     $context->{value}{String} = "context::Ordering::Value::String";
101 : dpvc 5960 $context->{value}{Ordering} = "context::Ordering::Value::Ordering";
102 : dpvc 5959 $context->strings->add('='=>{hidden=>1},'>'=>{hidden=>1});
103 :     $context->{error}{msg}{"Variable '%s' is not defined in this context"} = "'%s' is not defined in this context";
104 :     $context->{error}{msg}{"Unexpected character '%s'"} = "Can't use '%s' in this context";
105 :     $context->{error}{msg}{"Missing operand before '%s'"} = "Missing letter before '%s'";
106 :     $context->{error}{msg}{"Missing operand after '%s'"} = "Missing letter after '%s'";
107 :    
108 :     $context = $main::context{'Ordering-List'} = $context->copy;
109 :     $context->{name} eq 'Ordering-List';
110 :     $context->operators->redefine(',',from => "Full");
111 :     $context->{value}{List} = "context::Ordering::Value::List";
112 :    
113 :     main::PG_restricted_eval('sub Letters {context::Ordering::Letters(@_)}');
114 :     main::PG_restricted_eval('sub Ordering {context::Ordering::Ordering(@_)}');
115 :     }
116 :    
117 :     #
118 :     # A routine to set the letters allowed in this context.
119 :     # (Old letters are cleared, and > and = are allowed, but hidden,
120 :     # since they are used in the List() objects that implement the context).
121 :     #
122 :     sub Letters {
123 :     my $context = (Value::isContext($_[0]) ? shift : main::Context());
124 :     my @strings;
125 :     foreach my $x (@_) {push(@strings, $x => {isLetter => 1, caseSensitive => 1})}
126 :     $context->strings->are(@strings);
127 :     $context->strings->add('='=>{hidden=>1},'>'=>{hidden=>1});
128 :     }
129 :    
130 :     #
131 :     # Create orderings from strings or lists of letter => value pairs.
132 :     # A copy of the current context is created that contains the proper
133 :     # letters, and the correct string is created and parsed into an
134 :     # Ordering object.
135 :     #
136 :     sub Ordering {
137 :     my $context = main::Context()->copy; my $string;
138 :     Value->Error("The current context is not the Ordering context")
139 :     unless $context->{name} =~ m/Ordering/;
140 :     if (scalar(@_) == 1) {
141 :     $string = shift;
142 :     my $letters = $string; $letters =~ s/[^A-Z]//ig;
143 :     context::Ordering::Letters($context,split(//,$letters));
144 :     } else {
145 :     my %letter = @_; my @letters = keys %letter;
146 :     context::Ordering::Letters($context,@letters);
147 :     foreach my $x (@letters) {$letter{$x} = Value::Real->new($context,$letter{$x})}
148 :     my @order = main::PGsort(
149 :     sub {$letter{$_[0]} == $letter{$_[1]} ? $_[0] lt $_[1] : $letter{$_[0]} > $letter{$_[1]}},
150 :     @letters
151 :     );
152 :     my $a = shift(@order); my $b; $string = $a;
153 :     while ($b = shift(@order)) {
154 :     $string .= ($letter{$a} == $letter{$b} ? " = " : " > ") . $b;
155 :     $a = $b;
156 :     }
157 :     }
158 :     return main::Formula($context,$string)->eval;
159 :     }
160 :    
161 :     #############################################################
162 :     #
163 :     # This is a Parser BOP used to create the Ordering objects
164 :     # used internally. They are actually lists with the operator
165 :     # and the two operands, and the comparisons is based on the
166 :     # standard list comparisons. The operands are either the strings
167 :     # for individual letters, or another Ordering object as a
168 :     # nested List.
169 :     #
170 :    
171 : dpvc 5962 package context::Ordering::BOP::ordering;
172 : dpvc 5959 our @ISA = ('Parser::BOP');
173 :    
174 :     sub class {"Ordering"}
175 :    
176 :     sub isOrdering {
177 :     my $self = shift; my $obj = shift; my $class = $obj->class;
178 : dpvc 5962 return $class eq 'Ordering' || $obj->{def}{isLetter};
179 : dpvc 5959 }
180 :    
181 :     sub _check {
182 :     my $self = shift;
183 : dpvc 5962 $self->Error("Operators of %s must be letters",$self->{bop})
184 :     unless $self->isOrdering($self->{lop}) && $self->isOrdering($self->{rop});
185 :     $self->{letters} = $self->{lop}{letters}; # we modify {lop}{letters} this way, but that doesn't matter
186 :     foreach my $x (keys %{$self->{rop}{letters}}) {
187 :     if (defined($self->{letters}{$x})) {
188 :     $self->{ref} = $self->{rop}{letters}{$x};
189 :     $self->Error("Letters can appear only once in an ordering");
190 :     }
191 :     $self->{letters}{$x} = $self->{rop}{letters}{$x};
192 :     }
193 :     $self->{equation}{letters} = $self->{letters}; # removed by context::Ordering::Parser::BOP;
194 : dpvc 5959 }
195 :    
196 :     sub _eval {
197 :     my $self = shift;
198 : dpvc 5960 return $self->Package("Ordering")->new($self->context,$self->{bop},@_);
199 : dpvc 5959 }
200 :    
201 :     sub string {
202 :     my $self = shift;
203 :     return $self->{lop}->string." ".$self->{bop}." ".$self->{rop}->string;
204 :     }
205 :    
206 :     sub TeX {
207 :     my $self = shift;
208 :     return $self->{lop}->TeX." ".$self->{bop}." ".$self->{rop}->TeX;
209 :     }
210 :    
211 : dpvc 5962
212 : dpvc 5959 #############################################################
213 :     #
214 :     # This is the Value object used to implement the list That represents
215 :     # one ordering operation. It is simply a normal Value::List with the
216 :     # operator as the first entry and the two operands as the remaing
217 :     # entries in the list. The new() method is overriden to make binary
218 :     # trees of equal operators into flat sorted lists. We override the
219 :     # List string and TeX methods so that they print correctly as binary
220 :     # operators. The cmp_equal method is overriden to make sure the that
221 :     # the lists are treated as a unit during answer checking. There is
222 :     # also a routine for adding letters to the object's context.
223 :     #
224 :    
225 :     package context::Ordering::Value::Ordering;
226 :     our @ISA = ('Value::List');
227 :    
228 :     #
229 :     # Put all equal letters into one list and sort them
230 :     #
231 :     sub new {
232 :     my $self = shift;
233 :     my $context = (Value::isContext($_[0]) ? shift : $self->context);
234 :     my $bop = shift; my @letters = @_;
235 :     if ($bop eq '=') {
236 :     if (Value::classMatch($letters[0],'Ordering') && $letters[0]->{data}[0] eq '=')
237 :     {@letters = ($letters[0]->value,$letters[1]); shift @letters}
238 :     @letters = main::lex_sort(@letters);
239 :     }
240 :     return $self->SUPER::new($context,$bop,@letters);
241 :     }
242 :    
243 :     sub string {
244 :     my $self = shift;
245 :     my ($bop,@rest) = $self->value;
246 :     foreach my $x (@rest) {$x = $x->string};
247 :     return join(" $bop ",@rest);
248 :     }
249 :    
250 :     sub TeX {
251 :     my $self = shift;
252 :     my ($bop,@rest) = $self->value;
253 :     foreach my $x (@rest) {$x = $x->TeX};
254 :     return join(" $bop ",@rest);
255 :     }
256 :    
257 :     #
258 :     # Make sure we do comparison as a list of lists (rather than as the
259 :     # individual entries in the underlying Value::List that encodes
260 :     # the ordering)
261 :     #
262 :     sub cmp_equal {
263 : dpvc 5962 my $self = shift; my $ans = $_[0];
264 : dpvc 5959 $ans->{typeMatch} = $ans->{firstElement} = $self;
265 :     $self = $ans->{correct_value} = Value::List->make($self);
266 :     $ans->{student_value} = Value::List->make($ans->{student_value})
267 :     if Value::classMatch($ans->{student_value},'Ordering');
268 :     return $self->SUPER::cmp_equal(@_);
269 :     }
270 :    
271 :     #
272 :     # Add more letters to the ordering's context (so student answers
273 :     # can include them even if they aren't in the correct answer).
274 :     #
275 :     sub AddLetters {
276 :     my $self = shift; my $context = $self->context;
277 :     my @strings;
278 :     foreach my $x (@_) {
279 :     push(@strings, $x => {isLetter => 1, caseSensitive => 1})
280 :     unless $context->strings->get($x);
281 :     }
282 :     $context->strings->add(@strings) if scalar(@strings);
283 :     }
284 :    
285 :     #############################################################
286 :     #
287 :     # This overrides the TeX method of the letters
288 :     # so that they don't print using the \rm font.
289 :     #
290 :    
291 :     package context::Ordering::Value::String;
292 :     our @ISA = ('Value::String');
293 :    
294 :     sub TeX {shift->value}
295 :    
296 :    
297 :     #############################################################
298 :     #
299 : dpvc 5962 # Override Parser classes so that we can check for repeated letters
300 :     #
301 :    
302 :     package context::Ordering::Parser::String;
303 :     our @ISA = ('Parser::String');
304 :    
305 :     #
306 :     # Save the letters positional reference
307 :     #
308 :     sub new {
309 :     my $self = shift;
310 :     $self = $self->SUPER::new(@_);
311 :     $self->{letters}{$self->{value}} = $self->{ref} if $self->{def}{isLetter};
312 :     return $self;
313 :     }
314 :    
315 :     #########################
316 :    
317 :     package context::Ordering::Parser::Value;
318 :     our @ISA = ('Parser::Value');
319 :    
320 :     #
321 :     # Move letters to Value object
322 :     #
323 :     sub new {
324 :     my $self = shift;
325 :     $self = $self->SUPER::new(@_);
326 :     if (defined($self->{value}{letters})) {
327 :     $self->{letters} = $self->{value}{letters};
328 :     delete $self->{value}{letters};
329 :     }
330 :     return $self;
331 :     }
332 :    
333 :     #
334 :     # Return Ordering class if the object is one
335 :     #
336 :     sub class {
337 :     my $self = shift;
338 :     return "Ordering" if $self->{value}->classMatch('Ordering');
339 :     return $self->SUPER::class;
340 :     }
341 :    
342 :     #########################
343 :    
344 :     package context::Ordering::Parser::BOP;
345 :     our @ISA = ('Parser::BOP');
346 :    
347 :     #
348 :     # If a BOP is constant and so reduced automatically we will lose the
349 :     # letters hash, so it is stored temporarily in the equation by _check(),
350 :     # and replaced here. A hack, but that avoids adding letters to the
351 :     # Ordering object in eval() and having them remain there after parsing.
352 :     #
353 :     sub new {
354 :     my $self = shift;
355 :     $self = $self->SUPER::new(@_);
356 :     $self->{letters} = $self->{equation}{letters} unless defined $self->{letters} || $self->class ne 'Ordering';
357 :     delete $self->{equation}{letters};
358 :     return $self;
359 :     }
360 :    
361 :    
362 :     #############################################################
363 :     #
364 : dpvc 5959 # This overrides the cmp_equal method to make sure that
365 :     # Ordering lists are put into nested lists (since the
366 :     # underlying ordering is a list, we don't want the
367 :     # list checker to test the individual parts of the list,
368 :     # but rather the list as a whole).
369 :     #
370 :    
371 :     package context::Ordering::Value::List;
372 :     our @ISA = ('Value::List');
373 :    
374 :     sub cmp_equal {
375 :     my $self = shift; my $ans = $_[0];
376 :     $ans->{student_value} = Value::List->make($ans->{student_value})
377 :     if Value::classMatch($ans->{student_value},'Ordering');
378 :     return $self->SUPER::cmp_equal(@_);
379 :     }
380 : dpvc 5962
381 :     #############################################################
382 :    
383 :     1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9