[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 6292 - (download) (as text) (annotate)
Tue Jun 8 18:00:53 2010 UTC (9 years, 7 months ago) by mgage
File size: 29224 byte(s)
syncing cvn with changes made to the cvs

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9