Page 2 of 2

Re: math.clamp?

Posted: Thu Nov 19, 2015 8:06 pm
by Taehl
This actually works! :crazy: But edge-cases might exist?

Code: Select all

function clamp (low, num, high)
	local s = string
	local digitLength = s.len( math.floor(high) )
	local function c(n, p)
		return unpack( 					-- it's not a party without unpack and string.format
			{ s.byte (					-- convert to ASCII code
				s.format( "%0"..digitLength..".0f", n )
			, p ) +2 }						-- "0" is 48 and "9" is 57, so to align it, +2 is needed
		)
	end
	
	if c(high,i) < c(num,i) then		-- if the first digit of high is < num's digit
		return high
	end
	
	for i = 1, digitLength do			-- one at a time, going from left to right
		local l, n = c(low,i), c(num,i)	-- arrange numbers to evenly, get one digit from each
		if l > n then return low			-- compare them
		elseif l < n then return num
		end									-- if they're both equal, proceed to next digit
	end
	
	return num								-- low and num were entirely equal
end

Re: math.clamp?

Posted: Fri Nov 20, 2015 5:47 am
by ivan
I like mlepage's approach, it's clear and fast.
No variable arguments, no function calls, no intermediate tables or objects:

Code: Select all

function clamp(v, a, b)
   assert(a <= b, "invalid clamp range") -- optional: debug message
   if v < a then
      v = a
   elseif v > b then
      v = b
   end
   return v
end
Tahel/vrld approach is ok - it uses "math.min" and "math.max"
but works slower since these functions accept variable arguments.

Robin's method is cool but it creates an intermediate table and table.sort
would probably be slower for common cases with 2 arguments.

Re: math.clamp?

Posted: Fri Nov 20, 2015 11:59 pm
by vrld
ivan wrote:I like mlepage's approach, it's clear and fast.
No variable arguments, no function calls, no intermediate tables or objects:
[...]
Tahel/vrld approach is ok - it uses "math.min" and "math.max"
but works slower since these functions accept variable arguments.

Robin's method is cool but it creates an intermediate table and table.sort
would probably be slower for common cases with 2 arguments.
Oh yeah?

Code: Select all

function clamp_one(v, a, b)
   assert(a <= b, "invalid clamp range") -- optional: debug message
   if v < a then
      v = a
   elseif v > b then
      v = b
   end
   return v
end

function clamp_two(v, a, b)
   if v < a then
      v = a
   elseif v > b then
      v = b
   end
   return v
end

function clamp_two_and_a_half(v, a, b)
   if v < a then
      return v
   elseif v > b then
      return b
   end
   return v
end

function clamp_three(v, a, b)
    return math.max(a, math.min(v, b))
end

local min, max = math.min, math.max
function clamp_four(v, a, b)
    return max(a, min(v, b))
end

function clamp_now_thats_just_silly(...)
    local s = {...}
    table.sort(s)
    return s[2] --fixed //thelinx
end

function clamp_verbose(a, b, c)
     return (a < b and b < c and b) or (c < b and b < a and b) or (a < b and c < b and c) or (b < a and a < c and a) or c
end

function clamp_the_hard_way(low, num, high)
   local s = string
   local digitLength = s.len( math.floor(high) )
   local function c(n, p)
      return unpack(                -- it's not a party without unpack and string.format
         { s.byte (               -- convert to ASCII code
            s.format( "%0"..digitLength..".0f", n )
         , p ) +2 }                  -- "0" is 48 and "9" is 57, so to align it, +2 is needed
      )
   end

   if c(high,i) < c(num,i) then      -- if the first digit of high is < num's digit
      return high
   end

   for i = 1, digitLength do         -- one at a time, going from left to right
      local l, n = c(low,i), c(num,i)   -- arrange numbers to evenly, get one digit from each
      if l > n then return low         -- compare them
      elseif l < n then return num
      end                           -- if they're both equal, proceed to next digit
   end

   return num                        -- low and num were entirely equal
end

function time(f, N)
    local dts = {}
    for i = 1,N or 2000000 do
        local t = love.timer.getTime()
        f(math.random(), math.random(), math.random()+1)
        dts[#dts+1] = love.timer.getTime() - t
    end

    local t = 0
    for _, dt in ipairs(dts) do
        t = t + dt
    end
    local dt_mean = t / #dts
    local dt_std = 0
    for _, dt in ipairs(dts) do
        dt_std = dt_std + (dt_mean - dt) ^2
    end
    dt_std = math.sqrt(dt_std / (#dts-1))

    print(("  spent %ss in %d calls (%ss +- %ss per call)"):format(t, #dts, dt_mean, dt_std))
end

function love.load()
    love.timer.sleep(1)

    print("if-else with assert")
    time(clamp_one)

    print("\nif-else without assert")
    time(clamp_two)

    print("\nif-else without assert early out")
    time(clamp_two_and_a_half)

    print("\nmath.max/math.min")
    time(clamp_three)

    print("\nmath.max/math.min localized")
    time(clamp_four)

    print("\ntable.sort")
    time(clamp_now_thats_just_silly)

    print("\nlazy logic")
    time(clamp_verbose)

    print("\nstring compare")
    time(clamp_the_hard_way)

    love.event.quit()
end
Output (shortened)

Code: Select all

if-else with assert
  spent 0.93s in 2000000 calls (4.65e-07s +- 1.35e-07s per call)

if-else without assert
  spent 0.93s in 2000000 calls (4.67e-07s +- 1.24e-07s per call)

if-else without assert early out
  spent 0.95s in 2000000 calls (4.75e-07s +- 1.35e-07s per call)

math.max/math.min
  spent 0.98s in 2000000 calls (4.88e-07s +- 1.17e-07s per call)

math.max/math.min localized
  spent 0.94s in 2000000 calls (4.69e-07s +- 1.25e-07s per call)

table.sort
  spent 1.49s in 2000000 calls (7.46e-07s +- 8.85e-06s per call)

lazy logic
  spent 0.94s in 2000000 calls (4.69e-07s +- 1.16e-07s per call)

string compare
  spent 9.97s in 2000000 calls (4.99e-06s +- 1.45e-05s per call)
Here is a chart of the mean time and and the standard deviation of the time for each test case:
timeplot.png
timeplot.png (26.02 KiB) Viewed 6312 times
Here is a closer look:
timeplot-serious.png
timeplot-serious.png (25 KiB) Viewed 6312 times
None of the serious solutions is significantly faster than any other.

The lesson? Measure before you assume.

Re: math.clamp?

Posted: Sat Nov 21, 2015 5:50 am
by pgimeno
ivan wrote:I like mlepage's approach, it's clear and fast.
No variable arguments, no function calls, no intermediate tables or objects:

Code: Select all

function clamp(v, a, b)
   assert(a <= b, "invalid clamp range") -- optional: debug message
   if v < a then
      v = a
   elseif v > b then
      v = b
   end
   return v
end
Here's a small improvement over this (potentially one less branch for the case v < a, and potential improvements for the cases v == a and v == b -- not sure how smart the JIT compiler will be optimizing these):

Code: Select all

function clamp(v, a, b)
   assert(a <= b, "invalid clamp range") -- optional: debug message
   if v <= a then
      return a
   end
   if v >= b then
      return b
   end
   return v
end
It's similar to DeltaF1's but it doesn't subtract and also compares for equals, which should cause less branches if that case is hit.

Re: math.clamp?

Posted: Sat Sep 08, 2018 11:35 am
by Descaii

Code: Select all

function clamp(v,a,b)
    return (v < a and a) or (v > b and b) or v
end

Re: math.clamp?

Posted: Tue Jun 30, 2020 10:43 am
by wazoowazoo
Here is my own version, actually found it on the web and it was meant for a lower level language where you could convert bool to int :

Code: Select all

function clamp(x, a, b)
	--a is lower bound, b is upper bound because why not ?
	local function int(bool)
		if bool then return 1 end
		return 0
	end
	
	return a * int(x < a) + b * int(x > b) + x * int(x >= a and x <= b)
end

Re: math.clamp?

Posted: Sat Jul 04, 2020 6:34 am
by ivan
wazoowazoo, that piece of code creates a new closure each time "clamp" is called.
At least move the "int" function outside of "clamp".

Re: math.clamp?

Posted: Sat Jul 04, 2020 1:12 pm
by pgimeno
Agreed with ivan, but that's full of branches anyway. LuaJIT performs well with math.max and math.min, so you can use this:

Code: Select all

local min, max = math.min, math.max
function clamp(x, a, b)
  return max(a, min(b, x))
end

Re: math.clamp?

Posted: Mon Jul 06, 2020 4:05 am
by JJSax
Nixola wrote: Mon Apr 27, 2015 6:58 pm Maybe he was searching, noticed this and didn't notice the topic was so old
EDIT: why don't we turn this topic into a list of original and weird ways to write math.clamp?
* actively resists the urge to post an obnoxiously long, highly overcomplicated, Rube Goldberg like method to clamp numbers.