Tricky catch

KBK (11 January 2002) - Having seen RS's fraction procedures in Bag of Algorithms, I saw a challenge: write a procedure that comes as close as possible to converting ANY float to a fraction. This actually isn't as hard as might seem.

The following code was my first attempt, which stayed on the Wiki for over a year. It does the job reasonably well, albeit slowly. (There's a faster version over in Fraction math.) One thing that might interest Tcl'ers about it is the way that catch is used as a control structure. The throw inside the quotient_rep procedure is used as a convenient way of breaking out of the 'for' and 'while' loops simultaneously.

It's interesting to walk through the code when quotient_rep is given an integer as a parameter. It gets a division by zero, which gets caught and handled. In fact, if integer overflow threw an error, there would be no need for any termination test in the while { 1 } loop at all!


 # Convert a floating point number to a pair of integers
 # that are the numerator and denominator of its fractional
 # representation.  The optional arg "maxint" is the largest
 # integer that will appear in the fraction, e.g.,
 # [quotient_rep 3.14159 1000] -> {355 113}

 proc quotient_rep { num { maxint 2147483647 } } {

    set i [expr { int( $num ) }]
    set f [expr { $num - $i }]
    set retval [list $i 1]
    set clist {}

    catch {
        while { 1 } {
            set clist [linsert $clist 0 $i]
            set n 0
            set d 1
            foreach c $clist {
                if { ( 1.0 * $c * $d + $n ) > $maxint } {
                    throw "break two loops at once!"
                } else {
                    set newd [expr { $c * $d + $n }]
                    set n $d
                    set d $newd
                }
            }
            set retval [list $d $n]

            # the following calculations can overflow or
            # zero divide, but the outer catch does the
            # right thing!

            set num [expr { 1.0 / $f }]
            set i [expr { int( $num ) }]
            set f [expr { $num - $i }]
        }
    }
    return $retval
 }

(The Zen joke that the [throw] command is undefined was intentional)