About me

About me

Feeds

RSS feed

Improving truncate

28th August 2025

As an example of simple recursion I recently wrote a function to reverse the digits of an integer:

(defun reversedigits (n &optional (a 0))
  (cond
   ((zerop n) a)
   (t (reversedigits
       (truncate n 10)
       (+ (* 10 a) (mod n 10))))))

I was a bit shocked when, for a particular input, it gave the wrong result:

> (reversedigits 123456789)
997654321

I checked on an implementation of Common Lisp, and it gave the expected answer:

CL-USER 1 > (reversedigits 123456789)
987654321

I couldn't imagine what could be causing this problem, but it turned out to be less trivial to solve than I expected.

The problem

In my uLisp interpreter I was evaluating:

(truncate n m)

by evaluating:

(truncate (/ n m))

which seemed perfectly reasonable. However:

(/ 123456789 10)

when expressed as a 32-bit floating-point number doesn't have the 8 digits of precision to give the correct answer of 12345678. You can demonstrate this in Common Lisp by evaluating:

CL-USER 1 > (truncate 12345678.9)
12345679 

So how does Common Lisp manage to get the following correct:

CL-USER 1 > (truncate 123456789 10)
12345678

The answer is that it stores (/ 123456789 10) as the exact rational 123456789/10, rather than as a 32-bit floating-point number which would lose precision [1].

uLisp is a compact subset of Common Lisp and doesn't include rational numbers, so what should truncate do?

The solution

I concluded that it should give the most accurate answer possible for the parameters its given. I can't fix the case of:

(truncate (/ 123456789 10))

but in the case of:

(truncate 123456789 10)

where the parameters are 32-bit integers, it's possible to return a precise result.

Implementing ceiling

It affects floor and ceiling too, and they are slightly more complicated, so here's the new algorithm to implement ceiling using the C function ceil(). Note that the C function ceil() returns a floating-point argument:

  • If there is only one argument: if it's an integer return it, otherwise return (int)ceil(arg1).
  • If there are two arguments and they are not both integers return (int)ceil(arg1/arg2).
  • If there are two arguments and they are both integers:
    • Calculate the integer quotient and remainder: quo = arg1/arg2, and rem = arg1-(quo*arg2).
    • If arg1 and arg2 are different signs or rem=0 return quo; otherwise return quo+1.

The implementation of floor and truncate is similar.

Latest release

The latest releases of uLisp (ARM uLisp 4.8e and ESP uLisp 4.8f) implement new versions of truncate, floor, and ceiling; all three functions now give exact results for two integer parameters.

So now reversedigits gives the correct answer:

> (reversedigits 123456789)
987654321

  1. ^ Thank you to Rainer Joswig for a discussion that helped me understand this.

blog comments powered by Disqus