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

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

Mon Jan 19 18:50:45 2009 UTC (11 years ago) by dpvc
File size: 25753 byte(s)
Fixed some typos in the comments.


    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$class = ref($self) ||$self;
503   my $context = (Value::isContext($_[0]) ? shift : $self->context); 504 my$x = shift; $x =$x->eval if scalar(@_) == 0 && Value::classMatch($x,'Fraction'); 505$self->SUPER::new($context,$x,@_);
506 }
507
508 ###########################################################################
509 ###########################################################################
510 #
511 #  Implements the MathObject for fractions
512 #
513
514 package context::Fraction::Fraction;
515 our @ISA = ('Value');
516
517 sub new {
518   my $self = shift; my$class = ref($self) ||$self;
519   my $context = (Value::isContext($_[0]) ? shift : $self->context); 520 my$x = shift; $x = [$x,@_] if scalar(@_) > 0;
521   return $x->inContext($context) if Value::classMatch($x,'Fraction'); 522$x = [$x] unless ref($x) eq 'ARRAY'; $x->[1] = 1 if scalar(@{$x}) == 1;
523   Value::Error("Can't convert ARRAY of length %d to %s",scalar(@{$x}),Value::showClass($self))
524     unless (scalar(@{$x}) == 2); 525$x->[0] = Value::makeValue($x->[0],context=>$context);
526   $x->[1] = Value::makeValue($x->[1],context=>$context); 527 return$x->[0] if Value::classMatch($x->[0],'Fraction') && scalar(@_) == 0; 528$x = context::Fraction::toFraction($context,$x->[0]->value) if Value::isReal($x->[0]) && scalar(@_) == 0; 529 return$self->formula($x) if Value::isFormula($x->[0]) || Value::isFormula($x->[1]); 530 Value::Error("Fraction numerators must be integers") unless isInteger($x->[0]);
531   Value::Error("Fraction denominators must be integers") unless isInteger($x->[1]); 532 my ($a,$b) = ($x->[0]->value,$x->[1]->value); ($a,$b) = (-$a,-$b) if$b < 0;
533   Value::Error("Denominator can't be zero") if $b == 0; 534 ($a,$b) = context::Fraction::reduce($a,$b) if$context->flag("reduceFractions");
535   bless {data => [$a,$b], context => $context},$class;
536 }
537
538 #
539 #  Produce a real if one of the terms is not an integer
540 #  otherwise produce a fraction.
541 #
542 sub make {
543   my $self = shift; my$class = ref($self) ||$self;
544   my $context = (Value::isContext($_[0]) ? shift : $self->context); 545 push(@_,0) if scalar(@_) == 0; push(@_,1) if scalar(@_) == 1; 546 my ($a,$b) = @_; ($a,$b) = (-$a,-$b) if$b < 0;
547   return $context->Package("Real")->make($context,$a/$b) unless isInteger($a) && isInteger($b);
548   ($a,$b) = context::Fraction::reduce($a,$b) if $context->flag("reduceFractions"); 549 bless {data => [$a,$b], context =>$context}, $class; 550 } 551 552 # 553 # Promote to a fraction, allowing reals to be$x/1 even when
554 #  not an integer (later $self->make() will produce a Real in 555 # that case) 556 # 557 sub promote { 558 my$self = shift; my $class = ref($self) || $self; 559 my$context = (Value::isContext($_[0]) ? shift :$self->context);
560   my $x = (scalar(@_) ? shift :$self);
561   if (scalar(@_) == 0) {
562     return $x->inContext($context) if ref($x) eq$class;
563     return (bless {data => [$x->value,1], context =>$context}, $class) if Value::isReal($x);
564     return (bless {data => [$x,1], context =>$context}, $class) if Value::matchNumber($x);
565   }
566   return $self->new($context,$x,@_); 567 } 568 569 570 # 571 # Create a new formula from the number 572 # 573 sub formula { 574 my$self = shift; my $value = shift; 575 my$formula = $self->Package("Formula")->blank($self->context);
576   my ($l,$r) = Value::toFormula($formula,@{$value});
577   $formula->{tree} =$formula->Item("BOP")->new($formula,'/',$l,$r); 578 return$formula;
579 }
580
581 #
582 #  Return the real number type
583 #
584 sub typeRef {return $Value::Type{number}} 585 sub length {2} 586 587 sub isZero {(shift)->{data}[0] == 0} 588 sub isOne {(shift)->eval == 1} 589 590 # 591 # Return the real value 592 # 593 sub eval { 594 my$self = shift;
595   my ($a,$b) = $self->value; 596 return$a/$b; 597 } 598 599 # 600 # parts are not Value objects, so don't transfer 601 # 602 sub transferFlags {} 603 604 # 605 # Check if a value is an integer 606 # 607 sub isInteger { 608 my$n = shift;
609   $n =$n->value if Value::isReal($n); 610 return$n =~ m/^-?\d+$/; 611 }; 612 613 614 ################################################## 615 # 616 # Binary operations 617 # 618 619 sub add { 620 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 621 my (($a,$b),($c,$d)) = ($l->value,$r->value); 622 my$M = context::Fraction::lcm($b,$d);
623   return $self->inherit($other)->make($a*($M/$b)+$c*($M/$d),$M); 624 } 625 626 sub sub { 627 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 628 my (($a,$b),($c,$d)) = ($l->value,$r->value); 629 my$M = context::Fraction::lcm($b,$d);
630   return $self->inherit($other)->make($a*($M/$b)-$c*($M/$d),$M); 631 } 632 633 sub mult { 634 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 635 my (($a,$b),($c,$d)) = ($l->value,$r->value); 636 return$self->inherit($other)->make($a*$c,$b*$d); 637 } 638 639 sub div { 640 my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); 641 my (($a,$b),($c,$d)) = ($l->value,$r->value); 642 Value::Error("Division by zero") if$c == 0;
643   return $self->inherit($other)->make($a*$d,$b*$c);
644 }
645
646 sub power {
647   my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_);
648   my (($a,$b),($c,$d)) = ($l->value,$r->reduce->value);
649   ($a,$b,$c) = ($b,$a,-$c) if $c < 0; 650 my ($x,$y) = ($c == 1 ? ($a,$b) : ($a**$c,$b**$c));
651   if ($d != 1) { 652 if ($x < 0 && $d % 2 == 1) {$x = -(-$x)**(1/$d)} else {$x =$x**(1/$d)}; 653 if ($y < 0 && $d % 2 == 1) {$y = -(-$y)**(1/$d)} else {$y =$y**(1/$d)}; 654 } 655 return$self->inherit($other)->make($x,$y) unless$x eq 'nan' || $y eq 'nan'; 656 Value::Error("Can't raise a negative number to a power") if$a*$b < 0; 657 Value::Error("Result of exponention is not a number"); 658 } 659 660 sub compare { 661 my ($self,$l,$r) = Value::checkOpOrderWithPromote(@_);
662   return $l->eval <=>$r->eval;
663 }
664
665 ##################################################
666 #
667 #   Numeric functions
668 #
669
670 sub abs  {my $self = shift;$self->make(CORE::abs($self->{data}[0]),CORE::abs($self->{data}[1]))}
671 sub neg  {my $self = shift;$self->make(-($self->{data}[0]),$self->{data}[1])}
672 sub exp  {my $self = shift;$self->make(CORE::exp($self->eval))} 673 sub log {my$self = shift; $self->make(CORE::log($self->eval))}
674 sub sqrt {my $self = shift;$self->make(CORE::sqrt($self->{data}[0]),CORE::sqrt($self->{data}[1]))}
675
676 ##################################################
677 #
678 #   Trig functions
679 #
680
681 sub sin {my $self = shift;$self->make(CORE::sin($self->eval))} 682 sub cos {my$self = shift; $self->make(CORE::cos($self->eval))}
683
684 sub atan2 {
685   my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_);
686   return $self->inherit($other)->make(CORE::atan2($l->eval,$r->eval));
687 }
688
689 ##################################################
690 #
691 #  Utility
692 #
693
694 sub reduce {
695   my $self = shift; 696 my ($a,$b) = context::Fraction::reduce($self->value);
697   return $self->make($a,$b); 698 } 699 700 sub isReduced { 701 my$self = shift;
702   my (($a,$b),($c,$d)) = ($self->value,$self->reduce->value);
703   return $a ==$c && $b ==$d;
704 }
705
706 ##################################################
707 #
708 #  Formatting
709 #
710
711 sub string {
712   my $self = shift; my$equation = shift; my $prec = shift; 713 my ($a,$b) = @{$self->{data}}; my $n = ""; 714 return$a if $b == 1; 715 if ($self->getFlag("showProperFractions") && CORE::abs($a) >$b)
716     {$n = int($a/$b);$a = CORE::abs($a) %$b; $n .= " " unless$a == 0}
717   $n .= "$a/$b" unless$a == 0 && $n ne ''; 718$n = "($n)" if defined$prec && $prec >= 1; 719 return$n;
720 }
721
722 sub TeX {
723   my $self = shift; my$equation = shift; my $prec = shift; 724 my ($a,$b) = @{$self->{data}}; my $n = ""; 725 return$a if $b == 1; 726 if ($self->getFlag("showProperFractions") && CORE::abs($a) >$b)
727     {$n = int($a/$b);$a = CORE::abs($a) %$b; $n .= " " unless$a == 0}
728   my $s = ""; ($a,$s) = (-$a,"-") if $a < 0; 729$n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{\\textstyle\\frac{$a}{$b}}") 730 unless$a == 0 && $n ne ''; 731$n = "\\left($n\\right)" if defined$prec && $prec >= 1; 732 return$n;
733 }
734
735 ###########################################################################
736 #
738 #
739
740 sub cmp_defaults {(
741   shift->SUPER::cmp_defaults(@_),
742   ignoreInfinity => 1,
743   studentsMustReduceFractions => 0,
744   showFractionReduceWarnings => 1,
745   requireFraction => 0,
746 )}
747
748 sub cmp_contextFlags {
749   my $self = shift; my$ans = shift;
750   return (
751     $self->SUPER::cmp_contextFlags($ans),
752     reduceFractions => !$ans->{studentsMustReduceFractions}, 753 ); 754 } 755 756 sub cmp_class {"a fraction of integers"} 757 758 sub typeMatch { 759 my$self = shift; my $other = shift; my$ans = shift;
760   return 1 unless ref($other); 761 return 0 if Value::isFormula($other);
762   return 1 if $other->type eq 'Infinity' &&$ans->{ignoreInfinity};
763   return 0 if $ans->{requireFraction} && !$other->classMatch("Fraction");
764   $self->type eq$other->type;
765 }
766
767 sub cmp_postprocess {
768   my $self = shift; my$ans = shift;
769   my $student =$ans->{student_value};
770   return if $ans->{isPreview} || 771 !$ans->{studentsMustReduceFractions} ||
772       !Value::classMatch($student,'Fraction') || 773$student->isReduced;
774   $ans->score(0); 775$self->cmp_Error($ans,"Your fraction is not reduced") if$ans->{showFractionReduceWarnings};
776 }
777
778 ###########################################################################
779
780 1;