All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.stjs.javascript.Array Maven / Gradle / Ivy

/**
 *  Copyright 2011 Alexandru Craciun, Eyal Kaspi
 *
 *  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.
 */
package org.stjs.javascript;

import java.util.*;

import org.stjs.javascript.annotation.BrowserCompatibility;
import org.stjs.javascript.annotation.ServerSide;
import org.stjs.javascript.annotation.Template;
import org.stjs.javascript.functions.Callback1;
import org.stjs.javascript.functions.Callback3;
import org.stjs.javascript.functions.Function3;
import org.stjs.javascript.functions.Function4;

import static org.stjs.javascript.JSGlobal.String;

/**
 * This interface represents an array from Javascript.The value may be typed. The iteration is done on the indexes to have the javascript
 * equivalent of 
* for(var key in array)
* The methods are prefixed with $ to let the generator know that is should generate braket access instead, i.e
* array.$get(key) => array[key]
* array.$set(key, value) => array[key]=value It is generally a bad idea for code written in ST-JS to create subclasses of Array, as that will * not be translated properly. However, it may be useful for some bridges. *

* The documentation of this class is mostly adapted from the ECMAScript 5.1 Specification: * http://www.ecma-international.org/ecma-262/5.1/ *

* Browser compatibility information comes from: http://kangax.github.io/es5-compat-table * @author acraciun, npiguet */ public class Array implements Iterable { private static final Object UNSET = new Object(); private ArrayStore array = new PackedArrayStore(); private long length = 0; private long setElements = 0; private java.util.Map nonArrayElements = new LinkedHashMap(); /** * Constructs a new empty Array. */ public Array() { // nothing } /** * Constructs a new empty Array and sets it's length property to len. * * @param len * the length of this new array */ public Array(Number len) { double signedLen = len.doubleValue(); double unsignedLen = JSAbstractOperations.ToUInt32(signedLen); if (signedLen != unsignedLen) { throw new Error("RangeError", len + " is out of range for Array length"); } this.$length((int) unsignedLen); } /** * Constructs an Array based on a java.lang.List. This constructor can only be used on server side code. The * returned array has the same length as the List, and has no unset indices. Indices that are null * in the List are considered to be set to null in the returned Array. * * @param values the list of values to add to this array * @return a new Array that is equivalent to the specified List */ @ServerSide public Array(List values) { this.length = values.size(); this.setElements = values.size(); this.array = new PackedArrayStore<>(values); } /** * Constructs a new Array containing all the specified elements in the order in which they appear in the * argument list. If the specified values contain exactly one element, and that element is a Number, then this * constructor behaves like Array(Number). * * @param values * the values to add to this array, in the order in which they appear in the argument list */ @SafeVarargs public Array(V... values) { // special case when there is a single Number argument: Should behave like Array(Number) if (values.length == 1 && values[0] instanceof Number) { double signedLen = ((Number) values[0]).doubleValue(); double unsignedLen = JSAbstractOperations.ToUInt32(signedLen); if (signedLen != unsignedLen) { throw new Error("RangeError", signedLen + " is out of range for Array length"); } this.$length((int) unsignedLen); } else { this.push(values); } } /** * Returns a java.lang.List that corresponds to this Array. This method can only be called from server side code * and cannot be used in code that is translated to JavaScript. * * @return a List that corresponds this Array * @throws IllegalStateException if the Array contains more than Integer.MAX_VALUE elements, * if it contains non-Array elements or * if it has some unset indices (holes) */ @ServerSide public List toList() { if (this.length > Integer.MAX_VALUE) { throw new IllegalStateException("Array is too long: " + this.length + " > " + Integer.MAX_VALUE); } else if (this.nonArrayElements.size() > 0) { throw new IllegalStateException("Array contains non-array elements: " + this.nonArrayElements.keySet()); } else if(this.length != this.setElements){ throw new IllegalStateException("Array is sparse"); } ArrayList result = new ArrayList<>((int) length); for (int i = 0; i < this.$length(); i++) { result.add(this.$get(i)); } return result; } /** * Returns an Iterator that allow this Array to be used in foreach statements. The returned * iterator is designed to make Java for-each statements on Arrays match the corresponding JavaScript * for-in statement. As a result, the returned iterator will iterate over the indices of the Array * converted to String (the JavaScript behavior) instead of iterating directly over the values (the Java * behavior). * *

* This method is marked as Deprecated because you should never call this method directly in code that will be * translated to JavaScript; the generated JavaScript code will not work because the prototype of Array in * JavaScript does not contain any iterator method. *

* You may safely call this method from a Java runtime if necessary. */ @Override @ServerSide @BrowserCompatibility("none") public Iterator iterator() { // Extract from the ECMA-262 specification: chapter 12.6.4 The for-in Statement // // The mechanics and order of enumerating the properties is not specified. Properties of the object being // enumerated may be deleted during enumeration. If a property that has not yet been visited during // enumeration is deleted, then it will not be visited. If new properties are added to the object being // enumerated during enumeration, the newly added properties are not guaranteed to be visited in the active // enumeration. A property name must not be visited more than once in any enumeration. return new Iterator() { private Iterator> arrayIter = entryIterator(0, $length(), true); private Iterator nonArrayIter = nonArrayElements.keySet().iterator(); @Override public boolean hasNext() { return arrayIter.hasNext() || nonArrayIter.hasNext(); } @Override public String next() { if (arrayIter.hasNext()) { Entry nextEntry = arrayIter.next(); return Long.toString(nextEntry.key); } return nonArrayIter.next(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } private Iterator> entryIterator(final long actualStart, final long actualEndExcluded, final boolean isForward){ return new Iterator>() { private ArrayStore prevStore = array; private Long prevKey = 0L; private Iterator> iter = array.entryIterator(actualStart, actualEndExcluded, isForward); @Override public boolean hasNext() { updateEntryIterator(); return iter.hasNext(); } @Override public Entry next() { updateEntryIterator(); Entry entry = iter.next(); prevKey = entry.key; return entry; } @Override public void remove() { throw new UnsupportedOperationException(); } private void updateEntryIterator(){ if(prevStore != array){ prevStore = array; iter = array.entryIterator(prevKey + (isForward ? 1 : -1), actualEndExcluded, isForward); } } }; } /** * Translated to array[index] in JavaScript, returns the element at the specified index in this * Array. * * @param index * the index * @return the element a the specified index */ @Template("get") public V $get(int index) { return this.$get((long) index); } /** * Translated to array[index] in JavaScript, returns the element at the specified index in this * Array. * * @param index * the index * @return the element a the specified index */ @Template("get") public V $get(long index){ if (index < 0) { // index is not an array index, so we'll look into the non-array element values return this.nonArrayElements.get(Long.toString(index)); } // index is an array element, so let's ask the array store return this.array.get(index); } /** * Translated to array[index] in JavaScript, returns the element at the specified index in this * Array. * * @param index * the index * @return the element a the specified index */ @Template("get") public V $get(String index) { Long i = toArrayIndex(index); if (i == null) { // index is not an array Index , look in the non-array elements return this.nonArrayElements.get(index); } return array.get(i); } private Long toArrayIndex(String index) { if (index == null) { return null; } Double asNum = JSAbstractOperations.ToNumber(index); Double asInt = JSAbstractOperations.ToInteger(asNum); if (Double.isNaN(asNum) || asInt < 0 || asInt >= JSAbstractOperations.UINT_MAX_VALUE_D - 1 || !asInt.equals(asNum)) { // index is not an array Index , look in the non-array elements return null; } return asInt.longValue(); } /** * Translated to array[index] = value in JavaScript, sets the element at the specified index in this * Array to the specified value. * * @param index * the index * @param value * the value */ @Template("set") public void $set(int index, V value) { this.$set((long)index, value); } /** * Translated to array[index] = value in JavaScript, sets the element at the specified index in this * Array to the specified value. * * @param index * the index * @param value * the value */ @Template("set") public void $set(long index, V value) { if (index < 0) { // not an array index. Set the value in the non-array properties this.nonArrayElements.put(JSAbstractOperations.ToString(index), value); } else { this.doSet(index, value); } } /** * Translated to array[index] = value in JavaScript, sets the element at the specified index in this * Array to the specified value. * * @param index * the index * @param value * the value */ @Template("set") public void $set(String index, V value) { Long i = this.toArrayIndex(index); if (i == null) { this.nonArrayElements.put(JSAbstractOperations.ToString(index), value); } else { this.$set(i, value); } } /** * Deletes the element at the specified index from this Array, leaving an empty hole. Unlike * splice() This method does not affect the length of this Array, it only unsets the value at the * specified index. After $delete() is called on an index, that index will not be iterated over any more in * for-each loops on this Array. $delete() can also be used to remove non-array elements from this * Array (e.g. myArray.$delete("foobar")). *

* myArray.$delete(index) is translated to delete myArray[index] in JavaScript. * * @param index * the index * @return true */ @Template("delete") public boolean $delete(String index) { Long i = this.toArrayIndex(index); if (i == null) { this.nonArrayElements.remove(index); } else { this.doDelete(i); } return true; } /** * Deletes the element at the specified index from this Array, leaving an empty hole. Unlike * splice() This method does not affect the length of this Array, it only unsets the value at the * specified index. After $delete() is called on an index, that index will not be iterated over any more in * for-each loops on this Array. $delete() can also be used to remove non-array elements from this * Array (e.g. myArray.$delete("foobar")) *

* myArray.$delete(index) is translated to delete myArray[index] in JavaScript. * * @param index * the index * @return true */ @Template("delete") public boolean $delete(int index) { return this.$delete((long)index); } /** * Deletes the element at the specified index from this Array, leaving an empty hole. Unlike * splice() This method does not affect the length of this Array, it only unsets the value at the * specified index. After $delete() is called on an index, that index will not be iterated over any more in * for-each loops on this Array. $delete() can also be used to remove non-array elements from this * Array (e.g. myArray.$delete("foobar")) *

* myArray.$delete(index) is translated to delete myArray[index] in JavaScript. * * @param index * the index * @return true */ @Template("delete") public boolean $delete(long index) { if (index < 0) { // not an array index. Remove the property from the non-array properties this.nonArrayElements.remove(JSAbstractOperations.ToString(index)); } else { this.doDelete(index); } return true; } private void doDelete(long index) { if (index >= this.$length()) { // nothing to do return; } boolean isSet = array.isSet(index); long newSetElements = this.setElements; if (isSet) { // there is only something to do if the element is set. If it is already unset, nothing to do. newSetElements--; this.switchStoreIfNeeded(this.length, newSetElements); this.array.delete(index); this.setElements = newSetElements; } } private void doSet(long index, V value) { // check if store type is correct for setting this value boolean isSet = array.isSet(index); long newLength = (long) Math.max(this.length, index + 1); long newSetElements = this.setElements; if (!isSet) { // unsetElement, we may need to convert between stores newSetElements++; this.switchStoreIfNeeded(newLength, newSetElements); } // store type is now correct, let's do the actual setting if (newLength > this.length) { array.padTo(newLength); } array.set(index, value); this.length = newLength; this.setElements = newSetElements; } /** * Translated to array.length in JavaScript, returns the length of this Array. The returned value * is always greater than the highest index in this Array. * * @return the length of this Array. */ @Template("toProperty") public int $length() { return (int) this.length; } /** * Translated to array.length = newLength in JavaScript, sets the length of this Array. * *

* Attempting to set the length property of this Array to a value that is numerically less than or equal to * the largest index contained within this Array will result in the Array being truncated to the * new length. * * @param newLength * the new length of this Array */ @Template("toProperty") public void $length(int newLength) { if (newLength == this.length) { // nothing to do, we are not changing the length of the array return; } else if (newLength > this.length) { // growing the array this.switchStoreIfNeeded(newLength, this.setElements); this.array.padTo(newLength); this.length = newLength; } else { // truncating the array long newSetElements = 0; if (newLength > 0) { newSetElements = this.setElements - this.array.getSetElements(newLength - 1, this.length); } this.switchStoreIfNeeded(newLength, newSetElements); this.array.truncateFrom(newLength); this.length = newLength; this.setElements = newSetElements; } } private void switchStoreIfNeeded(long newLength, long newSetElements) { if (!this.array.isEfficientStoreFor(newLength, newSetElements)) { this.array = this.array.switchStoreType(newLength); } } /** * Appends all the elements of each of the specified Arrays in order to the elements of this Array * . This method does not change the existing arrays, but returns a new array, containing the values of the joined * arrays. * * @param arrays * the Arrays to be concatenated to this Array * @return a new Array, containing all the elements of the joined Arrays. */ @SafeVarargs public final Array concat(Array... arrays) { Array result = new Array<>(); // Add the elements of this array long i = 0; Iterator> thisIter = this.entryIterator(0, this.$length(), true); while (thisIter.hasNext()) { Entry entry = thisIter.next(); result.$set(i + entry.key, entry.value); } i = this.$length(); if (arrays != null) { // add the elements of all the other specified arrays for (Array arr : arrays) { Iterator> arrIter = arr.array.entryIterator(0, arr.$length(), true); while (arrIter.hasNext()) { Entry entry = arrIter.next(); result.$set(i + entry.key, entry.value); } i += arr.$length(); } } result.$length((int) i); return result; } /** * Appends all the specified elements in the order they appear in the arguments list to the elements of this * Array. This method does not change the existing arrays, but returns a new array, containing the values * of the joined arrays. * * @param values * the elements to be concatenated to this Array * @return a new Array, containing all the elements of the joined Arrays. */ @SafeVarargs public final Array concat(V... values) { Array result = new Array(); // add the elements of this array long i = 0; Iterator> iter = this.entryIterator(0, this.$length(), true); while (iter.hasNext()) { Entry entry = iter.next(); result.$set(i + entry.key, entry.value); } i = this.$length(); result.$length(this.$length()); if (values != null) { // add the specified values for (int j = 0; j < values.length; j++) { result.$set(i + j, values[j]); } } return result; } /** * Searches this Array for the specified item, and returns its position. Items are compared for equality * using the === operator. * *

* The search will start at the beginning and end at the end of the Array. * *

* Returns -1 if the item is not found. * *

* If the item is present more than once, indexOf returns the position of the first occurrence. * * @param element * the item to search for * @return the index at which the element was found, -1 if not found */ @BrowserCompatibility("IE:9+") public int indexOf(V element) { return indexOf(element, 0); } /** * Searches this Array for the specified item, and returns its position. Items are compared for equality * using the === operator. * *

* The search will start at the specified position and end the search at the end of the Array. * *

* Returns -1 if the item is not found. * *

* If the specified position is greater than or equal to the length of this array, this Array is * not searched and -1 is returned. If start is negative, it will be treated as length+start. If * the computed starting element is less than 0, this whole Array will be searched. * *

* If the item is present more than once, indexOf method returns the position of the first occurrence. * * @param element * the item to search for * @param start * where to start the search. Negative values will start at the given position counting from the end * @return the index at which the element was found, -1 if not found */ @BrowserCompatibility("IE:9+") public int indexOf(V element, int start) { long actualStart; if (start < 0) { actualStart = (long) Math.max(0, this.$length() + start); } else { actualStart = (long) Math.min(this.$length(), start); } Iterator> iter = this.entryIterator(actualStart, this.$length(), true); return indexOf(element, iter); } private int indexOf(V element, Iterator> iter) { // this method doesn't depend on the order of iteration, and is used both by indexOf and lastIndexOf while (iter.hasNext()) { Entry entry = iter.next(); // Double.equals has an annoying behavior that we must correct for // this implementation to be as close to JS as possible // ie: myDouble.equals(Double.NaN) will return true if myDouble.isNan() // but myDouble == Double.NaN will return false... // In JS, there is no such distinction, and any NaN is not equal to anything boolean isNan = entry.value instanceof Double && Double.isNaN((Double) entry.value); if (!isNan && // (entry.value != null && entry.value.equals(element) || // entry.value == null && element == null) // ) { return (int) entry.key; } } return -1; } /** * Converts all of the elements of this Array to Strings, and concatenates these Strings * using a single comma (",") as a separator. * *

* If this Array is empty, the empty string is returned. * * @return the string representation of the values in this Array, separated by a comma. */ public String join() { return join(","); } /** * Converts all of the elements of this Array to Strings, and concatenates these Strings * using the specified separator. * *

* If this Array is empty, the empty string is returned. * * @return the string representation of the values in this Array, separated by the specified separator. */ public String join(String separator) { StringBuilder builder = new StringBuilder(); String realSeparator = ""; for (int i = 0; i < this.length; i++) { builder.append(realSeparator); realSeparator = separator; V item = this.array.get(i); if (item != null) { builder.append(JSAbstractOperations.ToString(item)); } } return builder.toString(); } /** * Removes the last element from this Array and returns it. * *

* If this Array is empty, undefined is returned. * * @return the last element from this Array */ public V pop() { if (this.length == 0) { return null; } Array removed = this.splice((int) (this.length - 1), 1); return removed.$get(0); } /** * The specified elements are appended at the end of this Array in the order in which they appear in the * arguments list and the new length of this Array is returned. * * @param values * the values to be appended * @return the new length of this Array */ @SafeVarargs public final int push(V... values) { this.splice(this.$length(), 0, values); return (int) this.length; } /** * The elements of this Array are rearranged so as to reverse their order, and this Array is * returned. * * @return this Array */ public Array reverse() { this.array.reverse(); return this; } /** * Removes the first element from this Array and returns it. * *

* If this Array is empty, null is returned. * * @return the first element of this Array. */ public V shift() { Array removed = this.splice(0, 1); if (removed.$length() == 1) { return removed.$get(0); } return null; } /** * Returns a new Array containing all the elements of this Array starting from the given start * index until the end of this Array. * *

* If start is negative, it is treated as length+start. * *

* If the selection range falls outside of this array, an empty Array is returned. * * @param start * the index from which to start the selection. Use negative values to specified an index starting from * the end of this Array. * @return a new Array containing all the elements of this Array starting from the given start * index until the end of the Array. */ public Array slice(int start) { return slice(start, this.$length()); } /** * Returns a new Array containing all the elements of this Array starting from the given start * index and ending at (but not including) the given end index. * *

* If start is negative, it is treated as length+start. If end is negative, it is treated * as length+end. * *

* If the selection range falls outside of this Array, an empty Array is returned. * * @param start * the index from which to start the selection. Use negative values to specified an index starting from * the end * @param end * the index at which to end the selection (the element at this index is excluded). Use negative values * to specified an index starting from the end * @return a new Array containing all the elements of this Array starting from the given start * index and ending at the given end index */ public Array slice(int start, int end) { long actualStart; if (start < 0) { actualStart = (long) Math.max(this.$length() + start, 0); } else { actualStart = (long) Math.min(start, this.$length()); } long actualEnd; if (end < 0) { actualEnd = (long) Math.max(this.$length() + end, 0); } else { actualEnd = (long) Math.min(end, this.$length()); } return this.array.slice(actualStart, actualEnd); } /** * Deletes the specified number of elements from this Array starting at specified start index, and returns * a new Array containing the deleted elements (if any). * *

* If start is negative, it is treated as length+start. * * @param start * the index at which to start deleting elements. Use negative values to specified an index starting from * the end * @param deleteCount * the number of elements to be deleted * @return a new Array containing the deleted elements (if any) */ public Array splice(int start, int deleteCount) { return this.splice(start, deleteCount, (V[]) null); } /** * Deletes the specified number of elements from this Array starting at specified start index, replaces the * deleted elements with the specified values, and returns a new Array containing the deleted elements (if * any). * *

* If start is negative, it is treated as length+start. * * @param start * the index at which to start deleting elements. Use negative values to specify an index starting from * the end. * @param deleteCount * the number of elements to be deleted * @param values * the elements with which the deleted elements must be replaced * @return a new Array containing the deleted elements (if any) */ @SafeVarargs public final Array splice(int start, int deleteCount, V... values) { long actualStart; if (start < 0) { actualStart = (long) Math.max(this.length + start, 0); } else { actualStart = (long) Math.min(this.length, start); } long actualDeleteCount = (long) Math.min(Math.max(deleteCount, 0), this.length - actualStart); if (values == null) { @SuppressWarnings("unchecked") V[] tmp = (V[]) new Object[0]; values = tmp; } if (actualDeleteCount == 0 && values.length == 0) { // we are not deleting or adding anything. This is basically a NOP return new Array(); } long newLength = this.length - actualDeleteCount + values.length; long newSetElements = this.setElements + values.length - this.array.getSetElements(actualStart, actualStart + deleteCount); this.switchStoreIfNeeded(newLength, newSetElements); Array deleted = this.slice((int) actualStart, (int) (actualStart + actualDeleteCount)); this.array.splice(actualStart, actualDeleteCount, values); this.length = newLength; this.setElements = newSetElements; return deleted; } /** * Sorts the elements of this Array using the natural order of their string representations. * *

* The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their * original order). * *

* sort returns this Array. * *

* Note1: Because non-existent property values always compare greater than undefined property * values, and undefined always compares greater than any other value, undefined property values always sort to the * end of the result, followed by non-existent property values. * * @return this array */ public Array sort() { return this.sort(null); } /** * Sorts the elements of this Array using the specified SortFunction to determine ordering. * *

* The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their * original order). * *

* This methods return this Array. * *

* If the specified SortFunction is null, then this method is equivalent to sort(). * *

* If comparefn is not a consistent comparison function for the elements of this Array, the * behaviour of sort is implementation-defined. * *

* Note1: Because non-existent property values always compare greater than undefined property * values, and undefined always compares greater than any other value, undefined property values always sort to the * end of the result, followed by non-existent property values. * * @param comparefn * a sort function that can compare elements of this Array. * @return this Array */ public Array sort(SortFunction comparefn) { if(comparefn == null) { this.array.sort(defaultSortComparator()); } else { this.array.sort(comparefn); } return this; } private SortFunction defaultSortComparator(){ return new SortFunction() { @Override public int $invoke(V a, V b) { String aString = String(a); String bString = String(b); return aString.compareTo(bString); } }; } /** * Prepends the specified values to the start of this Array, such that their order within this * Array is the same as the order in which they appear in the argument list. Returns the new length of this * Array. * * @param values * the values to the prepended to the start of this Array * @return the new length of this Array */ @SafeVarargs public final int unshift(V... values) { this.splice(0, 0, values); return this.$length(); } /** * Calls the specified callback function once for each element present in this Array, in ascending order. * callbackfn is called only for elements of this Array which actually exist; it is not called for * missing elements of this Array. * *

* callbackfn is called with the value of the element as its argument. * *

* forEach does not directly mutate this Array but this Array may be mutated by the calls * to callbackfn. * *

* The range of elements processed by forEach is set before the first call to callbackfn. Elements * which are appended to this Array after the call to forEach begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to callback * will be the value at the time forEach visits them; elements that are deleted after the call to * forEach begins and before being visited are not visited. * * @param callbackfn * the callback function to be called for each element */ @BrowserCompatibility("IE:9+") public void forEach(final Callback1 callbackfn) { forEach(new Callback3>() { @Override public void $invoke(V v, Long aLong, Array strings) { callbackfn.$invoke(v); } }); } /** * Calls the specified callback function once for each element present in this Array, in ascending order. * callbackfn is called only for elements of this Array which actually exist; it is not called for * missing elements of this Array. * *

* callbackfn is called with three arguments: the value of the element, the index of the element, and the * Array being traversed (this Array). * *

* forEach does not directly mutate this Array but this Array may be mutated by the calls * to callbackfn. * *

* The range of elements processed by forEach is set before the first call to callbackfn. Elements * which are appended to this Array after the call to forEach begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to callback * will be the value at the time forEach visits them; elements that are deleted after the call to * forEach begins and before being visited are not visited. * * @param callbackfn * the callback function to be called for each element */ @BrowserCompatibility("IE:9+") public void forEach(Callback3> callbackfn) { if(callbackfn == null){ throw new Error("TypeError", "callbackfn is null"); } Iterator> iter = this.entryIterator(0, $length(), true); while(iter.hasNext()){ Entry val = iter.next(); callbackfn.$invoke(val.value, val.key, this); } } /** * this method does exactly as {@link #forEach(Callback1)}, but allows in java 8 the usage of lambda easily as the forEach method overloads * the method from the new Iterable interface. The generated code is, as expected, forEach * @param callbackfn */ @Template("prefix") @BrowserCompatibility("IE:9+") public void $forEach(Callback1 callbackfn) { forEach(callbackfn); } /** * this method does exactly as {@link #forEach(Callback1)}, but allows in java 8 the usage of lambda easily as the forEach method overloads * the method from the new Iterable interface. The generated code is, as expected, forEach * @param callbackfn */ @Template("prefix") @BrowserCompatibility("IE:9+") public void $forEach(Callback3> callbackfn) { forEach(callbackfn); } /** * Converts all the elements of this Array to their String representation, concatenates them using a comma * as a separator. Calling this method is equivalent to calling join(). * * @return the string representation of the values in this Array, separated by a comma. */ @Override public String toString() { return join(); } /** * Converts all the elements of this Array to Strings using their toLocaleString method, and concatenate * the resulting Strings by a separator String that has been derived in an implementation-defined locale-specific * way. The result of calling this function is intended to be analogous to the result of toString(), except * that the result of this function is intended to be locale-specific. * * @return a string representation of all the elements in this Array, separated by a locale-specific * separator */ public String toLocaleString() { StringBuilder builder = new StringBuilder(); String sep = ""; for (int i = 0; i < this.length; i++) { builder.append(sep); sep = ","; V item = this.array.get(i); if (item != null) { builder.append(JSObjectAdapter.toLocaleString(item)); } } return builder.toString(); } /** * Searches the elements of this Array in descending order for the specified element, and returns the index * of the last position at which the element was found, or -1 if not found. * *

* Elements are compared for equality using the === operator. * *

* If there are multiple occurrences of the specified element in this Array, then the largest index among * occurrences is returned. * * @param searchElement * the element to search for * @return the last index where this element is in this Array, or -1 if not found */ @BrowserCompatibility("IE:9+") public int lastIndexOf(V searchElement) { return lastIndexOf(searchElement, this.$length() - 1); } /** * Searches the elements of this Array in descending order starting at the specified index for the * specified element, and returns the index of the last index at which the element was found, or -1 if not found. * *

* Elements are compared for equality using the === operator. * *

* If there are multiple occurrences of the specified element in this Array, then the largest index among * occurrences is returned. * *

* If fromIndex is greater than or equal to the length of this Array, the whole array will be * searched. If fromIndex is negative, then it is treated as length+fromIndex. If the computed * index is less than 0, -1 is returned. * * @param searchElement * The element to search for * @param fromIndex * the index from which to start searching * @return the last index where this element is in this Array, or -1 if not found */ @BrowserCompatibility("IE:9+") public int lastIndexOf(V searchElement, int fromIndex) { long actualStart; if (fromIndex < 0) { actualStart = (long) Math.max(-1, this.$length() + fromIndex); } else { actualStart = (long) Math.min(this.$length() - 1, fromIndex); } Iterator> iter = this.entryIterator(actualStart, -1, false); return indexOf(searchElement, iter); } /** * Returns true if the specified callback function returns true for every element in this * Array. * *

* callbackfn is called on each element in ascending order until one of the calls returns false. * Once callbackfn has returned false for the first time, the search in this Array is interrupted, * callbackfn is never called on any more elements and every returns false. If * callbackfn returned true for all the elements, then every returns true. * callbackfn is called only for elements of this Array which actually exist; it is not called for * missing elements of this Array. * *

* callbackfn has 3 arguments: the value of the element, the index of the element, and the Array * being traversed (this Array). * *

* every does not directly mutate this Array, but this Array may be mutated by the calls * to callbackfn. * *

* The range of elements processed by every is set before the first call to callbackfn. Elements * which are appended to this Array after the call to every begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time every visits them; elements that are deleted after the * call to every begins and before being visited are not visited. every acts like the "for all" * quantifier in mathematics. In particular, for an empty Array, it returns true. * * @param callbackfn * the callback function to call * @return true if callbackfn returns true for ALL the elements in this Array, * false if not */ @BrowserCompatibility("IE:9+") public boolean every(Function3, Boolean> callbackfn) { if(callbackfn == null){ throw new Error("TypeError", "callbackfn is null"); } Iterator> iter = this.entryIterator(0, this.$length(), true); while (iter.hasNext()) { Entry entry = iter.next(); Boolean result = callbackfn.$invoke(entry.value, entry.key, this); if (!Boolean.TRUE.equals(result)) { // false or null was returned return false; } } return true; } /** * Returns true if the specified callback function returns true for at least one element in this * Array. * *

* some calls callbackfn once for each element present in this Array, in ascending order, * until it finds one where callbackfn returns true. If such an element is found, some * immediately returns true. Otherwise, some returns false. callbackfn is called * only for elements of this Array which actually exist; it is not called for missing elements of this * Array. * *

* callbackfn has three arguments: the value of the element, the index of the element, and the * Array being traversed (this Array). * * some does not directly mutate this Array but this Array may be mutated by the calls to * callbackfn. * *

* The range of elements processed by some is set before the first call to callbackfn. Elements * that are appended to this Array after the call to some begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time that some visits them; elements that are deleted after * the call to some begins and before being visited are not visited. some acts like the "exists" * quantifier in mathematics. In particular, for an empty Array, it returns false. * * @param callbackfn * the callback function to call * @return true if callbackfn returns true for at least one element in this * Array, false otherwise */ @BrowserCompatibility("IE:9+") public boolean some(Function3, Boolean> callbackfn) { if(callbackfn == null){ throw new Error("TypeError", "callbackfn is null"); } Iterator> iter = this.entryIterator(0, this.$length(), true); while (iter.hasNext()) { Entry entry = iter.next(); Boolean result = callbackfn.$invoke(entry.value, entry.key, this); if (Boolean.TRUE.equals(result)) { // false or null was returned return true; } } return false; } /** * Constructs a new Array, such that the value at each index in the new Array is the result of * calling the specified callback function on the value at the corresponding index in this Array. * *

* map calls callbackfn once for each element in this Array, in ascending order, and * constructs a new Array from the results. callbackfn is called only for elements of the array * which actually exist; it is not called for missing elements of this Array. * *

* callbackfn has three arguments: the value of the element, the index of the element, and the * Array being traversed (this Array). * *

* map does not directly mutate this Array but this Array may be mutated by the calls to * callbackfn. * *

* The range of elements processed by map is set before the first call to callbackfn. Elements * which are appended to this Array after the call to map begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time map visits them; elements that are deleted after the * call to map begins and before being visited are not visited. * * @param callbackfn * the callback used to create the elements of the new array * @return a new Array containing new elements as returned by the specified callback function */ @BrowserCompatibility("IE:9+") public Array map(Function3, T> callbackfn) { if(callbackfn == null){ throw new Error("TypeError", "callbackfn is null"); } int lengthBefore = this.$length(); Iterator> iter = this.entryIterator(0, this.$length(), true); Array result = new Array<>(); while (iter.hasNext()) { Entry entry = iter.next(); T mapped = callbackfn.$invoke(entry.value, entry.key, this); result.$set(entry.key, mapped); } result.$length(lengthBefore); return result; } /** * Constructs a new Array containing only the elements of this Array for which the specified * callback function returns true. * *

* filter calls callbackfn once for each element in this Array, in ascending order, and * constructs a new Array of all the values for which callbackfn returns true. * callbackfn is called only for elements of this Array which actually exist; it is not called for * missing elements of this Array. * *

* callbackfn has three arguments: the value of the element, the index of the element, and the * Array being traversed (this Array). * *

* filter does not directly mutate this Array, but this Array may be mutated by the calls * to callbackfn. * *

* The range of elements processed by filter is set before the first call to callbackfn. Elements * which are appended to this Array after the call to filter begins will not be visited by * callbackfn. If existing elements of this Array are changed their value as passed to * callbackfn will be the value at the time filter visits them; elements that are deleted after * the call to filter begins and before being visited are not visited. * * @param callbackfn * the callback function used to decide if an element should be appended to the new Array * @return a new Array containing only the elements of this Array for which the specified callback * function returns true. */ @BrowserCompatibility("IE:9+") public Array filter(Function3, Boolean> callbackfn) { if(callbackfn == null){ throw new Error("TypeError", "callbackfn is null"); } Iterator> iter = this.entryIterator(0, this.$length(), true); Array result = new Array<>(); while (iter.hasNext()) { Entry entry = iter.next(); boolean selected = callbackfn.$invoke(entry.value, entry.key, this); if(selected){ result.push(entry.value); } } return result; } /** * Applies the specified function against an accumulator and each value of this Array (from left-to-right) * as to reduce it to a single value, omitting the first element and using it as initial accumulator value. * *

* reduce calls the callback, as a function, once for each element present in this Array except * for the first one, in ascending order. * *

* callbackfn has four arguments: the previousValue (or value from the previous call to * callbackfn), the currentValue (value of the current element), the currentIndex, and * the Array being traversed (this Array). The first time that callback is called (that is, on the * second element element of this array) previousValue will be equal to the first value in this * Array and currentValue will be equal to the second. It is a TypeError if this Array * contains no elements. * *

* reduce does not directly mutate this Array but this Array may be mutated by the calls * to callbackfn. * *

* The range of elements processed by reduce is set before the first call to callbackfn. Elements * that are appended to this Array after the call to reduce begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time reduce visits them; elements that are deleted after * the call to reduce begins and before being visited are not visited. * * @param callbackfn * the function used to reduce the array to a single value * @return a single value derived from calling the callback function on all the elements of this Array */ @BrowserCompatibility("IE:9+, Safari:4+, Opera:10.50+") public V reduce(Function4, V> callbackfn) { return doReduce(callbackfn, UNSET, true); } /** * Applies the specified function against an accumulator and each value of this Array (from left-to-right) * as to reduce it to a single value. * *

* reduce calls the callback, as a function, once for each element present in this Array, in * ascending order. * *

* callbackfn has four arguments: the previousValue (or value from the previous call to * callbackfn), the currentValue (value of the current element), the currentIndex, and * the Array being traversed (this Array). The first time that callback is called * previousValue will be equal to initialValue and currentValue will be equal to the * first value in this Array. * *

* reduce does not directly mutate this Array but this Array may be mutated by the calls * to callbackfn. * *

* The range of elements processed by reduce is set before the first call to callbackfn. Elements * that are appended to this Array after the call to reduce begins will not be visited by * callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time reduce visits them; elements that are deleted after * the call to reduce begins and before being visited are not visited. * * @param callbackfn * the function used to reduce this Array to a single value * @return a single value derived from calling the callback function on all the elements of this Array */ @BrowserCompatibility("IE:9+, Safari:4+, Opera:10.50+") public T reduce(Function4, T> callbackfn, T initialValue) { return doReduce(callbackfn, initialValue, true); } /** * Applies the specified function against an accumulator and each value of this Array (from right-to-left) * as to reduce it to a single value, omitting the first element and using it as initial accumulator value. * *

* reduceRight calls the callback, as a function, once for each element present in this Array * except for the first one, in descending order. * *

* callbackfn has four arguments: the previousValue (or value from the previous call to * callbackfn), the currentValue (value of the current element), the currentIndex, and * the Array being traversed (this Array). The first time that callback is called (that is, on the * second-to-last element element of this array) previousValue will be equal to the last value in * this Array and currentValue will be equal to the second-to-last. It is a TypeError if this * Array contains no elements. * *

* reduceRight does not directly mutate this Array but this Array may be mutated by the * calls to callbackfn. * *

* The range of elements processed by reduceRight is set before the first call to callbackfn. * Elements that are appended to this Array after the call to reduceRight begins will not be * visited by callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time reduceRight visits them; elements that are deleted * after the call to reduceRight begins and before being visited are not visited. * * @param callbackfn * the function used to reduce the array to a single value * @return a single value derived from calling the callback function on all the elements of this Array */ @BrowserCompatibility("IE:9+, Safari:4+, Opera:10.50+") public V reduceRight(Function4, V> callbackfn) { return doReduce(callbackfn, UNSET, false); } /** * Applies the specified function against an accumulator and each value of this Array (from right-to-left) * as to reduce it to a single value. * *

* reduceRight calls the callback, as a function, once for each element present in this Array, in * descending order. * *

* callbackfn has four arguments: the previousValue (or value from the previous call to * callbackfn), the currentValue (value of the current element), the currentIndex, and * the Array being traversed (this Array). The first time that callback is called * previousValue will be equal to initialValue and currentValue will be equal to the last * value in this Array. * *

* reduceRight does not directly mutate this Array but this Array may be mutated by the * calls to callbackfn. * *

* The range of elements processed by reduceRight is set before the first call to callbackfn. * Elements that are appended to this Array after the call to reduceRight begins will not be * visited by callbackfn. If existing elements of this Array are changed, their value as passed to * callbackfn will be the value at the time reduceRight visits them; elements that are deleted * after the call to reduceRight begins and before being visited are not visited. * * @param callbackfn * the function used to reduce this Array to a single value * @return a single value derived from calling the callback function on all the elements of this Array */ @BrowserCompatibility("IE:9+, Safari:4+, Opera:10.50+") public T reduceRight(Function4, T> callbackfn, T initialValue) { return doReduce(callbackfn, initialValue, false); } private T doReduce(Function4, T> callbackfn, Object initialValue, boolean isForward){ if(callbackfn == null){ throw new Error("TypeError", "callbackfn is null"); } Iterator> iter; if(isForward) { iter = this.entryIterator(0, this.$length(), true); } else { iter = this.entryIterator(this.$length(), -1, false); } T accumulator; if(initialValue == UNSET){ // when initialValue is UNSET (the parameter was not specified) // then the types T and V are the same. if(!iter.hasNext()){ throw new Error("TypeError", "Array is empty and initialValue was not provided"); } @SuppressWarnings("unchecked") T temp = (T)iter.next().value; accumulator = temp; } else { // when initialValue is set (ie: not UNSET, the parameter was specified, but may be null) // then the type of initialValue is T @SuppressWarnings("unchecked") T temp = (T)initialValue; accumulator = temp; } while (iter.hasNext()) { Entry entry = iter.next(); accumulator = callbackfn.$invoke(accumulator, entry.value, entry.key, this); } return accumulator; } /** * Returns true if the specified object is an Array. * @param o the object to check * @return true if the object is an Array, false if not */ public static boolean isArray(Object o){ return o instanceof Array; } private abstract class ArrayStore { ArrayStore switchStoreType(long newLength) { ArrayStore that; if (this instanceof PackedArrayStore) { that = new SparseArrayStore<>(); } else { that = new PackedArrayStore<>(); } that.padTo(newLength); Iterator> entries = this.entryIterator(0, newLength, true); while (entries.hasNext()) { Entry entry = entries.next(); that.set(entry.key, entry.value); } return that; } abstract void truncateFrom(long newLength); abstract void padTo(long newLength); abstract void splice(long actualStart, long actualDeleteCount, E[] values); abstract long getSetElements(long firstIncluded, long lastExcluded); abstract Iterator> entryIterator(long actualStart, long actualEndExcluded, boolean isForward); abstract boolean isEfficientStoreFor(long newLength, long newElementCount); abstract void set(long index, E value); abstract boolean isSet(long index); abstract void delete(long index); abstract E get(long index); abstract Array slice(long fromIncluded, long toExcluded); abstract void reverse(); public abstract void sort(SortFunction comparefn); } private final class PackedArrayStore extends ArrayStore { /** * We can't use instead of here, because we must be able to make a difference between elements set * to null, and unset elements (represented by UNSET). */ private ArrayList elements; private PackedArrayStore() { elements = new ArrayList<>(); } private PackedArrayStore(List elements) { this.elements = new ArrayList(elements); } @Override boolean isEfficientStoreFor(long newLength, long newElementCount) { if (newLength > Integer.MAX_VALUE) { // PackedArrayStore cannot store values with index > Integer.MAX_VALUE return false; } if (newLength < 120) { // for small arrays, PackedArrayStore is always better // note that the threshold (120) intentionally doesn't match with the threshold defined in // SparseArrayStore.isEfficientStoreFor(), to make sure that we don't convert back and forth // between both types of ArrayStore return true; } if (newElementCount == 0 || (newLength / newElementCount) >= 6) { // if we have more than 5/6 unset elements, we're better off with SparseArrayStore // thresholds between the two ArrayStore types don't match: see length condition return false; } // we're good enough with the current store return true; } @Override void set(long index, E value) { this.elements.set((int) index, value); } @Override public void padTo(long newLength) { while (this.elements.size() < newLength) { this.elements.add(UNSET); } } @SuppressWarnings("unchecked") @Override E get(long index) { if (index > Integer.MAX_VALUE || index >= elements.size()) { return null; } Object value = this.elements.get((int) index); if (value == UNSET) { return null; } return (E) value; } @SuppressWarnings("unchecked") @Override Array slice(long fromIncluded, long toExcluded) { Array result = new Array(); for (int i = (int) fromIncluded, n = 0; i < toExcluded && i < this.elements.size(); i++, n++) { Object value = this.elements.get(i); if (value != UNSET) { result.$set(n, (E) this.elements.get(i)); } } return result; } @Override void reverse() { Collections.reverse(this.elements); } @Override public void sort(final SortFunction comparefn) { // This comparator implements the SortCompare algorithm in section 15.4.4.11 of the ECMA-262 Specification Comparator comparator = new Comparator(){ @Override @SuppressWarnings("unchecked") public int compare(Object x, Object y) { if(x == UNSET && y == UNSET){ return 0; } if(x == UNSET){ return 1; } if(y == UNSET){ return -1; } // In Java we don't have undefined values, so we skip steps 10 through 12 return comparefn.$invoke((E)x, (E)y); } }; Collections.sort(this.elements, comparator); } @Override boolean isSet(long index) { return index < this.elements.size() && this.elements.get((int) index) != UNSET; } @Override Iterator> entryIterator(final long actualStart, final long actualEndExcluded, final boolean isForward) { return new Iterator>() { private int nextIndex = (int) actualStart; @Override public boolean hasNext() { skipToNext(); if (isForward) { return nextIndex < actualEndExcluded; } else { return nextIndex > actualEndExcluded; } } @Override public Entry next() { skipToNext(); Entry entry = new Entry(); entry.key = nextIndex; entry.value = get(nextIndex); if (isForward) { nextIndex++; } else { nextIndex--; } return entry; } private void skipToNext() { if (isForward) { while (nextIndex < actualEndExcluded && !isSet(nextIndex)) { nextIndex++; } } else { while (nextIndex > actualEndExcluded && !isSet(nextIndex)) { nextIndex--; } } } @Override public void remove() { throw new UnsupportedOperationException(); } }; } @Override long getSetElements(long firstIncluded, long lastExcluded) { int setElements = 0; for (int i = (int) firstIncluded; i < this.elements.size() && i < lastExcluded; i++) { if (this.elements.get(i) != UNSET) { setElements++; } } return setElements; } @Override void splice(long actualStart, long actualDeleteCount, E[] values) { // we must try to avoid splicing in 0(n^2) if possible // let's compute the cost of two different methods of splicing in number of moves long trailingElements = this.elements.size() - actualStart - actualDeleteCount; long inplaceCost = (long) Math.abs(actualDeleteCount - values.length) * trailingElements; long rebuildCost = this.elements.size() + values.length - actualDeleteCount; // execute the cheapest strategy. We give an unfair advantage to inplace, because rebuild has // uses more memory (2n), so we only want to pick it when it's really worth it if (inplaceCost > 2 * rebuildCost) { // Rebuild the underlying ArrayList from scratch. // add the head untouched elements ArrayList newElements = new ArrayList((int) rebuildCost); newElements.addAll(this.elements.subList(0, (int) actualStart)); // add the inserted elements newElements.addAll(Arrays.asList(values)); // add the trailing untouched elements newElements .addAll(this.elements.subList((int) (actualStart + actualDeleteCount), this.elements.size())); this.elements = newElements; } else { // add/remove/set the elements in place, trying to add or remove as few elements as possible // as long as the deleted and added elements overlap, we can use Set instead of insert + delete // it's a lot cheaper to do it that way on an ArrayList int overlap = (int) Math.min(values.length, actualDeleteCount); for (int i = 0; i < overlap; i++) { this.elements.set((int) actualStart + i, values[i]); } // more deletions than insertions, we have a few more elements to delete // remove elements starting from the end, so we have to move fewer elements // in the backing array every time one element is removed for (int i = (int) (actualStart + actualDeleteCount - 1); i >= actualStart + overlap; i--) { this.elements.remove(i); } // more insertions than deletions, we have a few more element to insert // (this loop is mutually exclusive with the loop above) for (int i = overlap; i < values.length; i++) { this.elements.add((int) actualStart + i, values[i]); } } } @Override void truncateFrom(long newLength) { if (newLength < this.elements.size() / 4) { // if newLength is small enough, it's faster to just copy the remaining values instead of deleting the // truncated ones this.elements = new ArrayList(this.elements.subList(0, (int) newLength)); } else { for (int i = this.elements.size() - 1; i >= newLength; i--) { this.elements.remove(i); } } } @Override void delete(long index) { this.elements.set((int) index, UNSET); } } private final class SparseArrayStore extends ArrayStore { TreeMap elements = new TreeMap(); @Override boolean isEfficientStoreFor(long newLength, long newElementCount) { if (newLength > Integer.MAX_VALUE) { // SparseArrayStore is the only one that can store values with index > Integer.MAX_VALUE return true; } if (newLength < 80) { // for small arrays, PackedArrayStore is always better // note that the threshold (80) intentionally doesn't match with the threshold defined in // PackedArrayStore.isEfficientStoreFor(), to make sure that we don't convert back and forth // between both types of ArrayStore return false; } if ((newLength / newElementCount) < 3) { // if we have less than 2/3 empty elements, we're better off with PackedArrayStore. // thresholds between the two ArrayStore types don't match: see length condition return false; } // we're happy enough with the current store return true; } @Override void set(long index, E value) { this.elements.put(index, value); } @Override E get(long index) { return this.elements.get(index); } @Override Array slice(long fromIncluded, long toExcluded) { Array result = new Array(); Long firstKey = this.elements.ceilingKey(fromIncluded); Long lastKey = this.elements.lowerKey(toExcluded); if (firstKey != null && lastKey != null) { // only add stuff to the array if the selected range actually contains something // if we don't have a key that is higher than fromIncluded, it means the slice is empty // if we don't have a key that is smaller than toExcluded, it means the slice is empty java.util.Map selected = this.elements.subMap(firstKey, lastKey); for (java.util.Map.Entry entry : selected.entrySet()) { result.$set(entry.getKey(), entry.getValue()); } } // we must also set the length, just in case the last element that was requested was unset result.$length((int) (toExcluded - fromIncluded)); return result; } @Override void reverse() { long lenght = $length(); Set indices = new HashSet<>(this.elements.keySet()); // new instance to avoid concurrent modification Set alreadySwitched = new HashSet<>(); for(Long index : indices){ if(alreadySwitched.contains(index)){ continue; } E value = this.elements.get(index); long newIndex = length - 1 - index; if(this.elements.containsKey(newIndex)){ // there is already an element at this index, we must exchange both values // and record the one at that location as already switched E temp = this.elements.get(newIndex); this.elements.put(newIndex, value); this.elements.put(index, temp); alreadySwitched.add(newIndex); } else { // we can simply move the value to the new index this.elements.remove(index); this.elements.put(newIndex, value); } } } @Override public void sort(final SortFunction comparefn) { ArrayList values = new ArrayList<>(this.elements.values()); Comparator comparator = new Comparator(){ @Override public int compare(E x, E y) { // We do not have to worry about unset/undefined values, because they are not // present in the TreeMap anyway. return comparefn.$invoke(x, y); } }; Collections.sort(values, comparator); this.elements.clear(); // don't create a new instance, so we don't disrupt any iterators or submaps. for(int i = 0; i < values.size(); i ++){ this.elements.put((long)i, values.get(i)); } } @Override boolean isSet(long index) { return elements.containsKey(index); } @Override Iterator> entryIterator(final long actualStart, final long actualEndExcluded, final boolean isForward) { return new Iterator>() { private long lastProduced; { if(isForward){ lastProduced = actualStart - 1; } else { lastProduced = actualStart + 1; } } @Override public boolean hasNext() { if(isForward){ Long nextKey = elements.higherKey(lastProduced); return nextKey != null && nextKey < actualEndExcluded; } Long nextKey = elements.lowerKey(lastProduced); return nextKey != null && nextKey > actualEndExcluded; } @Override public Entry next() { Long nextKey; if(isForward){ nextKey = elements.higherKey(lastProduced); } else { nextKey = elements.lowerKey(lastProduced); } E value = elements.get(nextKey); Entry entry = new Entry<>(); entry.key = nextKey; entry.value = value; lastProduced = nextKey; return entry; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } @Override long getSetElements(long firstIncluded, long lastExcluded) { Long actualFirstIncluded = this.elements.ceilingKey(firstIncluded); Long actualLastExcluded = this.elements.lowerKey(lastExcluded); if (actualFirstIncluded == null || actualLastExcluded == null) { // there are no keys greater than first included OR // there are no keys smaller than last included // => the corresponding submap would be empty return 0; } return this.elements.subMap(actualFirstIncluded, actualLastExcluded).size(); } @Override void splice(long actualStart, long actualDeleteCount, E[] values) { // as long as the deleted and added elements overlap, we can use put() instead of insert + delete // this gives only one lookup in the tree instead of two long overlap = (long) Math.min(values.length, actualDeleteCount); for (int i = 0; i < overlap; i++) { this.elements.put(actualStart + i, values[i]); } // more deletions than insertions, we have a few more elements to delete for (int i = (int) actualDeleteCount - 1; i >= overlap; i--) { this.elements.remove((int) actualStart + overlap + i); } // more insertions than deletions, we have a few more element to insert // (this loop is mutually exclusive with the loop above) for (int i = (int) overlap; i < values.length; i++) { this.elements.put(actualStart + i, values[i]); } } @Override void truncateFrom(long newLength) { Long ceil = this.elements.ceilingKey(newLength); if (ceil != null) { // there are keys larger than newLength SortedMap toRemove = this.elements.subMap(ceil, true, this.elements.lastKey(), true); toRemove.clear(); } } @Override void padTo(long newLength) { // no padding needed } @Override void delete(long index) { this.elements.remove(index); } } private static class Entry { long key; E value; } }