lremove

--- JMN 2024 With the implementation of TIP #367 which is mentioned at the bottom of this page; for Tcl 8.7+ compatibility it is probably better to use a different name for the 'lremove by value' concept discussed on this page.

lremove for Tip 367 removes elements by supplied indices - which may be in any order.

As a first draft of a pure Tcl equivalent for Tcl 8.6 I offer the following:

         proc ::lremove {list args} {
            set data [lmap v $list {list data $v}]
            foreach doomed_index $args {
                if {[llength $doomed_index] != 1} {error "bad index \"$doomed_index\": must be integer?\[+-]integer? or end?\[+-]integer?"}
                lset data $doomed_index x ;#x won't collide as all our data has been mapped to 2 elements per value
            }
            set keep [lsearch -all -inline -not -exact $data x]
            return [lmap v $keep {lindex $v 1}]
        }

There may be better ways to do it, and this hasn't been reviewed or thoroughly tested.

---

How to remove items from a list not by their position (lreplace) but by the actual content. My solution would be:

 proc K { x y } { set x }
 proc lremove { listvar string } {
         upvar $listvar in
         foreach item [K $in [set in [list]]] {
                 if {[string equal $item $string]} { continue }
                 lappend in $item
         }
 }

which gives us

 % set a [list a b c a b c a b c]
 a b c a b c a b c
 % lremove a b
 % set a
 a c a c a c

Is there any reason not to do it that way?

Gotisch


RS proposes this built-in way, waving many flags :)

 % set a [lsearch -all -inline -not -exact $a b]
 a c a c a c

wdb Simply ingenious!


@JaviGarate Awesome!


Todd A. Jacobs suggests this slightly more flexible version:

 # lremove ?-all? list pattern
 #
 # Removes matching elements from a list, and returns a new list.
 proc lremove {args} {
     if {[llength $args] < 2} {
        puts stderr {Wrong # args: should be "lremove ?-all? list pattern"}
     }
     set list [lindex $args end-1]
     set elements [lindex $args end]
     if [string match -all [lindex $args 0]] {
        foreach element $elements {
            set list [lsearch -all -inline -not -exact $list $element]
        }
     } else {
        # Using lreplace to truncate the list saves having to calculate
        # ranges or offsets from the indexed element. The trimming is
        # necessary in cases where the first or last element is the
        # indexed element.
        foreach element $elements {
            set idx [lsearch $list $element]
            set list [string trim \
                "[lreplace $list $idx end] [lreplace $list 0 $idx]"]
        }
     }
     return $list
 }

 # Test cases.
 set foo {1 2 3 4 5 6 7 8 9 10}
 puts "Foo: [list $foo]"
 puts "Foo: replace one: [lremove $foo $argv]"
 puts "Foo: replace all: [lremove -all $foo $argv]"

 puts {}

 set bar {1 2 3 4 1 6 7 1 9 10}
 puts "Bar: [list $bar]"
 puts "Bar: replace one: [lremove $bar $argv]"
 puts "Bar: replace all: [lremove -all $bar $argv]"

dzach ...but then, when I do :

 set bar {1 2 3 {} 4 1 6 7 1 9 10 {}}
 lremove -all $bar {}

I still get:

 1 2 3 {} 4 1 6 7 1 9 10 {}

jmn this is because the proc expects a *list* of patterns as the 2nd argument. Try instead:

 lremove -all $bar [list {}] 

I don't see why the complicated 'lreplace' line with the 'string trim' is required. As far as I can see - it could be replaced with:

 set list [lreplace $list $idx $idx]

When removing a single value (1st encountered match) from a list - I use:

 set posn [lsearch -exact $list $val]
 set list [lreplace $list $posn $posn]

This is slightly different in functionality from RS's beautiful one liner above, which removes all instances. If you know you only have one instance of the value to be removed - this 2 liner may be slightly faster on large lists (tested on Tcl8.6b1).. but on a 2007 vintage machine we're talking maybe a couple of hundred microseconds for lists even of size approx 10K, so it's not likely to be an issue except for some pretty intensive inner loops with large lists.

milarepa How about this:

proc lremove {list match} {
    set idx_list [lsearch -all $list $match]
    foreach idx [lreverse $idx_list] {
        set list [lreplace $list $idx $idx]
    }
    return $list
}

gold Multiple entries, using Suchenworth's idea:

             # written on Windows XP on eTCL
             # working under TCL version 8.5.6 and eTCL 1.0.1
             # gold on TCL WIKI , 3jul2013
        package require Tk
        proc cleaner {target args} {
            set res $target
            foreach unwant $args {
              # suchenworth idea
              set res [lsearch -all -inline -not -exact $res $unwant ]
            }
            return $res
        }
        console show
        set a_target_list [list a b c a b c a b c 1234 1234 5678]
        puts " [cleaner {a b c a b c a b c} b c]"
        puts " [cleaner $a_target_list 1234 5678]"

 Output:
 a a a
 a b c a b c a b c

Another lremove method, by Cyril :

proc lremove {l p} {
    set p [lsearch -exact $l $p]
    return "[lrange $l 0 $p-1] [lrange $l $p+1 end]"
}

Example :

set l {a b c d e}
puts [lremove $l c]
# => a b d e

2018-09-13: note that concat [lrange $l 0 $p-1 [lrange $l $p+1 end] is a better way to concatenate the lists.


See also