0

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")
12
  • 1
    Why '("B") and not "B"? Is your requirement actually more complex than stated?
    – phils
    May 20, 2020 at 4:05
  • Also, are you looking to create a new list, or to modify the original list? And likewise, should the replacement list be used directly, or copied? You're using a lot of quoted constants in your example code, which makes things trickier if you want to use or modify them directly. Consider changing them all to (list "A" "B" "C") etc to ensure the code wouldn't be modified in the process.
    – phils
    May 20, 2020 at 4:06
  • emacs.stackexchange.com/tags/elisp/info
    – Drew
    May 20, 2020 at 17:09
  • 2
    Substantially changing the minimal working example in the question after multiple people have posted answers renders one or more of those answers obsolete -- those forum participants who posted answers have essentially wasted their time. I deleted my answer based thereon.
    – lawlist
    May 22, 2020 at 13:54
  • 2
    Leaving the question as it was and posting your other question as a new/separate question would have meant that each question then existed on the site with answers that actually matched the question, and without messing anyone around.
    – phils
    May 23, 2020 at 2:43

3 Answers 3

1

Here 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")
0

(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"))
1
  • n.b. I didn't make the second argument a list, as no purpose was indicated for that in the question.
    – phils
    May 21, 2020 at 12:23
0

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")

Your Answer

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

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