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 pairs
and 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.