Search code examples
lualua-table

How to remove elements inside a table based on a value of those elements?


Iam trying to remove elements in a table which have a certain value

test = {
    {
        id = 473385593,
        deleted = true,
    },
    {
        id = 473385619,
        deleted = true,
    },
    {
        id = 473432275,
        deleted = false,
    },
    {
        id = 473786710,
        deleted = false,
    },
}

for index, value in ipairs(test) do
    if value.deleted then
        table.remove(test, index)
    end
end

for _, value in pairs(test) do
    print(value.id)
    print(value.deleted)
end

I would like that i the output is

473432275
false
473786710
false

But

{
        id = 473385619,
        deleted = true,
    },

is not deleted

So i guess as im iterating over the table to access the value and then do something when deleted == true i remove that element but also reorder the table and so i skip the next element after i call table.remove. At least thats what i figured whats happening.

So how do i remove in lua a tables elements based on their value?


Solution

  • As you figured out already, this snippet

    for index, value in ipairs(test) do
        if value.deleted then
            table.remove(test, index)
        end
    end
    

    is doing the grave mistake of removing table elements - shifting subsequent elements down - while iterating a table. for index, value in ipairs(test) do <body> end is roughly equivalent to

    do
        local index = 0
        while true do
            index = index + 1
            local value = test[index]
            if value == nil then
                break
            end
            <body>
        end
    end
    

    So of course if <body> does a table.remove(test, index), then in the next iteration test[index] will be the previous test[index+1] and you will skip an element.

    I would suggest "filtering" test, and building a new table containing only the elements you want to keep. This would look as follows:

    local test_filtered = {}
    for index, value in ipairs(test) do
        if not value.deleted then
            table.insert(test_filtered, value)
        end
    end
    for _, value in ipairs(test_filtered) do
        print(value.id)
        print(value.deleted)
    end
    

    This is asymptotically optimal runtime-wise (linear time). Very often it's cleaner to not mutate the original table; that can lead to surprises.

    In case you really want to modify test, you could iterate in reverse order, e.g. using for i = #test, 1, -1 do. Then table.remove will work fine. But be warned that this has a time complexity bound of O(n²).

    By keeping a second index you can also do in-place bulk removal of elements in O(n).

    Also: Why do you use a "list" here at all? Assuming that you don't care about order, a "hash table" (which lets you remove elements while iterating) would make more sense, given that you have IDs. Consider

    test = {
        [473385593] = {deleted = true},
        [473385619] = {deleted = true},
        [473432275] = {deleted = false},
        [473786710] = {deleted = false},
    }
    

    then you could just do

    for id, value in pairs(test) do
        if value.deleted then
            test[id] = nil
        end
    end