diff --git a/htdocs/js/MathQuill/mqeditor.js b/htdocs/js/MathQuill/mqeditor.js index c2f589a3d3..a68d5b5c43 100644 --- a/htdocs/js/MathQuill/mqeditor.js +++ b/htdocs/js/MathQuill/mqeditor.js @@ -286,6 +286,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 f26b0f4930..40e46bf539 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 {