Playing with rationals

Richard Suchenwirth 2005-03-22 - Rational numbers, a.k.a. fractions (see Fraction math), can be thought of as pairs of integers {numerator denominator}, such that their "real" numerical value is numerator/denominator (and not in integer nor "double" division!). They can be more precise than any "float" or "double" numbers on computers, as those can't exactly represent any fractions whose denominator isn't a power of 2 - consider 1/3 which can not at any precision be exactly represented as floating-point number to base 2, nor as decimal fraction (base 10), even if bignum. Reading in SICP once more, I wanted to play with rationals in Tcl again - so here's another evening fun project.

An obvious string representation of a rational is of course "n/d". The following "constructor" does that, plus it normalizes the signs, reduces to lowest terms, and returns just the integer n if d==1:

 proc rat {n d} {
   if {!$d} {error "denominator can't be 0"}
   if {$d<0} {set n [- $n]; set d [- $d]}
   set g [gcd $n $d]
   set n [/ $n $g]
   set d [/ $d $g]
   expr {$d==1? $n: "$n/$d" }
 }

Conversely, this "deconstructor" splits zero or more rational or integer strings into num and den variables, such that [ratsplit 1/3 a b] assigns 1 to a and 3 to b:

 proc ratsplit args {
    foreach {r _n _d} $args {
       upvar 1 $_n n  $_d d
       foreach {n d} [split $r /] break
       if {$d eq ""} {set d 1}
    }
 }
#-- Four-species math on "rats":
 proc rat+ {r s} {
    ratsplit $r a b $s c d
    rat [+ [* $a $d] [* $c $b]] [* $b $d]
 }
 proc rat- {r s} {
    ratsplit $r a b $s c d
    rat [- [* $a $d] [* $c $b]] [* $b $d]
 }
 proc rat* {r s} {
    ratsplit $r a b $s c d
    rat [* $a $c] [* $b $d]
 }
 proc rat/ {r s} {
    ratsplit $r a b $s c d
    rat [* $a $d] [* $b $c]
 }

Arithmetical helper functions can be wrapped with func if they only consist of one call of expr:

 proc func {name argl body} {proc $name $argl [list expr $body]}

#-- [Greatest common denominator]:
 func gcd {u v} {$u? [gcd [% $v $u] $u]: abs($v)}

#-- Binary [expr] operators exported:
 foreach op {+ * / %} {func $op {a b} \$a$op\$b}

#-- "-" can have 1 or 2 operands:
 func - {a {b ""}} {$b eq ""? -$a: $a-$b}

#-- a little tester reports the unexpected:
 proc ? {cmd expected} {
    catch {uplevel 1 $cmd} res
    if {$res ne $expected} {puts "$cmd -> $res, expected $expected"}
 }
#-- The test suite should silently pass when this file is [source]d:
 ? {rat 42 6} 7
 ? {rat 1 -2} -1/2
 ? {rat -1 -2} 1/2
 ? {rat 1 0} "denominator can't be 0"
 ? {rat+ 1/3 1/3} 2/3
 ? {rat+ 1/2 1/2} 1
 ? {rat+ 1/2 1/3} 5/6
 ? {rat+ 1 1/2}    3/2
 ? {rat- 1/2 1/8} 3/8
 ? {rat- 1/2 1/-8} 5/8
 ? {rat- 1/7 1/7} 0
 ? {rat* 1/2 1/2} 1/4
 ? {rat/ 1/4 1/4} 1
 ? {rat/ 4 -6} -2/3

See also Fraction Math