-
-
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
Macro for nondecimal bases #768
Conversation
Although the code works, it is not as efficient as it could be. In particular, the conversion to base 10 can be improved in several ways. Your current loop uses two multiplications, one addition, and two assignments for each digit processed, but it can be done with one multiplication, one addition, and one assignment (see below). But the real inefficiency is in using To eliminate one multiplication and one assignment from the loop (and also the building of a new list from the That means you can compute the value by taking the first digit, multiplying by b and adding the next digit, then multiply that sum by b and add the next digit, and so on. That is one multiply, one addition, and one assignment per digit (rather than two multiplies and assignments), and doesn't require you to reverse the digit list. The following example code implements this approach, using the digit lookup table rather than the sub convertBase {Base::convert(@_)}
package Base;
#
# 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) };
sub convert {
my $value = shift;
#
# Get the conversion options
#
my %options = (
from => 10,
to => 10,
digits => $digits16,
@_
);
my $from = $options{'from'};
my $to = $options{'to'};
my $digits = $options{'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(@$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 $digit = ( $digits == $digits16 ? $digit16 :
{ map { ($digits->[$_], $_) } (0 .. $maxBase - 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"
unless $value =~ m/^\d+$/;
$base10 = $value;
} else {
foreach my $d (split(//, $value)) {
die "The number to convert must consist only of digits: " . join(',', @$digits[0 .. $from - 1])
unless defined($digit->{$d});
$base10 = $base10 * $from + $digit->{$d};
}
}
return $base10 if $to == 10;
#
# Convert to desired base
#
my @base = ();
do {
my $d = $base10 % $to;
$base10 = ($base10 - $d) / $to;
unshift(@base, $digits->[$d]);
} while $base10;
return join('', @base);
} This uses a package namespace to make the It also handles the The check for valid digits is done in the conversion loop rather than using a dynamically created regex, which is more expensive (also, you haven't quoted regex special characters, so someone could potentially create an invalid or unexpected regex by using things like Finally, the conversion from base 10 to another base uses an array that is joined at the end rather than creating multiple strings, though both are about the same efficiency. In my timing tests, the conversion to base 10 is about 4 times faster using Horner's method with the loop hash, skipping all the validity checking to just concentrate on the algorithm, and even with the checking, it is still 3 times faster. The conversion from base 10 to an alternate base is only marginally faster with the arrays rather than the strings. I changed the I note that your examples all use This has the advantage of being able to do arithmetic in the given base, while the Of course, these are just suggestions. You can take them or leave them as you see fit. |
Also, one could use Math::Base::Convert for converting bases. |
@dpvc Appreciate the feedback. I was thinking of getting a functional conversion first rather than an efficient one (thinking that efficiency wouldn't really matter for ww problems), but this is great.
I looked into this and decided that this would be simple enough to do "in house" instead of requiring another package. I was thinking of creating a context. I'll look into your example from the forum. |
60b8d33
to
0e73b19
Compare
@dpvc I'm starting to convert it to a context using your Hex example and just pushed current changes. What I was thinking was, for example, I could create a context and the set the base, then perform arithmetic (at least +,-,*). However, the following
correctly parses 240 in base 5, 113 in base, however also adds 240+113 in base 10 (353), then tries to convert 353 from base 5 and I get an error. Do I need to override +,-,* ? |
0e73b19
to
0f38727
Compare
Sorry for not getting back to you earlier on this. I looked at it, but didn't have time to write initially. I haven't tried changing anything, but my firth thought is that you are trying to do conversion both to and from the base all in the The MathObjects library consists of two packages, Your The current I will make more comments in the code itself, but that, I think, is the main issue. |
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've made some extensive comments, here, that I hope will help you get this working.
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); |
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.
There is no $max_base
variable, and if you are using $digits16
, you know the highest is 16.
But since your convert()
allows for specifying a different set of digits, it may be that you want to allow the digits to be specified here. Then you can tell what the base is and can check that you have the right number of digits (rather than the other way around), and can create the digits array and reverse mapping once instead of doing it in convert()
each time. The base, the digits, and the reverse mapping can be stored as properties in the context itself, rather than as globals. If you set them up here, you won't need $digits16
or $digit16any more (just use
['0' ... '9', 'A' .. 'F']` as the default).
You are right to set the $context->{pattern}{number}
based on the actual digits (but should only use the ones up to the base requested), though you will need to do $context->update
after doing so.
If you subclass the Context object to add convert()
as a method, you should also make setBase()
a method as well. Otherwise, you will need to pass the context whose base you are setting as an argument to setBase()
.
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.
If you subclass the Context object to add convert() as a method, you should also make setBase() a method as well. Otherwise, you will need to pass the context whose base you are setting as an argument to setBase().
I think I'm also missing something here. This would be a subclass to Parser::Context
?
0f38727
to
281274e
Compare
@dpvc when you get a chance to look now. Lot's of cleanup, but adding isn't working right now. I'm testing |
281274e
to
a196e6a
Compare
@dpvc What I'm working on now is to be able to do I have something working, but seems like I'm going about this the right way. I have a class
but is there a way to have the parser automatically convert the operands for the operations +,-,* ? (I might drop / ) |
You do not need that, but I think you misunderstand the way it is supposed to work. Your sub TeX {
my $self = shift;
my $base = $self->{context}{flags}{base};
return '\text{' . $self->string . '}';
} for that. To fix your return context::NondecimalBase::convert($self->{value}, to => $base); which (incorrectly) returns a string (the same string the student entered, which was converted to base 10 and then back, for no apparent reason). That lets it fall through to $self->Package('Real')->make($self->context, $self->{value}); which produces the I also think you may not have the right idea about having a subclass of the Context class, but that is a separate issue for now. See if you can get the computation working first, and just set the |
@dpvc thanks for the help. I think I have a better handle on this now. If And addition is working. That is |
Exactly.
Great! Good job. The next step is to straighten out the context. Let me know when you are ready to do that. |
Seems to be mostly working now. I'm trying to build a limited version of the context as well so students aren't allowed to put in operations. |
a196e6a
to
06d8def
Compare
I wouldn't call this "ready for prime-time". Maybe ready for late-night viewing instead, but quite functional and passing all its tests!!! I added a 'LimitedNondecimalContext' as well which (consistent with other packages) allows only numerical values. Also, the code is cleanup and much better documentation. Here are two sample problems using these Contexts. |
44feb3a
to
aa739da
Compare
I notice two things testing this out, first the example given with different letters for digits doesn't seem to work:
I get an error that Second, if I pick two characters in the A-F range, I don't get any errors, but then I don't get the same characters back.
This gives me |
I had tested this with '8E', but E is by definition defined, but hadn't tested '8T'. It appears that this is being parsed as 8*T, however '8E' is being parsed as the number '8E'. |
aa739da
to
f1f1ba3
Compare
Another update with the error that @somiaj noticed is fixed. More testing I determined that the digits need to be updated any time the base is changed. (For example if we go from base 12 to base 16). I couldn't figure out how to run code when the |
541a93c
to
44fa83d
Compare
@dpvc Can you take a look at this again when you get a chance? |
44fa83d
to
952ef3d
Compare
So I think I was able to do what I described. That is, make this happen:
The question is if everyone thinks this is a good distinction to have. The TLDR of my post before was to consistently make strings be interpreted with the alternate base, and plain perl integers be interpreted base ten. |
Let's forget what I was trying to do in the last few posts. While I can get I opened a PR (pstaabp#22) to your branch with four commits:
|
update update
update convert to a context
updates updates
update update
More cleanup More cleanup
8fd6144
to
1b4c7bb
Compare
Just merged in the suggestions from @Alex-Jordan and added a few more unit tests. |
macros/contexts/contextBaseN.pl
Outdated
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. |
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.
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.
Officially marking for approval.
Convert [$a] to base-5: | ||
|
||
[$a] = [__]{$a_5}[`_5`] | ||
END_PGML |
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.
oops, this is almost a complete PG file but missing the last line.
I marked for approval. But then remembered one last thing I had thought to add. I opened a PR to your branch about that. Beyond that, was there any consideration for case insensitivity? Like having hexadecimal, but letting 10AE equal 10ae? I know it could be more trouble than it's worth when you consider a digit set that contains both capital and lowercase letters. But assuming the only letters in the digit set were capital, you could imagine allowing case insensitivity. Maybe that is just enabling inattentive students... |
automatically print base for Real answers
Maybe on a case-by-case basis. Clearly on your new base64 example, case sensitivity is important. |
This provides a macro from converting decimals to and from non-decimal bases. Although not necessary, the maximum base is 16 (hexadecimal). Also, a colleague of mine uses base-12 with T and E as the digits ten and eleven in that base, so there is an optional array of digits to use.
A test file is provided and to run it enter (from PG_ROOT):
prove -v t/macros_math/nondecimal_base.t
Also, a sample PG is provided:
nondecimal.pg.txt
There are plenty of examples in the test file and the sample PG file.