New Lua functions, math.sign and math.clamp

New math functions to help you do some math! Courtesy of @0xBAADF00D

number math.clamp(number x, number min, number max)

  • If min is greater than max, an error is thrown indicating that min must be less than max
  • If x is less than min, it returns min
  • If x is greater than max, it returns max
  • If any argument is missing, nil, or otherwise not a number, an error will be thrown stating this
  • Otherwise, it returns val

number math.sign(number x)
Returns 1 if x is greater than 0, -1 if x is less than 0, or 0 if x is 0.

81 Likes

Sweet. @0xBAADF00D can we get a native rounding function? I want to line up a number to a 0.25 interval, for instance. I could do a few floors/ceilings and multiply/divide but that’s a lot of code just to make 0.23 round to 0.25

20 Likes

I’ll give it some thought. If you can help find examples in other codebases, that’d help a lot. I know C# has Math.Sign, at least. It also has Math.Round but it has a lot of overloads and I’m not sure we want that sort of complexity.

4 Likes

Thanks. I don’t know about other codebases but I do know that almost all of my projects have used number clamping using some ugly code.

is there a reason why we should use these instead of just ternary operations, or is this purely for developers who are still learning?

ie, what is more efficient?

math.clamp(x, min, max)

vs

x < min and min or x > max and max or x

and then

math.sign(x)

vs

x > 0 and 1 or x < 0 and -1 or 0
3 Likes

I’d assume the bridge between lua and c++ would, here, cause the most delay. I’d test it out myself but I’m on my phone right now – mostly asking for archiving purposes so that in the future if a new developer is looking for the answer they can find it! :slight_smile:

Just did a test for clamp. @Polymorphic


local t = tick()
for i = 0,100 do
	local x = math.clamp(i,20,60)
end
local t2 = tick()
print("Test1",t2-t) -- 1.2874603271484e-05

local t = tick()
for i = 0,100 do
	local x = i < 20 and 20 or i > 60 and 60 or x
end
local t2 = tick()
print("Test2",t2-t)-- 4.7683715820313e-06

EDIT: Did a test where I set clamp as variable so no tables are in the way. The difference is so small it doesn’t matter. Use math.clamp for neatness is what I would say.

8 Likes

Figured as much, like I said before I assume most of the overhead comes from both the table index and the expense in going from Lua to C.

Ternary looks bad. In pretty much every case where you care so much about performance that you’d use ternary over clamp/sign, your bottleneck is actually somewhere else (like setting a property).

6 Likes

It’s more about connivence and ease of use, as doing this in Lua will be faster because of the overhead from transitioning between Lua and C++.

4 Likes

Performance wasn’t really a concern because either way is pretty fast. It’s 100% about code readability. I did do some testing, but the difference is really negligible. Using ternary is even a little faster. Sharksie is completely right.

There’s a good reason why people say not to micro-optimize. It’s because your time is better spent finding less complex algorithms. Understanding big-O notation and using it to judge your algorithms is a huge first step!

7 Likes

<3!

In the future, could we possibly have a function called math.movetowards(current, target, maxDelta)?
It would behave like this:

local function moveTowards(current, target, maxDelta)
	if current < target then
		return math.min(target,current + maxDelta)
	elseif current > target then
		return math.max(target,current - maxDelta)
	else
		return target
	end
end

I use this function a lot in my code, mostly in heartbeat threads for doing visual transitions between states.
It would be handy to have something like it built-in.

6 Likes

That, plus the Lua pattern of A and B or C also does not always behave like a ternary, which can be a pitfall if you get comfortable using it with literals and then try to use it to conditionally copy the value of a variable in position B that happens to end up having the value of false or nil, because:

false and false or true -> true     false ? false : true -> true
true and false or true -> true(!)   true ? false : true -> false
false and true or true -> true      false ? true : true -> true
true and true or true -> true       true ? true : true -> true
5 Likes

You mean you don’t want to use the new math.sign() function to rewrite it like this? :laughing:

local function moveTowards(current, target, maxDelta)
	return current + math.sign(target-current) * math.min(maxDelta, math.abs(target-current))
end
5 Likes

New clamp is actually twice as fast as ternary in unprivileged code.
The speed gain is less significant in plugins–blame the mysterious dispatch overhead.

6 Likes

not entirely sure why you would use ternary for clamp. Is it faster than
math.min (max, math.max (x, min))

According to this glorious thread, ternary is faster than min-max.
“Which clamp is faster” is almost never a question that you need to ask, though.

Ayoooo that was my solutionnnn

Awesome. But as always, I’m bad at being grateful for what we have and always want more (what a materialistic world we live in). So I’d really like us to have a math.round function too :slight_smile:

3 Likes