2
votes

I need a pattern that will work with string.find (or string.match if necessary) that will return true if a "table path" string matches. This is my function:

local function FindValueFromPattern(myTable, pattern, previousPath)
    for key, value in pairs(myTable) do
        local path;

        if (not previousPath) then
            path = key;
        else
            path = string.format("%s.%s", previousPath, key);
        end

        if (path:find(pattern)) then
            return value;

        elseif (type(value) == "table") then
            value = FindValueFromPattern(value, pattern, path);

            if (value ~= nil) then
               return value;
            end
        end
    end

    return nil;
end

local tbl = {}
tbl.settings = {};
tbl.settings.module = {};
tbl.settings.module.appearance = {};
tbl.settings.module.appearance.color = "blue";

print(FindValueFromPattern(tbl, "settings.module.appearance.color")); -- prints "blue";

The code above works BUT I want to now change the pattern to:

"module.<ANY>.color" where <ANY> is any child table of "module" and also has a child table called "color", so when traversing down the table, a value will be returned regardless of what table is used (does not have to be the appearance table):

-- should also print "blue" ("setting." should not be required);
print(FindValueFromPattern(tbl, "module.<ANY>.color"));

Rather than returning found values straight away, I may have to change the logic to insert found values in a table and then return the table after the for-loop but I wrote this quickly to illustrate the problem.

So the question is, what would that pattern look like? Thank you.

2
Are numeric keys allowed? Should "settings.module[3].color" be found?Egor Skriptunoff
@EgorSkriptunoff Good question. No, because the path is artificially created and I join each key explicitly with "." so if the pairs function returned a number then, for example, it would still look like "module.2.property" and I can use this in the pattern I send to the function. Thanks for asking.Mayron
To match the "module.<ANY>.color" string you might use a "module%.[^.]+%.color" pattern. If you need to extract the ANY part, wrap the [^.]+ with capturing group: "module%.([^.]+)%.color"Wiktor Stribiżew
@WiktorStribiżew Thanks, that looks like a great answer. Not necessary for the solution I posted but would using "module%.([^.]+)%.([^.]+)%color" also work for cases where there are more than 1 of these keys? I might need to do something like this in the future.Mayron
The point is that [^.]+ matches any consecutive chars other than . one or more times. I guess yes. If you need to restrict the chars to letters and digits use %w+ instead. If you need to match any 0+ chars use .- (to get to the first .color) or .+ (to match up to the last .color).Wiktor Stribiżew

2 Answers

0
votes

What you do there is extremely inefficient. A much better approach would be to split the string at each . and just index the table.

A simple version that doesn't accept "any" could look like this

function findintable(tab, path)
    local pos = path:find(".", 1, true)
    if pos then
        local tab = tab[path:sub(1, pos-1)]
        if not type(tab) then error("Expected value to be table, got "..type(tab), 1) end
        return findintable(tab, path:sub(pos+1, -1))
    else
        return tab[path]
    end
end

Adding the possibility for an any key (he... he... he...) would add some complexity and needs a loop, but it's also doable

    function findintable(tab, path)
        local pos = path:find(".", 1, true)
        if not pos then
            return tab[path]
        end
        local key, rest = path:sub(1, pos-1), path:sub(pos+1, -1)
        if key == "*" then
            for k, v in pairs(tab) do
                if type(v)~="table" then return end
                local res = findintable(v, rest)
                if res then return res end
            end
            return
        else
            local tab = tab[path:sub(1, pos-1)]
            if not type(tab) then error("Expected value to be table, got "..type(tab), 1) end
            return findintable(tab, path:sub(pos+1, -1))
        end
    end

This should do what you want. Just change the "*" to whatever you want the placeholder to be.

0
votes

I used gmatch with the pattern %.*([^.]+) to iterate through each key in the keys provided.

This function could be changed to output a table of all colors found, but currently only returns a single value. The value returned is the color found or nil if no matches were found.

function FindValueFromPattern(tab, keys)
    local t_ref = tab

    for k in keys:gmatch("%.*([^.]+)") do
        if k == "<ANY>" and type(t_ref) == "table" then
            local temp1
            local temp2

            for any in pairs(t_ref) do
                local new_keys = keys:gsub(k, any)
                temp1 = FindValueFromPattern(tab, new_keys)

                new_keys = keys:gsub(k, any .. ".<ANY>")
                temp2 = FindValueFromPattern(tab, new_keys)

                if temp1 or temp2 then
                    break
                end
            end
            t_ref = temp1 or temp2
            break
        else
            if t_ref == nil or type(t_ref) ~= "table" then
                t_ref = nil
                break
            end

            t_ref = t_ref[k]
        end
    end
    return t_ref
end

Example use:

sample = {
    a = {
        b = {
            c = {
                color = "blue",

            },
            roloc = 1,
            color = "red",
        },
        d = {
            e = {
                color = "purple",

            },
            roloc = "wolley",
            color = "yellow",
        },
    }
}
colorFound = FindValueFromPattern(sample, "a.<ANY>.color")
if colorFound then
    print("Found: " .. colorFound )
else
    print("No matches found")
end
>> Found: red

Keep in mind the behavior is nondeterministic, it is possible for the output to be yellow rather then red, and it cannot be known which it will be until the code is run.