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

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

Sat Mar 13 01:09:08 2010 UTC (9 years, 9 months ago) by dpvc
File size: 28245 byte(s)
Fixed a problem with the signed number pattern that cause Real("4*3") to produce 4 rather than 12.


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