Imaginary base numbers are a non-standard positional numeral system which uses an imaginary number as its radix. The most common is quater-imaginary with radix 2i.

The quater-imaginary numeral system was first proposed by Donald Knuth in 1955 as a submission for a high school science talent search. [Ref.]

Other imaginary bases are possible too but are not as widely discussed and aren't specifically named.

Task: Write a set of procedures (functions, subroutines, however they are referred to in your language) to convert base 10 numbers to an imaginary base and back.

At a minimum, support quater-imaginary (base 2i).

For extra kudos, support positive or negative bases 2i through 6i (or higher).

As a stretch goal, support converting non-integer numbers ( E.G. 227.65625+10.859375i ) to an imaginary base.

See Wikipedia: Quater-imaginary_base for more details.

For reference, here are some some decimal and complex numbers converted to quater-imaginary.






Base 10
Base 2i


1
 1


2
 2


3
 3


4
 10300


5
 10301


6
 10302


7
 10303


8
 10200


9
 10201


10
 10202


11
 10203


12
 10100


13
 10101


14
 10102


15
 10103


16
 10000






Base 10
Base 2i


−1
 103


−2
 102


−3
 101


−4
 100


−5
 203


−6
 202


−7
 201


−8
 200


−9
 303


−10
 302


−11
 301


−12
 300


−13
 1030003


−14
 1030002


−15
 1030001


−16
 1030000






Base 10
Base 2i


1i
10.2


2i
10.0


3i
20.2


4i
20.0


5i
30.2


6i
30.0


7i
103000.2


8i
103000.0


9i
103010.2


10i
103010.0


11i
103020.2


12i
103020.0


13i
103030.2


14i
103030.0


15i
102000.2


16i
102000.0






Base 10
Base 2i


−1i
0.2


−2i
1030.0


−3i
1030.2


−4i
1020.0


−5i
1020.2


−6i
1010.0


−7i
1010.2


−8i
1000.0


−9i
1000.2


−10i
2030.0


−11i
2030.2


−12i
2020.0


−13i
2020.2


−14i
2010.0


−15i
2010.2


−16i
2000.0





use strict;
use warnings;
use feature 'say';
 
use Math::Complex;
use List::AllUtils qw(sum mesh);
use ntheory qw<todigitstring fromdigits>;
 
sub zip {
    my($a,$b) = @_;
    my($la, $lb) = (length $a, length $b);
    my $l = '0' x abs $la - $lb;
    $a .= $l if $la < $lb;
    $b .= $l if $lb < $la;
    (join('', mesh(@{[split('',$a),]}, @{[split('',$b),]})) =~ s/0+$//r) or 0;
}
 
sub base_i {
    my($num,$radix,$precision) = @_;
    die unless $radix > -37 and $radix < -1;
    return '0' unless $num;
    my $value  = $num;
    my $result = '';
    my $place  = 0;
    my $upper_bound = 1 / (-$radix + 1);
    my $lower_bound = $radix * $upper_bound;
 
    $value = $num / $radix ** ++$place until $lower_bound <= $value and $value < $upper_bound;
 
    while (($value or $place > 0) and $place > $precision) {
        my $digit = int $radix * $value - $lower_bound;
        $value    =  $radix * $value - $digit;
        $result  .= '.' unless $place or not index($result, '.');
        $result  .= $digit == -$radix ? todigitstring($digit-1, -$radix) . '0' : (todigitstring($digit, -$radix) or '0');
        $place--;
    }
    $result
}
 
sub base_c {
    my($num, $radix, $precision) = @_;
    die "Base $radix out of range" unless
        (-6 <= $radix->Im or $radix->Im <= -2) or (2 <= $radix->Im or $radix->Im <= 6);
    my ($re,$im);
    defined $num->Im ? ($re, $im) = ($num->Re, $num->Im) : $re = $num;
    my ($re_wh, $re_fr) = split /\./, base_i(  $re,               -1 * int($radix->Im**2), $precision);
    my ($im_wh, $im_fr) = split /\./, base_i( ($im/($radix->Im)), -1 * int($radix->Im**2), $precision);
    $_ //= '' for $re_fr, $im_fr;
 
    my $whole = reverse zip scalar reverse($re_wh), scalar reverse($im_wh);
    my $fraction = zip $im_fr, $re_fr;
    $fraction eq 0 ? "$whole" : "$whole.$fraction"
}
 
sub parse_base {
    my($str, $radix) = @_;
    return -1 * parse_base( substr($str,1), $radix) if substr($str,0,1) eq '-';
    my($whole, $frac) = split /\./, $str;
    my $fraction = 0;
    my $k = 0;
    $fraction = sum map { (fromdigits($_, int $radix->Im**2) || 0) * $radix ** -($k++ +1) } split '', $frac
        if $frac;
    $k = 0;
    $fraction + sum map { (fromdigits($_, int $radix->Im**2) || 0) * $radix ** $k++  } reverse split '', $whole;
}
 
for (
    [  0*i,  2*i], [1+0*i,  2*i], [5+0*i,      2*i], [ -13+0*i, 2*i],
    [  9*i,  2*i], [ -3*i,  2*i], [7.75-7.5*i, 2*i], [0.25+0*i, 2*i],
    [5+5*i,  2*i], [5+5*i,  3*i], [5+5*i,  4*i], [5+5*i,  5*i], [5+5*i,  6*i],
    [5+5*i, -2*i], [5+5*i, -3*i], [5+5*i, -4*i], [5+5*i, -5*i], [5+5*i, -6*i]
) {
    my($v,$r) = @$_;
    my $ibase = base_c($v, $r, -6);
    my $rt = cplx parse_base($ibase, $r);
    $rt->display_format('format' => '%.2f');
    printf "base(%3s): %10s  => %9s  => %13s\n", $r, $v, $ibase, $rt;
}
 
say '';
say 'base( 6i): 31432.6219135802-2898.5266203704*i => ' .
         base_c(31432.6219135802-2898.5266203704*i, 0+6*i, -3);