Thread Tools Display Modes
07/08/14, 09:11 AM   #1
zgrssd
AddOn Author - Click to view addons
Join Date: May 2014
Posts: 280
LibConstantMapper

While trying to figure out wich unambigious Chat Category Constant name relates to wich category I ran into the issue that the Constant Names are the keys in the global table. They can be identified via Pattern Matching, but they are not a subtable that you can easily itterate over.

Doing excessive pattern matching is bad for performance. Also there are other similary organised groups of values (like the Chat_Channels or all the Event ID's) you might want to find an easy match for.
So I started development on this library. It consists of two files:
mappings.lua
Lua Code:
  1. ConstantMapperMappings = {
  2.     { mapping = "ChatCategories", pattern = "^CHAT_CATEGORY_", exclude = "^CHAT_CATEGORY_HEADER" }
  3. }
Just a table. It contains the mapping (wich Key the table will be saved under and will be requestable), the pattern to match (in this case for chat categories) and a pattern to not include (in this case I want all the entries starting with "CHAT_CATEGORY_", except the ones starting wiht "CHAT_CATEGORY_HEADER").
If I also want the chat channels using this library, I just have to modify it like this:
Lua Code:
  1. ConstantMapperMappings = {
  2.     { mapping = "ChatCategories", pattern = "^CHAT_CATEGORY_", exclude = "^CHAT_CATEGORY_HEADER" },
  3.     { mapping = "ChatChannels", pattern = "^CHAT_CHANNEL_"}
  4. }

main.lua
Lua Code:
  1. local Mappings = ConstantMapperMappings
  2. local data = { }
  3.  
  4. --Make certain data contains a table for each mapping
  5. for i=1, #Mappings, 1 do
  6.     local currentMap = Mappings[i]
  7.    
  8.     if(data[currentMap.mapping] == nil) then
  9.         data[currentMap.mapping] = {}
  10.     end
  11. end
  12.  
  13. --For every entry in the Global table
  14. for key, value in zo_insecurePairs(_G) do
  15.     --Compare it to every mapping
  16.     for i=1, #Mappings, 1 do
  17.         local currentMap = Mappings[i]
  18.        
  19.         if (key):find(currentMap.pattern) and (currentMap.exclude == nil or not(key):find(currentMap.exclude)) then
  20.             --found a Value for this mapping, so store it in the table
  21.             local newentry = { key = key, value = value}
  22.            
  23.             table.insert(data[currentMap.mapping], newentry)
  24.         end
  25.     end
  26. end
I go over all entries in the global table. Compare each entry to each mapping. And if it matches, I store the value as a subtable in Data.
That way I get a easy to itterate over table containing wich ConstantName matches wich Category.

Going over the whoel global table is a lot faster then I anticipated so maybe I switch to a "retreive on demand, but cache" approach instead.

Of course the whole thing will use libStub to avoid multiple execution in the final version. I just wanted to ask if there are any mistakes in this code I have to look out for.
  Reply With Quote
07/08/14, 10:37 AM   #2
merlight
AddOn Author - Click to view addons
Join Date: Jul 2014
Posts: 671
Some suggestions.

a) either allow more exludes (make it a table) to make things like this work (because Lua's regular expressions are so weak they can't accomplish this in one go):
Lua Code:
  1. { mapping = "Stats", pattern = "^STAT_", exclude = {"^STAT_BONUS_OPTION_", "^STAT_SOFT_CAP_OPTION", "^STAT_VALUE_COLOR"} }
b) or replace excludes with a non-mapping...
Lua Code:
  1. { mapping = false, pattern = "^CHAT_CATEGORY_HEADER_" }, -- non-mapping
  2. { mapping = "ChatCategories", pattern = "^CHAT_CATEGORY_" },
  3. { mapping = "StatBonusOptions", pattern = "^STAT_BONUS_OPTION_" },
  4. { mapping = "StatSoftCapOptions", pattern = "^STAT_SOFT_CAP_OPTION_" },
  5. { mapping = false, pattern = "^STAT_VALUE_COLOR" }, -- non-mapping
  6. { mapping = "Stats", pattern = "^STAT_" },
...and stop after the first match
Lua Code:
  1. --For every entry in the Global table
  2. for key, value in zo_insecurePairs(_G) do
  3.     --Compare it to every mapping
  4.     local foundMap = nil
  5.     for _, currentMap in ipairs(Mappings) do
  6.         if key:find(currentMap.pattern) then
  7.             foundMap = currentMap
  8.             break -- stop at the first match
  9.         end
  10.     end
  11.     if foundMap and foundMap.mapping then
  12.             --found a Value for this mapping, so store it in the table
  13.             local newentry = { key = key, value = value}
  14.             table.insert(data[foundMap.mapping], newentry)
  15.     end
  16. end

Also it would be nice to have direct [value] => "key" mapping in addition to the table of pairs (I'm aware there might be collisions and the library would have to deal somehow, that's why wrote in addition to )

Last edited by merlight : 07/08/14 at 10:40 AM.
  Reply With Quote
07/08/14, 12:15 PM   #3
Garkin
 
Garkin's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2014
Posts: 832
Some more suggestions:

Lua Code:
  1. local MAJOR, MINOR = "LibConstantMapper-1.0", 1
  2.  
  3. local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
  4. if not lib then return end
  5.  
  6. local mappingsTable = {
  7.    ["ACTION_TYPE"] = { exclude = "DEPRECATED" },
  8.    ["ATTRIBUTE"] ={ exclude = "ATTRIBUTE_.+_" },
  9.    ["CHAT_CATEGORY"] = { exclude = "^CHAT_CATEGORY_HEADER" },
  10.    ["CURRENCY_CHANGE_REASON"] = {},
  11.    ["DAMAGE_TYPE" = {},
  12.    ["ITEM_QUALITY"] = {},
  13.    ["ITEM_TRAIT_TYPE"] = {},
  14.    ["ITEMSTYLE"] = { exclude = "DEPRECATED" },
  15.    ["ITEMTYPE"] = {},
  16.    ["LINK_TYPE"] = { pattern = "_LINK_TYPE$" }
  17.    ["STAT"] = { exclude = {"^STAT_BONUS_OPTION_", "^STAT_SOFT_CAP_OPTION", "^STAT_VALUE_COLOR"} }
  18. }
  19.  
  20. --do not overwrite existing data
  21. lib.data = lib.data or {}
  22.  
  23. --internal functions
  24. function lib:AddData(mapping, pattern, exclude)
  25.    for name, value in zo_insecurePairs(_G) do
  26.       if type(mapping) == "table" then
  27.          for mappingName, mappingTable in pairs(mapping) do
  28.             lib:SaveDataEntry(name, value, mappingName, mappingTable.pattern, mappingTable.exclude)
  29.          end
  30.       elseif type(mapping) == "string" then
  31.          lib:SaveDataEntry(name, value, mapping, pattern, exclude)
  32.       end
  33.    end
  34. end
  35.  
  36. function lib:SaveDataEntry(name, value, mapping, pattern, exclude)
  37.    if (name):find(pattern or "^"..mapping.."_") == nil then return end
  38.  
  39.    local skip = false
  40.    if exclude then
  41.       if type(exclude) == "table" then
  42.          for i,v in ipairs(exclude) do
  43.             if (name):find(v) then
  44.                skip = true
  45.                break
  46.             end
  47.          end
  48.       else
  49.          skip = (name):find(exclude) ~= nil
  50.       end
  51.    end
  52.    
  53.    if not skip then
  54.       lib.data[mapping] = data[mapping] or {}
  55.       lib.data[mapping][name] = value
  56.    end
  57. end
  58.  
  59. if oldminor then
  60.    --library update, add only new/changed items?
  61.    local newItems = {}
  62.    for name, value in pairs(mappingsTable) do
  63.       if value ~= lib.mappings[name] then
  64.          newItems[name] = value
  65.          lib.mappings = value
  66.       end
  67.    end
  68.    lib:AddData(newItems)
  69. else
  70.    lib.mappings = mappingsTable
  71.    lib:AddData(mappingsTable)
  72. end
  73.  
  74.  
  75. --external functions
  76. function lib:AddMapping(mapping, pattern, exclude)
  77.    --add or replace enxisting entry
  78.    if type(mapping) == "table" then
  79.       for name, value in pairs(mapping) do
  80.          lib.mappings[name] = value
  81.       end
  82.    else  
  83.       lib.mappings[mapping] = { ["pattern"] = pattern, ["exclude"] = exclude }
  84.    end
  85.  
  86.    lib:AddData(mapping, pattern, exclude)  
  87. end
  88.  
  89. function lib:GetConstants(mapping)
  90.    return lib.data[mapping]
  91. end

1) Mapping can be part of the name and pattern is defined only in special cases
2) Do not create a new global table ConstantMapperMappings, just make it accessible using the library reference
3) As merlight said, it would be nice to have direct [value] => "key" mapping, so I changed it that way
  Reply With Quote
07/09/14, 04:25 AM   #4
zgrssd
AddOn Author - Click to view addons
Join Date: May 2014
Posts: 280
Originally Posted by merlight View Post
Some suggestions.

a) either allow more exludes (make it a table) to make things like this work (because Lua's regular expressions are so weak they can't accomplish this in one go):
Lua Code:
  1. { mapping = "Stats", pattern = "^STAT_", exclude = {"^STAT_BONUS_OPTION_", "^STAT_SOFT_CAP_OPTION", "^STAT_VALUE_COLOR"} }
b) or replace excludes with a non-mapping...
[highlight="Lua"]
[...]
Also it would be nice to have direct [value] => "key" mapping in addition to the table of pairs (I'm aware there might be collisions and the library would have to deal somehow, that's why wrote in addition to )
I want the same Key to be able to pop up in multiple mappings (i.e. Events appareing ina big "all events" mapping as well as sublist for each class like guild), so non-mapping is out of the question.

The final structure for mappings might looks something like this:
1 Mapping (the mapping name)
-> with 1...N patterns to match
-> each pattern with 0...M exclusions

But that is pretty far in the future. First things first.

Originally Posted by Garkin View Post
1) Mapping can be part of the name and pattern is defined only in special cases
2) Do not create a new global table ConstantMapperMappings, just make it accessible using the library reference
3) As merlight said, it would be nice to have direct [value] => "key" mapping, so I changed it that way
1) Due to the long term plans, a mapping might be able to have multiple patterns. Combinign them would just make the code mroe complex
2) The global table was only a temporary solution for my first trials. Currently I actually tend towards mappings being registered at runtime the same way you register media with LibMedia or libaries with LibStub. And only being processed on demand. if so the mappings.lua file will propably run after the actuall library and just call the register function in a loop.
3) My original idea was a [value] => "key" mapping. But that would cause issues if I ever end up with the same value more then once (the whole Chat Cat header issues pointed me at that).
The output tables are designed to be itterable via # operator, no use of "pairs" needed. But taht is only personal preference of int indexes over string indexes.
Not using the value as key also means I can accomodate non-key worthy values. For example, afaik "" is a valid value. But not a valid index. BugEaters inability to deal with nil and "" values pointed me towards that issue.
A simple int indexed array of key&value allows a lot more ways to process the data.

Edit (too 3):
The more I think about it, the better my idea sounds. The global table is full of values that would never make a valid index for a table: empty stings, functions, other tables - non of that could ever make a proper index. Plus sometimes the same function/table might be used as value multiple keys (so I had the same index used multiple times).
Clear index-worthy int and string values seem to be a odd thing in the global table, rather then a common one.

Last edited by zgrssd : 07/09/14 at 04:38 AM.
  Reply With Quote
07/09/14, 08:31 AM   #5
merlight
AddOn Author - Click to view addons
Join Date: Jul 2014
Posts: 671
Originally Posted by zgrssd View Post
For example, afaik "" is a valid value. But not a valid index. BugEaters inability to deal with nil and "" values pointed me towards that issue.
Anything but nil is a valid index (you can even use another table as key). In SavedVars only int/string/boolean keys are allowed, but that is SavedVars limitation, not Lua limitation. Empty string is a valid key in both.

Skip down to *CONSTRUCTIVE* if you only want something constructive, this below is just rant

Originally Posted by zgrssd View Post
A simple int indexed array of key&value allows a lot more ways to process the data.
But not the one way I find most convenient - quickly answering the question "what's the symbolic name of this value?".

Originally Posted by zgrssd View Post
Edit (too 3):
The more I think about it, the better my idea sounds. The global table is full of values that would never make a valid index for a table: empty stings, functions, other tables - non of that could ever make a proper index.
I don't think I would ask the name of anything but integer constants (except when I was writing a function to dump _G, I used some heuristics to give names to metatables, and that was when I used [{table}] => "string" mapping; but that's something completely out of scope of this library, I guess).

Originally Posted by zgrssd View Post
Plus sometimes the same function/table might be used as value multiple keys (so I had the same index used multiple times).
Clear index-worthy int and string values seem to be a odd thing in the global table, rather then a common one.
Look at it this way. You give me an array of pairs. I want to know the name of a thing. So I'll write this function:
Lua Code:
  1. local function nameIt1(value, mapping)
  2.     for i, m in ipairs(mapping) do
  3.         if value == m.value then
  4.             return m.key
  5.         end
  6.     end
  7. end

Someone else might instead do it this way:
Lua Code:
  1. local function nameIt2(mapping, value)
  2.     local name = nil
  3.     for i, m in ipairs(mapping) do
  4.         if value == m.value then
  5.             name = m.key
  6.         end
  7.     end
  8.     return name
  9. end

nameIt1 returns the first key with matching value, nameIt2 returns the last. Not to mention that the order of values in mapping depends on the order in which you get them from zo_insecurePairs! Which may be different every time you /reloadui, so addons using any nameIt function might behave strangely sometimes. I know it's hard do deal with conflicts properly, but if it's done in a library, at least it can be done consistently.


*CONSTRUCTIVE*

You could:
1) Add type requirement to your mapping spec. For example, "only int values allowed", everything else thrown away.
2) Sort your data={{value=5,key="foo"}, {value=5,key="bar"}} pairs table on value and key (sorting elements with equal values on key is important if you want consistent search results). Or zo_binaryinsert right away, instead of table.insert's and table.sort afterwards.
3) Provide a lookup function which would zo_binarysearch for given value in the sorted data.

Last edited by merlight : 07/09/14 at 08:34 AM.
  Reply With Quote
07/09/14, 11:21 AM   #6
zgrssd
AddOn Author - Click to view addons
Join Date: May 2014
Posts: 280
Originally Posted by merlight View Post
But not the one way I find most convenient - quickly answering the question "what's the symbolic name of this value?".
If you want it done differently, you can always do it differently.

My experience tells me that a int-indexed, properly formed array consisting of key/value tables offers the best overall usage while still having acceptable looping performance.
Looping over the array with # and comparing a string to "something" is going to have the same performance as asking the system for the value to key "something". Because all those tables are "under the hood" are one List without allowed duplicates and one with allowed duplicates.

Also I cannot guarantee that any value will be there only once. That is simply a matter of how the data looks and the patterns/exclusiong are set.
Better ending up with the same key twice in the array (easy to diagnose) then having sometimes the one key in use, sometimes the other (hard to diagnose).
  Reply With Quote

ESOUI » AddOns » Released AddOns » LibConstantMapper


Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off