[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 6208 - (download) (as text) (annotate)
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 
    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<< 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;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9