#494 enhancement
William Warby

Feature Request: Number.constrain

Reported by William Warby | December 16th, 2008 @ 04:53 PM | in After 1.7

I have a suggestion for a new method on the Number prototype:


Number.prototype.constrain = function(from, to) {
  return this < from ? from : (this > to ? to : this);
};

Comments and changes to this ticket

  • Juriy Zaytsev

    Juriy Zaytsev December 16th, 2008 @ 06:37 PM

    • Milestone set to 1.7
    • Assigned user set to “Andrew Dupont”
    • State changed from “new” to “enhancement”

    +1

    Few corresponding unit tests would be great!

  • William Warby

    William Warby December 16th, 2008 @ 07:08 PM

    Just found something very interesting whilst testing this. When the number was within the range, it was being returned as an Object rather than a Number - check this out:

    
    Number.prototype.bar = function() { return this; };
    var foo = 1;
    document.write('Before: ' + typeof foo + '<br />'); // <--- Number
    document.write('After: ' + typeof foo.bar()); // <--- Object
    

    parseFloat fixes it:

    
    Number.prototype.constrain = function(from, to) {
    	return this < from ? from : (this > to ? to : parseFloat(this));
    }
    

    Regarding unit tests: very happy to help with these but I'm afraid I don't know anything about unit tests - how to write them or in what format they need to be in. Is there a repository of existing ones I can use as examples somewhere?

  • Juriy Zaytsev

    Juriy Zaytsev December 16th, 2008 @ 10:32 PM

    Yeah, if I'm not mistaken, this must always reference an object, so when method is called within a context of primitive, that primitive is automatically converted to an object:

    
    String.prototype.bar = function(){ return this; };
    typeof 'foo'; // "string"
    typeof 'foo'.bar(); // "object"
    

    You can take a look at tests within /test/unit folder (after downloading/cloning prototype git repository)

  • Xanadu

    Xanadu December 19th, 2008 @ 11:35 PM

    What's the expected behaviour when the min (from) is higher max (to)?

    
    var two = 2;
    var four = 4;
    document.write(two.constrain(3,1)); // 3
    document.write(four.constrain(3,1)); // 1
    
  • Juriy Zaytsev

    Juriy Zaytsev December 20th, 2008 @ 07:21 AM

    @Xanadu,

    Good point. We can't rely on first value being smaller than the second one.

    
    Number.prototype.constrain = function(v1, v2) {
      var max = Math.max(v1, v2), min = Math.min(v1, v2);
      return this > max ? max : this < min ? min : parseFloat(this);
    };
    
    console.assert((5).constrain(1,10) === 5);
    console.assert((5).constrain(10,1) === 5);
    console.assert((5).constrain(1,3) === 3);
    console.assert((5).constrain(3,1) === 3);
    console.assert((5).constrain(1000, 1002) === 1000);
    console.assert((5).constrain(1,1) === 1);
    console.assert((5).constrain(-12,-10) === -10);
    console.assert((5.87).constrain(1.43, 25.72) === 5.87);
    
  • Radoslav Stankov

    Radoslav Stankov December 20th, 2008 @ 10:08 AM

    I think that parseInt should be parseFloat so constrain can work with float numbers to.

  • William Warby

    William Warby December 20th, 2008 @ 10:23 AM

    Hi guys,

    Very pleased to see this idea is being picked up and run with. I did think about handling reversed max/min when I made the suggestion, but I figured you guys would kill that idea on efficiency grounds. It does make sense though. It should definitely be parseFloat rather than parseInt

    Do the examples provided by Juriy count as test cases? I haven't had a chance to learn what a git repository is yet so that I can do some myself...

  • Juriy Zaytsev

    Juriy Zaytsev December 20th, 2008 @ 02:58 PM

    @William, those assertions are pretty much similar to what we need. Fwiw, I just tested alternative implementation, but it was a tiny bit slower than the original one (and is probably more memory consuming)

    
    (function(){
      var cmpFn = function(a,b){ return a-b };
      Number.prototype.constrain2 = function(v1, v2) {
        return parseFloat([v1, this, v2].sort(cmpFn)[1]);
      }
    })();
    
  • Spezi

    Spezi December 20th, 2008 @ 06:11 PM

    Another possible implementation switching the arguments if they are not in the expected order:

    
    Number.prototype.constrain = function(min, max) {
    	return min > max ? this.constrain(max, min) : this > max ? max : this < min ? min : parseFloat(this);
    };
    
  • Juriy Zaytsev

    Juriy Zaytsev December 20th, 2008 @ 06:54 PM

    @Spezi,

    Nice use of recursion : ) Could you compare your version performance to previous ones?

  • Spezi

    Spezi December 20th, 2008 @ 08:39 PM

    to Juriy:

    It indeed seems so be a bit more faster than yours. I could only hardly believe it but some more tests showed slight difference (delta of around 4-10ms for 200 calls).

  • Radoslav Stankov

    Radoslav Stankov December 21st, 2008 @ 12:13 AM

    As I was walking to home from work I thought about writing something like this:

    Number.prototype.constrain = function(min, max){
        return min > max ? this.constrain(max, min) : Math.max(min, Math.min(max, this));
    };
    

    In my firefox tests parseFloat wasn't needed. Tomorrow I will try to make some test for its performance.

  • Juriy Zaytsev

    Juriy Zaytsev December 21st, 2008 @ 01:44 AM

    The results vary based on the actual "range" given, but the second version (by Spezi) is generally the fastest one, as it avoids the Math.max/Math.min altogether.

    Here's the test I used (on FF3.0.4)

    
    Number.prototype.constrain = function(v1, v2) {
      var max = Math.max(v1, v2), min = Math.min(v1, v2);
      return this > max ? max : this < min ? min : parseFloat(this);
    };
    
    Number.prototype.constrain2 = function(min, max) {
      return min > max 
        ? this.constrain(max, min) 
        : this > max ? max : this < min ? min : parseFloat(this);
    };
    
    Number.prototype.constrain3 = function(min, max){
      return min > max 
        ? this.constrain(max, min) 
        : Math.max(min, Math.min(max, this));
    }
    
    var num   = 5, 
        v1    = 1, 
        v2    = 3, 
        limit = 20000;
    
    
    console.time(1);
    for (var i=0; i<limit; i++) num.constrain(v1, v2);
    console.timeEnd(1);
    
    console.time(2);
    for (var i=0; i<limit; i++) num.constrain2(v1, v2);
    console.timeEnd(2);
    
    console.time(3);
    for (var i=0; i<limit; i++) num.constrain3(v1, v2);
    console.timeEnd(3);
    @@
    
  • Xanadu

    Xanadu December 25th, 2008 @ 05:26 PM

    Ran the test 3 times with Firefox 3.0.5, here are the averages: 1: 161ms 2: 42ms 3: 56ms

  • William Warby

    William Warby December 29th, 2008 @ 01:59 PM

    Hi guys. I have another suggestion on this theme. Whilst using my constrain function I found that I was often writing something like this to test whether a number was within a range:

    
    (x === x.constrain(1,10)) //Pretty concise
    (My.very.long.path.to.object.property === My.very.long.path.to.object.property.constrain(1,10)) //Not so consise
    

    So it occurred to me that perhaps Number.within would also be useful:

    
    Number.prototype.within = function(from, to) {
    	return parseInt(this, 10) === this.constrain(from, to);
    }
    

    If you agree that this would be a useful addition, there are loads of ways to write it and I'll leave it to the experts to work your magic in determining the best implementation.

  • Juriy Zaytsev

    Juriy Zaytsev December 29th, 2008 @ 05:54 PM

    @William

    
    $R(1,10).include(5); // true
    $R(1,10).include(12); // false
    
  • William Warby

    William Warby December 29th, 2008 @ 06:54 PM

    Juriy,

    That hadn't occurred to me ;) I still prefer a .within method so I'll just keep that as a prototype in my own codebase. The only thing I would ask is how efficient the method you've indicated is. I'll run some tests when I have a spare moment.

  • Juriy Zaytsev

    Juriy Zaytsev December 29th, 2008 @ 09:26 PM

    @William, most likely not too efficient as $R creates an instance of ObjectRange every time it's called.

  • William Warby

    William Warby December 29th, 2008 @ 09:52 PM

    Juriy,

    My results are as follows:

    
    Number.prototype.constrain = function(min, max) {
    	return min > max 
    	? this.constrain(max, min) 
    	: this > max ? max : this < min ? min : parseFloat(this);
    };
    
    Number.prototype.within1 = function(min, max) {
    	return $R(min, max).include(5);
    };
    
    Number.prototype.within2 = function(min, max) {
    	parseFloat(this) === this.constrain(min, max);
    };
    
    Number.prototype.within3 = function(min, max) {
    	return min > max ? this.within3(max, min) : this >= min && this <= max;
    };
    
    var num = 5, 
    v1    = 1, 
    v2    = 3, 
    limit = 20000;
    
    console.time(1);
    for (var i=0; i<limit; i++) num.within1(v1, v2);
    console.timeEnd(1);
    
    console.time(2);
    for (var i=0; i<limit; i++) num.within2(v1, v2);
    console.timeEnd(2);
    
    console.time(3);
    for (var i=0; i<limit; i++) num.within3(v1, v2);
    console.timeEnd(3);
    

    Average results:

    within1: 182ms within2: 69ms within3: 38ms

    Does that qualify it as a feature request?

  • Juriy Zaytsev

    Juriy Zaytsev December 29th, 2008 @ 10:30 PM

    Almost 5 times faster and much more memory efficient. I would say, yes, looks like a reasonable enhancement.

  • Samuel Lebeau

    Samuel Lebeau August 26th, 2009 @ 08:04 AM

    Another, probably nicer way to obtain the primitive number is to use the Number factory method, just as we do return String(this) in String#scan.

    typeof new Number(3);
    // => "object"
    
    typeof Number(new Number(3));
    // => "number"
    
  • T.J. Crowder

    T.J. Crowder November 16th, 2009 @ 04:50 PM

    • Assigned user cleared.

    [responsible:none bulk edit command]

  • Victor

    Victor February 3rd, 2010 @ 06:24 PM

    Which result is expected if number will be NaN? E.g. parseInt("zzz",).constrain(1,999) ?

  • Tobie Langel

    Tobie Langel March 1st, 2010 @ 01:25 AM

    • Tag set to section:lang
  • Andrew Dupont

    Andrew Dupont October 17th, 2010 @ 06:58 AM

    • Milestone changed from 1.7 to After 1.7
    • Importance changed from “” to “Low”

Please Sign in or create a free account to add a new ticket.

With your very own profile, you can contribute to projects, track your activity, watch tickets, receive and update tickets through your email and much more.

New-ticket Create new ticket

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile ยป

The Prototype JavaScript library.

Shared Ticket Bins

Pages