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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5976 - (download) (as text) (annotate)
Mon Jan 19 18:50:45 2009 UTC (11 years, 1 month 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 
    5 =head1 DESCRIPTION
    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 
   18   loadMacros("contextFraction.pl");
   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 #
  737 #  Answer Checker
  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;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9