From: Mike G. v. a. <we...@ma...> - 2009-06-25 20:54:34
|
Log Message: ----------- merging HEAD into 2-4-patches branch Tags: ---- rel-2-4-patches Modified Files: -------------- pg/macros: PGasu.pl compoundProblem.pl Added Files: ----------- pg/macros: contextFraction.pl contextOrdering.pl problemPanic.pl Revision Data ------------- Index: PGasu.pl =================================================================== RCS file: /webwork/cvs/system/pg/macros/PGasu.pl,v retrieving revision 1.7.2.1 retrieving revision 1.7.2.1.2.1 diff -Lmacros/PGasu.pl -Lmacros/PGasu.pl -u -r1.7.2.1 -r1.7.2.1.2.1 --- macros/PGasu.pl +++ macros/PGasu.pl @@ -118,6 +118,11 @@ This is useful if you want students to report the value of sin(pi/4), but you don't want to allow "sin(pi/4)" as the answer. +A similar effect can be accomplished with Contexts() by undefining +the trig functions. +See http://webwork.maa.org/wiki/Modifying_contexts_%28advanced%29#.282.29_Functions + + =cut # ^function no_trig_fun @@ -203,6 +208,10 @@ Second argument is optional, and tells us whether yes or no Third argument is the error message to produce (if any). +A similar effect can be accomplished with Contexts() by undefining +the trig functions. +See http://webwork.maa.org/wiki/Modifying_contexts_%28advanced%29 + =cut Index: compoundProblem.pl =================================================================== RCS file: /webwork/cvs/system/pg/macros/compoundProblem.pl,v retrieving revision 1.1.2.1 retrieving revision 1.1.2.1.2.1 diff -Lmacros/compoundProblem.pl -Lmacros/compoundProblem.pl -u -r1.1.2.1 -r1.1.2.1.2.1 --- macros/compoundProblem.pl +++ macros/compoundProblem.pl @@ -225,6 +225,9 @@ new_answers => "", # answer labels for THIS part ans_rule_count => 0, # the ans_rule count from previous parts new_ans_rule_count => 0, # the ans_rule count from THIS part + images_created => 0, # the image count from the precious parts + new_images_created => 0, # the image count from THIS part + imageName => "", # name of images_created image file score => 0, # the (weighted) score on this part total => 0, # the total on previous parts raw => 0, # raw score on this part @@ -284,7 +287,7 @@ main::RECORD_FORM_LABEL("_next"); main::RECORD_FORM_LABEL("_status"); $self->{status} = $self->decode; - $self->{isNew} = $main::inputs_ref->{_next} || ($main::inputs_ref->{submitAnswers} && + $self->{isNew} = $main::inputs_ref->{_next} || ($main::inputs_ref->{submitAnswers} && $main::inputs_ref->{submitAnswers} eq ($self->{nextLabel} || "Go on to Next Part")); if ($self->{isNew}) { $self->checkAnswers; @@ -301,6 +304,8 @@ sub initPart { my $self = shift; $main::ans_rule_count = $self->{status}{ans_rule_count}; + $main::images_created{$self->{status}{imageName}} = $self->{status}{images_created} + if $self->{status}{imageName}; main::install_problem_grader(\&compoundProblem::grader); $main::PG_FLAGS{compoundProblem} = $self; $self->initAnswers($self->{status}{answers}); @@ -360,6 +365,7 @@ $status->{answers} .= ';' if $status->{answers}; $status->{answers} .= $status->{new_answers}; $status->{ans_rule_count} = $status->{new_ans_rule_count}; + $status->{images_created} = $status->{new_images_created}; $status->{total} += $status->{score}; $status->{score} = $status->{raw} = 0; $status->{new_answers} = ''; @@ -392,6 +398,10 @@ return {%defaultStatus} unless $status; my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''} @data = split('\\|',join('',@data)); $status = {%defaultStatus}; + if (scalar(@data) == 8) { + # insert imageName, images_created, new_images_created, if missing + splice(@data,2,0,"",0); splice(@data,6,0,0); + } foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)} return $status; } @@ -571,6 +581,10 @@ $status->{raw} = $result->{score}; $status->{score} = $result->{score}*$weight; $status->{new_ans_rule_count} = $main::ans_rule_count; + if (defined(%main::images_created)) { + $status->{imageName} = (keys %main::images_created)[0]; + $status->{new_images_created} = $main::images_created{$status->{imageName}}; + } $status->{new_answers} = join(';',@answers); my $data = quoteHTML($self->encode); @@ -623,3 +637,5 @@ return ($result,$state); } + +1; --- /dev/null +++ macros/problemPanic.pl @@ -0,0 +1,226 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2009 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: pg/macros/problemPanic.pl,v 1.4.2.1 2009/06/25 20:40:39 gage Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +=head1 NAME + +problemPanic.pl - Allow for a PANIC button that gives additional + hints, possibly costing some points. + +=head1 DESCRIPTION + +This file implements a mechanism for you to provide one or more "panic +button" that your students can use to get additional hints, at the +cost of a portion of their score. + +To include the button, use the command Panic::Button command within a +BEGIN_TEXT/END_TEXT block. E.g., + + BEGIN_TEXT + \{Panic::Button(label => "Request a Hint", penalty => .25)\} + (you will lose 25% of your points if you do) + END_TEXT + +When the student presses the hint button, the button will not longer +be available, and the "panic level" will be increased. This sets the +variable $panicked, which you can use to determine whether to include the +hints or not. For example + + if ($panicked) { + BEGIN_TEXT + Hint: You should factor the numerator and cancel + one of the factors with the denominator. + END_TEXT + } + +Note that you can create a "cascade" of hints by including a second +panic button in the hint received from the first button. This will +set $panic to 2 (panic level 2) and you can use that to include the +second hint. + + if ($panicked) { + BEGIN_TEXT + Hint: You should factor the numerator and cancel + one of the factors with the denominator. + $PAR + \{Panic::Button(label => "Another Hint", penalty => .25)\} + (costing an additional 25%) + END_TEXT + + if ($panicked > 1) { + BEGIN_TEXT + Additional Hint: one of the factors is \(x+$a)\). + END_TEXT + } + } + +You can add more buttons in a similar way. You can not have separate +buttons for separate hints that are NOT cascaded, however. (That may +be possible in future versions.) + +The Panic::Button command takes two optional parameters: + +=over + +=item S<C<< label => "text" >>> + +Sets the text to use for the button. The default is "Request a Hint". + +=item S<C<< penalty => percent >>> + +Specifies the number points to lose (as a number from 0 to 1) if this +hint is displayed. When more than one panic button is used, the +penalties are cumulative. That is, two penalties of .25 would produce +a total penalty of .5, so the student would lose half his points if +both hints were given. + +=back + +Once a hint is displayed, the panic button for that hint will no +longer be shown, and the hint will continue to be displayed as the +student submits new answers. + +A professor will be given a "Reset problem hints" checkbox at the +bottom of the problem, and can use that to request that the panic +level be reset back to 0. This also sets the score and the number of +attempts back to 0 as well, so this effectively resets the problem to +its original state. This is intended for use primarily during problem +development, but can be used to allow a student to get full credit for +a problem even after he or she has asked for a hint. + +To allow the grading penalties to work, you must include the command + + Panic::GradeWithPenalty; + +in order to install the panic-button grader. You should do this afer +setting the grader that you want to use for the problem itself, as the +panic grader will use the one that is installed at the time the +Panic::GradWithPenalty command is issued. + +=cut + +sub _problemPanic_init {Panic::Init()} + + +# +# The packge to contain the routines and data for the Panic buttons +# +package Panic; + +my $isTeX = 0; # true in hardcopy mode +my $allowReset = 0; # true if a professor is viewing the problem +my $buttonCount = 0; # number of panic buttons displayed so far +my @penalty = (0); # accummulated penalty values +my $grader; # problem's original grader + +# +# Allow resets if permission level is high enough. +# Look up the panic level and reset it if needed. +# Save the panic level for the next time through. +# +sub Init { + $main::permissionLevel = 0 unless defined $main::permissionLevel; + $allowReset = $main::permissionLevel > $main::PRINT_FILE_NAMES_PERMISSION_LEVEL; + $isTeX = ($main::displayMode eq 'TeX'); + unless ($isTeX) { + $main::panicked = $main::inputs_ref->{_panicked} || 0; + $main::panicked = 0 if $main::inputs_ref->{_panic_reset} && $allowReset; + main::TEXT(qq!<input type="hidden" name="_panicked" id="_panicked" value="$main::panicked" />!); + main::RECORD_FORM_LABEL("_panicked"); + } +} + +# +# Place a panic button on the page, if it's not hardcopy mode and its not at the wrong level. +# You can set the label, the penalty for taking this hint, and the panic level for this button. +# Use submitAnswers if it is before the due date, and checkAnswers otherwise. +# +sub Button { + $buttonCount++; + my %options = ( + label => "Request a Hint", + level => $buttonCount, + penalty => 0, + @_ + ); + my $label = $options{label}; + my $level = $options{level}; + $penalty[$buttonCount] = $penalty[$buttonCount] + $options{penalty}; + $penalty[$buttonCount] = 1 if $penalty[$buttonCount] > 1; + return if $isTeX || $main::panicked >= $level; + my $time = time(); + my $name = ($main::openDate <= $time && $time <= $main::dueDate ? "submitAnswers" : "checkAnswers"); + $value = quoteHTML($value); + return qq!<input type="submit" name="$name" value="$label" onclick="document.getElementById('_panicked').value++">!; +} + +# +# The reset button +# +sub ResetButton { + main::RECORD_FORM_LABEL("_panic_reset"); + return qq!<input type="checkbox" name="_panic_reset"> Reset problem hints!; +} + +# +# Handle HTML in the value +# +sub quoteHTML { + my $string = shift; + $string =~ s/&/\&/g; $string =~ s/"/\"/g; + $string =~ s/>/\>/g; $string =~ s/</\</g; + return $string; +} + +# +# Install the panic grader, saving the original one +# +sub GradeWithPenalty { + $grader = $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader; + main::install_problem_grader(\&Panic::grader); +} + +# +# The grader for the panic levels. +# +sub grader { + # + # Save the old score and call the original grader. + # Compute the penalized score, and save it, if it is better than the old score. + # Reset the values if we are resetting scores. + # + my $oldScore = $_[1]->{recorded_score} || 0; + my ($result,$state) = &{$grader}(@_); + $result->{score} *= 1-$penalty[$main::panicked]; + $state->{recorded_score} = ($result->{score} > $oldScore ? $result->{score} : $oldScore); + $state->{recorded_score} = $state->{num_of_incorrect_ans} = $state->{num_of_correct_ans} = 0 + if $main::inputs_ref->{_panic_reset} && $allowReset; + + # + # Add the problemPanic message and data + # + $result->{type} = "problemPanic ($result->{type})"; + if ($main::panicked) { + $result->{msg} .= '</i><p><b>Note:</b> <i>' if $result->{msg}; + $result->{msg} .= 'Your score was reduced by '.(int($penalty[$main::panicked]*100)).'%' + . ' because you accepted '.($main::panicked == 1 ? 'a hint.' : $main::panicked.' hints.'); + # + # Add the reset checkbox, if needed + # + $result->{msg} .= '<p>'.ResetButton() if $allowReset; + } + + return ($result,$state); +} --- /dev/null +++ macros/contextOrdering.pl @@ -0,0 +1,389 @@ +=head1 NAME + +contextOrdering.pl - Parses ordered lists of letters like "B > A = C > D" + +=head1 DESCRIPTION + +This context provides a structured way to parse and check answers that +are ordered lists of letters, where the letters are separated by +greater-than signs or equal signs. The only operators allowed are > +and =, and the only letters allowed are the ones you specify explicitly. + +To access the context, you must include + + loadMacros("contextOrdering.pl"); + +at the top of your problem file, and then specify the Ordering context: + + Context("Ordering"); + +There are two main ways to use the Ordering context. The first is to +use the Ordering() command to generate your ordering. This command +creates a context in which the proper letters are defined, and returns +a MathObject that represents the ordering you have provided. For +example, + + $ans = Ordering("B > A > C"); + +or + + $ans = Ordering(A => 2, B => 2.5, C => 1); + +would both produce the same ordering. The first form gives the +ordering as the student must type it, and the second gives the +ordering by specifying numeric values for the various letters that +induce the resulting order. Note that equality is determined using +the default tolerances for the Ordering context. You can change these +using commands like the following: + + Context("Ordering"); + Context()->flags->set(tolerance => .01, tolType => 'absolute'); + +If you want to allow lists of orderings, use the Ordering-List context: + + Context("Ordering-List"); + $ans = Ordering("A > B , B = C"); + +Note that each Ordering() call uses its own copy of the current +context. If you need to modify the actual context used, then use the +context() method of the resulting object. + +The second method of generating orderings is to declare the letters +you wish to use explicitly, and then build the Ordering objects using +the standard Compute() method: + + Context("Ordering"); + Letters("A","B","C","D"); + $a = Compute("A > B = C"); + $b = Compute("C > D"); + +Note that in this case, D is still a valid letter that students can +enter in response to an answer checker for $a, and similarly for A and +B with $b. Note also that both $a and $b use the same context, unlike +orderings produced by calls to the Ordering() function. Changes to +the current context WILL affect $a and $b. + +If the ordering contains duplicate letters (e.g., "A > B > A"), then a +warning message will be issued. If not all the letters are used by +the student, then that also produces a warning message. The latter +can be controlled by the showMissingLetterHints flag to the cmp() +method. For example: + + ANS(Ordering("A > B > C")->cmp(showMissingLetterHints => 0)); + +would prevent the message from being issued if the student submitted +just "A > B". + +=cut + +loadMacros("MathObjects.pl"); + +sub _contextOrdering_init {context::Ordering::Init()} + +########################################### +# +# The main Ordering routines +# + +package context::Ordering; + +# +# Here we set up the prototype contexts and define the needed +# functions in the main:: namespace. Some error messages are +# modified to read better for these contexts. +# +sub Init { + my $context = $main::context{Ordering} = Parser::Context->getCopy("Numeric"); + $context->{name} = "Ordering"; + $context->parens->clear(); + $context->variables->clear(); + $context->constants->clear(); + $context->operators->clear(); + $context->functions->clear(); + $context->strings->clear(); + $context->operators->add( + '>' => {precedence => 1.5, associativity => 'left', type => 'bin', class => 'context::Ordering::BOP::ordering'}, + '=' => {precedence => 1.7, associativity => 'left', type => 'bin', class => 'context::Ordering::BOP::ordering'}, + ); + $context->{parser}{String} = "context::Ordering::Parser::String"; + $context->{parser}{Value} = "context::Ordering::Parser::Value"; + $context->{value}{String} = "context::Ordering::Value::String"; + $context->{value}{Ordering} = "context::Ordering::Value::Ordering"; + $context->strings->add('='=>{hidden=>1},'>'=>{hidden=>1}); + $context->{error}{msg}{"Variable '%s' is not defined in this context"} = "'%s' is not defined in this context"; + $context->{error}{msg}{"Unexpected character '%s'"} = "Can't use '%s' in this context"; + $context->{error}{msg}{"Missing operand before '%s'"} = "Missing letter before '%s'"; + $context->{error}{msg}{"Missing operand after '%s'"} = "Missing letter after '%s'"; + + $context = $main::context{'Ordering-List'} = $context->copy; + $context->{name} = 'Ordering-List'; + $context->operators->redefine(',',from => "Full"); + $context->{value}{List} = "context::Ordering::Value::List"; + + main::PG_restricted_eval('sub Letters {context::Ordering::Letters(@_)}'); + main::PG_restricted_eval('sub Ordering {context::Ordering::Ordering(@_)}'); +} + +# +# A routine to set the letters allowed in this context. +# (Old letters are cleared, and > and = are allowed, but hidden, +# since they are used in the List() objects that implement the context). +# +sub Letters { + my $context = (Value::isContext($_[0]) ? shift : main::Context()); + my @strings; + foreach my $x (@_) {push(@strings, $x => {isLetter => 1, caseSensitive => 1})} + $context->strings->are(@strings); + $context->strings->add('='=>{hidden=>1},'>'=>{hidden=>1}); +} + +# +# Create orderings from strings or lists of letter => value pairs. +# A copy of the current context is created that contains the proper +# letters, and the correct string is created and parsed into an +# Ordering object. +# +sub Ordering { + my $context = main::Context()->copy; my $string; + Value->Error("The current context is not the Ordering context") + unless $context->{name} =~ m/Ordering/; + if (scalar(@_) == 1) { + $string = shift; + my $letters = $string; $letters =~ s/ //g; + context::Ordering::Letters($context,split(/[>=]/,$letters)); + } else { + my %letter = @_; my @letters = keys %letter; + context::Ordering::Letters($context,@letters); + foreach my $x (@letters) {$letter{$x} = Value::Real->new($context,$letter{$x})} + my @order = main::PGsort( + sub {$letter{$_[0]} == $letter{$_[1]} ? $_[0] lt $_[1] : $letter{$_[0]} > $letter{$_[1]}}, + @letters + ); + my $a = shift(@order); my $b; $string = $a; + while ($b = shift(@order)) { + $string .= ($letter{$a} == $letter{$b} ? " = " : " > ") . $b; + $a = $b; + } + } + return main::Formula($context,$string)->eval; +} + +############################################################# +# +# This is a Parser BOP used to create the Ordering objects +# used internally. They are actually lists with the operator +# and the two operands, and the comparisons is based on the +# standard list comparisons. The operands are either the strings +# for individual letters, or another Ordering object as a +# nested List. +# + +package context::Ordering::BOP::ordering; +our @ISA = ('Parser::BOP'); + +sub class {"Ordering"} + +sub isOrdering { + my $self = shift; my $obj = shift; my $class = $obj->class; + return $class eq 'Ordering' || $obj->{def}{isLetter}; +} + +sub _check { + my $self = shift; + $self->Error("Operands of %s must be letters",$self->{bop}) + unless $self->isOrdering($self->{lop}) && $self->isOrdering($self->{rop}); + $self->{letters} = $self->{lop}{letters}; # we modify {lop}{letters} this way, but that doesn't matter + foreach my $x (keys %{$self->{rop}{letters}}) { + if (defined($self->{letters}{$x})) { + $self->{ref} = $self->{rop}{letters}{$x}; + $self->Error("Each letter may appear only once in an ordering"); + } + $self->{letters}{$x} = $self->{rop}{letters}{$x}; + } +} + +sub _eval { + my $self = shift; + my $ordering = $self->Package("Ordering")->new($self->context,$self->{bop},@_); + $ordering->{letters} = $self->{letters}; + return $ordering; +} + +sub string { + my $self = shift; + return $self->{lop}->string." ".$self->{bop}." ".$self->{rop}->string; +} + +sub TeX { + my $self = shift; + return $self->{lop}->TeX." ".$self->{bop}." ".$self->{rop}->TeX; +} + + +############################################################# +# +# This is the Value object used to implement the list That represents +# one ordering operation. It is simply a normal Value::List with the +# operator as the first entry and the two operands as the remaing +# entries in the list. The new() method is overriden to make binary +# trees of equal operators into flat sorted lists. We override the +# List string and TeX methods so that they print correctly as binary +# operators. The cmp_equal method is overriden to make sure the that +# the lists are treated as a unit during answer checking. There is +# also a routine for adding letters to the object's context. +# + +package context::Ordering::Value::Ordering; +our @ISA = ('Value::List'); + +# +# Put all equal letters into one list and sort them +# +sub new { + my $self = shift; + my $context = (Value::isContext($_[0]) ? shift : $self->context); + my $bop = shift; my @letters = @_; + if ($bop eq '=') { + if (Value::classMatch($letters[0],'Ordering') && $letters[0]->{data}[0] eq '=') + {@letters = ($letters[0]->value,$letters[1]); shift @letters} + @letters = main::lex_sort(@letters); + } + return $self->SUPER::new($context,$bop,@letters); +} + +sub string { + my $self = shift; + my ($bop,@rest) = $self->value; + foreach my $x (@rest) {$x = $x->string}; + return join(" $bop ",@rest); +} + +sub TeX { + my $self = shift; + my ($bop,@rest) = $self->value; + foreach my $x (@rest) {$x = $x->TeX}; + return join(" $bop ",@rest); +} + +# +# Make sure we do comparison as a list of lists (rather than as the +# individual entries in the underlying Value::List that encodes +# the ordering) +# +sub cmp_equal { + my $self = shift; my $ans = $_[0]; + $ans->{typeMatch} = $ans->{firstElement} = $self; + $ans->{correct_formula} = $self->{equation}; + $self = $ans->{correct_value} = Value::List->make($self); + $ans->{student_value} = Value::List->make($ans->{student_value}) + if Value::classMatch($ans->{student_value},'Ordering'); + return $self->SUPER::cmp_equal(@_); +} + +sub cmp_defaults { + my $self = shift; + return ( + $self->SUPER::cmp_defaults(@_), + showMissingLetterHints => 1, + ); +} + +sub cmp_postprocess { + my $self = shift; my $ans = shift; + return if $ans->{isPreview} || $ans->{score} != 0; + $self->cmp_Error($ans,"Your ordering should include all the letters") + if $ans->{showMissingLetterHints} && + scalar(keys %{$ans->{correct_formula}{tree}{letters}}) != + scalar(keys %{$ans->{student_formula}{tree}{letters}}); +} + +# +# Add more letters to the ordering's context (so student answers +# can include them even if they aren't in the correct answer). +# +sub AddLetters { + my $self = shift; my $context = $self->context; + my @strings; + foreach my $x (@_) { + push(@strings, $x => {isLetter => 1, caseSensitive => 1}) + unless $context->strings->get($x); + } + $context->strings->add(@strings) if scalar(@strings); +} + +############################################################# +# +# This overrides the TeX method of the letters +# so that they don't print using the \rm font. +# + +package context::Ordering::Value::String; +our @ISA = ('Value::String'); + +sub TeX {shift->value} + + +############################################################# +# +# Override Parser classes so that we can check for repeated letters +# + +package context::Ordering::Parser::String; +our @ISA = ('Parser::String'); + +# +# Save the letters positional reference +# +sub new { + my $self = shift; + $self = $self->SUPER::new(@_); + $self->{letters}{$self->{value}} = $self->{ref} if $self->{def}{isLetter}; + return $self; +} + +######################### + +package context::Ordering::Parser::Value; +our @ISA = ('Parser::Value'); + +# +# Move letters to Value object +# +sub new { + my $self = shift; + $self = $self->SUPER::new(@_); + $self->{letters} = $self->{value}{letters} if defined $self->{value}{letters}; + return $self; +} + +# +# Return Ordering class if the object is one +# +sub class { + my $self = shift; + return "Ordering" if $self->{value}->classMatch('Ordering'); + return $self->SUPER::class; +} + +############################################################# +# +# This overrides the cmp_equal method to make sure that +# Ordering lists are put into nested lists (since the +# underlying ordering is a list, we don't want the +# list checker to test the individual parts of the list, +# but rather the list as a whole). +# + +package context::Ordering::Value::List; +our @ISA = ('Value::List'); + +sub cmp_equal { + my $self = shift; my $ans = $_[0]; + $ans->{student_value} = Value::List->make($ans->{student_value}) + if Value::classMatch($ans->{student_value},'Ordering'); + return $self->SUPER::cmp_equal(@_); +} + +############################################################# + +1; --- /dev/null +++ macros/contextFraction.pl @@ -0,0 +1,793 @@ +=head1 NAME + +contextFraction.pl - Implements a MathObject class for Fractions. + +=head1 DESCRIPTION + +This context implements a Fraction object that works like a Real, but +keeps the numerator and denominator separate. It provides methods for +reducing the fractions, and for allowing fractions with a whole-number +preceeding it, as in 4 1/2 for "four and one half". The answer +checker can require that students reduce their results, and there are +contexts that don't allow entery of decimal values (only fractions), +and that don't allow any operators or functions (other than division +and negation). + +To use these contexts, first load the contextFraction.pl file: + + loadMacros("contextFraction.pl"); + +and then select the appropriate context -- one of the following three: + + Context("Fraction"); + Context("Fraction-NoDecimals"); + Context("LimitedFraction"); + +The first is the most general, and allows fractions to be intermixed +with real numbers, so 1/2 + .5 would be allowed. Also, 1/2.5 is +allowed, though it produces a real number, not a fraction, since this +fraction class only implements fractions of integers. All operators +and functions are defined, so there are no restrictions on what is +allowed by the student. + +The second does not allow decimal numbers to be entered, but they can +still be produced as the result of function calls, or by named +constants such as "pi". For example, 1/sqrt(2) is allowed (and +produces a real number result). All functions and operations are +defined, and the only real difference between this and the previous +context is that decimal numbers can't be typed in explicitly. + +The third context limits the operations that can be performed: in +addition to not being able to type decimal numbers, no operations +other than division and negation are allowed, and no function calls at +all. Thus 1/sqrt(2) would be illegal, as would 1/2 + 2. The student +must enter a whole number or a fraction in this context. It is also +permissible to enter a whole number WITH a fraction, as in 2 1/2 for +"two and one half", or 5/2. + +You can use the Compute() function to generate fraction objects, or +the Fraction() constructor to make one explicitly. For example: + + Context("Fraction"); + $a = Compute("1/2"); + $b = Compute("4 - 1/6"); + $c = Compute("(4/9)^(1/2)"); + + Context("LimitedFraction"); + $d = Compute("4 2/3"); + $e = Compute("-1 1/2"); + + $f = Fraction(-2,5); + +Note that $c will be 2/3, $d will be 14/3, $e will be -3/2, and $f +will be -2/5. + +Once you have created a fraction object, you can use it as you would +any real number. For example: + + Context("Fraction"); + $a = Compute("1/2"); + $b = Compute("1/3"); + $c = $a - $b; + $d = asin($a); + $e = $b**2; + +Here $c will be the equivalent of Compute("1/6"), $d will be +equivalent to Compute("pi/6"), and $e will be the same as Compute("1/9"); + +You can an answer checker for a fraction in the same way as you do for +ALL MathObjects -- via its cmp() method: + + ANS(Compute("1/2")->cmp); + +or + + $b = Compute("1/2"); + ANS($b->cmp); + +There are several options to the cmp() method that control how the +answer checker will work. The first is controls whether unreduced +fractions are accepted as correct. Unreduced fractions are allowed in +the Fraction and Fraction-NoDecimals contexts, but not in the +LimitedFraction context. You can control this using the +studentsMustReduceFractions option: + + Context("Fraction"); + ANS(Compute("1/2")->cmp(studentsMustReduceFractions=>1)); + +or + + Context("LimitedFraction"); + ANS(Compute("1/2")->cmp(studentsMustReduceFractions=>0)); + +The second controls whether warnings are issued when students don't +reduce their answers, or to mark the answer incorrect silently. This +is specified by the showFractionReductionWarnings option. The default +is to report the warnings, but this option has an effect only when +studentsMustReduceFractions is 1, and so only in the LimitedFraction +context. For example, + + Context("LimitedFraction"); + ANS(Compute("1/2")->cmp(showFractionReductionWarnings=>0)); + +turns off these warnings. + +The final option, requireFraction, specifies whether a fraction MUST +be entered (e.g. one would have to enter 2/1 for a whole number). The +default is 0. + +In addition to these options for cmp(), there are Context flags that +control how fractions are handled. These include the following. + +=over + +=item S<C<< reduceFractions >>> + +This determines whether fractions are reduced automatically when they +are created. The default is to reduce fractions (except when +studentsMustReduceFractions is set), so Compute("4/6") would produce +the fraction 2/3. To leave fractions unreduced, set +reduceFractions=>0. The LimitedFraction context has +studentsMustReduceFractions set, so reduceFractions is unset +automatically for students, but not for correct answers, so +Fraction(2,4) would still produce 1/2, even though 2/4 would not be +allowed in a student answer. + +=item S<C<< strictFractions >>> + +This determines whether division is allowed only between integers or +not. If you want to prevent division from accepting non-integers, +then set strictFractions=>1 (and also strictMinus=>1 and +strictMultiplication=>1). These are all three 0 by default in the +Fraction and Fraction-NoDecimals contexts, but 1 in LimitedFraction. + +=item S<C<< allowProperFractions >>> + +This determines whether a space between a whole number and a fraction +is interpretted as implicit multiplication (as it usually would be in +WeBWorK), or as addition, allowing "4 1/2" to mean "4 and 1/2". By +default, it acts as multiplication in the Fraction and +Fraction-NoDecimals contexts, and as addition in LimitedFraction. If +you set allowProperFractions=>1 you should also set reduceConstants=>0. + +=item S<C<< requireProperFractions >>> + +This determines whether fractions MUST be entered as proper fractions. +It is 0 by default, meaning improper fractions are allowed. When set, +you will not be able to enter 5/2 as a fraction, but must use "2 1/2". +Set it to 1 only when you also set allowProperFractions, or you will +not be able to specify fractions bigger than one. It is off by +default in all three contexts. + +=item S<C<< showProperFractions >>> + +This controls whether fractions are displayed as proper fractions or +not. When set, 5/2 will be displayed as 2 1/2 in the answer preview +area, otherwise it will be displayed as 5/2. This flag is 0 by +default in the Fraction and Fraction-NoDecimals contexts, and 1 in +LimitedFraction. + +=back + +Fraction objects have two methods that can be useful when +reduceFractions is set to 0. The reduce() method will reduce a +fraction to lowest terms, and the isReduced() method returns true when +the fraction is reduced and false otherwise. + +If you wish to convert a fraction to its numeric (real number) form, +use the Real() constructor to coerce it to a real. E.g., + + $a = Compute("1/2"); + $r = Real($a); + +would set $r to the value 0.5. Similarly, use Fraction() to convert a +real number to (an approximating) fraction. E.g., + + $r = Real(.5); + $a = Fraction($r); + +would set $a to be 1/2. The fraction produced is good to about 6 +decimal places, so it can't be used for numbers that are too small. + +A side-effect of using the Fraction context is that fractions can be +used to take powers of negative numbers when the reduced form of the +fraction has an odd denominator. Thus (-8)^(1/3) will produce -2 as a +result, while in the standard Numeric context it would produce an +error. + +=cut + +sub _contextFraction_init {context::Fraction::Init()}; + +########################################################################### + +package context::Fraction; + +# +# Initialize the contexts and make the creator function. +# +sub Init { + my $context = $main::context{Fraction} = Parser::Context->getCopy("Numeric"); + $context->{name} = "Fraction"; + $context->{pattern}{signedNumber} .= '|-?\d+/\d+'; + $context->operators->set( + "/" => {class => "context::Fraction::BOP::divide"}, + "//" => {class => "context::Fraction::BOP::divide"}, + "/ " => {class => "context::Fraction::BOP::divide"}, + " /" => {class => "context::Fraction::BOP::divide"}, + "u-" => {class => "context::Fraction::UOP::minus"}, + " " => {precedence => 2.8, string => ' *'}, + " *" => {class => "context::Fraction::BOP::multiply", precedence => 2.8}, + # precedence is lower to get proper parens in string() and TeX() calls + " " => {precedence => 2.7, associativity => 'left', type => 'bin', string => ' ', + class => 'context::Fraction::BOP::multiply', TeX => [' ',' '], hidden => 1}, + ); + $context->flags->set( + reduceFractions => 1, + strictFractions => 0, strictMinus => 0, strictMultiplication => 0, + allowProperFractions => 0, # also set reduceConstants => 0 if you change this + requireProperFractions => 0, + showProperFractions => 0, + ); + $context->reduction->set('a/b' => 1,'a b/c' => 1, '0 a/b' => 1); + $context->{value}{Fraction} = "context::Fraction::Fraction"; + $context->{value}{Real} = "context::Fraction::Real"; + $context->{parser}{Value} = "context::Fraction::Value"; + $context->{parser}{Number} = "Parser::Legacy::LimitedNumeric::Number"; + + $context = $main::context{'Fraction-NoDecimals'} = $context->copy; + $context->{name} = "Fraction-NoDecimals"; + Parser::Number::NoDecimals($context); + + $context = $main::context{LimitedFraction} = $context->copy; + $context->{name} = "LimitedFraction"; + $context->operators->undefine( + '+', '-', '*', '* ', '^', '**', + 'U', '.', '><', 'u+', '!', '_', ',', + ); + $context->parens->undefine('|','{','['); + $context->functions->disable('All'); + $context->flags->set( + strictFractions => 1, strictMinus => 1, strictMultiplication => 1, + allowProperFractions => 1, reduceConstants => 0, + showProperFractions => 1, + ); + $context->{cmpDefaults}{Fraction} = {studentsMustReduceFractions => 1}; + + main::PG_restricted_eval('sub Fraction {Value->Package("Fraction()")->new(@_)};'); +} + +# +# Convert a real to a reduced fraction approximation +# +sub toFraction { + my $context = shift; my $x = shift; + my $Real = $context->Package("Real"); + my $d = 1000000; + my ($a,$b) = reduce(int($x*$d),$d); + return [$Real->make($a),$Real->make($b)]; +} + +# +# Greatest Common Divisor +# +sub gcd { + my $a = abs(shift); my $b = abs(shift); + ($a,$b) = ($b,$a) if $a < $b; + return $a if $b == 0; + my $r = $a % $b; + while ($r != 0) { + ($a,$b) = ($b,$r); + $r = $a % $b; + } + return $b; +} + +# +# Least Common Multiple +# +sub lcm { + my ($a,$b) = @_; + return ($a/gcd($a,$b))*$b; +} + + +# +# Reduced fraction +# +sub reduce { + my $a = shift; my $b = shift; + ($a,$b) = (-$a,-$b) if $b < 0; + my $gcd = gcd($a,$b); + return ($a/$gcd,$b/$gcd); +} + +########################################################################### + +package context::Fraction::BOP::divide; +our @ISA = ('Parser::BOP::divide'); + +# +# Create a Fraction or Real from the given data +# +sub _eval { + my $self = shift; my $context = $self->{equation}{context}; + return $_[0]/$_[1] if Value::isValue($_[0]) || Value::isValue($_[1]); + my $n = $context->Package("Fraction")->make($context,@_); + $n->{isHorizontal} = 1 if $self->{def}{noFrac}; + return $n; +} + +# +# When strictFraction is in effect, only allow division +# with integers and negative integers +# +sub _check { + my $self = shift; + $self->SUPER::_check; + return unless $self->context->flag("strictFractions"); + $self->Error("The numerator of a fraction must be an integer") + unless $self->{lop}->class =~ /INTEGER|MINUS/; + $self->Error("The denominator of a fraction must be a (non-negative) integer") + unless $self->{rop}->class eq 'INTEGER'; + $self->Error("The numerator must be less than the denominator in a proper fraction") + if $self->context->flag("requireProperFractions") && CORE::abs($self->{lop}->eval) >= CORE::abs($self->{rop}->eval); +} + +# +# Reduce the fraction, if it is one, otherwise do the usual reduce +# +sub reduce { + my $self = shift; + return $self->SUPER::reduce unless $self->class eq 'FRACTION'; + my $reduce = $self->{equation}{context}{reduction}; + return $self->{lop} if $self->{rop}{isOne} && $reduce->{'x/1'}; + $self->Error("Division by zero"), return $self if $self->{rop}{isZero}; + return $self->{lop} if $self->{lop}{isZero} && $reduce->{'0/x'}; + if ($reduce->{'a/b'}) { + my ($a,$b) = context::Fraction::reduce($self->{lop}->eval,$self->{rop}->eval); + if ($self->{lop}->class eq 'INTEGER') {$self->{lop}{value} = $a} else {$self->{lop}{op}{value} = -$a} + $self->{rop}{value} = $b; + } + return $self; +} + +# +# Display minus signs outside the fraction +# +sub TeX { + my $self = shift; my $bop = $self->{def}; + return $self->SUPER::TeX(@_) if $self->class ne 'FRACTION' || $bop->{noFrac}; + my ($precedence,$showparens,$position,$outerRight) = @_; + $showparens = '' unless defined($showparens); + my $addparens = + defined($precedence) && + ($showparens eq 'all' || ($precedence > $bop->{precedence} && $showparens ne 'nofractions') || + ($precedence == $bop->{precedence} && ($bop->{associativity} eq 'right' || $showparens eq 'same'))); + + my $TeX = $self->eval->TeX; + $TeX = '\left('.$TeX.'\right)' if ($addparens); + return $TeX; +} + +# +# Indicate if the value is a fraction or not +# +sub class { + my $self = shift; + return "FRACTION" if $self->{lop}->class =~ /INTEGER|MINUS/ && + $self->{rop}->class eq 'INTEGER'; + return $self->SUPER::class; +} + +########################################################################### + +package context::Fraction::BOP::multiply; +our @ISA = ('Parser::BOP::multiply'); + +# +# For proper fractions, add the integer to the fraction +# +sub _eval { + my ($self,$a,$b)= @_; + return ($a > 0 ? $a + $b : $a - $b); +} + +# +# If the implied multiplication represents a proper fraction with a +# preceeding integer, then switch to the proper fraction operator +# (for proper handling of string() and TeX() calls), otherwise, +# convert the object to a standard multiplication. +# +sub _check { + my $self = shift; + $self->SUPER::_check; + my $isFraction = 0; + if ($self->context->flag("allowProperFractions")) { + $isFraction = ($self->{lop}->class =~ /INTEGER|MINUS/ && !$self->{lop}{hadParens} && + $self->{rop}->class eq 'FRACTION' && !$self->{rop}{hadParens} && + $self->{rop}->eval >= 0); + } + if ($isFraction) { + $self->{bop} = " "; + $self->{def} = $self->context->{operators}{$self->{bop}}; + if ($self->{lop}->class eq 'MINUS') { + # + # Hack to replace BOP with unary negation of BOP. + # (When check() is changed to accept a return value, + # this will not be necessary.) + # + my $copy = bless {%$self}, ref($self); $copy->{lop} = $copy->{lop}{op}; + my $neg = $self->Item("UOP")->new($self->{equation},"u-",$copy); + map {delete $self->{$_}} (keys %$self); + map {$self->{$_} = $neg->{$_}} (keys %$neg); + bless $self, ref($neg); + } + } else { + $self->Error("Can't use implied multiplication in this context",$self->{bop}) + if $self->context->flag("strictMultiplication"); + bless $self, $ISA[0]; + } +} + +# +# Reduce the fraction +# +sub reduce { + my $self = shift; + my $reduce = $self->{equation}{context}{reduction}; + my ($a,($b,$c)) = (CORE::abs($self->{lop}->eval),$self->{rop}->eval->value); + if ($reduce->{'a b/c'}) { + ($b,$c) = context::Fraction::reduce($b,$c) if $reduce->{'a/b'}; + $a += int($b/$c); $b = $b % $c; + $self->{lop}{value} = $a; + $self->{rop}{lop}{value} = $b; + $self->{rop}{rop}{value} = $c; + return $self->{lop} if $b == 0 || $c == 1; + } + return $self->{rop} if $a == 0 && $reduce->{'0 a/b'}; + return $self; +} + +########################################################################### + +package context::Fraction::UOP::minus; +our @ISA = ('Parser::UOP::minus'); + +# +# For strict fractions, only allow minus on certain operands +# +sub _check { + my $self = shift; + $self->SUPER::_check; + $self->{hadParens} = 1 if $self->{op}{hadParens}; + return unless $self->context->flag("strictMinus"); + my $uop = $self->{def}{string} || $self->{uop}; + $self->Error("You can only use '%s' with (non-negative) numbers",$uop) + unless $self->{op}->class =~ /Number|INTEGER|FRACTION/; +} + +# +# class is MINUS if it is a negative number +# +sub class { + my $self = shift; + return "MINUS" if $self->{op}->class =~ /Number|INTEGER/; + $self->SUPER::class; +} + +########################################################################### + +package context::Fraction::Value; +our @ISA = ('Parser::Value'); + +# +# Indicate if the Value object is a fraction or not +# +sub class { + my $self = shift; + return "FRACTION" if $self->{value}->classMatch('Fraction'); + return $self->SUPER::class; +} + +########################################################################### + +package context::Fraction::Real; +our @ISA = ('Value::Real'); + +# +# Allow Real to convert Fractions to Reals +# +sub new { + my $self = shift; my $context = (Value::isContext($_[0]) ? shift : $self->context); + my $x = shift; + $x = $context->Package("Formula")->new($context,$x)->eval if ref($x) eq "" && $x =~ m!/!; + $x = $x->eval if scalar(@_) == 0 && Value::classMatch($x,'Fraction'); + $self->SUPER::new($context,$x,@_); +} + +# +# Since the signed number pattern now include fractions, we need to make sure +# we handle them when a real is made and it looks like a fraction +# +sub make { + my $self = shift; my $context = (Value::isContext($_[0]) ? shift : $self->context); + my $x = shift; + $x = $context->Package("Formula")->new($context,$x)->eval if ref($x) eq "" && $x =~ m!/!; + $x = $x->eval if scalar(@_) == 0 && Value::classMatch($x,'Fraction'); + $self->SUPER::make($context,$x,@_); +} + +########################################################################### +########################################################################### +# +# Implements the MathObject for fractions +# + +package context::Fraction::Fraction; +our @ISA = ('Value'); + +sub new { + my $self = shift; my $class = ref($self) || $self; + my $context = (Value::isContext($_[0]) ? shift : $self->context); + my $x = shift; $x = [$x,@_] if scalar(@_) > 0; + return $x->inContext($context) if Value::classMatch($x,'Fraction'); + $x = [$x] unless ref($x) eq 'ARRAY'; $x->[1] = 1 if scalar(@{$x}) == 1; + Value::Error("Can't convert ARRAY of length %d to %s",scalar(@{$x}),Value::showClass($self)) + unless (scalar(@{$x}) == 2); + $x->[0] = Value::makeValue($x->[0],context=>$context); + $x->[1] = Value::makeValue($x->[1],context=>$context); + return $x->[0] if Value::classMatch($x->[0],'Fraction') && scalar(@_) == 0; + $x = context::Fraction::toFraction($context,$x->[0]->value) if Value::isReal($x->[0]) && scalar(@_) == 0; + return $self->formula($x) if Value::isFormula($x->[0]) || Value::isFormula($x->[1]); + Value::Error("Fraction numerators must be integers") unless isInteger($x->[0]); + Value::Error("Fraction denominators must be integers") unless isInteger($x->[1]); + my ($a,$b) = ($x->[0]->value,$x->[1]->value); ($a,$b) = (-$a,-$b) if $b < 0; + Value::Error("Denominator can't be zero") if $b == 0; + ($a,$b) = context::Fraction::reduce($a,$b) if $context->flag("reduceFractions"); + bless {data => [$a,$b], context => $context}, $class; +} + +# +# Produce a real if one of the terms is not an integer +# otherwise produce a fraction. +# +sub make { + my $self = shift; my $class = ref($self) || $self; + my $context = (Value::isContext($_[0]) ? shift : $self->context); + push(@_,0) if scalar(@_) == 0; push(@_,1) if scalar(@_) == 1; + my ($a,$b) = @_; ($a,$b) = (-$a,-$b) if $b < 0; + return $context->Package("Real")->make($context,$a/$b) unless isInteger($a) && isInteger($b); + ($a,$b) = context::Fraction::reduce($a,$b) if $context->flag("reduceFractions"); + bless {data => [$a,$b], context => $context}, $class; +} + +# +# Promote to a fraction, allowing reals to be $x/1 even when +# not an integer (later $self->make() will produce a Real in +# that case) +# +sub promote { + my $self = shift; my $class = ref($self) || $self; + my $context = (Value::isContext($_[0]) ? shift : $self->context); + my $x = (scalar(@_) ? shift : $self); + if (scalar(@_) == 0) { + return $x->inContext($context) if ref($x) eq $class; + return (bless {data => [$x->value,1], context => $context}, $class) if Value::isReal($x); + return (bless {data => [$x,1], context => $context}, $class) if Value::matchNumber($x); + } + return $self->new($context,$x,@_); +} + + +# +# Create a new formula from the number +# +sub formula { + my $self = shift; my $value = shift; + my $formula = $self->Package("Formula")->blank($self->context); + my ($l,$r) = Value::toFormula($formula,@{$value}); + $formula->{tree} = $formula->Item("BOP")->new($formula,'/',$l,$r); + return $formula; +} + +# +# Return the real number type +# +sub typeRef {return $Value::Type{number}} +sub length {2} + +sub isZero {(shift)->{data}[0] == 0} +sub isOne {(shift)->eval == 1} + +# +# Return the real value +# +sub eval { + my $self = shift; + my ($a,$b) = $self->value; + return $a/$b; +} + +# +# parts are not Value objects, so don't transfer +# +sub transferFlags {} + +# +# Check if a value is an integer +# +sub isInteger { + my $n = shift; + $n = $n->value if Value::isReal($n); + return $n =~ m/^-?\d+$/; +}; + + +################################################## +# +# Binary operations +# + +sub add { + my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); + my (($a,$b),($c,$d)) = ($l->value,$r->value); + my $M = context::Fraction::lcm($b,$d); + return $self->inherit($other)->make($a*($M/$b)+$c*($M/$d),$M); +} + +sub sub { + my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); + my (($a,$b),($c,$d)) = ($l->value,$r->value); + my $M = context::Fraction::lcm($b,$d); + return $self->inherit($other)->make($a*($M/$b)-$c*($M/$d),$M); +} + +sub mult { + my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); + my (($a,$b),($c,$d)) = ($l->value,$r->value); + return $self->inherit($other)->make($a*$c,$b*$d); +} + +sub div { + my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); + my (($a,$b),($c,$d)) = ($l->value,$r->value); + Value::Error("Division by zero") if $c == 0; + return $self->inherit($other)->make($a*$d,$b*$c); +} + +sub power { + my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); + my (($a,$b),($c,$d)) = ($l->value,$r->reduce->value); + ($a,$b,$c) = ($b,$a,-$c) if $c < 0; + my ($x,$y) = ($c == 1 ? ($a,$b) : ($a**$c,$b**$c)); + if ($d != 1) { + if ($x < 0 && $d % 2 == 1) {$x = -(-$x)**(1/$d)} else {$x = $x**(1/$d)}; + if ($y < 0 && $d % 2 == 1) {$y = -(-$y)**(1/$d)} else {$y = $y**(1/$d)}; + } + return $self->inherit($other)->make($x,$y) unless $x eq 'nan' || $y eq 'nan'; + Value::Error("Can't raise a negative number to a power") if $a*$b < 0; + Value::Error("Result of exponention is not a number"); +} + +sub compare { + my ($self,$l,$r) = Value::checkOpOrderWithPromote(@_); + return $l->eval <=> $r->eval; +} + +################################################## +# +# Numeric functions +# + +sub abs {my $self = shift; $self->make(CORE::abs($self->{data}[0]),CORE::abs($self->{data}[1]))} +sub neg {my $self = shift; $self->make(-($self->{data}[0]),$self->{data}[1])} +sub exp {my $self = shift; $self->make(CORE::exp($self->eval))} +sub log {my $self = shift; $self->make(CORE::log($self->eval))} +sub sqrt {my $self = shift; $self->make(CORE::sqrt($self->{data}[0]),CORE::sqrt($self->{data}[1]))} + +################################################## +# +# Trig functions +# + +sub sin {my $self = shift; $self->make(CORE::sin($self->eval))} +sub cos {my $self = shift; $self->make(CORE::cos($self->eval))} + +sub atan2 { + my ($self,$l,$r,$other) = Value::checkOpOrderWithPromote(@_); + return $self->inherit($other)->make(CORE::atan2($l->eval,$r->eval)); +} + +################################################## +# +# Utility +# + +sub reduce { + my $self = shift; + my ($a,$b) = context::Fraction::reduce($self->value); + return $self->make($a,$b); +} + +sub isReduced { + my $self = shift; + my (($a,$b),($c,$d)) = ($self->value,$self->reduce->value); + return $a == $c && $b == $d; +} + +################################################## +# +# Formatting +# + +sub string { + my $self = shift; my $equation = shift; my $prec = shift; + my ($a,$b) = @{$self->{data}}; my $n = ""; + return $a if $b == 1; + if ($self->getFlag("showProperFractions") && CORE::abs($a) > $b) + {$n = int($a/$b); $a = CORE::abs($a) % $b; $n .= " " unless $a == 0} + $n .= "$a/$b" unless $a == 0 && $n ne ''; + $n = "($n)" if defined $prec && $prec >= 1; + return $n; +} + +sub TeX { + my $self = shift; my $equation = shift; my $prec = shift; + my ($a,$b) = @{$self->{data}}; my $n = ""; + return $a if $b == 1; + if ($self->getFlag("showProperFractions") && CORE::abs($a) > $b) + {$n = int($a/$b); $a = CORE::abs($a) % $b; $n .= " " unless $a == 0} + my $s = ""; ($a,$s) = (-$a,"-") if $a < 0; + $n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{\\textstyle\\frac{$a}{$b}}") + unless $a == 0 && $n ne ''; + $n = "\\left($n\\right)" if defined $prec && $prec >= 1; + return $n; +} + +########################################################################### +# +# Answer Checker +# + +sub cmp_defaults {( + shift->SUPER::cmp_defaults(@_), + ignoreInfinity => 1, + studentsMustReduceFractions => 0, + showFractionReduceWarnings => 1, + requireFraction => 0, +)} + +sub cmp_contextFlags { + my $self = shift; my $ans = shift; + return ( + $self->SUPER::cmp_contextFlags($ans), + reduceFractions => !$ans->{studentsMustReduceFractions}, + ); +} + +sub cmp_class {"a fraction of integers"} + +sub typeMatch { + my $self = shift; my $other = shift; my $ans = shift; + return 1 unless ref($other); + return 0 if Value::isFormula($other); + return 1 if $other->type eq 'Infinity' && $ans->{ignoreInfinity}; + return 0 if $ans->{requireFraction} && !$other->classMatch("Fraction"); + $self->type eq $other->type; +} + +sub cmp_postprocess { + my $self = shift; my $ans = shift; + my $student = $ans->{student_value}; + return if $ans->{isPreview} || + !$ans->{studentsMustReduceFractions} || + !Value::classMatch($student,'Fraction') || + $student->isReduced; + $ans->score(0); + $self->cmp_Error($ans,"Your fraction is not reduced") if $ans->{showFractionReduceWarnings}; +} + +########################################################################### + +1; |