diff --git a/README.md b/README.md index 569ab2a..237d2a4 100644 --- a/README.md +++ b/README.md @@ -461,6 +461,27 @@ Collections: (Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) +### deleteAll(value, opt_equals) + +Deletes every value equivalent to the given value from the collection. +For sets, this is equivalent to delete, but for lists, arrays, and sorted +arrays, may delete more than one value. +For lists and arrays, this involves a linear search, from the beginning, +splicing out each node as it is traversed. +For sorted arrays, there is a mode for the provided equals and the intrinsic +equals. +The provided equals falls back to the linear search provided by the underlying +array. +However, if deleteAll uses its intrinsic order and equivalence, it can guarantee +that all intrinsic values are within a range from the first to the last +equivalent value, so it can splice all equivalent values at once, using a binary +search to find the first equivalent value, and a linear search to find the last.. +The method is not implemented on Deque or Heap since random manipulation of +internal content is out of scope for these collections. + +Collections: (Array+, List, Set, SortedSet, LruSet, LfuSet, SortedArray, +SortedArraySet) + ### indexOf(value) Returns the position in the collection of a value, or `-1` if it is diff --git a/checklist.csv b/checklist.csv index cb5eb7c..d2a1f7f 100644 --- a/checklist.csv +++ b/checklist.csv @@ -16,6 +16,7 @@ Order,Method,Interface,Set,SortedSet,LruSet,LfuSet,SortedArraySet,FastSet,ArrayS 0d2a1b,"delete(value, equals)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(na),(alt),(alt),(na),Array,(alt),,,, 0d2a2,delete(key or index),map,(alt),(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(alt),(na),(alt),(alt),(na),(alt),(todo maybe for the property change),,,GenericMap, 0d2b1,"deleteEach(keys or values, optional equals)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo maybe),GenericCollection,(na),GenericCollection,GenericCollection,(na),GenericCollection,(todo maybe),GenericCollection,,, +0d2c,"deleteAll(value, equals)",(na),GenericSet,GenericSet,GenericSet,GenericSet,GenericSet,GenericSet,GenericSet,(na),(na),(na),(na),(na),(na),(na),(na),List,(na),(na),SortedArray,(na),Array,(na),,GenericSet,, 1a1,"indexOf(value, index)",array,(na),SortedSet,(na),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),Deque,(na),SortedArray O(log length),(todo),(spec),(na),,,, 1a2,"lastIndexOf(value, index)",order,(na),(na because uniqueness guarantees equivalence to indexOf),(na),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),Deque,(na),SortedArray O(log length),(todo),(spec),(na),,,, 1b1,"find(callback, thisp, index)",order,(todo),(todo),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),ES6,(na),,,, diff --git a/generic-set.js b/generic-set.js index f2df9fa..64bb179 100644 --- a/generic-set.js +++ b/generic-set.js @@ -30,6 +30,13 @@ GenericSet.prototype.symmetricDifference = function (that) { return union.difference(intersection); }; +GenericSet.prototype.deleteAll = function (value) { + // deleteAll is equivalent to delete for sets since they guarantee that + // only one value exists for an equivalence class, but deleteAll returns + // the count of deleted values instead of whether a value was deleted. + return +this["delete"](value); +}; + GenericSet.prototype.equals = function (that, equals) { var self = this; return ( diff --git a/list.js b/list.js index 5dda0d5..c49ffb7 100644 --- a/list.js +++ b/list.js @@ -37,7 +37,7 @@ List.prototype.find = function (value, equals, index) { var head = this.head; var at = this.scan(index, head.next); while (at !== head) { - if (equals(at.value, value)) { + if (equals(value, at.value)) { return at; } at = at.next; @@ -49,7 +49,7 @@ List.prototype.findLast = function (value, equals, index) { var head = this.head; var at = this.scan(index, head.prev); while (at !== head) { - if (equals(at.value, value)) { + if (equals(value, at.value)) { return at; } at = at.prev; @@ -88,6 +88,22 @@ List.prototype['delete'] = function (value, equals) { return false; }; +List.prototype.deleteAll = function (value, equals) { + equals = equals || this.contentEquals; + var head = this.head; + var at = head.next; + var count = 0; + while (at !== head) { + if (equals(value, at.value)) { + at["delete"](); + count++; + } + at = at.next; + } + this.length -= count; + return count; +}; + List.prototype.clear = function () { var plus, minus; if (this.dispatchesRangeChanges) { diff --git a/shim-array.js b/shim-array.js index 1e91434..4237dee 100644 --- a/shim-array.js +++ b/shim-array.js @@ -118,6 +118,20 @@ define("delete", function (value, equals) { return false; }); +define("deleteAll", function (value, equals) { + equals = equals || this.contentEquals || Object.equals; + var count = 0; + for (var index = 0; index < this.length;) { + if (equals(value, this[index])) { + this.swap(index, 1); + count++; + } else { + index++; + } + } + return count; +}); + define("find", function (value, equals) { equals = equals || this.contentEquals || Object.equals; for (var index = 0; index < this.length; index++) { diff --git a/sorted-array.js b/sorted-array.js index 461bab1..6f7f0fb 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -158,6 +158,34 @@ SortedArray.prototype["delete"] = function (value, equals) { } }; +SortedArray.prototype.deleteAll = function (value, equals) { + if (equals) { + var count = this.array.deleteAll(value, equals); + this.length -= count; + return count; + } else { + var start = searchFirst(this.array, value, this.contentCompare, this.contentEquals); + if (start !== -1) { + var end = start; + while (this.contentEquals(value, this.array[end])) { + end++; + } + var minus = this.slice(start, end); + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange([], minus, start); + } + this.array.splice(start, minus.length); + this.length -= minus.length; + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange([], minus, start); + } + return minus.length; + } else { + return 0; + } + } +}; + SortedArray.prototype.indexOf = function (value) { return searchFirst(this.array, value, this.contentCompare, this.contentEquals); }; @@ -173,6 +201,7 @@ SortedArray.prototype.find = function (value, equals, index) { if (index) { throw new Error("SortedArray#find does not support third argument: index"); } + // TODO support initial partition index return searchFirst(this.array, value, this.contentCompare, this.contentEquals); }; @@ -183,6 +212,7 @@ SortedArray.prototype.findLast = function (value, equals, index) { if (index) { throw new Error("SortedArray#findLast does not support third argument: index"); } + // TODO support initial partition index return searchLast(this.array, value, this.contentCompare, this.contentEquals); }; diff --git a/spec/array-spec.js b/spec/array-spec.js index 7cfbfd8..2b8c207 100644 --- a/spec/array-spec.js +++ b/spec/array-spec.js @@ -286,5 +286,13 @@ describe("Array", function () { }); + describe("deleteAll", function () { + it("should delete a range of equivalent values", function () { + var array = [1, 1, 1, 2, 2, 2, 3, 3, 3]; + expect(array.deleteAll(2)).toBe(3); + expect(array).toEqual([1, 1, 1, 3, 3, 3]); + }); + }); + }); diff --git a/spec/list-spec.js b/spec/list-spec.js index dc8e7ad..9759351 100644 --- a/spec/list-spec.js +++ b/spec/list-spec.js @@ -205,6 +205,20 @@ describe("List", function () { }); + describe("deleteAll", function () { + it("deletes all equivalent values", function () { + var anyEven = { + equals: function (that) { + return that % 2 === 0; + } + }; + var collection = List([1, 2, 3, 4, 5]); + expect(collection.deleteAll(anyEven)).toBe(2); + expect(collection.toArray()).toEqual([1, 3, 5]); + expect(collection.length).toBe(3); + }); + }); + describeRangeChanges(List); }); diff --git a/spec/set.js b/spec/set.js index 793efcc..e743d2e 100644 --- a/spec/set.js +++ b/spec/set.js @@ -49,6 +49,12 @@ function describeSet(Set, sorted) { expect(set.has(object)).toBe(false); }); + it("can deleteAll", function () { + var set = new Set([0]); + expect(set.deleteAll(0)).toBe(1); + expect(set.deleteAll(0)).toBe(0); + }); + if (!sorted) { it("can add and delete objects from the same bucket", function () { var a = {id: 0}, b = {id: 1}; diff --git a/spec/sorted-array-spec.js b/spec/sorted-array-spec.js index f3f5ae7..32e5376 100644 --- a/spec/sorted-array-spec.js +++ b/spec/sorted-array-spec.js @@ -19,8 +19,27 @@ describe("SortedArray", function () { }); describe("non-uniqueness", function () { - var array = SortedArray([1, 2, 3, 1, 2, 3]); - expect(array.slice()).toEqual([1, 1, 2, 2, 3, 3]); + it("should retain non-unique values", function () { + var array = SortedArray([1, 2, 3, 1, 2, 3]); + expect(array.slice()).toEqual([1, 1, 2, 2, 3, 3]); + }); + }); + + describe("deleteAll", function () { + it("should delete a range of equivalent values", function () { + var array = SortedArray([1, 1, 1, 2, 2, 2, 3, 3, 3]); + expect(array.deleteAll(2)).toBe(3); + expect(array.toArray()).toEqual([1, 1, 1, 3, 3, 3]); + }); + it("deletes all equivalent values for an alternate relation", function () { + var equivalent = function (a, b) { + return a % 2 === b % 2; + }; + var collection = SortedArray([1, 2, 3, 4, 5]); + expect(collection.deleteAll(2, equivalent)).toBe(2); + expect(collection.toArray()).toEqual([1, 3, 5]); + expect(collection.length).toBe(3); + }); }); // TODO test stability