views:

575

answers:

4

I was wondering what was the most efficient way to rotate a JavaScript array.

I came up with this solution, where a positive n rotates the array to the right, and a negative n to the left (-length < n < length) :

Array.prototype.rotate = function( n ) {
  this.unshift( this.splice( n, this.length ) )
}

Which can then be used this way:

var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
months.rotate( new Date().getMonth() )

My original version above has a flaw, as pointed out by Christoph in the comments bellow, a correct version is (the additional return allows chaining):

Array.prototype.rotate = function( n ) {
  this.unshift.apply( this, this.splice( n, this.length ) )
  return this;
}

Is there a more compact and/or faster solution, possibly in the context of a JavaScript framework? (none of the proposed versions bellow is either more compact or faster)

Is there any JavaScript framework out there with an array rotate built-in? (Still not answered by anyone)

+2  A: 

I would probably do something like this:

Array.prototype.rotate = function(n) {
    return this.slice(n, this.length).concat(this.slice(0, n));
}

Edit    Here’s a mutator version:

Array.prototype.rotate = function(n) {
    while (this.length && n < 0) n += this.length;
    this.push.apply(this, this.splice(0, n));
    return this;
}
Gumbo
keep in mind that this function keeps the original array unchanged
Christoph
It needs to modify the original Array (this), just like push, pop, shift, unshift, concat, and splice do. Other than that this is valid.
Jean Vincent
@Gumbo, why the while loop? We don't need to make n positive, splice works with negative values just as well.For the end, this is right but pretty much Christoph version which got it right first without the overloading caveat.
Jean Vincent
@Gumbo, correction on my previous comment, negative numbers work only the splice( n. this.length ) version. Using splice( 0, n ) as in your version, requires a positive int.
Jean Vincent
A: 

How about incrementing a counter and then getting the remainder of a division by the array length to get where you are supposed to be.

var i = 0;
while (true);
{
    var position = i % months.length;
    alert(months[position]);
    ++i;
}

Language syntax aside this should work fine.

tgandrews
This does not rotate the Array in any way.
Jean Vincent
True (as stated in the answer), but it saves having to rotate the array.
tgandrews
But the whole point of this IS to rotate an Array, per the title, not to display an array from a certain offset. If you'd use the same loop to rebuild the new array it would terribly slower than other versions using native Array methods. Plus you forgot to break out of the infinite loop.
Jean Vincent
+5  A: 

Type-safe, generic version which mutates the array:

Array.prototype.rotate = (function() {
    // save references to array functions to make lookup faster
    var push = Array.prototype.push,
        splice = Array.prototype.splice;

    return function(count) {
        var len = this.length >>> 0, // convert to uint
            count = count >> 0; // convert to int

        // convert count to value in range [0, len[
        count = ((count % len) + len) % len;

        // use splice.call() instead of this.splice() to make function generic
        push.apply(this, splice.call(this, 0, count));
        return this;
    };
})();

In the comments, Jean raised the issue that the code doesn't support overloading of push() and splice(). I don't think this is really useful (see comments), but a quick solution (somewhat of a hack, though) would be to replace the line

push.apply(this, splice.call(this, 0, count));

with this one:

(this.push || push).apply(this, (this.splice || splice).call(this, 0, count));

Using unshift() instead of push() is nearly twice as fast in Opera 10, whereas the differences in FF were negligible; the code:

Array.prototype.rotate = (function() {
    var unshift = Array.prototype.unshift,
        splice = Array.prototype.splice;

    return function(count) {
        var len = this.length >>> 0,
            count = count >> 0;

        unshift.apply(this, splice.call(this, count % len, len));
        return this;
    };
})();
Christoph
Nice caching of `Array.prototype` methods! +1
J-P
Very nice bullet proof implementation but in general I would prefer to rely on exceptions, or just plain bad responses, for bad usage. This to keep the code clean and fast.It's the responsibility of the user to pass correct parameters or pay the consequences. I don't like the penalty for good users.Other than that this is perfect because it does modify the Array as requested and it does not suffer from the flaw in my implementation which unshifted an array instead of the individual elements as you noted.Returning this is also better to allow chaining. So thanks.
Jean Vincent
What is the cost of the closure here, just to cache push and splice?
Jean Vincent
@Jean: well, closures captures the whole scope chain; as long as the outer function is top-level, the impact should be negligible and it's O(1) anyway, whereas looking up the Array methods is O(n) in the number of invocations; optimizing implementations might inline the lookup so there won't be any gain, but with the rather stupid interpreters we've had to deal with for a long time, caching variables in a lower scope could have a significant impact
Christoph
as for the type conversions: they are done to be consistent with the functions defined by the ECMA-standard; actually, standard functions usually wouldn't check the types, but just do the conversion and plunge ahead; I'll edit to match this style, which should result in a slight performance gain...
Christoph
@Christoph, caching push and splice is probably ok most of the time but would break any overloading of push and splice. What's you take on this?Also what if the same rotate function is reused on a different class (so to speak), then push and splice would be forced on Array:`Roles.prototype.rotate = Array.prototype.rotate`
Jean Vincent
@Jean: you have to decide if you want `rotate()` to work with any array-like object (eg `arguments`) by only using generic array functions, or to work with any objects which provides their own implementations of these methods; imo the former is more 'in the spirit' of the standard and more useful as well, as my version of `rotate()` depends on the somewhat specialised `splice()`, ie I don't think there would be a lot of use-cases for the latter; you're right that you should call the overloaded methods when creating your own inheritance hierarchies, though; I'll add some comments to my answer
Christoph
@Christoph, I agree with you regarding use cases, especially because the original question really is about Arrays and nothing else. However it would still be nice as an added bonus to be able to reuse the function on other objects. Now regarding overloading, does `this.push || push` allow overloading by an intermediate class between this and Array? And, similar question, does `this.push.apply` allow overloading by an intermediate class? Finally, what is the benefit of the alternate version, wouldn't `this.push` always be faster than `this.push || push`?
Jean Vincent
Ignore my last question, the alternate version helps to use rotate on objects that do not implement `push` or `splice`. The 2 other questions are still valid although I could find the answer myself if I was not that lazy. I also like your answers (that's the drawback of providing good answers, people bother you with questions they could answer themselves).
Jean Vincent
Regarding the >>> operator, does it really improve performances considering that JS always converts back integers to double? This could result into length being converted to integer, shifted (not), then converted back to double, and finally used as a double later. This could decrease performances unless some JS implementation actually stores (unsigned) integers in variables. In which case one might think that such an implementation would store this.length as an integer and therefore >>> 0 would not improve anything. Also this.length would already be unsigned.
Jean Vincent
Running some performance tests (FF 3.5 only), I found a very slight performance improvement using `unshift.apply( this, splice.call( this, count % this.length, this.length ) );` over the use of two intermediate variables and the shift operators. I have also tried to put the shift operators directly into the modulo expression and it was slightly slower.
Jean Vincent
I figured out the answer to method overloading by intermediate classes. The prototype of the object class will have the reference to the last overloaded definition. So in short, it will work.
Jean Vincent
@Jean: `this.push` is a regular property lookup, ie the prototype chain is followed and intermediate 'classes' will be visited; the shifting (`>>>` and `>>`) is done for type conversion, not performance; if you read the ECMA-spec, you'll see that the built-in functions use things like `ToUint32()` and `ToInt32()`, which are not exposed to the programmer, but can be emulated by unsigned and signed shifts by `0` (a noop for ints); about FF 3.5: with TraceMonkey (Mozillas new JS engine), some of the optmization approaches suited for more primitive interpreters might indeed no longer apply
Christoph
Running performance tests under FF 3.5, IE8, Safari 4.0, Opera 10.10, with a simplified version of yours (without the count, len and count % len), I found almost no difference between the closure version and the direct version using this.unshift and this.splice. Statistically though, the version without the closure seems a little faster in IE, Safari, but not in FF. There is no clear-cut winner because one cannot control garbage collection and other parasites. So as much as I like the idea of using a closure and caching Array methods, I don't see any real benefit of doing so in this context.
Jean Vincent
Also, running these tests in Opera 10.10 proved very slow, about 50 times slower than IE8 and 200 times slower than FF3.5. Any idea why? Maybe they don't have native unshift and/or splice?
Jean Vincent
In http://www.w3schools.com/jsref/jsref_unshift.asp, they mention that `The unshift() method is supported in all major browsers, except Internet Explorer`. It is certainly no longer true in IE8, but would you happen to know which version of IE first supported unshift?
Jean Vincent
@Jean: `unshift()` was added to IE in version 5.5; I don't know why mutating the arrays is so slow in Opera: either their implementation of either `unshift()` or `splice()` is broken, or there's some fundamental problem with how sparse arrays are represented internally; in most JS engines, sparse arrays will be stored as actual arrays - which seems reasonable, so I don't know why the Opera developers should do this differently; I don't know how you did your benchmarking, but http://pastebin.com/f2793ae87 suggests that caching will increase performance at least in IE8, FF3.5 and Opera10
Christoph
ups.. that should read _dense_ arrays, not sparse ;) there's another problem with Opera, though: using the same array in the benchmark code again to time the second function will result in a huge performance hit (ie the second function tested will always be slower); if you use a new array, you'll see that the caching version seems to be faster, but results are inconsistent (too lazy to gather some real statistics ;)); that reusing the array results in a severe performance penalty might suggest that there really is something wrong with Opera's array implementation
Christoph
My benchmark (http://pastebin.com/f51f54816) is slightly different than yours and shows that between different rounds the time returned varies sometimes to the advantage of one version, sometimes to the advantage of another version. It also shows that some garbage collector or other parasite (windows here) process is messing up with the benchmark. So unless the difference was always in one direction, there is no way to call a winner here. At least not on my machine under FF and other browsers.
Jean Vincent
This test also clearly shows some kind of leak in Opera 10.10. We should probably report it.
Jean Vincent
I have reported the bug to Opera.
Jean Vincent
Note also that the benchmark shows that the non-closure version runs systematically faster on Safari 4.0 for Windows Vista. The difference is about 5%.
Jean Vincent
A: 

If your array is going to be large and/or you are going to rotate a lot, you might want to consider using a linked list instead of an array.

trinithis
Agreed, but the question pertains to Arrays, and more specifically to extend Array verbs.
Jean Vincent