math.clamp?

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
Taehl
Dreaming in associative arrays
Posts: 1025
Joined: Mon Jan 11, 2010 5:07 am
Location: CA, USA
Contact:

Re: math.clamp?

Post 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
Earliest Love2D supporter who can't Love anymore. Let me disable pixel shaders if I don't use them, dammit!
Lenovo Thinkpad X60 Tablet, built like a tank. But not fancy enough for Love2D 0.10.0+.
User avatar
ivan
Party member
Posts: 1911
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: math.clamp?

Post 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.
User avatar
vrld
Party member
Posts: 917
Joined: Sun Apr 04, 2010 9:14 pm
Location: Germany
Contact:

Re: math.clamp?

Post 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 5675 times
Here is a closer look:
timeplot-serious.png
timeplot-serious.png (25 KiB) Viewed 5675 times
None of the serious solutions is significantly faster than any other.

The lesson? Measure before you assume.
I have come here to chew bubblegum and kick ass... and I'm all out of bubblegum.

hump | HC | SUIT | moonshine
User avatar
pgimeno
Party member
Posts: 3548
Joined: Sun Oct 18, 2015 2:58 pm

Re: math.clamp?

Post 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.
Descaii
Prole
Posts: 1
Joined: Sat Sep 08, 2018 11:31 am

Re: math.clamp?

Post by Descaii »

Code: Select all

function clamp(v,a,b)
    return (v < a and a) or (v > b and b) or v
end
User avatar
wazoowazoo
Prole
Posts: 16
Joined: Sun Jan 22, 2017 2:47 pm

Re: math.clamp?

Post 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
User avatar
ivan
Party member
Posts: 1911
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: math.clamp?

Post 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".
User avatar
pgimeno
Party member
Posts: 3548
Joined: Sun Oct 18, 2015 2:58 pm

Re: math.clamp?

Post 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
JJSax
Prole
Posts: 47
Joined: Fri Apr 04, 2014 3:59 am

Re: math.clamp?

Post 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.
Post Reply

Who is online

Users browsing this forum: No registered users and 165 guests