From 241bcb14684b700409d35b659318614d47074c42 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 23 Nov 2024 18:34:57 -0600 Subject: [PATCH] Add a `parserLogb.pl` macro for logarithms with base b. This is similar to the `parserRoot.pl` macro. To enable the usage of the `logb` function in the current context call `Parser::Logb->Enable`. You can call `$n = logb(3, 5)` to get the value of the logarithm with base 3 evaluated at 5, or `$ans = Compute("logb(3, 5)")` to use as an answer. You can also call `Parser::Logb->EnableComplex` to allow negative bases and evaluation at negative numbers. Note that attempting to use a negative base or evaluate at a negative number would otherwise produce an error message. An answer in this context will automatically have the MathQuill option `logsChangeBase => 0` set. To facilitate this without the need to have numerous special cases for contexts in the `ENDDOCUMENT` method of `PG.pl`, there is a new `mathQuillOpts` context flag. The default value for this flag is a reference to a hash with no keys. Any context can set keys in this hash, and those will be tranferred to the MathQuill options for any answer rule in the context. This is now used by the the `parserRoot.pl` macro to set `rootsAreExponents => 0`, and by the new `parserLogb.pl` macro to set `logsChangeBase => 0`. To facilitate students entering a logarithm with a base, if the `logsChangeBase` option is 0 (or not set), then a subscript button is added to the MathQuill toolbar. --- htdocs/js/MathQuill/mqeditor.js | 3 + lib/Parser/Context/Default.pm | 1 + macros/PG.pl | 8 +- macros/parsers/parserLogb.pl | 213 ++++++++++++++++++++++++++++++++ macros/parsers/parserRoot.pl | 1 + 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 macros/parsers/parserLogb.pl diff --git a/htdocs/js/MathQuill/mqeditor.js b/htdocs/js/MathQuill/mqeditor.js index 3e2c860ca2..4840784f62 100644 --- a/htdocs/js/MathQuill/mqeditor.js +++ b/htdocs/js/MathQuill/mqeditor.js @@ -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: '|' }, diff --git a/lib/Parser/Context/Default.pm b/lib/Parser/Context/Default.pm index a57198509a..1f3e4ea154 100644 --- a/lib/Parser/Context/Default.pm +++ b/lib/Parser/Context/Default.pm @@ -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 => {}, }; ############################################################################ diff --git a/macros/PG.pl b/macros/PG.pl index ca9588269f..07cce8ae95 100644 --- a/macros/PG.pl +++ b/macros/PG.pl @@ -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); diff --git a/macros/parsers/parserLogb.pl b/macros/parsers/parserLogb.pl new file mode 100644 index 0000000000..08d8058c96 --- /dev/null +++ b/macros/parsers/parserLogb.pl @@ -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 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 +function that evaluates the logarithm with base b at x. For example, +C would return the equivalent of +C 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 function. You can use C both within C and +C 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 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 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 evaluated at C by typing C. To facilitate +students entering such answers, a subscript button is present in the MathQuill +toolbar for answers with the C 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; diff --git a/macros/parsers/parserRoot.pl b/macros/parsers/parserRoot.pl index 2b2d468de4..07233f2a2e 100644 --- a/macros/parsers/parserRoot.pl +++ b/macros/parsers/parserRoot.pl @@ -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 {