Thread Tools Display Modes
07-12-12, 07:26 PM   #1
Brusalk
An Aku'mai Servant
 
Brusalk's Avatar
AddOn Author - Click to view addons
Join Date: May 2010
Posts: 32
I need some help!

Hey There!

So I've written this function to be used in one of my addons. It accepts a bunch of tables, and then returns a table containing all values common to every table passed in.

Now, where I'm confused is that it's seemingly never reaching a point in execution, even though it should be.

Code:
local function tableMerge(...) 
	if select('#', ...) < 3 then error("tableMerge", "inputs") return end
	local toReturn = {}
	for i=1, select('#', ...) do
		local tab = select(i, ...)
		if type(tab) == "table" then --ignore any inputs which aren't tables
			if not toReturn[1] then
				for j,spellbar in pairs(tab) do -- do it this way so we don't actually do anything to the tables passed in
					table.insert(toReturn, spellbar)
					print("Adding " .. spellbar)
				end
			else
				for j,v in pairs(toReturn) do -- for everything that's common up to this point in iteration:
					print("Testing " .. v)
					local found = false
					for k,spellbar in pairs(tab) do -- for everything in this new table to check
						if v == spellbar then		-- if we found that the spellbar exists in this new table, say we found it
							found = true
							print("Found " .. spellbar)
						end
					end
					if not found then -- if we didn't find this element, then remove it from the common table
						print("Removing " .. toReturn[j])
						table.remove(toReturn, j)
					end
				end				
			end			
		end
	end
	return toReturn -- return everything that's common
end

function testMerge()
	return tableMerge({1,2,3,4,5,6,7}, "test", {2,4,6}, {})
end
Here's example output of what testMerge dumps:
Dump: value=testMerge()
Adding 1
Adding 2
Adding 3
Adding 4
Adding 5
Adding 6
Adding 7
Testing 1
Removing 1
Testing 3
Removing 3
Testing 5
Removing 5
Testing 7
Removing 7
Testing 2
Removing 2
Testing 6
Removing 6
[1]={
[1] = 4
}


I'm very perplexed because it's never actually finding any common elements, even though it should be (It's not removing 2,4,6 when it reaches the 3rd table)

What's even more perplexing to me is that it's never removing the 4th value, even though it should be as the last table is empty.

I've been trying to debug this for over an hour now and I can't figure out why it's behaving like it is. (setting the last table to 2 causes the output to be {2,6}



Any help you guys can give me I would be VERY appreciative!

-Brusalk
  Reply With Quote
07-12-12, 08:32 PM   #2
kaels
A Cyclonian
AddOn Author - Click to view addons
Join Date: Jan 2011
Posts: 46
Your iterator is getting mucked up because you're using pairs over a table whose indexes are changing inside the loop. As a rule, if you're iterating over a table with pairs or ipairs, you should try to avoid using table.remove and table.insert on that table inside the loop to avoid the sort of unpredictable behaviour you're seeing here.

Try this - it's definitely not optimized, but it should work:

lua Code:
  1. local function tableMerge(...)
  2.     if select('#', ...) < 3 then error("tableMerge", "inputs") return end
  3.     local toReturn = {}
  4.     for i=1, select('#', ...) do
  5.         local tab = select(i, ...)
  6.         if type(tab) == "table" then --ignore any inputs which aren't tables
  7.             if not toReturn[1] then
  8.                 for j,spellbar in pairs(tab) do -- do it this way so we don't actually do anything to the tables passed in
  9.                     table.insert(toReturn, spellbar)
  10.                     print("Adding " .. spellbar)
  11.                 end
  12.             else
  13.                 local n = #toReturn -- get the size of the array
  14.                 -- iterate over the indices in the array
  15.                 for j = 1,n do -- for everything that's common up to this point in iteration:
  16.                     local v = toReturn[j] -- get the value at this index
  17.                     print("Testing " .. v)
  18.                     local found = false
  19.                     for k,spellbar in pairs(tab) do -- for everything in this new table to check
  20.                         if v == spellbar then       -- if we found that the spellbar exists in this new table, say we found it
  21.                             found = true
  22.                             print("Found " .. spellbar)
  23.                         end
  24.                     end
  25.                     if not found then -- if we didn't find this element, then remove it from the common table
  26.                         print("Removing " .. toReturn[j])
  27.                         toReturn[j] = nil -- set to nil to remove without changing the array
  28.                     end
  29.                 end
  30.                 -- now we need to clean up the table
  31.                 for j = 1,n do -- iterate over the array again
  32.                     local v = toReturn[j] -- get the value at this index
  33.                     if v then -- look for non-empty values
  34.                         toReturn[j] = nil -- clear the current index
  35.                         table.insert(toReturn, v) -- reinsert the value at the first non-empty index
  36.                     end
  37.                 end            
  38.             end        
  39.         end
  40.     end
  41.     return toReturn -- return everything that's common
  42. end

And here's an alternative that may be quicker (only runs a cleanup function once) but won't preserve order, and might actually be slower (at least for a small number of tables) because it has to use pairs:
lua Code:
  1. local function tableMerge(...)
  2.     if select('#', ...) < 3 then error("tableMerge", "inputs") return end
  3.     local toReturn = {}
  4.     local n -- define n outside the loop so we can use it later
  5.     for i=1, select('#', ...) do
  6.         local tab = select(i, ...)
  7.         if type(tab) == "table" then --ignore any inputs which aren't tables
  8.             if not toReturn[1] then
  9.                 for j,spellbar in pairs(tab) do -- do it this way so we don't actually do anything to the tables passed in
  10.                     table.insert(toReturn, spellbar)
  11.                     print("Adding " .. spellbar)
  12.                 end
  13.                 n = #toReturn -- get the size of the initial table - it won't get any bigger than this
  14.             else
  15.                 -- we have to iterate with pairs because after the first run, our table will have empty indices
  16.                 for j, v in pairs(toReturn) do -- for everything that's common up to this point in iteration:
  17.                     print("Testing " .. v)
  18.                     local found = false
  19.                     for k,spellbar in pairs(tab) do -- for everything in this new table to check
  20.                         if v == spellbar then       -- if we found that the spellbar exists in this new table, say we found it
  21.                             found = true
  22.                             print("Found " .. spellbar)
  23.                         end
  24.                     end
  25.                     if not found then -- if we didn't find this element, then remove it from the common table
  26.                         print("Removing " .. toReturn[j])
  27.                         toReturn[j] = nil -- set to nil to remove without changing the array
  28.                     end
  29.                 end
  30.                 -- note that the resulting table has a bunch of empty indices after each loop          
  31.             end        
  32.         end
  33.     end
  34.     -- now we can do a single cleanup run
  35.     for j = 1, n do -- iterate over the maximum size of the table
  36.         local v = toReturn[j] -- get the value at the current index
  37.         if v then -- if we have a non-nil value
  38.             toReturn[j] = nil -- remove the value from its current index
  39.             table.insert(toReturn, v) -- put the value in the first empty index
  40.         end
  41.     end
  42.     return toReturn -- return everything that's common
  43. end
You'll want to check for typos, I haven't tested either code.

Last edited by kaels : 07-12-12 at 08:52 PM. Reason: modified second suggestion to behave better
  Reply With Quote
07-12-12, 09:40 PM   #3
Brusalk
An Aku'mai Servant
 
Brusalk's Avatar
AddOn Author - Click to view addons
Join Date: May 2010
Posts: 32
Thanks a bunch!

I ended up going with the second one, but I had to make one change to make sure it works right:
Code:
local function tableMerge(...) 
	if select('#', ...) < 3 then error("tableMerge", "inputs") return end
	local toReturn = {}
	for j,spellbar in pairs(...) do -- do it this way so we don't actually do anything to the tables passed in
		table.insert(toReturn, spellbar)
		print("Adding " .. spellbar)
	end
	local n = #toReturn -- get the size of the initial table - it won't get any bigger than this
	for i=2, select('#', ...) do
		local tab = select(i, ...)
		if type(tab) == "table" then --ignore any inputs which aren't tables
				-- we have to iterate with pairs because after the first run, our table will have empty indices
			for j, v in pairs(toReturn) do -- for everything that's common up to this point in iteration:
				print("Testing " .. v)
				local found = false
				for k,spellbar in pairs(tab) do -- for everything in this new table to check
					if v == spellbar then		-- if we found that the spellbar exists in this new table, say we found it
						found = true
						print("Found " .. spellbar)
					end
				end
				if not found then -- if we didn't find this element, then remove it from the common table
					print("Removing " .. toReturn[j])
					toReturn[j] = nil -- set to nil to remove without changing the array
				end
			end
			-- note that the resulting table has a bunch of empty indices after each loop			
		end
	end
	-- now we can do a single cleanup run
	for j = 1, n do -- iterate over the maximum size of the table
		local v = toReturn[j] -- get the value at the current index
		if v then -- if we have a non-nil value	
			toReturn[j] = nil -- remove the value from its current index
       		table.insert(toReturn, v) -- put the value in the first empty index
       	end
	end
	return toReturn -- return everything that's common
end

function testMerge()
	return tableMerge({1,2,3,4,5,6,7}, "test", {2,4,6}, {2,4})
end
I had to move the init of toReturn outside of the loop and just start iterating at the second input. Since the loop was able to remove the first entry it was adding the whole table back in again if the iteration nil'd the first entry in toReturn.

Simple fix though. Thank you!
  Reply With Quote
07-12-12, 09:50 PM   #4
Vrul
A Scalebane Royal Guard
 
Vrul's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2007
Posts: 404
Originally Posted by Brusalk View Post
I'm very perplexed because it's never actually finding any common elements, even though it should be (It's not removing 2,4,6 when it reaches the 3rd table)
Originally Posted by Brusalk View Post
What's even more perplexing to me is that it's never removing the 4th value, even though it should be as the last table is empty.
Those two quotes contradict one another. Since one of your inputs is an empty table you should never have any common values in the output. The reason you do get an output is somewhat answered by kaels, you are using table.remove on a table you are iterating over via pairs(). As table.remove can change table indexes and that causes undefined behavior (i.e. unexpected results) during ipairs/pairs iterations.

The fix is simple though. You are mixing dictionary indexing with array indexing in the same loop for no apparent reason. You know toReturn is an array as you set it up that way so you should always treat it that way. All you need to do is iterate over the table in reverse order so that table.remove (tremove to avoid an unnecessary table look up) doesn't throw everything off when it shifts the contents of the table.

Reusing your code with some minor tweaks:
Code:
local function tableMerge(...)
	if select('#', ...) < 3 then error("tableMerge", "inputs") return end
	local toReturn = { }
	for i = 1, select('#', ...) do
		local tab = select(i, ...)
		if type(tab) == 'table' then --ignore any inputs which aren't tables
			if toReturn[1] then
				for index = #toReturn, 1, -1 do -- for everything that's common up to this point in iteration:
					local value, found = toReturn[index], false
					print("Testing " .. value)
					for _, spellbar in pairs(tab) do -- for everything in this new table to check
						if value == spellbar then		-- if we found that the spellbar exists in this new table, say we found it
							found = true
							print("Found " .. spellbar)
							break
						end
					end
					if not found then -- if we didn't find this element, then remove it from the common table
						print("Removing " .. value)
						tremove(toReturn, index)
					end
				end				
				if not toReturn[1] then break end
			else
				for _, spellbar in pairs(tab) do -- do it this way so we don't actually do anything to the tables passed in
					toReturn[#toReturn + 1] = spellbar
					print("Adding " .. spellbar)
				end
			end			
		end
	end
	return toReturn -- return everything that's common
end

I also threw in a fix for a bug in your code:
Code:
function testMerge()
	return tableMerge({1,2,3,4,5,6,7}, "test", {}, {2,4,6})
end
That input in your original code (providing it worked as intended) would've had an output of {2,4,6}.
  Reply With Quote
07-12-12, 11:27 PM   #5
Brusalk
An Aku'mai Servant
 
Brusalk's Avatar
AddOn Author - Click to view addons
Join Date: May 2010
Posts: 32
Sorry. What I meant was that it wasn't finding any common elements before getting to the end of execution. So, when it was going from the first table of values to the second, it should've found some common ones and printed found: #, but it wasn't.
  Reply With Quote

WoWInterface » Developer Discussions » Lua/XML Help » I need some help!


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