[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 6022 - (download) (as text) (annotate)
Tue Mar 10 14:08:50 2009 UTC (10 years, 11 months ago) by dpvc
File size: 26281 byte(s)
Fix the context::Fraction::Real object so that new() and make() will
handle being passed a fraction as a string (since Value::makeValue()
will pass that to context::Fraction::Real->make() when it is given a
fraction as a string).

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9