9

Since I find myself writing a lot of matrices and tables, I'm looking for a way to align numbers nicely in Emacs (similar to the align package in vim). I found that there is align-regexp, but I couldn't get it to work the way I wanted. Is there a way to align numbers at their decimals --- and if there are no decimals align in front of the other decimals. It would also be nice to be able to align at 'thousands'-separators and align complex numbers. Preferably with two whitespaces in-between numbers for readability. Here is an example:

Input:

A = [-15 9 33.34;...
1.0 0.99 1+3i;...
13,000 2 11 ];

Desired output:

A = [   -15     9     33.34 ;...
          1.0  -0.99   1+3i ;...
     13,000     2     11    ];

Alternatively, to make it a bit easier (without 'thousands'-separator and complex numbers):

Input:

A = [-15 9 33.34;...
1.0 0.99 1;...
13000 2 11 ];

Desired output:

A = [  -15     9      33.34 ; ...
         1.0   0.99    1    ; ...
     13000     2      11    ];

Thanks a lot.

DayAndNight
  • 103
  • 5

1 Answers1

5

This took me quite a bit more time than I originally estimated, and the code is a bit too long to post it all here, so I posted it to Patebin: http://pastebin.com/Cw82x11i

It is not entirely complete though and it can use some more work, so if anyone will have suggestions or contributions, I might re-arrange this as a Git repository somewhere / repost this to Emacs wiki.

Few important points:

  1. No attempt has been made to cater for matrices with delimiters other than spaces.
  2. I didn't try to parse complex numbers either.
  3. The treatment of non-numerical entries is different than the one in your example (to be honest, I wouldn't really know how to parse it in exactly the way you want. My guess is that the semicolon is the Matlab / Octave row delimiter, but if I try to make it more generic, it's really hard to wrap my head around it. Also, my guess is that the ellipsis is the Matlab / Octave way of telling the interpreter that the statement continues on the next line, but, again, trying to make this more generic would be really hard. Instead, I'm just treating whatever non-numeric value I encounter as if it was a whole number.
  4. Finally, I had to give up the align-regexp because it was too complicated to try to make it align exactly using the rule you seem to have in mind.

Here's what it would look like:

;; before
A = [-15 9 33.34;...
1.0 0.99 1;...
13000 2 11 ];

;; after
A = [  -15   9    33.34 ;... 
         1.0 0.99  1    ;... 
     13000   2    11         ];

PS. You can adjust the space between the columns by changing the value of the spacer variable.

OK, I've also made a little refinement to the code where it can now ask for the string to fill in between the columns.

(defun my/string-to-number (line re)
  (let ((matched (string-match re line)))
    (if matched
        (list (match-string 0 line)
              (substring line (length (match-string 0 line))))
      (list nil line))))

(defun my/string-to-double (line)
  (my/string-to-number
   line
   "\\s-*[+-]?[0-9]+\\(?:\\.[0-9]+\\(?:[eE][+-]?[0-9]+\\)?\\)?"))

(defun my/string-to-int (line)
  (my/string-to-number line "\\s-*[+-]?[0-9]+"))

(defun my/vector-transpose (vec)
  (cl-coerce
   (cl-loop for i below (length (aref vec 0))
            collect (cl-coerce 
                     (cl-loop for j below (length vec)
                              collect (aref (aref vec j) i))
                     'vector))
   'vector))

(defun my/align-metric (col num-parser)
  (cl-loop with max-left = 0
           with max-right = 0
           with decimal = 0
           for cell across col
           for nump = (car (funcall num-parser cell))
           for has-decimals = (cl-position ?\. cell) do
           (if nump
               (if has-decimals
                   (progn
                     (setf decimal 1)
                     (when (> has-decimals max-left)
                       (setf max-left has-decimals))
                     (when (> (1- (- (length cell) has-decimals))
                              max-right)
                       (setf max-right (1- (- (length cell) has-decimals)))))
                 (when (> (length cell) max-left)
                   (setf max-left (length cell))))
             (when (> (length cell) max-left)
               (setf max-left (length cell))))
           finally (cl-return (list max-left decimal max-right))))

(defun my/print-matrix (rows metrics num-parser prefix spacer)
  (cl-loop with first-line = t
           for i upfrom 0
           for row across rows do
           (unless first-line (insert prefix))
           (setf first-line nil)
           (cl-loop with first-row = t
                    for cell across row
                    for metric in metrics
                    for has-decimals =
                    (and (cl-position ?\. cell)
                         (car (funcall num-parser cell)))
                    do
                    (unless first-row (insert spacer))
                    (setf first-row nil)
                    (cl-destructuring-bind (left decimal right) metric
                      (if has-decimals
                          (cl-destructuring-bind (whole fraction)
                              (split-string cell "\\.")
                            (insert (make-string (- left (length whole)) ?\ )
                                    whole
                                    "."
                                    fraction
                                    (make-string (- right (length fraction)) ?\ )))
                        (insert (make-string (- left (length cell)) ?\ )
                                cell
                                (make-string (1+ right) ?\ )))))
           (unless (= i (1- (length rows)))
             (insert "\n"))))

(defun my/read-rows (beg end)
  (cl-coerce
   (cl-loop for line in (split-string
                         (buffer-substring-no-properties beg end) "\n")
            collect
            (cl-coerce
             (nreverse
              (cl-loop with result = nil
                       with remaining = line do
                       (cl-destructuring-bind (num remainder)
                           (funcall num-parser remaining)
                         (if num
                             (progn
                               (push (org-trim num) result)
                               (setf remaining remainder))
                           (push (org-trim remaining) result)
                           (cl-return result)))))
             'vector))
   'vector))

(defvar my/parsers '((:double . my/string-to-double)
                     (:int . my/string-to-int)))

(defun my/align-matrix (parser &optional spacer)
  (interactive
   (let ((sym (intern
               (completing-read
                "Parse numbers using: "
                (mapcar 'car my/parsers)
                nil nil nil t ":double")))
         (spacer (if current-prefix-arg
                     (read-string "Interleave with: ")
                   " ")))
     (list sym spacer)))
  (unless spacer (setf spacer " "))
  (let ((num-parser
         (or (cdr (assoc parser my/parsers))
             (and (functionp parser) parser)
             'my/string-to-double))
        beg end)
    (if (region-active-p)
        (setf beg (region-beginning)
              end (region-end))
      (setf end (1- (search-forward-regexp "\\s)" nil t))
            beg (1+ (progn (backward-sexp) (point)))))
    (goto-char beg)
    (let* ((prefix (make-string (current-column) ?\ ))
           (rows (my/read-rows beg end))
           (cols (my/vector-transpose rows))
           (metrics
            (cl-loop for col across cols
                     collect (my/align-metric col num-parser))))
      (delete-region beg end)
      (my/print-matrix rows metrics num-parser prefix spacer))))
wvxvw
  • 11,222
  • 2
  • 30
  • 55
  • Awesome work. I think that you should still share the code here. Your answer will be useless if the pastebin link were to go dead. I have seen much much longer code snippets than 122 lines on SE :) – Kaushal Modi Aug 25 '15 at 13:00
  • Wooow, thank you VERY much. I'm sorry to cause you that much work, I was hoping some fancy regex or plugin could do the job. Its exactly what I was look for though. However I can't get it to work. How do I use it (sorry I don't have a lot of experience in lisp)? I tried to mark the region and call my/align-matrix, but it gives me the following error: "Attemnt to set a constant symbol: t" – DayAndNight Aug 25 '15 at 23:26
  • @DayAndNight this is really strange. I can't find a place where this error may happen. But if you can give me an example data, then my chances will be better. You may not need to mark the region before calling `my/align-matrix`. If the numbers are inside something Emacs treats as a kind of parenthesis (typically anyone of [], (), {}), then the code will make an effort to find that region on its own. – wvxvw Aug 26 '15 at 07:44