Version 10 of lremove

Updated 2009-06-20 12:59:27 by jmn

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!


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.


See also list


Category Command