替换并同时收集被替换的字符

我用下列的代码替换字符串中的子字符串,同时收集被替换的字符串,

(let ((mystring "This is a three")
      (start 0)
      (mylist nil))
  (while (string-match "th" mystring start)
    (setq start (match-end 0))
    (push (match-string 0 mystring) mylist)
    (setq mystring (replace-match "ha" nil nil mystring 0))
    )
  (mapconcat #'identity (nreverse mylist) "\n\n")
  ;; mystring
  )

问题是如果替换后的字符和原来的长度不一样,会导致字符长度改变,搜索的起始位置可能超出范围,然后 string-match 会报错,请问大家这个应该怎样处理?我能想到比较 dirty 的办法,是算一下两个字符串的长度差,然后修正起始位置。

隐约感觉会是这个问题:

不过我对代码也不敏感,除非你直接给个一键执行的出错例子。

(let ((mystring "This is a three")
      (start 0)
      (mylist nil))
  (while (string-match "three" mystring start)
    (setq start (match-end 0))
    (push (match-string 0 mystring) mylist)
    (setq mystring (replace-match "two" nil nil mystring))
    )
  (mapconcat #'identity (nreverse mylist) "\n\n")
  ;; mystring
  )

我的意思是,给出结果的对照组:

  • expected
  • actual

这会比文字描述更清晰,帮助他人更快进入思考状态。

第一组代码正常执行,mystring 中的 th 被替换,并且被替换的 th 被收集到一个列表里;

第二组代码无法正常执行,执行过程中报错。

噢,看到了。那应该是 save-match-data 无关,应该是不能在循环中 (setq mystring ...) 修改原字符串本身。

是的,然后我现在的做法是根据替换字符的长度调整起始位置:

(let ((mystring "This is a three")
      (start 0)
      (mylist nil))
  (while (string-match "three" mystring start)
    (setq start (+ (match-end 0) (- 3 (length (match-string 0 mystring))))) ; 这一句调整起始位置
    (push (match-string 0 mystring) mylist)
    (setq mystring (replace-match "two" nil nil mystring))
    )
  (mapconcat #'identity (nreverse mylist) "\n\n")
  mystring
  )

最好是不修改原字符串,可以参考一下 replace-regexp-in-string 函数的实现。精简掉不需要的步骤,加入收集被替换字符的操作。

replace-regexp-in-string 的话,比如想把 "1-3-2" 中的数字替换成对应的英文单词,并且搜集这些数字:

(let (matches)
  (list
   (replace-regexp-in-string
    "[0-9]"
    (lambda (s)
      (let ((match (match-string 0 s)))
        (push match matches)
        (pcase match
          ("1" "one")
          ("2" "two")
          ("3" "three"))))
    "1-3-2")
   (nreverse matches)))
;; => ("one-three-two" ("1" "3" "2"))
3 个赞

非常赞!

我看了一下函数文档, 不用 match-string 函数也可以,因此可以再稍微简化一点:

(let (matches)
  (list
   (replace-regexp-in-string
    "[0-9]"
    (lambda (match)
      (push match matches)
      (pcase match
        ("1" "one")
        ("2" "two")
        ("3" "three")))
    "1-3-2")
   (nreverse matches)))
;; => ("one-three-two" ("1" "3" "2"))
2 个赞