From 1cf6e59393f2a41933ed58bd0dac762ff173dfad Mon Sep 17 00:00:00 2001 From: Cory Forsyth Date: Wed, 12 Aug 2015 15:53:16 -0400 Subject: [PATCH] Add LinkedList#removeBy --- src/js/utils/linked-list.js | 37 ++++++--- tests/unit/utils/linked-list-test.js | 110 +++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 9 deletions(-) diff --git a/src/js/utils/linked-list.js b/src/js/utils/linked-list.js index 2eab22491..8892fb40d 100644 --- a/src/js/utils/linked-list.js +++ b/src/js/utils/linked-list.js @@ -3,12 +3,19 @@ export default class LinkedList { this.head = null; this.tail = null; this.length = 0; + if (options) { - let {adoptItem, freeItem} = options; - this.adoptItem = adoptItem; - this.freeItem = freeItem; + const {adoptItem, freeItem} = options; + this._adoptItem = adoptItem; + this._freeItem = freeItem; } } + adoptItem(item) { + if (this._adoptItem) { this._adoptItem(item); } + } + freeItem(item) { + if (this._freeItem) { this._freeItem(item); } + } get isEmpty() { return this.length === 0; } @@ -28,10 +35,11 @@ export default class LinkedList { this.insertBefore(item, nextItem); } insertBefore(item, nextItem) { - this.remove(item); - if (this.adoptItem) { - this.adoptItem(item); + if (item.next || item.prev || this.head === item) { + throw new Error('Cannot insert an item into a list if it is already in a list'); } + this.adoptItem(item); + if (nextItem && nextItem.prev) { // middle of the items let prevItem = nextItem.prev; @@ -62,9 +70,8 @@ export default class LinkedList { this.length++; } remove(item) { - if (this.freeItem) { - this.freeItem(item); - } + this.freeItem(item); + let didRemove = false; if (item.next && item.prev) { // Middle of the list @@ -148,4 +155,16 @@ export default class LinkedList { this.insertBefore(newItem, nextItem); }); } + removeBy(conditionFn) { + let item = this.head; + while (item) { + let nextItem = item.next; + + if (conditionFn(item)) { + this.remove(item); + } + + item = nextItem; + } + } } diff --git a/tests/unit/utils/linked-list-test.js b/tests/unit/utils/linked-list-test.js index 31e996e17..ca5c50b28 100644 --- a/tests/unit/utils/linked-list-test.js +++ b/tests/unit/utils/linked-list-test.js @@ -345,3 +345,113 @@ test(`#splice can reorganize items`, (assert) => { assert.equal(list.objectAt(1), itemOne, 'itemOne is present'); assert.equal(list.objectAt(2), itemTwo, 'itemTwo is present'); }); + +test(`#removeBy mutates list when item is in middle`, (assert) => { + let list = new LinkedList(); + let items = [ + new LinkedItem(), + new LinkedItem(), + new LinkedItem(), + new LinkedItem() + ]; + items[1].shouldRemove = true; + items.forEach(i => list.append(i)); + + assert.equal(list.length, 4); + list.removeBy(i => i.shouldRemove); + assert.equal(list.length, 3); + assert.equal(list.head, items[0]); + assert.equal(list.objectAt(1), items[2]); + assert.equal(list.objectAt(2), items[3]); + assert.equal(list.tail, items[3]); +}); + +test(`#removeBy mutates list when item is first`, (assert) => { + let list = new LinkedList(); + let items = [ + new LinkedItem(), + new LinkedItem(), + new LinkedItem(), + new LinkedItem() + ]; + items[0].shouldRemove = true; + items.forEach(i => list.append(i)); + + assert.equal(list.length, 4); + list.removeBy(i => i.shouldRemove); + assert.equal(list.length, 3); + assert.equal(list.head, items[1]); + assert.equal(list.objectAt(1), items[2]); + assert.equal(list.tail, items[3]); +}); + +test(`#removeBy mutates list when item is last`, (assert) => { + let list = new LinkedList(); + let items = [ + new LinkedItem(), + new LinkedItem(), + new LinkedItem(), + new LinkedItem() + ]; + items[3].shouldRemove = true; + items.forEach(i => list.append(i)); + + assert.equal(list.length, 4); + list.removeBy(i => i.shouldRemove); + assert.equal(list.length, 3); + assert.equal(list.head, items[0]); + assert.equal(list.objectAt(1), items[1]); + assert.equal(list.tail, items[2]); +}); + +test('#removeBy calls `freeItem` for each item removed', (assert) => { + let freed = []; + + let list = new LinkedList({ + freeItem(item) { + freed.push(item); + } + }); + + let items = [ + new LinkedItem(), + new LinkedItem(), + new LinkedItem() + ]; + items[0].name = '0'; + items[1].name = '1'; + items[2].name = '2'; + + items[0].shouldRemove = true; + items[1].shouldRemove = true; + + items.forEach(i => list.append(i)); + + list.removeBy(i => i.shouldRemove); + + assert.deepEqual(freed, [items[0], items[1]]); +}); + +test('#insertBefore throws if item to be inserted is already in this list', (assert) => { + let item1 = new LinkedItem(); + let list1 = new LinkedList(); + list1.append(item1); + + assert.throws(() => { + list1.insertBefore(item1, null); + }); +}); + +test('#insertBefore throws if item to be inserted is in another non-empty list', (assert) => { + let item1 = new LinkedItem(); + let item2 = new LinkedItem(); + let list1 = new LinkedList(); + list1.append(item1); + list1.append(item2); + + let list2 = new LinkedList(); + + assert.throws(() => { + list2.insertBefore(item1, null); + }); +});