mediawiki-extensions-Visual.../modules/ve/ve.OrderedHashSet.js
Trevor Parscal b696e1e84b Fixed ve.OrderedHashSet.filter
Was cloning the original set, then adding to it - resulting in always containing the whole set in any match.

Also using test instead of exec since exec returns a string result, which under strange circumstances could return a falsy result, despite the RegExp finding a match.

Change-Id: I09a7cb264521d58f02d6ff2547edad9a740b23b2
2012-09-26 13:57:19 -07:00

355 lines
10 KiB
JavaScript

/**
* VisualEditor OrderedHashSet class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Ordered set of objects. Objects are treated as equal if their hashes are equal. This means that
* you can't add two objects with the same hash, and it also means that you can search for one
* object and get a different one back that has the same hash.
*
* The second parameter to this constructor can be an array of objects, or an existing
* ve.OrderedHashSet object. In both cases, the new Set will be a shallow copy: object references
* to the individual objects will be maintained, but references to the original array or
* ve.OrderedHashSet will not be.
*
* @class
* @abstract
* @constructor
* @param {Function} hash Hash function
* @param {Array|ve.OrderedHashSet} [items] Items to populate this set with
*/
ve.OrderedHashSet = function ( hash, items ) {
var i;
this.hash = hash;
// Array of object references
this.arr = [];
// Object mapping object hashes to object references
this.map = {};
// HACK HACK HACK
// We should fix our inheritance so instanceof works
if ( items instanceof ve.OrderedHashSet || items instanceof ve.AnnotationSet ) {
this.map = ve.extendObject( {}, items.map );
this.arr = items.arr.slice( 0 );
} else if ( ve.isArray( items ) && items.length > 0 ) {
this.arr = items.slice( 0 );
for ( i = 0; i < this.arr.length; i++ ) {
this.map[this.hash( this.arr[i] )] = this.arr[i];
}
}
};
/* Methods */
/**
* Create a clone of this set, with the same hash function and the same contents.
* @returns {ve.OrderedHashSet}
*/
ve.OrderedHashSet.prototype.clone = function () {
return new ve.OrderedHashSet( this.hash, this );
};
/**
* Get a value or all values from the set.
*
* set.get( 5 ) returns the object at index 5, set.get() returns an array with all objects in
* the entire set.
*
* @param {Number} [index] If set, only get the element at this index
* @returns {Array|Object|undefined} The object at index, or an array of all objects in the set
*/
ve.OrderedHashSet.prototype.get = function ( index ) {
if ( index !== undefined ) {
return this.arr[index];
} else {
return this.arr.slice( 0 );
}
};
/**
* @returns {Number} The number of objects in the set
*/
ve.OrderedHashSet.prototype.getLength = function () {
return this.arr.length;
};
/**
* @returns {Boolean} True if the set is empty, false otherwise
*/
ve.OrderedHashSet.prototype.isEmpty = function () {
return this.arr.length === 0;
};
/**
* Check whether a given value occurs in the set. Values are compared by hash.
*
* @returns {Boolean} True if there is an object in the set with the same hash as value, false otherwise
*/
ve.OrderedHashSet.prototype.contains = function ( value ) {
return this.hash( value ) in this.map;
};
/**
* Check whether this set contains any of the values in another set.
*
* @param {ve.OrderedHashSet} set Set to compare this set with
* @returns {Boolean} True if there is at least one value in set that is also in this set, false otherwise
*/
ve.OrderedHashSet.prototype.containsAnyOf = function ( set ) {
var key;
for ( key in set.map ) {
if ( key in this.map ) {
return true;
}
}
return false;
};
/**
* Check whether this set contains all of the values in another set.
*
* @param {ve.OrderedHashSet} set Set to compare this set with
* @returns {Boolean} True if all values in set are also in this set, false otherwise
*/
ve.OrderedHashSet.prototype.containsAllOf = function ( set ) {
var key;
for ( key in set.map ) {
if ( !( key in this.map ) ) {
return false;
}
}
return true;
};
/**
* Get the index of a given value in this set.
*
* @param {Object} value Value to search for
* @returns {Number} Index of value in this set, or -1 if value is not in this set.
*/
ve.OrderedHashSet.prototype.indexOf = function ( value ) {
var hash = this.hash( value );
if ( hash in this.map ) {
return ve.indexOf( this.map[hash], this.arr );
} else {
return -1;
}
};
/**
* Filter this set by a given property. This returns a new set with all values in this set for
* which value.property matches filter (if filter is a RegExp) or is equal to filter,
*
* @param {String} property Property to check
* @param {Mixed|RegExp} filter Regular expression or value to filter for
* @param {Boolean} [returnBool] For internal use only
* @returns {ve.OrderedHashSet} New set containing only the matching values
*/
ve.OrderedHashSet.prototype.filter = function ( property, filter, returnBool ) {
var i, result;
if ( !returnBool ) {
// TODO: Consider alternative ways to instantiate a new set of the same type as the subclass
result = this.clone();
// TODO: Should we be returning this on all methods that modify the original? Might help
// with chainability, but perhaps it's also confusing because most chainable methods return
// a new hash set.
result.removeAll();
}
for ( i = 0; i < this.arr.length; i++ ) {
if (
( filter instanceof RegExp && filter.test( this.arr[i][property] ) ) ||
( typeof filter === 'string' && this.arr[i][property] === filter )
) {
if ( returnBool ) {
return true;
} else {
result.push( this.arr[i] );
}
}
}
return returnBool ? false : result;
};
/**
* Check if this set contains at least one value where a given property matches a given filter.
*
* This is equivalent to (but more efficient than) !this.filter( .. ).isEmpty()
* @see ve.OrderedHashSet.prototype.filter
*
* @param {String} property
* @param {Mixed|RegExp} filter
* @returns {Boolean} True if at least one value matches, false otherwise
*/
ve.OrderedHashSet.prototype.containsMatching = function ( property, filter ) {
return this.filter( property, filter, true );
};
/**
* Add a value to this set. If the value is already present in this set, nothing happens.
*
* The value will be inserted before the value that is currently at the given index. If index is
* negative, it will be counted from the end (i.e. index -1 is the last item, -2 the second-to-last,
* etc.). If index is out of bounds, the value will be added to the end of the set.
*
* @param {Object} value Value to add
* @param {Number} index Index to add the value at
*/
ve.OrderedHashSet.prototype.add = function ( value, index ) {
var hash;
if ( index < 0 ) {
index = this.arr.length + index;
}
if ( index >= this.arr.length ) {
this.push( value );
return;
}
hash = this.hash( value );
if ( !( hash in this.map ) ) {
this.arr.splice( index, 0, value );
this.map[hash] = value;
}
};
/**
* Add all values in the given set to the end of this set. Values from the other set that are
* already in this set will not be added again.
* @param {ve.OrderedHashSet} set Set to add to this set
*/
ve.OrderedHashSet.prototype.addSet = function ( set ) {
var i;
for ( i = 0; i < set.arr.length; i++ ) {
this.push( set.arr[i] );
}
};
/**
* Add a value at the end of this set. If the value is already present in this set, nothing happens.
* @param {Object} value Value to add
*/
ve.OrderedHashSet.prototype.push = function ( value ) {
var hash = this.hash( value );
if ( !( hash in this.map ) ) {
this.arr.push( value );
this.map[hash] = value;
}
};
/**
* Remove the value at a given index
* @param {Number} index Index to remove item at. If negative, this counts from the end, see add()
* @throws {Error} 'Index out of bounds'
*/
ve.OrderedHashSet.prototype.removeAt = function ( index ) {
if ( index < 0 ) {
index = this.arr.length + index;
}
if ( index >= this.arr.length ) {
throw new Error( 'Index out of bounds' );
}
delete this.map[this.hash( this.arr[index] )];
this.arr.splice( index, 1 );
};
/**
* Remove a given value from the set. If the value isn't in the set, nothing happens.
* @param {Object} value Value to remove
*/
ve.OrderedHashSet.prototype.remove = function ( value ) {
var index, hash = this.hash( value );
if ( hash in this.map ) {
index = ve.indexOf( this.map[hash], this.arr );
delete this.map[hash];
this.arr.splice( index, 1 );
}
};
/**
* Remove all values.
*/
ve.OrderedHashSet.prototype.removeAll = function () {
var i;
for ( i = 0; i < this.arr.length; i++ ) {
this.remove( this.arr[i] );
}
return this;
};
/**
* Remove all values in a given set from this set. Values that aren't in this set are ignored.
* @param {ve.OrderedHashSet} set Set to remove from this set
*/
ve.OrderedHashSet.prototype.removeSet = function ( set ) {
var i;
for ( i = 0; i < set.arr.length; i++ ) {
this.remove( set.arr[i] );
}
};
/**
* Remove all values that are not also in a given other set from this set.
* @param {ve.OrderedHashSet} set Set to intersect with this set
*/
ve.OrderedHashSet.prototype.removeNotInSet = function ( set ) {
var i;
for ( i = this.arr.length - 1; i >= 0; i-- ) {
if ( !set.contains( this.arr[i] ) ) {
this.removeAt( i );
}
}
};
/**
* Reverse this set.
*
* This returns a copy, the original set is not modified.
*
* @returns {ve.OrderedHashSet} Copy of this set with the order reversed.
*/
ve.OrderedHashSet.prototype.reversed = function () {
var newSet = this.clone();
newSet.arr.reverse();
return newSet;
};
/**
* Merge another set into this set.
*
* This returns a copy, the original set is not modified.
*
* @param {ve.OrderedHashSet} set Other set
* @returns {ve.OrderedHashSet} Set containing all values in this set as well as all values in set
*/
ve.OrderedHashSet.prototype.mergeWith = function ( set ) {
var newSet = this.clone();
newSet.addSet( set );
return newSet;
};
/**
* Get the difference between this set and another set.
*
* @param {ve.OrderedHashSet} set Other set
* @returns {ve.OrderedHashSet} New set containing all values that are in this set but not in set
*/
ve.OrderedHashSet.prototype.diffWith = function ( set ) {
var newSet = this.clone();
newSet.removeSet( set );
return newSet;
};
/**
* Get the intersection of this set with another set.
*
* @param {ve.OrderedHashSet} set Other set
* @returns {ve.OrderedHashSet} New set containing all values that are both in this set and in set
*/
ve.OrderedHashSet.prototype.intersectWith = function ( set ) {
var newSet = this.clone();
newSet.removeNotInSet( set );
return newSet;
};