Balanced Ternary Arithmetic
April 14, 2015
We begin by writing a function that converts a decimal integer to ternary. As we learned in the previous exercise, (quotient n 3)
does the wrong thing with negative n, so we use floor
and the / division operator:
(define (ternary n) (let loop ((n n) (ts (list))) (if (zero? n) (normalize ts) (case (modulo n 3) ; (quotient n 3) does the wrong thing if negative n ((0) (loop (floor (/ n 3)) (cons 0 ts))) ((1) (loop (floor (/ n 3)) (cons 1 ts))) ((2) (loop (floor (/ (+ n 1) 3)) (cons -1 ts)))))))
Turning a ternary number back into decimal works left-to-right through the trits, multiplying by 3 as it goes:
(define (decimal ts) (fold-left (lambda (x y) (+ x x x y)) 0 ts)) > (ternary 523) (1 -1 0 1 1 0 1) > (ternary -476) (-1 1 0 0 1 0 1)
Some people like to write ternary numbers as a string of trits with a plus sign, minus sign and zero for the 1, -1 and 0 trits:
(define (to-string ts) (let ((digits '((0 . #) (1 . #\+) (-1 . #\-)))) (apply string (map (lambda (d) (cdr (assoc d digits))) ts)))) (define (from-string str) (let ((chars '((# . 0) (#\+ . 1) (#\- . -1)))) (map (lambda (c) (cdr (assoc c chars))) (string->list str)))) > (to-string (ternary 523)) "+-0++0+" > (to-string (ternary -476)) "-+00+0+"
Now the fun starts. Two ternary numbers are added trit-by-trit, from right-to-left, carrying as necessary. Here’s the addition table for two trits:
-1 0 1 ---- ---- ---- -1 -1 1 -1 0 0 -1 0 1 1 0 1 1 -1
So to add two trits plus a carry, we first add the two trits, then add the carry in a second operation. Or, we could make a three-dimensional addition table and calculate the trit and the carry in a single step. The third possibility, which we choose, is to add the three trits using decimal arithmetic, then convert using the ternary
function that we already wrote, and finally split the carry from the trit:
(define (plus xs ys) (define (add x y c) (let ((ts (ternary (+ x y c)))) (if (null? (cdr ts)) (values 0 (car ts)) (values (car ts) (cadr ts))))) (let loop ((xs (reverse xs)) (ys (reverse ys)) (zs (list)) (carry 0)) (cond ((and (null? xs) (null? ys)) (normalize (cons carry zs))) ((null? xs) (let-values (((c s) (add 0 (car ys) carry))) (loop xs (cdr ys) (cons s zs) c))) ((null? ys) (let-values (((c s) (add (car xs) 0 carry))) (loop (cdr xs) ys (cons s zs) c))) (else (let-values (((c s) (add (car xs) (car ys) carry))) (loop (cdr xs) (cdr ys) (cons s zs) c)))))) > (plus (ternary 523) (ternary 476)) (1 1 0 1 0 0 0) > (decimal (plus (ternary 523) (ternary 476))) 999
Negating a number is easy; just multiply each trit, individually, by -1:
(define (negate xs) (map (lambda (d) (- d)) xs)) > (ternary 476) (1 -1 0 0 -1 0 -1) > (negate (ternary 476)) (-1 1 0 0 1 0 1)
Multiplication is easy: work through the multiplier trit-by-trit, working right to left, at each step shifting in one more zero on the right; if the current trit is 1, add the multiplicand (plus shifted zeros) to the accumulating product, if the current trit is 0, do nothing, and if the current trit is -1, add the negative of the multiplicand (plus shifted zeros) to the accumulating product:
(define (times xs ys) (let loop ((xs (reverse xs)) (shift (list)) (prod (list))) (if (null? xs) (normalize prod) (loop (cdr xs) (cons 0 shift) (case (car xs) ((-1) (plus (negate (append ys shift)) prod)) ((0) prod) ((1) (plus (append ys shift) prod))))))) > (times (ternary 523) (ternary 476)) (1 1 1 -1 0 -1 1 1 1 1 0 -1) > (decimal (times (ternary 523) (ternary 476))) 248948
We’ll stop there. If you want to continue, you could do division using the grade-school algorithm, or you could write a library for working with fixed-point real numbers. Although binary computation won, in the early days of computing balanced ternary computation was considered as an alternative, because it is more efficient (the number of digits times the size of the “alphabet” is less for ternary than binary, leading to smaller, faster computation) and because comparisons are naturally three-valued (less than, equal, greater than), and there are still some arguments in its favor, not to mention some really neat trit-twiddling hacks. If you are interested, Google can tell you more.
You can run the program at http://ideone.com/97fr7R. Note that the ternary
function changes because Chicken Scheme assumes that the division operator / indicates a floating-point number.
In Python.
We can do this in a similar way to the previous problem, here most of the work is done by the “adjust” function which puts a single digit into the correct range and adjusts the carry accordingly. I’ve added a separate “normalize” function, and we can simplify addition assuming the inputs are normalized:
That mult function is wrong – it throws away the final carry from each addition. Here’s a better version (and a better test program):
See this web site for a library of C (works in C++ too) code allowing use of binary-coded ternary data representations, coding each ternary trit as two consecutive binary bits.
— http://www.cs.uiowa.edu/~jones/ternary/libtern.shtml
This does a ternary add in about 10 times the time it takes for a corresponding integer add, so it’s way more efficient than the digit serial code shown above. There’s support there for both balanced ternary (trit values -1 0 +1) and unsigned ternary (trit values 0 1 2).