From b44b71e866c8f21d6f098eebfd9810789db2e5a6 Mon Sep 17 00:00:00 2001 From: Michael Gage <1203580+mgage@users.noreply.github.com> Date: Tue, 20 Jul 2021 06:30:02 -0400 Subject: [PATCH 01/38] fix type replace assemble_matrix with assemble_tableau (#591) --- macros/tableau.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/tableau.pl b/macros/tableau.pl index 95d42f8725..1eaab6cd87 100755 --- a/macros/tableau.pl +++ b/macros/tableau.pl @@ -515,7 +515,7 @@ sub initialize { $self->{align} = ($self->{align})//$myAlignString; $self->{S} = Value::Matrix->I($m); $self->{basis_columns} = [($n+1)...($n+$m)] unless ref($self->{basis_columns})=~/ARRAY/; - my @rows = $self->assemble_matrix; + my @rows = $self->assemble_tableau; $self->M( _Matrix([@rows]) ); #original matrix $self->{data}= $self->M->data; my $new_obj_row = $self->objective_row; From c5efa09ee6b8a3e56405f434cbaef0473a4b6b4c Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Tue, 10 Aug 2021 07:00:31 -0400 Subject: [PATCH 02/38] Resolve problem with space after function to a power. (#598) --- lib/Parser.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Parser.pm b/lib/Parser.pm index d10af497db..d3a249932c 100644 --- a/lib/Parser.pm +++ b/lib/Parser.pm @@ -266,7 +266,7 @@ sub pushBlankOperand { # Otherwise, report the missing operand for this operator # sub Op { - my $self = shift; my $name = shift; + my $self = shift; my $name = shift; my $NAME = $name; my $ref = $self->{ref} = shift; my $context = $self->{context}; my $op; ($name,$op) = $context->operators->resolve($name); @@ -283,10 +283,10 @@ sub Op { $self->pushOperand($self->Item("UOP")->new($self,$name,$top->{value},$ref)); } else { my $def = $context->operators->resolveDef(' '); - $name = $def->{string} if defined($name) and ($name eq ' ' or $name eq $def->{space}); + $name = $def->{string} if defined($NAME) and ($NAME eq ' ' or $NAME eq $def->{space}); $self->pushOperator($name,$op->{precedence}); } - } elsif (($ref && $name ne ' ') || $self->state ne 'fn') {$self->Op($name,$ref)} + } elsif (($ref && $NAME ne ' ') || $self->state ne 'fn') {$self->Op($NAME,$ref)} } } else { ($name,$op) = $context->operators->resolve('u'.$name) From 76d11db6815360c651a71337fb68163e982b3161 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 14 Oct 2021 20:25:46 -0700 Subject: [PATCH 03/38] fix calls to Parser::Value::TeX & too many (cooks) --- macros/contextFraction.pl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index 2c5585a93a..f44686c3b1 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -618,7 +618,7 @@ sub reduce { # sub string { my $self = shift; - my $string = $self->SUPER::string($self, @_); + my $string = $self->SUPER::string(@_); return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; @@ -627,15 +627,17 @@ sub string { } # -# Add parentheses if they are needed by precedence +# Add parentheses if they were there originally, or +# are needed by precedence and we asked for exxxtra parens # sub TeX { my $self = shift; - my $string = $self->SUPER::TeX($self, @_); + my $string = $self->SUPER::TeX(@_); return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; - $string = '\left(' . $string . '\right)' if defined $precedence && $precedence > $frac; + $string = '\left(' . $string . '\right)' + if defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1; return $string; } @@ -898,7 +900,6 @@ sub string { if ($self->getFlagWithAlias("showMixedNumbers","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"; } @@ -911,7 +912,6 @@ sub TeX { 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"; } From 3edee9166201369b0403e6d8b8374bd37225185b Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 15 Oct 2021 08:54:45 -0700 Subject: [PATCH 04/38] forgot to include explicit parens --- macros/contextFraction.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index f44686c3b1..03c980499b 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -636,8 +636,8 @@ sub TeX { return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; - $string = '\left(' . $string . '\right)' - if defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1; + $string = '\left(' . $string . '\right)' if $self->{hadParens} || + (defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1); return $string; } From f90297b90aaf97068887e64c95d6686f49d2fe07 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 15 Oct 2021 14:54:24 -0700 Subject: [PATCH 05/38] drop custom typeMatch & let SUPER do it instead --- macros/parserAssignment.pl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl index 9dd9840ab8..cb63d563bf 100644 --- a/macros/parserAssignment.pl +++ b/macros/parserAssignment.pl @@ -351,18 +351,6 @@ sub new { return $f; } -sub typeMatch { - my $self = shift; my $other = shift; my $ans = shift; - return 0 unless $self->type eq $other->type; - $other = $other->Package("Formula")->new($self->context,$other) unless $other->isFormula; - my $typeMatch = ($self->createRandomPoints(1))[1]->[0]{data}[1]; - $main::__other__ = sub {($other->createRandomPoints(1))[1]->[0]{data}[1]}; - $other = main::PG_restricted_eval('&$__other__()'); - delete $main::{__other__}; - return 1 unless defined($other); # can't really tell, so don't report type mismatch - $typeMatch->typeMatch($other,$ans); -} - sub cmp_class { my $self = shift; my $value; if ($self->{tree}{rop}{isConstant}) { From c4a26ee75831c336bec50387dbf8191b9fe39247 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Sat, 16 Oct 2021 09:06:45 -0700 Subject: [PATCH 06/38] don't declare outermost parens --- lib/Parser.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Parser.pm b/lib/Parser.pm index d3a249932c..b2d1affd68 100644 --- a/lib/Parser.pm +++ b/lib/Parser.pm @@ -401,8 +401,8 @@ sub Close { ($top->type eq 'Comma') ? $top->entryType : $top->typeRef, ($type ne 'start') ? ($open,$paren->{close}) : () )}; } else { - $top->{value}{hadParens} = 1; - } + $top->{value}{hadParens} = 1 unless $open eq 'start'; + } $self->pop; $self->push($top); $self->CloseFn() if ($paren->{function} && $self->prev->{type} eq 'fn'); } elsif ($paren->{formInterval} eq $type && $self->top->{value}->length == 2) { From 7d0d3e4336d1b8d7905d8d6e2caf64f10e1cba25 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Wed, 20 Oct 2021 16:17:54 -0700 Subject: [PATCH 07/38] Update macros/contextFraction.pl Co-authored-by: Davide P. Cervone --- macros/contextFraction.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index 03c980499b..2644529566 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -636,8 +636,9 @@ sub TeX { return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; + my $noparens = shift; $string = '\left(' . $string . '\right)' if $self->{hadParens} || - (defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1); + (defined $precedence && $precedence > $frac && !$noparens); return $string; } From 0e591bf2d5a959b1ce16894b6ed731fe6bb4236a Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 4 Nov 2021 08:32:39 -0700 Subject: [PATCH 08/38] restore typeMatch and block string assignment --- macros/parserAssignment.pl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl index cb63d563bf..c3f0feaef7 100644 --- a/macros/parserAssignment.pl +++ b/macros/parserAssignment.pl @@ -348,9 +348,20 @@ sub new { my $self = shift; $class = ref($self) || $self; my $f = $self->SUPER::new(@_); bless $f, $class if $f->type eq 'Assignment'; + my $rhs = $f->getTypicalValue($f)->{data}[1]; + Value->Error('Assignment of strings is not allowed.') if $rhs && $rhs->type eq 'String'; return $f; } +sub typeMatch { + my $self = shift; my $other = shift; my $ans = shift; + return 0 unless $self->type eq $other->type; + my $typeMatch = $self->getTypicalValue($self)->{data}[1]; + $other = $self->getTypicalValue($other,1)->{data}[1]; + return 1 unless defined($other); # can't really tell, so don't report type mismatch + $typeMatch->typeMatch($other,$ans); +} + sub cmp_class { my $self = shift; my $value; if ($self->{tree}{rop}{isConstant}) { From 302ba3fa3003dc5dd4cb3355a73a22058ddc0461 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 27 Dec 2021 21:09:19 -0600 Subject: [PATCH 09/38] Fix an issue introduced by #609 in which equations parsed in PGML via `[:: ::]` now fail to parse. --- macros/parserAssignment.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl index c3f0feaef7..4e2ec2baa7 100644 --- a/macros/parserAssignment.pl +++ b/macros/parserAssignment.pl @@ -347,7 +347,8 @@ package parser::Assignment::Formula; sub new { my $self = shift; $class = ref($self) || $self; my $f = $self->SUPER::new(@_); - bless $f, $class if $f->type eq 'Assignment'; + return $f unless $f->type eq 'Assignment'; + bless $f, $class; my $rhs = $f->getTypicalValue($f)->{data}[1]; Value->Error('Assignment of strings is not allowed.') if $rhs && $rhs->type eq 'String'; return $f; From 5b90c7fd22dcd8096f2542defd8ab0c8a89aa6a2 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 28 Jan 2022 19:45:53 -0600 Subject: [PATCH 10/38] Fix another regression caused by #609. If a problem sets the answer to be a formula with an assigment, (something like `Formula("x = 5")` then all answers are counted incorrect and a warning is displayed that the evaluated answer is not an answer hash. This worked prior to #609. The cause of this was the removal of a line that should not have been removed that converts the other answer being compared to to a Formula if it is not already. Note that the original issue that was attempted to be fixed by #609 still is fixed with this change. This fixes issue #644 that I just submitted about this. --- macros/parserAssignment.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl index 4e2ec2baa7..763f09e840 100644 --- a/macros/parserAssignment.pl +++ b/macros/parserAssignment.pl @@ -357,6 +357,7 @@ sub new { sub typeMatch { my $self = shift; my $other = shift; my $ans = shift; return 0 unless $self->type eq $other->type; + $other = $other->Package("Formula")->new($self->context,$other) unless $other->isFormula; my $typeMatch = $self->getTypicalValue($self)->{data}[1]; $other = $self->getTypicalValue($other,1)->{data}[1]; return 1 unless defined($other); # can't really tell, so don't report type mismatch From 3851503883a36b9bc464f1ba476577000bbd57ba Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 19 Apr 2022 11:27:13 -0500 Subject: [PATCH 11/38] Update the copyright in files and version bump. --- LICENSE | 2 +- README | 2 +- VERSION | 4 +- htdocs/js/apps/Problem/problem.scss | 2 +- lib/AnswerHash.pm | 180 ++++++++++++------------ lib/DragNDrop.pm | 2 +- lib/LaTeXImage.pm | 6 +- lib/PGEnvironment.pm | 2 +- lib/PGUtil.pm | 7 +- lib/PGalias.pm | 3 +- lib/PGanswergroup.pm | 7 +- lib/PGcore.pm | 3 +- lib/PGloadfiles.pm | 7 +- lib/PGresource.pm | 7 +- lib/PGresponsegroup.pm | 7 +- lib/Statistics.pm | 8 +- lib/WeBWorK/EquationCache.pm | 24 +++- lib/WeBWorK/PG/IO.pm | 14 +- lib/WeBWorK/PG/IO/Daemon2.pm | 14 +- lib/WeBWorK/PG/IO/WW1.pm | 34 +++-- lib/WeBWorK/PG/IO/WW2.pm | 14 +- lib/WeBWorK/PG/ImageGenerator.pm | 78 ++++++----- lib/WeBWorK/PG/Translator.pm | 192 ++++++++++++++------------ macros/LinearProgramming.pl | 7 +- macros/MathObjects.pl | 7 +- macros/PG.pl | 3 +- macros/PGanswermacros.pl | 7 +- macros/PGauxiliaryFunctions.pl | 6 +- macros/PGbasicmacros.pl | 4 +- macros/PGchoicemacros.pl | 5 +- macros/PGcomplexmacros.pl | 21 ++- macros/PGcomplexmacros2.pl | 18 ++- macros/PGessaymacros.pl | 7 +- macros/PGfunctionevaluators.pl | 7 +- macros/PGinfo.pl | 20 ++- macros/PGlateximage.pl | 6 +- macros/PGmiscevaluators.pl | 7 +- macros/PGnumericevaluators.pl | 7 +- macros/PGstringevaluators.pl | 7 +- macros/PGtextevaluators.pl | 7 +- macros/PGtikz.pl | 6 +- macros/Parser.pl | 7 +- macros/answerComposition.pl | 7 +- macros/answerCustom.pl | 7 +- macros/answerHints.pl | 7 +- macros/answerVariableList.pl | 7 +- macros/compoundProblem5.pl | 7 +- macros/contextABCD.pl | 7 +- macros/contextAlternateDecimal.pl | 7 +- macros/contextAlternateIntervals.pl | 7 +- macros/contextComplexExtras.pl | 3 +- macros/contextComplexJ.pl | 7 +- macros/contextCongruence.pl | 7 +- macros/contextCurrency.pl | 7 +- macros/contextInequalities.pl | 7 +- macros/contextInequalitySetBuilder.pl | 7 +- macros/contextInteger.pl | 7 +- macros/contextIntegerFunctions.pl | 7 +- macros/contextLimitedComplex.pl | 7 +- macros/contextLimitedNumeric.pl | 7 +- macros/contextLimitedPoint.pl | 7 +- macros/contextLimitedPolynomial.pl | 7 +- macros/contextLimitedPowers.pl | 7 +- macros/contextLimitedVector.pl | 7 +- macros/contextMatrixExtras.pl | 3 +- macros/contextPartition.pl | 7 +- macros/contextPercent.pl | 7 +- macros/contextPeriodic.pl | 7 +- macros/contextPermutation.pl | 7 +- macros/contextPermutationUBC.pl | 7 +- macros/contextPiecewiseFunction.pl | 7 +- macros/contextPolynomialFactors.pl | 7 +- macros/contextRationalFunction.pl | 7 +- macros/contextScientificNotation.pl | 7 +- macros/contextString.pl | 7 +- macros/contextTF.pl | 7 +- macros/contextTrigDegrees.pl | 7 +- macros/draggableProof.pl | 2 +- macros/draggableSubsets.pl | 2 +- macros/extraAnswerEvaluators.pl | 7 +- macros/parserAssignment.pl | 7 +- macros/parserAutoStrings.pl | 7 +- macros/parserCustomization.pl | 7 +- macros/parserDifferenceQuotient.pl | 7 +- macros/parserFormulaAnyVar.pl | 7 +- macros/parserFormulaUpToConstant.pl | 7 +- macros/parserFormulaWithUnits.pl | 7 +- macros/parserFunction.pl | 7 +- macros/parserFunctionPrime.pl | 3 +- macros/parserGraphTool.pl | 2 +- macros/parserImplicitEquation.pl | 7 +- macros/parserImplicitPlane.pl | 7 +- macros/parserLinearInequality.pl | 7 +- macros/parserMultiAnswer.pl | 7 +- macros/parserMultiPart.pl | 7 +- macros/parserNumberWithUnits.pl | 7 +- macros/parserOneOf.pl | 7 +- macros/parserParametricLine.pl | 7 +- macros/parserParametricPlane.pl | 7 +- macros/parserPopUp.pl | 7 +- macros/parserPrime.pl | 7 +- macros/parserRadioButtons.pl | 3 +- macros/parserRoot.pl | 7 +- macros/parserSolutionFor.pl | 7 +- macros/parserVectorUtils.pl | 7 +- macros/parserWordCompletion.pl | 7 +- macros/problemPanic.pl | 7 +- macros/problemPreserveAnswers.pl | 7 +- macros/problemRandomize.pl | 7 +- macros/scaffold.pl | 2 +- 110 files changed, 620 insertions(+), 598 deletions(-) diff --git a/LICENSE b/LICENSE index 8df1667545..980aae59d3 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Online Homework Delivery System Version 2.* - Copyright 2000-2018, The WeBWorK Project + Copyright 2000-2022, The WeBWorK Project All rights reserved. This program is free software; you can redistribute it and/or modify diff --git a/README b/README index 2326906afb..45ed6d2993 100644 --- a/README +++ b/README @@ -6,6 +6,6 @@ http://webwork.maa.org/wiki/Category:Release_Notes - Copyright 2000-2017, The WeBWorK Project + Copyright 2000-2022, The WeBWorK Project http://webwork.maa.org All rights reserved. diff --git a/VERSION b/VERSION index e9d9c39ee9..0048b529d4 100644 --- a/VERSION +++ b/VERSION @@ -1,4 +1,4 @@ -$PG_VERSION ='2.16+develop'; -$PG_COPYRIGHT_YEARS = '1996-2021'; +$PG_VERSION ='2.17'; +$PG_COPYRIGHT_YEARS = '1996-2022'; 1; diff --git a/htdocs/js/apps/Problem/problem.scss b/htdocs/js/apps/Problem/problem.scss index 465f12e7e9..d85a39204e 100644 --- a/htdocs/js/apps/Problem/problem.scss +++ b/htdocs/js/apps/Problem/problem.scss @@ -1,5 +1,5 @@ /* WeBWorK Online Homework Delivery System - * Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork + * Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork * * 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 diff --git a/lib/AnswerHash.pm b/lib/AnswerHash.pm index 8a029cc8a4..f199b4266f 100755 --- a/lib/AnswerHash.pm +++ b/lib/AnswerHash.pm @@ -3,16 +3,26 @@ ## ## Provides a data structure for answer hashes. Currently just a wrapper ## for the hash, but that might change -#################################################################### -# Copyright @ 1995-2002 WeBWorK Team -# All Rights Reserved -#################################################################### +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. +################################################################################ #$Id$ =head1 NAME AnswerHash.pm -- located in the courseScripts directory - + This file contains the packages/classes: AnswerHash and AnswerEvaluator @@ -20,14 +30,14 @@ AnswerHash -- this class stores information related to the student's answer. It is little more than a standard perl hash with - a special name, but it does have some access and + a special name, but it does have some access and manipulation methods. More of these may be added as it becomes necessary. - + Usage: $rh_ans = new AnswerHash; - + AnswerEvaluator -- this class organizes the construction of - answer evaluator subroutines which check the + answer evaluator subroutines which check the student's answer. By plugging filters into the answer evaluator class you can customize the way the student's answer is normalized and checked. Our hope @@ -36,7 +46,7 @@ combinations to obtain different answer evaluators, thus greatly reducing the programming and maintenance required for constructing answer evaluators. - + Usage: $ans_eval = new AnswerEvaluator; =cut @@ -72,16 +82,16 @@ The answer hash class is guaranteed to contain the following instance variables: This is displayed in the section reporting the results of checking the student answers. - $ans_hash->{original_student_ans} -- This is the original student answer. + $ans_hash->{original_student_ans} -- This is the original student answer. This is displayed on the preview page and may be used for sticky answers. - $ans_hash->{ans_message} -- Any error message, or hint provided by + $ans_hash->{ans_message} -- Any error message, or hint provided by the answer evaluator. This is also displayed in the section reporting the results of checking the student answers. - $ans_hash->{type} -- A string indicating the type of answer evaluator. + $ans_hash->{type} -- A string indicating the type of answer evaluator. This helps in preprocessing the student answer for errors. Some examples: 'number_with_units' @@ -97,13 +107,13 @@ The answer hash class is guaranteed to contain the following instance variables: same as $ans_hash{student_ans}. - $ans_hash->{preview_latex_string} -- + $ans_hash->{preview_latex_string} -- THIS IS OPTIONAL. This is latex version of the student answer which is used to show a typeset view on the answer on the preview page. For a student answer of 2/3, this would be \frac{2}{3}. 'ans_message' => '', # null string - + 'preview_text_string' => undef, 'preview_latex_string' => undef, 'error_flag' => undef, @@ -116,7 +126,7 @@ The answer hash class is guaranteed to contain the following instance variables: BEGIN { # main::be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. - + } package AnswerHash; @@ -140,14 +150,14 @@ my %fields = ( 'score' => undef, =head4 new Useage $rh_anshash = new AnswerHash; - + returns an object of type AnswerHash. - + =cut sub new { my $class = shift @_; - + my $self = { 'score' => 0, 'correct_ans' => 'No correct answer specified', 'student_ans' => undef, @@ -161,10 +171,10 @@ sub new { 'error_message' => '', }; # return a reference to a hash. - + bless $self, $class; $self -> setKeys(@_); - + return $self; } @@ -172,26 +182,26 @@ sub new { ## Checks to make sure that the keys are valid, ## then sets their value -=head4 setKeys - - $rh_ans->setKeys(score=>1, student_answer => "yes"); +=head4 setKeys + + $rh_ans->setKeys(score=>1, student_answer => "yes"); Sets standard elements in the AnswerHash (the ones defined above). Will give error if one attempts to set non-standard keys. - + To set a non-standard element in a hash use - + $rh_ans->{non-standard-key} = newValue; - + There are no safety checks when using this method. =cut - + sub setKeys { my $self = shift; my %inits = @_; foreach my $item (keys %inits) { - if ( exists $fields{$item} ) { + if ( exists $fields{$item} ) { $self -> {$item} = $inits{$item}; } else { @@ -206,14 +216,14 @@ sub setKeys { Usage: $rh_ans->data('foo'); set $rh_ans->{student_ans} = 'foo'; $student_input = $rh_ans->data(); retrieve value of $rh_ans->{student_ans} - + synonym for input -=head4 input +=head4 input Usage: $rh_ans->input('foo') sets $rh_ans->{student_ans} = 'foo'; $student_input = $rh_ans->input(); - + synonym for data =cut @@ -230,16 +240,16 @@ sub input { #$rh_ans->input('foo') is a synonym for $rh_ans->{student_ans}=' $self->{student_ans} } -=head4 input +=head4 input - Usage: $rh_ans->score(1) + Usage: $rh_ans->score(1) $score = $rh_ans->score(); - + Retrieve or set $rh_ans->{score}, the student's score on the problem. =cut -sub score { +sub score { my $self = shift; my $score = shift; $self->{score} = $score if defined($score); @@ -276,20 +286,20 @@ sub stringify_hash { =head4 throw_error Usage: $rh_ans->throw_error("FLAG", "message"); - - FLAG is a distinctive word that describes the type of error. + + FLAG is a distinctive word that describes the type of error. Examples are EVAL for an evaluation error or "SYNTAX" for a syntax error. The entry $rh_ans->{error_flag} is set to "FLAG". - + The catch_error and clear_error methods use this entry. - + message is a descriptive message for the end user, defining what error occured. =head4 catch_error Usage: $rh_ans->catch_error("FLAG2"); - + Returns true (1) if $rh_ans->{error_flag} equals "FLAG2", otherwise it returns false (empty string). @@ -298,8 +308,8 @@ sub stringify_hash { =head4 clear_error Usage: $rh_ans->clear_error("FLAG2"); - - If $rh_ans->{error_flag} equals "FLAG2" then the {error_flag} entry is set to + + If $rh_ans->{error_flag} equals "FLAG2" then the {error_flag} entry is set to the empty string as is the entry {error_message} =head4 error_flag @@ -307,11 +317,11 @@ sub stringify_hash { =head4 error_message Usage: $flag = $rh_ans -> error_flag(); - + $message = $rh_ans -> error_message(); - Retrieve or set the {error_flag} and {error_message} entries. - + Retrieve or set the {error_flag} and {error_message} entries. + Use catch_error and throw_error where possible. =cut @@ -359,15 +369,15 @@ sub error_message { # error print out method # =head4 pretty_print -# -# +# +# # Usage: $rh_ans -> pretty_print(); -# -# +# +# # Returns a string containing a representation of the AnswerHash as an HTML table. -# +# # =cut -# +# # sub pretty_print { # my $r_input = shift; # my $level = shift; @@ -391,7 +401,7 @@ sub error_message { # while (@array) { # $out .= pretty_print(shift @array, $level) . " , "; # } -# $out .= " )"; +# $out .= " )"; # } elsif (ref($r_input) eq 'CODE') { # $out = "$r_input"; # } else { @@ -401,14 +411,14 @@ sub error_message { # $out; # } -# action methods +# action methods =head4 OR Usage: $rh_ans->OR($rh_ans2); - + Returns a new AnswerHash whose score is the maximum of the scores in $rh_ans and $rh_ans2. - The correct answers for the two hashes are combined with "OR". + The correct answers for the two hashes are combined with "OR". The types are concatenated with "OR" as well. Currently nothing is done with the error flags and messages. @@ -418,9 +428,9 @@ sub error_message { Usage: $rh_ans->AND($rh_ans2); - + Returns a new AnswerHash whose score is the minimum of the scores in $rh_ans and $rh_ans2. - The correct answers for the two hashes are combined with "AND". + The correct answers for the two hashes are combined with "AND". The types are concatenated with "AND" as well. Currently nothing is done with the error flags and messages. @@ -433,11 +443,11 @@ sub error_message { sub OR { my $self = shift; - + my $rh_ans2 = shift; my %options = @_; return($self) unless defined($rh_ans2) and ref($rh_ans2) eq 'AnswerHash'; - + my $out_hash = new AnswerHash; # score is the maximum of the two scores $out_hash->{score} = ( $self->{score} < $rh_ans2->{score} ) ? $rh_ans2->{score} :$self->{score}; @@ -498,17 +508,17 @@ use PGUtil qw(not_null pretty_print); sub new { my $class = shift @_; - + my $self = { pre_filters => [ [\&blank_prefilter] ], evaluators => [], post_filters => [ [\&blank_postfilter] ], debug => 0, rh_ans => new AnswerHash, - + }; - + bless $self, $class; - $self->rh_ans(@_); #initialize answer hash + $self->rh_ans(@_); #initialize answer hash return $self; } sub clone { @@ -527,15 +537,15 @@ sub dereference_array_ans { } $rh_ans; } - + sub get_student_answer { my $self = shift; - my $input = shift; + my $input = shift; my %answer_options = @_; my $display_input = $input; $display_input =~ s/\0/\\0/g; # make null spacings visible eval (q!main::DEBUG_MESSAGE( "Raw student answer is |$display_input|")!) if $self->{debug}; - $input = '' unless defined($input); + $input = '' unless defined($input); if (ref($input) =~/AnswerHash/) { # in this case nothing needs to be done, since the student's answer is already in an answerhash. # This is useful when an AnswerEvaluator is used as a filter in another answer evaluator. @@ -544,17 +554,17 @@ sub get_student_answer { $self-> {rh_ans} -> {original_student_ans} = " ( " .join(", ",@input) . " ) "; $input = \@input; $self-> {rh_ans} -> {student_ans} = $input; - } elsif (ref($input) eq 'ARRAY' ) { # sometimes the answer may already be decoded into an array. + } elsif (ref($input) eq 'ARRAY' ) { # sometimes the answer may already be decoded into an array. my @input = @$input; $self-> {rh_ans} -> {original_student_ans} = " ( " .join(", ",@input) . " ) "; $input = \@input; $self-> {rh_ans} -> {student_ans} = $input; } else { - + $self-> {rh_ans} -> {original_student_ans} = $input; $self-> {rh_ans} -> {student_ans} = $input; } - $self->{rh_ans}->{ans_label} = $answer_options{ans_label} if defined($answer_options{ans_label}); + $self->{rh_ans}->{ans_label} = $answer_options{ans_label} if defined($answer_options{ans_label}); $self->{rh_ans}->{_filter_name} = 'get_student_answer'; $input; } @@ -572,12 +582,12 @@ sub evaluate { $self->get_student_answer(@_); # dereference $self->{rh_ans}; my $rh_ans = $self ->{rh_ans}; - $rh_ans->{error_flag}=undef; #reset the error flags in case + $rh_ans->{error_flag}=undef; #reset the error flags in case $rh_ans->{done}=undef; #the answer evaluator is called twice - + eval (q!main::DEBUG_MESSAGE( "

Answer evaluator information:

")!) if defined($self->{debug}) and $self->{debug}>0; $self->print_result_if_debug('pre_filter',$rh_ans); - + my @prefilters = @{$self -> {pre_filters}}; $count = 0; # the get student answer filter is counted as filter -1 foreach my $i (@prefilters) { @@ -630,7 +640,7 @@ sub print_result_if_debug { my $name = (defined($rh_ans->{_filter_name})) ? $rh_ans->{_filter_name}: 'unnamed'; eval (q! main::DEBUG_MESSAGE( "\n $count. Result from queue $queue: name: \"$name\"n", pretty_print($rh_ans,'html',4)) !); - ++$count; + ++$count; } $rh_ans->{_filter_name} = undef; } @@ -639,7 +649,7 @@ sub print_result_if_debug { # sub correct_answer_evaluate { # my $self = shift; # $self-> {rh_ans} -> {correct_ans} = shift @_; -# my $rh_ans = $self ->{rh_ans}; +# my $rh_ans = $self ->{rh_ans}; # my @prefilters = @{$self -> {correct_answer_pre_filters}}; # my $count = -1; # the blank filter is counted as filter 0 # foreach my $i (@prefilters) { @@ -669,7 +679,7 @@ sub print_result_if_debug { # $rh_ans = &$filter($rh_ans,@array); # warn "Filter Name:", $rh_ans->{_filter_name},"
\n" if $self->{debug}>0 and defined($rh_ans->{_filter_name}) # } -# $rh_ans = $self->dereference_array_ans($rh_ans); +# $rh_ans = $self->dereference_array_ans($rh_ans); # # make sure that the student answer is not an array so that it is reported correctly in answer section. # warn "final result: ", $self->{rh_ans}->pretty_print() if defined($self->{debug}) and $self->{debug}>0; # $self ->{rh_ans} = $rh_ans; @@ -811,7 +821,7 @@ sub withPostFilter { sub ans_hash { #alias for rh_ans my $self = shift; $self->rh_ans(@_); -} +} sub rh_ans { my $self = shift; my %in_hash = @_; @@ -823,11 +833,11 @@ sub rh_ans { =head1 Description: Filters -A filter is a subroutine which takes one AnswerHash as an input, followed by +A filter is a subroutine which takes one AnswerHash as an input, followed by a hash of options. Usage: filter($ans_hash, option1 =>value1, option2=> value2 ); - + The filter performs some operations on the input AnswerHash and returns an AnswerHash as output. @@ -837,17 +847,17 @@ three queues: pre_filters: these normalize student input, prepare text and so forth evaluators: these decide whether or not an answer is correct - post_filters: typically these clean up error messages or process errors + post_filters: typically these clean up error messages or process errors and generate error messages. If a filter detects an error it can throw an error message using the C<$rh_ans->throw_error()> method. This skips the AnswerHash by all remaining pre_filter C<$rh_ans->catch_error>, decides how ( or whether) it is supposed to handle the error and then passes the result on -to the next post_filter. +to the next post_filter. -Setting the flag C<$rh_ans->{done} = 1> will skip -the AnswerHash past the remaining post_filters. +Setting the flag C<$rh_ans->{done} = 1> will skip +the AnswerHash past the remaining post_filters. =head3 Built in filters @@ -867,7 +877,7 @@ the AnswerHash past the remaining post_filters. sub blank_prefilter { # check for blanks - my $rh_ans = shift; + my $rh_ans = shift; $rh_ans->{_filter_name} = 'blank_prefilter'; # undefined answers are BLANKS ( not defined($rh_ans->{student_ans}) ) && do {$rh_ans->throw_error("BLANK", 'The answer is blank'); @@ -882,7 +892,7 @@ sub blank_prefilter { # check for blanks $rh_ans; }; -sub blank_postfilter { +sub blank_postfilter { my $rh_ans=shift; $rh_ans->{_filter_name} = 'blank_postfilter'; return($rh_ans) unless defined($rh_ans->{error_flag}) and $rh_ans->{error_flag} eq 'BLANK'; diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 591e1e4cf8..fc08c37317 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # 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 diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index aec9db1b4c..43ea3317f1 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -1,13 +1,13 @@ #!/bin/perl ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/PGEnvironment.pm b/lib/PGEnvironment.pm index e4944b9a16..c7bd9b793e 100644 --- a/lib/PGEnvironment.pm +++ b/lib/PGEnvironment.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # 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 diff --git a/lib/PGUtil.pm b/lib/PGUtil.pm index 06dcee6c75..b2a92e0ad0 100644 --- a/lib/PGUtil.pm +++ b/lib/PGUtil.pm @@ -1,13 +1,12 @@ ############################################################################### # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGcore.pm,v 1.6 2010/05/25 22:47:52 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/PGalias.pm b/lib/PGalias.pm index a2b1556719..9216e69120 100644 --- a/lib/PGalias.pm +++ b/lib/PGalias.pm @@ -1,7 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGalias.pm,v 1.6 2010/05/15 18:41:23 gage Exp $ +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # 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 diff --git a/lib/PGanswergroup.pm b/lib/PGanswergroup.pm index 14a8eff61c..bfd84893b0 100644 --- a/lib/PGanswergroup.pm +++ b/lib/PGanswergroup.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGanswergroup.pm,v 1.1 2010/05/14 11:39:02 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/PGcore.pm b/lib/PGcore.pm index 446010a40b..57e44d9319 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -1,7 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGcore.pm,v 1.6 2010/05/25 22:47:52 gage Exp $ +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # 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 diff --git a/lib/PGloadfiles.pm b/lib/PGloadfiles.pm index 035329d2d0..f13c339a6d 100644 --- a/lib/PGloadfiles.pm +++ b/lib/PGloadfiles.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.org -# $CVSHeader: pg/lib/PGloadfiles.pm,v 1.1 2010/05/14 11:39:02 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/PGresource.pm b/lib/PGresource.pm index 2d35fdb943..d6208f9ac1 100644 --- a/lib/PGresource.pm +++ b/lib/PGresource.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGalias.pm,v 1.6 2010/05/15 18:41:23 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/PGresponsegroup.pm b/lib/PGresponsegroup.pm index 27d093efb7..33f236ef56 100644 --- a/lib/PGresponsegroup.pm +++ b/lib/PGresponsegroup.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGresponsegroup.pm,v 1.2 2010/05/25 22:13:52 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/Statistics.pm b/lib/Statistics.pm index b06084abd1..b759ff3a56 100644 --- a/lib/Statistics.pm +++ b/lib/Statistics.pm @@ -1,15 +1,13 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2013 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: $ -# Author: Kelly Black - kjblack@gmail.com -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # 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 diff --git a/lib/WeBWorK/EquationCache.pm b/lib/WeBWorK/EquationCache.pm index db144de5f6..9e886c423b 100644 --- a/lib/WeBWorK/EquationCache.pm +++ b/lib/WeBWorK/EquationCache.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::EquationCache; @@ -63,7 +73,7 @@ sub new { my $self = { %options, }; - + bless $self, $class; } @@ -90,14 +100,14 @@ sub lookup { # $tex =~ s/\s+//g; my $md5 = md5_hex(encode_utf8($tex)); - + my $db = $self->{cacheDB}; unless($db) { return($md5 ."1"); } sysopen(DB, $db, O_RDWR|O_CREAT) or die "failed to create/open cacheDB $db: $!"; flock(DB, LOCK_EX) or die "failed to write-lock cacheDB $db: $!"; - + my $line = 0; my $max = 0; my $match = 0; @@ -114,14 +124,14 @@ sub lookup { $max = $1 if $1 > $max; } } - + unless ($match) { # no match: invent a new instance number and add TeX string to DB $match = $max + 1; seek(DB, 0, 2); # we should already be at EOF, but what the hell. print DB "$md5\t$match\t$tex\n"; } - + close(DB); return "$md5$match"; } diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index 4d30da0bed..4364e6c7fd 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::PG::IO; diff --git a/lib/WeBWorK/PG/IO/Daemon2.pm b/lib/WeBWorK/PG/IO/Daemon2.pm index 8b446acf9f..7afd5e8939 100644 --- a/lib/WeBWorK/PG/IO/Daemon2.pm +++ b/lib/WeBWorK/PG/IO/Daemon2.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod-perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::PG::IO::Daemon2; diff --git a/lib/WeBWorK/PG/IO/WW1.pm b/lib/WeBWorK/PG/IO/WW1.pm index e400aeb22d..faf06ca36b 100644 --- a/lib/WeBWorK/PG/IO/WW1.pm +++ b/lib/WeBWorK/PG/IO/WW1.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod-perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::PG::IO::WW1; @@ -22,7 +32,7 @@ BEGIN { send_mail_to surePathToTmpFile ); - + $WeBWorK::PG::IO::SHARE{$_} = __PACKAGE__ foreach @EXPORT; } @@ -46,30 +56,30 @@ in a new environment. It uses the C module. sub send_mail_to { my $user_address = shift; # user must be an instructor my %options = @_; - + my $subject = ''; $subject = $options{'subject'} if defined($options{'subject'}); - + my $msg_body = ''; $msg_body =$options{'body'} if defined($options{'body'}); - + my @mail_to_allowed_list = (); @mail_to_allowed_list = @{ $options{'ALLOW_MAIL_TO'} } if defined($options{'ALLOW_MAIL_TO'}); my $out; - + # check whether user is an instructor my $mailing_allowed_flag = 0; - + while (@mail_to_allowed_list) { if ($user_address eq shift @mail_to_allowed_list ) { $mailing_allowed_flag =1; last; } } - + my $REMOTE_HOST = (defined( $ENV{'REMOTE_HOST'} ) ) ? $ENV{'REMOTE_HOST'}: 'unknown host'; my $REMOTE_ADDR = (defined( $ENV{'REMOTE_ADDR'}) ) ? $ENV{'REMOTE_ADDR'}: 'unknown address'; - + if ($mailing_allowed_flag) { ## mail header text: my $email_msg ="To: $user_address\n" @@ -79,7 +89,7 @@ sub send_mail_to { my $smtp = Net::SMTP->new($Global::smtpServer, Timeout=>10) or warn "Couldn't contact SMTP server."; $smtp->mail($Global::webmaster); - + if ( $smtp->recipient($user_address)) { # this one's okay, keep going $smtp->data( $email_msg) or warn("Unknown problem sending message data to SMTP server."); @@ -95,7 +105,7 @@ sub send_mail_to { . "Permitted addresses are specified in the courseWeBWorK.ph file."; $out = 0; } - + return $out; } diff --git a/lib/WeBWorK/PG/IO/WW2.pm b/lib/WeBWorK/PG/IO/WW2.pm index 0c7676c16c..223d950c97 100644 --- a/lib/WeBWorK/PG/IO/WW2.pm +++ b/lib/WeBWorK/PG/IO/WW2.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod-perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::PG::IO::WW2; diff --git a/lib/WeBWorK/PG/ImageGenerator.pm b/lib/WeBWorK/PG/ImageGenerator.pm index 5d38de4053..2b9d4068ce 100644 --- a/lib/WeBWorK/PG/ImageGenerator.pm +++ b/lib/WeBWorK/PG/ImageGenerator.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::PG::ImageGenerator; @@ -175,14 +185,14 @@ sub new { $self->{dvipng_align} = 'absmiddle' unless defined($self->{dvipng_align}); $self->{store_depths} = 1 if ($self->{dvipng_align} eq 'mysql'); $self->{useMarkers} = $self->{useMarkers} || 0; - + if ($self->{useCache}) { $self->{dir} = $self->{cacheDir}; $self->{url} = $self->{cacheURL}; $self->{basename} = ""; $self->{equationCache} = WeBWorK::EquationCache->new(cacheDB => $self->{cacheDB}); } - + bless $self, $class; } @@ -194,7 +204,7 @@ For example Will define a question wide style for interpreting \( \myVec{v} \) - + If this statement is placed in PGcourse.pl then the backslashes must be doubled since it is a .pl file not a .pg file @@ -231,7 +241,7 @@ for displaying the image. sub add { my ($self, $string, $mode) = @_; - + my $names = $self->{names}; my $strings = $self->{strings}; my $dir = $self->{dir}; @@ -239,7 +249,7 @@ sub add { my $basename = $self->{basename}; my $useCache = $self->{useCache}; my $depths = $self->{depths}; - + # if the string came in with delimiters, chop them off and set the mode # based on whether they were \[ .. \] or \( ... \). this means that if # the string has delimiters, the mode *argument* is ignored. @@ -249,16 +259,16 @@ sub add { $mode = "inline"; } # otherwise, leave the string and the mode alone. - + # assume that a bare string with no mode specified is inline $mode ||= "inline"; - + # now that we know what mode we're dealing with, we can generate a "real" # string to pass to latex my $realString = ($mode eq "display") ? '\(\displaystyle{' . $string . '}\)' : '\(' . $string . '\)'; - + # alignment tag could be a fixed default my ($imageNum, $aligntag) = (0, qq|align="$self->{dvipng_align}"|); # if the default is for variable heights, the default should be meaningful @@ -277,7 +287,7 @@ sub add { } else { $imageNum = @$strings + 1; } - + # We are banking on the fact that if useCache is true, then basename is empty. # Maybe we should simplify and drop support for useCache =0 and having a basename. @@ -285,15 +295,15 @@ sub add { my $imageName = ($basename) ? "$basename.$imageNum.png" : "$imageNum.png"; - + # store the full file name of the image, and the "real" tex string to the object push @$names, $imageName; push @$strings, $realString; #warn "ImageGenerator: added string $realString with name $imageName\n"; - + # ... and the full URL. my $imageURL = "$url/$imageName"; - + my $safeString = PGcore::encode_pg_and_html($string); my $imageTag = ($mode eq "display") @@ -305,7 +315,7 @@ sub add { =item render(%options) -Uses LaTeX and dvipng to render the equations stored in the object. +Uses LaTeX and dvipng to render the equations stored in the object. The option C is a reference to the text of the problem's text. After rendering the images and figuring out their depths, we go through and fix the tags @@ -328,7 +338,7 @@ NOTE: It's not clear to me that mtime has been implemented -- MEG - 2011/06 sub render { my ($self, %options) = @_; - + my $tempDir = $self->{tempDir}; my $dir = $self->{dir}; my $basename = $self->{basename}; @@ -347,8 +357,8 @@ sub render { my $success = mkdir "$dir"; warn "Could not make directory $dir" unless $success; } - - ############################################### + + ############################################### # determine which images need to be generated ############################################### my (@newStrings, @newNames); @@ -363,12 +373,12 @@ sub render { push @newNames, $name; } } - + if(@newStrings) { # Don't run latex if there are no images to generate - + # create temporary directory in which to do TeX processing my $wd = makeTempDirectory($tempDir, "ImageGenerator"); - + # store equations in a tex file my $texFile = "$wd/equation.tex"; open my $tex, ">", $texFile @@ -379,19 +389,19 @@ sub render { print $tex $TexPostamble; close $tex; warn "tex file $texFile was not written" unless -e $texFile; - + ############################################### # call LaTeX ############################################### my $latexCommand = "cd $wd && $latex equation > latex.out 2> latex.err"; my $latexStatus = system $latexCommand; - + if ($latexStatus and $latexStatus !=256) { warn "$latexCommand returned non-zero status $latexStatus: $!"; warn "cd $wd failed" if system "cd $wd"; warn "Unable to write to directory $wd. " unless -w $wd; warn "Unable to execute $latex " unless -e $latex ; - + warn `ls -l $wd`; my $errorMessage = ''; if (-r "$wd/equation.log") { @@ -401,10 +411,10 @@ sub render { warn "Unable to read logfile $wd/equation.log "; } } - + warn "$latexCommand failed to generate a DVI file" unless -e "$wd/equation.dvi"; - + ############################################ # call dvipng ############################################ @@ -418,29 +428,29 @@ sub render { my @dvipngdepths = ($dvipngout =~ /depth=(\d+)/g); # kill them all if something goes wrnog @dvipngdepths = () if(scalar(@dvipngdepths) != scalar(@newNames)); - + ############################################ # move/rename images ############################################ - + chmod (0664,<$wd/*>); # first make everything group writable so that a WeBWorK admin can delete images foreach my $image (readDirectory($wd)) { # only work on equation#.png files next unless $image =~ m/^equation(\d+)\.png$/; - + # get image number from above match my $imageNum = $1; # note, problems with solutions/hints can have empty values in newNames next unless $newNames[$imageNum-1]; - + # record the dvipng offset my $hashkey = $newNames[$imageNum-1]; $hashkey =~ s|/||; $hashkey =~ s|\.png$||; $depths->{"$hashkey"} = $dvipngdepths[$imageNum-1] if(defined($dvipngdepths[$imageNum-1])); - + #warn "ImageGenerator: found generated image $imageNum with name $newNames[$imageNum-1]\n"; - + # move/rename image #my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/$basename.$imageNum.png"; # check to see if this requires a directory we haven't made yet @@ -457,12 +467,12 @@ sub render { warn "$mvCommand returned non-zero status $mvStatus: $!"; warn "Can't write to tmp/equations directory $dir" unless -w $dir; } - + } ############################################ # remove temporary directory (and its contents) ############################################ - + if ($PreserveTempFiles) { warn "ImageGenerator: preserved temp files in working directory '$wd'.\n"; chmod (0775,$wd); diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 15725609e0..92fb7f50a6 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2012 The Open WeBWorK Project (openwebwork.org) -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# 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. ################################################################################ package WeBWorK::PG::Translator; @@ -46,7 +56,7 @@ WeBWorK::PG::Translator - Evaluate PG code and evaluate answers safely # other macros are loaded from within the problem using loadMacros $pt ->translate(); # translate the problem (the out following 4 pieces of information are created) - + $PG_PROBLEM_TEXT_ARRAY_REF = $pt->ra_text(); # output text for the body of the HTML file (in array form) $PG_PROBLEM_TEXT_REF = $pt->r_text(); # output text for the body of the HTML file $PG_HEADER_TEXT_REF = $pt->r_header;#\$PG_HEADER_TEXT; # text for the header of the HTML file @@ -55,7 +65,7 @@ WeBWorK::PG::Translator - Evaluate PG code and evaluate answers safely $PG_FLAGS_REF = $pt ->rh_flags; # misc. status flags. $pt -> process_answers(\%inputs); # evaluates all of the answers using submitted answers from %input - + my $rh_answer_results = $pt->rh_evaluated_answers; # provides a hash of the results of evaluating the answers. my $rh_problem_result = $pt->grade_problem; # grades the problem using the default problem grading method. @@ -149,7 +159,7 @@ BEGIN { require 'ww_strict.pm'; strict::import(); } - + # also define in Main::, for PG modules. sub Main::be_strict { &be_strict } } @@ -175,7 +185,7 @@ sub evaluate_modules { s/\.pm$// and warn "fixing your broken package name: $_.pm => $_"; # call runtime_use on the package name # don't worry -- runtime_use won't load a package twice! - #eval { runtime_use $_ }; # + #eval { runtime_use $_ }; # eval "package Main; require $_; import $_"; # change for WW1 warn "Failed to evaluate module $_: $@" if $@; # record this in the appropriate place @@ -198,7 +208,7 @@ sub load_extra_packages{ my $self = shift; my @package_list = @_; my $package_name; - + foreach (@package_list) { # ensure that the name is in fact a base name s/\.pm$// and warn "fixing your broken package name: $_.pm => $_"; @@ -308,7 +318,7 @@ The macros shared with the safe compartment are =cut # SHARE variables and routines with safe compartment -# +# # Some symbols are defined here (or in the IO module), and used inside the safe # compartment. Under WeBWorK 1.x, functions defined here had access to the # Global:: namespace, which contained course-specific data such things as @@ -317,16 +327,16 @@ The macros shared with the safe compartment are # which need access to course-specific data are now defined in the IO.pl macro # file, which has access to the problem environment. Several entries have been # added to the problem environment to support this move. -# +# # Useful for timing portions of the translating process # The timer $WeBWorK::timer is defined in the module WeBWorK.pm -# You must make sure that the code in that script for initialzing the +# You must make sure that the code in that script for initialzing the # timer is active. sub time_it { - my $msg = shift; + my $msg = shift; $WeBWorK::timer->continue("PG macro:". $msg) if defined($WeBWorK::timer); } @@ -335,14 +345,14 @@ my %Translator_shared_subroutine_hash = ( 'time_it' => __PACKAGE__, '&PG_answer_eval' => __PACKAGE__, '&PG_restricted_eval' => __PACKAGE__, - '&PG_macro_file_eval' => __PACKAGE__, + '&PG_macro_file_eval' => __PACKAGE__, '&be_strict' => __PACKAGE__, '&PGsort' => __PACKAGE__, '&dumpvar' => __PACKAGE__, ); # add names from WeBWorK::PG::IO and WeBWorK::PG::IO::* -my %IO_shared_subroutine_hash = %WeBWorK::PG::IO::SHARE; +my %IO_shared_subroutine_hash = %WeBWorK::PG::IO::SHARE; sub initialize { my $self = shift; @@ -363,7 +373,7 @@ sub initialize { #$safe_cmpt -> share('$rf_answer_eval'); #$safe_cmpt -> share('$rf_restricted_eval'); use strict; - + # The standalone renderer does this when the module is compiled. unless (exists($ENV{MOJO_MODE})) { $safe_cmpt -> share_from('main', $self->{ra_included_modules} ); @@ -461,7 +471,7 @@ sub unrestricted_load { my $store_mask = $safe_cmpt->mask(); $safe_cmpt->mask(Opcode::empty_opset()); my $safe_cmpt_package_name = $safe_cmpt->root(); - + my $macro_file_name = fileFromPath($filePath); $macro_file_name =~s/\.pl//; # trim off the extenstion my $export_subroutine_name = "_${macro_file_name}_export"; @@ -476,11 +486,11 @@ sub unrestricted_load { my $macro_file_loaded = ref($init_subroutine) =~ /CODE/ && defined(&$init_subroutine); - #print STDERR "$macro_file_name has not yet been loaded\n" unless $macro_file_loaded; + #print STDERR "$macro_file_name has not yet been loaded\n" unless $macro_file_loaded; unless ($macro_file_loaded) { ## load the $filePath file ## Using rdo insures that the $filePath file is loaded for every problem, allowing initializations to occur. - ## Ordinary mortals should not be fooling with the fundamental macros in these files. + ## Ordinary mortals should not be fooling with the fundamental macros in these files. my $local_errors = ""; if (-r $filePath ) { my $rdoResult = $safe_cmpt->rdo($filePath); @@ -493,7 +503,7 @@ sub unrestricted_load { $self ->{errors} .= $local_errors if $local_errors; } $safe_cmpt -> mask($store_mask); - + } # try again to define the initization subroutine $init_subroutine = eval { \&{"$init_subroutine_name"} }; @@ -668,7 +678,7 @@ that, so we remove it as well. File names are shortened, when possible, by replacing the templates directory with [TMPL], the WeBWorK root directory by [WW] and the PG root directory by [PG]. - + =cut sub PG_errorMessage { @@ -726,7 +736,7 @@ sub PG_errorMessage { '$GLOBAL_VARIABLE' => \'global', '$t' => \undef, '$s' => \'regular output' - + @@ -740,7 +750,7 @@ sub PG_undef_var_check { local $Data::Dumper::Maxdepth = 2; # takes all lexical variables from caller-nemaspace my $possibles = Data::Dumper::Dumper({ %{PadWalker::peek_my(1)}, %{PadWalker::peek_our(1)} }); - + $possibles ne "\$VAR1 = {};\n" ? ($possibles =~ s/^.*?\n(.*)\n.*?\n$/$1/ms) : ($possibles = ''); return "Warning: " . join(', ', @_) . "Possible variables are:\n$possibles\n"; } @@ -761,20 +771,20 @@ sub translate { $self ->{errors} .= qq{ERROR: This problem file was empty!\n} unless ($evalString) ; $self ->{errors} .= qq{ERROR: You must define the environment before translating.} unless defined( $self->{envir} ); - + # install handlers for warn and die that call PG_errorMessage. # if the existing signal handler is not a coderef, the built-in warn or # die function is called. this does not account for the case where the # handler is set to "IGNORE" or to the name of a function. in these cases # the built-in function will be called. - + my $outer_sig_warn = $SIG{__WARN__}; local $SIG{__WARN__} = sub { ref $outer_sig_warn eq "CODE" ? &$outer_sig_warn(PG_errorMessage('message', $_[0])) : warn PG_errorMessage('message', $_[0]); }; - + my $outer_sig_die = $SIG{__DIE__}; local $SIG{__DIE__} = sub { ref $outer_sig_die eq "CODE" @@ -847,10 +857,10 @@ without warnings. ########################################## ###### PG preprocessing code ############# ########################################## - + $evalString = &{$self->{preprocess_code}}($evalString); - + # # default_preprocess_code # # BEGIN_TEXT and END_TEXT must occur on a line by themselves. # $evalString =~ s/\n\s*END_TEXT[\s;]*\n/\nEND_TEXT\n/g; @@ -873,7 +883,7 @@ case the previously defined safe compartment is used. (See item 1.) my ($PG_PROBLEM_TEXT_REF, $PG_HEADER_TEXT_REF, $PG_POST_HEADER_TEXT_REF,$PG_ANSWER_HASH_REF, $PG_FLAGS_REF, $PGcore) =$safe_cmpt->reval(" $evalString"); - + # This section could use some more error messages. In particular if a problem doesn't produce the right output, the user needs # information about which problem was at fault. @@ -1000,7 +1010,7 @@ the errors. $self ->{ rh_correct_answers } = $PG_ANSWER_HASH_REF; $self ->{ PG_FLAGS_REF } = $PG_FLAGS_REF; $self ->{ rh_pgcore } = $PGcore; - + #warn "PGcore is ", ref($PGcore), " in Translator"; #$self ->{errors}; } # end translate @@ -1082,29 +1092,29 @@ sub process_answers{ # the foreach loop around the conditional involving $rf_fun, but for efficiency # we've moved it out here. This means that the handlers will be active during the # code before and after the actual answer evaluation. - + my $outer_sig_warn = $SIG{__WARN__}; local $SIG{__WARN__} = sub { ref $outer_sig_warn eq "CODE" ? &$outer_sig_warn(PG_errorMessage('message', $_[0])) : warn PG_errorMessage('message', $_[0]); }; - + # the die handler is a closure over %errorTable and $outer_sig_die. - # + # # %errorTable accumulates a "full" error message for each error that occurs during # answer evaluation. then, right after the evaluation (which is done within a call # to Safe::reval), $@ is checked and it's value is looked up in %errorTable to get # the full error to report. - # + # # my question: why is this a hash? this is die, so once one occurs, we exit the reval. # wouldn't it be sufficient to have a scalar like $backtrace_for_last_error? - # + # # Note that %errorTable is cleared for each answer. my %errorTable; my $outer_sig_die = $SIG{__DIE__}; local $SIG{__DIE__} = sub { - + # this chunk taken from dpvc's original handler my $fullerror = PG_errorMessage('traceback', @_); my ($error,$traceback) = split /\n/, $fullerror, 2; @@ -1112,7 +1122,7 @@ sub process_answers{ $error .= "\n"; $errorTable{$error} = $fullerror; # end of dpvc's original code - + ref $outer_sig_die eq "CODE" ? &$outer_sig_die($error) : die $error; @@ -1121,11 +1131,11 @@ sub process_answers{ # apply each instructors answer to the corresponding student answer # my $printAnsHash = 1; # this turns on error printing of the answer hashes - + #$PG->debug_message("Student answers are: ", join(" ", %h_student_answers) ) if $printAnsHash==1; # we will want to redefine the entry order in terms of the answer evaluators - - #### foreach my $ans_name ( @answer_entry_order ) { + + #### foreach my $ans_name ( @answer_entry_order ) { #### @answer_entry_order was created from PG_ANSWERS_HASH which maintains the #### order of its keys @@ -1139,7 +1149,7 @@ sub process_answers{ #################################### # local($rf_fun,$temp_ans)= (undef,undef); -# +# # my ($ans, $errors) = $self->filter_answer( $h_student_answers{$ans_name} ); # $temp_ans = $ans; # $temp_ans = '' unless defined($temp_ans); #make sure that answer is always defined @@ -1147,33 +1157,33 @@ sub process_answers{ # #################################### # # get answer evaluator (old method) # #################################### -# +# # if ( defined($rh_correct_answers ->{$ans_name} ) ) { # $rf_fun = $rh_correct_answers->{$ans_name}; # } else { # warn "There is no answer evaluator for the question labeled $ans_name"; # } # $self->{safe}->share('$rf_fun','$temp_ans'); -# +# #################################### # gather answers and answer evaluator (new method) #################################### local($new_rf_fun,$new_temp_ans) = (undef,undef); - my $answergrp = $PG->{PG_ANSWERS_HASH}->{$ans_name}; #this has all answer evaluators AND answer blanks (just to be sure ) + my $answergrp = $PG->{PG_ANSWERS_HASH}->{$ans_name}; #this has all answer evaluators AND answer blanks (just to be sure ) my $responsegrp = $answergrp->response_obj; ################# # refactor to answer group? ################## - $new_rf_fun = $answergrp->ans_eval; - my ($ans, $errors) = $self->filter_answer( $responsegrp->get_response($ans_name) ); + $new_rf_fun = $answergrp->ans_eval; + my ($ans, $errors) = $self->filter_answer( $responsegrp->get_response($ans_name) ); $new_temp_ans = $ans; # avoid undefined errors in translator my $skip_evaluation=0; if (not defined($new_rf_fun) ) { $PG->warning_message( "No answer evaluator for the question labeled: $ans_name "); $skip_evaluation=1; } elsif (not ref($new_rf_fun) =~ /AnswerEvaluator/ ) { - $PG->warning_message( "Error in Translator.pm::process_answers: Answer $ans_name: + $PG->warning_message( "Error in Translator.pm::process_answers: Answer $ans_name: Unrecognized evaluator type |". ref($rf_fun). "|"); $skip_evaluation=1; } @@ -1182,7 +1192,7 @@ sub process_answers{ $skip_evaluation=1; } $PG->debug_message("Answers associated with $ans_name are $new_temp_ans ref=". ref($new_temp_ans) ) if defined $new_temp_ans and $local_debug; - #FIXME -- this is a hack for handling check boxes. + #FIXME -- this is a hack for handling check boxes. if (ref( $new_temp_ans) =~/HASH/i) { my @tmp = (); foreach my $key (sort keys %$new_temp_ans) { @@ -1192,7 +1202,7 @@ sub process_answers{ $PG->debug_message("Hash answer associated with $ans_name is $new_temp_ans. ref=". ref($new_temp_ans)."
".pretty_print($new_temp_ans) ) if $local_debug; } #FIXME -- hack to allow answers such as <4,6,7> -- < and > have been escaped. - unless( $skip_evaluation) { + unless( $skip_evaluation) { # $new_temp_ans =~ s/\<\;//g; # > # $new_temp_ans =~ s/\&\#91\;/\[/g; # < @@ -1204,12 +1214,12 @@ sub process_answers{ $self->{safe}->share('$new_rf_fun','$new_temp_ans'); - + # clear %errorTable for each problem %errorTable = (); # is the error table being used? perhaps by math objects? - + my ($rh_ans_evaluation_result, $new_rh_ans_evaluation_result); - + if (ref($new_rf_fun) eq 'CODE' ) { # $rh_ans_evaluation_result = $self->{safe} ->reval( '&{ $rf_fun }($temp_ans, ans_label => \''.$ans_name.'\')' ) ; # warn "Error in Translator.pm::process_answers: Answer $ans_name: |$temp_ans|\n $@\n" if $@; @@ -1225,13 +1235,13 @@ sub process_answers{ # warn "Evaluation error: Answer $ans_name:
\n", # $rh_ans_evaluation_result->error_flag(), " :: ", # $rh_ans_evaluation_result->error_message(),"
\n" -# if defined($rh_ans_evaluation_result) +# if defined($rh_ans_evaluation_result) # and defined($rh_ans_evaluation_result->error_flag()); -# +# # $rh_ans_evaluation_result = {%$rh_ans_evaluation_result}; #needed to protect result # # from next evaluation apparently ######################################### - + # Get full traceback, but save it in local variable $errorTable so that # we can add it later. This is because some evaluators use # eval to trap errors and then report them in the message @@ -1246,22 +1256,22 @@ sub process_answers{ $PG->warning_message( "Evaluation error in new process: Answer $ans_name:
\n", $new_rh_ans_evaluation_result->error_flag(), " :: ", $new_rh_ans_evaluation_result->error_message(),"
\n") - if defined($new_rh_ans_evaluation_result) && ref($new_rh_ans_evaluation_result) + if defined($new_rh_ans_evaluation_result) && ref($new_rh_ans_evaluation_result) && defined($new_rh_ans_evaluation_result->error_flag()); } else { $PG->warning_message(" The evaluated answer is not an answer hash ".($new_rh_ans_evaluation_result//'').': |'.ref($new_rh_ans_evaluation_result)."|."); - } + } # $PG->debug_message( $self->{envir}->{'probFileName'} ." new_temp_ans and temp_ans don't agree: ". -# ref($new_temp_ans)." $new_temp_ans ". ref($temp_ans). " $temp_ans".length($new_temp_ans).length($temp_ans)) +# ref($new_temp_ans)." $new_temp_ans ". ref($temp_ans). " $temp_ans".length($new_temp_ans).length($temp_ans)) # unless $new_temp_ans eq $temp_ans; } else { $PG->warning_message( "Answer evaluator $ans_name was not executed due to errors. ", "========"); } -######################################################### +######################################################### # End refactor to answer group ? ############################################# - -######################################################### + +######################################################### # check that old and new answers are the same # foreach my $key (keys %$rh_ans_evaluation_result) { # next unless defined $key; @@ -1272,14 +1282,14 @@ sub process_answers{ # $rh_ans_evaluation_result->{$key}." new: ".$new_rh_ans_evaluation_result->{$key}) # unless $rh_ans_evaluation_result->{$key} eq $new_rh_ans_evaluation_result->{$key}; # } - -######################################################### + +######################################################### #$PG->debug_message("old $ans_name: $rf_fun -- ans: $temp_ans",pretty_print($rh_ans_evaluation_result)) if $printAnsHash==1; - $PG->debug_message("new $ans_name: $new_rf_fun -- ans: - $new_temp_ans",pretty_print($new_rh_ans_evaluation_result)) - if ($PG->{envir}->{inputs_ref}->{showAnsHashInfo})//'' + $PG->debug_message("new $ans_name: $new_rf_fun -- ans: + $new_temp_ans",pretty_print($new_rh_ans_evaluation_result)) + if ($PG->{envir}->{inputs_ref}->{showAnsHashInfo})//'' and ($PG->{envir}->{permissionLevel})//0 >=10; -######################################################### +######################################################### # decide whether to return the new or old answer evaluator hash $rh_ans_evaluation_result = $new_rh_ans_evaluation_result; @@ -1287,7 +1297,7 @@ sub process_answers{ # This warning may not be needed. # unless ( ( ref($rh_ans_evaluation_result) eq 'HASH') or ( ref($rh_ans_evaluation_result) eq 'AnswerHash') ) { # warn "Error in Translator.pm::process_answers: Answer $ans_name:
\n -# Answer evaluators must return a hash or an AnswerHash type, not type |", +# Answer evaluators must return a hash or an AnswerHash type, not type |", # ref($rh_ans_evaluation_result), "|"; # } $rh_ans_evaluation_result ->{ans_message} .= "$errors \n" if $errors; @@ -1519,8 +1529,8 @@ sub filter_answer { my $errors=''; if (ref($ans) eq 'ARRAY') { #handle the case where the answer comes from several inputs with the same name # In many cases this will be passed as a reference to an array - # if it is passed as a single string (separated by \0 characters) as - # some early versions of CGI behave, then + # if it is passed as a single string (separated by \0 characters) as + # some early versions of CGI behave, then # it is unclear what will happen when the answer is filtered. foreach my $item (@{$ans}) { my ($filtered_ans, $error) = &{ $self->{rf_safety_filter} } ($item); @@ -1528,11 +1538,11 @@ sub filter_answer { $errors .= " ". $error if $error; # add error message if error is non-zero. } (\@filtered_answers,$errors); - + } else { &{ $self->{rf_safety_filter} } ($ans); } - + } sub rf_safety_filter { @@ -1592,16 +1602,16 @@ have special significance. C$b} @list> C -sorts the list numerically and lexically respectively. +sorts the list numerically and lexically respectively. If C is used in a problem, before the sort routine is defined in a macro, then -things get badly confused. To correct this the macro PGsort is defined below. It is +things get badly confused. To correct this the macro PGsort is defined below. It is evaluated before the problem template is read. In PGbasicmacros.pl, the two subroutines PGsort sub { $_[0] < $_[1] }, @list; PGsort sub { $_[0] lt $_[1] }, @list; -(called num_sort and lex_sort) provide slightly slower, but safer, routines for the PG language. +(called num_sort and lex_sort) provide slightly slower, but safer, routines for the PG language. (The subroutines for ordering are B. Note the commas!) =cut @@ -1690,10 +1700,10 @@ sub PG_restricted_eval_helper { sub PG_macro_file_eval { # would like to modify this so that it requires use strict on the files that it evaluates. my $string = shift; my ($pck,$file,$line) = caller; - + local $SIG{__WARN__} = "DEFAULT"; local $SIG{__DIE__} = "DEFAULT"; - + no strict; my $out = eval ("package main; be_strict();\n" . $string ); my $errors =$@; @@ -1701,7 +1711,7 @@ sub PG_macro_file_eval { # would like to modify this so that it requires us . $errors . "The calling package is $pck\n" if defined($errors) && $errors =~/\S/; use strict; - + return (wantarray) ? ($out, $errors,$full_error_report) : $out; } =head2 PG_answer_eval @@ -1725,20 +1735,20 @@ sub PG_answer_eval { my($string) = shift; # I made this local just in case -- see PG_restricted_eval my $errors = ''; my $full_error_report = ''; - my ($pck,$file,$line) = caller; + my ($pck,$file,$line) = caller; # Because of the global variable $PG::compartment_name and $PG::safe_cmpt # only one problem safe compartment can be active at a time. # This might cause problems at some point. In that case a cleverer way # of insuring that the package stays in scope until the answer is evaluated # will be required. - + # This is pretty tricky and doesn't always work right. # We seem to need PG_priv instead of main when PG_answer_eval is called within a completion # 'package PG_priv; ' - + local $SIG{__WARN__} = sub {die(@_)}; # make warn die, so all errors are reported. local $SIG{__DIE__} = "DEFAULT"; - + no strict; my $out = eval('package main;'.$string); $out = '' unless defined($out); @@ -1747,7 +1757,7 @@ sub PG_answer_eval { $errors The calling package is $pck\n" if defined($errors) && $errors =~/\S/; use strict; - + return (wantarray) ? ($out, $errors,$full_error_report) : $out; @@ -1759,7 +1769,7 @@ sub PG_answer_eval { # $evalString =~ s/\n\s*END_TEXT[\s;]*\n/\nEND_TEXT\n/g; # $evalString =~ s/\n\s*BEGIN_TEXT[\s;]*\n/\nTEXT\(EV3\(<<'END_TEXT'\)\);\n/g; # $evalString =~ s/ENDDOCUMENT.*/ENDDOCUMENT();/s; # remove text after ENDDOCUMENT -# +# # $evalString =~ s/\\/\\\\/g; # \ can't be used for escapes because of TeX conflict # $evalString =~ s/~~/\\/g; # use ~~ as escape instead, use # for comments # $evalString; @@ -1794,33 +1804,33 @@ sub default_postprocess_code { $evalString_ref; } -no strict; +no strict; sub dumpvar { my ($packageName) = @_; local(*alias); - + sub emit { print @_; } - + *stash = *{"${packageName}::"}; $, = " "; - + emit "Content-type: text/html; charset=UTF-8\n\n
\n";
-    
-    
+
+
     while ( ($varName, $globValue) = each %stash) {
         emit "$varName\n";
-        
+
 	*alias = $globValue;
 	next if $varName=~/main/;
-	
+
 	#if (defined($alias) ) {  # get rid of defined since this is deprecated
 	if ($alias ) {
 	    emit "  \$$varName $alias \n";
 	}
-	
+
 	if ( @alias) {
 	    emit "  \@$varName @alias \n";
 	}
diff --git a/macros/LinearProgramming.pl b/macros/LinearProgramming.pl
index ea7c8f7070..d1edca911a 100644
--- a/macros/LinearProgramming.pl
+++ b/macros/LinearProgramming.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/MathObjects.pl b/macros/MathObjects.pl
index b1a17b7fe4..6c8392ecdf 100644
--- a/macros/MathObjects.pl
+++ b/macros/MathObjects.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PG.pl b/macros/PG.pl
index 4228bb852f..b3d81779ba 100644
--- a/macros/PG.pl
+++ b/macros/PG.pl
@@ -913,8 +913,7 @@ sub includePGproblem {
 
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/PG.pl,v 1.46 2010/05/27 02:22:51 gage Exp $
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/PGanswermacros.pl b/macros/PGanswermacros.pl
index cf3e3ef005..f2d23cbf95 100644
--- a/macros/PGanswermacros.pl
+++ b/macros/PGanswermacros.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/PGanswermacros.pl,v 1.72 2010/02/01 01:33:05 apizer Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGauxiliaryFunctions.pl b/macros/PGauxiliaryFunctions.pl
index b813e81524..53042ec4a6 100644
--- a/macros/PGauxiliaryFunctions.pl
+++ b/macros/PGauxiliaryFunctions.pl
@@ -1,12 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGbasicmacros.pl b/macros/PGbasicmacros.pl
index 94ccfe0c81..14599bf583 100644
--- a/macros/PGbasicmacros.pl
+++ b/macros/PGbasicmacros.pl
@@ -1,6 +1,6 @@
 ################################################################################
-# WeBWorK Program Generation Language
-# Copyright © 2000-2020 The WeBWorK Project, http://openwebwork.sf.net/
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/PGchoicemacros.pl b/macros/PGchoicemacros.pl
index 997d9837f6..1ed09e5550 100644
--- a/macros/PGchoicemacros.pl
+++ b/macros/PGchoicemacros.pl
@@ -1,7 +1,6 @@
 ################################################################################
-# WeBWorK Program Generation Language
-# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/PGcomplexmacros.pl b/macros/PGcomplexmacros.pl
index 7ad47e505e..16e89ca03e 100644
--- a/macros/PGcomplexmacros.pl
+++ b/macros/PGcomplexmacros.pl
@@ -1,13 +1,20 @@
 # This file     is PGcomplexmacros.pl
 # This includes the subroutines for the ANS macros, that
 # is, macros allowing a more flexible answer checking
-####################################################################
-# Copyright @ 1995-2002 The WeBWorK Team
-# All Rights Reserved
-####################################################################
-#$Id$
-
-
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# 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
 
diff --git a/macros/PGcomplexmacros2.pl b/macros/PGcomplexmacros2.pl
index e307d004e5..d42cf9d242 100644
--- a/macros/PGcomplexmacros2.pl
+++ b/macros/PGcomplexmacros2.pl
@@ -2,10 +2,20 @@
 # This file     is PGcomplexmacros2.pl
 # This includes the subroutines for the ANS macros, that
 # is, macros allowing a more flexible answer checking
-####################################################################
-# Copyright @ 2006-2007 The WeBWorK Team
-# All Rights Reserved
-####################################################################
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# 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
diff --git a/macros/PGessaymacros.pl b/macros/PGessaymacros.pl
index 164409ab63..6abfab559c 100644
--- a/macros/PGessaymacros.pl
+++ b/macros/PGessaymacros.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/PGanswermacros.pl,v 1.72 2010/02/01 01:33:05 apizer Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGfunctionevaluators.pl b/macros/PGfunctionevaluators.pl
index ae91f4faca..bfcecdf704 100644
--- a/macros/PGfunctionevaluators.pl
+++ b/macros/PGfunctionevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGinfo.pl b/macros/PGinfo.pl
index ad0a0e0291..d72c19be67 100644
--- a/macros/PGinfo.pl
+++ b/macros/PGinfo.pl
@@ -1,8 +1,18 @@
 
-####################################################################
-# Copyright @ 1995-2007 University of Rochester
-# All Rights Reserved
-####################################################################
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# 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
 
@@ -110,4 +120,4 @@ sub pp {
     my $hash = shift;
     "printing |". ref($hash)."|$BR". pretty_print($hash);
 }
-1;
\ No newline at end of file
+1;
diff --git a/macros/PGlateximage.pl b/macros/PGlateximage.pl
index 59df97a77e..660b9c1082 100644
--- a/macros/PGlateximage.pl
+++ b/macros/PGlateximage.pl
@@ -1,12 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGmiscevaluators.pl b/macros/PGmiscevaluators.pl
index ef9c8e0a9d..d23c140b19 100644
--- a/macros/PGmiscevaluators.pl
+++ b/macros/PGmiscevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGnumericevaluators.pl b/macros/PGnumericevaluators.pl
index 684806eab7..12caad6bbb 100644
--- a/macros/PGnumericevaluators.pl
+++ b/macros/PGnumericevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGstringevaluators.pl b/macros/PGstringevaluators.pl
index af4ebb1b3f..34f690cb06 100644
--- a/macros/PGstringevaluators.pl
+++ b/macros/PGstringevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGtextevaluators.pl b/macros/PGtextevaluators.pl
index c3ac67728c..91648dfd77 100644
--- a/macros/PGtextevaluators.pl
+++ b/macros/PGtextevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/PGtikz.pl b/macros/PGtikz.pl
index 851fd87f77..a6583a638d 100644
--- a/macros/PGtikz.pl
+++ b/macros/PGtikz.pl
@@ -1,12 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/Parser.pl b/macros/Parser.pl
index 6c94f98e54..e2b60d6b11 100644
--- a/macros/Parser.pl
+++ b/macros/Parser.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/answerComposition.pl b/macros/answerComposition.pl
index 84930396da..f63fbcb1fc 100644
--- a/macros/answerComposition.pl
+++ b/macros/answerComposition.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/answerComposition.pl,v 1.8 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/answerCustom.pl b/macros/answerCustom.pl
index 2b166f9ef9..46ebaee69d 100644
--- a/macros/answerCustom.pl
+++ b/macros/answerCustom.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/answerHints.pl b/macros/answerHints.pl
index ad7faaf820..6c67574fb5 100644
--- a/macros/answerHints.pl
+++ b/macros/answerHints.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/answerVariableList.pl b/macros/answerVariableList.pl
index 8b0c7a1a5e..32b3ea58b9 100644
--- a/macros/answerVariableList.pl
+++ b/macros/answerVariableList.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/compoundProblem5.pl b/macros/compoundProblem5.pl
index cf814cef4e..2523e16dbd 100644
--- a/macros/compoundProblem5.pl
+++ b/macros/compoundProblem5.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextABCD.pl b/macros/contextABCD.pl
index 774daa5be8..23e34303d8 100644
--- a/macros/contextABCD.pl
+++ b/macros/contextABCD.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextAlternateDecimal.pl b/macros/contextAlternateDecimal.pl
index c5b9b07d37..cfa4eb213e 100644
--- a/macros/contextAlternateDecimal.pl
+++ b/macros/contextAlternateDecimal.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextAlternateIntervals.pl b/macros/contextAlternateIntervals.pl
index 8500cd2766..fca902678f 100644
--- a/macros/contextAlternateIntervals.pl
+++ b/macros/contextAlternateIntervals.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextComplexExtras.pl b/macros/contextComplexExtras.pl
index dc5a468c31..c2441deb5b 100644
--- a/macros/contextComplexExtras.pl
+++ b/macros/contextComplexExtras.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextComplexExtras.pl,v 1.0 2012/08/01 11:33:50 dpvc Exp $
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/contextComplexJ.pl b/macros/contextComplexJ.pl
index c386ccd39e..80bcdfcf0a 100644
--- a/macros/contextComplexJ.pl
+++ b/macros/contextComplexJ.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextCongruence.pl b/macros/contextCongruence.pl
index 8e1b9937e2..f7cea379cb 100644
--- a/macros/contextCongruence.pl
+++ b/macros/contextCongruence.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2017 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextCurrency.pl b/macros/contextCurrency.pl
index 0b8111efb8..68bac416a8 100644
--- a/macros/contextCurrency.pl
+++ b/macros/contextCurrency.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextCurrency.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextInequalities.pl b/macros/contextInequalities.pl
index 6e209ce3f9..70e5a72efe 100644
--- a/macros/contextInequalities.pl
+++ b/macros/contextInequalities.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextInequalities.pl,v 1.23 2010/03/22 11:01:55 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextInequalitySetBuilder.pl b/macros/contextInequalitySetBuilder.pl
index f3bc52fdba..c68e625e6d 100644
--- a/macros/contextInequalitySetBuilder.pl
+++ b/macros/contextInequalitySetBuilder.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextInteger.pl b/macros/contextInteger.pl
index 15878aaef8..47c943e9f9 100644
--- a/macros/contextInteger.pl
+++ b/macros/contextInteger.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2017 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextIntegerFunctions.pl b/macros/contextIntegerFunctions.pl
index 49c6ae9808..38488c5ca3 100644
--- a/macros/contextIntegerFunctions.pl
+++ b/macros/contextIntegerFunctions.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextLimitedComplex.pl b/macros/contextLimitedComplex.pl
index f58cb1bf47..39214b480a 100644
--- a/macros/contextLimitedComplex.pl
+++ b/macros/contextLimitedComplex.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextLimitedNumeric.pl b/macros/contextLimitedNumeric.pl
index adce6bdef0..c678fbbdc8 100644
--- a/macros/contextLimitedNumeric.pl
+++ b/macros/contextLimitedNumeric.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextLimitedPoint.pl b/macros/contextLimitedPoint.pl
index 72d93d8083..2d1ba73828 100644
--- a/macros/contextLimitedPoint.pl
+++ b/macros/contextLimitedPoint.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextLimitedPoint.pl,v 1.14 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextLimitedPolynomial.pl b/macros/contextLimitedPolynomial.pl
index 017dcc0bea..e4f8c94665 100644
--- a/macros/contextLimitedPolynomial.pl
+++ b/macros/contextLimitedPolynomial.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextLimitedPowers.pl b/macros/contextLimitedPowers.pl
index adb18de1b2..0a9de2deb2 100644
--- a/macros/contextLimitedPowers.pl
+++ b/macros/contextLimitedPowers.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextLimitedVector.pl b/macros/contextLimitedVector.pl
index abe12ed044..1c5314b4bb 100644
--- a/macros/contextLimitedVector.pl
+++ b/macros/contextLimitedVector.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextMatrixExtras.pl b/macros/contextMatrixExtras.pl
index d42e2483b3..d3e835c000 100644
--- a/macros/contextMatrixExtras.pl
+++ b/macros/contextMatrixExtras.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextMatrixExtras.pl,v 1.0 2012/08/01 11:21:45 dpvc Exp $
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/contextPartition.pl b/macros/contextPartition.pl
index 2e0e4cbcae..d5d74c7b6a 100644
--- a/macros/contextPartition.pl
+++ b/macros/contextPartition.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextPercent.pl b/macros/contextPercent.pl
index fb7c788380..f4dd56bb2f 100644
--- a/macros/contextPercent.pl
+++ b/macros/contextPercent.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextCurrency.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextPeriodic.pl b/macros/contextPeriodic.pl
index b8186c881b..0a3706326e 100644
--- a/macros/contextPeriodic.pl
+++ b/macros/contextPeriodic.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextPermutation.pl b/macros/contextPermutation.pl
index 4db42ab4c3..60fb1f61a0 100644
--- a/macros/contextPermutation.pl
+++ b/macros/contextPermutation.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextPermutationUBC.pl b/macros/contextPermutationUBC.pl
index b5972b7732..8b9215d176 100644
--- a/macros/contextPermutationUBC.pl
+++ b/macros/contextPermutationUBC.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextPiecewiseFunction.pl b/macros/contextPiecewiseFunction.pl
index 724574caa1..1bf49cfda8 100644
--- a/macros/contextPiecewiseFunction.pl
+++ b/macros/contextPiecewiseFunction.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextPolynomialFactors.pl b/macros/contextPolynomialFactors.pl
index 95686be6eb..40d342b844 100644
--- a/macros/contextPolynomialFactors.pl
+++ b/macros/contextPolynomialFactors.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2010 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextPolynomialFactors.pl,v 1.2 2010/03/31 21:45:42 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextRationalFunction.pl b/macros/contextRationalFunction.pl
index 46f07c24ee..84f6f63d46 100644
--- a/macros/contextRationalFunction.pl
+++ b/macros/contextRationalFunction.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2010 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextRationalFunction.pl,v 1.1 2010/03/31 21:01:14 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextScientificNotation.pl b/macros/contextScientificNotation.pl
index c098724e6c..dfe52e114e 100644
--- a/macros/contextScientificNotation.pl
+++ b/macros/contextScientificNotation.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextString.pl b/macros/contextString.pl
index 6a323c3b5b..1e418d12bd 100644
--- a/macros/contextString.pl
+++ b/macros/contextString.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextTF.pl b/macros/contextTF.pl
index 22fe43c053..29933658f5 100644
--- a/macros/contextTF.pl
+++ b/macros/contextTF.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/contextTrigDegrees.pl b/macros/contextTrigDegrees.pl
index a20aa7e7d9..3e8754859a 100644
--- a/macros/contextTrigDegrees.pl
+++ b/macros/contextTrigDegrees.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl
index a9cdde8fdd..aba3b584c7 100644
--- a/macros/draggableProof.pl
+++ b/macros/draggableProof.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl
index d72b636987..4a82bcc69e 100644
--- a/macros/draggableSubsets.pl
+++ b/macros/draggableSubsets.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/extraAnswerEvaluators.pl b/macros/extraAnswerEvaluators.pl
index 3c0c257d18..36a9329c5f 100644
--- a/macros/extraAnswerEvaluators.pl
+++ b/macros/extraAnswerEvaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl
index 763f09e840..a0ef1089d2 100644
--- a/macros/parserAssignment.pl
+++ b/macros/parserAssignment.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserAutoStrings.pl b/macros/parserAutoStrings.pl
index e260701d73..b5bd26b4e7 100644
--- a/macros/parserAutoStrings.pl
+++ b/macros/parserAutoStrings.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserCustomization.pl b/macros/parserCustomization.pl
index e57172b3f3..f28e64528a 100644
--- a/macros/parserCustomization.pl
+++ b/macros/parserCustomization.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserDifferenceQuotient.pl b/macros/parserDifferenceQuotient.pl
index 048c5f1371..7f0d4b4c48 100644
--- a/macros/parserDifferenceQuotient.pl
+++ b/macros/parserDifferenceQuotient.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserFormulaAnyVar.pl b/macros/parserFormulaAnyVar.pl
index 049661be60..2ae71ded43 100644
--- a/macros/parserFormulaAnyVar.pl
+++ b/macros/parserFormulaAnyVar.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserFormulaUpToConstant.pl b/macros/parserFormulaUpToConstant.pl
index f54e138c67..30db29374e 100644
--- a/macros/parserFormulaUpToConstant.pl
+++ b/macros/parserFormulaUpToConstant.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserFormulaUpToConstant.pl,v 1.23 2010/02/08 13:56:09 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserFormulaWithUnits.pl b/macros/parserFormulaWithUnits.pl
index 556648c743..4eeba49335 100644
--- a/macros/parserFormulaWithUnits.pl
+++ b/macros/parserFormulaWithUnits.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserFunction.pl b/macros/parserFunction.pl
index b407c11d4d..13458982a1 100644
--- a/macros/parserFunction.pl
+++ b/macros/parserFunction.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserFunctionPrime.pl b/macros/parserFunctionPrime.pl
index 2382d183d6..a02183a4dc 100644
--- a/macros/parserFunctionPrime.pl
+++ b/macros/parserFunctionPrime.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl
index 9741945c15..1edfd21dd0 100644
--- a/macros/parserGraphTool.pl
+++ b/macros/parserGraphTool.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2020 The WeBWorK Project, http://openwebwork.sf.net/
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/parserImplicitEquation.pl b/macros/parserImplicitEquation.pl
index 269d8edad1..7e66a5c70a 100644
--- a/macros/parserImplicitEquation.pl
+++ b/macros/parserImplicitEquation.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserImplicitEquation.pl,v 1.14 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserImplicitPlane.pl b/macros/parserImplicitPlane.pl
index 50a8111c5f..f482f23b76 100644
--- a/macros/parserImplicitPlane.pl
+++ b/macros/parserImplicitPlane.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserLinearInequality.pl b/macros/parserLinearInequality.pl
index 943edcf6dd..6bcd34b3e4 100644
--- a/macros/parserLinearInequality.pl
+++ b/macros/parserLinearInequality.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserMultiAnswer.pl b/macros/parserMultiAnswer.pl
index 7d8670177a..b37a547ae0 100644
--- a/macros/parserMultiAnswer.pl
+++ b/macros/parserMultiAnswer.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserMultiAnswer.pl,v 1.11 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserMultiPart.pl b/macros/parserMultiPart.pl
index f5a1937e12..77b2196a76 100644
--- a/macros/parserMultiPart.pl
+++ b/macros/parserMultiPart.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserNumberWithUnits.pl b/macros/parserNumberWithUnits.pl
index 3d58bae5b1..6c8169bbea 100644
--- a/macros/parserNumberWithUnits.pl
+++ b/macros/parserNumberWithUnits.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserOneOf.pl b/macros/parserOneOf.pl
index 424d496d09..5033cce410 100644
--- a/macros/parserOneOf.pl
+++ b/macros/parserOneOf.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserParametricLine.pl b/macros/parserParametricLine.pl
index 9cda736bc5..b903df6242 100644
--- a/macros/parserParametricLine.pl
+++ b/macros/parserParametricLine.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserParametricLine.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserParametricPlane.pl b/macros/parserParametricPlane.pl
index 8c64772d31..ec6447221e 100644
--- a/macros/parserParametricPlane.pl
+++ b/macros/parserParametricPlane.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserParametricLine.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserPopUp.pl b/macros/parserPopUp.pl
index f8781a6861..25aa271d9a 100644
--- a/macros/parserPopUp.pl
+++ b/macros/parserPopUp.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserPopUp.pl,v 1.10 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserPrime.pl b/macros/parserPrime.pl
index 089debb573..81deebfe05 100644
--- a/macros/parserPrime.pl
+++ b/macros/parserPrime.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2009 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserPrime.pl,v 1.2 2009/10/03 15:58:49 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserRadioButtons.pl b/macros/parserRadioButtons.pl
index 751eb618b6..f3a9e90dad 100644
--- a/macros/parserRadioButtons.pl
+++ b/macros/parserRadioButtons.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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
diff --git a/macros/parserRoot.pl b/macros/parserRoot.pl
index 122beb9f01..30a7563e41 100644
--- a/macros/parserRoot.pl
+++ b/macros/parserRoot.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserSolutionFor.pl b/macros/parserSolutionFor.pl
index 894058f09e..6cf8778831 100644
--- a/macros/parserSolutionFor.pl
+++ b/macros/parserSolutionFor.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserVectorUtils.pl b/macros/parserVectorUtils.pl
index f43061561f..c554c81518 100644
--- a/macros/parserVectorUtils.pl
+++ b/macros/parserVectorUtils.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/parserWordCompletion.pl b/macros/parserWordCompletion.pl
index d55ce6dbc0..5c556cbbc7 100644
--- a/macros/parserWordCompletion.pl
+++ b/macros/parserWordCompletion.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright � 2000-2015 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserWordCompletion.pl,v 1.0 2015/11/25 23:28:44 paultpearson Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/problemPanic.pl b/macros/problemPanic.pl
index 4a398a63ac..fe11f3544a 100644
--- a/macros/problemPanic.pl
+++ b/macros/problemPanic.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2009 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/problemPanic.pl,v 1.6 2010/04/27 02:00:37 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/problemPreserveAnswers.pl b/macros/problemPreserveAnswers.pl
index ad72195a2d..9ba7752c1a 100644
--- a/macros/problemPreserveAnswers.pl
+++ b/macros/problemPreserveAnswers.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/problemRandomize.pl b/macros/problemRandomize.pl
index 092e391949..e996990d4b 100644
--- a/macros/problemRandomize.pl
+++ b/macros/problemRandomize.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/problemRandomize.pl,v 1.12 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # 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
diff --git a/macros/scaffold.pl b/macros/scaffold.pl
index 1b7d2b5cc7..54aba86773 100644
--- a/macros/scaffold.pl
+++ b/macros/scaffold.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # 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

From 18d4db0c526451bb73af5f9306192dee66a30430 Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Fri, 22 Apr 2022 18:03:54 -0500
Subject: [PATCH 12/38] Fix tab order for the three point quadratic and four
 point cubic tools.

There was an indexing error in the logic of the tab focus handler of the selection tool that results in the last defining point of an object that has three or more defining points not working correctly.

To see the issue, graph any object and then graph a quadratic or cubic after that.  Immediately after completing the graph of the quadratic or cubic hit shift-tab.  With the code in the release candidate branch the focus will jump back to the last point on first object that was graphed.  With this pull request the focus will go to the next to last point on the quadratic or cubic as it should.
---
 htdocs/js/apps/GraphTool/graphtool.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/htdocs/js/apps/GraphTool/graphtool.js b/htdocs/js/apps/GraphTool/graphtool.js
index 0885a92bf2..2a52364559 100644
--- a/htdocs/js/apps/GraphTool/graphtool.js
+++ b/htdocs/js/apps/GraphTool/graphtool.js
@@ -1174,7 +1174,8 @@ window.graphTool = (containerId, options) => {
 							if (e.key !== 'Tab' ||
 								(index === 0 && e.shiftKey) ||
 								(index === gt.graphedObjs.length - 1 && !e.shiftKey) ||
-								(a.length > 1 && ((pIndex === 0 && !e.shiftKey) || (pIndex === 1 && e.shiftKey)))
+								(a.length > 1 &&
+									((pIndex === 0 && !e.shiftKey) || (pIndex === a.length - 1 && e.shiftKey)))
 							)
 								return;
 
@@ -1194,8 +1195,8 @@ window.graphTool = (containerId, options) => {
 
 							obj.blur();
 						};
+						point.rendNode.addEventListener('keydown', point.focusOutHandler);
 					}
-					point.rendNode.addEventListener('keydown', point.focusOutHandler);
 
 					// Attach a focusin handler to all points to update the coordinates display.
 					point.focusInHandler = (e) => gt.setTextCoords(point.X(), point.Y());

From 4ae6f0e061591ca3825592a871c8c1fa58b21edc Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Thu, 19 May 2022 07:08:23 -0500
Subject: [PATCH 13/38] Fix pointer focus for the center of a circle.

Curently the center point of a circle can not be focused by clicking on
it with the mouse.  This is because the jsxgraph circle hasPoint method
only returns true if the circle itself contains the given point, but not
if the center point contains the given point.  So redefine the hasPoint
method for a circle to fix that.
---
 htdocs/js/apps/GraphTool/graphtool.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/htdocs/js/apps/GraphTool/graphtool.js b/htdocs/js/apps/GraphTool/graphtool.js
index 2a52364559..e76d02063a 100644
--- a/htdocs/js/apps/GraphTool/graphtool.js
+++ b/htdocs/js/apps/GraphTool/graphtool.js
@@ -664,6 +664,11 @@ window.graphTool = (containerId, options) => {
 			));
 			this.definingPts.push(center, point);
 			this.focusPoint = center;
+
+			// Redefine the circle's hasPoint method to return true if the center point has the given coordinates, so
+			// that a pointer over the center point will give focus to the object with the center point activated.
+			const circleHasPoint = this.baseObj.hasPoint.bind(this.baseObj);
+			this.baseObj.hasPoint = (x, y) => circleHasPoint(x, y) || center.hasPoint(x, y);
 		}
 
 		handleKeyEvent(e, el) {

From 0cf8cd437ad65f3d19da093ce6b30b2938bdac98 Mon Sep 17 00:00:00 2001
From: Nathan Wallach 
Date: Tue, 24 May 2022 21:28:08 +0300
Subject: [PATCH 14/38] Patch AnswerHints so that if a subroutine in the list
 "dies" there will still be a final result from the AnswerHints postfilter.

---
 macros/answerHints.pl | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/macros/answerHints.pl b/macros/answerHints.pl
index 6c67574fb5..86843e0e11 100644
--- a/macros/answerHints.pl
+++ b/macros/answerHints.pl
@@ -146,11 +146,16 @@ sub AnswerHints {
       foreach my $wrong (@{$wrongList}) {
 	if (ref($wrong) eq 'CODE') {
 	  if (($ans->{score} < 1 || $options{checkCorrect}) &&
-	      ($ans->{ans_message} eq "" || $options{replaceMessage}) &&
-	      &$wrong($correct,$student,$ans)) {
-	    $ans->{ans_message} = $ans->{error_message} = $message;
-	    $ans->{score} = $options{score} if defined $options{score};
-	    last;
+	      ($ans->{ans_message} eq "" || $options{replaceMessage}) ) {
+	      # Make the call to run the function inside an eval to trap errors
+	      my $myResult = 0;
+	      eval { $myResult = &$wrong($correct,$student,$ans); };
+	      if ( $@ ) { $myResult = 0; }
+	      if ( $myResult ) {
+	        $ans->{ans_message} = $ans->{error_message} = $message;
+	        $ans->{score} = $options{score} if defined $options{score};
+	        last;
+	      }
 	  }
 	} else {
 	  $wrong = Value::makeValue($wrong);

From ddcc42c04453b4cdaa6d670d4d053189fcd7dbb4 Mon Sep 17 00:00:00 2001
From: Nathan Wallach 
Date: Thu, 26 May 2022 16:06:46 +0300
Subject: [PATCH 15/38] Provide an error message as the result message when a
 subroutine called by AnswerHints dies.

---
 macros/answerHints.pl | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/macros/answerHints.pl b/macros/answerHints.pl
index 86843e0e11..725c92b8c1 100644
--- a/macros/answerHints.pl
+++ b/macros/answerHints.pl
@@ -149,9 +149,11 @@ sub AnswerHints {
 	      ($ans->{ans_message} eq "" || $options{replaceMessage}) ) {
 	      # Make the call to run the function inside an eval to trap errors
 	      my $myResult = 0;
-	      eval { $myResult = &$wrong($correct,$student,$ans); };
-	      if ( $@ ) { $myResult = 0; }
-	      if ( $myResult ) {
+	      eval { $myResult = &$wrong($correct,$student,$ans); } or do {
+	        $ans->{ans_message} = "error during AnswerHints processing";
+	        last;
+	      };
+	      if ($myResult) {
 	        $ans->{ans_message} = $ans->{error_message} = $message;
 	        $ans->{score} = $options{score} if defined $options{score};
 	        last;

From daec8d93352eea39df4bc135b3ce19f457766c60 Mon Sep 17 00:00:00 2001
From: Nathan Wallach 
Date: Thu, 26 May 2022 16:12:19 +0300
Subject: [PATCH 16/38] perltidy macros/answerHints.pl

---
 macros/answerHints.pl | 169 +++++++++++++++++++++++-------------------
 1 file changed, 94 insertions(+), 75 deletions(-)

diff --git a/macros/answerHints.pl b/macros/answerHints.pl
index 725c92b8c1..f88f64a890 100644
--- a/macros/answerHints.pl
+++ b/macros/answerHints.pl
@@ -113,66 +113,81 @@ =head1 AnswerHints()
 
 =cut
 
-sub _answerHints_init {}
+sub _answerHints_init { }
 
 sub AnswerHints {
-  return (sub {
-    my $ans = shift; $ans->{_filter_name} = "Answer Hints Post Filter";
-    my $correct = $ans->{correct_value};
-    my $student = $ans->{student_value};
-    Value::Error("AnswerHints can only be used with MathObjects answer checkers") unless ref($correct);
-    return $ans unless ref($student);
-    my $context = $correct->context;
-    my $hash = $context->{answerHash};
-    $context->{answerHash} = $ans;
-    my $processPreview = $correct->getFlag('answerHintsProcessPreview', 0);
-    $context->{answerHash} = $hash;
-
-    while (@_) {
-      my $wrongList = shift; my $message = shift; my @options;
-      ($message,@options) = @{$message} if ref($message) eq 'ARRAY';
-      my %options = (
-        checkCorrect => 0,
-        replaceMessage => 0,
-	checkTypes => 1,
-        processPreview => $processPreview,
-        score => undef,
-        cmp_options => [],
-        @options,
-      );
-      next if $options{checkTypes} && $correct->type ne $student->type;
-      next if !$options{processPreview} && $ans->{isPreview};
-      $wrongList = [$wrongList] unless ref($wrongList) eq 'ARRAY';
-      foreach my $wrong (@{$wrongList}) {
-	if (ref($wrong) eq 'CODE') {
-	  if (($ans->{score} < 1 || $options{checkCorrect}) &&
-	      ($ans->{ans_message} eq "" || $options{replaceMessage}) ) {
-	      # Make the call to run the function inside an eval to trap errors
-	      my $myResult = 0;
-	      eval { $myResult = &$wrong($correct,$student,$ans); } or do {
-	        $ans->{ans_message} = "error during AnswerHints processing";
-	        last;
-	      };
-	      if ($myResult) {
-	        $ans->{ans_message} = $ans->{error_message} = $message;
-	        $ans->{score} = $options{score} if defined $options{score};
-	        last;
-	      }
-	  }
-	} else {
-	  $wrong = Value::makeValue($wrong);
-	  if (($ans->{score} < 1 || $options{checkCorrect} || AnswerHints::Compare($correct,$wrong,$ans)) &&
-	      ($ans->{ans_message} eq "" || $options{replaceMessage}) &&
-	      AnswerHints::Compare($wrong,$student,$ans,@{$options{cmp_options}})) {
-	    $ans->{ans_message} = $ans->{error_message} = $message;
-	    $ans->{score} = $options{score} if defined $options{score};
-	    last;
-	  }
-	}
-      }
-    }
-    return $ans;
-  },@_);
+	return (
+		sub {
+			my $ans = shift;
+			$ans->{_filter_name} = "Answer Hints Post Filter";
+			my $correct = $ans->{correct_value};
+			my $student = $ans->{student_value};
+			Value::Error("AnswerHints can only be used with MathObjects answer checkers") unless ref($correct);
+			return $ans unless ref($student);
+			my $context = $correct->context;
+			my $hash    = $context->{answerHash};
+			$context->{answerHash} = $ans;
+			my $processPreview = $correct->getFlag('answerHintsProcessPreview', 0);
+			$context->{answerHash} = $hash;
+
+			while (@_) {
+				my $wrongList = shift;
+				my $message   = shift;
+				my @options;
+				($message, @options) = @{$message} if ref($message) eq 'ARRAY';
+				my %options = (
+					checkCorrect   => 0,
+					replaceMessage => 0,
+					checkTypes     => 1,
+					processPreview => $processPreview,
+					score          => undef,
+					cmp_options    => [],
+					@options,
+				);
+				next if $options{checkTypes}      && $correct->type ne $student->type;
+				next if !$options{processPreview} && $ans->{isPreview};
+				$wrongList = [$wrongList] unless ref($wrongList) eq 'ARRAY';
+
+				foreach my $wrong (@{$wrongList}) {
+					if (ref($wrong) eq 'CODE') {
+						if (   ($ans->{score} < 1 || $options{checkCorrect})
+							&& ($ans->{ans_message} eq "" || $options{replaceMessage}))
+						{
+							# Make the call to run the function inside an eval to trap errors
+							my $myResult = 0;
+							eval { $myResult = &$wrong($correct, $student, $ans); } or do {
+								$ans->{ans_message} = "error during AnswerHints processing";
+								last;
+							};
+							if ($myResult) {
+								$ans->{ans_message} = $ans->{error_message} = $message;
+								$ans->{score}       = $options{score} if defined $options{score};
+								last;
+							}
+						}
+					} else {
+						$wrong = Value::makeValue($wrong);
+						if (
+							(
+								   $ans->{score} < 1
+								|| $options{checkCorrect}
+								|| AnswerHints::Compare($correct, $wrong, $ans)
+							)
+							&& ($ans->{ans_message} eq "" || $options{replaceMessage})
+							&& AnswerHints::Compare($wrong, $student, $ans, @{ $options{cmp_options} })
+							)
+						{
+							$ans->{ans_message} = $ans->{error_message} = $message;
+							$ans->{score}       = $options{score} if defined $options{score};
+							last;
+						}
+					}
+				}
+			}
+			return $ans;
+		},
+		@_
+	);
 }
 
 package AnswerHints;
@@ -182,23 +197,27 @@ package AnswerHints;
 #  and returns true if the two values match and false otherwise.
 #
 sub Compare {
-  my $self = shift; my $other = shift; my $ans = shift;
-  $ans = bless {%{$ans},@_}, ref($ans);  # make a copy
-  $ans->{typeError} = 0; $ans->{ans_message} = $ans->{error_message} = ""; $ans->{score} = 0;
-  if (sprintf("%p",$self) ne sprintf("%p",$ans->{correct_value})) {
-    $ans->{correct_ans} = $self->string;
-    $ans->{correct_value} = $self;
-    $ans->{correct_formula} = Value->Package("Formula")->new($self);
-  }
-  if (sprintf("%p",$other) ne sprintf("%p",$ans->{student_value})) {
-    $ans->{student_ans} = $other->string;
-    $ans->{student_value} = $other;
-    $ans->{student_formula} = Value->Package("Formula")->new($other);
-  }
-  $self->cmp_preprocess($ans);
-  $self->cmp_equal($ans);
-  $self->cmp_postprocess($ans) if !$ans->{error_message} && !$ans->{typeError};
-  return $ans->{score} >= 1;
+	my $self  = shift;
+	my $other = shift;
+	my $ans   = shift;
+	$ans = bless { %{$ans}, @_ }, ref($ans);    # make a copy
+	$ans->{typeError}   = 0;
+	$ans->{ans_message} = $ans->{error_message} = "";
+	$ans->{score}       = 0;
+	if (sprintf("%p", $self) ne sprintf("%p", $ans->{correct_value})) {
+		$ans->{correct_ans}     = $self->string;
+		$ans->{correct_value}   = $self;
+		$ans->{correct_formula} = Value->Package("Formula")->new($self);
+	}
+	if (sprintf("%p", $other) ne sprintf("%p", $ans->{student_value})) {
+		$ans->{student_ans}     = $other->string;
+		$ans->{student_value}   = $other;
+		$ans->{student_formula} = Value->Package("Formula")->new($other);
+	}
+	$self->cmp_preprocess($ans);
+	$self->cmp_equal($ans);
+	$self->cmp_postprocess($ans) if !$ans->{error_message} && !$ans->{typeError};
+	return $ans->{score} >= 1;
 }
 
 1;

From 84b66adda38622f463709dfd0cefbe670588578d Mon Sep 17 00:00:00 2001
From: Nathan Wallach 
Date: Thu, 26 May 2022 16:50:59 +0300
Subject: [PATCH 17/38] Limit loop to compute factorial to n<=170, for n>170
 return Perl Inf, as that would be the result of the calculation used for any
 such n due to overflow of the Perl floating point data type.

---
 lib/Parser/UOP/factorial.pm | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/Parser/UOP/factorial.pm b/lib/Parser/UOP/factorial.pm
index 65600fec45..83b8866f28 100644
--- a/lib/Parser/UOP/factorial.pm
+++ b/lib/Parser/UOP/factorial.pm
@@ -25,6 +25,7 @@ sub _eval {
   my $n = shift; my $f = 1;
   $self->Error("Factorial can only be taken of (non-negative) integers")
     unless $n =~ m/^\d+$/;
+  return Inf if $n > 170;
   while ($n > 0) {$f *= $n; $n--}
   return $f;
 }

From d5d55d1be0b2ed8192a464320e83643435f60e48 Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Wed, 25 May 2022 21:58:04 -0500
Subject: [PATCH 18/38] Fix ww3 mq-editor preview button code causing console
 log error in ww2 gateway quizzes.

---
 htdocs/js/apps/MathQuill/mqeditor.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/htdocs/js/apps/MathQuill/mqeditor.js b/htdocs/js/apps/MathQuill/mqeditor.js
index 4793f2654f..eb52f4bd4a 100644
--- a/htdocs/js/apps/MathQuill/mqeditor.js
+++ b/htdocs/js/apps/MathQuill/mqeditor.js
@@ -161,7 +161,7 @@
 				document.querySelector('input[name=previewAnswers]')?.click();
 				// For ww3
 				const previewButtonId =
-					answerQuill.textarea.closest('[name=problemMainForm]')[0]?.id
+					answerQuill.textarea.closest('[name=problemMainForm]')?.id
 						.replace('problemMainForm', 'previewAnswers');
 				if (previewButtonId) document.getElementById(previewButtonId)?.click();
 			}

From db249d920da4f42c9a401b1a4839c8d902913f84 Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Thu, 26 May 2022 15:34:53 -0500
Subject: [PATCH 19/38] Revert to the mqeditor toolbar focus hack previously
 used with WeBWorK and PG 2.16.

It seems that Safari does not properly honor the specifications for the FocusEvent.RelatedTarget, and the new method does not work for that lame browser (at least the current version).
---
 htdocs/js/apps/MathQuill/mqeditor.js | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/htdocs/js/apps/MathQuill/mqeditor.js b/htdocs/js/apps/MathQuill/mqeditor.js
index 4793f2654f..508ccc81eb 100644
--- a/htdocs/js/apps/MathQuill/mqeditor.js
+++ b/htdocs/js/apps/MathQuill/mqeditor.js
@@ -84,8 +84,11 @@
 			{ id: 'text', latex: '\\text', tooltip: 'text mode (")', icon: 'Tt' }
 		];
 
+		answerQuill.hasFocus = false;
+
 		// Open the toolbar when the mathquill answer box gains focus.
 		answerQuill.textarea.addEventListener('focusin', () => {
+			answerQuill.hasFocus = true;
 			if (answerQuill.toolbar) return;
 
 			answerQuill.toolbar = document.createElement('div');
@@ -114,6 +117,7 @@
 				}));
 
 				button.addEventListener('click', () => {
+					answerQuill.hasFocus = true;
 					answerQuill.mathField.cmd(button.dataset.latex);
 					answerQuill.textarea.focus();
 				})
@@ -136,15 +140,15 @@
 		});
 
 		answerQuill.textarea.addEventListener('focusout', (e) => {
-			if (e.relatedTarget && (e.relatedTarget.closest('.quill-toolbar') ||
-				e.relatedTarget.classList.contains('symbol-button')))
-				return;
-			if (answerQuill.toolbar) {
-				window.removeEventListener('resize', answerQuill.toolbar.adjustWidth);
-				answerQuill.toolbar.tooltips.forEach((tooltip) => tooltip.dispose());
-				answerQuill.toolbar.remove();
-				delete answerQuill.toolbar;
-			}
+			answerQuill.hasFocus = false;
+			setTimeout(function() {
+				if (!answerQuill.hasFocus && answerQuill.toolbar) {
+					window.removeEventListener('resize', answerQuill.toolbar.adjustWidth);
+					answerQuill.toolbar.tooltips.forEach((tooltip) => tooltip.dispose());
+					answerQuill.toolbar.remove();
+					delete answerQuill.toolbar;
+				}
+			}, 200);
 		});
 
 		// Trigger an answer preview when the enter key is pressed in an answer box.

From a25ced5ca3ab300e372d99b2b9c5d774f4866fbd Mon Sep 17 00:00:00 2001
From: Peter Staab 
Date: Fri, 27 May 2022 07:20:28 -0400
Subject: [PATCH 20/38] Factorial unit test (#1)

* Added a test for the factorial.

* FIX: clarified some of the language in the tests.
---
 lib/PGEnvironment.pm        |  3 +-
 lib/Parser/UOP/factorial.pm |  2 +-
 t/math_objects/factorial.t  | 78 +++++++++++++++++++++++++++++++++++++
 3 files changed, 81 insertions(+), 2 deletions(-)
 create mode 100644 t/math_objects/factorial.t

diff --git a/lib/PGEnvironment.pm b/lib/PGEnvironment.pm
index c7bd9b793e..51c382c13e 100644
--- a/lib/PGEnvironment.pm
+++ b/lib/PGEnvironment.pm
@@ -62,7 +62,8 @@ sub new {
 		# Load from the conf file.
 		$self->{pg_dir} = $ENV{PG_ROOT};
 
-		my $defaults_file = $self->{pg_dir} . "/conf/pg_defaults.yml";
+		my $defaults_file = "$self->{pg_dir}/conf/pg_defaults.yml";
+		$defaults_file = "$self->{pg_dir}/conf/pg_defaults.dist.yml" unless -r $defaults_file;
 		die "Cannot read the configuration file $defaults_file" unless -r $defaults_file;
 
 		my $options = LoadFile($defaults_file);
diff --git a/lib/Parser/UOP/factorial.pm b/lib/Parser/UOP/factorial.pm
index 83b8866f28..a5f8add846 100644
--- a/lib/Parser/UOP/factorial.pm
+++ b/lib/Parser/UOP/factorial.pm
@@ -25,7 +25,7 @@ sub _eval {
   my $n = shift; my $f = 1;
   $self->Error("Factorial can only be taken of (non-negative) integers")
     unless $n =~ m/^\d+$/;
-  return Inf if $n > 170;
+  return $self->Package("Infinity")->new() if $n > 170;
   while ($n > 0) {$f *= $n; $n--}
   return $f;
 }
diff --git a/t/math_objects/factorial.t b/t/math_objects/factorial.t
new file mode 100644
index 0000000000..5342054ad5
--- /dev/null
+++ b/t/math_objects/factorial.t
@@ -0,0 +1,78 @@
+use warnings;
+use strict;
+
+package main;
+
+use Test::More;
+use Test::Exception;
+
+# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+
+BEGIN {
+	die 'PG_ROOT not found in environment.\n' unless $ENV{PG_ROOT};
+	$main::pg_dir = $ENV{PG_ROOT};
+}
+
+use lib "$main::pg_dir/lib";
+
+require("$main::pg_dir/t/build_PG_envir.pl");
+
+## END OF TOP_MATERIAL
+
+use Parser;
+
+loadMacros('MathObjects.pl');
+
+Context('Numeric');
+Context()->variables->add(y => "Real");
+Context()->variables->add(n => "Real");
+
+my $five_fact = Compute('5!');
+
+use Data::Dumper;
+print Dumper $five_fact->class;
+
+is($five_fact->class, 'Real',   'factorial: check class of object');
+is($five_fact->type,  'Number', 'factorial: check type of object');
+
+ok(Value::isValue($five_fact),    'factorial: check if an object is a value');
+ok(Value::isNumber($five_fact),   'factorial: check if an object is a number');
+ok(Value::isReal($five_fact),     'factorial: check if a number is a real number');
+ok(!Value::isComplex($five_fact), 'factorial: check if an integer is complex');
+ok(!Value::isFormula($five_fact), 'factorial: check if a number is not a formula');
+
+is($five_fact->value,120, 'factorial: 5! is 120');
+is(Compute("0!")->value, 1, 'factorial: 0! is 1');
+
+note('The double factorial is not defined here.');
+my $four_double_fact = Compute("4!!")->value;
+ok(6.2e+23 < $four_double_fact && $four_double_fact < 6.3e+23, 'factorial: 4!! is defined as (4!)!=24!' );
+
+ok(Compute("170!") > 1e+306, 'factorial: 170! is large but not infinite.');
+
+note('Tests for throwing exceptions.');
+
+throws_ok {
+	Compute("(-1)!");
+}
+qr/Factorial can only be taken of \(non-negative\) integers/, 'factorial: can\'t take factorial of negative integers.';
+
+throws_ok {
+	Compute("1.5!");
+}
+qr/Factorial can only be taken of \(non-negative\) integers/, 'factorial: can\'t take factorial of non-integer reals.';
+
+note('Try taking factorials of variables');
+my $n_fact = Compute("n!");
+is($n_fact->class, "Formula", "factorial: n! is a Formula");
+is($n_fact->type,  "Number",  "factorial: n! has type is Number");
+is($n_fact->eval(n=>5), 120, 'factorial: n! evaluated at n=5 is correct.');
+
+# check infinite values
+note('Tests for infinite values');
+
+my $large_fact = Compute('171!');
+my $inf = Compute('inf');
+is($large_fact->value, $inf, '171! is infinite.');
+
+done_testing();

From f8bdf3ceaf17aab63978f57aae4ccae657af1c86 Mon Sep 17 00:00:00 2001
From: Nathan Wallach 
Date: Sun, 29 May 2022 14:47:27 +0300
Subject: [PATCH 21/38] MathQuill when used in an RTL language should still
 create the input boxes as LTR and the menu should internally be in LTR so the
 symbols will appear properly.

---
 htdocs/js/apps/MathQuill/mqeditor.css | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/htdocs/js/apps/MathQuill/mqeditor.css b/htdocs/js/apps/MathQuill/mqeditor.css
index 57d4889c72..1cfc8b61db 100644
--- a/htdocs/js/apps/MathQuill/mqeditor.css
+++ b/htdocs/js/apps/MathQuill/mqeditor.css
@@ -15,6 +15,7 @@ span[id^="mq-answer"].incorrect {
 }
 
 span[id^="mq-answer"] {
+	/*rtl:ignore*/
 	direction: ltr;
 	padding: 4px 5px 2px 5px;
 	border-radius: 4px !important;
@@ -31,6 +32,7 @@ input[type="text"].codeshard.mq-edit {
 	max-height: 95vh;
 	position: fixed;
 	font-size: .75em;
+	/*rtl:ignore*/
 	direction: ltr;
 	display: flex;
 	flex-direction: column;

From 1acd618ceb42d435d81b0bf93428ba3b2d12d7ee Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Sun, 29 May 2022 11:39:59 -0500
Subject: [PATCH 22/38] Fix a typo in the scaffold.scss file.

---
 htdocs/js/apps/Scaffold/scaffold.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/htdocs/js/apps/Scaffold/scaffold.scss b/htdocs/js/apps/Scaffold/scaffold.scss
index 1c7d5da450..a62cbb3a5f 100644
--- a/htdocs/js/apps/Scaffold/scaffold.scss
+++ b/htdocs/js/apps/Scaffold/scaffold.scss
@@ -33,7 +33,7 @@
 			& > button.accordion-button {
 				background: #eee;
 
-				&::focus {
+				&:focus {
 					box-shadow: none;
 				}
 

From feb490ce06e227c7af1c64c6a5653c91387f82d8 Mon Sep 17 00:00:00 2001
From: Nathan Wallach 
Date: Thu, 2 Jun 2022 10:03:03 +0300
Subject: [PATCH 23/38] Fix a serious bug discovered by Glenn Rice - incorrect
 handling if the AnswerHints test "wrong" subroutine used to trigger hints
 returns a zero. Change the text of the error message issued when the "eval"
 fails, and make it a "warn" instead of a results table "answer message".

---
 macros/answerHints.pl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/macros/answerHints.pl b/macros/answerHints.pl
index f88f64a890..9ad34f7346 100644
--- a/macros/answerHints.pl
+++ b/macros/answerHints.pl
@@ -155,8 +155,8 @@ sub AnswerHints {
 						{
 							# Make the call to run the function inside an eval to trap errors
 							my $myResult = 0;
-							eval { $myResult = &$wrong($correct, $student, $ans); } or do {
-								$ans->{ans_message} = "error during AnswerHints processing";
+							eval { $myResult = &$wrong($correct, $student, $ans); 1; } or do {
+								warn "An error occurred in this problem.";
 								last;
 							};
 							if ($myResult) {

From 33048ac0d7d0041cdac3fd7891b7109f867e2b5f Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sat, 4 Jun 2022 14:06:11 +0100
Subject: [PATCH 24/38] Correct value for electron volt in Units.pm

Correct exponential for factor of eV
fixes #677

Adds definitions for kev, MeV and GeV

Remove t/README because it has been superseded by t/README.md
---
 lib/Units.pm | 31 ++++++++++++++++++++++++++-----
 t/README     | 14 --------------
 2 files changed, 26 insertions(+), 19 deletions(-)
 delete mode 100644 t/README

diff --git a/lib/Units.pm b/lib/Units.pm
index 50830b868f..a2dc07505f 100644
--- a/lib/Units.pm
+++ b/lib/Units.pm
@@ -475,9 +475,12 @@ our %known_units = ('m'  => {
 # cal    -- calorie
 # kcal   -- kilocalorie
 # eV     -- electron volt
+# keV    -- kilo electron volt
+# MeV    -- mega electron volt
+# GeV    -- giga electron volt
 # kWh    -- kilo Watt hour
 #
-                    'J'  => {
+                  'J'  => {
                            'factor'    => 1,
                            'm'         => 2,
                            'kg'        => 1,
@@ -501,13 +504,13 @@ our %known_units = ('m'  => {
                            'kg'        => 1,
                            's'         => -2
                           },
-                'kt'  => {
+                 'kt'  => {
                            'factor'    => 4.184E12,
                            'm'         => 2,
                            'kg'        => 1,
                            's'         => -2
                           },
-                'Mt'  => {
+                 'Mt'  => {
                            'factor'    => 4.184E15,
                            'm'         => 2,
                            'kg'        => 1,
@@ -525,8 +528,26 @@ our %known_units = ('m'  => {
                            'kg'        => 1,
                            's'         => -2
                           },
-                'eV'  => {
-                           'factor'    => 1.60E-9,
+                 'eV'  => {
+                           'factor'    => 1.6022E-19,
+                           'm'         => 2,
+                           'kg'        => 1,
+                           's'         => -2
+                          },
+                'keV'  => {
+                           'factor'    => 1.6022E-16,
+                           'm'         => 2,
+                           'kg'        => 1,
+                           's'         => -2
+                          },
+                'MeV'  => {
+                           'factor'    => 1.6022E-13,
+                           'm'         => 2,
+                           'kg'        => 1,
+                           's'         => -2
+                          },
+                'GeV'  => {
+                           'factor'    => 1.6022E-10,
                            'm'         => 2,
                            'kg'        => 1,
                            's'         => -2
diff --git a/t/README b/t/README
deleted file mode 100644
index 77ca89b829..0000000000
--- a/t/README
+++ /dev/null
@@ -1,14 +0,0 @@
-These files, contributed by Boyd Duffe, are an example of writing unit tests 
-for PG modules so that we can determine (a) that they work, and (b) that they continue
-to work when some seemingly innocuous change elsewhere in the system causes troubles
-in this module.  
-
-Of course this is not supposed to happen when modular programming practices are followed
-but it happens anyway. :-)
-
-We need more "unit tests" like this.  (The unit does not refer to Unit.pm but to testing
-a small piece or unit of the WeBWorK/PG code.)  I would also like to wire these
-in so that administrators can perform these tests from the admin page on the web
-to reassure themselves that the new version of PG they are using is functioning correctly.
-
- 

From a0d5829f814a0e312d822cd9da59cb81341da61c Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sat, 4 Jun 2022 18:09:07 +0100
Subject: [PATCH 25/38] Added unit tests for Units.pm

Tests to validate the correctness of #677
Unit tests for Units.pm electron volts
Tests for parserNumberWithUnits.pl with basic energy conversions

Adding a Test::PG module in t/lib/ to take the place of t/build_PG_envir.pl
---
 t/README.md              |  83 ++++------------
 t/lib/Test/PG.pm         | 108 +++++++++++++++++++++
 t/units/basic_module.t   |  24 +++++
 t/units/basic_parser.t   | 202 +++++++++++++++++++++++++++++++++++++++
 t/units/electron_volts.t |  30 ++++++
 5 files changed, 383 insertions(+), 64 deletions(-)
 create mode 100644 t/lib/Test/PG.pm
 create mode 100644 t/units/basic_module.t
 create mode 100644 t/units/basic_parser.t
 create mode 100644 t/units/electron_volts.t

diff --git a/t/README.md b/t/README.md
index 93f8ef8c8b..d40515112f 100644
--- a/t/README.md
+++ b/t/README.md
@@ -1,72 +1,27 @@
-# Unit Tests for PG
+# Welcome to WeBWorK
 
-The individual unit tests are located in each of the directories.
+![main workflow](https://github.com/pstaabp/pg/actions/workflows/coverage.yml/badge.svg)
+[![codecov](https://codecov.io/gh/pstaabp/pg/branch/unit-test/graph/badge.svg?token=H7WYHBDB9S)](https://codecov.io/gh/pstaabp/pg)
+![GitHub last commit](https://img.shields.io/github/last-commit/pstaabp/pg/unit-test)
 
-Formal unit tests are located in the the `macros` and `contexts` directories that are designed to test the pg macros and contexts respectively.
+WeBWorK is an open-source online homework system for math and sciences courses. WeBWorK is supported by the MAA and the NSF and comes with an Open Problem Library (OPL) of over 30,000 homework problems. Problems in the OPL target most lower division undergraduate math courses and some advanced courses. Supported courses include college algebra, discrete mathematics, probability and statistics, single and multivariable calculus, differential equations, linear algebra and complex analysis.  Find out more at the main WeBWorK [webpage](http://webwork.maa.org).
 
-## Running the tests
+## Information for Users
 
-```bash
-cd $PG_ROOT
-prove -r .
-```
+New users interested in getting started with their own WeBWorK server, or instructors looking to learn more about how to use WeBWorK in their classes, should take a look at one of the following resources:
 
-will run all of the tests in `.t` files within subdirectories of `t`.
+* [WeBWorK wiki](http://webwork.maa.org/wiki/Main_Page) - The main WeBWorK wiki
+  * [Instructors](http://webwork.maa.org/wiki/Instructors) - Information for Instructors
+  * [Problem Authors](http://webwork.maa.org/wiki/Authors) - Information for Problem Authors
+* [WW_Install](https://github.com/aubreyja/ww_install) - Information for using the WW_install script
+* [Forum](http://webwork.maa.org/moodle/mod/forum/index.php?id=3) - The WeBWorK Forum
+* [Frequently Asked Questions](https://github.com/openwebwork/webwork2/wiki/Frequently-Asked-Questions) - A list of frequently asked questions.
 
-### Running an individual test
+## Information For Developers
 
-If instead, you want to run an individual test, for example the `pgaux.t` test suite,
+People interested in developing new features for WeBWorK should take a look at the following resources.  People interested in developing new problems for WeBWorK should visit [Problem Authors](http://webwork.maa.org/wiki/Authors).
 
-```bash
-cd $PG_ROOT/t/macros
-prove -v pgaux.t
-```
-
-which will be verbose (`-v`).
-
-## Writing a Unit Test
-
-To write a unit test, the following is needed at the top of the file:
-
-```perl
-use warnings;
-use strict;
-
-package main;
-
-use Test::More;
-use Test::Exception;
-
-## the following needs to include at the top of any testing  down to TOP_MATERIAL
-
-BEGIN {
-    die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-    $main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
-
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
-```
-
-and ensure that `PG_ROOT` is in your environmental variables.
-
-### Example: Running a test
-
-The following shows how to test a Math object
-
-```perl
-loadMacros("MathObjects.pl");
-
-Context("Numeric");
-
-my $f = Compute("x^2");
-
-# evaluate f at x=2
-
-is(check_score($f->eval(x=>2),"4"),1,"math objects: eval x^2 at x=2");
-```
-
-The `check_score` subroutine evaluates and compares a MathObject with a string representation of the answer.  If the score is 1, then the two are equal.
+* [First Time Setup](https://github.com/openwebwork/webwork2/wiki/First-Time-Setup) - Setting up your clone of this github repo for the first time.
+* [Coding and Workflow](https://github.com/openwebwork/webwork2/wiki/Coding-and-Workflow) -  Our suggested workflow processes.  Following this will make it much easier to get code accepted into the repo.
+* [Creating Pull Requests](https://github.com/openwebwork/webwork2/wiki/Creating-Pull-Requests) - Instructions on how to submit a pull request.
+* [More Information](https://github.com/openwebwork/webwork2/wiki/) - Our Github wiki has additional information for developers, including information about WeBWorK3.
diff --git a/t/lib/Test/PG.pm b/t/lib/Test/PG.pm
new file mode 100644
index 0000000000..c382a303b8
--- /dev/null
+++ b/t/lib/Test/PG.pm
@@ -0,0 +1,108 @@
+package Test::PG;
+
+BEGIN {
+    die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
+    $main::pg_dir = $ENV{PG_ROOT};
+}
+
+use warnings;
+use strict;
+
+use lib "$main::pg_dir/lib"; # only needed if not using prove with -l
+
+
+=head1 Test::PG
+
+This module provides the environment and some generic functions for
+writing tests for PG macros.  Mostly copied from F,
+it also redefines Test2's "exists" function from C to C
+to avoid the conflict with WebWork's exponential function.
+Its final action is to load C.
+
+The reason for the module is that There Is More Than One Way To Do It
+and people develop different styles.  This is my coding style.
+Also we learn the underlying structures better by re-inventing the wheel.
+We just try to make a better wheel, but success is not guaranteed.
+
+This module strives for elegance in reducing boiler plate in test files,
+a minimum of duplicated code and adheres to the principle of least surprise
+by locating modules below the C directory.
+It does not make a decent cup of tea.
+
+=head2 Usage
+
+To test a macro that needs to be loaded, include this preamble
+at the top of your test file and customize.
+
+  use Test2::V0;
+
+  use MyPGLib;   # does your macro need a module?
+
+  use lib 't/lib';
+  use Test::PG;    # setup a minimal WW environment
+
+  loadMacros("parserMyPGMacro.pl");
+
+  Context("Numeric");
+
+And run your test from the $PG_ROOT directory with
+
+  prove -l t/my_macro/my_test.t
+
+
+=head2 TODO
+
+=head3 Quiet warnings in F
+
+The functions sin, cos and atan2 are redefined and generate warnings.
+C them from the symbol table before loading the macro.
+
+F declares C<$deg> twice, it being
+a required file, the package scope isn't heeded by the second C.
+I wonder if this happens every time this macro is loaded.
+
+=cut
+
+
+package main;
+
+$main::{EXISTS} = delete $main::{E}; # redefine Test2's E() function as EXISTS()
+
+my $macros_dir = "$main::pg_dir/macros";
+
+# use WeBWorK::Localize;
+use PGcore;
+use Parser;
+
+# build up enough of a PG environment to get things running
+our %envir = (
+    htmlDirectory       => '/opt/webwork/courses/daemon_course/html',
+    htmlURL             => 'http://localhost/webwork2/daemon_course/html',
+    tempURL             => 'http://localhost/webwork2/daemon_course/tmp',
+    pgDirectories       => { macrosPath => ["$macros_dir"] },
+    macrosPath          => ["$macros_dir"],
+    displayMode         => 'HTML_MathJax',
+    language            => 'en-us',
+    language_subroutine => sub { return @_; },    # return the string passed in instead going to maketext
+);
+
+sub be_strict {
+        require 'ww_strict.pm';
+        strict::import();
+}
+
+sub PG_restricted_eval {
+        WeBWorK::PG::Translator::PG_restricted_eval(@_);
+}
+
+sub check_score {
+        my ($correct_answer, $ans) = @_;
+        return $correct_answer->cmp->evaluate($ans)->{score};
+}
+
+require "$macros_dir/PG.pl";
+DOCUMENT();
+
+loadMacros('PGbasicmacros.pl');
+
+1;
diff --git a/t/units/basic_module.t b/t/units/basic_module.t
new file mode 100644
index 0000000000..0ad70fe095
--- /dev/null
+++ b/t/units/basic_module.t
@@ -0,0 +1,24 @@
+use Test2::V0;
+
+use Units;
+
+# get unit hashes
+my %joule = evaluate_units('J');
+my %newton_metre = evaluate_units('N*m');
+my %energy_base_units = evaluate_units('kg*m^2/s^2');
+
+# basic definitions of energy equivalence
+is \%joule, \%newton_metre,
+    'A joule is a newton-metre';
+is \%joule, \%energy_base_units,
+    'A joule is a kg metre squared per second squared';
+
+
+# test the error handling
+my $fake = 'bleurg';
+ok my %error = evaluate_units($fake);
+like $error{ERROR}, qr/UNIT ERROR Unrecognizable unit: \|$fake\|/,
+    "No unit '$fake' defined in Units file";
+
+
+done_testing;
diff --git a/t/units/basic_parser.t b/t/units/basic_parser.t
new file mode 100644
index 0000000000..8fb3bc661a
--- /dev/null
+++ b/t/units/basic_parser.t
@@ -0,0 +1,202 @@
+use Test2::V0;
+
+use Parser::Legacy::NumberWithUnits; # load this before the parser macro
+use Units;
+
+use lib 't/lib';
+use Test::PG;
+
+loadMacros("parserNumberWithUnits.pl");
+
+Context("Numeric");
+
+
+=head1 NumberWithUnits
+
+We test the basic functionality of the NumberWithUnits parser,
+F, to give us faith
+that the parser and its methods are working.
+Other test files will probe deeper into specific use cases of
+the NumberWithUnits macro and the L module.
+
+=head2 Testing Strategy
+
+Test all the methods of an object, check the attributes, verify the errors
+are thrown correctly, display strings look the way they should and
+all the ways that a student could answer produce the appropriate results.
+Check that the objects we create belong to their expected class.
+
+Demonstrate some of the flavour of Test2, with hash, bag, dies, todo, etc.
+Group similar tests into subtests.
+
+=head3 Setup
+
+All the boilerplate code is loaded with Test::PG and assume that people run
+it from the root directory with C.
+Load your base modules before loading the macros which depend on them
+and set the Context, if appropriate.
+
+See the example in the documentation of L
+
+  perldoc t/lib/Test/PG.pm
+
+=head2 TODO list
+
+=over 4
+
+=item Fix display of temperature units
+
+=item Test adding new units
+
+=item Look up how to get the value of the object instead of reaching into the hashref
+
+=item Test messages from wrong student answer submissions
+
+=back
+
+=cut
+
+
+# define some basic objects
+ok my $joule = NumberWithUnits(1, 'J');
+ok my $Nm    = NumberWithUnits(1, 'N*m');
+ok my $energy_base_units = NumberWithUnits(1, 'kg*m^2/s^2');
+
+
+subtest 'Verify classes and methods' => sub {
+    isa_ok $joule, 'Parser::Legacy::NumberWithUnits';
+    can_ok $joule,
+        [ qw/cmp splitUnits getUnitNames getUnits TeXunits cmp_parse adjustCorrectValue
+             add_fundamental_unit add_unit string TeX / ],
+        'Can we NumberWithUnits';
+
+    ok my $evaluator = $joule->cmp($Nm), 'Get an AnswerEvaluator';
+    isa_ok $evaluator, 'AnswerEvaluator';
+    can_ok $evaluator, [ qw/ evaluate / ], 'We Can Evaluate';
+};
+
+
+subtest 'Check attributes' => sub {
+    is(
+        $joule,
+        {
+            data      => [ 1 ],
+            units     => 'J',
+            units_ref => { kg => 1, m => 2, s => -2, factor => 1,
+                           amp => 0, cd => 0, mol => 0, rad => 0,
+                           degC => 0, degF => 0, degK => 0,
+            },
+            isValue => T(),
+            context => check_isa 'Parser::Context',
+        },
+        'This looks like a joule'
+);
+};
+
+subtest 'Basic definitions of energy equivalence' => sub {
+    is $joule->{data}->[0], $Nm->{data}->[0], 'One joule is one newton-metre';
+    is $joule->getUnits, $Nm->getUnits, 'A joule has the same dimensions as a newton-metre';
+
+    is (check_score($joule, $Nm), 1, 'A Joule is a Newton-metre');
+    is (check_score($joule, $energy_base_units), 1, 'A Joule can be expressed in SI base units');
+};
+
+subtest 'Test error handling' => sub {
+    my $fake = 'bleurg';
+
+    like(
+        dies { NumberWithUnits(1, "$fake") },
+        qr/Unrecognizable unit: \|$fake\|/,
+        "No unit '$fake' defined in Units file"
+    );
+    like(
+        dies { NumberWithUnits(1) },
+        qr/You must provide units for your number/,
+        "No unit given"
+    );
+    like(
+        dies { NumberWithUnits('J') },
+        qr/You must provide units for your number/,
+        "No value given, wants 2 arguments"
+    );
+};
+
+subtest 'Check parsing of arguments' => sub {
+    ok my $three_args = NumberWithUnits(1, 'N', 'm'), 'Ignores extra argument';
+    is $three_args->string, '1 N', 'Only sees the first unit';
+
+    ok my $string_arg = NumberWithUnits('1J'), 'Parses string argument';
+    is $string_arg->string, '1 J', 'Parses string correctly';
+};
+
+subtest 'Check some known units' => sub {
+    ok my @unit_names = (split /\|/, $joule->getUnitNames), 'Can getUnitNames';
+
+    is \@unit_names,
+        bag {
+            all_items( match qr/^[-%\w]+$/ );
+            item 'J'; item 'N';
+            item 'm'; item 'kg'; item 's';
+            etc;
+        },
+        'Basic units loaded, sanity check';
+};
+
+subtest 'Check other methods' => sub {
+    is [ $joule->splitUnits ], ['1', 'J'], 'splitUnits creates an array';
+
+    is $joule->adjustCorrectValue, 0, 'What is adjustCorrectValue?';
+};
+
+subtest 'Check display methods' => sub {
+    is $joule->string,   '1 J',           'Displays string - Joule';
+    is $joule->TeX,      '1\ {\rm J}',    'Displays LaTeX string - Joule';
+    is $joule->TeXunits, '{\rm 1 J}',     'Displays LaTeX string - Joule';
+    is $Nm->TeX,         '1\ {\rm N\,m}', 'Displays LaTeX string - Newton metre';
+    like $energy_base_units->TeX,
+        qr/ 1\\ \s \{ \S* \\frac\{ \\rm\S* \s kg \\, m\^\{2\}\} \{\\rm\S* \s s\^\{2\}\}\} /x,
+        'Displays LaTeX string - energy in SI base units';
+
+    ok my $celsius = NumberWithUnits(1, 'degC');
+    ok my $kelvin  = NumberWithUnits(1, 'degK');
+    todo 'Fix the display of temperatures' => sub {
+        is $celsius->TeX, '1\ {\rm ^{\circ}C}', 'Displays LaTeX string for degrees (finally)';
+        is $kelvin->TeX,  '1\ {\rm K}',         'Displays LaTeX string for kelvin, no degree sign';
+    };
+};
+
+subtest 'Check possible answer format branches' => sub {
+    # re-write without check_score so we can get the messages to students
+
+    is check_score($joule, '1 J'),       1, 'one Joule plain';
+    is check_score($joule, '1.00 J'),    1, 'one Joule float';
+    is check_score($joule, '1E0 J'),     1, 'one Joule exponential notation';
+    is check_score($joule, '7/7 J'),     1, 'one Joule value calculated';
+    is check_score($joule, '1 J^1'),     1, 'one Joule to the power of one';
+    is check_score($joule, 'J 1'),       0, 'one Joule wrong order';
+    is check_score($joule, '2 J'),       0, 'one Joule wrong value';
+    is check_score($joule, '1 j'),       0, 'one Joule wrong case';
+    is check_score($joule, '1'),         0, 'one Joule missing unit';
+    is check_score($joule, 'J'),         0, 'one Joule missing value';
+    is check_score($joule, '1J'),        1, 'one Joule missing space between value and unit is valid';
+    is check_score($joule, '1 N'),       0, 'one Joule wrong unit force not energy';
+    is check_score($joule, '1 Nm'),      0, 'one Joule Nm missing *';
+    is check_score($joule, '1 N*m'),     1, 'one Joule as Newton metre';
+    is check_score($joule, '1 Joule'),   0, 'one Joule in words';
+    is check_score($joule, '1E-3 kJ'),   1, 'one Joule value as exponential';
+};
+
+todo 'check_score is stateful.  Cannot handle repeated calls' => sub { 
+    is check_score($joule, '1E-3 kJ'),   1, 'one Joule value as exponential second call';
+    is check_score($joule, '1E-3 kJ'),   1, 'one Joule value as exponential third call';
+
+    # the other tests I'd like to run
+    is check_score($joule, '0.001 kJ'),     1, 'one Joule decimal kJ';
+    is check_score($joule, '1/1000 kJ'),    1, 'one Joule fractional kJ';
+    is check_score($joule, '10^-3 kJ'),     1, 'one Joule latex power kJ';
+    is check_score($joule, '1 x 10^-3 kJ'), 1, 'one Joule scientific notation';
+    is check_score($joule, '10**-3 kJ'),    1, 'one Joule power of 10 kJ';
+};
+
+
+done_testing;
diff --git a/t/units/electron_volts.t b/t/units/electron_volts.t
new file mode 100644
index 0000000000..03b667f0f1
--- /dev/null
+++ b/t/units/electron_volts.t
@@ -0,0 +1,30 @@
+use Test2::V0;
+
+use Units;
+
+my %joule = evaluate_units('J');
+my %newton_metre = evaluate_units('N*m');
+my %base_units = evaluate_units('kg*m^2/s^2');
+
+my %electron_volt = evaluate_units('eV');
+my %kev = evaluate_units('keV');
+my %mev = evaluate_units('MeV');
+my %gev = evaluate_units('GeV');
+
+is \%electron_volt, by_factor( 1.6022E-19, \%joule ),
+    'eV and joules differ by a factor of 1.6022 x 10^19';
+is \%kev, by_factor( 1000, \%electron_volt ),  'kilo is factor 1000';
+is \%mev, by_factor( 10**6, \%electron_volt ), 'mega is factor 10^6';
+is \%gev, by_factor( 10**9, \%electron_volt ), 'giga is factor 10^9';
+
+
+done_testing;
+
+sub by_factor {
+    my ($value, $unit) = @_;
+    my $new_unit = { %$unit }; # shallow copy hash values
+
+    $new_unit->{factor} *= $value;
+
+    return $new_unit;
+}

From 283feb809f0040d901a1e57f198045d498234c1a Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sun, 5 Jun 2022 11:51:36 +0100
Subject: [PATCH 26/38] Migrate trig_degrees.t to Test2

Use the Test::PG environment with Test2

Find that the original test produced unexpected results
and the tests that expose the incorrect behaviour have
been wrapped in a TODO block
---
 t/contexts/trig_degrees.t | 60 +++++++++++++++++++++++++++++----------
 1 file changed, 45 insertions(+), 15 deletions(-)

diff --git a/t/contexts/trig_degrees.t b/t/contexts/trig_degrees.t
index 3d2230ad75..ba8481721f 100644
--- a/t/contexts/trig_degrees.t
+++ b/t/contexts/trig_degrees.t
@@ -1,23 +1,26 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
-use Test::Exception;
+# remove warnings about redefining trig functions
+delete $main::{sin};
+delete $main::{cos};
+delete $main::{atan2};
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+=head2 Errors to be fixed
 
-use lib "$main::pg_dir/lib";
+There is something wrong with either contextTrigDegrees.pl or how this test
+sets up the context.  It looks like it still calculates in radians.
+Maybe the problem is how it imports symbols?
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+When you fix it, the test output will report the test numbers after C
+
+These are the same results as the original Test::More version with build_PG_env.pl
+
+=cut
 
-## END OF TOP_MATERIAL
 
 loadMacros("contextTrigDegrees.pl");
 
@@ -25,9 +28,21 @@ my $ctx = Context("TrigDegrees");
 
 ok(Value::isContext($ctx), "trig degrees: check context");
 
-my $cos60 = Compute("cos(60)");
+ok my $cos60 = Compute("cos(60)"), 'Call Compute';
+ok my $eval_cos60 = $cos60->cmp->evaluate("1/2"), 'evalute an answer to cos(60)';
+
+is $eval_cos60,
+    hash {
+        field type          => 'Value (Real)';
+        field score         => 0;
+        field correct_ans   => "cos(60)";
+        field student_ans   => 0.5;
+        field error_flag    => U();
+        field error_message => DF();
+        etc();
+    }, 'What does the Compute("cos(60)") object look like?';
+
 
-Compute("cos(60)")->cmp->evaluate("1/2");
 # dd Compute("1/2")->value;
 # is (check_score($cos60,"1/2"),1,"trig degrees: cos(60) = 1/2");
 
@@ -38,4 +53,19 @@ Compute("cos(60)")->cmp->evaluate("1/2");
 
 # is (check_score(Compute("cos(60)"),"sin(30)"),1,"trig degrees: cos(60) = 1/2");
 
+# simple sanity checking
+is check_score( Compute('sin(0)'), '0'), 1, 'trig degrees: sin(0) = 0';
+is check_score( Compute('sin(90)'), '1'), 1, 'trig degrees: sin(90) = 1';
+is check_score( Compute('cos(0)'), '1'), 1, 'trig degrees: cos(0) = 1';
+
+todo 'why is cos(90) not equal to 0' => sub {
+	# are we still computing in radians?
+	is check_score( Compute('cos(90)'), '0'), 1, 'trig degrees: cos(90) = 0';
+	is check_score( Compute('cos(90)'), '1.6155E-15'), 1, 'trig degrees: cos(90) ~ 0';
+
+	my $pi = 4 * atan2(1,1);
+	is check_score( Compute("cos($pi/3)"), "sin($pi/6)"), 1, 'trig degrees: cos(60) = sin(30)';
+};
+
+
 done_testing();

From 183b6e995549a8afedc7199e0e19018861c4afc5 Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sun, 5 Jun 2022 13:03:44 +0100
Subject: [PATCH 27/38] Migrate remaining tests in t/context to Test2

Minor changes to migrate tests in t/context from Test::More to Test2
There were few tests in these files.

toltype_digits.t organized into subtests by topic
---
 t/contexts/fraction.t       | 28 ++++-------
 t/contexts/integer.t        | 23 +++------
 t/contexts/toltype_digits.t | 96 +++++++++++++++++--------------------
 3 files changed, 61 insertions(+), 86 deletions(-)

diff --git a/t/contexts/fraction.t b/t/contexts/fraction.t
index ecebcd848f..ec10c3deaf 100644
--- a/t/contexts/fraction.t
+++ b/t/contexts/fraction.t
@@ -1,23 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+# should I "use" Parser Value Parser::Legacy here instead?
 
-use Test::More;
-use Test::Exception;
+use lib 't/lib';
+use Test::PG;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+=head2 Fraction context
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
-
-require("$main::pg_dir/t/build_PG_envir.pl");
+To test the reduction of fractions
 
-## END OF TOP_MATERIAL
+=cut
 
 loadMacros("PGstandard.pl", "MathObjects.pl", "contextFraction.pl");
 
@@ -39,9 +31,9 @@ Context("Fraction");
 # require("Parser::Legacy::LimitedNumeric::Number");
 # require("Parser::Legacy");
 
-my $a1 = Compute("1/2");
-my $a2 = Compute("2/4");
+ok my $a1 = Compute("1/2");
+ok my $a2 = Compute("2/4");
 
-is($a1->value, $a2->value, "contextFraction: reduce fractions");
+is $a1->value, $a2->value, 'contextFraction: reduce fractions';
 
 done_testing();
diff --git a/t/contexts/integer.t b/t/contexts/integer.t
index 7981c240dd..d5f495e84a 100644
--- a/t/contexts/integer.t
+++ b/t/contexts/integer.t
@@ -1,23 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+# should I "use" Parser Value Parser::Legacy here instead?
 
-use Test::More;
-use Test::Exception;
+use lib 't/lib';
+use Test::PG;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+=head2 Integer context
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
+To test for greatest common denomenators and such like.
 
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
+=cut
 
 loadMacros("MathObjects.pl", "contextInteger.pl");
 
@@ -33,4 +25,3 @@ ANS($b->cmp);
 ok(1, "integer test: dummy test");
 
 done_testing();
-
diff --git a/t/contexts/toltype_digits.t b/t/contexts/toltype_digits.t
index f878cea032..4b3f741f85 100644
--- a/t/contexts/toltype_digits.t
+++ b/t/contexts/toltype_digits.t
@@ -1,64 +1,56 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
-use Test::Exception;
+=head2 TolType context
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+To test for tolerances
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
-
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
+=cut
 
 loadMacros("PGstandard.pl", "MathObjects.pl");
 
 my $ctx = Context("Numeric");
-$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 1);
-
 my $pi = Real("pi");
 
-is(check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14");
-is(check_score($pi, Compute("3.141")), 1, "toltype digits: pi is 3.141");
-is(check_score($pi, Compute("3.142")), 1, "toltype digits: pi is 3.142");
-is(check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143");
-is(check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15");
-
-note("");
-note("change tolTrunction to 0");
-
-$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 0);
-is(check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14");
-is(check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141");
-is(check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142");
-is(check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143");
-is(check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15");
-
-note("");
-note("set tolExtraDigits to 2");
-
-$ctx->flags->set(
-	tolType        => 'digits',
-	tolerance      => 3,
-	tolTruncation  => 0,
-	tolExtraDigits => 2
-);
-is(check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14");
-is(check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141");
-is(check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142");
-is(check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143");
-is(check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15");
-
-is(check_score($pi, Compute("3.1416")),    1, "toltype digits: pi is 3.1416");
-is(check_score($pi, Compute("3.1415888")), 1, "toltype digits: pi is 3.1415888");
-is(check_score($pi, Compute("3.1415")),    0, "toltype digits: pi is not 3.1415");
+subtest 'set tolTrunction to 1' => sub {
+	$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 1);
+
+	is check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14";
+	is check_score($pi, Compute("3.141")), 1, "toltype digits: pi is 3.141";
+	is check_score($pi, Compute("3.142")), 1, "toltype digits: pi is 3.142";
+	is check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143";
+	is check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15";
+};
+
+subtest 'set tolTrunction to 0' => sub {
+	$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 0);
+
+	is check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14";
+	is check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141";
+	is check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142";
+	is check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143";
+	is check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15";
+};
+
+subtest 'set tolExtraDigits to 2' => sub {
+	$ctx->flags->set(
+		tolType        => 'digits',
+		tolerance      => 3,
+		tolTruncation  => 0,
+		tolExtraDigits => 2
+	);
+
+	is check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14";
+	is check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141";
+	is check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142";
+	is check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143";
+	is check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15";
+
+	is check_score($pi, Compute("3.1416")),    1, "toltype digits: pi is 3.1416";
+	is check_score($pi, Compute("3.1415888")), 1, "toltype digits: pi is 3.1415888";
+	is check_score($pi, Compute("3.1415")),    0, "toltype digits: pi is not 3.1415";
+};
 
 done_testing();

From 769145049b79ae3e07f0662af377f283993cc8e9 Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sun, 5 Jun 2022 13:24:29 +0100
Subject: [PATCH 28/38] Skip electron volt tests until PG-2.17

---
 t/units/basic_parser.t   |  2 +-
 t/units/electron_volts.t | 24 +++++++++++++++++++-----
 2 files changed, 20 insertions(+), 6 deletions(-)

diff --git a/t/units/basic_parser.t b/t/units/basic_parser.t
index 8fb3bc661a..81e2be0f9e 100644
--- a/t/units/basic_parser.t
+++ b/t/units/basic_parser.t
@@ -90,7 +90,7 @@ subtest 'Check attributes' => sub {
             context => check_isa 'Parser::Context',
         },
         'This looks like a joule'
-);
+    );
 };
 
 subtest 'Basic definitions of energy equivalence' => sub {
diff --git a/t/units/electron_volts.t b/t/units/electron_volts.t
index 03b667f0f1..2a86920c6f 100644
--- a/t/units/electron_volts.t
+++ b/t/units/electron_volts.t
@@ -11,11 +11,25 @@ my %kev = evaluate_units('keV');
 my %mev = evaluate_units('MeV');
 my %gev = evaluate_units('GeV');
 
-is \%electron_volt, by_factor( 1.6022E-19, \%joule ),
-    'eV and joules differ by a factor of 1.6022 x 10^19';
-is \%kev, by_factor( 1000, \%electron_volt ),  'kilo is factor 1000';
-is \%mev, by_factor( 10**6, \%electron_volt ), 'mega is factor 10^6';
-is \%gev, by_factor( 10**9, \%electron_volt ), 'giga is factor 10^9';
+SKIP: {
+	skip('New eV units not available until PG-2.17')
+		if $kev{ERROR} =~ /^UNIT ERROR Unrecognizable unit/;
+
+	is \%electron_volt, by_factor( 1.6022E-19, \%joule ),
+		'eV and joules differ by a factor of 1.6022 x 10^19';
+
+	is \%kev, by_factor( 1000, \%electron_volt ),  'kilo is factor 1000';
+	is \%mev, by_factor( 10**6, \%electron_volt ), 'mega is factor 10^6';
+	is \%gev, by_factor( 10**9, \%electron_volt ), 'giga is factor 10^9';
+}
+
+subtest 'electron volt has units of energy' => sub {
+	my ($ev, $J) = ( { %electron_volt }, { %joule } );
+	delete $ev->{factor};
+	delete $J->{factor};
+
+	is $ev, $J, 'electron volt has units of energy';
+};
 
 
 done_testing;

From 4a1291ec93fd82efb55efed7dbed22770fef946b Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sun, 5 Jun 2022 13:36:48 +0100
Subject: [PATCH 29/38] Add correct t/README.md

This expands on the original t/README.md on the details of
testing.

It replaces the earlier commit in this branch where it
was accidentally replaced with the top level README (or something)
---
 t/README.md | 146 +++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 127 insertions(+), 19 deletions(-)

diff --git a/t/README.md b/t/README.md
index d40515112f..29f3053262 100644
--- a/t/README.md
+++ b/t/README.md
@@ -1,27 +1,135 @@
-# Welcome to WeBWorK
+# Testing PG
 
-![main workflow](https://github.com/pstaabp/pg/actions/workflows/coverage.yml/badge.svg)
-[![codecov](https://codecov.io/gh/pstaabp/pg/branch/unit-test/graph/badge.svg?token=H7WYHBDB9S)](https://codecov.io/gh/pstaabp/pg)
-![GitHub last commit](https://img.shields.io/github/last-commit/pstaabp/pg/unit-test)
+This directory houses the resources for testing PG. It includes a mix
+of strategies for testing at different scales. It helps to catch errors
+before they are found in production and prevent regressions from being
+re-introduced.
 
-WeBWorK is an open-source online homework system for math and sciences courses. WeBWorK is supported by the MAA and the NSF and comes with an Open Problem Library (OPL) of over 30,000 homework problems. Problems in the OPL target most lower division undergraduate math courses and some advanced courses. Supported courses include college algebra, discrete mathematics, probability and statistics, single and multivariable calculus, differential equations, linear algebra and complex analysis.  Find out more at the main WeBWorK [webpage](http://webwork.maa.org).
+The philosophy of
+[Test Driven Design](https://en.wikipedia.org/wiki/Test-driven_development)
+is that when a bug is found, a test is written to show it failing
+and when it is fixed, the test will pass.
+The unit tests are easy to run and amenable to automation.  Some services
+can be "mocked" so that behaviour can be tested in their absence.
+All of this is to provide confidence that the code does what is intended
+and a working test can be better than documentation because it shows how
+the code currently works in practice.
 
-## Information for Users
+Old references can be found on the WebWork wiki page
+[Unit Testing](https://webwork.maa.org/wiki/Unit_Testing)
 
-New users interested in getting started with their own WeBWorK server, or instructors looking to learn more about how to use WeBWorK in their classes, should take a look at one of the following resources:
 
-* [WeBWorK wiki](http://webwork.maa.org/wiki/Main_Page) - The main WeBWorK wiki
-  * [Instructors](http://webwork.maa.org/wiki/Instructors) - Information for Instructors
-  * [Problem Authors](http://webwork.maa.org/wiki/Authors) - Information for Problem Authors
-* [WW_Install](https://github.com/aubreyja/ww_install) - Information for using the WW_install script
-* [Forum](http://webwork.maa.org/moodle/mod/forum/index.php?id=3) - The WeBWorK Forum
-* [Frequently Asked Questions](https://github.com/openwebwork/webwork2/wiki/Frequently-Asked-Questions) - A list of frequently asked questions.
+# Unit Tests
 
-## Information For Developers
+[Unit tests](https://en.wikipedia.org/wiki/Unit_testing) look at small chunks
+of self-coherent code to verify the behaviour of a subroutine or module.
+This is the test you write to catch corner cases or to explore code branches.
+In this repository, all files with the `.t` extension are unit tests which
+are found by Perl's [prove](https://perldoc.perl.org/prove) command.
 
-People interested in developing new features for WeBWorK should take a look at the following resources.  People interested in developing new problems for WeBWorK should visit [Problem Authors](http://webwork.maa.org/wiki/Authors).
+The individual unit tests are located in each of the directories.
+Best practice is to create a directory for each module being tested and
+group similar tests together in separate files with a descriptive name,
+such as **t/units/** for testing the **Units.pm** module.
 
-* [First Time Setup](https://github.com/openwebwork/webwork2/wiki/First-Time-Setup) - Setting up your clone of this github repo for the first time.
-* [Coding and Workflow](https://github.com/openwebwork/webwork2/wiki/Coding-and-Workflow) -  Our suggested workflow processes.  Following this will make it much easier to get code accepted into the repo.
-* [Creating Pull Requests](https://github.com/openwebwork/webwork2/wiki/Creating-Pull-Requests) - Instructions on how to submit a pull request.
-* [More Information](https://github.com/openwebwork/webwork2/wiki/) - Our Github wiki has additional information for developers, including information about WeBWorK3.
+Formal unit tests are located in the the `macros` and `contexts` directories
+that are designed to test the pg macros and contexts respectively.
+
+## Running the tests
+
+```bash
+cd $PG_ROOT
+prove -lr t/
+```
+
+will run all of the tests in `.t` files within subdirectories of `t`.
+
+### Running an individual test
+
+If instead, you want to run an individual test, for example the `pgaux.t` test suite,
+
+```bash
+cd $PG_ROOT/t/macros
+prove -v pgaux.t
+```
+
+which will be verbose (`-v`).
+Or you could use `prove -lv t/macros/pgaux.t` from the root directory. 
+
+## Writing a Unit Test
+
+To write a unit test, the following is needed at the top of the file:
+
+```perl
+use warnings;
+use strict;
+
+package main;
+
+use Test::More;
+use Test::Exception;
+
+## the following needs to include at the top of any testing  down to TOP_MATERIAL
+
+BEGIN {
+    die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
+    $main::pg_dir = $ENV{PG_ROOT};
+}
+
+use lib "$main::pg_dir/lib";
+
+require("$main::pg_dir/t/build_PG_envir.pl");
+
+## END OF TOP_MATERIAL
+```
+
+and ensure that `PG_ROOT` is in your environmental variables.
+
+### Example: Running a test
+
+The following shows how to test a Math object
+
+```perl
+loadMacros("MathObjects.pl");
+
+Context("Numeric");
+
+my $f = Compute("x^2");
+
+# evaluate f at x=2
+
+is(check_score($f->eval(x=>2),"4"),1,"math objects: eval x^2 at x=2");
+```
+
+The `check_score` subroutine evaluates and compares a MathObject with a string representation of the answer.  If the score is 1, then the two are equal.
+
+
+# Integration tests
+
+[Integration testing](https://en.wikipedia.org/wiki/Integration_testing)
+tests components working together as a group.  The files with the `.pg`
+extension are used to demonstrate the output of the rendering engine.
+
+**TODO:** add an explanation of how to run these integration tests
+and their requirements.
+
+
+# Test Dependencies
+
+The tests for **Units.pm** have brought in a new module dependency,
+[Test2](https://metacpan.org/pod/Test2::V0) which is the state of the art in
+testing Perl modules.  It can compare data structures, examine warnings and
+catch fatal errors thrown under expected conditions.  It provides many tools
+for testing and randomly executes its subtests to avoid the programmer
+depending on stateful data.
+
+To make these easier to install with
+[cpanm](https://metacpan.org/dist/App-cpanminus/view/bin/cpanm), there is a
+[cpanfile](https://metacpan.org/dist/Module-CPANfile/view/lib/cpanfile.pod)
+in the root directory.  Use
+
+  cpanm --installdeps .
+
+which will install the runtime and test dependencies.
+To use the cpanfile for a minimal install skipping the test requirements,
+use the `--notest` option with cpanm.

From ccecf083245272fca6e1a9b0f031fdc7d78d802f Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sun, 5 Jun 2022 18:07:22 +0100
Subject: [PATCH 30/38] Migrate macros tests to Test2

Three test files migrated from Test::More to Test2 without modification
(other than moving the environment setup from the script to Test::PG)
---
 t/macros/basicmacros.t       | 24 +++++++-----------------
 t/macros/math_objects_more.t | 20 ++++++--------------
 t/macros/pgaux.t             | 18 ++++++------------
 3 files changed, 19 insertions(+), 43 deletions(-)

diff --git a/t/macros/basicmacros.t b/t/macros/basicmacros.t
index eab341352f..fd69c3d5e7 100644
--- a/t/macros/basicmacros.t
+++ b/t/macros/basicmacros.t
@@ -1,29 +1,19 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
-
-use Test::More;
-use Test::Exception;
+use HTML::Entities;
+use HTML::TagParser;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+use lib 't/lib';
+use Test::PG;
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
 
-use lib "$main::pg_dir/lib";
+=head1 MathObjects
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+=cut
 
-## END OF TOP_MATERIAL
 
 loadMacros("PGbasicmacros.pl");
 
-use HTML::Entities;
-use HTML::TagParser;
-
 my $name      = "myansrule";
 my $named_box = NAMED_ANS_RULE($name);
 
diff --git a/t/macros/math_objects_more.t b/t/macros/math_objects_more.t
index e54bb409e8..37c0ec29d7 100644
--- a/t/macros/math_objects_more.t
+++ b/t/macros/math_objects_more.t
@@ -1,23 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
-use Test::Exception;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+=head1 MathObjects
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+Tests pass
 
-use lib "$main::pg_dir/lib";
+=cut
 
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
 
 loadMacros("MathObjects.pl");
 
diff --git a/t/macros/pgaux.t b/t/macros/pgaux.t
index 73bc3031de..34bec6aeb7 100644
--- a/t/macros/pgaux.t
+++ b/t/macros/pgaux.t
@@ -1,21 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
 
-## the following needs to include at the top of any testing  down to TOP_MATERIAL
+=head1 PGauxiliaryFunctions
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+Tests pass
 
-use lib "$main::pg_dir/lib";
-require("$main::pg_dir/t/build_PG_envir.pl");
+=cut
 
-## END OF TOP_MATERIAL
 
 loadMacros("PGauxiliaryFunctions.pl");
 

From 730a1542100ab1c7b003b7444bd3b744d48ceaed Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Sun, 5 Jun 2022 21:35:09 +0100
Subject: [PATCH 31/38] Migrate math_objects_basics.t to Test2

Convert `throws_ok` to the like/dies construct
and the tests pass.
Arranged sections in subtests
---
 t/macros/math_objects_basics.t | 156 +++++++++++++++++----------------
 1 file changed, 80 insertions(+), 76 deletions(-)

diff --git a/t/macros/math_objects_basics.t b/t/macros/math_objects_basics.t
index a0b5a18d72..933199f73a 100644
--- a/t/macros/math_objects_basics.t
+++ b/t/macros/math_objects_basics.t
@@ -1,25 +1,18 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
-
-use Test::More;
-use Test::Exception;
+use Parser;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+use lib 't/lib';
+use Test::PG;
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
 
-use lib "$main::pg_dir/lib";
+=head1 MathObjects
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+Test MathObject properties and operations.
+Try out operations with Infinity.
 
-## END OF TOP_MATERIAL
+=cut
 
-use Parser;
 
 loadMacros("MathObjects.pl");
 
@@ -28,71 +21,82 @@ Context("Numeric");
 my ($val1, $val2) = (10, 5);
 my $obj1 = Compute($val1);
 my $obj2 = Compute($val2);
-my $one  = Compute("1");
 my $zero = Compute("0");
-
-is($obj1->class, "Real",   "math objects: check class of object");
-is($obj2->type,  "Number", "math objects: check type of object");
-ok($one->isOne,   "math objects: check if a number is 1");
-ok(!$zero->isOne, "math objects: check if a number is not 1");
-ok($zero->isZero, "math objects: check if a number is 0");
-ok(!$one->isZero, "math objects: check if a number is not 0");
-
-ok(Value::isValue($obj1),    "math objects: check if an object is a value");
-ok(Value::isNumber($obj1),   "math objects: check if an object is a number");
-ok(Value::isReal($obj1),     "math objects: check if a number is a real number");
-ok(!Value::isComplex($obj1), "math objects: check if an integer is complex");
-
-ok(!Value::isFormula($obj1), "math objects: check if a number is not a formula");
-
-# check infinite values
-note("Tests for infinite values");
-
-my $inf = Compute("inf");
-is($inf->value, "infinity", "math objects: check for infinity via a string");
-is($inf->class, "Infinity", "math objects: check that the class is Infinity");
-is($inf->type,  "Infinity", "math objects: check that the type is Infinity");
-ok(!Value::isNumber($inf), "math objects: check if inf is a number");
-
-# check that operations with infinity are not allowed
-
-throws_ok {
-	Compute("$obj1+$inf");
-}
-qr/can't be infinities/, "math objects: addition with infinity";
-throws_ok {
-	Compute("$obj1-$inf");
-}
-qr/can't be infinities/, "math objects: subtraction with infinity";
-throws_ok {
-	Compute("$obj1*$inf");
-}
-qr/can't be infinities/, "math objects: multiplication with infinity";
-throws_ok {
-	Compute("$obj1/$inf");
-}
-qr/can't be infinities/, "math objects: division with infinity";
-
-# is($result1->value,"infinity","math objects: check that the sum of a finite and infinite value is infinite");
+ok my $one  = Compute("1"), 'Create a MathObject with Compute';
+
+subtest 'Basic properties of MathObjects' => sub {
+	is $obj1->class,  'Real',   'math objects: check class of object';
+	is $obj2->type,   'Number', 'math objects: check type of object';
+	is $one->isOne,   T(), 'math objects: check if a number is 1';
+	is $zero->isOne,  F(), 'math objects: check if a number is not 1';
+	is $zero->isZero, T(), 'math objects: check if a number is 0';
+	is $one->isZero,  F(), 'math objects: check if a number is not 0';
+};
+
+subtest 'Class methods of Value to determine type' => sub {
+	is Value::isValue($obj1),   T(), 'math objects: check if an object is a value';
+	is Value::isNumber($obj1),  T(), 'math objects: check if an object is a number';
+	is Value::isReal($obj1),    T(), 'math objects: check if a number is a real number';
+	is Value::isComplex($obj1), F(), 'math objects: check if an integer is complex';
+
+	is Value::isFormula($obj1), F(), 'math objects: check if a number is not a formula';
+};
+
+ok my $inf = Compute("inf"), 'Can create Infinity';
+
+subtest 'Tests for infinite values' => sub {
+	is $inf->value, 'infinity', 'math objects: check for infinity via a string';
+	is $inf->class, 'Infinity', 'math objects: check that the class is Infinity';
+	is $inf->type,  'Infinity', 'math objects: check that the type is Infinity';
+	ok !Value::isNumber($inf),  'math objects: check if inf is a number';
+};
+
+subtest 'check that operations with infinity are not allowed' => sub {
+	like(
+		dies { Compute("$obj1+$inf") },
+		qr/can't be infinities/,
+		"math objects: addition with infinity"
+	);
+	like(
+		dies { Compute("$obj1-$inf") },
+		qr/can't be infinities/,
+		"math objects: subtraction with infinity"
+	);
+	like(
+		dies { Compute("$obj1*$inf") },
+		qr/can't be infinities/,
+		"math objects: multiplication with infinity"
+	);
+	like(
+		dies { Compute("$obj1/$inf") },
+		qr/can't be infinities/,
+		"math objects: division with infinity"
+	);
+
+	# is($result1->value,'infinity','math objects: check that the sum of a finite and infinite value is infinite');
+};
 
 my $sum  = $obj1 + $obj2;
 my $diff = $obj1 - $obj2;
-my $prod = $obj1 * $obj2;
-
-is($sum->value,  $val1 + $val2, "math objects: test sum");
-is($diff->value, $val1 - $val2, "math objects: test difference");
-is($prod->value, $val1 * $val2, "math objects: test product");
-
-## check scores using the cmp method
-
-is(check_score($sum,  Compute($sum)),  1, "math object: use cmp to check sum");
-is(check_score($diff, Compute($diff)), 1, "math object: use cmp to check diff");
-is(check_score($prod, Compute($prod)), 1, "math object: use cmp to check prod");
-
-## check some wrong answers;
+ok my $prod = $obj1 * $obj2, 'Operate on two MathObjects';
+
+subtest 'check object operations' => sub {
+	is $sum->value,  $val1 + $val2, 'math objects: test sum';
+	is $diff->value, $val1 - $val2, 'math objects: test difference';
+	is $prod->value, $val1 * $val2, 'math objects: test product';
+};
+
+subtest 'check scores using the cmp method' => sub {
+	is check_score($sum,  Compute($sum)),  1, 'math object: use cmp to check sum';
+	is check_score($diff, Compute($diff)), 1, 'math object: use cmp to check diff';
+	is check_score($prod, Compute($prod)), 1, 'math object: use cmp to check prod';
+};
+
+subtest 'check some wrong answers' => sub {
+	is check_score($sum,  Compute($sum + 1)),  0, 'math object: use cmp to check sum';
+	is check_score($diff, Compute($diff + 1)), 0, 'math object: use cmp to check diff';
+	is check_score($prod, Compute($prod + 1)), 0, 'math object: use cmp to check prod';
+};
 
-is(check_score($sum,  Compute($sum + 1)),  0, "math object: use cmp to check sum");
-is(check_score($diff, Compute($diff + 1)), 0, "math object: use cmp to check diff");
-is(check_score($prod, Compute($prod + 1)), 0, "math object: use cmp to check prod");
 
 done_testing();

From 71a81e62249135c2ea6031a8f9d0457d32ce3771 Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Thu, 9 Jun 2022 13:23:54 +0100
Subject: [PATCH 32/38] Migrating tableau test to Test2

PITA!  The circular references in MathObjects made
the deep comparisons impossible, so all MathObjects
were stringified for comparisons.

WARN and DEBUG message sub redefinitions were removed.
A "validator" and a stringify sub were added to get around
some of the comparison issues.

Organized into subtests.  Needs cleanup, but everything passes.
---
 t/macros/tableau.t | 559 +++++++++++++++++++++++++++------------------
 1 file changed, 331 insertions(+), 228 deletions(-)

diff --git a/t/macros/tableau.t b/t/macros/tableau.t
index a059ab4557..861294cf69 100755
--- a/t/macros/tableau.t
+++ b/t/macros/tableau.t
@@ -1,40 +1,44 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use Class::Accessor;
+use Parser;
+use PGcore;
+use Value;
 
-use Test::More;
-use Test::Exception;
+use lib 't/lib';
+use Test::PG;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+=head1 Tableau
 
-use lib "$main::pg_dir/lib";
+Deep recursion error possibly due to "context" being a reference
+to the original context variable.
+I can't find a way to stop the recursion.
+As a result Test::More::is_deeply( $A, $B )
+becomes Test2::Tools::Compare::is( $A->string, $B->string )
+I wrote a validator for the cases where you are trying to compare
+a Value::Matrix with an array ref of values which doesn't have
+a "string" method.  If someone can clean it up, future coders
+will thank you.
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+Tests many of the functions provided by F
 
-## END OF TOP_MATERIAL
+Removed the redefined WARN and DEBUG MESSAGES
 
-use Parser;
-use Value;
-use Class::Accessor;
-use PGcore;
+Error Messages:
 
-loadMacros("tableau.pl", "Value.pl");    #gives us Real() etc.
+A context appears to have been destroyed without first calling release().
+Based on $@ it does not look like an exception was thrown (this is not always
+a reliable test)
 
-note(
-	"THIS FILE TESTS MANY OF THE FUNCTIONS PROVIDED BY tableau.pl
-#
-#
-"
-);
+=cut
+
+
+loadMacros("tableau.pl", "Value.pl");    #gives us Real() etc.
 
 my %context = ();
 
+
 sub Context { Parser::Context->current(\%context, @_) }
 unless (%context && $context{current}) {
 	# ^variable our %context
@@ -43,13 +47,6 @@ unless (%context && $context{current}) {
 	Context();        # Initialize context (for persistent mod_perl)
 }
 
-sub WARN_MESSAGE {
-	warn("WARN MESSAGE: ", @_);
-}
-
-sub DEBUG_MESSAGE {
-	warn("DEBUG MESSAGE: ", @_);
-}
 Context("Matrix");
 
 Context()->flags->set(
@@ -57,11 +54,27 @@ Context()->flags->set(
 	zeroLevelTol => 1E-5
 );
 
+
 my $A = Real(.0000005);
 my $B = Real(0);
 
-is($A, $B, "test zeroLevel tolerance");
-ok($A == $B, "test zeroLevel tolerance with ok");
+subtest 'test zeroLevel tolerance' => sub {
+	is($A->value, within($B->value, $B->getFlag('zeroLevel')), 'test zeroLevel tolerance');
+	ok($A == $B, 'test zeroLevel tolerance with ok');
+
+	my $real_object = object {
+		prop isa => 'Value::Real';
+
+		field data    => array { item 0 => match qr/\d/; };
+		field context => D();
+
+		call string => match qr/\d/;
+		call TeX    => match qr/\d/;
+	};
+
+	is $A, $real_object, 'Zero is a Real';
+	is $B, $real_object, 'Near-Zero is a Real';
+};
 
 my $money_total = 6000;
 my $time_total  = 600;
@@ -99,186 +112,273 @@ my $tableau1 = Tableau->new(A => $a, b => $b, c => $c);
 #
 ###############################################################
 
-ok(1 == 1,             "trivial first test");
-ok(defined($tableau1), 'tableau has been defined and loaded');
-is(ref($tableau1), "Tableau", 'has type Tableau');
-
-# test "close_enough_to_zero" subroutine
-is $tableau1->close_enough_to_zero(0),     1, "checking_close_enough to zero";
-is $tableau1->close_enough_to_zero(1e-9),  0, "checking_close_enough to zero";
-is $tableau1->close_enough_to_zero(1e-5),  0, "checking_close_enough to zero";
-is $tableau1->close_enough_to_zero(1e-10), 1, "checking_NOT_close_enough to zero for 1e-10 ";
-note("sanity check 1e-10 vs 10**(-10):  ", 1e-10, " ", 10**(-10));
-note(1.e-10);
-note(0.9999e-10);
-note(-0.9999e-10);
-is $tableau1->close_enough_to_zero(0.9999e-10), 1, "checking_close_enough to zero for 0.9999e-10";
-
-is $tableau1->close_enough_to_zero(-0.9999e-10), 1, "checking_close_enough to zero for -0.9999e-10";
+is $tableau1, D(), 'tableau has been defined and loaded';
+is $tableau1, object { prop isa => 'Tableau' }, 'has type Tableau';
+
+subtest 'test "close_enough_to_zero" subroutine' => sub {
+	is $tableau1->close_enough_to_zero(0),     1, 'checking_close_enough to zero';
+	is $tableau1->close_enough_to_zero(1e-9),  0, 'checking_close_enough to zero';
+	is $tableau1->close_enough_to_zero(1e-5),  0, 'checking_close_enough to zero';
+	is $tableau1->close_enough_to_zero(1e-10), 1, 'checking_NOT_close_enough to zero for 1e-10 ';
+	note('sanity check 1e-10 vs 10**(-10):  ', 1e-10, ' ', 10**(-10));
+	note(1.e-10);
+	note(0.9999e-10);
+	note(-0.9999e-10);
+	is $tableau1->close_enough_to_zero(0.9999e-10), 1, 'checking_close_enough to zero for 0.9999e-10';
+
+	is $tableau1->close_enough_to_zero(-0.9999e-10), 1, 'checking_close_enough to zero for -0.9999e-10';
+};
+
+
 note("display stringified  \$tableau1: ", $tableau1, "\n");
 is ref($tableau1), "Tableau", "checking data type is Tableau";
 ok $tableau1 eq "[[-5000,-400,-1,0,1,0,0,-4700],[-3000,-500,0,-1,0,1,0,-4500]]", "checking_stringification of tableau";
 
 is($tableau1->{m}, 2, 'number of constraints is 2');
-is($tableau1->{n}, 4, 'number of variables is 4');
-is_deeply([ $tableau1->{m}, $tableau1->{n} ], [ $tableau1->{A}->dimensions ], '{m},{n} match dimensions of A');
-is_deeply($tableau1->{A},                     $a,                             'constraint matrix');
-is_deeply($tableau1->{b},                     Matrix([$b])->transpose,        'constraint constants is m by 1 matrix');
-is_deeply($tableau1->{c},                     $c,                             'objective function constants');
-is_deeply($tableau1->{A},                     $tableau1->A, '{A} original constraint matrix accessor');
-is_deeply($tableau1->{b},                     $tableau1->b, '{b} orginal constraint constants accessor');
-is_deeply($tableau1->{c},                     $tableau1->c, '{c} original objective function constants accessor');
+
+subtest 'check data structure of tableau object' => sub {
+	is($tableau1->{m}, 2, 'number of constraints is 2');
+	is($tableau1->{n}, 4, 'number of variables is 4');
+	is [ $tableau1->{m}, $tableau1->{n} ], [ $tableau1->{A}->dimensions ],
+		'{m},{n} match dimensions of A';
+
+	is $tableau1,
+		object {
+			field A => object {
+				prop isa => 'Value::Matrix';
+				call string => "$a";
+				field context => D();
+				field data => D();
+			};
+			field b => D();
+			field c => D();
+			etc();
+		}, 'tableau attributes';
+
+	# call the string method to avoid the circular refs
+	is $tableau1->{A}->string, $a->string, 'Constraint matrix';
+	is $tableau1->{b}->string, Matrix([$b])->transpose->string, 'Constraint constants is m by 1 matrix';
+	is $tableau1->{c}->string, $c->string, 'Objective function constants';
+
+	is $tableau1->{A}->string, $tableau1->A->string, '{A} original constraint matrix accessor';
+	is $tableau1->{b}->string, $tableau1->b->string, '{b} original constraint constants accessor';
+	is $tableau1->{c}->string, $tableau1->c->string, '{c} original objective function constants accessor';
+};
 
 my $test_constraint_matrix = Matrix($ra_matrix->[0], $ra_matrix->[1]);
-is_deeply($tableau1->{current_constraint_matrix},
-	$test_constraint_matrix, 'initialization of current_constraint_matrix');
-is_deeply(
-	$tableau1->{current_constraint_matrix},
-	$tableau1->current_constraint_matrix,
-	'current_constraint_matrix accessor'
-);
-is_deeply($tableau1->{current_b},               $tableau1->{b},       'initialization of current_b');
-is_deeply($tableau1->{current_b},               $tableau1->current_b, 'current_b accessor');
-is_deeply([ $tableau1->current_b->dimensions ], [ 2, 1 ],             'dimensions of current_b');
+
+is $tableau1->{current_constraint_matrix}->string,
+	$test_constraint_matrix->string, 'initialization of current_constraint_matrix';
+is
+	$tableau1->{current_constraint_matrix}->string,
+	$tableau1->current_constraint_matrix->string,
+	'current_constraint_matrix accessor';
+is $tableau1->{current_b}->string,               $tableau1->{b}->string,       'initialization of current_b';
+is $tableau1->{current_b}->string,               $tableau1->current_b->string, 'current_b accessor';
+
+is [ $tableau1->current_b->dimensions ], [ 2, 1 ],             'dimensions of current_b';
+
 my $obj_row_test = [ ((-$c)->value, 0, 0, 1, 0) ];
-is_deeply($tableau1->objective_row, $obj_row_test, 'initialization of $tableau->{obj_row}');
-
-is(ref($tableau1->{obj_row}), 'Value::Matrix', '->{obj_row} has type Value::Matrix');
-is(ref($tableau1->obj_row),   'Value::Matrix', '->obj_row has type Value::Matrix');
-is_deeply($tableau1->obj_row,            $tableau1->{obj_row},            'verify mutator for {obj_row}');
-is_deeply(ref($tableau1->objective_row), 'ARRAY',                         '->objective_row has type ARRAY');
-is_deeply($tableau1->objective_row,      [ $tableau1->{obj_row}->value ], 'access to {obj_row}');
-is_deeply($tableau1->objective_row,      [ $tableau1->obj_row->value ],   'objective_row is obj_row->value = ARRAY');
-
-is(ref($tableau1->current_tableau), 'Value::Matrix', '-> current_tableau is Value::Matrix');
-is_deeply($tableau1->current_tableau, Matrix($ra_matrix), 'entire tableau including obj coeff row');
-
-is(ref($tableau1->S), "Value::Matrix", 'slack variables are a Value::Matrix');
-is_deeply($tableau1->S, $tableau1->I($tableau1->m), 'slack variables are identity matrix');
-
-# test basis
-is_deeply(ref($tableau1->basis_columns), "ARRAY",  "{basis_column} has type ARRAY");
-is_deeply($tableau1->basis_columns,      [ 5, 6 ], "initialization of basis");
-is(
-	ref($tableau1->current_basis_matrix),
-	ref(Value::Matrix->I($tableau1->m)),
-	"current_basis_matrix type is MathObjectMatrix"
-);
-is_deeply($tableau1->current_basis_matrix, Value::Matrix->I($tableau1->m), "initialization of basis");
-
-# change basis and test again
-$tableau1->basis(2, 3);
-is_deeply(ref($tableau1->basis_columns), "ARRAY",  "{basis_column} has type ARRAY");
-is_deeply($tableau1->basis_columns,      [ 2, 3 ], " basis columns set to {2,3}");
-is(
-	ref($tableau1->current_basis_matrix),
-	ref($test_constraint_matrix->column_slice(2, 3)),
-	"current_basis_matrix type is MathObjectMatrix"
-);
-is_deeply(
-	$tableau1->current_basis_matrix,
-	$test_constraint_matrix->column_slice(2, 3),
-	"basis_matrix for columns {2,3} is correct"
-);
-is_deeply($tableau1->basis(Set(2, 3)),  List([ 2, 3 ]), "->basis(Set(2,3))");
-is_deeply($tableau1->basis(List(2, 3)), List([ 2, 3 ]), "->basis(List(2,3))");
-is_deeply($tableau1->basis([ 2, 3 ]),   List([ 2, 3 ]), "->basis([2,3])");
-
-# find basis column index corresponding to row index (and value of the basis coefficient)
-
-$tableau1->basis(5, 6);
-note("\nbasis is",                     $tableau1->basis(5, 6));
-note(print $tableau1->current_tableau, "\n");
-is_deeply([ $tableau1->find_leaving_column(1) ], [ 5, 1 ], "find_leaving_column returns [col_index, pivot_value] ");
-is_deeply([ $tableau1->find_leaving_column(2) ], [ 6, 1 ], "find_leaving_column returns [col_index, pivot_value] ");
-
-is_deeply($tableau1->find_next_basis_from_pivot(1, 2), Set(2, 6), "find next basis from pivot (1,2)");
-is_deeply($tableau1->find_next_basis_from_pivot(1, 3), Set(3, 6), "find next basis from pivot (1,3)");
-is_deeply($tableau1->find_next_basis_from_pivot(2, 1), Set(1, 5), "find next basis from pivot (2,1)");
-is_deeply($tableau1->find_next_basis_from_pivot(1, 1), Set(1, 6), "find next basis from pivot (1,1)");
-
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(2, 5) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (2,5)"
-);    # probably shouldn't be doing this.
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(1, 6) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (2,6)"
-);    # probably shouldn't be doing this.
-is_deeply($tableau1->find_next_basis_from_pivot(2, 1), Set(1, 5), "find next basis from pivot (2,1)");
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(2, 6) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (2,6)"
-);    # probably shouldn't be doing this.
-
-$tableau1->basis(2, 3);
-note("\nbasis is",                     $tableau1->basis());
-note(print $tableau1->current_tableau, "\n");
-is_deeply([ $tableau1->find_leaving_column(1) ], [ 2, 500 ], "find_leaving_column returns [col_index, pivot_value] ");
-is_deeply([ $tableau1->find_leaving_column(2) ], [ 3, 500 ], "find_leaving_column returns [col_index, pivot_value] ");
-
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(1, 2) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (1,2)"
-);    # probably shouldn't be doing this either.
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(1, 3) },
-	qr/pivot point should not be in a basis column.*/,
-	"can't pivot in basis column (1,3)"
-);    # probably shouldn't be doing this.
-is_deeply($tableau1->find_next_basis_from_pivot(2, 1), Set(1, 2), "find next basis from pivot (2,1)");
-is_deeply($tableau1->find_next_basis_from_pivot(1, 1), Set(1, 3), "find next basis from pivot (1,1)");
-
-$tableau1->basis(5, 6);
-note("\nbasis is ",              $tableau1->basis());
-note($tableau1->current_tableau, "\n");
-note("find next short cut pivots");
-# ($row_index, $value, $feasible_point) = $self->find_short_cut_row()
-is_deeply([ $tableau1->find_short_cut_row() ],     [ 1, -4700, 0 ], "row 1");
-is_deeply([ $tableau1->find_short_cut_column(1) ], [ 1, -5000, 0 ], "column 1 ");
-is_deeply([ $tableau1->next_short_cut_pivot() ],   [ 1, 1, 0, 0 ],  "pivot (1,1)");
-is_deeply([ $tableau1->next_short_cut_basis() ],   [ 1, 6, undef ], "new basis {1,6} continue");
-$tableau1->current_tableau(1, 6);
-note($tableau1->current_tableau);
-
-is_deeply([ $tableau1->find_short_cut_row ],       [ 2, Value::Real->new(-8.4E+06), 0 ], "find short cut row");
-is_deeply([ $tableau1->find_short_cut_column(2) ], [ 2, Value::Real->new(-1.3E+06), 0 ], "find short cut col 2 ");
-is_deeply([ $tableau1->next_short_cut_pivot() ],   [ 2, 2, 0, 0 ],                       "pivot (2,2)");
-is_deeply([ $tableau1->next_short_cut_basis() ],   [ 1, 2, undef ],                      "new basis {1,2} continue");
-
-$tableau1->current_tableau(1, 2);
-note($tableau1->current_tableau);
-
-is_deeply([ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], "feasible point found");
-is_deeply(
-	[ $tableau1->next_short_cut_basis() ],
-	[ 1, 2, 'feasible_point' ],
-	"all constraints positive at basis {1,2} --start phase2"
-);
-is_deeply([ $tableau1->find_pivot_column('max') ], [ 3, Value::Real->new(-100000), 0 ],      "col 3");
-is_deeply([ $tableau1->find_pivot_row(3) ],        [ 1, Value::Real->new(550000 / 500), 0 ], "row 1 ");
-is_deeply([ $tableau1->find_next_pivot('max') ],   [ 1, 3, 0, 0 ],                           "pivot (1,3)");
-is_deeply([ $tableau1->find_next_basis('max') ],   [ 2, 3, undef ], "new basis {2,3} continue");
-
-$tableau1->current_tableau(2, 3);
-note($tableau1->current_tableau);
-is_deeply([ $tableau1->find_pivot_column('max') ], [ 4, Value::Real->new(-300), 0 ], "col 4");
-is_deeply([ $tableau1->find_pivot_row(4) ], [ 1, 4500, 0 ], "row 2) ");
-
-is_deeply([ $tableau1->find_next_pivot('max') ], [ 1, 4, 0, 0 ], "pivot 1,4");
-is_deeply([ $tableau1->find_next_basis('max') ], [ 3, 4, undef ], "new basis {3,4} continue");
-
-$tableau1->current_tableau(3, 4);
-note($tableau1->current_tableau);
-is_deeply([ $tableau1->find_pivot_column('max') ], [ 5, Value::Real->new(-1), 0 ], "col 5");
-is_deeply([ $tableau1->find_pivot_row(5) ], [ undef, undef, 1 ], "row 2) ");
-
-is_deeply([ $tableau1->find_next_pivot('max') ], [ undef, 5, 0, 1 ], "unbounded -- no pivot");
-is_deeply([ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], "basis 3,4 unbounded");
+for (my $i = 0; $i < 4; $i++) {
+	is $tableau1->objective_row->[$i]->string, $obj_row_test->[$i]->string,
+		'initialization of $tableau->{obj_row}';
+}
+is @{$tableau1->objective_row}[4..7], @{$obj_row_test}[4..7], 'initialization of $tableau->{obj_row}';
+
+is $tableau1->{obj_row},         object { prop isa => 'Value::Matrix' }, '->{obj_row} has type Value::Matrix';
+is $tableau1->obj_row,           object { prop isa => 'Value::Matrix' }, '->obj_row has type Value::Matrix';
+is $tableau1->obj_row->string,   $tableau1->{obj_row}->string,           'verify mutator for {obj_row}';
+is ref $tableau1->objective_row, 'ARRAY',                                '->objective_row has type ARRAY';
+
+# the first 4 elements are Value::Real's and the remainder are perl scalars (numbers)
+# these are all mapped to array refs of scalars
+# should these use the validator( $compare_data ) pattern below?
+is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
+    [ map { ref $_ ? $_->{data} : $_ } $tableau1->{obj_row}->value ],
+    'access to {obj_row}';
+is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
+    [ map { ref $_ ? $_->{data} : $_ } $tableau1->obj_row->value ],
+    'objective_row is obj_row->value = ARRAY';
+
+is $tableau1->current_tableau, object { prop isa => 'Value::Matrix' }, '-> current_tableau is Value::Matrix';
+is $tableau1->current_tableau, Matrix($ra_matrix)->string, 'entire tableau including obj coeff row';
+
+is $tableau1->S, object { prop isa => 'Value::Matrix' }, 'slack variables are a Value::Matrix';
+is $tableau1->S, $tableau1->I($tableau1->m)->string, 'slack variables are identity matrix';
+
+sub stringify {
+	my $arrayref = shift;
+	warn "Not an array ref [$arrayref]" unless ref $arrayref eq 'ARRAY';
+	return sprintf("[%s]", 
+		join(',', map { my $s = $_; ref $s eq 'ARRAY' ? stringify($s) : $s } @{$arrayref})
+	);
+}
+
+my $aref = [ [qw/1 2/], 7, [3, 0.4], [ (5, -.6, [8, 9])], 0, -1, [qw/-2 -3/]];
+my $expected_string = '[[1,2],7,[3,0.4],[5,-0.6,[8,9]],0,-1,[-2,-3]]';
+is stringify($aref), $expected_string, 'Local stringify recursively descends the refs';
+
+# try out validator for mixed data types
+my $compare_data = sub {
+	my %params = @_;
+
+	# postfix dereferencing stable in perl 5.24
+	my ($g, $e) = map { ref $_ =~ /Value/ ? $_->copy : $_ } @{$params{got}};
+
+	my ($got, $exp);
+	$got = ref $g eq 'Value::Matrix' ? $g->string : stringify($g);
+	$exp = ref $e eq 'Value::Matrix' ? $e->string : stringify($e);
+
+	return is $got, $exp, 'Compare datastructures of MathObjects';
+};
+
+is [ $tableau1->obj_row, $tableau1->{obj_row} ],
+    validator( $compare_data ),
+    'verify mutator for {obj_row}';
+is [ $tableau1->objective_row, [$tableau1->obj_row->value] ],
+    validator( $compare_data ),
+    'objective_row is obj_row->value = ARRAY';
+
+subtest 'test basis' => sub {
+	is ref $tableau1->basis_columns, 'ARRAY',  '{basis_column} has type ARRAY';
+	is [$tableau1->basis_columns,  [ 5, 6 ]], validator( $compare_data ), 'initialization of basis';
+	is(
+		ref($tableau1->current_basis_matrix),
+		ref(Value::Matrix->I($tableau1->m)),
+		'current_basis_matrix type is MathObjectMatrix'
+	);
+	is $tableau1->current_basis_matrix->string,
+		Value::Matrix->I($tableau1->m)->string,
+		'initialization of basis';
+};
+
+
+subtest 'change basis and test again' => sub {
+	$tableau1->basis(2, 3);
+
+	is ref $tableau1->basis_columns, 'ARRAY',  '{basis_column} has type ARRAY';
+	is [$tableau1->basis_columns, [ 2, 3 ]], validator( $compare_data ), ' basis columns set to {2,3}';
+	is(
+		ref($tableau1->current_basis_matrix),
+		ref($test_constraint_matrix->column_slice(2, 3)),
+		'current_basis_matrix type is MathObjectMatrix'
+	);
+	is(
+		$tableau1->current_basis_matrix->string,
+		$test_constraint_matrix->column_slice(2, 3)->string,
+		'basis_matrix for columns {2,3} is correct'
+	);
+	is $tableau1->basis(Set(2, 3))->string,  List([ 2, 3 ])->string, '->basis(Set(2,3))';
+	is $tableau1->basis(List(2, 3))->string, List([ 2, 3 ])->string, '->basis(List(2,3))';
+	is $tableau1->basis([ 2, 3 ])->string,   List([ 2, 3 ])->string, '->basis([2,3])';
+};
+
+subtest 'find basis column index corresponding to row index' => sub {
+	# and value of the basis coefficient
+
+	$tableau1->basis(5, 6);
+	note("\nbasis is",                     $tableau1->basis(5, 6));
+	note(print $tableau1->current_tableau, "\n");
+	is [ $tableau1->find_leaving_column(1) ], [ 5, 1 ], 'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(2) ], [ 6, 1 ], 'find_leaving_column returns [col_index, pivot_value] ';
+
+	is $tableau1->find_next_basis_from_pivot(1, 2)->string, Set(2, 6)->string, 'find next basis from pivot (1,2)';
+	is $tableau1->find_next_basis_from_pivot(1, 3)->string, Set(3, 6)->string, 'find next basis from pivot (1,3)';
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string, 'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 6)->string, 'find next basis from pivot (1,1)';
+
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(2, 5)  },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (2,5)"
+	);    # probably shouldn't be doing this.
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(1, 6) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (2,6)"
+	);    # probably shouldn't be doing this.
+
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string, 'find next basis from pivot (2,1)';
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(2, 6) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (2,6)"
+	);    # probably shouldn't be doing this.
+};
+
+subtest 'find another basis (2,3)' => sub {
+	$tableau1->basis(2, 3);
+	note("\nbasis is",                     $tableau1->basis());
+	note(print $tableau1->current_tableau, "\n");
+
+	is [ $tableau1->find_leaving_column(1) ], [ 2, 500 ], 'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(2) ], [ 3, 500 ], 'find_leaving_column returns [col_index, pivot_value] ';
+
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(1, 2) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (1,2)"
+	);    # probably shouldn't be doing this either.
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(1, 3) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (1,3)"
+	);    # probably shouldn't be doing this.
+
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 2)->string, 'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 3)->string, 'find next basis from pivot (1,1)';
+};
+
+subtest 'find next short cut pivots' => sub {
+	$tableau1->basis(5, 6);
+	note("\nbasis is ",              $tableau1->basis());
+	note($tableau1->current_tableau, "\n");
+
+	# ($row_index, $value, $feasible_point) = $self->find_short_cut_row()
+
+	is [ $tableau1->find_short_cut_row() ],     [ 1, -4700, 0 ], "row 1";
+	is [ $tableau1->find_short_cut_column(1) ], [ 1, -5000, 0 ], "column 1 ";
+	is [ $tableau1->next_short_cut_pivot() ],   [ 1, 1, 0, 0 ],  "pivot (1,1)";
+	is [ $tableau1->next_short_cut_basis() ],   [ 1, 6, undef ], "new basis {1,6} continue";
+
+	$tableau1->current_tableau(1, 6);
+	note($tableau1->current_tableau);
+
+	is [ $tableau1->find_short_cut_row ],       [ 2, Value::Real->new(-8.4E+06)->string, 0 ], "find short cut row";
+	is [ $tableau1->find_short_cut_column(2) ], [ 2, Value::Real->new(-1.3E+06)->string, 0 ], "find short cut col 2 ";
+	is [ $tableau1->next_short_cut_pivot() ],   [ 2, 2, 0, 0 ],                       "pivot (2,2)";
+	is [ $tableau1->next_short_cut_basis() ],   [ 1, 2, undef ],                      "new basis {1,2} continue";
+
+	$tableau1->current_tableau(1, 2);
+	note($tableau1->current_tableau);
+
+	is [ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], "feasible point found";
+	is(
+		[ $tableau1->next_short_cut_basis() ],
+		[ 1, 2, 'feasible_point' ],
+		"all constraints positive at basis {1,2} --start phase2"
+	);
+	is [ $tableau1->find_pivot_column('max') ], [ 3, Value::Real->new(-100000)->string, 0 ],      "col 3";
+	is [ $tableau1->find_pivot_row(3) ],        [ 1, Value::Real->new(550000 / 500)->string, 0 ], "row 1 ";
+	is [ $tableau1->find_next_pivot('max') ],   [ 1, 3, 0, 0 ],                           "pivot (1,3)";
+	is [ $tableau1->find_next_basis('max') ],   [ 2, 3, undef ], "new basis {2,3} continue";
+
+	$tableau1->current_tableau(2, 3);
+	note($tableau1->current_tableau);
+	is [ $tableau1->find_pivot_column('max') ], [ 4, Value::Real->new(-300)->string, 0 ], "col 4";
+	is [ $tableau1->find_pivot_row(4) ], [ 1, 4500, 0 ], "row 2) ";
+
+	is [ $tableau1->find_next_pivot('max') ], [ 1, 4, 0, 0 ], "pivot 1,4";
+	is [ $tableau1->find_next_basis('max') ], [ 3, 4, undef ], "new basis {3,4} continue";
+
+	$tableau1->current_tableau(3, 4);
+	note($tableau1->current_tableau);
+	is [ $tableau1->find_pivot_column('max') ], [ 5, Value::Real->new(-1)->string, 0 ], "col 5";
+	is [ $tableau1->find_pivot_row(5) ], [ undef, undef, 1 ], "row 2) ";
+
+	is [ $tableau1->find_next_pivot('max') ], [ undef, 5, 0, 1 ], "unbounded -- no pivot";
+	is [ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], "basis 3,4 unbounded";
+};
 # note that the column is returned from find_next_pivot so one can find a certificate
 # of unboundedness (can return a line going off to infinity)
 
@@ -292,31 +392,34 @@ is_deeply([ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], "basis 3
 # # "unbounded, feasible_point, infeasible_tableau, optimal"?
 # # it might be easier to remember.
 #
-note("reset tableau to feasible point and try to minimize it for phase2");
-$tableau1->current_tableau(1, 2);
-note($tableau1->current_tableau);
-is_deeply([ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], "feasible point found");
-is_deeply(
-	[ $tableau1->next_short_cut_basis() ],
-	[ 1, 2, 'feasible_point' ],
-	"all constraints positive at basis {1,2} --start phase2"
-);
 
-is_deeply([ $tableau1->find_pivot_column('min') ], [ undef, undef, 1 ],                          "all neg coeff");
-is_deeply([ $tableau1->find_pivot_row(1) ],        [ 1, Value::Real->new(550000 / 1300000), 0 ], "row 1 ");
-is_deeply([ $tableau1->find_next_pivot('min') ],   [ undef, undef, 1, 0 ],                       "optimum");
-is_deeply([ $tableau1->find_next_basis('min') ],   [ 1, 2, 'optimum' ],                          "optimum");
-#
-#
+subtest 'reset tableau to feasible point and try to minimize it for phase2' => sub {
+	$tableau1->current_tableau(1, 2);
+	note($tableau1->current_tableau);
+	is [ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], 'feasible point found';
+	is(
+		[ $tableau1->next_short_cut_basis() ],
+		[ 1, 2, 'feasible_point' ],
+		'all constraints positive at basis {1,2} --start phase2'
+	);
 
-is_deeply(
-	$tableau1->statevars,    # round off errors
-	[ 550000 / 1300000, 8400000 / 1300000, 0, 0, 0, 0, 8.339999999999999E9 / 1300000 ], "state variables"
-);
+	is [ $tableau1->find_pivot_column('min') ], [ undef, undef, 1 ],                          'all neg coeff';
+	is [ $tableau1->find_pivot_row(1) ],        [ 1, Value::Real->new(550000 / 1300000)->string, 0 ], 'row 1 ';
+	is [ $tableau1->find_next_pivot('min') ],   [ undef, undef, 1, 0 ],                       'optimum';
+	is [ $tableau1->find_next_basis('min') ],   [ 1, 2, 'optimum' ],                          'optimum';
+
+	is(
+		$tableau1->statevars,    # round off errors
+		[ 550000 / 1300000, 8400000 / 1300000, 0, 0, 0, 0, 8.339999999999999E9 / 1300000 ],
+		'state variables'
+	);
+
+	is $tableau1->align, 'cccc|cc|c|c', 'check align';
+	is $tableau1->toplevel, [qw(x1 x2 x3 x4 x5 x6 z b)], 'check toplevel';
+
+	# diag($tableau1->align);
+	# diag(join(q{ } , @{$tableau1->toplevel}));
+};
 
-is($tableau1->align, 'cccc|cc|c|c', "check align");
-is_deeply($tableau1->toplevel, [qw(x1 x2 x3 x4 x5 x6 z b)], "check toplevel");
 
-# diag($tableau1->align);
-# diag(join(" " , @{$tableau1->toplevel}));
 done_testing();

From 3b612f300d40f4398276e06bfd553851fc20fe49 Mon Sep 17 00:00:00 2001
From: Boyd Duffee 
Date: Thu, 9 Jun 2022 20:50:46 +0100
Subject: [PATCH 33/38] Tidy tableau.t

Cleanup of code layout to make the file look half-decent
Needs another pair of eyes to make it sensible.
I'm just tired of it now.
---
 t/macros/tableau.t | 242 ++++++++++++++++++++++++++-------------------
 1 file changed, 139 insertions(+), 103 deletions(-)

diff --git a/t/macros/tableau.t b/t/macros/tableau.t
index 861294cf69..b09da818f4 100755
--- a/t/macros/tableau.t
+++ b/t/macros/tableau.t
@@ -34,7 +34,7 @@ a reliable test)
 =cut
 
 
-loadMacros("tableau.pl", "Value.pl");    #gives us Real() etc.
+loadMacros('tableau.pl', 'Value.pl');    #gives us Real() etc.
 
 my %context = ();
 
@@ -106,14 +106,13 @@ my $b = Value::Vector->new([ -$bill_profit, -$steve_profit ]);    # need vertica
 my $c = Value::Vector->new([ $money_total, $time_total, 1, 1 ]);
 
 my $tableau1 = Tableau->new(A => $a, b => $b, c => $c);
-###############################################################
-# Check mutators
-#
-#
+
 ###############################################################
 
-is $tableau1, D(), 'tableau has been defined and loaded';
-is $tableau1, object { prop isa => 'Tableau' }, 'has type Tableau';
+subtest 'Check mutators' => sub {
+	is $tableau1, D(), 'tableau has been defined and loaded';
+	is $tableau1, object { prop isa => 'Tableau' }, 'has type Tableau';
+};
 
 subtest 'test "close_enough_to_zero" subroutine' => sub {
 	is $tableau1->close_enough_to_zero(0),     1, 'checking_close_enough to zero';
@@ -129,12 +128,15 @@ subtest 'test "close_enough_to_zero" subroutine' => sub {
 	is $tableau1->close_enough_to_zero(-0.9999e-10), 1, 'checking_close_enough to zero for -0.9999e-10';
 };
 
+subtest 'Basic test warmup' => sub {
+	note("display stringified  \$tableau1: ", $tableau1, "\n");
+	is ref($tableau1), "Tableau", "checking data type is Tableau";
+	is $tableau1,
+		'[[-5000,-400,-1,0,1,0,0,-4700],[-3000,-500,0,-1,0,1,0,-4500]]',
+		'checking_stringification of tableau';
 
-note("display stringified  \$tableau1: ", $tableau1, "\n");
-is ref($tableau1), "Tableau", "checking data type is Tableau";
-ok $tableau1 eq "[[-5000,-400,-1,0,1,0,0,-4700],[-3000,-500,0,-1,0,1,0,-4500]]", "checking_stringification of tableau";
-
-is($tableau1->{m}, 2, 'number of constraints is 2');
+	is $tableau1->{m}, 2, 'number of constraints is 2';
+};
 
 subtest 'check data structure of tableau object' => sub {
 	is($tableau1->{m}, 2, 'number of constraints is 2');
@@ -167,56 +169,64 @@ subtest 'check data structure of tableau object' => sub {
 
 my $test_constraint_matrix = Matrix($ra_matrix->[0], $ra_matrix->[1]);
 
-is $tableau1->{current_constraint_matrix}->string,
-	$test_constraint_matrix->string, 'initialization of current_constraint_matrix';
-is
-	$tableau1->{current_constraint_matrix}->string,
-	$tableau1->current_constraint_matrix->string,
-	'current_constraint_matrix accessor';
-is $tableau1->{current_b}->string,               $tableau1->{b}->string,       'initialization of current_b';
-is $tableau1->{current_b}->string,               $tableau1->current_b->string, 'current_b accessor';
-
-is [ $tableau1->current_b->dimensions ], [ 2, 1 ],             'dimensions of current_b';
-
-my $obj_row_test = [ ((-$c)->value, 0, 0, 1, 0) ];
-for (my $i = 0; $i < 4; $i++) {
-	is $tableau1->objective_row->[$i]->string, $obj_row_test->[$i]->string,
-		'initialization of $tableau->{obj_row}';
-}
-is @{$tableau1->objective_row}[4..7], @{$obj_row_test}[4..7], 'initialization of $tableau->{obj_row}';
-
-is $tableau1->{obj_row},         object { prop isa => 'Value::Matrix' }, '->{obj_row} has type Value::Matrix';
-is $tableau1->obj_row,           object { prop isa => 'Value::Matrix' }, '->obj_row has type Value::Matrix';
-is $tableau1->obj_row->string,   $tableau1->{obj_row}->string,           'verify mutator for {obj_row}';
-is ref $tableau1->objective_row, 'ARRAY',                                '->objective_row has type ARRAY';
-
-# the first 4 elements are Value::Real's and the remainder are perl scalars (numbers)
-# these are all mapped to array refs of scalars
-# should these use the validator( $compare_data ) pattern below?
-is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
-    [ map { ref $_ ? $_->{data} : $_ } $tableau1->{obj_row}->value ],
-    'access to {obj_row}';
-is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
-    [ map { ref $_ ? $_->{data} : $_ } $tableau1->obj_row->value ],
-    'objective_row is obj_row->value = ARRAY';
+subtest 'Current constraint matrix' => sub {
+	is $tableau1->{current_constraint_matrix}->string,
+		$test_constraint_matrix->string,
+		'initialization of current_constraint_matrix';
+	is
+		$tableau1->{current_constraint_matrix}->string,
+		$tableau1->current_constraint_matrix->string,
+		'current_constraint_matrix accessor';
+	is $tableau1->{current_b}->string, $tableau1->{b}->string,       'initialization of current_b';
+	is $tableau1->{current_b}->string, $tableau1->current_b->string, 'current_b accessor';
+
+	is [ $tableau1->current_b->dimensions ], [ 2, 1 ],               'dimensions of current_b';
+};
 
-is $tableau1->current_tableau, object { prop isa => 'Value::Matrix' }, '-> current_tableau is Value::Matrix';
-is $tableau1->current_tableau, Matrix($ra_matrix)->string, 'entire tableau including obj coeff row';
+subtest 'Objective row properties' => sub {
+	my $obj_row_test = [ ((-$c)->value, 0, 0, 1, 0) ];
+
+	for (my $i = 0; $i < 4; $i++) {
+		is $tableau1->objective_row->[$i]->string, $obj_row_test->[$i]->string,
+			'initialization of $tableau->{obj_row} (first half)';
+	}
+	is @{$tableau1->objective_row}[4..7],
+		@{$obj_row_test}[4..7],
+		'initialization of $tableau->{obj_row} (second half)';
+
+	is $tableau1->{obj_row}, object { prop isa => 'Value::Matrix' }, '->{obj_row} has type Value::Matrix';
+	is $tableau1->obj_row,   object { prop isa => 'Value::Matrix' }, '->obj_row has type Value::Matrix';
+	is $tableau1->obj_row->string,   $tableau1->{obj_row}->string,   'verify mutator for {obj_row}';
+	is ref $tableau1->objective_row, 'ARRAY',                        '->objective_row has type ARRAY';
+
+    # the first 4 elements are Value::Real's and the remainder are perl scalars (numbers)
+    # these are all mapped to array refs of scalars
+    # should these use the validator( $compare_data ) pattern below?
+    is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
+        [ map { ref $_ ? $_->{data} : $_ } $tableau1->{obj_row}->value ],
+        'access to {obj_row}';
+    is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
+        [ map { ref $_ ? $_->{data} : $_ } $tableau1->obj_row->value ],
+        'objective_row is obj_row->value = ARRAY';
+};
 
-is $tableau1->S, object { prop isa => 'Value::Matrix' }, 'slack variables are a Value::Matrix';
-is $tableau1->S, $tableau1->I($tableau1->m)->string, 'slack variables are identity matrix';
+subtest 'Current tableau' => sub {
+	is $tableau1->current_tableau,
+		object { prop isa => 'Value::Matrix' },
+		'-> current_tableau is Value::Matrix';
+	is $tableau1->current_tableau,
+		Matrix($ra_matrix)->string,
+		'entire tableau including obj coeff row';
 
-sub stringify {
-	my $arrayref = shift;
-	warn "Not an array ref [$arrayref]" unless ref $arrayref eq 'ARRAY';
-	return sprintf("[%s]", 
-		join(',', map { my $s = $_; ref $s eq 'ARRAY' ? stringify($s) : $s } @{$arrayref})
-	);
-}
+	is $tableau1->S, object { prop isa => 'Value::Matrix' }, 'slack variables are a Value::Matrix';
+	is $tableau1->S, $tableau1->I($tableau1->m)->string,     'slack variables are identity matrix';
+};
 
-my $aref = [ [qw/1 2/], 7, [3, 0.4], [ (5, -.6, [8, 9])], 0, -1, [qw/-2 -3/]];
-my $expected_string = '[[1,2],7,[3,0.4],[5,-0.6,[8,9]],0,-1,[-2,-3]]';
-is stringify($aref), $expected_string, 'Local stringify recursively descends the refs';
+subtest 'Verify stringify subroutine' => sub {
+	my $aref = [ [qw/1 2/], 7, [3, 0.4], [ (5, -.6, [8, 9])], 0, -1, [qw/-2 -3/]];
+	my $expected_string = '[[1,2],7,[3,0.4],[5,-0.6,[8,9]],0,-1,[-2,-3]]';
+	is stringify($aref), $expected_string, 'Local stringify recursively descends the refs';
+};
 
 # try out validator for mixed data types
 my $compare_data = sub {
@@ -232,12 +242,15 @@ my $compare_data = sub {
 	return is $got, $exp, 'Compare datastructures of MathObjects';
 };
 
-is [ $tableau1->obj_row, $tableau1->{obj_row} ],
-    validator( $compare_data ),
-    'verify mutator for {obj_row}';
-is [ $tableau1->objective_row, [$tableau1->obj_row->value] ],
-    validator( $compare_data ),
-    'objective_row is obj_row->value = ARRAY';
+subtest 'Verify objective_row methods and properties' => sub {
+	is [ $tableau1->obj_row, $tableau1->{obj_row} ],
+		validator( $compare_data ),
+		'verify mutator for {obj_row}';
+
+	is [ $tableau1->objective_row, [$tableau1->obj_row->value] ],
+		validator( $compare_data ),
+		'objective_row is obj_row->value = ARRAY';
+};
 
 subtest 'test basis' => sub {
 	is ref $tableau1->basis_columns, 'ARRAY',  '{basis_column} has type ARRAY';
@@ -279,13 +292,19 @@ subtest 'find basis column index corresponding to row index' => sub {
 	$tableau1->basis(5, 6);
 	note("\nbasis is",                     $tableau1->basis(5, 6));
 	note(print $tableau1->current_tableau, "\n");
-	is [ $tableau1->find_leaving_column(1) ], [ 5, 1 ], 'find_leaving_column returns [col_index, pivot_value] ';
-	is [ $tableau1->find_leaving_column(2) ], [ 6, 1 ], 'find_leaving_column returns [col_index, pivot_value] ';
-
-	is $tableau1->find_next_basis_from_pivot(1, 2)->string, Set(2, 6)->string, 'find next basis from pivot (1,2)';
-	is $tableau1->find_next_basis_from_pivot(1, 3)->string, Set(3, 6)->string, 'find next basis from pivot (1,3)';
-	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string, 'find next basis from pivot (2,1)';
-	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 6)->string, 'find next basis from pivot (1,1)';
+	is [ $tableau1->find_leaving_column(1) ], [ 5, 1 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(2) ], [ 6, 1 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+
+	is $tableau1->find_next_basis_from_pivot(1, 2)->string, Set(2, 6)->string,
+		'find next basis from pivot (1,2)';
+	is $tableau1->find_next_basis_from_pivot(1, 3)->string, Set(3, 6)->string,
+		'find next basis from pivot (1,3)';
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string,
+		'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 6)->string,
+		'find next basis from pivot (1,1)';
 
 	like(
 		dies { $tableau1->find_next_basis_from_pivot(2, 5)  },
@@ -298,7 +317,8 @@ subtest 'find basis column index corresponding to row index' => sub {
 		"can't pivot in basis column (2,6)"
 	);    # probably shouldn't be doing this.
 
-	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string, 'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string,
+		'find next basis from pivot (2,1)';
 	like(
 		dies { $tableau1->find_next_basis_from_pivot(2, 6) },
 		qr/pivot point should not be in a basis column/,
@@ -311,8 +331,10 @@ subtest 'find another basis (2,3)' => sub {
 	note("\nbasis is",                     $tableau1->basis());
 	note(print $tableau1->current_tableau, "\n");
 
-	is [ $tableau1->find_leaving_column(1) ], [ 2, 500 ], 'find_leaving_column returns [col_index, pivot_value] ';
-	is [ $tableau1->find_leaving_column(2) ], [ 3, 500 ], 'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(1) ], [ 2, 500 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(2) ], [ 3, 500 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
 
 	like(
 		dies { $tableau1->find_next_basis_from_pivot(1, 2) },
@@ -325,8 +347,10 @@ subtest 'find another basis (2,3)' => sub {
 		"can't pivot in basis column (1,3)"
 	);    # probably shouldn't be doing this.
 
-	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 2)->string, 'find next basis from pivot (2,1)';
-	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 3)->string, 'find next basis from pivot (1,1)';
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 2)->string,
+		'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 3)->string,
+		'find next basis from pivot (1,1)';
 };
 
 subtest 'find next short cut pivots' => sub {
@@ -336,48 +360,50 @@ subtest 'find next short cut pivots' => sub {
 
 	# ($row_index, $value, $feasible_point) = $self->find_short_cut_row()
 
-	is [ $tableau1->find_short_cut_row() ],     [ 1, -4700, 0 ], "row 1";
-	is [ $tableau1->find_short_cut_column(1) ], [ 1, -5000, 0 ], "column 1 ";
-	is [ $tableau1->next_short_cut_pivot() ],   [ 1, 1, 0, 0 ],  "pivot (1,1)";
-	is [ $tableau1->next_short_cut_basis() ],   [ 1, 6, undef ], "new basis {1,6} continue";
+	is [ $tableau1->find_short_cut_row() ],     [ 1, -4700, 0 ], 'row 1';
+	is [ $tableau1->find_short_cut_column(1) ], [ 1, -5000, 0 ], 'column 1 ';
+	is [ $tableau1->next_short_cut_pivot() ],   [ 1, 1, 0, 0 ],  'pivot (1,1)';
+	is [ $tableau1->next_short_cut_basis() ],   [ 1, 6, undef ], 'new basis {1,6} continue';
 
 	$tableau1->current_tableau(1, 6);
 	note($tableau1->current_tableau);
 
-	is [ $tableau1->find_short_cut_row ],       [ 2, Value::Real->new(-8.4E+06)->string, 0 ], "find short cut row";
-	is [ $tableau1->find_short_cut_column(2) ], [ 2, Value::Real->new(-1.3E+06)->string, 0 ], "find short cut col 2 ";
-	is [ $tableau1->next_short_cut_pivot() ],   [ 2, 2, 0, 0 ],                       "pivot (2,2)";
-	is [ $tableau1->next_short_cut_basis() ],   [ 1, 2, undef ],                      "new basis {1,2} continue";
+	is [ $tableau1->find_short_cut_row ],
+		[ 2, Value::Real->new(-8.4E+06)->string, 0 ], 'find short cut row';
+	is [ $tableau1->find_short_cut_column(2) ],
+		[ 2, Value::Real->new(-1.3E+06)->string, 0 ], 'find short cut col 2 ';
+	is [ $tableau1->next_short_cut_pivot() ], [ 2, 2, 0, 0 ],  'pivot (2,2)';
+	is [ $tableau1->next_short_cut_basis() ], [ 1, 2, undef ], 'new basis {1,2} continue';
 
 	$tableau1->current_tableau(1, 2);
 	note($tableau1->current_tableau);
 
-	is [ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], "feasible point found";
+	is [ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], 'feasible point found';
 	is(
 		[ $tableau1->next_short_cut_basis() ],
 		[ 1, 2, 'feasible_point' ],
-		"all constraints positive at basis {1,2} --start phase2"
+		'all constraints positive at basis {1,2} --start phase2'
 	);
-	is [ $tableau1->find_pivot_column('max') ], [ 3, Value::Real->new(-100000)->string, 0 ],      "col 3";
-	is [ $tableau1->find_pivot_row(3) ],        [ 1, Value::Real->new(550000 / 500)->string, 0 ], "row 1 ";
-	is [ $tableau1->find_next_pivot('max') ],   [ 1, 3, 0, 0 ],                           "pivot (1,3)";
-	is [ $tableau1->find_next_basis('max') ],   [ 2, 3, undef ], "new basis {2,3} continue";
+	is [ $tableau1->find_pivot_column('max') ], [ 3, Value::Real->new(-100000)->string, 0 ],      'col 3';
+	is [ $tableau1->find_pivot_row(3) ],        [ 1, Value::Real->new(550000 / 500)->string, 0 ], 'row 1';
+	is [ $tableau1->find_next_pivot('max') ],   [ 1, 3, 0, 0 ],  'pivot (1,3)';
+	is [ $tableau1->find_next_basis('max') ],   [ 2, 3, undef ], 'new basis {2,3} continue';
 
 	$tableau1->current_tableau(2, 3);
 	note($tableau1->current_tableau);
-	is [ $tableau1->find_pivot_column('max') ], [ 4, Value::Real->new(-300)->string, 0 ], "col 4";
-	is [ $tableau1->find_pivot_row(4) ], [ 1, 4500, 0 ], "row 2) ";
+	is [ $tableau1->find_pivot_column('max') ], [ 4, Value::Real->new(-300)->string, 0 ], 'col 4';
+	is [ $tableau1->find_pivot_row(4) ], [ 1, 4500, 0 ], 'row 2';
 
-	is [ $tableau1->find_next_pivot('max') ], [ 1, 4, 0, 0 ], "pivot 1,4";
-	is [ $tableau1->find_next_basis('max') ], [ 3, 4, undef ], "new basis {3,4} continue";
+	is [ $tableau1->find_next_pivot('max') ], [ 1, 4, 0, 0 ], 'pivot 1,4';
+	is [ $tableau1->find_next_basis('max') ], [ 3, 4, undef ], 'new basis {3,4} continue';
 
 	$tableau1->current_tableau(3, 4);
 	note($tableau1->current_tableau);
-	is [ $tableau1->find_pivot_column('max') ], [ 5, Value::Real->new(-1)->string, 0 ], "col 5";
-	is [ $tableau1->find_pivot_row(5) ], [ undef, undef, 1 ], "row 2) ";
+	is [ $tableau1->find_pivot_column('max') ], [ 5, Value::Real->new(-1)->string, 0 ], 'col 5';
+	is [ $tableau1->find_pivot_row(5) ], [ undef, undef, 1 ], 'row 2';
 
-	is [ $tableau1->find_next_pivot('max') ], [ undef, 5, 0, 1 ], "unbounded -- no pivot";
-	is [ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], "basis 3,4 unbounded";
+	is [ $tableau1->find_next_pivot('max') ], [ undef, 5, 0, 1 ],    'unbounded -- no pivot';
+	is [ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], 'basis 3,4 unbounded';
 };
 # note that the column is returned from find_next_pivot so one can find a certificate
 # of unboundedness (can return a line going off to infinity)
@@ -403,10 +429,12 @@ subtest 'reset tableau to feasible point and try to minimize it for phase2' => s
 		'all constraints positive at basis {1,2} --start phase2'
 	);
 
-	is [ $tableau1->find_pivot_column('min') ], [ undef, undef, 1 ],                          'all neg coeff';
-	is [ $tableau1->find_pivot_row(1) ],        [ 1, Value::Real->new(550000 / 1300000)->string, 0 ], 'row 1 ';
-	is [ $tableau1->find_next_pivot('min') ],   [ undef, undef, 1, 0 ],                       'optimum';
-	is [ $tableau1->find_next_basis('min') ],   [ 1, 2, 'optimum' ],                          'optimum';
+	is [ $tableau1->find_pivot_column('min') ], [ undef, undef, 1 ], 'all neg coeff';
+	is [ $tableau1->find_pivot_row(1) ],
+		[ 1, Value::Real->new(550000 / 1300000)->string, 0 ],
+		'row 1';
+	is [ $tableau1->find_next_pivot('min') ], [ undef, undef, 1, 0 ], 'optimum';
+	is [ $tableau1->find_next_basis('min') ], [ 1, 2, 'optimum' ],    'optimum';
 
 	is(
 		$tableau1->statevars,    # round off errors
@@ -414,7 +442,7 @@ subtest 'reset tableau to feasible point and try to minimize it for phase2' => s
 		'state variables'
 	);
 
-	is $tableau1->align, 'cccc|cc|c|c', 'check align';
+	is $tableau1->align,    'cccc|cc|c|c',               'check align';
 	is $tableau1->toplevel, [qw(x1 x2 x3 x4 x5 x6 z b)], 'check toplevel';
 
 	# diag($tableau1->align);
@@ -423,3 +451,11 @@ subtest 'reset tableau to feasible point and try to minimize it for phase2' => s
 
 
 done_testing();
+
+sub stringify {
+	my $arrayref = shift;
+	warn "Not an array ref [$arrayref]" unless ref $arrayref eq 'ARRAY';
+	return sprintf("[%s]",
+		join(',', map { my $s = $_; ref $s eq 'ARRAY' ? stringify($s) : $s } @{$arrayref})
+	);
+}

From bbc272451d540e6bc1c0769e3e432bd41cf97ea6 Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Mon, 13 Jun 2022 11:40:32 -0500
Subject: [PATCH 34/38] Fix the prevention of creating degenerate objects with
 keyboard controls.

When graphing a parabola (via any of the tools that do so) or a cubic it is possible to create degenerate objects.  The code that is supposed to prevent that wasn't quite working right.  The jsxgraph board object needs to be updated after setting the position of a point programatically.

To test this graph any of those objects and then move one of the defining points of the object around with the keyboard.  For example, graph a three point quadratic and try to move one point onto the same vertical line as another point.  That should not be possible.
---
 htdocs/js/apps/GraphTool/cubictool.js     | 1 +
 htdocs/js/apps/GraphTool/graphtool.js     | 1 +
 htdocs/js/apps/GraphTool/quadratictool.js | 1 +
 3 files changed, 3 insertions(+)

diff --git a/htdocs/js/apps/GraphTool/cubictool.js b/htdocs/js/apps/GraphTool/cubictool.js
index a07f026580..b059f9724a 100644
--- a/htdocs/js/apps/GraphTool/cubictool.js
+++ b/htdocs/js/apps/GraphTool/cubictool.js
@@ -38,6 +38,7 @@
 					}
 
 					el.setPosition(JXG.COORDS_BY_USER, [x, el.Y()]);
+					gt.board.update();
 				}
 			},
 
diff --git a/htdocs/js/apps/GraphTool/graphtool.js b/htdocs/js/apps/GraphTool/graphtool.js
index e76d02063a..4bd6f88b62 100644
--- a/htdocs/js/apps/GraphTool/graphtool.js
+++ b/htdocs/js/apps/GraphTool/graphtool.js
@@ -452,6 +452,7 @@ window.graphTool = (containerId, options) => {
 		else if (y > boundingBox[1]) y = boundingBox[1] - gt.snapSizeY;
 
 		point1.setPosition(JXG.COORDS_BY_USER, [x, y]);
+		gt.board.update();
 	};
 
 	// Prevent paired points from being moved into the same position by a drag.  This
diff --git a/htdocs/js/apps/GraphTool/quadratictool.js b/htdocs/js/apps/GraphTool/quadratictool.js
index 8b7530dc50..a487077b6e 100644
--- a/htdocs/js/apps/GraphTool/quadratictool.js
+++ b/htdocs/js/apps/GraphTool/quadratictool.js
@@ -38,6 +38,7 @@
 					}
 
 					el.setPosition(JXG.COORDS_BY_USER, [x, el.Y()]);
+					gt.board.update();
 				}
 			},
 

From 5a7ae5f9c6b7756d699340dee674c9548225420e Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Thu, 7 Jul 2022 07:24:40 -0500
Subject: [PATCH 35/38] Tweak the styles for lables, text inputs, selects, and
 textareas in problems.

This removes the font size and from these elements.  It also removes the
font-family that was set on selects.  The font family still applies to
textreas and text inputs.
---
 htdocs/js/apps/Problem/problem.scss | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/htdocs/js/apps/Problem/problem.scss b/htdocs/js/apps/Problem/problem.scss
index d85a39204e..1073c66f15 100644
--- a/htdocs/js/apps/Problem/problem.scss
+++ b/htdocs/js/apps/Problem/problem.scss
@@ -34,7 +34,6 @@
 
 	/* Problem elements */
 	label, input[type=text], select, textarea {
-		font-size: 16px;
 		font-weight: normal;
 		line-height: 18px;
 		width: auto;
@@ -48,10 +47,13 @@
 		vertical-align: middle;
 		border: 1px solid #ccc;
 		border-radius: 4px;
-		font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
 		background-color: white;
 	}
 
+	textarea, input[type=text] {
+		font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+	}
+
 	input[type=text] {
 		height: 30px;
 		font-size: 14px;

From f28a5484a0cf4cda69950ae32b76a2a58bcc5835 Mon Sep 17 00:00:00 2001
From: Jaimos Skriletz 
Date: Thu, 26 May 2022 09:01:08 -0600
Subject: [PATCH 36/38] Only use \textstyle with mixed numbers.

  Only use \textstyle formating option in a fraction's TeX output for
  mixed numbers. For other cases don't include this to allow the size
  of the fraction to be formatted according to \displaystyle.
---
 macros/contextFraction.pl | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl
index 2a5afdc1de..b85cfcbed3 100644
--- a/macros/contextFraction.pl
+++ b/macros/contextFraction.pl
@@ -907,11 +907,16 @@ sub string {
 sub TeX {
   my $self = shift; my $equation = shift; shift; shift; my $prec = shift;
   my ($a,$b) = @{$self->{data}}; my $n = "";
+  my $textstyle = '';
   return "$a" if $b == 1;
-  if ($self->getFlagWithAlias("showMixedNumbers","showProperFractions") && CORE::abs($a) > $b)
-    {$n = int($a/$b); $a = CORE::abs($a) % $b; $n .= " " unless $a == 0}
+  if ($self->getFlagWithAlias("showMixedNumbers","showProperFractions") && CORE::abs($a) > $b) {
+    $n = int($a/$b);
+    $a = CORE::abs($a) % $b;
+    $n .= ' ' unless $a == 0;
+    $textstyle = '\\textstyle';
+  }
   my $s = ""; ($a,$s) = (-$a,"-") if $a < 0;
-  $n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{\\textstyle\\frac{$a}{$b}}")
+  $n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{$textstyle\\frac{$a}{$b}}")
     unless $a == 0 && $n ne '';
   return "$n";
 }

From 352d494527b268c0a8d1040decc60d8a230b0368 Mon Sep 17 00:00:00 2001
From: Alex Jordan 
Date: Fri, 8 Jul 2022 22:34:29 -0700
Subject: [PATCH 37/38] For PTX output, don't include hidden MathQuill inputs

---
 macros/PG.pl | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/macros/PG.pl b/macros/PG.pl
index b3d81779ba..c3d5f33a8d 100644
--- a/macros/PG.pl
+++ b/macros/PG.pl
@@ -475,8 +475,11 @@ sub ENDDOCUMENT {
 				my $data_mq_opts = scalar(keys %$mq_part_opts)
 					? qq!data-mq-opts="@{[encode_pg_and_html(JSON->new->encode($mq_part_opts))]}"!
 					: "";
-				TEXT(MODES(TeX => "",
-						HTML => qq!!));
+				TEXT(MODES(
+					TeX => "",
+					PTX => "",
+					HTML => qq!!
+				));
 			}
 		}
 	}

From e7f73e1fe52facfabf8eda990b80ea5b0076aef4 Mon Sep 17 00:00:00 2001
From: Glenn Rice 
Date: Wed, 13 Jul 2022 07:37:17 -0500
Subject: [PATCH 38/38] Make knowls wait until the page css has loaded before
 initializing.

This fixes issue https://github.com/openwebwork/webwork2/issues/1739.

In the PG problem editor, the page in the iframe is not displayed until
after the page loads.  As such the javascript getComputedStyle method
fails.  The same things happens with the graphtool.  The same solution
is now implemented for knowls.
---
 htdocs/js/apps/Knowls/knowl.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/htdocs/js/apps/Knowls/knowl.js b/htdocs/js/apps/Knowls/knowl.js
index bdfcbac8c4..57b8388a40 100644
--- a/htdocs/js/apps/Knowls/knowl.js
+++ b/htdocs/js/apps/Knowls/knowl.js
@@ -15,6 +15,11 @@
 	};
 
 	const initializeKnowl = (knowl) => {
+		if (getComputedStyle(knowl)?.display === '') {
+			setTimeout(() => initializeKnowl(knowl), 100);
+			return;
+		}
+
 		knowl.dataset.bsToggle = 'collapse';
 		if (!knowl.knowlContainer) {
 			knowl.knowlContainer = document.createElement('div');