-
-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Macro for nondecimal bases #768
Merged
drgrice1
merged 19 commits into
openwebwork:develop
from
pstaabp:convert-nondecimal-base
Feb 14, 2024
+546
−0
Merged
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
d9c0586
create a macro for nondecimal bases with a test.
pstaabp f4ba61a
convert to a context
pstaabp b708df5
updates
pstaabp 16dd2c4
more work on creating this as a context
pstaabp 1621027
update
pstaabp 37998ed
documentation and cleanup
pstaabp 72e4d8b
fix error with non-standard decimal characters.
pstaabp 61f38b3
removal of old versions.
pstaabp b2484b6
another update
pstaabp 3a9c123
Add integer division and improvement of documentation
pstaabp c958740
make the context a package variable and add the number pattern to the…
pstaabp a449e26
fix typos
pstaabp a4ab0f8
light edits to documentation
Alex-Jordan 783b9e3
some preset digit collections
Alex-Jordan 58cb5cb
add modulo operator
Alex-Jordan a65f2f5
rename contextNondecimalBase.pl to contextBaseN.pl
Alex-Jordan 1b4c7bb
Minor bug fix, documentation and more tests.
pstaabp ced1be1
automatically print base for Real answers
Alex-Jordan 0ab122a
Merge pull request #23 from Alex-Jordan/convert-nondecimal-base
pstaabp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,330 @@ | ||
|
||
=head1 NAME | ||
|
||
contextBaseN.pl - Implements a MathObject class and context for numbers | ||
in non-decimal bases | ||
|
||
=head1 DESCRIPTION | ||
|
||
This context implements positive integers and some operations on integers in a non-decimal base | ||
greater than or equal to 2. The numbers will be stored internally in decimal, though parsed | ||
and shown in the chosen base. | ||
|
||
In addition, basic integer arithemetic (+,-,*,/,^) are available for these numbers. | ||
Division is defined in an integer sense. | ||
|
||
The original purpose for this is simple conversion and operations in another base, however | ||
it is not limited to this. | ||
|
||
To use a non-decimal base MathObject, first load the contextBaseN.pl file: | ||
|
||
loadMacros('contextBaseN.pl'); | ||
|
||
There are two contexts: C<BaseN> and C<LimitedBaseN>, where the former | ||
allows operations between numbers and the latter only allows numbers. To use either, | ||
one must set the base. For example: | ||
|
||
Context('BaseN')->setBase(5); | ||
|
||
Now most numerical strings in Compute, Formula, and student answers will be read in base five. | ||
|
||
$a = Compute('104'); | ||
$b = Compute('233'); | ||
$sum = $a+$b # this is the base-5 number 342 (decimal 97) | ||
|
||
or a shorter way: | ||
|
||
$sum = Compute('104+233'); | ||
|
||
Also, when a string is the argument to some other Math Object and that string needs to | ||
be parsed, numerical substrings will be read in base 5: | ||
|
||
$point = Point('(104, 233)'); # this is (29, 68) in base ten | ||
|
||
For Math Object constructors that directly accept a number or numbers as arguments, | ||
the numbers will be read in base ten. All of the following should be read in base ten: | ||
|
||
$r = Real(29); | ||
$r = Real('68'); | ||
$p = Point(29, 68); | ||
|
||
For many problems, one may wish to not allow operators in the student answers. Use | ||
'LimitedBaseN' for this. | ||
|
||
Context('LimitedBaseN')->setBase(5); | ||
$sum = Compute("104+233"); # There will be an error on this line now. | ||
|
||
In both contexts, rather than pass the base as a number, another option is to pass the | ||
digits used for the number to the C<setBase> method. For example, if one wants to use base-12 | ||
and use the alternative digits 0..9,'T','E', then | ||
|
||
Context('BaseN')->setBase([0 .. 9, 'T', 'E']); | ||
|
||
Then one can use the digits 'T' and 'E' in a number like: | ||
|
||
Compute('9TE'); | ||
|
||
A few strings can be passed to the C<setBase> method with preset meanings: | ||
|
||
C<binary> for [0,1] | ||
C<octal> for [0 .. 7] | ||
C<decimal> for [0 .. 9] | ||
C<duodecimal> for [0 .. 9, 'A', 'B'] | ||
C<hexadecimal> for [0 .. 9, 'A' .. 'F'] | ||
C<base64> for ['A' .. 'Z', 'a' .. 'z', 0 .. 9, '_', '?'] | ||
|
||
The last two digits for C<base64> are nonstandard. We want to avoid '+' and '/' here as they have arithmetic meaning. | ||
|
||
=head2 Sample PG problem | ||
|
||
A simple PG problem that asks a student to convert a number into base-5: | ||
|
||
DOCUMENT(); | ||
loadMacros(qw(PGstandard.pl PGML.pl contextBaseN.pl)); | ||
|
||
Context('LimitedBaseN')->setBase(5); | ||
|
||
# decimal number picked randomly. | ||
$a = random(130,500); | ||
$a_5 = Real($a); # converts $a to base-5 | ||
|
||
BEGIN_PGML | ||
Convert [$a] to base-5: | ||
|
||
[$a] = [__]{$a_5}[`_5`] | ||
END_PGML | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops, this is almost a complete PG file but missing the last line. |
||
|
||
=cut | ||
|
||
sub _contextBaseN_init { | ||
context::BaseN::Init(@_); | ||
sub convertBase { context::BaseN::convert(@_); } | ||
} | ||
|
||
package context::BaseN; | ||
|
||
# Define the contexts 'BaseN' and 'LimitedBaseN' | ||
sub Init { | ||
my $context = $main::context{BaseN} = context::BaseN::Context->new(); | ||
$context = $main::context{LimitedBaseN} = $context->copy; | ||
$context->{name} = 'LimitedBaseN'; | ||
$context->operators->undefine($context->operators->names); | ||
$context->parens->undefine('|', '{', '['); | ||
} | ||
|
||
=head2 convertBase | ||
|
||
The function C<convertBase(value, opts)> converts the value from or to other bases depending on the options | ||
in C<opts>. The input C<value> is a positive number or string version of a positive number in some base. | ||
|
||
=head3 options | ||
|
||
=over | ||
|
||
=item * C<from> the base that C<value> is in. Default is 10. Can take the same values as C<setBase>. | ||
|
||
=item * C<to> the base that C<value> will be converted to. Default is 10. Can take the same values as C<setBase>. | ||
|
||
=back | ||
|
||
=head3 Examples | ||
|
||
For the following, since C<from> is not used, the base of C<value> is assumed to be 10. | ||
|
||
convertBase(58, to => 5); # returns 213 | ||
convertBase(58, to => 8); # returns 72 | ||
convertBase(734, to => 16); # returns 2DE | ||
|
||
For the following, since C<to> is not used, these are converted to base 10. | ||
|
||
convertBase(213, from => 5); # returns 58 | ||
convertBase(72, from => 8); # returns 58 | ||
convertBase('2DE', from => 16); # returns 734 | ||
|
||
Both C<to> and C<from> can be used together. | ||
|
||
convertBase(213, from => 5, to => 8); # returns 72 | ||
|
||
If one wants to use a different set of digits, say 0..9, 'T', 'E' for base-12 as an example | ||
|
||
convertBase(565, to => [0 .. 9, 'T', 'E']); # returns '3E1' | ||
|
||
=cut | ||
|
||
my $convertContext; | ||
|
||
sub convert { | ||
my ($value, %options) = @_; | ||
my $from = $options{'from'} // 10; | ||
my $to = $options{'to'} // 10; | ||
|
||
$convertContext = $main::context{BaseN}->copy unless $convertContext; | ||
if ($from != 10) { | ||
$convertContext->setBase($from); | ||
$value = $convertContext->fromBase($value); | ||
} | ||
if ($to != 10) { | ||
$convertContext->setBase($to); | ||
$value = $convertContext->toBase($value); | ||
} | ||
return $value; | ||
} | ||
|
||
package context::BaseN::Context; | ||
our @ISA = ('Parser::Context'); | ||
|
||
# Create a Context based on Numeric that allows +, -, *, /, %, and ^ on BaseN integers. | ||
|
||
sub new { | ||
my $self = shift; | ||
my $class = ref($self) || $self; | ||
my $context = bless Parser::Context->getCopy('Numeric'), $class; | ||
$context->{name} = 'BaseN'; | ||
$context->{parser}{Number} = 'context::BaseN::Number'; | ||
$context->{value}{Real} = 'context::BaseN::Real'; | ||
$context->functions->disable('All'); | ||
$context->constants->clear(); | ||
$context->{pattern}{number} = '[' . join('', 0 .. 9, 'A' .. 'Z') . ']+'; | ||
$context->{precedence}{BaseN} = $context->{precedence}{special}; | ||
$context->flags->set(limits => [ -1000, 1000, 1 ]); | ||
$context->operators->add( | ||
'%' => { | ||
class => 'context::BaseN::BOP::modulo', | ||
precedence => 3, | ||
associativity => 'left', | ||
type => 'bin', | ||
string => ' % ', | ||
TeX => '\mathbin{\%}', | ||
} | ||
); | ||
return $context; | ||
} | ||
|
||
# set the base of the context. Either an integer that is at least 2, an arrayref of digits, | ||
# or a preset: 'binary', 'octal', 'decimal', 'duodecimal', 'hexadecimal', or 'base64'. | ||
sub setBase { | ||
my ($self, $base) = @_; | ||
my $digits; | ||
|
||
$base = [ 0, 1 ] if ($base eq 'binary'); | ||
$base = [ 0 .. 7 ] if ($base eq 'octal'); | ||
$base = [ 0 .. 9 ] if ($base eq 'decimal'); | ||
$base = [ 0 .. 9, 'A', 'B' ] if ($base eq 'duodecimal'); | ||
$base = [ 0 .. 9, 'A' .. 'F' ] if ($base eq 'hexadecimal'); | ||
$base = [ 'A' .. 'Z', 'a' .. 'z', 0 .. 9, '_', '?' ] if ($base eq 'base64'); | ||
|
||
if (ref($base) eq 'ARRAY') { | ||
$digits = $base; | ||
$base = scalar(@$digits); | ||
die 'Base must be at least 2' unless $base >= 2; | ||
} else { | ||
die 'Base must be an integer' unless $base == int($base); | ||
die 'Base must be at least 2' unless $base >= 2; | ||
die 'You must provide a digit list for bases bigger than 36' if $base > 36; | ||
$digits = [ ('0' .. '9', 'A' .. 'Z')[ 0 .. $base - 1 ] ]; | ||
} | ||
|
||
$self->{base} = $base; | ||
$self->{digits} = $digits; | ||
$self->{digitMap} = { map { ($digits->[$_], $_) } (0 .. $base - 1) }; | ||
$self->{pattern}{number} = '[' . join('', @$digits) . ']+'; | ||
my $msg = 'Numbers should consist only of the digits: ' . join(',', @$digits); | ||
$self->{error}{msg}{"Variable '%s' is not defined in this context"} = $msg; | ||
$self->{error}{msg}{"'%s' is not defined in this context"} = $msg; | ||
$self->update; | ||
} | ||
|
||
sub copy { | ||
my $self = shift; | ||
my $copy = $self->SUPER::copy; | ||
$copy->{base} = $self->{base}; | ||
$copy->{digits} = $self->{digits}; | ||
$copy->{digitMap} = $self->{digitMap}; | ||
return $copy; | ||
} | ||
|
||
# Convert a number in base10 to the given base. | ||
sub toBase { | ||
my ($self, $base10) = @_; | ||
my $b = $self->{base}; | ||
my $digits = $self->{digits}; | ||
|
||
my @baseB; | ||
do { | ||
my $d = $base10 % $b; | ||
$base10 = ($base10 - $d) / $b; | ||
unshift(@baseB, $digits->[$d]); | ||
} while $base10; | ||
|
||
return join('', @baseB); | ||
} | ||
|
||
# Convert a number in a given base to base 10. | ||
sub fromBase { | ||
my ($self, $baseB) = @_; | ||
my $b = $self->{base}; | ||
my $digits = $self->{digits}; | ||
my $digit = $self->{digitMap}; | ||
|
||
my $base10 = 0; | ||
for my $d (split('', $baseB)) { | ||
die 'The number should only consist of the digits: ' . join(',', @$digits) unless defined($digit->{$d}); | ||
$base10 = $base10 * $b + $digit->{$d}; | ||
} | ||
|
||
return $base10; | ||
} | ||
|
||
# A replacement for Parser::Number that accepts numbers in a non-decimal base and | ||
# converts them to decimal for internal use | ||
package context::BaseN::Number; | ||
our @ISA = ('Parser::Number'); | ||
|
||
# Create a new number in the given base and convert to base 10. | ||
sub new { | ||
my ($self, $equation, $value, $ref) = @_; | ||
my $context = $equation->{context}; | ||
|
||
Value::Error('The base must be set for this context') unless $context->{base}; | ||
|
||
$value = $context->fromBase($value); | ||
return $self->SUPER::new($equation, $value, $ref); | ||
} | ||
|
||
sub eval { | ||
$self = shift; | ||
return $self->Package('Real')->make($self->context, $self->{value}); | ||
} | ||
|
||
# Modulo operator | ||
package context::BaseN::BOP::modulo; | ||
our @ISA = ('Parser::BOP::divide'); | ||
|
||
# | ||
# Do the division. | ||
# | ||
sub _eval { $_[1] % $_[2] } | ||
|
||
# A replacement for Value::Real that handles non-decimal integers | ||
package context::BaseN::Real; | ||
our @ISA = ('Value::Real'); | ||
|
||
# Stringify and TeXify the number in the context's base | ||
sub string { | ||
my $self = shift; | ||
return $self->context->toBase($self->value); | ||
} | ||
|
||
sub TeX { | ||
my $self = shift; | ||
return '\text{' . $self->string . '}'; | ||
} | ||
|
||
# Define division as integer division. | ||
sub div { | ||
my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); | ||
Value::Error("Division by zero") if $r->{data}[0] == 0; | ||
return $self->inherit($other)->make(int($l->{data}[0] / $r->{data}[0])); | ||
} | ||
|
||
1; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have mentioned
%
here.