What would be the an efficient/concise way to replace a sub-list with another list?
Example function call:
(replace-list-list-with-list '("A" "B" "C" "D") '("B" "C") '("NEW" "TEXT"))
Would result in:
'("A" "NEW" "TEXT" "D")
Emacs Stack Exchange is a question and answer site for those using, extending or developing Emacs. It only takes a minute to sign up.
Sign up to join this communityHere is a nondestructive soultion which is easy to code/understand, my-sublist-find
searches for a sublist and my-sublist-replace
replace a sublist.
(defun my-sublist-find (list sublist)
"Find the first occurrence of SUBLIST in LIST.
Return the indexes of the matching item or nil if not found."
(when (and list sublist (>= (length list) (length sublist)))
(cl-loop for start from 0 to (- (length list) (length sublist))
for end = (+ start (length sublist))
when (equal (cl-subseq list start end) sublist)
return (list start end))))
(defun my-sublist-replace (list1 start end list2)
"Replace the elements of LIST1 from START to END with the elements of LIST2."
(nconc (seq-subseq list1 0 start)
(copy-sequence list2)
(seq-subseq list1 end)))
(let ((list (list "A" "B" "C" "D"))
(old (list "B" "C"))
(new (list "NEW" "TEXT")))
(pcase (my-sublist-find list old)
(`(,start ,end)
(my-sublist-replace list start end new))))
;; => ("A" "NEW" "TEXT" "D")
(Edit: This answer is for the original question of replacing a list item, rather than for the modified question of replacing a sub-list. Extending this approach to recognise sub-lists is left as an exercise for the reader.)
Here's a destructive approach (but don't use it unless you know that you want a destructive approach -- definitely don't do so with quoted lists, like there are in the example in the question).
(defun replace-list-item-with-list (list item replacement)
"Substitute a REPLACEMENT list for ITEM in LIST, by altering the lists."
(catch 'substitution
(let ((current list)
parent)
(while current
(if (not (equal (car current) item))
(setq parent current
current (cdr current))
(if parent
(setcdr parent (nconc replacement (cdr current)))
(setq list (nconc replacement (cdr current))))
(throw 'substitution t)))))
list)
(replace-list-item-with-list (list "A" "B" "C") "A" (list "NEW" "TEXT"))
=> ("NEW" "TEXT" "B" "C")
(replace-list-item-with-list (list "A" "B" "C") "B" (list "NEW" "TEXT"))
=> ("A" "NEW" "TEXT" "C")
(replace-list-item-with-list (list "A" "B" "C") "C" (list "NEW" "TEXT"))
=> ("A" "B" "NEW" "TEXT")
(replace-list-item-with-list (list "A" "B" "C") "D" (list "NEW" "TEXT"))
=> ("A" "B" "C")
Assuming LIST is actually a variable, you would (as usual) need to assign the return value back to the list variable to handle the case where the first item was replaced.
(setq list (replace-list-item-with-list list item replacement))
FWIW I note that the dash
package has:
(-splice-list (lambda (x) (equal x "B"))
(list "NEW" "TEXT")
(list "A" "B" "C"))
This is a generic function that replaces a list within a list N times, taking an optional comparitor argument.
(defun my-list-replace-list-in-list (lst src dst &rest keyword-args)
"Replace the list SRC with DST in LST, returning a new list with the replacement(s) made.
Optional keyword arguments are:
:test TEST -- Defines how list items are compared, defaulting to `eq'.
:count COUNT -- The number of replacements to make, defaulting to nil (replace all)."
(let ((result (list)) ;; Build new list.
(current-count 0)
(lst-iter lst)
;; Keyword arguments.
(comparitor #'eq)
(count nil))
;; Extract keyword args.
(let ((kw-iter keyword-args))
(while kw-iter
(let ((kw-key (pop kw-iter))
(kw-val (pop kw-iter)))
(cond
((eq kw-key :test) (setq comparitor kw-val))
((eq kw-key :count) (setq count kw-val))
(t (error "Unknown keyword argument %S" kw-key))))))
(while lst-iter
(unless
(let ((found t) ;; Assume found until the match differs.
(src-iter src)
(lst-iter-test lst-iter))
(while src-iter
(if (and lst-iter-test (funcall comparitor (car lst-iter-test) (car src-iter)))
(progn
(setq src-iter (cdr src-iter))
(setq lst-iter-test (cdr lst-iter-test)))
;; Exit.
(setq src-iter nil)
(setq found nil)))
(when found
(let ((dst-iter dst))
(while dst-iter
(push (car dst-iter) result)
(setq dst-iter (cdr dst-iter))))
(setq current-count (1+ current-count))
;; Always fail when `count' is nil.
(if (eq current-count count)
;; All replacements made, finish and exit.
(progn
(while lst-iter-test
(push (car lst-iter-test) result)
(setq lst-iter-test (cdr lst-iter-test)))
;; Break the main loop.
(setq lst-iter nil))
;; Keep replacing.
(setq lst-iter lst-iter-test))
t))
;; No match, build list one by one.
(push (car lst-iter) result)
(setq lst-iter (cdr lst-iter))))
(nreverse result)))
Example use:
(let ((list (list "A" "B" "C" "D" "A" "B" "C" "D"))
(src (list "C" "D"))
(dst (list "NEW" "TEXT")))
(message
"Result %S"
(my-list-replace-list-in-list list src dst :test #'string-equal)))
Outputs:
Result ("A" "B" "NEW" "TEXT" "A" "B" "NEW" "TEXT")
'("B")
and not"B"
? Is your requirement actually more complex than stated?(list "A" "B" "C")
etc to ensure the code wouldn't be modified in the process.