24 days of Perl code from RJBS! Feed

Making a List, Checking it 0b10 Times...

Number::Nary - 2009-12-02

Beyond Hex and Octal

It's sort of easy to convert between decimal, hexadecimal, and octal in Perl. sprintf can produce decimal, hexadecimal, octal, or binary digit strings, and the oct built-in can read those strings back into numbers. For example, to convert from hex to binary strings, we might do the following:


1: 
2: 
3: 

 

my $hex = '99';
my $num = oct("0x$hex"); # or we could use hex($hex);
my $bin = sprintf '%b', $num; # ...and we get 10011001

 

This is a little inconsistent, but fast and easy. Unfortunately, sometimes we need to work with really weird digit sets. Fortunately, Number::Nary makes these really easy to work with. For example, if we need to convert from sexagesimal to trinary (presumably because we are writing a Star Trek script) we can just create routines using Number::Nary like this:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 

 

use Number::Nary
  -codec_pair => {
    -suffix => '_3',
    digits => '012',
  },
  -codec_pair => {
    -suffix => '_60',
    digits => [ map { sprintf '%02u', $_ } (0 .. 59) ], # 00 through 59
  };

 

Now we have four routines for converting between our numbering systems. Given the number 102012012 in trinary, we can easily convert it to sexagesimal:


1: 
2: 
3: 

 

my $trinary = '102012012';
my $number = decode_3($trinary);
my $sexa = encode_60($number); # "021559"

 

Neat, but is it ever practical? Yes! For one thing, it can be used to fix numbers into weird character sets. I've used it to cram several large numbers into email addresses by using the valid email address characters as a digit set. There's also Jesse Vincent's Number::RecordLocator, which "encodes integers into a short and easy to read and pronounce locator strings." Using Number::Nary, we can re-implement that module in about four lines:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 

 

use Number::Nary -codec_pair => {
  digits => [ 2 .. 9, 'A', 'C' .. 'R', 'T' .. 'Z' ],
  predecode => sub { my $s = $_[0]; $s =~ tr/01SBsb/OIFPFP/; return $s },
};

# We silently replace some digits with others to keep things easy to read.
ok(decode('1234') == decode('I234')); # 513430
ok(decode('10SB') == decode('IOFP')); # 491554

 

Number::Nary is slower than sprintf and oct are, but since they can only handle a few different digit sets, they're not always going to solve the problem at hand. Having Number::Nary in the wings when you need it can be a real time-saver.

Finally, here's a quick bit of Christmas spirit:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 

 

use Number::Nary 0.107 -codec_pair => {
  digits => [ 'ho ' ],
  postencode => sub {
    (my $s = shift) =~ s/ \z//;
    "Merry Christmas, $s!"
  },
  predecode => sub {
    my $s = lc shift;
    $s =~ s/\Amerry christmas, //;
    $s =~ s/!\z/ /;
    $s;
  },
};

# Appropriate:
say encode(3);

# Not:
say encode(1);

 

See Also

  • Number::Nary

  • UDCode - to determine if a set of digits will produce unambiguous strings

  • Math::BaseCalc - another take on the problem (but I don't recommend it)