Introduction

This is something I wrote for PenLight but alas it wasn’t merged. The problem they had is the use if __pairs which isn’t present in Lua 5.1. The project wants to maintain compatibility with 5.1 and LuaJIT which targets 5.1. All the Lua projects I deal with are Lua 5.3 and there isn’t a clean way to make a case insensitive table without using __pairs. So I’m posting this here because I find it useful.

Sometimes it can be useful to have a table with string keys and not care about the case. That said, Lua’s tables are case sensitive meaning “test” and “TEST” are completely different keys. Due to the flexibility of Lua’s metamethods it’s very easy to create a table that is case insensitive, key case preserving, and once created operates like any other table.

There are a few features I want to have with a case insensitive table. Obviously, case insensitive lookup for one. I also want the case of the key to be preserved. Meaning if you iterate the table you don’t just get all the keys returned lower case.

Iteration can be handled three different ways. The case is always changed to lower, but as I said I don’t want this. The case of the first insertion is saved. The case of the last access is saved. I like this last one best and that’s how this is going to work. Whatever the last access of a key has for the case will be remembered for iteration.

One thing to keep in mind is lower case is used for the case insensitive comparison and Lua only has basic UTF-8 support. All keys are compared using string.lower. If you’re using UTF-8 characters outside of what’s supported by string.lower this isn’t going to work like you’d expect.

The Table Creator

--- Creates a case table with case insensitive string lookup.
-- Table can be used to store and lookup non-string keys as well.
-- Keys are case preserved while iterating (calling pairs).
-- The case will be updated after a set operation.
--
-- E.g:
-- * keys "ABC", "abc" and "aBc" are all considered the same key.
-- * Setting "ABC", "abc" then "aBc" the case returned when iterating will be "aBc".
--
-- @return A table with case insensitive lookup.
function create_case_table()
    -- For case preservation.
    local lookup = {}

    -- Stores the values so we can properly update. We need the main table to always return nil on lookup
    -- so __newindex is called when setting a value. If this doesn't happen then a case change (FOO to foo)
    -- won't happen because __newindex is only called when the lookup fails (there isn't a metamethod for 
    -- lookup we can use).
    local values = {}
    local mt = {
        __index=function(t, k)
            local v = nil
            if type(k) == "string" then
                -- Try to get the value for the key normalized.
                v = values[k:lower()]
            end
            if v == nil then
                v = values[k]
            end
            return v
        end,
        __newindex=function(t, k, v)
            -- Store all strings normalized as lowercase.
            if type(k) == "string" then
                lookup[k:lower()] = v ~= nil and k or nil -- Clear the lookup value if we're setting to nil.
                k = k:lower()
            end
            values[k] = v
        end,
        __pairs=function(t)
            local function n(t, i)
                if i ~= nil then
                    -- Check that strings that have been normalized exist in the table.
                    if type(i) == "string" and values[i:lower()] ~= nil then
                        i = i:lower()
                    end
                    -- Ensure the value exists in the table.
                    if values[i] == nil then
                        return nil
                    end
                end
                local k,v = next(values, i)
                return lookup[k] or k, v
            end
            return n, t, nil
        end
    }

    return setmetatable({}, mt)
end

What we’re doing is taking the key and if it’s a string converting it to lower case for actual storage in the table. We use the lookup table to preserve the last case used to access a key. It’s a lower case to whatever case mapping.

Internally we’re going to store everything in the values table. This one has all string keys stored lowercase to achieve the case insensitive comparison. If you look at __index and __newindex You’ll see we check if the key is a string and lower case for any lookup’s in the values table. If the key is not a string, we store as is. Also, notice in __newindex we update the lookup table with the key that was passed in so the last use case can be preserved for iteration

For people not really fluent in Lua, let’s look at the line that updates the iteration case in the lookup table:

lookup[k:lower()] = v ~= nil and k or nil

If we break this down lookup[k:lower()] sets the key to lower case. This corresponds to key we have stored in values. The next part is a Luaism. v ~= nil when this is true, and k will be the value used. Otherwise v is nil then the or nil block is used, and the value is nil. This removes key from the table.

Testing

We need a small test app to demonstrate everything works.

function print_table(name, t)
    print(name .. ":")
    for k,v in pairs(t) do
        print("",k,v)
    end
end

function print_tablei(name, t)
    print(name .. ":")
    for i,v in ipairs(t) do
        print("",i,v)
    end
end

function main()
    local t1 = create_case_table()
    local t2 = { }

    print("Start off our table with a key 'test'")
    t1['test'] = 123
    t2['test'] = 123
    print_table("t1", t1)
    print_table("t2", t2)
    print()

    print("Use key 'tEst' and set a different value")
    t1['tEst'] = 456
    t2['tEst'] = 456
    print_table("t1", t1)
    print_table("t2", t2)
    print()

    print("Try 'TEsT' using . instead of [] notation")
    t1.TEsT = 789
    t2.TEsT = 789
    print_table("t1", t1)
    print_table("t2", t2)
    print()

    print("Set a different key 'TEss'")
    t1.TEss = 111
    t2.TEss = 111
    print_table("t1", t1)
    print_table("t2", t2)
    print()

    print("Change the value of key 'tess'")
    t1.tess = 222
    t2.tess = 222
    print_table("t1", t1)
    print_table("t2", t2)
    print()

    print("Done with the normal table. Let's keep looking at the case table")
    print("Add some numbers that could be picked up by iparis")
    for i=1,10 do
        if i % 2 == 0 then
            t1[i] = (string.char(string.byte('a') + i)) .. i
        else
            t1[i] = i*2
        end
    end
    print()
    print("Print using pairs")
    print_table("t1", t1)
    print()

    print("Print using iparis")
    print_tablei("t1", t1)

    return 0
end

return main()

This is pretty basic and just create a case table and a normal table. It pushes keys and values in using different cases and shows the case table updates the values ignoring case. Unlike the normal table which has an entry for every case combination used.

Also, it shows that iteration using both pairsand ipairs still works as expected.

Output

$ lua main.lua 
Start off our table with a key 'test'
t1:
	test	123
t2:
	test	123

Use key 'tEst' and set a different value
t1:
	tEst	456
t2:
	test	123
	tEst	456

Try 'TEsT' using . instead of [] notation
t1:
	TEsT	789
t2:
	tEst	456
	TEsT	789
	test	123

Set a different key 'TEss'
t1:
	TEsT	789
	TEss	111
t2:
	tEst	456
	TEsT	789
	TEss	111
	test	123

Change the value of key 'tess'
t1:
	TEsT	789
	tess	222
t2:
	test	123
	TEss	111
	tess	222
	tEst	456
	TEsT	789

Done with the normal table. Let's keep looking at the case table
Add some numbers that could be picked up by iparis

Print using pairs
t1:
	1	2
	2	c2
	3	6
	4	e4
	5	10
	6	g6
	7	14
	8	i8
	9	18
	10	k10
	TEsT	789
	tess	222

Print using iparis
t1:
	1	2
	2	c2
	3	6
	4	e4
	5	10
	6	g6
	7	14
	8	i8
	9	18
	10	k10

The output is as we’d expect. Keys that differ by case only have the value updated unlike the normal tables where new entries are added.