diff --git a/macros/contexts/contextExtensions.pl b/macros/contexts/contextExtensions.pl index c9f8700db..a5445a1aa 100644 --- a/macros/contexts/contextExtensions.pl +++ b/macros/contexts/contextExtensions.pl @@ -460,6 +460,26 @@ sub extensionContext { return $class; } +# +# Trap any calls to super-class methods that aren't in the overridden +# class and pass them on to the original class. +# +sub AUTOLOAD { + our $AUTOLOAD; + my $self = shift; + return unless defined $self; + my $class = $self->extensionContext; + my $method = (split(/::/, $AUTOLOAD))[-1]; + return if $method eq 'DESTROY'; + + if (substr($AUTOLOAD, 0, length($class) + 2) eq $class . '::') { + my $code = $self->super($method); + return &$code($self, @_) if $code; + } + + die "Can't locate object method '$method' via package \"" . ref($self) . (getCaller() // ''); +} + ################################################################################################# ################################################################################################# @@ -538,7 +558,10 @@ package context::Extensions::Data; # # Get the object's extensionData # -sub extensionData { (shift)->typeRef->{ $self->extensionID } } +sub extensionData { + my $self = shift; + $self->typeRef->{ $self->extensionID }; +} # # Set the object's extensionData (and the rest of its type) diff --git a/macros/contexts/contextSignificantFigures.pl b/macros/contexts/contextSignificantFigures.pl new file mode 100644 index 000000000..c8e53bd9b --- /dev/null +++ b/macros/contexts/contextSignificantFigures.pl @@ -0,0 +1,832 @@ + +=head1 NAME + +contextSignificantFigures.pl - Implements a context to handle numbers where +significant figures is important. + +=head1 DESCRIPTION + +This file implements a MathObject SignificantFigures class that provides the +ability to create numbers for given significant figures as well as the operations ++, -, *, /, ** + +To load this context, use + + loadMacros('contextSignificantFigures.pl'); + +and then set this context with + + Context('SignificantFigures'); + +or + + Context('LimitedSignificantFigures'); + +where the latter context, operations between numbers are not allowed. + +This is primarily for decimal numbers and keep track of significant figures. +With the context loaded, a call to C will parse the number or string to keep +track of significant figures. For example, + + $x = Real('37.45'); + $y = Real('37.1834'); + +and these numbers will have 4 and 6 significant figures respectively. To query +the number of significant figures, use the C method. For example, +C<< $x->sigfigs >> will return 4. + +The standard arithmetic operations +, -, *, / are defined for these and the result +will have the correct number of significant figures. For example + + $x + $y; + +returns the value C<74.63>, where the first number is rounded to the hundredths +place before adding. + + $x * $y; + +returns C<1392> or C<1.392E+02>. + +Finally, we can also perform subtraction as in + + $x - $y; + +however, subtraction can lose significant figures. The answer to this is C<0.27>, +resulting in only 2 significant figures. + +=head2 Significant Figure Rules + +A reminder about significant figures is that all non-zero digits are significant. +The rule about a zero's significance depends on where it is in a number. + +=over + +=item * Zeros between any significant digits are significant. The zeros in 12.0034 +are significant. There are 6 significant figures in this number. + +=item * Zeros to the left of a non-zero digit are not significant. The zeros in +0.00123 are not significant. There are 3 significant figures in this number. + +=item * Zeros to right of the decimal point and to the right a non-zero digit are +significant. The zeros in 12.3400 are significant. There are 6 significant figures +in this number. + +=item * Zeros to the left of the decimal point and to the right of a non-zero +digit are not significant. The zeros in 12300 are not significant. There are 3 +significant figures in this number. However, the presence of a significant zero +changes the rule. The zeros in 12300.0 are all significant because the rightmost +0 is significant and therefore the other zeros are significant. + + +=back + +Note: If a number only consists of zeros has some different rules generally as a +result of other operations. + +=over + +=item * The number 0 has infinite significant figures. + +=item * The number 0. has 1 significant figure. + +=item * The number 0.00 has 3 significant figures. + +=back + +=head3 Setting the number of significant figures. + +The number of significant figures can be set in two ways. The first, in creation +of the number if the option C is used. For example + + Real('100', sigfigs => 2) + +will give the number C<1.0 * 10^2>. Alternatively, if a number is already created, +then the C method can set the number of significant figures. If + + $x = Real('1000'); + +then C<$x> has 1 significant figure, but C<< $x->sigfigs(3) >> will update that +to C<1.00 * 10^3>. + +One can set a number with infinite significant figures with C<< sigfigs => 'inf' >>. +This is often done with known constants. A simple example would be that in the +perimeter of a square with side C, or C<4x>, the 4 would have infinite significant +figures, meaning that the result would have the same significant figures as the +number x. Example: + + $x = Real(17.05); + $p = Real(4, sigfigs => 'inf') * $x; + +One can set the number of significant figures after a number has been created +with the C method. For example, + + $x = Real(12.3456); + +which has 6 significant figures. If C<< $x->sigfigs(4) >>, then the result is +the number '12.35', where rounding has been performed. + +=head2 Flags + +There are two flags that give authors some control over messaging for near-correct +answers. The default behavior is that a correct answer in this context is only +given when a student has correct number of significant figures and the correct +answer (to all digits). + +=over + +=item Incorrect Significant Figures + +If an author wants show a message and possibly give partial credit for a correct +answer (within tolerance) but the incorrect number of significant figures, then +set the C flag to a value between 0 and 1. + +If the flag C is set, and a student has the correct answer +to within tolerance (using any of the tolerances set by C) as well +as the the incorrect number of significant figures, then a message will be +shown to the student and the student will receive partial credit with this value. + +For example, + + Context('SignificantFigures')->flags->set(tolerance => 0.01, partial_incorrect_sf => 0.6); + +will set the tolerance to 0.01 (this is the same C flag for reals) +and the amount of partial credit to give for the correct answer with wrong number +of significant figures. + +Note that if the author would like to have the message shown, but no partial credit, +use C<< partial_incorrect_sf => 0 >>. + +=item Correct Significant Figures and Close to the Correct Answer + +If an author would like to show a message and possibly give partial credit for a +correct answer (within tolerance) and correct number of significant figures, +then the flag C can be used. + +If this flag exists and a student has the correct number of significant figures +and the answer is within tolerance (using those set by C) then a +message will be shown and the student will receive this value on the answer. + +For example, + + Context('SignificantFigures')->flags->set(partial_sf_within_tolerance => 0.8); + +Note that if the author would like to have the message shown, but no partial credit, use +C<< partial_sf_within_tolerance => 0 >>. + +=back + +=head2 SigFigNumber + +The function C will also create a SigFigNumber with the second +argument the number of significant figures. For example, + + $a = SigFigNumber(12.345); + $b = SigFigNumber(10000,3); + +will create a number with 5 and 3 significant figures respectively. + +=cut + +loadMacros('MathObjects.pl', 'PGauxiliaryFunctions.pl'); + +sub _contextSignificantFigures_init { + context::SignificantFigures::Init(@_); + + sub SigFigNumber { + my ($x, @opts) = @_; + @opts = (sigfigs => $opts[0]) if @opts == 1; + return Value->Package('Real')->new($x, @opts); + } +} + +# A replacement for Value::Real that handles Significant figures +package context::SignificantFigures::Real; +our @ISA = ('Value::Real'); + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my $context = (Value::isContext($_[0]) ? shift : $self->context); + my ($value, %opts) = @_; + my $n = $opts{sigfigs}; + + if (!Value::isValue($value) && !Value::matchNumber($value)) { + $value = Value::makeValue($value, context => $context); + return $value if Value::isFormula($value); + Value::Error("Can't convert %s to %s", Value::showClass($value), Value::showClass($self)) + unless Value::isNumber($value); + } + return $value->eval if Value::isFormula($value); + if (Value::isValue($value) && $value->{sigfigs}) { + my $copy = $value->copy->inContext($context); + $copy->sigfigs($n) if defined($n) && $n != $copy->N; + return $copy; + } + + if (!defined $n) { + my $digits = $value; + $digits =~ s/^[-+]//; # Remove any leading sign. + if ($value !~ m/[.Ee]/) { # The number is an integer. + $digits =~ s/0+$//; # Remove trailing 0s. + } else { + $digits =~ s/[Ee].*$//; # Remove the exponent, if any + if ($value == 0) { + $digits =~ s/^0*\.?/0/; # Remove any leading 0s, leaving only one. + } else { + $digits =~ s/\.//; # Remove non-digits. + $digits =~ s/^0+//; # Remove leading zeros. + } + } + $n = length($digits) || 'inf'; # what remains are the significant digits + } + my $N = $context->checkSigFigs($n); + $self = bless $self->SUPER::new($context, ROUND($value, $n)), $class; + $value = $self->format('E', $self->value, $N) unless $N eq 'inf'; + $self->{exp} = $N eq 'inf' ? 0 : (split(/E/, $value))[1] + 0; + $self->{sigfigs} = $N; + $self->{pure} = 1 unless $opts{computed} || $self->{value}{hadParens}; + + return $self; +} + +sub make { shift->new(@_) } + +# Either return the current number of sigfigs for the number or set the current number. +sub sigfigs { + my ($self, $n) = @_; + return $self->{sigfigs} if !defined($n); + my $sigfigs = $self->context->checkSigFigs($n); + return $self->{sigfigs} if $self->{sigfigs} == $n; + $self->{sigfigs} = $sigfigs; + $self->{data}[0] = ROUND($self->value, $n); + return $self->{sigfigs}; +} + +# Shortcut for returning the number of significant figures. +sub N { shift->{sigfigs} } + +# Return the exponent. +sub E { shift->{exp} } + +# Return the exponential for the given $value with max($n,14-$n) sigfigs. +# This basically is log10 except handles 0 and negative numbers. +sub expFor { + my ($self, $value, $n) = @_; + $n = main::max(0, $n - 1, 14 - $n); + return (split(/E/, sprintf("%.${n}E", $value)))[1] + 0; +} + +# Stringify and TeXify the number in the context's base + +sub string { + my ($self, $equation, $precedence) = @_; + my $string = $self->format($self->{expForm} || $self->getFlag('alwaysExponentialForm') ? 'E' : 'f'); + $string =~ s/E(?:(-)|\+)0*(\d+)/ 'x10^' . (defined($1) ? $1 : '') . $2 /e; + $string =~ s/\^-(.*)/^(-$1)/; + return $string =~ m/x/ && $precedence ? "($string)" : $string; +} + +sub TeX { + my ($self, $equation, $precedence) = @_; + my $tex = $self->format($self->{expForm} || $self->getFlag('alwaysExponentialForm') ? 'E' : 'f'); + $tex =~ s/E([-+])0*(\d+)/'\\times 10^{' . ($1 eq '-' ? '-' : '') . $2 . '}'/e; + return $tex =~ m/\\times/ && $precedence ? "\\left($tex\\right)" : "{$tex}"; +} + +# Format the number in $value in either 'E' (exponential form) or 'f' decimal form using +# $n significant figures. + +# Example: format('E', '123.456', 6) returns 1.23456E+02 +# format('f', 1.23E-01, 3) returns '0.123'. + +sub format { + my ($self, $f, $value, $n) = @_; + $value = $self->value unless defined $value; + $n = $self->N unless defined $n; + return "$value" if $n == 'inf'; + $value = ROUND($value, 0) if $n == 0; + my $exp = $self->E // $self->expFor($value, 0); + $f = 'E' if $f eq 'f' && ($n - $exp < 1 || $exp >= 5 || -5 >= $exp); + $n -= $exp if $f eq 'f'; + $n = main::max(0, $n - 1); + $value = sprintf("%.${n}${f}", $value); + $value .= '.' if $n == 0 && $f eq 'f' && $value =~ m/0$/; + return $value; +} + +# Redefine addition. The leftmost significant place in the result is needed to get the +# correct value. + +sub add { + my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); + my $exp = main::min($l->N - $l->E, $r->N - $r->E); + my $value = $l->round($exp) + $r->round($exp); + return $self->new($value, sigfigs => main::max(0, $exp + $self->expFor($value, $exp)), computed => 1); +} + +# Redefine subtraction. The leftmost significant place in the result is needed to get the +# correct value. + +sub sub { + my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); + my $exp = main::min($l->N - $l->E, $r->N - $r->E); + my $value = $l->round($exp) - $r->round($exp); + return $self->new($value, sigfigs => main::max(0, $exp + $self->expFor($value, $exp)), computed => 1); +} + +# Redefine multiplication. Use the product of the two numbers and set the number of significant +# figures to the minimum of the two numbers. + +sub mult { + my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); + return $self->new($l->value * $r->value, sigfigs => main::min($l->{sigfigs}, $r->{sigfigs}), computed => 1); +} + +# Redefine multiplication. Use the quotient of the two numbers and set the number of significant +# figures to the minimum of the two numbers. + +sub div { + my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); + return $self->new($l->value / $r->value, sigfigs => main::min($l->{sigfigs}, $r->{sigfigs}), computed => 1); +} + +# Redefine powers. Record whether this is an integer power of 10 for use with exponential form. + +sub power { + my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); + my ($L, $R) = ($l->value, $r->value); + $self->Error("Can't raise a negative number to a non-integer power") if $L < 0 && CORE::int($R) != $R; + $self->Error("Zero to the zero power is undefined") if $L == 0 && $R == 0; + return $l->copy if $L == 0; + my $intPower = CORE::int($R) == $R; + my $n = $intPower ? $l->{sigfigs} : main::min($l->{sigfigs}, $r->{sigfigs}); + my $result = $self->make($L**$R, sigfigs => $n, computed => 1); + $result->{tenPower} = 1 if $intPower && $L == 10 && $l->{pure} && $r->{pure}; + return $result; +} + +# Redefine abs to return the absolute value of the number with the same number of sigfigs. + +sub abs { + my $self = shift; + return $self->make(CORE::abs($self->value), sigfigs => $self->{sigfigs}, computed => 1); +} + +# Redefined neg to handle the parsing of negative numbers with sigfigs. + +sub neg { + my $self = shift; + return $self->new(-$self->value, sigfigs => $self->{sigfigs}); +} + +# This promotes non-significant figure numbers that are used in expressions to a +# significant figure number with infinite precision. + +sub promote { + my $self = shift; + my $context = (Value::isContext($_[0]) ? shift : $self->context); + my $value = (scalar(@_) ? shift : $self); + return $value->inContext($context) if Value::isValue($value) && $value->{sigfigs}; + return $self->new($context, $value, sigfigs => 'inf'); +} + +# The compare method determines that the values are equal with the same number of +# significant figures. This also handles inequalities as in other Reals. + +sub compare { + my ($self, $l, $r) = Value::checkOpOrderWithPromote(@_); + return $l->value <=> $r->value if $l->N == $r->N || $l->value != $r->value; + return $l->N <=> $r->N; +} + +sub round { + my ($self, $exp) = @_; + return ROUND($self->value, $self->E + $exp); +} + +# Rounds the number $x to $n significant digits. + +sub ROUND { + my ($x, $n) = @_; + return $x + 0 if $n == 'inf'; # keep the same if infinite digits + return 0 if $n < 0 || $x == 0; # 0 if less than 0 digits wanted or there are no digits + my $N = main::max(0, $n - 1); # Number of decimals to use in E notation + my $r = sprintf("%.${N}E", $x); # preliminary rounding of $x + my $e = (split(/E/, $r))[1] + 0; # exponent for $r + my $s = ($x < 0 ? -1 : 1); # sign of $x + my $m = main::max($n, 14 - $n); # position to use for adjustment for repeated 9s + $x += $s * 10**($e - $m); # adjust for repeated 9s + return sprintf("%.${N}E", $x) + 0 unless $n == 0; # if we want digits, re-round the adjusted value + + # For zero digits, we add a digit just above the first one in $x, + # round that, then remove the added digit, getting 0 if $x didn't + # round up, or 1 in the proper place if it did. This means that + # 0.005 rounds to .01, for example, if we ask for no digits, + # so something like 1.23 - .005 will yield 1.22 properly. + + my $d = $s * 10**($e + 1); + return sprintf("%.0E", $x + $d) - $d; +} + +# The preview_latex_string isn't correct, so in the processing pipeline, fix it. + +sub cmp_preprocess { + my ($self, $ansHash) = @_; + $tex = $ansHash->{student_ans}; + $tex =~ s/E([-+])0*(\d+)/'\\times 10^{' . ($1 eq '-' ? '-' : '') . $2 . '}' /e; + $ansHash->{preview_latex_string} = $tex; +} + +# This method checks to see if the student answer is equal (in the Value::Real sense) +# to the correct answer, but the incorrect number of significant figures. +# If so, show a warning and given partial credit. + +sub cmp_postprocess { + my ($self, $ansHash) = @_; + + return if $ansHash->{isPreview}; + return + unless ($self->getFlag('partial_incorrect_sf') || $self->getFlag('partial_sf_within_tolerance')) + && $ansHash->score < 1; + + my $student = $ansHash->{student_value}; + my $correct = $ansHash->{correct_value}; + + # Create Value::Real versions of the student and correct answer + # and check if the numbers are within tolerance but the number of significant figures is not correct. + + my $student_real = Value::Real->new($student->value); + my $correct_real = Value::Real->new($correct->value); + + if ($self->getFlag('partial_incorrect_sf') + && $student_real == $correct_real + && $student->sigfigs != $correct->sigfigs) + { + $ansHash->{ans_message} = "Incorrect number of significant figures"; + $ansHash->score($self->getFlag('partial_incorrect_sf')); + } + + # This time check if the number of sigfigs are correct, but the student answer is not + # exactly identical to the correct answer, but within tolerance + + if ($self->getFlag('partial_sf_within_tolerance') + && $student_real == $correct_real + && $student->sigfigs == $correct->sigfigs) + { + $ansHash->{ans_message} = "Correct number of significant figures, but the value is not correct"; + $ansHash->{score} = $self->getFlag('partial_sf_within_tolerance'); + } +} + +package context::SignificantFigures; + +sub Init { + my $context = $main::context{SignificantFigures} = context::SignificantFigures::Context->new(); + $context = $main::context{LimitedSignificantFigures} = $context->copy; + $context->{name} = 'LimitedSignificantFigures'; + $context->parens->undefine('|', '{', '['); + $context->variables->remove('x'); + $context->operators->undefine('-', '+', '/', '//', ' /', '/ ', '!', '_', '.', 'U', '><'); + $context->flags->set(limitedSigFigs => 1); +} + +package context::SignificantFigures::Context; +our @ISA = ('Parser::Context'); + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my $context = bless Parser::Context->getCopy('Numeric'), $class; + $context->{name} = 'SignificantFigures'; + $context->{parser}{Number} = 'context::SignificantFigures::Number'; + $context->{parser}{Value} = 'context::SignificantFigures::Value'; + $context->{parser}{Variable} = 'context::SignificantFigures::Variable'; + $context->{value}{Real} = 'context::SignificantFigures::Real'; + $context->functions->disable('All'); + $context->constants->clear(); + $context->{precedence}{SignificantFigures} = $context->{precedence}{special}; + $context->flags->set(alwaysExponentialForm => 0); # controls whether all reals are given in exponential form + $context->operators->set( + '*' => { class => 'context::SignificantFigures::BOP::multiply' }, + '* ' => { class => 'context::SignificantFigures::BOP::multiply' }, + ' *' => { class => 'context::SignificantFigures::BOP::multiply' }, + '^' => { class => 'context::SignificantFigures::BOP::power' }, + '**' => { class => 'context::SignificantFigures::BOP::power' }, + ' ' => { class => 'context::SignificantFigures::BOP::space', space => ' ', string => ' ' }, + ' ' => + { %{ $context->operators->get('*') }, class => 'context::SignificantFigures::BOP::space', hidden => 1 }, + 'u-' => { class => 'context::SignificantFigures::UOP::minus' }, + 'u+' => { class => 'context::SignificantFigures::UOP::plus' }, + ); + # + # Arrange for variables to be higher priority than operators so variable 'x' is used rather + # than operator 'x', unless the variable is removed. + # + my $variables = '_' . $context->variables->{dataName}; + $context->{data}{objects} = [ (grep { $_ ne $variables } @{ $context->{data}{objects} }), $variables ]; + # + # Add the 'x' operator as a fallback when 'x' is not a variable. + # + $context->operators->set( + 'x' => { + %{ $context->operators->get('*') }, + class => 'context::SignificantFigures::BOP::multiply', + string => 'x', + TeX => '\\times' + }, + ); + return $context; +} + +sub checkSigFigs { + my ($self, $n) = @_; + return $n if $n eq 'inf'; + Value::Error('The number of significant figures must be an integer or "inf"') unless $n =~ m/^[+-]?\d+$/; + Value::Error('The number of significant figures must be non-negative') if $n < 0; + Value::Error('The number of significant figures must be less than 16') if $n > 15; + return main::max(1, $n); +} + +# Some common function for the Parser object overrides + +package context::SignificantFigures::common; + +# True when this is a pure real, not a computed one + +sub isPure { + my ($self, $value) = @_; + return $value->{pure} && !$value->{hadParens}; +} + +# Check for limited use of UOPs + +sub checkLimitedUOP { + my $self = shift; + Value::Error("You can only use '%s' on an unsigned constant", shift) + if $self->context->flag('limitedSigFigs') && !$self->{pure}; +} + +# Check whether multiplication is for exponential form + +sub checkExponentialForm { + my $self = shift; + my ($l, $r) = ($self->{lop}, $self->{rop}); + if ($r->{tenPower} && !$r->{hadParens} && $self->isPure($l)) { + $r = $r->{lop} if $r->class eq 'BOP'; + $r->{value}{sigfigs} = 'inf'; + $self->{expForm} = 1; + $self->{def} = { %{ $self->{def} }, string => 'x', TeX => '\times' }; + } else { + Value::Error("The '%s' operator can ony appear between a simple constant and an integer power of ten", + $self->{bop}) + if $self->context->flag('limitedSigFigs') || $self->{bop} eq 'x'; + } +} + +# Copy the special properties used for exponential notation processing + +sub COPY { + my ($self, $from, $to) = @_; + for my $name ('pure', 'hadParens', 'hadPlus', 'tenPower', 'expForm') { + delete $to->{name} if $to->{$name}; + $to->{$name} = 1 if $from->{$name}; + } + delete $to->{pure} if $to->{hadParens}; + return $to; +} + +# Properly handle constants in exponential form, and add parentheses if needed + +sub STRING { + my ($self, $fn, $precedence) = @_; + my $flags = Value::contextSet($self->context, alwaysExponentialForm => 0); + my $string = $self->{expForm} && $precedence ? $self->addParens(&$fn) : &$fn; + Value::contextSet($self->context, %$flags); + return $string; +} + +# Properly handle constants in exponential form, and add parentheses if needed + +sub TEX { + my ($self, $fn, $precedence) = @_; + my $flags = Value::contextSet($self->context, alwaysExponentialForm => 0); + my $tex = $self->{expForm} && $precedence ? '\left(' . &$fn . '\right)' : &$fn; + Value::contextSet($self->context, %$flags); + return $tex; +} + +# Override Parser::Number to handle SignificantFigures Reals and copy the properties +# needed for processing exponential form. + +package context::SignificantFigures::Number; +our @ISA = ('Parser::Number', 'context::SignificantFigures::common'); + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my ($equation, $x, $ref) = @_; + my $context = $equation->{context}; + + $self = bless $self->SUPER::new($equation, $x, $ref), $class; + $self->{value} = Value::isValue($x) + && $x->{sigfigs} ? $x->copy->inContext($context) : $self->Package('Real')->new($context, $self->{value_string}); + return $self->COPY($self->{value}, $self); +} + +sub class {'Number'} + +sub value { shift->{value}->value } + +sub eval { + my $self = shift; + return $self->COPY($self, $self->SUPER::eval(@_)); +} + +sub perl { + my $self = shift; + my $value = $self->{value}; + return $self->SUPER::perl unless $value->{sigfigs}; + return $self->context->Package('Real') . '->new(' . $value->value . ', sigfigs => {' . $value->N . '})'; +} + +# Override the Parser::Value class to avoid using CORE::abs that would otherwise +# mark the result as computed when it may be pure + +package context::SignificantFigures::Value; +our @ISA = ('Parser::Value'); + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my $equation = shift; + my $context = $equation->{context}; + my ($value, $ref) = @_; + $value = $value->[0] if ref($value) eq 'ARRAY' && scalar(@{$value}) == 1; + return $self->SUPER::new($equation, @_) unless Value::isValue($value) && $value->{sigfigs}; + return $self->Item("Number")->new($equation, $value); +} + +# Override the Parser::Variable class to count the number of times a variable is used +# (so we can remove it from the equation's variables if 'x' is used for exponential form) + +package context::SignificantFigures::Variable; +our @ISA = ('Parser::Variable'); + +sub new { + my $self = shift; + my $v = $self->SUPER::new(@_); + my ($equation, $name) = ($v->{equation}, $v->{name}); + $equation->{vCount}{$name} = 0 unless defined $equation->{vCount}{$name}; + $equation->{vCount}{$name}++; + return $v; +} + +sub class {'Variable'} + +# Override Parser::UOP::minus to allow negation of a constant, but mark +# any other usage as computed rather than pure + +package context::SignificantFigures::UOP::minus; +our @ISA = ('Parser::UOP::minus', 'context::SignificantFigures::common'); + +sub _check { + my $self = shift; + $self->SUPER::_check(@_); + my $op = $self->{op}; + $self->{pure} = 1 if $op->class eq 'Number' && $self->isPure($op) && !$op->{hadPlus}; + $self->checkLimitedUOP('-'); +} + +sub _eval { + my $self = shift; + return $self->COPY($self, $self->SUPER::_eval(@_)); +} + +# Override the Parser::UOP::plus to allow it to be used on a constant, but +# mark any other usage as computed rather than pure + +package context::SignificantFigures::UOP::plus; +our @ISA = ('Parser::UOP::plus', 'context::SignificantFigures::common'); + +sub _check { + my $self = shift; + $self->SUPER::_check(@_); + my $op = $self->{op}; + $self->{pure} = 1 if $op->class eq 'Number' && $self->isPure($op) && !$op->{hadPlus}; + $self->{hadPlus} = 1; + $self->checkLimitedUOP('+'); +} + +sub _eval { + my $self = shift; + return $self->COPY($self, $self->SUPER::_eval(@_)->with(hadPlus => 1)); +} + +# Override Parser::BOP::multiply to handle the formation of an exponential form + +package context::SignificantFigures::BOP::multiply; +our @ISA = ('Parser::BOP::multiply', 'context::SignificantFigures::common'); + +sub _check { + my $self = shift; + $self->SUPER::_check(@_); + $self->checkExponentialForm; +} + +sub _eval { + my $self = shift; + return $self->COPY($self, $self->SUPER::_eval(@_)); +} + +sub string { + my $self = shift; + return $self->STRING(sub { $self->SUPER::string }, @_); +} + +sub TeX { + my $self = shift; + return $self->TEX(sub { $self->SUPER::TeX }, @_); +} + +# Override implicit multiplication to form exponential form when we have +# a number (implicitly) times the 'x' (implicitly) times a power of 10. + +package context::SignificantFigures::BOP::space; +our @ISA = ('Parser::BOP::multiply', 'context::SignificantFigures::common'); + +sub _check { + my $self = shift; + $self->SUPER::_check(@_); + Value::Error("Can't use implied multiplication in this context") if $self->context->flag('limitedSigFigs'); + my ($l, $r) = ($self->{lop}, $self->{rop}); + return unless $r->{tenPower} && !$r->{hadParens} && $l->class eq 'BOP' && $l->{bop} eq ' '; + my ($L, $R) = ($l->{lop}, $l->{rop}); + if ($R->class eq 'Variable' && $R->{name} eq 'x' && $self->isPure($L)) { + # + # Mark the 10 as infinite precision and remove the 'x' and its multiplication, + # leaving only the number and the power of ten being multiplied. + # + $r = $r->{lop} if $r->class eq 'BOP'; + $r->{value}{sigfigs} = 'inf'; + $self->{lop} = $l->{lop}; + $self->{isConstant} = 1; + $self->{expForm} = 1; + # + # Set the string and TeX values for exponential form. + # + $self->{def} = { %{ $self->{def} }, string => 'x', TeX => '\times' }; + # + # Remove the variable from the expression if this was the only occurrence of 'x'. + # + my $equation = $self->{equation}; + $equation->{vCount}{x}--; + delete $equation->{variables}{x} if $equation->{vCount}{x} == 0; + } +} + +sub eval { + my $self = shift; + return $self->COPY($self, $self->SUPER::eval(@_)); +} + +sub string { + my $self = shift; + return $self->STRING(sub { $self->SUPER::string }, @_); +} + +sub TeX { + my $self = shift; + return $self->TEX(sub { $self->SUPER::TeX }, @_); +} + +# Override Parser::BOP::power to mark occurrences of ten-to-a-power so we can +# recognize them when checking for exponential form + +package context::SignificantFigures::BOP::power; +our @ISA = ('Parser::BOP::power', 'context::SignificantFigures::common'); + +sub _check { + my $self = shift; + $self->SUPER::_check(@_); + my ($l, $r) = ($self->{lop}, $self->{rop}); + delete $r->{hadParens} if $self->isPureParen($r); + if ($l->class eq 'Number' && $self->isPure($r)) { + my ($L, $R) = ($l->value, $r->eval->value); + $self->{tenPower} = 1 if $L == 10 && $self->isPure($l) && CORE::int($R) == $R; + } + $self->checkLimited(); +} + +sub isPureParen { + my ($self, $r) = @_; + return $r->{hadParens} && $r->{pure} && $r->class eq 'UOP'; +} + +sub checkLimited { + my $self = shift; + return unless $self->context->flag('limitedSigFigs'); + Value::Error("Exponents can only be used with a base of 10 here") unless $self->{lop}->value == 10; + Value::Error("Exponents can only be integers") unless $self->{tenPower}; +} + +1; diff --git a/t/contexts/significant_figures.t b/t/contexts/significant_figures.t new file mode 100644 index 000000000..643dd8026 --- /dev/null +++ b/t/contexts/significant_figures.t @@ -0,0 +1,551 @@ +#!/usr/bin/env perl + +=head1 SignificantFigure context + +Test the SignifcantFigure context defined in contextSignificantFigure.pl. + +=cut + +use Test2::V0 '!E', { E => 'EXISTS' }; + +die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT}; +do "$ENV{PG_ROOT}/t/build_PG_envir.pl"; + +use lib "$ENV{PG_ROOT}/lib"; + +loadMacros('contextSignificantFigures.pl'); + +use Value; +require Parser::Legacy; +import Parser::Legacy; + +use Data::Dumper; + +Context('SignificantFigures'); + +sub ROUND {&context::SignificantFigures::Real::ROUND} +sub expon { context::SignificantFigures::Real->expFor($_[0], 0) } +sub FORMAT { context::SignificantFigures::Real->format(@_) } + +subtest 'Test the helper functions.' => sub { + + is ROUND(1.335, 3), '1.34', 'Round 1.335 to 3 sig figs.'; + is ROUND(12.34567, 5), '12.346', 'Round 12.346 to 5 sig figs.'; + is ROUND(1.005, 3), '1.01', 'Check that rounding up is working with non-perfect values'; + is ROUND(0.005, 0), 0.01, 'Rounding to 0 sigfigs goes to nearest multiple of 10'; + is ROUND(0.045, 0), 0, 'Rounding to 0 sigfigs down goes to 0'; + + my $a1 = Real('1'); # Needed to get a Real (SignificantFigure) MathObject. + is $a1->expFor('12.34567', 5), '1', 'Find the exponential part of 12.3456'; + is $a1->expFor('0.00124567', 5), '-3', 'Find the exponential part of 0.00123456'; + + is FORMAT('E', '123.546', 6), '1.23546E+02', 'Write 123.546 in exponential form.'; + is FORMAT('E', 0.8, 1), '8E-01', '0.8 = 8E-01'; + is FORMAT('f', '1.23E-01', 3), '0.123', 'Write 1.23E-01 in decimal form.'; + is FORMAT('f', '1.23E-03', 3), '0.00123', 'Write 1.23E-01 in decimal form.'; + is FORMAT('f', '5.283E+02', 4), '528.3', 'Write 5.283+02 in decimal form.'; + is FORMAT('f', '1.23E+00', 3), '1.23', 'Write 1.23E+00 in decimal form.'; + is FORMAT('f', '1.23E+01', 3), '12.3', 'Write 1.23E+01 in decimal form.'; + is FORMAT('f', '1.23E+06', 3), '1.23E+06', 'Write 1.23E+01 in decimal form.'; + + is expon('1000.32'), 3, 'Find the exponent of 1000.32.'; + is expon('0'), 0, 'Find the exponent of 0.'; + is expon('-0.00328'), -3, 'Find the exponent of -0.00328.'; +}; + +subtest 'Create numbers with significant digits using Real' => sub { + + ok my $a1 = Real('0012.34'), 'Creating the number 0012.34'; + is $a1->sigfigs, 4, '0012.34 has 4 significant figures.'; + is $a1->E, 1, '0012.34 written as 1.234 * 10^1'; + ok $a1->value, 12.34, '0012.34 = 12.34'; + is $a1->format('E'), '1.234E+01', 'Correct exponential/internal form'; + is $a1->string, '12.34', 'Correct string output.'; + is $a1->TeX, '{12.34}', 'Correct TeX output.'; + + ok my $a2 = Real('0.00314'), 'Creating the number 0.00314'; + is $a2->sigfigs, 3, '0.00314 has 3 significant figures.'; + is $a2->E, -3, '0.00314 = 3.14 * 10^(-3)'; + is $a2->format('E'), '3.14E-03', 'Correct exponential/internal form'; + is $a2->string, '0.00314', 'Correct string output.'; + is $a2->TeX, '{0.00314}', 'Correct TeX output.'; + + ok my $a3 = Real('0.0031400'), 'Creating the number 0.0031400'; + is $a3->sigfigs, 5, '0.0031400 has 5 significant figures.'; + is $a3->E, -3, '0.0031400 = 3.1400 * 10^(-3)'; + is $a3->format('E'), '3.1400E-03', 'Correct exponential/internal form'; + is $a3->string, '0.0031400', 'Correct string output.'; + is $a3->TeX, '{0.0031400}', 'Correct TeX output.'; + + ok my $a4 = Real('0.31415'), 'Creating the number 0.31415'; + is $a4->sigfigs, 5, '0.31415 has 5 significant figures.'; + is $a4->E, -1, '0.31415 = 3.1415 * 10^(-1)'; + is $a4->format('E'), '3.1415E-01', 'Correct exponential/internal form'; + is $a4->string, '0.31415', 'Correct string output.'; + is $a4->TeX, '{0.31415}', 'Correct TeX output.'; + + ok my $a5 = Real('216.100'), 'Creating the number 216.100'; + is $a5->sigfigs, 6, '216.100 has 6 significant figures.'; + is $a5->E, 2, '216.100 = 2.16100 * 10^(2)'; + is $a5->format('E'), '2.16100E+02', 'Correct exponential/internal form'; + is $a5->string, '216.100', 'Correct string output.'; + is $a5->TeX, '{216.100}', 'Correct TeX output.'; + + ok my $a6 = Compute('1230000'), 'Creating the number 12300'; + is $a6->sigfigs, 3, '1230000 has 3 significant figures.'; + is $a6->E, 6, '1230000 = 1.23 * 10^6'; + is $a6->format('E'), '1.23E+06', 'Correct exponential/internal form of 1230000'; + is $a6->string, '1.23x10^6', 'Correct string output of 1230000'; + is $a6->TeX, '{1.23\times 10^{6}}', 'Correct TeX output of 1230000'; + + ok my $a7 = Compute('2'), 'Create the number 2'; + is $a7->sigfigs, 1, '2 has 1 significant figure.'; + is $a7->E, 0, '2 = 2 * 10^0'; + is $a7->format('E'), '2E+00', 'Correct exponential/internal form of 2'; + is $a7->string, '2', 'Correct string output of 2'; + is $a7->TeX, '{2}', 'Correct TeX output of 2'; + + ok my $a8 = Compute('-1.932'), 'Creating the number -1.932'; + is $a8->sigfigs, 4, '-1.932 has 4 significant figures.'; + is $a8->E, 0, '-1.932 = -1.932 * 10^(0)'; + is $a8->format('E'), '-1.932E+00', 'Correct exponential/internal form of -1.932'; + is $a8->string, '-1.932', 'Correct string output of -1.932'; + is $a8->TeX, '{-1.932}', 'Correct TeX output of -1.932.'; + + ok my $a9 = Compute('-12340000'), 'Creating the number -12340000'; + is $a9->sigfigs, 4, '-12340000 has 4 significant figures'; + is $a9->E, 7, '-12340000 = -1.234 * 10^(7)'; + is $a9->format('E'), '-1.234E+07', 'Correct exponential/internal form'; + is $a9->string, '-1.234x10^7', 'Correct string output.'; + is $a9->TeX, '{-1.234\times 10^{7}}', 'Correct TeX output.'; + + ok my $a10 = Compute('0.00000001234'), 'Creating the number 0.00000001234'; + is $a10->sigfigs, 4, '0.00000001234 has 4 significant figures'; + is $a10->E, -8, '0.00000001234 = 1.234 * 10^(-11)'; + is $a10->format('E'), '1.234E-08', 'Correct exponential/internal form'; + is $a10->string, '1.234x10^(-8)', 'Correct string output.'; + is $a10->TeX, '{1.234\times 10^{-8}}', 'Correct TeX output.'; + + ok my $a11 = Real('10', sigfigs => 'inf'), 'Creating the number 10 with infinite sigfigs.'; + is $a11->sigfigs, 'inf', 'This number has infinite sigfigs.'; + is $a11->E, 0, 'infinite sigfig numbers have exp = 0'; + is $a11->format('E'), '10', 'The number is stored in non-exponential form.'; + is $a11->string, '10', 'The correct string output.'; + is $a11->TeX, '{10}', 'The correct TeX output.'; + + my $a12 = Real('0.8'); + is $a12->sigfigs, 1, 'The number of sigfigs of 0.8 is 1.'; + is $a12->E, -1, '0.8 = 8 * 10^(-1)'; + is $a12->format('E'), '8E-01', 'The internal form is 8E-01'; + is $a12->string, '0.8', 'The string version is 0.8'; + is $a12->TeX, '{0.8}', 'The TeX version is {0.8}'; + + # check some forms of 0. + + my $a13 = Real('0.00'); + is $a13->sigfigs, 3, '0.00 has 2 sig figs.'; + is $a13->E, 0, 'The exponential of 0.00 is 0.'; + is $a13->format('E'), '0.00E+00', '0.00 is written in scientific notation as 0.00E+00'; + is $a13->string, '0.00', '0.00 is the correct string form.'; + is $a13->TeX, '{0.00}', 'The TeX version is {0.00}'; + + my $a14 = Real('0'); + is $a14->sigfigs, 'inf', '0 has infinite sigfigs.'; + is $a14->E, 0, 'The exponential of 0 is 0'; + is $a14->format('E'), '0', '0 written in scientific format is 0'; + is $a14->string, '0', 'The string version is 0'; + is $a14->TeX, '{0}', 'The TeX version is {0}'; + + my $a15 = Real('+00'); + is $a15->sigfigs, 'inf', '+00 has infinite sigfigs.'; + is $a15->E, 0, 'The exponential of +00 is 0'; + is $a15->format('E'), '0', '+00 written in scientific format is 0'; + is $a15->string, '0', 'The string version is 0'; + is $a15->TeX, '{0}', 'The TeX version is {0}'; + + my $a16 = Real('0.'); + is $a16->format('E'), '0E+00', 'Check the format of 0.'; + is $a16->sigfigs, 1, '0. has 1 signficant figure'; + is $a16->E, 0, 'The exponential of +00 is 0'; + is $a16->string, '0.', 'The string version is 0.'; + is $a16->TeX, '{0.}', 'The TeX version is {0.}'; +}; + +subtest 'Create numbers by specifying significant figures' => sub { + + my $a1 = Real(2, sigfigs => 3); + is $a1->format('E'), '2.00E+00', '2 with 3 sig figs is 2.00'; + is $a1->string, '2.00', 'Correct string output.'; + is $a1->TeX, '{2.00}', 'Correct TeX output.'; + + my $a2 = Real('0.25', sigfigs => 4); + is $a2->format('E'), '2.500E-01', '0.25 with 4 sig figs is 0.2500'; + is $a2->string, '0.2500', 'Correct string output.'; + is $a2->TeX, '{0.2500}', 'Correct TeX output.'; + + my $a3 = Real('5', sigfigs => 'inf'); + is $a3->format('E'), '5', "5 with infinite sig figs is stored as '5'"; + is $a3->sigfigs, 'inf', 'Ensure that the sigfigs is infinite'; + is $a3->string, '5', 'Correct string output.'; + is $a3->TeX, '{5}', 'Correct TeX output.'; +}; + +subtest 'Check for out of bounds significant figures' => sub { + + my $a1 = Real('1'); + like dies { Real('5', sigfigs => 20); }, qr/The number of significant figures must be less than 16/, + 'Try to create a real with more than 16 sigfigs.'; + like dies { $a1->sigfigs(20) }, qr/The number of significant figures must be less than 16/, + 'Try to set a number with more than 16 sigfigs.'; + + like dies { Real('5', sigfigs => -1); }, qr/The number of significant figures must be non-negative/, + 'Try to create a real with fewer than 1 sigfigs.'; + like dies { $a1->sigfigs(-1) }, qr/The number of significant figures must be non-negative/, + 'Try to set a number with fewer than 1 sigfigs.'; + + like dies { Real('5', sigfigs => 'eight'); }, qr/The number of significant figures must be an integer or "inf"/, + 'Try to create a real with non-numerical value'; + like dies { $a1->sigfigs('eight'); }, qr/The number of significant figures must be an integer or "inf"/, + 'Try to set a number with non-numerical value'; +}; + +subtest 'Creating numbers with significant figures using Compute' => sub { + + my $a1 = Compute('12.345'); + is $a1->format('E'), '1.2345E+01', 'Ensure that the internal storage of 12.345 is correct.'; + is $a1->string, '12.345', 'Ensure that the string output of 12.345 is correct.'; + is $a1->TeX, '{12.345}', 'Ensure that the TeX output of 12.345 is correct.'; + + # This test currently does not pass. + # my $a2 = Compute('1.0 * 10^2'); + # is $a2->format('E'), '1.0E+02', 'Ensure that the internal storage of 1.0 * 10^2 is correct.'; + +}; + +subtest 'Create numbers with significant digits using SigFig' => sub { + my $a1 = SigFigNumber(10000, 3); + is $a1->format('E'), '1.00E+04', 'Create 10000 with 3 sigfigs using SigFigNumber (check format).'; + is $a1->sigfigs, 3, 'Check that the created number has 3 sigfigs.'; + is $a1->E, 4, 'Check that the created number has the correct exponent.'; + + my $a2 = SigFigNumber(12.3456, 4); + is $a2->format('E'), '1.235E+01', 'Create 12.3456 with 4 sigfigs using SigFigNumber (check format).'; + is $a2->sigfigs, 4, 'Check that the created number has 4 sig figs.'; + is $a2->E, 1, 'Check that the created number has the correct exponent.'; + + my $a3 = SigFigNumber(123.45, 6); + is $a3->format('E'), '1.23450E+02', 'Create 123.45 with 6 sigfigs using SigFigNumber.'; + is $a3->sigfigs, 6, 'Check that the created number has 6 sigfigs.'; + is $a3->E, 2, 'Check that the created number has the correct exponent.'; + + my $a4 = SigFigNumber(2, 4); + is $a4->format('E'), '2.000E+00', '2 with 4 significant figures is 2.000 (check format)'; + is $a4->sigfigs, 4, 'Check that the created number has 4 sigfigs.'; + is $a4->E, 0, 'Check that the created number has the correct exponent.'; + + my $a5 = SigFigNumber('829342', sigfigs => 4); + is $a5->format('E'), '8.293E+05', 'Create 829342 using SigFigNumber with 4 sigfigs (check format)'; + is $a5->sigfigs, 4, 'Check that the created number has 4 sigfigs.'; + is $a5->E, 5, 'Check that the created number has the correct exponent.'; +}; + +subtest 'Set the number of significant digits' => sub { + my $a1 = Compute('12.34567'); + is $a1->sigfigs, 7, '12.34567 has 7 significant digits.'; + ok $a1->sigfigs(5), 'Call to the sigfigs method for n=5'; + is $a1->sigfigs, 5, 'The number has been changed to 5 sigfigs'; + is $a1->format('E'), '1.2346E+01', 'The format is now 1.2346E+01'; + + my $a2 = Compute('-123.45'); + is $a2->sigfigs, 5, '-123.45 has 5 significant digits'; + is $a2->sigfigs(7), 7, 'Change the number of signicant figures to 7'; + is $a2->format('E'), '-1.234500E+02', "The internal format is '-1.2345E+02'"; + + my $a3 = Real(100, sigfigs => 3); + is $a3->sigfigs('inf'), 'inf', "Set the sigfigs to 'inf'"; + is $a3->format("E"), '100', 'The interval format is now 100'; +}; + +subtest 'Multiplying two constants' => sub { + my $a1 = Real('12.34') * Real('0.314'); + is $a1->format('E'), '3.87E+00', '12.34*0.314 = 3.87 (check format)'; + is $a1->sigfigs, 3, '12.34*0.314 = 3.87 (check sigfigs)'; + is $a1->E, 0, '12.34*0.314 = 3.87 (check exp)'; + + my $a2 = Real('16.100') * Real('0.00043923'); + is $a2->format('E'), '7.0716E-03', '16.100*0.00043923 = 0.0070716 (check format)'; + is $a2->sigfigs, 5, '16.100*0.00043923 = 0.0070716 (check sigfigs)'; + is $a2->E, -3, '16.100*0.00043923 = 0.0070716 (check exp)'; + + my $a3 = Real('2.6') * Real('173.832'); + is $a3->format('E'), '4.5E+02', '2.6*173.832 = 450 (check format)'; + is $a3->sigfigs, 2, '2.6*173.832 = 450 (check sigfigs)'; + is $a3->E, 2, '2.6*173.832 = 450 (check exp)'; + + my $a4 = Real('7', sigfigs => 'inf') * Real('82.202'); + is $a4->format('E'), '5.7541E+02', '7*82.202 = 575.41 (check format)'; + is $a4->sigfigs, 5, '7*82.202 = 575.41 (check sigfigs)'; + is $a4->E, 2, '7*82.202 = 575.41 (check exp)'; +}; + +subtest 'Adding two constants' => sub { + my $a1 = Real('12.34') + Real('0.314'); + is $a1->format('E'), '1.265E+01', '12.34+0.314 = 12.65 (check format)'; + is $a1->sigfigs, 4, '12.34+0.314 = 12.65 (check sigfigs)'; + is $a1->E, 1, '12.34+0.314 = 12.65 (check exp)'; + + my $a2 = Real('1.234') + Real('2'); + is $a2->format('E'), '3E+00', '1.234 + 2 = 3 (check format)'; + is $a2->sigfigs, 1, '1.234 + 2 = 3 (check sigfigs)'; + is $a2->E, 0, '1.234 + 2 = 3 * 10^0 (check exp)'; + + my $a3 = Real('1.234') + Real('2.0'); + is $a3->format('E'), '3.2E+00', '1.234 + 2.0 = 3.2E+00 (check format)'; + is $a3->sigfigs, 2, '1.234 + 2.0 = 3.2 (check sigfigs)'; + is $a3->E, 0, '1.234 + 2.0 = 3.2 * 10^0 (check exp)'; + + my $a4 = Real('0.01234') + Real('2.0'); + is $a4->format('E'), '2.0E+00', '0.01234+2.0=2.0 (check format)'; + is $a4->sigfigs, 2, '0.01234+2.0=2.0 (check sigfigs)'; + is $a4->E, 0, '0.01234+2.0=2.0 * 10^0 (check exp)'; + + my $a5 = Real('-12.3') + Real('14.8676'); + is $a5->format('E'), '2.6E+00', '-12.3 + 14.8678 = 2.6 = 2.6E+00 (check format)'; + is $a5->sigfigs, 2, '-12.3 + 14.8678 = 2.6 (check sigfigs)'; + is $a5->E, 0, '-12.3 + 14.8678 = 2.6 (check exp)'; + + my $a6 = Real(-100.005) + Real(100); + is $a6->format('E'), '0E+00', '-100.005+100 = 0 (check format)'; + is $a6->sigfigs, 1, '-100.005+100 = 0. (1 sigfig)'; + +}; + +subtest 'Subtracting two constants' => sub { + my $a1 = Real('12.34') - Real('0.314'); + is $a1->format('E'), '1.203E+01', '12.34-0.314=12.03 (check format)'; + is $a1->sigfigs, 4, '12.34-0.314=12.03 (check sigfigs)'; + is $a1->E, 1, '12.34-0.314=12.03 (check exp)'; + + my $a2 = Real('20') - Real('10.645'); + is $a2->format('E'), '1E+01', '20-10.645 = 10 = 1E+01 (check format)'; + is $a2->sigfigs, 1, '20-10.645 = 10 (check sigfigs)'; + is $a2->E, 1, '20-10.645 = 10 (check exp)'; + + my $a3 = Real('2') - Real('1.234'); + is $a3->format('E'), '1E+00', '2-1.234 = 1 = 1E+00 (check format)'; + is $a3->sigfigs, 1, '2-1.234 = 1 (check sigfigs)'; + + my $a4 = Real('2.0') - Real('1.234'); + is $a4->format('E'), '8E-01', '2.0-1.234 = 0.8 = 8E-0.1 (check format)'; + is $a4->sigfigs, 1, '2.0 - 1.234 = 0.8 (check sigfigs)'; + is $a4->E, -1, '2.0 - 1.234 = 0.8 (check sigfigs)'; + + my $a5 = Real('10.49827') - Real('2.37'); + is $a5->format('E'), '8.13E+00', '10.49827 - 2.37 = 8.13 = 8.13E+00 (check format)'; + is $a5->sigfigs, 3, '10.49827 - 2.37 = 8.13 (check sigfigs)'; + is $a5->E, 0, '10.49827 - 2.37 = 8.13 (check exp)'; + + my $a6 = Real('253.32') - Real('10.2'); + is $a6->format('E'), '2.431E+02', '253.32 - 10.2 = 243.1 = 2.431E+02 (check format)'; + is $a6->sigfigs, 4, '253.32 - 10.2 = 243.1 (check sigfigs)'; + is $a6->E, 2, '253.32 - 10.2 = 243.1 (check exp)'; + + my $a7 = Real('12.34') - Real('12.21'); + is $a7->format('E'), '1.3E-01', '12.34 - 12.21 = 0.13 = 1.3E-01 (check format)'; + is $a7->sigfigs, 2, '12.34 - 12.21 = 0.13 = 1.3E-01 (check sigfigs)'; + is $a7->E, -1, '12.34 - 12.21 = 0.13 = 1.3E-01 (check exp)'; + + my $a8 = Real('2.0') - Real('1.234'); + is $a8->format('E'), '8E-01', '2.0-1.234 = 0.8 = 8E-01 (check format)'; + is $a8->sigfigs, 1, '2.0-1.234 = 1 (check sigfigs)'; + is $a8->E, -1, '2.0-1.234 = 8 * 10^(-1) (check exp)'; + + my $a9 = Real('2.0') - Real('0.01234'); + is $a9->format('E'), '2.0E+00', '2.0-0.01234 = 2.0 = 2.0E+00 (check format)'; + is $a9->sigfigs, 2, '2.0-0.01234 = 2.0 (check sigfigs)'; + is $a9->E, 0, '2.0-0.01234 = 2.0 * 10^(-1) (check exp)'; + + my $a10 = Real('10.35123') - Real('10.35'); + is $a10->format('E'), '0.00E+00', '10.35123 - 10.35 = 0.00 (check format)'; + is $a10->sigfigs, 3, '10.35123 - 10.35 = 0.00 (check 3 sigfigs).'; + is $a10->E, 0, '10.35123 - 10.35 = 0.00 (check exp).'; + + my $a11 = Real('1.03') - Real('0.005'); + is $a11->format('E'), '1.02E+00', '1.03 - .005 = 1.02 (check format)'; + is $a11->sigfigs, 3, '1.03 - .005 = 1.02 (check sigfigs)'; + is $a11->E, 0, '1.03 - .005 = 1.02 (check exp)'; +}; + +subtest 'Division' => sub { + my $a1 = Compute('1.000') / Compute('4.000'); + is $a1->format('E'), '2.500E-01', '1.000/4.000 = 0.2500 (check format)'; + is $a1->sigfigs, 4, '1.000/4.000 = 0.2500 has 4 sigfigs'; + is $a1->E, -1, '1.000/4.000 = 0.2500 = 2.500 * 10^(-1) (check exp)'; + + my $a2 = Compute('1.0000') / Compute('4.00'); + is $a2->format('E'), '2.50E-01', '1.000/4.00 = 0.250 (check format)'; + is $a2->sigfigs, 3, '1.0000/4.00 = 0.2500 has 3 sigfigs'; + is $a2->E, -1, '1.0000/4.00 = 0.2500 = 2.50 * 10^(-1) (check exp)'; + +}; + +subtest 'Significant Figures for integers' => sub { + my $a1 = Compute('1.0E+02'); + is $a1->format('E'), '1.0E+02', '1.0 * 10^2 internally is 1.0E+02'; +}; + +subtest 'Significant Figures for partial credit' => sub { + # test an actual problem + my $source = <<~'END_SOURCE'; + DOCUMENT(); + loadMacros("PGstandard.pl","PGML.pl",'contextSignificantFigures.pl'); + Context('SignificantFigures')->flags->set(tolerance => 0.001, + partial_incorrect_sf=>0.6, + partial_sf_within_tolerance => 0.8, + ); + $a=Real('123.0'); + BEGIN_PGML + [_]{$a} + END_PGML + ENDDOCUMENT(); + END_SOURCE + + ok my $pg = WeBWorK::PG->new( + r_source => \$source, + inputs_ref => { AnSwEr0001 => '123.0' }, + processAnswers => 1 + ), + 'Problem with given source string renders correctly.'; + + is $pg->{result}{score}, 1, 'The correct answer is scored correctly.'; + + my $pg2 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '123.00' }, + ); + + is $pg2->{result}{score}, 0.6, 'reduced credit is scored correctly'; + like $pg2->{answers}{AnSwEr0001}{ans_message}, qr/Incorrect number of significant figures/, + 'An answer with incorrect significant figures is processed showing message.'; + + my $pg3 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '1.230 * 10^2' }, + ); + + is $pg3->{result}{score}, 1, 'The answer given in scientific notation is processed correctly.'; + + my $pg4 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '1.23 * 10^2' }, + ); + + is $pg4->{result}{score}, 0.6, + 'The answer with incorrect number of sig. figs is given in sci. not. is scored with correct partial credit.'; + + like $pg4->{answers}{AnSwEr0001}{ans_message}, qr/Incorrect number of significant figures/, + 'The answer in scientific notation and incorrect number of signficant figures is processed showing message.'; + + my $pg5 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '123.1' }, + ); + + is $pg5->{result}{score}, 0.8, + 'The answer with correct number of significant figures but incorrect but close value is processed correctly.'; + + like $pg5->{answers}{AnSwEr0001}{ans_message}, + qr/Correct number of significant figures, but the value is not correct/, + 'Answer processed showing message.'; +}; + +subtest 'Significant Figures for partial credit' => sub { + + # test an actual problem + + my $source = <<~'END_SOURCE'; + DOCUMENT(); + loadMacros("PGstandard.pl","PGML.pl",'contextSignificantFigures.pl'); + Context('SignificantFigures')->flags->set(tolerance => 0.001, + partial_incorrect_sf=>0.6, + partial_sf_within_tolerance => 0.8, + ); + $a=Real('123.0'); + BEGIN_PGML + [_]{$a} + END_PGML + ENDDOCUMENT(); + END_SOURCE + + ok my $pg = WeBWorK::PG->new( + r_source => \$source, + inputs_ref => { AnSwEr0001 => '123.0' }, + processAnswers => 1 + ), + 'source string renders'; + + # print Dumper $pg->{result}; + + is $pg->{result}{score}, 1, 'correct answer is scored correctly'; + + my $pg2 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '123.00' }, + ); + + is $pg2->{result}{score}, 0.6, 'reduced credit is scored correctly'; + like $pg2->{answers}{AnSwEr0001}{ans_message}, qr/Incorrect number of significant figures/, + 'Answer processed showing message.'; + + my $pg3 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '1.230 * 10^2' }, + ); + + is $pg3->{result}{score}, 1, 'scientific notation is correct.'; + + my $pg4 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '1.23 * 10^2' }, + ); + + is $pg4->{result}{score}, 0.6, 'scientific notation is scored with correct partial credit.'; + + like $pg4->{answers}{AnSwEr0001}{ans_message}, qr/Incorrect number of significant figures/, + 'Answer processed showing message.'; + + my $pg5 = WeBWorK::PG->new( + r_source => \$source, + processAnswers => 1, + inputs_ref => { AnSwEr0001 => '123.1' }, + ); + + is $pg5->{result}{score}, 0.8, 'Answer has correct sf but incorrect value.'; + + like $pg5->{answers}{AnSwEr0001}{ans_message}, + qr/Correct number of significant figures, but the value is not correct/, + 'Answer processed showing message.'; +}; + +# The following is used the test if an expression (like an average) has a non sig fig +# perl number to get promoted to a sig fig with infinite precision. + +sub ave { + my $sum = 0; + $sum += $_ for (@_); + return $sum / @_; +} + +subtest 'Check promotion rules' => sub { + is ave(3.11, 10.49, 6.72), 6.77333333333333, 'check perl averages'; + is ave(Real(3.11), Real(10.49), Real(6.72)), 6.773, 'check average with sig fig'; +}; + +done_testing(); diff --git a/t/contexts/significant_figures_units.t b/t/contexts/significant_figures_units.t new file mode 100644 index 000000000..f061d7a86 --- /dev/null +++ b/t/contexts/significant_figures_units.t @@ -0,0 +1,45 @@ +#!/usr/bin/env perl + +=head1 SignificantFigure context + +Test the SignificantFigure context defined in contextSignificantFigure.pl. + +=cut + +use Test2::V0 '!E', { E => 'EXISTS' }; + +die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT}; +do "$ENV{PG_ROOT}/t/build_PG_envir.pl"; + +use lib "$ENV{PG_ROOT}/lib"; + +# load the Units module so that %Units::known_units is populated +use Units; +use Value; +require Parser::Legacy; +import Parser::Legacy; + +loadMacros('contextSignificantFigures.pl', 'contextUnits.pl'); + +my $context = context::Units::extending("SignificantFigures")->withUnitsFor('length'); + +subtest 'Setup a basic Unit context extending SignificantFigures' => sub { + Context($context); # make it current without copying + ok(defined $context && ref($context), 'Got a context object'); + is $context->{name}, 'Units-SignificantFigures', 'Context has correct name'; +}; + +subtest 'Test a number with length units and significant figures' => sub { + Context($context); + ok my $a = Compute("123.0 cm"), 'Compute handles a unit.'; + + is $a, '123.0 cm', 'Value stringifies with units and sig figs'; + ok $a == Compute('1.230 m'), 'Value stringifies with correct unit conversion and sig figs'; + ok $a != Compute('123 cm'), 'Value does not lose significant figure information when stringified'; + + ok $a == Compute('4.035 ft'), 'Value in feet'; + ok $a == Compute('4.034 ft'), 'Value in feet (a little off, but when converted to m is correct)'; + ok $a == Compute('4.036 ft'), 'Value in feet (a little off, but when converted to m is correct)'; +}; + +done_testing;