2015-11-07 Converting numbers to strings in a human-friendly way

Converting an Elisp number to a string (so that it can be e.g. displayed) is a surprisingly tricky thing to do. While (format "%s" number) seems a reasonable way, sometimes it’s not. In one application I was writing I wanted to display integer numbers as integer numbers, even if they happen to be floating point numbers with zero fractional part.

While this seems easy enough in theory, there are some complications. One of these is the question: what is an integer number? Assuming that I want, say, 2 decimal points, should I consider 5.001 an integer (and display it as 5) or not (and display it as 5.00)? While the second one seems better, actually it’s not. In my use case, numbers were always floating point, and they were usually results of some computation; hence even numbers that were meant to be integers fell into the 5.001 (or rather 5.000000000004 etc.) category. This means that (= number (floor number)) won’t help me. (Notice that unlike the usual equality predicates, = considers 5 and 5.0 to be equal! The same goes for equalp (defined in cl-extra as cl-equalp and aliased in cl, which basically means you should (require 'cl-lib) to use it), which in case of numbers just calls =.)

After some consideration, I decided to go the simple route, which might not be elegant, but works very well. What I do is converting to the required number of decimal places using format, and then I trim the trailing zeros (and the trailing decimal point, if there is nothing but zeros after it) using string operations.

Note that one of the reasons this approach works is that Emacs does not have a proper support for localization; if it had it, I wouldn’t know whether I have a decimal point or a decimal comma. In such a case, a “proper”, mathematical solution would be needed. That would require e.g. rounding, then checking for equality, and converting to an integer if needed. More complication for no apparent gain. (Interestingly, this more complicated mechanism might actually be faster, because strings are hard (and sometimes inefficient), but establishing that would require experiments I’m too lazy to conduct at the moment. If I had thousands of numbers to convert in a batch, I’d definitely do it, and I wouldn’t be surprised if the string version were slower, if only because of garbage collection.)

Another problem was deciding how much decimal places I need. For that, I just decreed that for smaller numbers I’ll use a maximum of two decimal places and for larger ones – a maximum of one decimal place. I could make it configurable, but I didn’t need to, so I didn’t bother.

The last thing I needed was padding. Since the numbers I generate may be displayed in a table, I need a possibility to specify a width for the generated representation. Once again I chose simplicity over correctness (actually, I knew that no number in my use case will be large enough to break things) and decided that it is enough to specify the minimum width (which is just a parameter in format specification) and not bother about numbers not fitting in that many characters. Of course, having a variable width in my format spec meant that I had to generate that spec somehow; unsurprisingly, I used format for that, leaving me with a format inside a format. Yo dawg, I heard you like format calls…

And finally, here’s the complete code. Feel free to adapt it to your needs.

(defun number-to-human-string (number &optional width)
  "Convert NUMBER to a human-friendly form, at least WIDTH characters.
If NUMBER is greater than 10, use one decimal place.  Otherwise,
use two.  Trim any non-significant trailing zeros and the decimal
point if needed."
  (let ((str (replace-regexp-in-string
	      "\\.?0+$" ""
	      (format (cond
		       ((> number 10) "%.1f")
		       (t "%.2f"))
		      number))))
    (if width (format (format "%%%ds" width) str) str)))

CategoryEnglish, CategoryBlog, CategoryEmacs