Skip to content

Commit

Permalink
Merge pull request #1147 from drgrice1/log-base-b-function
Browse files Browse the repository at this point in the history
Add a `parserLogb.pl` macro for logarithms with base b.
  • Loading branch information
somiaj authored Jan 14, 2025
2 parents 352b286 + 241bcb1 commit e159d43
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 3 deletions.
3 changes: 3 additions & 0 deletions htdocs/js/MathQuill/mqeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@
{ id: 'sqrt', latex: '\\sqrt', tooltip: 'square root (sqrt)', icon: '\\sqrt{\\text{ }}' },
{ id: 'nthroot', latex: '\\root', tooltip: 'nth root (root)', icon: '\\sqrt[\\text{ }]{\\text{ }}' },
{ id: 'exponent', latex: '^', tooltip: 'exponent (^)', icon: '\\text{ }^\\text{ }' },
...(cfgOptions.logsChangeBase
? []
: [{ id: 'subscript', latex: '_', tooltip: 'subscript (_)', icon: '\\text{ }_\\text{ }' }]),
{ id: 'infty', latex: '\\infty', tooltip: 'infinity (inf)', icon: '\\infty' },
{ id: 'pi', latex: '\\pi', tooltip: 'pi (pi)', icon: '\\pi' },
{ id: 'vert', latex: '\\vert', tooltip: 'such that (vert)', icon: '|' },
Expand Down
1 change: 1 addition & 0 deletions lib/Parser/Context/Default.pm
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ $flags = {
parseAlternatives => 0, # 1 = allow parsing of alternative tokens in the context
convertFullWidthCharacters => 0, # 1 = convert Unicode full width characters to ASCII positions
useMathQuill => 0,
mathQuillOpts => {},
};

############################################################################
Expand Down
8 changes: 5 additions & 3 deletions macros/PG.pl
Original file line number Diff line number Diff line change
Expand Up @@ -979,9 +979,11 @@ sub ENDDOCUMENT {
my $mq_part_opts = $ansHash->{mathQuillOpts} // $mq_opts;
next if $mq_part_opts =~ /^\s*disabled\s*$/i;

my $context = $ansHash->{correct_value}->context if $ansHash->{correct_value};
$mq_part_opts->{rootsAreExponents} = 0
if $context && $context->functions->get('root') && !defined $mq_part_opts->{rootsAreExponents};
if ($ansHash->{correct_value}) {
for (keys %{ $ansHash->{correct_value}->context->flag('mathQuillOpts') }) {
$mq_part_opts->{$_} = 0 unless defined $mq_part_opts->{$_};
}
}

my $name = "MaThQuIlL_$response";
RECORD_EXTRA_ANSWERS($name);
Expand Down
213 changes: 213 additions & 0 deletions macros/parsers/parserLogb.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
################################################################################
# WeBWorK Online Homework Delivery System
# Copyright © 2000-2020 The WeBWorK Project, http://openwebwork.sf.net/
#
# 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
parserLogb.pl - defines a C<logb(b, x)> function for the logarithm with base b
evaluated at x.
=head1 DESCRIPTION
This file defines the code necessary to add to any context a C<logb(b, x)>
function that evaluates the logarithm with base b at x. For example,
C<Compute("logb(3, 5)")> would return the equivalent of
C<Compute("log(5)/log(3)"> although it will be displayed as a logarithm with
base b.
To accomplish this, put the line
loadMacros("parserLogb.pl");
at the beginning of your problem file, then set the Context to the one you wish
to use in the problem. Then use the command:
Parser::Logb->Enable;
(You can also pass the Enable command a pointer to a context if you wish to
alter a context other than the current one.)
Once that is done, you (and students) can enter logarithms with base b by using
the C<logb()> function. You can use C<logb()> both within C<Formula()> and
C<Compute()> calls, and in Perl expressions, such as
$ans = Compute("logb(3, 5)";
$n = logb(3, 5);
to obtain the logarithm with base b. Note that by default C<logb()> will
produce an error message for logarithms evaluated at zero or negative numbers or
if the base is zero or negative.
However, if you enable C<logb()> in a context that allows complex numbers, you
may want to allow logarithms of negative numbers or with negative bases. To do
this, use
Parser::Logb->EnableComplex;
(again, you can pass a context to be altered, if you wish). This will force
logarithms of negative values or with negative bases to be promoted to complex
numbers. So
Parser::Logb->EnableComplex;
$z = logb(3, -9);
$y = logb(-3, 9);
would produce the equivalent of C<$z = Compute("log(-9)/log(3)");> and
C<$y = Compute("log(9)/log(-3)");> except that they will be displayed as
logarithms with base 3 or -3 respectively.
Note that if MathQuill is enabled, then students will be able to enter the
logarithm with base C<b> evaluated at C<x> by typing C<log_b(x)>. To facilitate
students entering such answers, a subscript button is present in the MathQuill
toolbar for answers with the C<logb> function enabled.
=cut

BEGIN { strict->import }

loadMacros('MathObjects.pl');

sub _parserLogb_init { }

sub logb { Parser::Function->call('logb', @_); }

package Parser::Logb;

sub Enable {
my ($self, $context, $complex) = @_;
$context = main::Context() unless Value::isContext($context);
$context->functions->add(logb => { class => 'Parser::Logb::Function::numeric2' });
$context->functions->set(logb => { negativeIsComplex => 1 }) if $complex;
$context->flag('mathQuillOpts')->{logsChangeBase} = 0;
return;
}

sub EnableComplex {
my ($self, $context) = @_;
$self->Enable($context, 1);
return;
}

package Parser::Logb::Function::numeric2;
our @ISA = qw(Parser::Function);

# Check for numeric arguments
sub _check {
my $self = shift;
my $context = $self->context;
return if ($self->checkArgCount(2));
$self->{type} = $Value::Type{number};
return if $context->flag('allowBadFunctionInputs');
my ($b, $x) = @{ $self->{params} };
$self->Error('Function "%s" must have numeric inputs', $self->{name})
unless $b->isNumber && $x->isNumber;
$self->{type} = $Value::Type{complex} if $x->isComplex || $b->isComplex;
return;
}

# Check that the inputs are OK and call the named routine
sub _call {
my ($self, $name, @inputs) = @_;
$self->Error('Function "%s" has too many inputs', $name) if scalar(@inputs) > 2;
$self->Error('Function "%s" has too few inputs', $name) if scalar(@inputs) < 2;
return $self->$name($self->checkArguments($name, @inputs));
}

# Call the appropriate routine
sub _eval {
my ($self, @inputs) = @_;
my $name = $self->{name};
return $self->$name($self->checkArguments($name, @inputs));
}

# Check that the parameters are OK
sub checkArguments {
my ($self, $name, @inputs) = @_;
my $context = $self->context;
my ($b, $x) = (map { Value::makeValue($_, $context) } @inputs);
$self->Error('Function "%s" must have numeric inputs', $name)
unless $b->isNumber && $x->isNumber;
return ($b, $x);
}

# Compute log base b using log(x)/log(b)
# If b < 0 or x < 0, either promote to a complex or throw an error.
sub logb {
my ($self, $b, $x) = @_;
$self->Error('Invalid base %s logarithm of %s', $b)
if $x->value == 0 || $b->value == 0;
if (($x->isReal && $x->value < 0) || ($b->isReal && $b->value < 0)) {
my $context = $x->context;
$self->Error('Invalid base %s logarithm of %s', $b, $x)
unless $context->functions->get('logb')->{negativeIsComplex};
$x = $self->Package('Complex')->promote($context, $x);
$b = $self->Package('Complex')->promote($context, $b);
}
return log($x) / log($b);
}

# Implement differentiation: (logb(b, u))' -> u'/(u * ln(b)) - b'/(b * ln(u)) * logb(b, u)
sub D {
my ($self, $x) = @_;
my $equation = $self->{equation};
my $BOP = $self->Item('BOP');
my $NUM = $self->Item('Number');
my ($b, $u) = @{ $self->{params} };
my $D = $BOP->new(
$equation,
'/',
$u->D($x),
$BOP->new(
$equation, '*', $u->copy($equation),
$self->Item('Function')->new($equation, 'ln', [ $b->copy($equation) ], $b->{isConstant})
)
);
$D = $BOP->new(
$equation,
'-', $D,
$BOP->new(
$equation,
'*',
$BOP->new(
$equation,
'/',
$b->D($x),
$BOP->new(
$equation, '*', $b->copy($equation),
$self->Item('Function')->new($equation, 'ln', [ $b->copy($equation) ], $b->{isConstant})
)
),
$self->copy($equation)
)
) if $b->getVariables->{$x};
return $D->reduce;
}

# Output TeX using \log_{b}(x)
sub TeX {
my ($self, $precedence, $showparens, $position, $outerRight, $power) = @_;
$showparens = '' unless defined $showparens;
my $fn = $self->{equation}{context}{operators}{'fn'};
my $fn_precedence = $fn->{parenPrecedence} || $fn->{precedence};
my ($b, $x) = @{ $self->{params} };
my $TeX = '\log_{' . $b->TeX . '}\left(' . $x->TeX . '\right)';
$TeX = '\left(' . $TeX . '\right)'
if $showparens eq 'all'
|| $showparens eq 'extra'
|| (defined($precedence) && $precedence > $fn_precedence)
|| (defined($precedence) && $precedence == $fn_precedence && $showparens eq 'same');
return $TeX;
}

1;
1 change: 1 addition & 0 deletions macros/parsers/parserRoot.pl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ sub Enable {
$context = main::Context() unless Value::isContext($context);
$context->functions->add(root => { class => 'parser::Root::Function::numeric2' },);
$context->functions->set(root => { negativeIsComplex => 1 }) if $complex;
$context->flag('mathQuillOpts')->{rootsAreExponents} = 0;
}

sub EnableComplex {
Expand Down

0 comments on commit e159d43

Please sign in to comment.