### ----------------------------------------------------
### -- Number::More
### -- Licenses: Artistic-2.0
### -- Authors: Tom Browder <tbrowder@acm.org>
### -- File: lib/Number/More.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 2
unit module Number::More;

my $DEBUG = 0;

my $LH = %*ENV<LENGTH_HANDLING>:exists ??
         %*ENV<LENGTH_HANDLING> !! 'ignore'; # other options: 'warn', 'fail'
my token length-action { ^ :i warn|fail $ }

our $bset = "01".comb.Set;
our $oset = (0..7).Set;
our $dset = (0..9).Set;
our $hset = "ABCDEFabcdef".comb.Set (|) $dset;

# Define tokens for common regexes (no prefixes are allowed)
my token binary is export(:token-binary)            { ^ <[01]>+ $ }
my token octal is export(:token-octal)              { ^ <[0..7]>+ $ }
my token decimal is export(:token-decimal)          { ^ \d+ $ } # actually an int
my token hexadecimal is export(:token-hexadecimal)  { :i ^ <[a..f\d]>+ $ }   

# For general base functions 2..62
my token all-bases is export(:token-all-bases)      { ^ 
                                                        <[2..9]>         | 
                                                        <[1..5]><[0..9]> |
                                                        6 <[0..2]> 
                                                    $ }

# Standard digit set for bases 2 through 62 (char 0 through 61).
# The array of digits is indexed by their decimal postion in the array (note
# the %dec2digit hash can be created programmatically from this
# array):
our @dec2digit is export(:dec2digit) = <
    0 1 2 3 4 5 6 7 8 9
    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
    a b c d e f g h i j k l m n o p q r s t u v w x y z
    >;

# Standard digit set for bases 2 through 62 (char 0 through 61).
# The hash is comprised of digit (char) keys and their decimal index value
# in the parent @dec2digit array.
our %digit2dec is export(:digit2dec) = @dec2digit.antipairs;

my token base { ^ 2|8|10|16 $ }

# This is a non-exported sub
sub pad-number(
    $num is copy, 
    UInt $base where &all-bases,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) {

    my UInt $len = $num.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    if $prefix and $suffix {
        die "FATAL: You cannot select both :prefix and :suffix";
    }

    # This also checks for length error, upper-lower casing, and handling
    if 10 < $base < 37 {
        if $LC {
	    # special feature for case-insensitive bases
            $num .= lc;
        }
    }

    # strip any leading type indicator
    if $base ~~ /2|8|10|16/ {
        $num ~~ s/^0 [b|o|d|x]//;   
    }
    # strip any leading zeroes
    if $num.chars > 1 {
        $num ~~ s/^0*//;
    }

    # now $num should be "clean"
    my $nc  = $num.chars; # with no extra characters (no leading zeroes)

    # num pad zeroes with no prefix
    my $npad0 = $length > $nc ?? ($length - $nc) !! 0;
    if $prefix {
        $npad0 -= 2;
        $npad0 = 0 if $npad0 < 0;     
    }

    my $ncmin = $prefix ?? ($nc + 2) !! $nc;
    if ($length and ($LH ~~ (&length-action)) and ($ncmin > $length)) {
        my $msg = "Desired length ($length) of number '$num' is\
                     less than required by it";
        $msg ~= " and its prefix" if $prefix;
        $msg ~= " ($ncmin).";

        if $LH ~~ /$ :i warn $/ {
            note "WARNING: $msg";
        }
        elsif $LH ~~ /$ :i ignore $/ {
            ; # okay
        }
        else {
            die "FATAL: $msg";
        }
    }

    =begin comment
    padding, num raw length, and prefixes
    num  length prefix result
    x    0      none   x
    x    1      none   x
    x    2      none   0x
    x    3      none   00x
    x    4      none   000x

    x    1      0b     0bx
    x    2      0b     0bx
    x    3      0b     0bx
    x    4      0b     0b0x

    xxx  0      none   xxx
    xxx  2      none   xxx
    xxx  3      none   xxx
    xxx  4      none   0xxx

    xxx  0      0b     0bxxx
    xxx  2      0b     0bxxx
    xxx  3      0b     0bxxx
    xxx  4      0b     0bxxx
    xxx  5      0b     0bxxx
    xxx  6      0b     0b0xxx
    =end comment

    if $npad0 {
        # padding with zeross is required
        # first pad with zeroes, less any desired prefix
        my $zpad = '0' x $npad0;
        $num = $zpad ~ $num;
        $nc  = $num.chars;

        # now the following test should always be true!!
        unless $length > $ncmin {
            die "debug FATAL: unexpected \$length ($length)\
                NOT greater than \$ncmin ($ncmin)";
        }
    }

    if $prefix {
        when $base == 2  { $num = '0b' ~ $num }
        when $base == 8  { $num = '0o' ~ $num }
        when $base == 10 { $num = '0d' ~ $num }
        when $base == 16 { $num = '0x' ~ $num }
    }

    if $suffix {
        my @c = $base.comb;
        my $s;
        for @c {
            when /0/ { $s ~= "\x2080" }
            when /1/ { $s ~= "\x2081" }
            when /2/ { $s ~= "\x2082" }
            when /3/ { $s ~= "\x2083" }
            when /4/ { $s ~= "\x2084" }
            when /5/ { $s ~= "\x2085" }
            when /6/ { $s ~= "\x2086" }
            when /7/ { $s ~= "\x2087" }
            when /8/ { $s ~= "\x2088" }
            when /9/ { $s ~= "\x2089" }
            default {
                die "FATAL: Unknown base digit '$_'";
            }
        }
	$num ~= $s;
    }

    $num
} # pad-number

#------------------------------------------------------------------------------
# Subroutine: hex2dec
# Purpose : Convert a non-negative hexadecimal number (string) to a decimal number.
# Params  : Hexadecimal number (string), desired length (optional), suffix (optional).
# Returns : Decimal number (or string).
sub hex2dec(
    $hex where &hexadecimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:hex2dec) {

    my UInt $len = $hex.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i = 16;
    constant $base-o = 10;

    my $dec = $hex.parse-base: $base-i;
    $dec = pad-number $dec, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $dec;
} # hex2dec

#------------------------------------------------------------------------------
# Subroutine: hex2bin
# Purpose : Convert a non-negative hexadecimal number (string) to a binary string.
# Params  : Hexadecimal number (string), desired length (optional), prefix (optional), suffix (optional).
# Returns : Binary number (string).
sub hex2bin(
    $hex where &hexadecimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:hex2bin) {

    my UInt $len = $hex.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i = 16;
    constant $base-o =  2;

    # have to get decimal first
    my $dec = $hex.parse-base: $base-i;
    my $bin = $dec.base: $base-o;

    $bin = pad-number $bin, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $bin;
} # hex2bin

#------------------------------------------------------------------------------
# Subroutine: dec2hex
# Purpose : Convert a non-negative integer to a hexadecimal number (string).
# Params  : Non-negative decimal number, desired length (optional), prefix (optional), suffix (optional), lower-case (optional).
# Returns : Hexadecimal number (string).
sub dec2hex(
    $dec where &decimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:dec2hex) {

    my UInt $len = $dec.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need base of outgoing number
    constant $base-o = 16;

    my $hex = $dec.Numeric.base: $base-o;
    $hex = pad-number $hex, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $hex;
} # dec2hex

#------------------------------------------------------------------------------
# Subroutine: dec2bin
# Purpose : Convert a non-negative integer to a binary number (string).
# Params  : Non-negative decimal number, desired length (optional), prefix (optional), suffix (optional).
# Returns : Binary number (string).
sub dec2bin(
    $dec where &decimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:dec2bin) {

    my UInt $len = $dec.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need base of outgoing number
    constant $base-o = 2;

    my $bin = $dec.Numeric.base: $base-o;
    $bin = pad-number $bin, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $bin;
} # dec2bin

#------------------------------------------------------------------------------
# Subroutine: bin2bin
# Purpose : Convert a binary number (string) to a binary number with possible 
#           augmented features
# Params  : Binary number (string)
# Options : Desired length (padding with zeroes), prefix, suffix
# Returns : Binary number (or string).
sub bin2bin(
    $bin-i where &binary,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:bin2bin) {

    my UInt $len = $bin-i.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # no change of base needed
    constant $base-o = 2;

    my $bin-o = $bin-i;   
    $bin-o = pad-number $bin-o, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $bin-o;
} # bin2bin

#------------------------------------------------------------------------------
# Subroutine: oct2oct
# Purpose : Convert an octal number (string) to an octal number with 
#           possible augmented features
# Params  : Octal number (string)
# Options : Desired length (padding with zeroes), prefix, suffix
# Returns : Octal number (or string).
sub oct2oct(
    $oct-i where &octal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:oct2oct) {

    my UInt $len = $oct-i.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # no change of base needed
    constant $base-o = 8;

    my $oct-o = $oct-i;   
    $oct-o = pad-number $oct-o, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $oct-o;
} # oct2oct

#------------------------------------------------------------------------------
# Subroutine: dec2dec
# Purpose : Convert an decimal number (string) to an decimal number with 
#           possible augmented features
# Params  : Decimal number (string)
# Options : Desired length (padding with zeroes), prefix, suffix
# Returns : Digital number (or string).
sub dec2dec(
    $dec-i where &decimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:dec2dec) {

    my UInt $len = $dec-i.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # no change in base needed
    constant $base-o = 10;

    my $dec-o = $dec-i.Numeric;
    $dec-o = pad-number $dec-o, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $dec-o;
} # dec2dec

#------------------------------------------------------------------------------
# Subroutine: hex2hex
# Purpose : Convert a hexadecimal number (string) to a hexadecimal number with 
#           possible augmented features
# Params  : Hexadecimal number (string)
# Options : Desired length (padding with zeroes), prefix, suffix
# Returns : Hexadecimal number (or string).
sub hex2hex(
    $hex-i where &hexadecimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:hex2hex) {

    my UInt $len = $hex-i.chars;

    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # no change of base needed
    constant $base-o = 16;

    my $hex-o = $hex-i;   
    $hex-o = pad-number $hex-o, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $hex-o;
} # hex2hex

#------------------------------------------------------------------------------
# Subroutine: bin2dec
# Purpose : Convert a binary number (string) to a decimal number with 
#           possible augmented features
# Purpose : Convert a binary number (string) to a decimal number.
# Params  : Binary number (string)
# Options : Desired length (padding with zeroes), prefix, suffix
# Returns : Decimal number (or string).
sub bin2dec(
    $bin where &binary,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:bin2dec) {

    my UInt $len = $bin.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing numbers
    constant $base-i =  2;
    constant $base-o = 10;

    my $dec = $bin.parse-base: $base-i;
    $dec = pad-number $dec, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $dec;
} # bin2dec

#------------------------------------------------------------------------------
# Subroutine: bin2hex
# Purpose : Convert a binary number (string) to a hexadecimal number (string).
# Params  : Binary number (string), desired length (optional), prefix (optional), suffix (optional), lower-case (optional).
# Returns : Hexadecimal number (string).
sub bin2hex(
    $bin where &binary,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:bin2hex) {

    my UInt $len = $bin.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i =  2;
    constant $base-o = 16;

    # need decimal intermediary
    #my $dec = $bin.parse-base: $base-i;
    my $dec = parse-base $bin, $base-i;
    my $hex = $dec.base: $base-o;
    $hex = pad-number $hex, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $hex;
} # bin2hex

#------------------------------------------------------------------------------
# Subroutine: oct2bin
# Purpose : Convert an octal number (string) to a binary number (string).
# Params  : Octal number (string), desired length (optional), prefix (optional), suffix (optional).
# Returns : Binary number (string).
sub oct2bin(
    $oct where &octal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:oct2bin) {

    my UInt $len = $oct.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i = 8;
    constant $base-o = 2;

    # need decimal intermediary
    my $dec = $oct.parse-base: $base-i;
    my $bin = $dec.base: $base-o;
    $bin = pad-number $bin, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $bin;
} # oct2bin

#------------------------------------------------------------------------------
# Subroutine: oct2hex
# Purpose : Convert an octal number (string) to a hexadecimal number (string).
# Params  : Octal number (string), desired length (optional), prefix (optional), suffix (optional), lower-case (optional).
# Returns : Hexadecimal number (string).
sub oct2hex(
    $oct where &octal, 
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:oct2hex) {

    my UInt $len = $oct.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i =  8;
    constant $base-o = 16;

    # need decimal intermediary
    my $dec = $oct.parse-base: $base-i;
    my $hex = $dec.base: $base-o;
    $hex = pad-number $hex, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $hex;
} # oct2hex

#------------------------------------------------------------------------------
# Subroutine: oct2dec
# Purpose : Convert an octal number (string) to a decimal number.
# Params  : Octal number (string), desired length (optional), suffix (optional).
# Returns : Decimal number (or string).
sub oct2dec(
    $oct where &octal, 
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:oct2dec) {

    my UInt $len = $oct.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i =  8;
    constant $base-o = 10;

    my $dec = $oct.parse-base: $base-i;
    $dec = pad-number $dec, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $dec;
} # oct2dec

#------------------------------------------------------------------------------
# Subroutine: bin2oct
# Purpose : Convert a binary number (string) to an octal number (string).
# Params  : Binary number (string), desired length (optional), prefix (optional), suffix (optional).
# Returns : Octal number (string).
sub bin2oct(
    $bin where &binary,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:bin2oct) {

    my UInt $len = $bin.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i = 2;
    constant $base-o = 8;

    # need decimal intermediary
    my $dec = $bin.parse-base: $base-i;
    my $oct = $dec.base: $base-o;

    $oct = pad-number $oct, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $oct;
} # bin2oct

#------------------------------------------------------------------------------
# Subroutine: dec2oct
# Purpose : Convert a non-negative integer to an octal number (string).
# Params  : Decimal number, desired length (optional), prefix (optional), suffix (optional).
# Returns : Octal number (string).
sub dec2oct(
    $dec where &decimal,
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:dec2oct) {

    my UInt $len = $dec.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need base of outgoing number
    constant $base-o =  8;

    my $oct = $dec.Numeric.base: $base-o;
    $oct = pad-number $oct, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $oct;
} # dec2oct

#------------------------------------------------------------------------------
# Subroutine: hex2oct
# Purpose : Convert a hexadecimal number (string) to an octal number (string).
# Params  : Hexadecimal number (string), desired length (optional), prefix (optional), suffix (optional).
# Returns : Octal number (string).
sub hex2oct(
    $hex where &hexadecimal, 
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Str
    ) is export(:hex2oct) {

    my UInt $len = $hex.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # need bases of incoming and outgoing number
    constant $base-i = 16;
    constant $base-o =  8;

    # need decimal intermediary
    my $dec = $hex.parse-base: $base-i;
    my $oct = $dec.base: $base-o;
    $oct = pad-number $oct, $base-o, :$prefix, :$suffix, :$length, :$LC;

    $oct;
} # hex2oct

#------------------------------------------------------------------------------
# Subroutine: rebase
# Purpose : Convert any number (integer or string) and base (2..62) to a number in another base (2..62).
# Params  : Number (string), desired length (optional), prefix (optional), suffix (optional), suffix (optional), lower-case (optional).
# Returns : Desired number (decimal or string) in the desired base.
sub rebase(
    $num-i,
    $base-i where &all-bases,
    $base-o where &all-bases,
    # optional args
    :$length is copy,
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:baseM2baseN) {

    my UInt $len = $num-i.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

    # make sure incoming number is in the right base
    my $bset = create-base-set $base-i;
    my $nset = create-set $num-i;

    unless $nset (<=) $bset {
        die "FATAL: Incoming number '$num-i' in sub 'rebase' is\
              not a member of base '$base-i'.";
    }

    # check for same bases
    if $base-i eq $base-o {
        say "WARNING: Both bases are the same ($base-i), no conversion necessary."
    }

    # check for known bases, eliminate any prefixes
    my ($bi, $bo);
    {
        when $base-i == 2  {
	    $bi = 'bin';
	    $num-i ~~ s:i/^0b//;
	}
        when $base-i == 8  {
	    $bi = 'oct';
	    $num-i ~~ s:i/^0o//;
	}
        when $base-i == 10  {
	    $bi = 'dec';
	    $num-i ~~ s:i/^0d//;
	}
        when $base-i == 16 {
	    $bi = 'hex';
	    $num-i ~~ s:i/^0x//;
	}
    }
    {
        when $base-o == 2  { $bo = 'bin' }
        when $base-o == 8  { $bo = 'oct' }
        when $base-o == 10 { $bo = 'dec' }
        when $base-o == 16 { $bo = 'hex' }
    }

    if $bi and $bo and (0 or $debug) {
        note "\nDEBUG Use function '{$bi}2{$bo}' instead for an easier interface.";
    }

    # treatment varies if in or out base is decimal
    my $num-o;
    if $base-i == 10 {
	if $base-o < 37 {
            $num-o = $num-i.base: $base-o;
	}
	else {
            $num-o = from-dec-to-b37-b62 $num-i, $base-o;
	}
    }
    elsif $base-o == 10 {
	if $base-i < 37 {
            $num-o = parse-base $num-i, $base-i;
	}
	else {
	    $num-o = to-dec-from-b37-b62 $num-i, $base-i;
	}
    }
    elsif ($base-i < 37) and ($base-o < 37) {
        # need decimal as intermediary
        my $dec = $num-i.parse-base: $base-i;
        $num-o  = $dec.base: $base-o;
    }
    else {
        # need decimal as intermediary
	my $dec;
	if $base-i < 37 {
            $dec = $num-i.parse-base: $base-i;
	}
	else {
	    $dec = to-dec-from-b37-b62 $num-i, $base-i;
	}

        # then convert to desired base
	if $base-o < 37 {
            $num-o = $dec.base: $base-o;
	}
	else {
            $num-o = from-dec-to-b37-b62 $dec, $base-o;
	}
    }

    # Finally, pad the number, make upper-case, and add prefix or suffix as
    # appropriate
    if $base-o == 2 || $base-o == 8 || $base-o == 10 {
        $num-o = pad-number $num-o, $base-o, :$prefix, :$suffix, :$length, :$LC;
    }
    elsif $base-o == 16 {
        $num-o = pad-number $num-o, $base-o, :$prefix, :$suffix, :$length, :$LC;
    }
    elsif (10 < $base-o < 37) {
	# case insensitive bases
        $num-o = pad-number $num-o, $base-o, :$prefix, :$suffix, :$length, :$LC;
    }
    elsif (1 < $base-o < 11) {
	# case N/A bases
        $num-o = pad-number $num-o, $base-o, :$prefix, :$suffix, :$length, :$LC;
    }
    else {
	# case SENSITIVE bases
        $num-o = pad-number $num-o, $base-o, :$prefix, :$suffix, :$length, :$LC;
    }

    $num-o;
} # rebase

sub to-dec-from-b37-b62(
    $num,
    UInt $base-i where ( 36 < $base-i < 63 ),
    # optional args
    :$length is copy, # for padding
    :$prefix is copy,
    :$suffix is copy,
    :$LC is copy,
    :$debug,
    --> Cool
    ) is export(:to-dec-from-b37-b62) {

    my UInt $len = $num.chars;
    $length = 0 if not $length.defined;
    $prefix = 0 if not $prefix.defined;
    $suffix = 0 if not $suffix.defined;
    $LC     = 0 if not $LC.defined;

=begin comment
# see simple algorithm for base to dec:

Let's say you have a number

  10121 in base 3

and you want to know what it is in base 10.  Well, in base three the
place values [from the highest] are

   4   3  2  1  0 <= digit place (position)
  81, 27, 9, 3, 1 <= value: digit x base ** place

so we have 1 x 81 + 0 x 27 + 1 x 9 + 2 x 3 + 1 x 1

  81 + 0 + 9 + 6 + 1 = 97

that is how it works.  You just take the number, figure out the place
values, and then add them together to get the answer.  Now, let's do
one the other way.

45 in base ten (that is the normal one.) Let's convert it to base
five.

Well, in base five the place values will be 125, 25, 5, 1

We won't have any 125's but we will have one 25. Then we will have 20
left.  That is four 5's, so in base five our number will be 140.

Hope that makes sense.  If you don't see a formula, try to work out a
bunch more examples and they should get easier.

-Doctor Ethan,  The Math Forum

=end comment

    # reverse the digits (chars) of the input number
    my @num'r = $num.comb.reverse;
    my $place = $num.chars;

    my $dec = 0;
    
    for @num'r -> $char  {
	--$place; # first place is num chars - 1
        if $char ~~ /:i z/ {
            note "DEBUG: input char is '$char', place = $place" if $DEBUG;
        }
	# need to convert the digit to dec first
	my $digit-val = %digit2dec{$char};
        if $char ~~ /:i z/ {
            note "DEBUG: input char is '$char', digit val