6

I have to deal with long integers frequently, typically 9+ digits, and all of the digits matter (so scientific notation doesn't help me. Is there a minor mode that helps make it easier to visually scan such numbers? I encounter these numbers both in files and in shell buffers (bash, ipython, matlab).

As an example, I would like numbers with more than 6 digits to be displayed in a way to visually demarcate the number, as we typically do with commas. For example: 10987654321.

Drew
  • 75,699
  • 9
  • 109
  • 225
petermao
  • 83
  • 4
  • I found a few existing options: [num3-mode](https://elpa.gnu.org/packages/num3-mode.html) on gnu-elpa and [digit-groups](https://github.com/adamsmd/digit-groups/commits/master) on melpa. Of those two, I prefer `num3-mode`, although I have also benefitted from @D-Gillis's code below. – petermao Jul 30 '22 at 07:17

1 Answers1

6

Rainbow-number-mode example I did some searching and surprisingly couldn't find any existing minor modes that seemed to do this. I thought it would be a fun little project, however, so I've written a minimal minor mode (rainbow-numbers-mode) that does this and included it below.

As it turns out, the only reasonable way to programmatically set text properties for something like this is to use Font Lock mode. This turned out to be trickier than I had anticipated since Font Lock mode wants to find search matches and then apply the same face to all matches of this same type before moving on to the next type of match. This is in stark contrast with the natural way to handle grouping long numbers by parsing to the end of one and then working backward to set the faces for each grouping before moving to the next number. (By grouping, I mean a group of 1 to 3 numbers that would be found before the first delimiter of a written integer, after the last delimiter, or between delimiters - e.g. in 12,456,789, "12", "456", and "789" are groupings.)

The easiest way to handle this is to divide all numbers into upto n groups of groupings and to create a matching function for each grouping i (modulo n), counting from the rightmost grouping. This allows groupings to be consistently colored no matter how long the number is (e.g. the least significant three digits are always the same color). We can then feed these matching functions into font-lock-add-keywords along with our desired face for each grouping.

After this code is loaded in (see the caveat below about lexical-binding), it can be run with rainbow-numbers-mode. Running rainbow-numbers-mode again will turn off this syntax highlighting. This mode only affects the syntax highlighting in the current buffer.

By default, it uses 6 different faces. The ones I picked don't have the most ideal levels of contrast, but I wanted to choose default faces that are included in all Emacs installations. These can be changed fairly easily in the source. It could also easily be adapted to use a different number of faces.

Some caveats:

  • This is best used when saved in a file and then loaded in. If you try to run this code interactively rather than loading it in a file, you will need to run (setq lexical-binding t) in the buffer in which you are evaluating this code before you evaluate it. You will also need to evaluate each of the s-expressions individually (instead of doing something like eval-buffer). This is necessary to allow the higher order function to properly produce all of the grouping-matching functions.
  • The delimiters this uses for determining what is a number are a little hack-y and may not cover all cases. This should work on non-integers, but only to the left of the decimal place. It also requires a number to have a space before it or be at the beginning of the line.
  • I tested that this worked in Emacs 26.3 without an init file loaded, but there's no guarantee that these font lock properties won't be overridden by other modes.
  • If you try to run it in Fundamental mode, you may need to manually re-fontify the buffer (by using font-lock-fontify-buffer) after modifying it to get the changes in highlighting to appear. However, I did not have this problem when trying it in several other major modes (Lisp Interaction mode, Python mode, C mode). I'll look into this issue and see if I can find a fix for it.

rainbow-numbers-mode.el

;;; -*- lexical-binding: t -*-

(setq rainbow-numbers-number-grouping-length 6)

(defun rainbow-numbers--get-number-of-groupings (start-pos end-pos)
  (let ((length (- end-pos start-pos)))
    (if (= (mod length 3) 0)
        (/ length 3)
      (+ 1 (/ length 3)))))

(defun rainbow-numbers--char-is-number (c)
  (and (not (null c))
       (>= c ?0)
       (<= c ?9)))

(defun rainbow-numbers--number-match-maybe-jump-to-number (limit)
  "Jump to number if one exists, unless already in a
number. Return non-nil if a number is found, otherwise return nil."
  (if (and (rainbow-numbers--char-is-number (char-before))
           (rainbow-numbers--char-is-number (char-after)))
      t
    (let ((word-found (re-search-forward
                       "\\(^\\|[[:blank:]]\\)[0-9]+\\($\\|[[:blank:]]\\|[\\.,]\\)"
                       limit
                       t)))
      (backward-word)
      word-found)))

(defun rainbow-numbers--get-nearest-k-grouping (start-pos end-pos k)
  "Return start and end of nearest k-parity grouping and move point
  to after this grouping."
  (let* ((grouping-num (rainbow-numbers--get-number-of-groupings start-pos end-pos))
        (out-of-bounds (rainbow-numbers--forward-k-groupings start-pos end-pos k grouping-num)))
    (if (null out-of-bounds)
        '()
      (cons (if (< (point) (+ start-pos 3))
                start-pos
              (- (point) 3))
            (point)))))

(defun rainbow-numbers--forward-k-groupings (start-pos end-pos k cur-grouping-num)
  "Goes to end of number if out of bounds.
Needs to order k-groupings from right to left. Currently doing the opposite.
Indexing issues here"
  (let* ((k2 (+ (mod (- cur-grouping-num 1 k) rainbow-numbers-number-grouping-length)
                1))
         (offset (mod (- end-pos start-pos) 3))
         (new-pos start-pos))
    (unless (= offset 0)
      (setq k2 (- k2 1))
      (setq new-pos (+ new-pos offset)))
    (let ((step-forward (+ new-pos (* k2 3))))
      (goto-char (min end-pos step-forward))
      (if (<= step-forward end-pos)
          t
        '()))))
    

(defun rainbow-numbers--set-match (start-pos end-pos)
  "This is the lazy way of setting the match."
  (save-excursion
    (goto-char start-pos)
    (re-search-forward (concat ".\\{"
                               (number-to-string (- end-pos start-pos))
                               "\\}"))))

(defun make-rainbow-numbers-number-match (k)
  (lambda (limit)
    (let ((keep-running t)
        (output '()))
    (while keep-running
      (let ((in-number (rainbow-numbers--number-match-maybe-jump-to-number limit)))
        (if (null in-number)
            (setq keep-running '())
          (let* ((end-pos (save-excursion
                                (forward-word)
                                (point)))
                 (start-pos (point))
                 (nearest-grouping (rainbow-numbers--get-nearest-k-grouping start-pos end-pos k)))
            (unless (null nearest-grouping)
              (setq keep-running '())
              (rainbow-numbers--set-match (car nearest-grouping)
                            (cdr nearest-grouping))
              (setq output t))))))
    output)))

(defun rainbow-numbers-number-match-0 (limit)
  (funcall (make-rainbow-numbers-number-match 0) limit))

(defun rainbow-numbers-number-match-1 (limit)
  (funcall (make-rainbow-numbers-number-match 1) limit))

(defun rainbow-numbers-number-match-2 (limit)
  (funcall (make-rainbow-numbers-number-match 2) limit))

(defun rainbow-numbers-number-match-3 (limit)
  (funcall (make-rainbow-numbers-number-match 3) limit))

(defun rainbow-numbers-number-match-4 (limit)
  (funcall (make-rainbow-numbers-number-match 4) limit))

(defun rainbow-numbers-number-match-5 (limit)
  (funcall (make-rainbow-numbers-number-match 5) limit))

(setq rainbow-numbers--font-lock-keywords
      (list (cons (function rainbow-numbers-number-match-0)
                  'font-lock-builtin-face)
            (cons (function rainbow-numbers-number-match-1)
                  'font-lock-variable-name-face)
            (cons (function rainbow-numbers-number-match-2)
                  'font-lock-function-name-face)
            (cons (function rainbow-numbers-number-match-3)
                  'font-lock-preprocessor-face)
            (cons (function rainbow-numbers-number-match-4)
                  'font-lock-warning-face)
            (cons (function rainbow-numbers-number-match-5)
                  'font-lock-type-face)))


(define-minor-mode rainbow-numbers-mode
  "Highlight groupings within numbers to make long numbers easier to read."
  nil "" nil
  (font-lock-remove-keywords nil rainbow-numbers--font-lock-keywords)
  (when (boundp 'font-lock-flush)
      (font-lock-flush))
  (font-lock-fontify-buffer)
  (when rainbow-numbers-mode
    (font-lock-add-keywords nil
                            rainbow-numbers--font-lock-keywords)
    (when font-lock-mode
      (when (boundp 'font-lock-flush)
        (font-lock-flush))
      (with-no-warnings (font-lock-fontify-buffer)))))
D. Gillis
  • 451
  • 2
  • 6