Skip to content

Commit

Permalink
create a macro for nondecimal bases with a test.
Browse files Browse the repository at this point in the history
update


update
  • Loading branch information
pstaabp committed Jan 23, 2023
1 parent 27d226f commit 60b8d33
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 0 deletions.
147 changes: 147 additions & 0 deletions macros/math/nondecimal_base.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
################################################################################
# WeBWorK Online Homework Delivery System
# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
#
# 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
nondecimal_base.pl - Handles conversions to and from non-decimal bases.
=head1 DESCRIPTION
The subroutine C<convertBase> converts nubmers to and from bases up to hexadecimal.
$x = 47;
$x2 = convertBase($x, to => 2);
$y16 = convertBase('2EF9', from => 16);
This can be used in problems with base conversion.
$x = random(200,500);
$x_16 = Real(convertBase($x, to => 16));
BEGIN_PGML
Convert the number [$x] (in decimal) to base-16
[$x] = [___]{$x_16} [`_{16}`]
END_PGML
=cut

sub _nondecimal_base_init { }

=head2 convertBase
Convert positive integers to and from non-decimal bases.
convertBase($x, %opts)
where C<$x> is an integer or string representation of a string. If the base (in either the
C<to> or C<from> options is greater than 1, the standard digits 'ABCDEF' are used for the next
6 digits.)
If instead, you wish to use other characters, the option C<digits> is an arrayref of digits.
See an example below.
The subroutine checks to ensure that the input number contain only provided digits (in the given
base), that the C<to> and C<from> options are only between 2 and 16 and that the C<digits>
option is an array ref.
=head3 Options
=over
=item *
C<to> the base to convert the integer to. C<to> should be an integer between 2 and 16. The
default is 10.
=item *
C<from> the base that the number C<$x> is in. C<from> should be an integer between 2 and 16.
The default is 10.
=item *
C<digits> is an arrayref of digits to use. The default is C<[0..9, 'A'..'F']>
=back
=head3 Examples
=over
=item *
C<convertBase(87, to=E<gt> 2)> returns C<1010111>
=item *
C<convertBase('9FE8', from =E<gt> 16)> returns C<40936>
=item *
The standard digits up to hexadecimal is 0..9,A,B,..,F. You can use non-standard digits
with the C<digits> option. The following uses 'T' and 'E' for the digit ten and eleven in
base 12.
convertBase(56, to => 12, digits = [0..9,'T','E'])
=back
=cut

sub convertBase {
my ($value, %opts) = @_;
$from = $opts{from} // 10;
$to = $opts{to} // 10;

return Value::Error('The option digits must be an array ref of length at least the larger of to/from.')
if defined($opts{digits})
&& (ref($opts{digits}) ne 'ARRAY' || scalar(@{ $opts{digits} }) < $to || scalar(@{ $opts{digits} }) < $from);
my @digits =
defined($opts{digits}) && ref($opts{digits}) eq 'ARRAY' ? @{ $opts{digits} } : ('0' .. '9', 'A' .. 'F');

return Value::Error('The base of conversion must be between 2 and 16')
unless $from >= 2 && $from <= 16 && $to >= 2 && $to <= 16;

# regular expression only of the digits up to from.
my $digre = '^[' . join('', @digits[ 0 .. ($from - 1) ]) . ']+$';
return Value::Error('The input number must consist only of the digits') unless $value =~ qr/$digre/;

# The value in base 10
my $val_10 = 0;
# convert $value to base 10 if not in that base
if ($from != 10) {
my $from_b = 1;
for my $ch (reverse($value =~ m/./g)) {
# convert
my $v = (grep { $digits[$_] eq $ch } (0 .. $#digits))[0];
$val_10 += $v * $from_b;
$from_b *= $from;
}
} else {
$val_10 = $value;
}
return $val_10 if $to == 10;

# Convert to the $to base
my $val_b = '';
do {
my $dig = $val_10 % $to;
$val_10 = int($val_10 / $to);
$val_b = "$digits[$dig]$val_b";
} while ($val_10 > 0);
return $val_b;
}
114 changes: 114 additions & 0 deletions t/macros_math/nondecimal_base.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env perl

=head1 nondecimal_base
Tests conversion of integers to non-decimal bases.
=cut

use Test2::V0 '!E', { E => 'EXISTS' };

die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
do "$ENV{PG_ROOT}/t/build_PG_envir.pl";

loadMacros('nondecimal_base.pl');

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 '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 'Check that errors are returned for illegal arguments' => sub {
like(
dies { convertBase('10E3', to => 16) },
qr/The input number must consist only of the digits/,
'The input number (base 10) doesn\'t consist of the given digits'
);
like(
dies { convertBase('10201', from => 2) },
qr/The input number must consist only of the digits/,
'The input number (base 2) doesn\'t consist of the given digits'
);
like(
dies { convertBase('807', from => 8) },
qr/The input number must consist only of the digits/,
'The input number (base 8) doesn\'t consist of the given digits'
);
like(
dies { convertBase('930C', from => 12) },
qr/The input number must consist only of the digits/,
'The input number (base 12) doesn\'t consist of the given digits'
);
like(
dies { convertBase('930A', from => 12, digits => [ 0 .. 9, 'T', 'E' ]) },
qr/The input number must consist only of the digits/,
'The input number (base 12) doesn\'t consist of the given digits (provided)'
);
};

subtest 'Check that errors are returned for illegal options' => sub {

like(
dies { convertBase(87, to => 14, digits => [ 0 .. 9, 'T' ]) },
qr/The option digits must be an array ref/,
'The digits option must have enough digits.'
);
like(
dies { convertBase(87, from => 12, digits => [ 0 .. 9, 'T' ]) },
qr/The option digits must be an array ref/,
'The digits option must have enough digits.'
);
like(
dies { convertBase(87, to => 8, digits => (0 .. 7)) },
qr/The option digits must be an array ref/,
'The digits option must be an array ref.'
);
like(
dies { convertBase(87, to => 1) },
qr/The base of conversion must be between 2 and 16/,
'The to option must be between 2 and 16.'
);
like(
dies { convertBase(87, to => 24) },
qr/The base of conversion must be between 2 and 16/,
'The to option must be between 2 and 16.'
);

like(
dies { convertBase('0110101', from => 1) },
qr/The base of conversion must be between 2 and 16/,
'The from option must be between 2 and 16.'
);
like(
dies { convertBase(87, from => 24) },
qr/The base of conversion must be between 2 and 16/,
'The from option must be between 2 and 16.'
);

};

done_testing;

0 comments on commit 60b8d33

Please sign in to comment.