aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Engdegård2024-05-13 10:44:05 +0200
committerMattias Engdegård2024-05-13 12:39:41 +0200
commit49e243c0c85d18fc775970d9ebd846eba3a6866e (patch)
tree2ee32a6f81bfe93b7d0f3f84d83ee82f655808d1
parent334fb0ddfe603097c3493297c3e78a185b22d1af (diff)
downloademacs-49e243c0c85d18fc775970d9ebd846eba3a6866e.tar.gz
emacs-49e243c0c85d18fc775970d9ebd846eba3a6866e.zip
Avoid resizing mutation in subst-char-in-string, take two
This time we take care to preserve properties, and add a test. * lisp/subr.el (subst-char-in-string): Use string-replace to avoid resizing mutation and O(n^2) time. * test/lisp/subr-tests.el (subr--subst-char-in-string): New test.
-rw-r--r--lisp/subr.el26
-rw-r--r--test/lisp/subr-tests.el48
2 files changed, 67 insertions, 7 deletions
diff --git a/lisp/subr.el b/lisp/subr.el
index 0ac71560c59..d18765c2a5b 100644
--- a/lisp/subr.el
+++ b/lisp/subr.el
@@ -5690,13 +5690,25 @@ The SEPARATOR regexp defaults to \"\\s-+\"."
5690(defun subst-char-in-string (fromchar tochar string &optional inplace) 5690(defun subst-char-in-string (fromchar tochar string &optional inplace)
5691 "Replace FROMCHAR with TOCHAR in STRING each time it occurs. 5691 "Replace FROMCHAR with TOCHAR in STRING each time it occurs.
5692Unless optional argument INPLACE is non-nil, return a new string." 5692Unless optional argument INPLACE is non-nil, return a new string."
5693 (let ((i (length string)) 5693 (if (and (not inplace)
5694 (newstr (if inplace string (copy-sequence string)))) 5694 (if (multibyte-string-p string)
5695 (while (> i 0) 5695 (> (max fromchar tochar) 127)
5696 (setq i (1- i)) 5696 (> tochar 255)))
5697 (if (eq (aref newstr i) fromchar) 5697 ;; Avoid quadratic behaviour from resizing replacement.
5698 (aset newstr i tochar))) 5698 (let ((res (string-replace (string fromchar) (string tochar) string)))
5699 newstr)) 5699 (unless (eq res string)
5700 ;; Mend properties broken by the replacement.
5701 ;; Not fast, but this case never was.
5702 (dolist (p (object-intervals string))
5703 (set-text-properties (nth 0 p) (nth 1 p) (nth 2 p) res)))
5704 res)
5705 (let ((i (length string))
5706 (newstr (if inplace string (copy-sequence string))))
5707 (while (> i 0)
5708 (setq i (1- i))
5709 (if (eq (aref newstr i) fromchar)
5710 (aset newstr i tochar)))
5711 newstr)))
5700 5712
5701(defun string-replace (from-string to-string in-string) 5713(defun string-replace (from-string to-string in-string)
5702 "Replace FROM-STRING with TO-STRING in IN-STRING each time it occurs." 5714 "Replace FROM-STRING with TO-STRING in IN-STRING each time it occurs."
diff --git a/test/lisp/subr-tests.el b/test/lisp/subr-tests.el
index 119c124f3a5..6f28e057342 100644
--- a/test/lisp/subr-tests.el
+++ b/test/lisp/subr-tests.el
@@ -1330,5 +1330,53 @@ final or penultimate step during initialization."))
1330 (t x) (:success (1+ x))) 1330 (t x) (:success (1+ x)))
1331 '(error ""))))) 1331 '(error "")))))
1332 1332
1333(ert-deftest subr--subst-char-in-string ()
1334 ;; Cross-validate `subst-char-in-string' with `string-replace',
1335 ;; which should produce the same results when there are no properties.
1336 (dolist (str '("ananas" "na\x80ma\x80s" "hétérogénéité"
1337 "Ω, Ω, Ω" "é-\x80-\x80"))
1338 (dolist (mb '(nil t))
1339 (unless (and (not mb) (multibyte-string-p str))
1340 (let ((str (if (and mb (not (multibyte-string-p str)))
1341 (string-to-multibyte str)
1342 str)))
1343 (dolist (inplace '(nil t))
1344 (dolist (from '(?a ?é ?Ω #x80 #x3fff80))
1345 (dolist (to '(?o ?á ?ƒ ?☃ #x1313f #xff #x3fffc9))
1346 ;; Can't put a non-byte value in a non-ASCII unibyte string.
1347 (unless (and (not mb) (> to #xff)
1348 (not (string-match-p (rx bos (* ascii) eos) str)))
1349 (let* ((in (copy-sequence str))
1350 (ref (if (and (not mb) (> from #xff))
1351 in ; nothing to replace
1352 (string-replace
1353 (if (and (not mb) (<= from #xff))
1354 (unibyte-string from)
1355 (string from))
1356 (if (and (not mb) (<= to #xff))
1357 (unibyte-string to)
1358 (string to))
1359 in)))
1360 (out (subst-char-in-string from to in inplace)))
1361 (should (equal out ref))
1362 (if inplace
1363 (should (eq out in))
1364 (should (equal in str))))))))))))
1365
1366 ;; Verify that properties are preserved.
1367 (dolist (str (list "cocoa" (string-to-multibyte "cocoa") "écalé"))
1368 (dolist (from '(?a ?o ?c ?é))
1369 (dolist (to '(?i ?à ?☃))
1370 (let ((in (copy-sequence str)))
1371 (put-text-property 0 5 'alpha 1 in)
1372 (put-text-property 1 4 'beta 2 in)
1373 (put-text-property 0 2 'gamma 3 in)
1374 (put-text-property 1 4 'delta 4 in)
1375 (put-text-property 2 3 'epsilon 5 in)
1376 (let* ((props-in (copy-tree (object-intervals in)))
1377 (out (subst-char-in-string from to in))
1378 (props-out (object-intervals out)))
1379 (should (equal props-out props-in))))))))
1380
1333(provide 'subr-tests) 1381(provide 'subr-tests)
1334;;; subr-tests.el ends here 1382;;; subr-tests.el ends here