It is currently November 14th, 2019, 7:44 am

[Tip] Nested Expressions

Discuss the use of Lua in Script measures.
User avatar
smurfier
Moderator
Posts: 1937
Joined: January 29th, 2010, 1:43 am
Location: Willmar, MN

[Tip] Nested Expressions

smurfier » February 14th, 2014, 3:58 pm

Lua can get pretty complicated, especially when you want to create strings using values based on other values. This usually also includes a lot of concatenation.

'On ' .. os.date('%A') .. ' Jane goes to ' .. Locations[os.date('%a')] .. '.'

Using a little ingenuity this task could get a lot simpler.

'On {LongDay} Janes goes to {{ShortDay}Location}.'

This is something called Nested Expressions. This could be an insurmountable task, but thankfully it's not. As long as we follow some rules we can use one of Lua's built in abilities for this.

The most important rule we need to follow is that our expression must begin and end with different characters. For example, let's use the curly brackets (i.e. '{Expression}'). If both characters were the same Lua wouldn't be able to tell the difference between the beginning and the end.

Now we need to define a function for our needs. Yes, we really do need a function for this. Since we need to follow the same steps for each nested expression we will need to call the function inside itself.

Code: Select all

function nested(line)
	-- Do Something Here
end
Now we need to start adding the bits that do all the work. What makes this all possible is the balanced expression matching capability of Lua. This would be the %b pattern matching class. We use it by placing the beginning and ending characters after it. More detail can be found here.

Code: Select all

function nested(line)
	return (line:gsub('%b{}', function(input)
		-- Do Something Here
	end))
end
Now that we have our expression we need to do something with it. The first thing is to call our function again so we can deal with the nested expressions first. We'll have to strip off the beginning and ending characters or we'll just create an infinite loop.

Code: Select all

function nested(line)
	return (line:gsub('%b{}', function(input)
		local newline = nested(input:match('^{(.-)}$'))
		--Do Something Here
	end))
end
Now we need to start doing something with our expression. This requires some testing. The first thing is to make sure that what we have indeed received a valid expression and act accordingly. If it's not a valid expression we need to return exactly what we received, including the beginning and ending characters.

You can do whatever you want to your expression, just be sure you return something. It is important to remember to use the newline variable in whatever you do from this point on. This is the variable that has all of the nested expressions already evaluated.

Code: Select all

function nested(line)
	return (line:gsub('%b{}', function(input)
		local newline = nested(input:match('^{(.-)}$'))
		if newline ~= '' then
			return --Do Something Here
		else
			return string.format('{%s}', newline)
		end
	end))
end
There is one very important thing to note. Our function as written must be a global function. If you want it to be local there is a way to fix this. We must pass the function to itself. It's best not to think about this too hard.

Code: Select all

local nested = function(line, self)
	return (line:gsub('%b{}', function(input)
		local newline = self(input:match('^{(.-)}$'), self)
		if newline ~= '' then
			return --Do Something Here
		else
			return string.format('{%s}', newline)
		end
	end))
end

SomeString = nested('{Variable{AnotherVariable}}', nested)
EXAMPLES

Here's something extremely simple. We're just going to see what Jane is doing today.

Code: Select all

Variables = {
	LongDay = os.date('%A'),
	ShortDay = os.date('%a'),
	SunLocation = 'Church',
	MonLocation = 'Grocery Store',
	TueLocation = 'the Salon',
	WedLocation = 'a PTA meeting',
	ThuLocation = 'a concert',
	FriLocation = 'the bar',
	SatLocation = 'the beach',
}

function nested(line)
	return (line:gsub('%b{}', function(input)
		local newline = nested(input:match('^{(.-)}$'))
		if Variables[newline] then
			return Variables[newline]
		else
			return string.format('{%s}', newline)
		end
	end))
end

SomeString = nested('On {LongDay} Janes goes to {{ShortDay}Location}.')
This one is much more complicated. It's used to replace nested Rainmeter measures and variables.

Code: Select all

function Replace(input)
	return (input:gsub('(%b[])', function(line)
		local newline = Replace(line:match('^%[(.-)]$'))
		local typ, name = newline:match('(.)(.+)')
		-- Establish an string to return in case we encounter an error
		local ErrorString = ('[%s]'):format(newline)
		-- Make allowance for escaped expression
		if newline:match('^%*(.-)%*$') then
			return newline:gsub('^%*(.-)%*$', '[%1]')
		-- Measures / Meters
		elseif typ == '&' then
			-- Make allowance for section variables
			local section = name:match('([^:]+)')
			-- Check if Measure / Meter exists
			if SKIN:GetMeasure(section) or SKIN:GetMeter(section) then
				-- Use existing function for maximum flexibility
				return SKIN:ReplaceVariables(('[%s]'):format(name))
			-- Measure / Meter does not exist
			else
				return ErrorString
			end
		-- Variable
		elseif typ == '#' then
			return SKIN:GetVariable(name) or ErrorString
		-- Did not define anything we are looking for
		else
			return ErrorString
		end
	end
	))
end

SomeLine = Replace('[&SomeMeasure[#SomeVariable[&SomeMeter:X]]]')
Here's an example that uses a rapper function so that we can specify a table of variables when we call the function.

Code: Select all

function SomeFunction(InputExpression, FuncTbl)
	local nested = function(line, self)
		return (line:gsub('%b{}', function(input)
			local newline = self(input:match('^{(.-)}$'), self)
			if FuncTbl[newline] then
				return FuncTbl[newline]
			else
				return string.format('{%s}', newline)
			end
		end))
	end
	
	return nested(InputExpression, nested)
end

Variables = {
	LongDay = os.date('%A'),
	ShortDay = os.date('%a'),
	SunLocation = 'Church',
	MonLocation = 'Grocery Store',
	TueLocation = 'the Salon',
	WedLocation = 'a PTA meeting',
	ThuLocation = 'a concert',
	FriLocation = 'the bar',
	SatLocation = 'the beach',
}

SomeString = SomeFunction('On {LongDay} Janes goes to {{ShortDay}Location}.', Variables)
GitHub | DeviantArt | Tumblr
This is the song that never ends. It just goes on and on my friends. Some people started singing it not knowing what it was, and they'll continue singing it forever just because . . .