// Copyright 2007 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Python style iteration utilities. * @author arv@google.com (Erik Arvidsson) */ goog.provide('goog.iter'); goog.provide('goog.iter.Iterator'); goog.provide('goog.iter.StopIteration'); goog.require('goog.array'); goog.require('goog.asserts'); // TODO(nnaze): Add more functions from Python's itertools. // http://docs.python.org/library/itertools.html /** * @typedef {goog.iter.Iterator|{length:number}|{__iterator__}} */ goog.iter.Iterable; // For script engines that already support iterators. if ('StopIteration' in goog.global) { /** * Singleton Error object that is used to terminate iterations. * @type {Error} */ goog.iter.StopIteration = goog.global['StopIteration']; } else { /** * Singleton Error object that is used to terminate iterations. * @type {Error} * @suppress {duplicate} */ goog.iter.StopIteration = Error('StopIteration'); } /** * Class/interface for iterators. An iterator needs to implement a {@code next} * method and it needs to throw a {@code goog.iter.StopIteration} when the * iteration passes beyond the end. Iterators have no {@code hasNext} method. * It is recommended to always use the helper functions to iterate over the * iterator or in case you are only targeting JavaScript 1.7 for in loops. * @constructor */ goog.iter.Iterator = function() {}; /** * Returns the next value of the iteration. This will throw the object * {@see goog.iter#StopIteration} when the iteration passes the end. * @return {*} Any object or value. */ goog.iter.Iterator.prototype.next = function() { throw goog.iter.StopIteration; }; /** * Returns the {@code Iterator} object itself. This is used to implement * the iterator protocol in JavaScript 1.7 * @param {boolean=} opt_keys Whether to return the keys or values. Default is * to only return the values. This is being used by the for-in loop (true) * and the for-each-in loop (false). Even though the param gives a hint * about what the iterator will return there is no guarantee that it will * return the keys when true is passed. * @return {!goog.iter.Iterator} The object itself. */ goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) { return this; }; /** * Returns an iterator that knows how to iterate over the values in the object. * @param {goog.iter.Iterable} iterable If the object is an iterator it * will be returned as is. If the object has a {@code __iterator__} method * that will be called to get the value iterator. If the object is an * array-like object we create an iterator for that. * @return {!goog.iter.Iterator} An iterator that knows how to iterate over the * values in {@code iterable}. */ goog.iter.toIterator = function(iterable) { if (iterable instanceof goog.iter.Iterator) { return iterable; } if (typeof iterable.__iterator__ == 'function') { return iterable.__iterator__(false); } if (goog.isArrayLike(iterable)) { var i = 0; var newIter = new goog.iter.Iterator; newIter.next = function() { while (true) { if (i >= iterable.length) { throw goog.iter.StopIteration; } // Don't include deleted elements. if (!(i in iterable)) { i++; continue; } return iterable[i++]; } }; return newIter; } // TODO(arv): Should we fall back on goog.structs.getValues()? throw Error('Not implemented'); }; /** * Calls a function for each element in the iterator with the element of the * iterator passed as argument. * * @param {goog.iter.Iterable} iterable The iterator to iterate * over. If the iterable is an object {@code toIterator} will be called on * it. * @param {function(this:T,?,?,?):?} f The function to call for every * element. This function * takes 3 arguments (the element, undefined, and the iterator) and the * return value is irrelevant. The reason for passing undefined as the * second argument is so that the same function can be used in * {@see goog.array#forEach} as well as others. * @param {T=} opt_obj The object to be used as the value of 'this' within * {@code f}. * @template T */ goog.iter.forEach = function(iterable, f, opt_obj) { if (goog.isArrayLike(iterable)) { /** @preserveTry */ try { // NOTES: this passes the index number to the second parameter // of the callback contrary to the documentation above. goog.array.forEach(/** @type {goog.array.ArrayLike} */(iterable), f, opt_obj); } catch (ex) { if (ex !== goog.iter.StopIteration) { throw ex; } } } else { iterable = goog.iter.toIterator(iterable); /** @preserveTry */ try { while (true) { f.call(opt_obj, iterable.next(), undefined, iterable); } } catch (ex) { if (ex !== goog.iter.StopIteration) { throw ex; } } } }; /** * Calls a function for every element in the iterator, and if the function * returns true adds the element to a new iterator. * * @param {goog.iter.Iterable} iterable The iterator to iterate over. * @param {function(this:T,?,undefined,?):boolean} f The function to call for * every element. This function * takes 3 arguments (the element, undefined, and the iterator) and should * return a boolean. If the return value is true the element will be * included in the returned iteror. If it is false the element is not * included. * @param {T=} opt_obj The object to be used as the value of 'this' within * {@code f}. * @return {!goog.iter.Iterator} A new iterator in which only elements that * passed the test are present. * @template T */ goog.iter.filter = function(iterable, f, opt_obj) { var iterator = goog.iter.toIterator(iterable); var newIter = new goog.iter.Iterator; newIter.next = function() { while (true) { var val = iterator.next(); if (f.call(opt_obj, val, undefined, iterator)) { return val; } } }; return newIter; }; /** * Creates a new iterator that returns the values in a range. This function * can take 1, 2 or 3 arguments: *
* range(5) same as range(0, 5, 1) * range(2, 5) same as range(2, 5, 1) ** * @param {number} startOrStop The stop value if only one argument is provided. * The start value if 2 or more arguments are provided. If only one * argument is used the start value is 0. * @param {number=} opt_stop The stop value. If left out then the first * argument is used as the stop value. * @param {number=} opt_step The number to increment with between each call to * next. This can be negative. * @return {!goog.iter.Iterator} A new iterator that returns the values in the * range. */ goog.iter.range = function(startOrStop, opt_stop, opt_step) { var start = 0; var stop = startOrStop; var step = opt_step || 1; if (arguments.length > 1) { start = startOrStop; stop = opt_stop; } if (step == 0) { throw Error('Range step argument must not be zero'); } var newIter = new goog.iter.Iterator; newIter.next = function() { if (step > 0 && start >= stop || step < 0 && start <= stop) { throw goog.iter.StopIteration; } var rv = start; start += step; return rv; }; return newIter; }; /** * Joins the values in a iterator with a delimiter. * @param {goog.iter.Iterable} iterable The iterator to get the values from. * @param {string} deliminator The text to put between the values. * @return {string} The joined value string. */ goog.iter.join = function(iterable, deliminator) { return goog.iter.toArray(iterable).join(deliminator); }; /** * For every element in the iterator call a function and return a new iterator * with that value. * * @param {goog.iter.Iterable} iterable The iterator to iterate over. * @param {function(this:T,?,undefined,?):?} f The function to call for every * element. This function * takes 3 arguments (the element, undefined, and the iterator) and should * return a new value. * @param {T=} opt_obj The object to be used as the value of 'this' within * {@code f}. * @return {!goog.iter.Iterator} A new iterator that returns the results of * applying the function to each element in the original iterator. * @template T */ goog.iter.map = function(iterable, f, opt_obj) { var iterator = goog.iter.toIterator(iterable); var newIter = new goog.iter.Iterator; newIter.next = function() { while (true) { var val = iterator.next(); return f.call(opt_obj, val, undefined, iterator); } }; return newIter; }; /** * Passes every element of an iterator into a function and accumulates the * result. * * @param {goog.iter.Iterable} iterable The iterator to iterate over. * @param {function(this:T,V,?):V} f The function to call for every * element. This function takes 2 arguments (the function's previous result * or the initial value, and the value of the current element). * function(previousValue, currentElement) : newValue. * @param {V} val The initial value to pass into the function on the first call. * @param {T=} opt_obj The object to be used as the value of 'this' * within f. * @return {V} Result of evaluating f repeatedly across the values of * the iterator. * @template T,V */ goog.iter.reduce = function(iterable, f, val, opt_obj) { var rval = val; goog.iter.forEach(iterable, function(val) { rval = f.call(opt_obj, rval, val); }); return rval; }; /** * Goes through the values in the iterator. Calls f for each these and if any of * them returns true, this returns true (without checking the rest). If all * return false this will return false. * * @param {goog.iter.Iterable} iterable The iterator object. * @param {function(this:T,?,undefined,?):boolean} f The function to call for * every value. This function * takes 3 arguments (the value, undefined, and the iterator) and should * return a boolean. * @param {T=} opt_obj The object to be used as the value of 'this' within * {@code f}. * @return {boolean} true if any value passes the test. * @template T */ goog.iter.some = function(iterable, f, opt_obj) { iterable = goog.iter.toIterator(iterable); /** @preserveTry */ try { while (true) { if (f.call(opt_obj, iterable.next(), undefined, iterable)) { return true; } } } catch (ex) { if (ex !== goog.iter.StopIteration) { throw ex; } } return false; }; /** * Goes through the values in the iterator. Calls f for each these and if any of * them returns false this returns false (without checking the rest). If all * return true this will return true. * * @param {goog.iter.Iterable} iterable The iterator object. * @param {function(this:T,?,undefined,?):boolean} f The function to call for * every value. This function * takes 3 arguments (the value, undefined, and the iterator) and should * return a boolean. * @param {T=} opt_obj The object to be used as the value of 'this' within * {@code f}. * @return {boolean} true if every value passes the test. * @template T */ goog.iter.every = function(iterable, f, opt_obj) { iterable = goog.iter.toIterator(iterable); /** @preserveTry */ try { while (true) { if (!f.call(opt_obj, iterable.next(), undefined, iterable)) { return false; } } } catch (ex) { if (ex !== goog.iter.StopIteration) { throw ex; } } return true; }; /** * Takes zero or more iterators and returns one iterator that will iterate over * them in the order chained. * @param {...goog.iter.Iterator} var_args Any number of iterator objects. * @return {!goog.iter.Iterator} Returns a new iterator that will iterate over * all the given iterators' contents. */ goog.iter.chain = function(var_args) { var args = arguments; var length = args.length; var i = 0; var newIter = new goog.iter.Iterator; /** * @return {*} The next item in the iteration. * @this {goog.iter.Iterator} */ newIter.next = function() { /** @preserveTry */ try { if (i >= length) { throw goog.iter.StopIteration; } var current = goog.iter.toIterator(args[i]); return current.next(); } catch (ex) { if (ex !== goog.iter.StopIteration || i >= length) { throw ex; } else { // In case we got a StopIteration increment counter and try again. i++; return this.next(); } } }; return newIter; }; /** * Builds a new iterator that iterates over the original, but skips elements as * long as a supplied function returns true. * @param {goog.iter.Iterable} iterable The iterator object. * @param {function(this:T,?,undefined,?):boolean} f The function to call for * every value. This function * takes 3 arguments (the value, undefined, and the iterator) and should * return a boolean. * @param {T=} opt_obj The object to be used as the value of 'this' within * {@code f}. * @return {!goog.iter.Iterator} A new iterator that drops elements from the * original iterator as long as {@code f} is true. * @template T */ goog.iter.dropWhile = function(iterable, f, opt_obj) { var iterator = goog.iter.toIterator(iterable); var newIter = new goog.iter.Iterator; var dropping = true; newIter.next = function() { while (true) { var val = iterator.next(); if (dropping && f.call(opt_obj, val, undefined, iterator)) { continue; } else { dropping = false; } return val; } }; return newIter; }; /** * Builds a new iterator that iterates over the original, but only as long as a * supplied function returns true. * @param {goog.iter.Iterable} iterable The iterator object. * @param {function(this:T,?,undefined,?):boolean} f The function to call for * every value. This function * takes 3 arguments (the value, undefined, and the iterator) and should * return a boolean. * @param {T=} opt_obj This is used as the 'this' object in f when called. * @return {!goog.iter.Iterator} A new iterator that keeps elements in the * original iterator as long as the function is true. * @template T */ goog.iter.takeWhile = function(iterable, f, opt_obj) { var iterator = goog.iter.toIterator(iterable); var newIter = new goog.iter.Iterator; var taking = true; newIter.next = function() { while (true) { if (taking) { var val = iterator.next(); if (f.call(opt_obj, val, undefined, iterator)) { return val; } else { taking = false; } } else { throw goog.iter.StopIteration; } } }; return newIter; }; /** * Converts the iterator to an array * @param {goog.iter.Iterable} iterable The iterator to convert to an array. * @return {!Array} An array of the elements the iterator iterates over. */ goog.iter.toArray = function(iterable) { // Fast path for array-like. if (goog.isArrayLike(iterable)) { return goog.array.toArray(/** @type {!goog.array.ArrayLike} */(iterable)); } iterable = goog.iter.toIterator(iterable); var array = []; goog.iter.forEach(iterable, function(val) { array.push(val); }); return array; }; /** * Iterates over 2 iterators and returns true if they contain the same sequence * of elements and have the same length. * @param {goog.iter.Iterable} iterable1 The first iterable object. * @param {goog.iter.Iterable} iterable2 The second iterable object. * @return {boolean} true if the iterators contain the same sequence of * elements and have the same length. */ goog.iter.equals = function(iterable1, iterable2) { iterable1 = goog.iter.toIterator(iterable1); iterable2 = goog.iter.toIterator(iterable2); var b1, b2; /** @preserveTry */ try { while (true) { b1 = b2 = false; var val1 = iterable1.next(); b1 = true; var val2 = iterable2.next(); b2 = true; if (val1 != val2) { return false; } } } catch (ex) { if (ex !== goog.iter.StopIteration) { throw ex; } else { if (b1 && !b2) { // iterable1 done but iterable2 is not done. return false; } if (!b2) { /** @preserveTry */ try { // iterable2 not done? val2 = iterable2.next(); // iterable2 not done but iterable1 is done return false; } catch (ex1) { if (ex1 !== goog.iter.StopIteration) { throw ex1; } // iterable2 done as well... They are equal return true; } } } } return false; }; /** * Advances the iterator to the next position, returning the given default value * instead of throwing an exception if the iterator has no more entries. * @param {goog.iter.Iterable} iterable The iterable object. * @param {*} defaultValue The value to return if the iterator is empty. * @return {*} The next item in the iteration, or defaultValue if the iterator * was empty. */ goog.iter.nextOrValue = function(iterable, defaultValue) { try { return goog.iter.toIterator(iterable).next(); } catch (e) { if (e != goog.iter.StopIteration) { throw e; } return defaultValue; } }; /** * Cartesian product of zero or more sets. Gives an iterator that gives every * combination of one element chosen from each set. For example, * ([1, 2], [3, 4]) gives ([1, 3], [1, 4], [2, 3], [2, 4]). * @see http://docs.python.org/library/itertools.html#itertools.product * @param {...!goog.array.ArrayLike.<*>} var_args Zero or more sets, as arrays. * @return {!goog.iter.Iterator} An iterator that gives each n-tuple (as an * array). */ goog.iter.product = function(var_args) { var someArrayEmpty = goog.array.some(arguments, function(arr) { return !arr.length; }); // An empty set in a cartesian product gives an empty set. if (someArrayEmpty || !arguments.length) { return new goog.iter.Iterator(); } var iter = new goog.iter.Iterator(); var arrays = arguments; // The first indicies are [0, 0, ...] var indicies = goog.array.repeat(0, arrays.length); iter.next = function() { if (indicies) { var retVal = goog.array.map(indicies, function(valueIndex, arrayIndex) { return arrays[arrayIndex][valueIndex]; }); // Generate the next-largest indicies for the next call. // Increase the rightmost index. If it goes over, increase the next // rightmost (like carry-over addition). for (var i = indicies.length - 1; i >= 0; i--) { // Assertion prevents compiler warning below. goog.asserts.assert(indicies); if (indicies[i] < arrays[i].length - 1) { indicies[i]++; break; } // We're at the last indicies (the last element of every array), so // the iteration is over on the next call. if (i == 0) { indicies = null; break; } // Reset the index in this column and loop back to increment the // next one. indicies[i] = 0; } return retVal; } throw goog.iter.StopIteration; }; return iter; }; /** * Create an iterator to cycle over the iterable's elements indefinitely. * For example, ([1, 2, 3]) would return : 1, 2, 3, 1, 2, 3, ... * @see: http://docs.python.org/library/itertools.html#itertools.cycle. * @param {!goog.iter.Iterable} iterable The iterable object. * @return {!goog.iter.Iterator} An iterator that iterates indefinitely over * the values in {@code iterable}. */ goog.iter.cycle = function(iterable) { var baseIterator = goog.iter.toIterator(iterable); // We maintain a cache to store the iterable elements as we iterate // over them. The cache is used to return elements once we have // iterated over the iterable once. var cache = []; var cacheIndex = 0; var iter = new goog.iter.Iterator(); // This flag is set after the iterable is iterated over once var useCache = false; iter.next = function() { var returnElement = null; // Pull elements off the original iterator if not using cache if (!useCache) { try { // Return the element from the iterable returnElement = baseIterator.next(); cache.push(returnElement); return returnElement; } catch (e) { // If an exception other than StopIteration is thrown // or if there are no elements to iterate over (the iterable was empty) // throw an exception if (e != goog.iter.StopIteration || goog.array.isEmpty(cache)) { throw e; } // set useCache to true after we know that a 'StopIteration' exception // was thrown and the cache is not empty (to handle the 'empty iterable' // use case) useCache = true; } } returnElement = cache[cacheIndex]; cacheIndex = (cacheIndex + 1) % cache.length; return returnElement; }; return iter; };