[system] / trunk / pg / macros / contextFraction.pl Repository: Repository Listing bbplugincoursesdistsnplrochestersystemwww

View of /trunk/pg/macros/contextFraction.pl

Tue Mar 10 14:08:50 2009 UTC (10 years, 11 months ago) by dpvc
File size: 26281 byte(s)
Fix the context::Fraction::Real object so that new() and make() will
handle being passed a fraction as a string (since Value::makeValue()
will pass that to context::Fraction::Real->make() when it is given a
fraction as a string).


    1 =head1 NAME
2
3 contextFraction.pl - Implements a MathObject class for Fractions.
4
6
7 This context implements a Fraction object that works like a Real, but
8 keeps the numerator and denominator separate.  It provides methods for
9 reducing the fractions, and for allowing fractions with a whole-number
10 preceeding it, as in 4 1/2 for "four and one half".  The answer
11 checker can require that students reduce their results, and there are
12 contexts that don't allow entery of decimal values (only fractions),
13 and that don't allow any operators or functions (other than division
14 and negation).
15
16 To use these contexts, first load the contextFraction.pl file:
17
19
20 and then select the appropriate context -- one of the following three:
21
22   Context("Fraction");
23   Context("Fraction-NoDecimals");
24   Context("LimitedFraction");
25
26 The first is the most general, and allows fractions to be intermixed
27 with real numbers, so 1/2 + .5 would be allowed.  Also, 1/2.5 is
28 allowed, though it produces a real number, not a fraction, since this
29 fraction class only implements fractions of integers.  All operators
30 and functions are defined, so there are no restrictions on what is
31 allowed by the student.
32
33 The second does not allow decimal numbers to be entered, but they can
34 still be produced as the result of function calls, or by named
35 constants such as "pi".  For example, 1/sqrt(2) is allowed (and
36 produces a real number result).  All functions and operations are
37 defined, and the only real difference between this and the previous
38 context is that decimal numbers can't be typed in explicitly.
39
40 The third context limits the operations that can be performed: in
41 addition to not being able to type decimal numbers, no operations
42 other than division and negation are allowed, and no function calls at
43 all.  Thus 1/sqrt(2) would be illegal, as would 1/2 + 2.  The student
44 must enter a whole number or a fraction in this context.  It is also
45 permissible to enter a whole number WITH a fraction, as in 2 1/2 for
46 "two and one half", or 5/2.
47
48 You can use the Compute() function to generate fraction objects, or
49 the Fraction() constructor to make one explicitly.  For example:
50
51   Context("Fraction");
52   $a = Compute("1/2"); 53$b = Compute("4 - 1/6");
54   $c = Compute("(4/9)^(1/2)"); 55 56 Context("LimitedFraction"); 57$d = Compute("4 2/3");
58   $e = Compute("-1 1/2"); 59 60$f = Fraction(-2,5);
61
62 Note that $c will be 2/3,$d will be 14/3, $e will be -3/2, and$f
63 will be -2/5.
64
65 Once you have created a fraction object, you can use it as you would
66 any real number.  For example:
67
68   Context("Fraction");
69   $a = Compute("1/2"); 70$b = Compute("1/3");
71   $c =$a - $b; 72$d = asin($a); 73$e = $b**2; 74 75 Here$c will be the equivalent of Compute("1/6"), $d will be 76 equivalent to Compute("pi/6"), and$e will be the same as Compute("1/9");
77
78 You can an answer checker for a fraction in the same way as you do for
79 ALL MathObjects -- via its cmp() method:
80
81   ANS(Compute("1/2")->cmp);
82
83 or
84
85   $b = Compute("1/2"); 86 ANS($b->cmp);
87
88 There are several options to the cmp() method that control how the
89 answer checker will work.  The first is controls whether unreduced
90 fractions are accepted as correct.  Unreduced fractions are allowed in
91 the Fraction and Fraction-NoDecimals contexts, but not in the
92 LimitedFraction context.  You can control this using the
93 studentsMustReduceFractions option:
94
95   Context("Fraction");
96   ANS(Compute("1/2")->cmp(studentsMustReduceFractions=>1));
97
98 or
99
100   Context("LimitedFraction");
101   ANS(Compute("1/2")->cmp(studentsMustReduceFractions=>0));
102
103 The second controls whether warnings are issued when students don't
104 reduce their answers, or to mark the answer incorrect silently.  This
105 is specified by the showFractionReductionWarnings option.  The default
106 is to report the warnings, but this option has an effect only when
107 studentsMustReduceFractions is 1, and so only in the LimitedFraction
108 context.  For example,
109
110   Context("LimitedFraction");
111   ANS(Compute("1/2")->cmp(showFractionReductionWarnings=>0));
112
113 turns off these warnings.
114
115 The final option, requireFraction, specifies whether a fraction MUST
116 be entered (e.g. one would have to enter 2/1 for a whole number).  The
117 default is 0.
118
119 In addition to these options for cmp(), there are Context flags that
120 control how fractions are handled.  These include the following.
121
122 =over
123
124 =item S<C<< reduceFractions >>>
125
126 This determines whether fractions are reduced automatically when they
127 are created.  The default is to reduce fractions (except when
128 studentsMustReduceFractions is set), so Compute("4/6") would produce
129 the fraction 2/3.  To leave fractions unreduced, set
130 reduceFractions=>0.  The LimitedFraction context has
131 studentsMustReduceFractions set, so reduceFractions is unset
132 automatically for students, but not for correct answers, so
133 Fraction(2,4) would still produce 1/2, even though 2/4 would not be
134 allowed in a student answer.
135
136 =item S<C<< strictFractions >>>
137
138 This determines whether division is allowed only between integers or
139 not.  If you want to prevent division from accepting non-integers,
140 then set strictFractions=>1 (and also strictMinus=>1 and
141 strictMultiplication=>1).  These are all three 0 by default in the
142 Fraction and Fraction-NoDecimals contexts, but 1 in LimitedFraction.
143
144 =item S<C<< allowProperFractions >>>
145
146 This determines whether a space between a whole number and a fraction
147 is interpretted as implicit multiplication (as it usually would be in
148 WeBWorK), or as addition, allowing "4 1/2" to mean "4 and 1/2".  By
149 default, it acts as multiplication in the Fraction and
150 Fraction-NoDecimals contexts, and as addition in LimitedFraction.  If
151 you set allowProperFractions=>1 you should also set reduceConstants=>0.
152
153 =item S<C<< requireProperFractions >>>
154
155 This determines whether fractions MUST be entered as proper fractions.
156 It is 0 by default, meaning improper fractions are allowed.  When set,
157 you will not be able to enter 5/2 as a fraction, but must use "2 1/2".
158 Set it to 1 only when you also set allowProperFractions, or you will
159 not be able to specify fractions bigger than one.  It is off by
160 default in all three contexts.
161
162 =item S<C<< showProperFractions >>>
163
164 This controls whether fractions are displayed as proper fractions or
165 not.  When set, 5/2 will be displayed as 2 1/2 in the answer preview
166 area, otherwise it will be displayed as 5/2.  This flag is 0 by
167 default in the Fraction and Fraction-NoDecimals contexts, and 1 in
168 LimitedFraction.
169
170 =back
171
172 Fraction objects have two methods that can be useful when
173 reduceFractions is set to 0.  The reduce() method will reduce a
174 fraction to lowest terms, and the isReduced() method returns true when
175 the fraction is reduced and false otherwise.
176
177 If you wish to convert a fraction to its numeric (real number) form,
178 use the Real() constructor to coerce it to a real.  E.g.,
179
180   $a = Compute("1/2"); 181$r = Real($a); 182 183 would set$r to the value 0.5.  Similarly, use Fraction() to convert a
184 real number to (an approximating) fraction.  E.g.,
185
186   $r = Real(.5); 187$a = Fraction($r); 188 189 would set$a to be 1/2.  The fraction produced is good to about 6
190 decimal places, so it can't be used for numbers that are too small.
191
192 A side-effect of using the Fraction context is that fractions can be
193 used to take powers of negative numbers when the reduced form of the
194 fraction has an odd denominator.  Thus (-8)^(1/3) will produce -2 as a
195 result, while in the standard Numeric context it would produce an
196 error.
197
198 =cut
199
200 sub _contextFraction_init {context::Fraction::Init()};
201
202 ###########################################################################
203
204 package context::Fraction;
205
206 #
207 #  Initialize the contexts and make the creator function.
208 #
209 sub Init {
210   my $context =$main::context{Fraction} = Parser::Context->getCopy("Numeric");
211   $context->{name} = "Fraction"; 212$context->{pattern}{signedNumber} .= '|-?\d+/\d+';
213   $context->operators->set( 214 "/" => {class => "context::Fraction::BOP::divide"}, 215 "//" => {class => "context::Fraction::BOP::divide"}, 216 "/ " => {class => "context::Fraction::BOP::divide"}, 217 " /" => {class => "context::Fraction::BOP::divide"}, 218 "u-" => {class => "context::Fraction::UOP::minus"}, 219 " " => {precedence => 2.8, string => ' *'}, 220 " *" => {class => "context::Fraction::BOP::multiply", precedence => 2.8}, 221 # precedence is lower to get proper parens in string() and TeX() calls 222 " " => {precedence => 2.7, associativity => 'left', type => 'bin', string => ' ', 223 class => 'context::Fraction::BOP::multiply', TeX => [' ',' '], hidden => 1}, 224 ); 225$context->flags->set(
226     reduceFractions => 1,
227     strictFractions => 0, strictMinus => 0, strictMultiplication => 0,
228     allowProperFractions => 0,  # also set reduceConstants => 0 if you change this
229     requireProperFractions => 0,
230     showProperFractions => 0,
231   );
232   $context->reduction->set('a/b' => 1,'a b/c' => 1, '0 a/b' => 1); 233$context->{value}{Fraction} = "context::Fraction::Fraction";
234   $context->{value}{Real} = "context::Fraction::Real"; 235$context->{parser}{Value} = "context::Fraction::Value";
236   $context->{parser}{Number} = "Parser::Legacy::LimitedNumeric::Number"; 237 238$context = $main::context{'Fraction-NoDecimals'} =$context->copy;
239   $context->{name} = "Fraction-NoDecimals"; 240 Parser::Number::NoDecimals($context);
241
242   $context =$main::context{LimitedFraction} = $context->copy; 243$context->{name} = "LimitedFraction";
244   $context->operators->undefine( 245 '+', '-', '*', '* ', '^', '**', 246 'U', '.', '><', 'u+', '!', '_', ',', 247 ); 248$context->parens->undefine('|','{','[');
249   $context->functions->disable('All'); 250$context->flags->set(
251     strictFractions => 1, strictMinus => 1, strictMultiplication => 1,
252     allowProperFractions => 1, reduceConstants => 0,
253     showProperFractions => 1,
254   );
255   $context->{cmpDefaults}{Fraction} = {studentsMustReduceFractions => 1}; 256 257 main::PG_restricted_eval('sub Fraction {Value->Package("Fraction()")->new(@_)};'); 258 } 259 260 # 261 # Convert a real to a reduced fraction approximation 262 # 263 sub toFraction { 264 my$context = shift; my $x = shift; 265 my$Real = $context->Package("Real"); 266 my$d = 1000000;
267   my ($a,$b) = reduce(int($x*$d),$d); 268 return [$Real->make($a),$Real->make($b)]; 269 } 270 271 # 272 # Greatest Common Divisor 273 # 274 sub gcd { 275 my$a = abs(shift); my $b = abs(shift); 276 ($a,$b) = ($b,$a) if$a < $b; 277 return$a if $b == 0; 278 my$r = $a %$b;
279   while ($r != 0) { 280 ($a,$b) = ($b,$r); 281$r = $a %$b;
282   }
283   return $b; 284 } 285 286 # 287 # Least Common Multiple 288 # 289 sub lcm { 290 my ($a,$b) = @_; 291 return ($a/gcd($a,$b))*$b; 292 } 293 294 295 # 296 # Reduced fraction 297 # 298 sub reduce { 299 my$a = shift; my $b = shift; 300 ($a,$b) = (-$a,-$b) if$b < 0;
301   my $gcd = gcd($a,$b); 302 return ($a/$gcd,$b/$gcd); 303 } 304 305 ########################################################################### 306 307 package context::Fraction::BOP::divide; 308 our @ISA = ('Parser::BOP::divide'); 309 310 # 311 # Create a Fraction or Real from the given data 312 # 313 sub _eval { 314 my$self = shift; my $context =$self->{equation}{context};
315   return $_[0]/$_[1] if Value::isValue($_[0]) || Value::isValue($_[1]);
316   my $n =$context->Package("Fraction")->make($context,@_); 317$n->{isHorizontal} = 1 if $self->{def}{noFrac}; 318 return$n;
319 }
320
321 #
322 #  When strictFraction is in effect, only allow division
323 #  with integers and negative integers
324 #
325 sub _check {
326   my $self = shift; 327$self->SUPER::_check;
328   return unless $self->context->flag("strictFractions"); 329$self->Error("The numerator of a fraction must be an integer")
330     unless $self->{lop}->class =~ /INTEGER|MINUS/; 331$self->Error("The denominator of a fraction must be a (non-negative) integer")
332     unless $self->{rop}->class eq 'INTEGER'; 333$self->Error("The numerator must be less than the denominator in a proper fraction")
334    if $self->context->flag("requireProperFractions") && CORE::abs($self->{lop}->eval) >= CORE::abs($self->{rop}->eval); 335 } 336 337 # 338 # Reduce the fraction, if it is one, otherwise do the usual reduce 339 # 340 sub reduce { 341 my$self = shift;
342   return $self->SUPER::reduce unless$self->class eq 'FRACTION';
343   my $reduce =$self->{equation}{context}{reduction};
344   return $self->{lop} if$self->{rop}{isOne} && $reduce->{'x/1'}; 345$self->Error("Division by zero"), return $self if$self->{rop}{isZero};
346   return $self->{lop} if$self->{lop}{isZero} && $reduce->{'0/x'}; 347 if ($reduce->{'a/b'}) {
348     my ($a,$b) = context::Fraction::reduce($self->{lop}->eval,$self->{rop}->eval);
349     if ($self->{lop}->class eq 'INTEGER') {$self->{lop}{value} = $a} else {$self->{lop}{op}{value} = -$a} 350$self->{rop}{value} = $b; 351 } 352 return$self;
353 }
354
355 #
356 #  Display minus signs outside the fraction
357 #
358 sub TeX {
359   my $self = shift; my$bop = $self->{def}; 360 return$self->SUPER::TeX(@_) if $self->class ne 'FRACTION' ||$bop->{noFrac};
361   my ($precedence,$showparens,$position,$outerRight) = @_;
362   $showparens = '' unless defined($showparens);
363   my $addparens = 364 defined($precedence) &&
365       ($showparens eq 'all' || ($precedence > $bop->{precedence} &&$showparens ne 'nofractions') ||
366       ($precedence ==$bop->{precedence} && ($bop->{associativity} eq 'right' ||$showparens eq 'same')));
367
368   my $TeX =$self->eval->TeX;
369   $TeX = '\left('.$TeX.'\right)' if ($addparens); 370 return$TeX;
371 }
372
373 #
374 #  Indicate if the value is a fraction or not
375 #
376 sub class {
377   my $self = shift; 378 return "FRACTION" if$self->{lop}->class =~ /INTEGER|MINUS/ &&
379                        $self->{rop}->class eq 'INTEGER'; 380 return$self->SUPER::class;
381 }
382
383 ###########################################################################
384
385 package context::Fraction::BOP::multiply;
386 our @ISA = ('Parser::BOP::multiply');
387
388 #
389 #  For proper fractions, add the integer to the fraction
390 #
391 sub _eval {
392   my ($self,$a,$b)= @_; 393 return ($a > 0 ? $a +$b : $a -$b);
394 }
395
396 #
397 #  If the implied multiplication represents a proper fraction with a
398 #  preceeding integer, then switch to the proper fraction operator
399 #  (for proper handling of string() and TeX() calls), otherwise,
400 #  convert the object to a standard multiplication.
401 #
402 sub _check {
403   my $self = shift; 404$self->SUPER::_check;
405   my $isFraction = 0; 406 if ($self->context->flag("allowProperFractions")) {
407     $isFraction = ($self->{lop}->class =~ /INTEGER|MINUS/ && !$self->{lop}{hadParens} && 408$self->{rop}->class eq 'FRACTION' && !$self->{rop}{hadParens} && 409$self->{rop}->eval >= 0);
410   }
411   if ($isFraction) { 412$self->{bop} = "  ";
413     $self->{def} =$self->context->{operators}{$self->{bop}}; 414 if ($self->{lop}->class eq 'MINUS') {
415       #
416       #  Hack to replace BOP with unary negation of BOP.
417       #  (When check() is changed to accept a return value,
418       #   this will not be necessary.)
419       #
420       my $copy = bless {%$self}, ref($self);$copy->{lop} = $copy->{lop}{op}; 421 my$neg = $self->Item("UOP")->new($self->{equation},"u-",$copy); 422 map {delete$self->{$_}} (keys %$self);
423       map {$self->{$_} = $neg->{$_}} (keys %$neg); 424 bless$self, ref($neg); 425 } 426 } else { 427$self->Error("Can't use implied multiplication in this context",$self->{bop}) 428 if$self->context->flag("strictMultiplication");
429     bless $self,$ISA[0];
430   }
431 }
432
433 #
434 #  Reduce the fraction
435 #
436 sub reduce {
437   my $self = shift; 438 my$reduce = $self->{equation}{context}{reduction}; 439 my ($a,($b,$c)) = (CORE::abs($self->{lop}->eval),$self->{rop}->eval->value);
440   if ($reduce->{'a b/c'}) { 441 ($b,$c) = context::Fraction::reduce($b,$c) if$reduce->{'a/b'};
442     $a += int($b/$c);$b = $b %$c;
443     $self->{lop}{value} =$a;
444     $self->{rop}{lop}{value} =$b;
445     $self->{rop}{rop}{value} =$c;
446     return $self->{lop} if$b == 0 || $c == 1; 447 } 448 return$self->{rop} if $a == 0 &&$reduce->{'0 a/b'};
449   return $self; 450 } 451 452 ########################################################################### 453 454 package context::Fraction::UOP::minus; 455 our @ISA = ('Parser::UOP::minus'); 456 457 # 458 # For strict fractions, only allow minus on certain operands 459 # 460 sub _check { 461 my$self = shift;
462   $self->SUPER::_check; 463$self->{hadParens} = 1 if $self->{op}{hadParens}; 464 return unless$self->context->flag("strictMinus");
465   my $uop =$self->{def}{string} || $self->{uop}; 466$self->Error("You can only use '%s' with (non-negative) numbers",$uop) 467 unless$self->{op}->class =~ /Number|INTEGER|FRACTION/;
468 }
469
470 #
471 #  class is MINUS if it is a negative number
472 #
473 sub class {
474   my $self = shift; 475 return "MINUS" if$self->{op}->class =~ /Number|INTEGER/;
476   $self->SUPER::class; 477 } 478 479 ########################################################################### 480 481 package context::Fraction::Value; 482 our @ISA = ('Parser::Value'); 483 484 # 485 # Indicate if the Value object is a fraction or not 486 # 487 sub class { 488 my$self = shift;
489   return "FRACTION" if $self->{value}->classMatch('Fraction'); 490 return$self->SUPER::class;
491 }
492
493 ###########################################################################
494
495 package context::Fraction::Real;
496 our @ISA = ('Value::Real');
497
498 #
499 #  Allow Real to convert Fractions to Reals
500 #
501 sub new {
502   my $self = shift; my$context = (Value::isContext($_[0]) ? shift :$self->context);
503   my $x = shift; 504$x = $context->Package("Formula")->new($context,$x)->eval if ref($x) eq "" && $x =~ m!/!; 505$x = $x->eval if scalar(@_) == 0 && Value::classMatch($x,'Fraction');
506   $self->SUPER::new($context,$x,@_); 507 } 508 509 # 510 # Since the signed number pattern now include fractions, we need to make sure 511 # we handle them when a real is made and it looks like a fraction 512 # 513 sub make { 514 my$self = shift; my $context = (Value::isContext($_[0]) ? shift : $self->context); 515 my$x = shift;
516   $x =$context->Package("Formula")->new($context,$x)->eval if ref($x) eq "" &&$x =~ m!/!;
517   $x =$x->eval if scalar(@_) == 0 && Value::classMatch($x,'Fraction'); 518$self->SUPER::make($context,$x,@_);
519 }
520
521 ###########################################################################
522 ###########################################################################
523 #
524 #  Implements the MathObject for fractions
525 #
526
527 package context::Fraction::Fraction;
528 our @ISA = ('Value');
529
530 sub new {
531   my $self = shift; my$class = ref($self) ||$self;
532   my $context = (Value::isContext($_[0]) ? shift : $self->context); 533 my$x = shift; $x = [$x,@_] if scalar(@_) > 0;
534   return $x->inContext($context) if Value::classMatch($x,'Fraction'); 535$x = [$x] unless ref($x) eq 'ARRAY'; $x->[1] = 1 if scalar(@{$x}) == 1;
536   Value::Error("Can't convert ARRAY of length %d to %s",scalar(@{$x}),Value::showClass($self))
537     unless (scalar(@{$x}) == 2); 538$x->[0] = Value::makeValue($x->[0],context=>$context);
539   $x->[1] = Value::makeValue($x->[1],context=>$context); 540 return$x->[0] if Value::classMatch($x->[0],'Fraction') && scalar(@_) == 0; 541$x = context::Fraction::toFraction($context,$x->[0]->value) if Value::isReal($x->[0]) && scalar(@_) == 0; 542 return$self->formula($x) if Value::isFormula($x->[0]) || Value::isFormula($x->[1]); 543 Value::Error("Fraction numerators must be integers") unless isInteger($x->[0]);
544   Value::Error("Fraction denominators must be integers") unless isInteger($x->[1]); 545 my ($a,$b) = ($x->[0]->value,$x->[1]->value); ($a,$b) = (-$a,-$b) if$b < 0;
546   Value::Error("Denominator can't be zero") if $b == 0; 547 ($a,$b) = context::Fraction::reduce($a,$b) if$context->flag("reduceFractions");
548   bless {data => [$a,$b], context => $context},$class;
549 }
550
551 #
552 #  Produce a real if one of the terms is not an integer
553 #  otherwise produce a fraction.
554 #
555 sub make {
556   my $self = shift; my$class = ref($self) ||$self;
557   my $context = (Value::isContext($_[0]) ? shift : $self->context); 558 push(@_,0) if scalar(@_) == 0; push(@_,1) if scalar(@_) == 1; 559 my ($a,$b) = @_; ($a,$b) = (-$a,-$b) if$b < 0;
560   return $context->Package("Real")->make($context,$a/$b) unless isInteger($a) && isInteger($b);
561   ($a,$b) = context::Fraction::reduce($a,$b) if $context->flag("reduceFractions"); 562 bless {data => [$a,$b], context =>$context}, $class; 563 } 564 565 # 566 # Promote to a fraction, allowing reals to be$x/1 even when
567 #  not an integer (later $self->make() will produce a Real in 568 # that case) 569 # 570 sub promote { 571 my$self = shift; my $class = ref($self) || $self; 572 my$context = (Value::isContext($_[0]) ? shift :$self->context);
573   my $x = (scalar(@_) ? shift :$self);
574   if (scalar(@_) == 0) {
575     return $x->inContext($context) if ref($x) eq$class;
576     return (bless {data => [$x->value,1], context =>$context}, $class) if Value::isReal($x);
577     return (bless {data => [$x,1], context =>$context}, $class) if Value::matchNumber($x);
578   }
579   return $self->new($context,$x,@_); 580 } 581 582 583 # 584 # Create a new formula from the number 585 # 586 sub formula { 587 my$self = shift; my $value = shift; 588 my$formula = $self->Package("Formula")->blank($self->context);
589   my ($l,$r) = Value::toFormula($formula,@{$value});
590   $formula->{tree} =$formula->Item("BOP")->new($formula,'/',$l,$r); 591 return$formula;
592 }
593
594 #
595 #  Return the real number type
596 #
597 sub typeRef {return $Value::Type{number}} 598 sub length {2} 599 600 sub isZero {(shift)->{data}[0] == 0} 601 sub isOne {(shift)->eval == 1} 602 603 # 604 # Return the real value 605 # 606 sub eval { 607 my$self = shift;
608   my ($a,$b) = $self->value; 609 return$a/$b; 610 } 611 612 # 613 # parts are not Value objects, so don't transfer 614 # 615 sub transferFlags {} 616 617 # 618 # Check if a value is an integer 619 # 620 sub isInteger { 621 my$n = shift;
622   $n =$n->value if Value::isReal($n); 623 return$n =~ m/^-?\d+$/; 624 }; 625 626 627 ################################################## 628 # 629 # Binary operations 630 # 631 632 sub add { 633 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 634 my (($a,$b),($c,$d)) = ($l->value,$r->value); 635 my$M = context::Fraction::lcm($b,$d);
636   return $self->inherit($other)->make($a*($M/$b)+$c*($M/$d),$M); 637 } 638 639 sub sub { 640 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 641 my (($a,$b),($c,$d)) = ($l->value,$r->value); 642 my$M = context::Fraction::lcm($b,$d);
643   return $self->inherit($other)->make($a*($M/$b)-$c*($M/$d),$M); 644 } 645 646 sub mult { 647 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 648 my (($a,$b),($c,$d)) = ($l->value,$r->value); 649 return$self->inherit($other)->make($a*$c,$b*$d); 650 } 651 652 sub div { 653 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 654 my (($a,$b),($c,$d)) = ($l->value,$r->value); 655 Value::Error("Division by zero") if$c == 0;
656   return $self->inherit($other)->make($a*$d,$b*$c);
657 }
658
659 sub power {
660   my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_);
661   my (($a,$b),($c,$d)) = ($l->value,$r->reduce->value);
662   ($a,$b,$c) = ($b,$a,-$c) if $c < 0; 663 my ($x,$y) = ($c == 1 ? ($a,$b) : ($a**$c,$b**$c));
664   if ($d != 1) { 665 if ($x < 0 && $d % 2 == 1) {$x = -(-$x)**(1/$d)} else {$x =$x**(1/$d)}; 666 if ($y < 0 && $d % 2 == 1) {$y = -(-$y)**(1/$d)} else {$y =$y**(1/$d)}; 667 } 668 return$self->inherit($other)->make($x,$y) unless$x eq 'nan' || $y eq 'nan'; 669 Value::Error("Can't raise a negative number to a power") if$a*$b < 0; 670 Value::Error("Result of exponention is not a number"); 671 } 672 673 sub compare { 674 my ($self,$l,$r) = Value::checkOpOrderWithPromote(@_);
675   return $l->eval <=>$r->eval;
676 }
677
678 ##################################################
679 #
680 #   Numeric functions
681 #
682
683 sub abs  {my $self = shift;$self->make(CORE::abs($self->{data}[0]),CORE::abs($self->{data}[1]))}
684 sub neg  {my $self = shift;$self->make(-($self->{data}[0]),$self->{data}[1])}
685 sub exp  {my $self = shift;$self->make(CORE::exp($self->eval))} 686 sub log {my$self = shift; $self->make(CORE::log($self->eval))}
687 sub sqrt {my $self = shift;$self->make(CORE::sqrt($self->{data}[0]),CORE::sqrt($self->{data}[1]))}
688
689 ##################################################
690 #
691 #   Trig functions
692 #
693
694 sub sin {my $self = shift;$self->make(CORE::sin($self->eval))} 695 sub cos {my$self = shift; $self->make(CORE::cos($self->eval))}
696
697 sub atan2 {
698   my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_);
699   return $self->inherit($other)->make(CORE::atan2($l->eval,$r->eval));
700 }
701
702 ##################################################
703 #
704 #  Utility
705 #
706
707 sub reduce {
708   my $self = shift; 709 my ($a,$b) = context::Fraction::reduce($self->value);
710   return $self->make($a,$b); 711 } 712 713 sub isReduced { 714 my$self = shift;
715   my (($a,$b),($c,$d)) = ($self->value,$self->reduce->value);
716   return $a ==$c && $b ==$d;
717 }
718
719 ##################################################
720 #
721 #  Formatting
722 #
723
724 sub string {
725   my $self = shift; my$equation = shift; my $prec = shift; 726 my ($a,$b) = @{$self->{data}}; my $n = ""; 727 return$a if $b == 1; 728 if ($self->getFlag("showProperFractions") && CORE::abs($a) >$b)
729     {$n = int($a/$b);$a = CORE::abs($a) %$b; $n .= " " unless$a == 0}
730   $n .= "$a/$b" unless$a == 0 && $n ne ''; 731$n = "($n)" if defined$prec && $prec >= 1; 732 return$n;
733 }
734
735 sub TeX {
736   my $self = shift; my$equation = shift; my $prec = shift; 737 my ($a,$b) = @{$self->{data}}; my $n = ""; 738 return$a if $b == 1; 739 if ($self->getFlag("showProperFractions") && CORE::abs($a) >$b)
740     {$n = int($a/$b);$a = CORE::abs($a) %$b; $n .= " " unless$a == 0}
741   my $s = ""; ($a,$s) = (-$a,"-") if $a < 0; 742$n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{\\textstyle\\frac{$a}{$b}}") 743 unless$a == 0 && $n ne ''; 744$n = "\\left($n\\right)" if defined$prec && $prec >= 1; 745 return$n;
746 }
747
748 ###########################################################################
749 #
751 #
752
753 sub cmp_defaults {(
754   shift->SUPER::cmp_defaults(@_),
755   ignoreInfinity => 1,
756   studentsMustReduceFractions => 0,
757   showFractionReduceWarnings => 1,
758   requireFraction => 0,
759 )}
760
761 sub cmp_contextFlags {
762   my $self = shift; my$ans = shift;
763   return (
764     $self->SUPER::cmp_contextFlags($ans),
765     reduceFractions => !$ans->{studentsMustReduceFractions}, 766 ); 767 } 768 769 sub cmp_class {"a fraction of integers"} 770 771 sub typeMatch { 772 my$self = shift; my $other = shift; my$ans = shift;
773   return 1 unless ref($other); 774 return 0 if Value::isFormula($other);
775   return 1 if $other->type eq 'Infinity' &&$ans->{ignoreInfinity};
776   return 0 if $ans->{requireFraction} && !$other->classMatch("Fraction");
777   $self->type eq$other->type;
778 }
779
780 sub cmp_postprocess {
781   my $self = shift; my$ans = shift;
782   my $student =$ans->{student_value};
783   return if $ans->{isPreview} || 784 !$ans->{studentsMustReduceFractions} ||
785       !Value::classMatch($student,'Fraction') || 786$student->isReduced;
787   $ans->score(0); 788$self->cmp_Error($ans,"Your fraction is not reduced") if$ans->{showFractionReduceWarnings};
789 }
790
791 ###########################################################################
792
793 1;