Search code examples
lualpeg

How to make LPeg.match return nil


I'm currently getting familiar with the LPeg parser module. For this I want to match a version string (e.g. 11.4) against a list.

Such a list is a string with a tight syntax that can also contain ranges. Here is an EBNF-like, but in any case quite simple grammar (I write it down because LPeg code below can be a bit difficult to read):

S = R, { ',', R }
R = N, [ '-', N ]
N = digit+, [ '.', digit+ ]

An example string would be 1-9,10.1-11,12. Here is my enormous code:

local L = require "lpeg"
local LV, LP, LC, LR, floor = L.V, L.P, L.C, L.R, math.floor
local version = "7.25"

local function check(a, op, b)
    if op and a+0 <= version and version <= b+0 then
        return a..op..b -- range
    elseif not op and floor(version) == floor(a+0) then
        return a        -- single item
    end
end
local grammar = LP({ "S",
    S = LV"R" * (LP"," * LV"R")^0,
    R = LV"V" * (LC(LP"-") * LV"V")^-1 / check,
    V = LC(LV"D" * (LP"." * LV"D")^-1),
    D = (LR("09")^1),
})
function checkversion(str)
    return grammar:match(str)
end

So you would call it like checkversion("1-7,8.1,8.3,9") and if the current version is not matched by the list you should get nil.

Now, the trouble is, if all calls to check return nothing (meaning, if the versions do not match), grammar:match(...) will actually have no captures and so return the current position of the string. But this is exactly what I do not want, I want checkversion to return nil or false if there is no match and something that evaluates to true otherwise, actually just like string:match would do.

If I on the other hand return false or nil from check in case of a non-match, I end up with return values from match like nil, "1", nil, nil which is basically impossible to handle.

Any ideas?


Solution

  • This is the pattern I eventually used:

    nil_capturing_pattern * lpeg.Cc(nil)
    

    I incorporated it into the grammar in the S rule (Note that this also includes changed grammar to "correctly" determine version order, since in version numbering "4.7" < "4.11" is true, but not in calculus)

    local Minor_mag = log10(Minor);
    local function check(a, am, op, b, bm)
        if op then
            local mag = floor(max(log10(am), log10(bm), Minor_mag, 1))+1;
            local a, b, v = a*10^mag+am, b*10^mag+bm, Major*10^mag+Minor;
    
            if a <= v and v <= b then
                return a..op..b;
            end
        elseif a == Major and (am == "0" or am == Minor) then
            return a.."."..am;
        end
    end
    
    local R, V, C, Cc = lpeg.R, lpeg.V, lpeg.C, lpeg.Cc
    local g = lpeg.P({ "S",
        S = V("R") * ("," * V("R"))^0 * Cc(nil), 
        R = (V("Vm") + V("VM")) * (C("-") * (V("Vm") + V("VM")))^-1 / check,
        VM = V("D") * Cc("0"),
        Vm = V("D") * "." * V("D"),
        D = C(R("09")^1),
    });