From 1c17b6381a904906c495b02fb2105f6445c05f8d Mon Sep 17 00:00:00 2001 From: Sam Stephenson Date: Mon, 9 Jul 2007 18:55:58 +0000 Subject: [PATCH] prototype: Enhance the Enumerable and Array APIs to more closely match those of JavaScript 1.6 as implemented in Firefox 1.5. Closes #6650, #8409. --- CHANGELOG | 7 ++++ src/array.js | 33 +++++++++++------ src/base.js | 2 + src/enumerable.js | 78 ++++++++++++++++++++++++--------------- test/unit/array.html | 27 ++++++++++++++ test/unit/base.html | 42 ++++++++------------- test/unit/enumerable.html | 55 +++++++++++++++++++-------- 7 files changed, 162 insertions(+), 82 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1f0d6ff..b5eb35c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ *SVN* +* Enhance the Enumerable and Array APIs to more closely match those of JavaScript 1.6 as implemented in Firefox 1.5. Closes #6650, #8409. [Mislav Marohnić, Sylvain Zimmer] + - Add Array#lastIndexOf, and change Array#indexOf not to overwrite the native method. + - Make Enumerable use Array.prototype.forEach instead of _each when possible (slight speed increase). + - Add "filter", "entries", "every", and "some" Array aliases. + - All Enumerable methods now have an additional parameter, "context", which, if present, specifies the object to which the iterators' "this" is bound. + - Function#bind and #curry now return the receiving function if the binding object is undefined. + * Temporary workaround for Prototype.BrowserFeatures.SpecificElementExtensions incorrectly evaluating to true on iPhone. (needs further investigation) [sam] * The action for Form#request defaults to the current URL if the "action" attribute is empty. (This is what most of the major browsers do.) Fixes #8483. [Tomas, Mislav Marohnić] diff --git a/src/array.js b/src/array.js index e2da61c..3cc7b51 100644 --- a/src/array.js +++ b/src/array.js @@ -1,8 +1,7 @@ function $A(iterable) { if (!iterable) return []; - if (iterable.toArray) { - return iterable.toArray(); - } else { + if (iterable.toArray) return iterable.toArray(); + else { var results = []; for (var i = 0, length = iterable.length; i < length; i++) results.push(iterable[i]); @@ -29,8 +28,7 @@ Array.from = $A; Object.extend(Array.prototype, Enumerable); -if (!Array.prototype._reverse) - Array.prototype._reverse = Array.prototype.reverse; +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; Object.extend(Array.prototype, { _each: function(iterator) { @@ -71,12 +69,6 @@ Object.extend(Array.prototype, { }); }, - indexOf: function(object) { - for (var i = 0, length = this.length; i < length; i++) - if (this[i] == object) return i; - return -1; - }, - reverse: function(inline) { return (inline !== false ? this : this.toArray())._reverse(); }, @@ -115,6 +107,25 @@ Object.extend(Array.prototype, { } }); +// use native browser JS 1.6 implementation if available +if (typeof Array.prototype.forEach == 'function') + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +} + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +} + Array.prototype.toArray = Array.prototype.clone; function $w(string) { diff --git a/src/base.js b/src/base.js index e38522f..d99eb1e 100644 --- a/src/base.js +++ b/src/base.js @@ -68,6 +68,7 @@ Object.extend(Object, { Object.extend(Function.prototype, { bind: function() { + if (arguments.length < 2 && arguments[0] === undefined) return this; var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); @@ -82,6 +83,7 @@ Object.extend(Function.prototype, { }, curry: function() { + if (!arguments.length) return this; var __method = this, args = $A(arguments); return function() { return __method.apply(this, args.concat($A(arguments))); diff --git a/src/enumerable.js b/src/enumerable.js index 97ed380..16e9b7f 100644 --- a/src/enumerable.js +++ b/src/enumerable.js @@ -1,8 +1,9 @@ var $break = {}; var Enumerable = { - each: function(iterator) { + each: function(iterator, context) { var index = 0; + iterator = iterator.bind(context); try { this._each(function(value) { iterator(value, index++); @@ -13,40 +14,45 @@ var Enumerable = { return this; }, - eachSlice: function(number, iterator) { + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var index = -number, slices = [], array = this.toArray(); while ((index += number) < array.length) slices.push(array.slice(index, index+number)); - return slices.map(iterator); + return slices.collect(iterator, context); }, - - all: function(iterator) { + + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result = true; this.each(function(value, index) { - result = result && !!(iterator || Prototype.K)(value, index); + result = result && !!iterator(value, index); if (!result) throw $break; }); return result; }, - - any: function(iterator) { + + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result = false; this.each(function(value, index) { - if (result = !!(iterator || Prototype.K)(value, index)) + if (result = !!iterator(value, index)) throw $break; }); return result; }, - - collect: function(iterator) { + + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var results = []; this.each(function(value, index) { - results.push((iterator || Prototype.K)(value, index)); + results.push(iterator(value, index)); }); return results; }, - detect: function(iterator) { + detect: function(iterator, context) { + iterator = iterator.bind(context); var result; this.each(function(value, index) { if (iterator(value, index)) { @@ -57,7 +63,8 @@ var Enumerable = { return result; }, - findAll: function(iterator) { + findAll: function(iterator, context) { + iterator = iterator.bind(context); var results = []; this.each(function(value, index) { if (iterator(value, index)) @@ -66,20 +73,24 @@ var Enumerable = { return results; }, - grep: function(pattern, iterator) { + grep: function(pattern, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var results = []; this.each(function(value, index) { var stringValue = value.toString(); if (stringValue.match(pattern)) - results.push((iterator || Prototype.K)(value, index)); - }) + results.push(iterator(value, index)); + }); return results; }, include: function(object) { + if (typeof this.indexOf == 'function') + return this.indexOf(object) != -1; + var found = false; this.each(function(value) { - if (value == object) { + if (value === object) { found = true; throw $break; } @@ -95,7 +106,8 @@ var Enumerable = { }); }, - inject: function(memo, iterator) { + inject: function(memo, iterator, context) { + iterator = iterator.bind(context); this.each(function(value, index) { memo = iterator(memo, value, index); }); @@ -109,30 +121,33 @@ var Enumerable = { }); }, - max: function(iterator) { + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result; this.each(function(value, index) { - value = (iterator || Prototype.K)(value, index); + value = iterator(value, index); if (result == undefined || value >= result) result = value; }); return result; }, - min: function(iterator) { + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result; this.each(function(value, index) { - value = (iterator || Prototype.K)(value, index); + value = iterator(value, index); if (result == undefined || value < result) result = value; }); return result; }, - partition: function(iterator) { + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var trues = [], falses = []; this.each(function(value, index) { - ((iterator || Prototype.K)(value, index) ? + (iterator(value, index) ? trues : falses).push(value); }); return [trues, falses]; @@ -140,13 +155,14 @@ var Enumerable = { pluck: function(property) { var results = []; - this.each(function(value, index) { + this.each(function(value) { results.push(value[property]); }); return results; }, - reject: function(iterator) { + reject: function(iterator, context) { + iterator = iterator.bind(context); var results = []; this.each(function(value, index) { if (!iterator(value, index)) @@ -155,7 +171,8 @@ var Enumerable = { return results; }, - sortBy: function(iterator) { + sortBy: function(iterator, context) { + iterator = iterator.bind(context); return this.map(function(value, index) { return {value: value, criteria: iterator(value, index)}; }).sort(function(left, right) { @@ -192,6 +209,9 @@ Object.extend(Enumerable, { map: Enumerable.collect, find: Enumerable.detect, select: Enumerable.findAll, + filter: Enumerable.findAll, member: Enumerable.include, - entries: Enumerable.toArray + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any }); diff --git a/test/unit/array.html b/test/unit/array.html index 38d7101..1eb5b0f 100644 --- a/test/unit/array.html +++ b/test/unit/array.html @@ -111,6 +111,33 @@ assertEqual(-1, [0].indexOf(1)); assertEqual(0, [1].indexOf(1)); assertEqual(1, [0,1,2].indexOf(1)); + assertEqual(0, [1,2,1].indexOf(1)); + assertEqual(2, [1,2,1].indexOf(1, -1)); + assertEqual(1, [undefined,null].indexOf(null)); + }}, + + testLastIndexOf: function(){ with(this) { + assertEqual(-1,[].lastIndexOf(1)); + assertEqual(-1, [0].lastIndexOf(1)); + assertEqual(0, [1].lastIndexOf(1)); + assertEqual(2, [0,2,4,6].lastIndexOf(4)); + assertEqual(3, [4,4,2,4,6].lastIndexOf(4)); + assertEqual(3, [0,2,4,6].lastIndexOf(6,3)); + assertEqual(-1, [0,2,4,6].lastIndexOf(6,2)); + assertEqual(0, [6,2,4,6].lastIndexOf(6,2)); + + var fixture = [1,2,3,4,3]; + assertEqual(4, fixture.lastIndexOf(3)); + assertEnumEqual([1,2,3,4,3],fixture); + + //tests from http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:lastIndexOf + var array = [2, 5, 9, 2]; + assertEqual(3,array.lastIndexOf(2)); + assertEqual(-1,array.lastIndexOf(7)); + assertEqual(3,array.lastIndexOf(2,3)); + assertEqual(0,array.lastIndexOf(2,2)); + assertEqual(0,array.lastIndexOf(2,-2)); + assertEqual(3,array.lastIndexOf(2,-1)); }}, testInspect: function(){ with(this) { diff --git a/test/unit/base.html b/test/unit/base.html index 413de9b..8bd781c 100644 --- a/test/unit/base.html +++ b/test/unit/base.html @@ -54,41 +54,31 @@ assert3(a3); } - var globalBindTest = null; - new Test.Unit.Runner({ testFunctionBind: function() { with(this) { - function methodWithoutArguments(){ - globalBindTest = this.hi; - } - function methodWithArguments(){ - globalBindTest = this.hi + ',' + $A(arguments).join(','); - } - function methodWithBindArguments(){ - globalBindTest = this.hi + ',' + $A(arguments).join(','); - } - function methodWithBindArgumentsAndArguments(){ - globalBindTest = this.hi + ',' + $A(arguments).join(','); - } - - methodWithoutArguments.bind({hi:'without'})(); - assertEqual('without', globalBindTest); - - methodWithArguments.bind({hi:'with'})('arg1','arg2'); - assertEqual('with,arg1,arg2', globalBindTest); - - methodWithBindArguments.bind({hi:'withBindArgs'},'arg1','arg2')(); - assertEqual('withBindArgs,arg1,arg2', globalBindTest); - - methodWithBindArgumentsAndArguments.bind({hi:'withBindArgsAndArgs'},'arg1','arg2')('arg3','arg4'); - assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4', globalBindTest); + function methodWithoutArguments() { return this.hi }; + function methodWithArguments() { return this.hi + ',' + $A(arguments).join(',') }; + var func = Prototype.emptyFunction; + + assertIdentical(func, func.bind()); + assertIdentical(func, func.bind(undefined)); + assertNotIdentical(func, func.bind(null)); + + assertEqual('without', methodWithoutArguments.bind({ hi: 'without' })()); + assertEqual('with,arg1,arg2', methodWithArguments.bind({ hi: 'with' })('arg1','arg2')); + assertEqual('withBindArgs,arg1,arg2', + methodWithArguments.bind({ hi: 'withBindArgs' }, 'arg1', 'arg2')()); + assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4', + methodWithArguments.bind({ hi: 'withBindArgsAndArgs' }, 'arg1', 'arg2')('arg3', 'arg4')); }}, testFunctionCurry: function() { with(this) { var split = function(delimiter, string) { return string.split(delimiter); }; var splitOnColons = split.curry(":"); + assertNotIdentical(split, splitOnColons); assertEnumEqual(split(":", "0:1:2:3:4:5"), splitOnColons("0:1:2:3:4:5")); + assertIdentical(split, split.curry()); }}, testFunctionDelay: function() { with(this) { diff --git a/test/unit/enumerable.html b/test/unit/enumerable.html index 3f9387f..f50fc91 100644 --- a/test/unit/enumerable.html +++ b/test/unit/enumerable.html @@ -36,6 +36,8 @@ Nicknames: $w('sam- noradio htonl Ulysses'), + Basic: [1, 2, 3], + Primes: [ 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, @@ -57,7 +59,7 @@ new Test.Unit.Runner({ testEachBreak: function() {with(this) { var result = 0; - [1, 2, 3].each(function(value) { + Fixtures.Basic.each(function(value) { if ((result = value) == 2) throw $break; }); @@ -66,7 +68,7 @@ testEachReturnActsAsContinue: function() {with(this) { var results = []; - [1, 2, 3].each(function(value) { + Fixtures.Basic.each(function(value) { if (value == 2) return; results.push(value); }); @@ -76,7 +78,26 @@ testEachChaining: function() {with(this) { assertEqual(Fixtures.Primes, Fixtures.Primes.each(Prototype.emptyFunction)); - assertEqual(3, [1, 2, 3].each(Prototype.emptyFunction).length); + assertEqual(3, Fixtures.Basic.each(Prototype.emptyFunction).length); + }}, + + testEnumContext: function() {with(this) { + var results = []; + Fixtures.Basic.each(function(value) { + results.push(value * this.i); + }, { i: 2 }); + + assertEqual('2 4 6', results.join(' ')); + + assert(Fixtures.Basic.all(function(value){ + return value >= this.min && value <= this.max; + }, { min: 1, max: 3 })); + assert(!Fixtures.Basic.all(function(value){ + return value >= this.min && value <= this.max; + })); + assert(Fixtures.Basic.any(function(value){ + return value == this.target_value; + }, { target_value: 2 })); }}, testAny: function() {with(this) { @@ -86,11 +107,11 @@ assert([true, false, false].any()); assert(![false, false, false].any()); - assert([1, 2, 3, 4, 5].any(function(value) { - return value > 3; + assert(Fixtures.Basic.any(function(value) { + return value > 2; })); - assert(![1, 2, 3, 4, 5].any(function(value) { - return value > 10; + assert(!Fixtures.Basic.any(function(value) { + return value > 5; })); }}, @@ -101,11 +122,11 @@ assert(![true, false, false].all()); assert(![false, false, false].all()); - assert([1, 2, 3, 4, 5].all(function(value) { + assert(Fixtures.Basic.all(function(value) { return value > 0; })); - assert(![1, 2, 3, 4, 5].all(function(value) { - return value > 3; + assert(!Fixtures.Basic.all(function(value) { + return value > 1; })); }}, @@ -129,7 +150,7 @@ assertEnumEqual([], [].eachSlice(2)); assertEqual(1, [1].eachSlice(1).length); assertEnumEqual([1], [1].eachSlice(1)[0]); - assertEqual(2, [1,2,3].eachSlice(2).length); + assertEqual(2, Fixtures.Basic.eachSlice(2).length); assertEnumEqual( [3, 2, 1, 11, 7, 5, 19, 17, 13, 31, 29, 23, 43, 41, 37, 59, 53, 47, 71, 67, 61, 83, 79, 73, 97, 89], Fixtures.Primes.eachSlice( 3, function(slice){ return slice.reverse() }).flatten() @@ -184,18 +205,20 @@ assertEnumEqual([1, 2, 3, 4], arr[0]); assertEnumEqual([5, 6, null, null], arr[1]); - arr = [1, 2, 3].inGroupsOf(4,'x'); + var basic = Fixtures.Basic + + arr = basic.inGroupsOf(4,'x'); assertEqual(1, arr.length); assertEnumEqual([1, 2, 3, 'x'], arr[0]); - assertEnumEqual([1,2,3,'a'], [1,2,3].inGroupsOf(2, 'a').flatten()); + assertEnumEqual([1,2,3,'a'], basic.inGroupsOf(2, 'a').flatten()); - arr = [1, 2, 3].inGroupsOf(5, ''); + arr = basic.inGroupsOf(5, ''); assertEqual(1, arr.length); assertEnumEqual([1, 2, 3, '', ''], arr[0]); - assertEnumEqual([1,2,3,0], [1,2,3].inGroupsOf(2, 0).flatten()); - assertEnumEqual([1,2,3,false], [1,2,3].inGroupsOf(2, false).flatten()); + assertEnumEqual([1,2,3,0], basic.inGroupsOf(2, 0).flatten()); + assertEnumEqual([1,2,3,false], basic.inGroupsOf(2, false).flatten()); }}, testInject: function() {with(this) {