WeBWorK Problems

Regex works in problem, but not in macro

Regex works in problem, but not in macro

by Nathan Warkentin -
Number of replies: 2

I author WeBWorK problems internally for my high school's Precalculus and Calculus curricula. I am currently putting some time into creating a library of macros (in /opt/webwork/pg/macros) to ease authoring of common question types. One such class of problems is review on factoring (not just polynomials), and the library of factoring techniques on the wiki is awkward; I have no interest in creating a separate answer blank for each factor.

Instead, I have created a subroutine which isolates all parenthesized expressions. I have several answer checkers which use this subroutine. It works fine when I include its definition in problem code, but it ceases to work when in a macro. After a bit of poking around, I realized that the following regex is creating the issue:

/~~((?:[^)(]+|(?R))*+~~)/g

When included in the macro, the subroutine always returns an empty array because this regex matches nothing. My guess is that the recursion element of the Regex is not supported in a macro. My question is this: is it possible to correct this and allow the execution of this regex in a macro file? Or am I doomed to having to copy-paste this code into every problem file that needs it?

----------------------------------------

Here is an MWE if it helps. (After pasting it, I realized that I could make parentheticals() slightly more efficient, so please avoid commenting on that element - I'll deal with it later once this is sorted.) The goal is to put parentheticals() and $factorChecker in the macro.

DOCUMENT();
loadMacros(
  "PGstandard.pl",
  "PGML.pl",
  "MathObjects.pl",
  "PGcourse.pl");
TEXT(beginproblem());

sub parentheticals {
# Purpose: A subroutine to return an array with all parenthetical expressions of all levels of nesting. It seems regex isn't up to this task by itself.
# Requires: A text string which contains a WeBWorK formula (function)
# Returns: An array containing every expression contained by matched parentheses, with nesting
my ($formula) = @_;
my @return = @temp_old = @temp_new = @temp_parens = ();

my @parens = $formula =~ /~~((?:[^)(]+|(?R))*+~~)/g;
# This regex keeps parentheses in first and final positions, so remove them
unless( scalar @parens > 0 ){ return (); }
foreach(@parens){ push(@temp_new, substr $_, 1, -1); }
push(@return, @temp_new);
@temp_old = @temp_new;
@temp_new = ();

# Now recurse through the entries in @temp, collecting parenthetical expressions
until(scalar @parens == 0){
@parens = ();
foreach(@temp_old){
@temp_parens = $_ =~ /~~((?:[^)(]+|(?R))*+~~)/g;
push(@parens, @temp_parens);
foreach(@parens){ push(@temp_new, substr $_, 1, -1); }
}
push(@return, @temp_new);
@temp_old = @temp_new;
@temp_new = ();
}

return @return;
}

$factorChecker = sub {
# Purpose: Custom answer checker to make sure that a response matches a given function, as well as containing a set of specific parenthecized factors.
# Requires: parentheticals(), a list of parenthetical factors to be passed as an arg
# Usage: $ans = Formula("")->cmp(bypass_equivalence_test => 1, checker => $factorChecker, factors => \@factors);
# Note: Factors without addition/subtraction should NOT be included in the list of factors, as they are not necessarily parenthesized
my ($correct,$student,$ansHash) = @_;
return 0 if $ansHash->{isPreview};
my $cor = Formula($correct);
my $ans = Formula($student);
my $fac = $ansHash->{factors};
my @factors = @{$$fac};
my $text = $ansHash->{original_student_ans};

unless($ans == $cor){ return 0; }

# Check whether factors are present
my @parens = parentheticals($text);
foreach $factor (@factors){
$factor_present = 0;

foreach(@parens){
if(Formula($_) == Formula($factor) ||
Formula($_) == -1*Formula($factor) )
{ $factor_present = 1; } }

unless($factor_present == 1){
Value->Error("Your expression is equivalent to the original function, but you have not factored fully.");
return 0; }
}

return 1;
};

Context("Numeric");
@factors = ("x-1","x+1");
$answer = Formula("(x-1)*(x+1)")->cmp(bypass_equivalence_test => 1, checker => $factorChecker, factors => \@factors);

######################################################################
BEGIN_PGML
Factor this expression: [`x^2-1=`][____________________]{$answer}
END_PGML
######################################################################

ENDDOCUMENT();
In reply to Nathan Warkentin

Re: Regex works in problem, but not in macro

by Alex Jordan -

In a .pg file, the first stage of processing replaced each \ with \\.  (In part because later, the escaped backslash will become a single one.) Occasionally you actually want a single backslash at the stage where escapings are processed. So the double tilde was chosen for that. At the same time \ becomes \\, also ~~ becomes \.

None of this happens to code in a macro file. So you must adjust your use of backslashes and tildes accordingly.

In reply to Alex Jordan

Re: Regex works in problem, but not in macro

by Nathan Warkentin -
Thank you for the efficient and quick reply, Alex! After making these changes, everything works as expected. I was also able to fix several other regular expressions before errors arose.

For anyone in a similar position reading this post, the corrected regex just uses regular Perl regex syntax:

/\((?:[^)(]+|(?R))*+\)/g

So anytime you want to put a regex in a macro, simply revert to using backlashes as you normally would.