5

EDIT: gave more context for my first approach.

My goal is to search my notes for strings of the form "4/1/2014" to org-mode timestamps "<2014-04-01 Wed>" (that particular day-of-week may be incorrect). I also want querying to be on so I do not accidentally do things like replacing part of a URL this way.

Assuming I already wrote a Day-of-the-week function (complaints welcome, though not part of the main question):

(defun dow (year month day)
  (interactive)
  (let ((encoded-time (encode-time 1 1 10 day month year)))
    (nth (nth 6 (decode-time encoded-time))
     '("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"))))

I seem to be unable to do the rest. Here are my attempts and when they fall short:

  1. First, I tried to use query-replace-regexp:
(defun date-replace ()
  (interactive)
  (goto-char 1)
  (query-replace-regexp "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]\\{4\\}\\)"
            "<\\3-\\1-\\2 \\,(dow (string-to-number \\3) (string-to-number \\1) (string-to-number \\2))>"))

I get an "invalid use of '\' in replacement text" error and do not know what to do with that even after googling; it seems the most likely cause is escaping something in the replacement string that's NOT '\', but I do none of that.

By the way, doing

M-x query-replace-regexp RET \([0-9]+\)/\([0-9]+\)/\([0-9]\{4\}\) RET <\3-\1-\2 \,(dow (string-to-number \3) (string-to-number \1) (string-to-number \2))> RET

works, which is why I thought all I had to do was to escape the backslashes.

(another problem with this approach is it does not 0-pad 1-digit numbers in a way that seems clean to me, even if everything else were to work)

  1. Then, I tried to use search-forward-regexp. Here's a clunky solution that works (replacing the relevant portion of above code):
(while (search-forward-regexp "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]\\{4\\}\\)" nil t)
  (let* ((ynum (string-to-number (match-string 3)))
       (mnum (string-to-number (match-string 1)))
       (dnum (string-to-number (match-string 2)))
       (dowstr (dow ynum mnum dnum)))
    (replace-match (format "<%04d-%02d-%02d %s>" ynum mnum dnum dowstr) t nil)))

The only problem here is that it does not query, and there does not seem to be flags that allow me to do that.

  1. My final attempt is looking up query-replace and the manual suggesting using perform-replace. So here I have:
  (perform-replace "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]\\{4\\}\\)"
           ((lambda (data numreps)
              (format "<%04d-%02d-%02d %s>" (nth 1 data) (nth 2 data) (nth 3 data)
                  (dow (nth 1 data) (nth 2 data) (nth 3 data))))
            .
            ((string-to-number (match-string 3))
             (string-to-number (match-string 1))
             (string-to-number (match-string 2))))
           t t t)

and I get "Wrong type argument: stringp, nil" that I cannot decipher.

While my immediate goal is to find out which type of solution is the correct way to proceed and to make it work, I am overall more interested in why these bugs occur and how to get better at debugging, as I am new to learning elisp. Thanks!

4
  • 1
    M-x toggle-debug-on-error if necessary. Re that last error, you should be seeing a backtrace with string-to-number(nil) as the cause of that error, which obviously indicates that one of your (string-to-number (match-string N)) instances was getting nil back from match-string.
    – phils
    Jan 31, 2019 at 2:36
  • ...which is presumably because you're evaluating that before any searching has taken place. (cons (lambda ...) ARG) would be the format for the REPLACEMENTS part, and I presume ARG has to be something established at the point in time that the perform-replace is called. It doesn't sound like it gets eval'd during the replacement process.
    – phils
    Jan 31, 2019 at 2:49
  • 1
    nth is indexed from zero, btw.
    – phils
    Jan 31, 2019 at 2:50
  • 2
    For dow, there is a builtin calendar-day-of-week: (calendar-day-of-week '(1 31 2019)) ;; => 4.
    – xuchunyang
    Jan 31, 2019 at 3:14

1 Answer 1

4

You might write the perform-replace version like this:

(perform-replace
 "\\b\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]\\{4\\}\\)\\b"
 (cons (lambda (&rest _)
         (let ((data (list (string-to-number (match-string 1))
                           (string-to-number (match-string 2))
                           (string-to-number (match-string 3)))))
           (format "<%04d-%02d-%02d %s>"
                   (nth 2 data) (nth 1 data) (nth 0 data)
                   (dow (nth 0 data) (nth 1 data) (nth 2 data)))))
       nil)
 t t nil)

Experimentally the DELIMITED arg doesn't behave as I'd expected. I've set that nil and used \b explicitly in the regexp. I'm sure there's a reason for that. (Potentially when it says "surrounded by word boundaries" it also means "and containing only a single word", in which case the / instances would be the issue.)


The reason your query-replace-regexp attempt failed was that you expected that its arguments should look exactly the same as you enter them interactively; but in fact the interactive spec of that command manipulates what you enter into the format that it actually requires.

After performing the interactive command, you can use repeat-complex-command to learn what it actually did:

C-x M-: runs the command repeat-complex-command (found in global-map),
which is an interactive compiled Lisp function in ‘simple.el’.

It is bound to <again>, <redo>, C-x M-:, C-x M-ESC.
(query-replace-regexp
 "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]\\{4\\}\\)"
 (quote (replace-eval-replacement
         concat "<\\3-\\1-\\2 "
         (replace-quote (dow (string-to-number (match-string 3))
                             (string-to-number (match-string 1))
                             (string-to-number (match-string 2))))
         ">"))
 nil nil nil nil nil)

(All the optional nil args can, of course, be omitted.)

3
  • 1
    Thanks phils; not only for this answer but also teaching me how to fish above (esp. with the backtrace; I was only aware of looking at messages for errors). =) I guess I thought the (a . b) notation was supposed to cons. Jan 31, 2019 at 3:25
  • The lisp reader will read (a . b) as a cons cell; but if (a . b) is evaluated it is a malformed function call, and hence an error, so one would typically be quoting that: '(a . b), which then evaluates to the cons cell that was read. It is possible to write and use unquoted dotted-pair notation, but it would be rather unusual to do so -- I think that using (cons ...) is invariably going to be more sensible.
    – phils
    Jan 31, 2019 at 6:32
  • e.g.: ((lambda . ((x . (y . ())) . ((message . ("%s, %s" . (x . (y . ()))))))) . ("Hello" . ("world." . ())))
    – phils
    Jan 31, 2019 at 6:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.