Skip to content

Commit

Permalink
more work on creating this as a context
Browse files Browse the repository at this point in the history
updates
  • Loading branch information
pstaabp committed Mar 7, 2023
1 parent 73913d4 commit 281274e
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 144 deletions.
185 changes: 52 additions & 133 deletions macros/contexts/contextNondecimalBase.pl
Original file line number Diff line number Diff line change
@@ -1,111 +1,82 @@

=head1 NAME
contextHex.pl - Implements a MathObject class and context for integers
in hexadecimal notation.
contextNonDecimalBase.pl - Implements a MathObject class and context for numbers in non-decimal bases
=head1 DESCRIPTION
This context implements a Hex object that works like a Real, but
where you enter numbers in hexadecimal, and they display in hexadecimal.
You can perform the usual numeric operations (addition, subtraction,
etc.), but division is integer division (so 7/3 = 2). The context defines
the bitwise operators &, |, ^, >>, <<, and ~ (for bitwise and, or,
exclusive or, shift-right, shift-left, and one's complement not). You can
apply these operations within your PG code to variables that store Hex
objects. Remember that you can also obtain Perl reals via hex notation,
for example, 0x1A.
This context implements a Hex object that works like a Real, but can implement numbers
in any non-decimal base between 2 and 16. The numbers will be stored internally in
decimal, though parsed and shown in the chosen base.
To use hexadecimal MathObjects, first load the contextHex.pl file:
In addition, basic integer arithemetic (+,-,*,^) are available for these number.
loadMacros("contextHex.pl");
The original purpose for this is simple conversion and operations in another base, however
it is not limited to that
and then select the appropriate context -- one of the following:
To use a non-decimal base MathObject, first load the contextNondecimalBase.pl file:
loadMacros("contextNondecimalBase.pl");
Context("Hex");
Context("LimitedHex");
and then select the appropriate context -- one of the following:
The latter only allows the student to enter hexadecimal literals (not
expressions), or lists of hexadecimal numbers. The former allows
expression involving numeric operations and bitwise operations.
Context("NondecimalBase");
Once one of these contexts is selected, all the nummbers parsed by
MathObjects will be considered to be in hexadecimal, so
$n = Compute('10');
produces the hexadecimal number 10 (decimal 16). You could also obtain
a hex MathObject using
$n = Hex(0x10);
Once you have such a value, use
ANS($n->cmp)
to get an answer checker for the number. You can also perform numeric or
bitwise operations on the value, as in
$m = $n + 0xE3;
$N = $n << 2; # shifts n to the left 2 (binary) places,
# $N = Hex(0x40) when $n = Hex(0x10)
=cut

sub _contextNondecimalBase_init {
context::NondecimalBase::Init();
sub setBase { context::NondecimalBase::setBase(@_); }
context::NondecimalBase::Init(@_);
# sub setBase { context::NondecimalBase::setBase(@_); }
sub convertBase { context::NondecimalBase::convert(@_); }
}

###########################################################################

package context::NondecimalBase;
our @ISA = ('Parser::Context');

use Data::Dumper;

# defines the base for the context.
our $base = 10;
#
# The standard digits, pre-built so it doesn't have to be done each time the conversion is called
#
our $digits16 = [ '0' .. '9', 'A' .. 'F' ];
our $digit16 = { map { ($digits16->[$_], $_) } (0 .. scalar(@$digits16) - 1) };

#
# Initialize the contexts and make the creator function.
#
sub Init {
my $context = $main::context{NondecimalBase} = Parser::Context->getCopy("Numeric");
$context->{name} = 'NondecimalBase';
$context->{parser}{Number} = 'context::NondecimalBase::Number';
# $context->{value}{Real} = 'context::Hex::Hex';
$context->{value}{Real} = 'context::NondecimalBase::Real';
$context->{pattern}{number} = '[0-9A-F]+';
$context->functions->disable('All');
$context->operators->remove('^');
# $context->operators->remove('^');
$context->parens->remove('|');
$context->constants->clear();
# $context->operators->add(
# '&' => {precedence => .6, associativity => 'left', type => 'bin', string => ' & ',
# class => 'context::Hex::BOP::hex', eval => sub {$_[0] & $_[1]}},
# '|' => {precedence => .5, associativity => 'left', type => 'bin', string => ' | ',
# class => 'context::Hex::BOP::hex', eval => sub {$_[0] | $_[1]}},
# '^' => {precedence => .5, associativity => 'left', type => 'bin', string => ' ^ ',
# class => 'context::Hex::BOP::hex', eval => sub {$_[0] ^ $_[1]}},
# '>>' => {precedence => .4, associativity => 'left', type => 'bin', string => ' >> ',
# class => 'context::Hex::BOP::hex', eval => sub {$_[0] >> $_[1]}},
# '<<' => {precedence => .4, associativity => 'left', type => 'bin', string => ' << ',
# class => 'context::Hex::BOP::hex', eval => sub {$_[0] << $_[1]}},
# '~' => {precedence => 6, associativity => 'left', type => 'unary', string => '~',
# class => 'context::Hex::UOP::not'},
# );
# $context->{precedence}{Hex} = $context->{precedence}{special};
$context->{precedence}{NondecimalBase} = $context->{precedence}{special};
$context->flags->set(limits => [ -1000, 1000, 1 ]);
$context->update;

# main::PG_restricted_eval('sub Hex {context::Hex::Hex->new(@_)}');
}

sub setBase {
my ($name, $base) = @_;
print Dumper 'in setBase';
print Dumper $name;
my $context = $main::context{$name} = Parser::Context->getCopy($name);
print Dumper $context;
print Dumper $base;
}

sub convert {
my $value = shift;
# print Dumper 'in convert';
# print Dumper $value;
# Set default options and get passed in options.
my %options = (
from => 10,
Expand All @@ -117,25 +88,23 @@ sub convert {
my $to = $options{'to'};
my $digits = $options{'digits'};

# print Dumper $from;
# print Dumper $to;
# print Dumper $digits;

die "The digits option must be an array of characters to use for the digits"
unless ref($digits) eq 'ARRAY';

#
# The highest base the digits will support
#
my $maxBase = scalar(@$digits16);
my $maxBase = scalar(@$digits);

die "The base of conversion must be between 2 and $maxBase"
unless $to >= 2 && $to <= $maxBase && $from >= 2 && $from <= $maxBase;

#
# Reverse map the digits to base 10 values
#
my $baseBdigits = { map { ($digits->[$_], $_) } (0 .. $from - 1) };

#
# Convert to base 10
#
my $base10;
if ($from == 10) {
die "The number to convert must consist only of digits: 0,1,2,3,4,5,6,7,8,9"
Expand All @@ -151,9 +120,7 @@ sub convert {
}
return $base10 if $to == 10;

#
# Convert to desired base
#
my @base;
do {
my $d = $base10 % $to;
Expand All @@ -164,31 +131,15 @@ sub convert {
return join('', @base);
}

# set the base for the context.

sub setBase {
my $b = shift;
return Value::Error('The base must be greater than 1 and less than or equal to $max_base')
unless $b >= 2 && $b <= scalar(@$digits16);
$base = $b;
# $digits16 = [map { $digits16->[$_] } (0..($base-1))];
# $digit16 = { map { ($digits16->[$_], $_) } (0 .. scalar(@$digits16) - 1) };
# $context->{pattern}{number} = '[' . join('',@$digits16) . ']+';
}

###########################################################################
#
# A replacement for Parser::Number that acepts numbers in
# hexadecimal and converts them to decimal for internal use
#
# A replacement for Parser::Number that acepts numbers in a NonDecimal base and converts them to decimal for internal use
package context::NondecimalBase::Number;
our @ISA = ('Parser::Number');

# Create a new number in the given base and convert to base 10.

use Data::Dumper;
sub new {
my $self = shift;
my ($equation, $value, $ref) = @_;
my ($self, $equation, $value, $ref) = @_;
my $base = $equation->{context}{flags}{base};
$value = context::NondecimalBase::convert($value, from => $base);
return $self->SUPER::new($equation, $value, $ref);
}
Expand All @@ -198,68 +149,36 @@ sub new {
#
sub eval {
$self = shift;
my $base = $self->{equation}{context}{flags}{base};
return context::NondecimalBase::convert($self->{value}, to => $base);
$self->Package('Real')->make($self->context, $self->{value});
}

###########################################################################
#
# A replacement for Value::Real that handles hexadecimal integers
# A replacement for Value::Real that handles non-decimal integers
#
package context::Hex::Hex;
package context::NondecimalBase::Real;
our @ISA = ('Value::Real');

use Data::Dumper;
#
# Stringify and TeXify in hex notation
# Stringify and TeXify the number in the context's base
#
sub string {
my $self = shift;
return main::spf($self->value);
my $base = $self->{context}{flags}{base};
return context::NondecimalBase::convert($self->value, to => $base);
}

sub TeX {
my $self = shift;
return '\text{' . $self->string . '}';
}

###########################################################################
#
# This is a Parser::BOP that handles the bitwise operations (all of
# them call the same class, and the operators list gives the code to
# perform the operation)
#
package context::Hex::BOP::hex;
our @ISA = ('Parser::BOP');

sub _check {
my $self = shift;
return if $self->checkNumbers;
$self->Error("Arguments to '%s' must be Numbers", $self->{bop});
my $base = $self->{context}{flags}{base};
return '\text{' . context::NondecimalBase::convert($self->string, to => $base) . '}';
}

sub _eval {
my ($self, $a, $b) = @_;
$a->inherit($b)->make(&{ $self->{def}{eval} }($a->value, $b->value));
sub add {
my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_);
print Dumper 'in add';
}

###########################################################################
#
# The Parser::UOP subclass for one's complement not.
#
package context::Hex::UOP::not;
our @ISA = ('Parser::UOP');

sub _check {
my $self = shift;
return if $self->checkNumber;
$self->Error("Argument to '%s' must be a Number", $self->{uop});
}

sub _eval {
my ($self, $a) = @_;
$a->make(~($a->value));
}

###########################################################################

1;
65 changes: 54 additions & 11 deletions t/contexts/nondecimal_base.t
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,68 @@ use Value;
require Parser::Legacy;
import Parser::Legacy;

use Data::Dumper;

Context('NondecimalBase');

setBase(5);
# test that convert is working

# ok my $a1 = Compute('10');
ok my $a2 = Compute('240');
is convertBase($a2->value, from => 5), 70, 'Base 5 stored correctly in base 10';
# subtest 'conversion from a non-decimal base to base 10' => sub {
# is convertBase('101010', from => 2), 42, 'convert from base 2';
# is convertBase('44011', from => 5), 3006, 'convert from base 5';
# is convertBase('5073', from => 8), 2619, 'convert from base 8';
# is convertBase('98A', from => 12), 1402, 'convert from base 12';
# is convertBase('98T', from => 12, digits => [ '0' .. '9', 'T', 'E' ]), 1402,
# 'convert from base 12 with non-standard digits';
# is convertBase('9FE8', from => 16), 40936, 'convert from base 16';
# };

subtest 'check that non-valid digits return errors' => sub {
like dies { Compute('456'); }, qr/^The number to convert must consist/,
'Try to build a base-5 number will illegal digits';
};
# subtest 'Convert from decimal to non-decimal bases' => sub {
# is convertBase(12, to => 2), '1100', 'convert to base 2';
# is convertBase(47, to => 2), '101111', 'convert to base 2';

# is convertBase(98, to => 5), '343', 'convert to base 5';
# is convertBase(761, to => 5), '11021', 'convert to base 5';

# is convertBase(519, to => 8), '1007', 'convert to base 8';
# is convertBase(2023, to => 8), '3747', 'convert to base 8';

# is convertBase(853, to => 12), '5B1', 'convert to base 12';
# is convertBase(2023, to => 12), '1207', 'convert to base 12';
# is convertBase(1678, to => 12, digits => [ '0' .. '9', 'T', 'E' ]), 'E7T',
# 'convert to base 12 using non-standard digits';

# is convertBase(5752, to => 16), '1678', 'convert to base 16';
# is convertBase(41446, to => 16), 'A1E6', 'convert to base 16';
# };

# subtest 'Convert between two non-decimal bases' => sub {
# is convertBase('1234', from => 5, to => 16), 'C2', 'convert from base 5 to 16';
# };

# Now test the Context.

Context()->flags->set(base => 5);
# # context::NondecimalBase->setBase(5);

# subtest 'Check that the Context parses number correct' => sub {
# is Context()->{flags}->{base}, 5, 'Check that the base is stored.';

# ok my $a1 = Compute('10'), "The string '10' is parsed correctly";
# ok my $a2 = Compute('240'), "The string '240' is parsed correctly";
# is convertBase($a2->value, from => 5), 70, 'Base 5 stored correctly in base 10';
# };

# subtest 'check that non-valid digits return errors' => sub {
# like dies { Compute('456'); }, qr/^The number to convert must consist/,
# 'Try to build a base-5 number will illegal digits';
# };

subtest 'check arithmetic in non-decimal base' => sub {
my $a3 = Compute('240+113');
ok $a3->value, '403', 'Base 5 addition is correct';
my $a4 = Compute('240-113');
ok $a4->value, '122', 'Base 5 subtraction is correct';
# my $a4 = Compute('240-113');
# ok $a4->value, '122', 'Base 5 subtraction is correct';
};


done_testing();

0 comments on commit 281274e

Please sign in to comment.