Mooeypoo has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/249329

Change subject: Add an OO.SortedEmitterList
......................................................................

Add an OO.SortedEmitterList

Extend OO.EmitterList to produce a sorted list that automatically
inserts items in their intended positions based on a given
sorting function.

Change-Id: I3fd569691549f24c134c0ef0c3a670c0d5a2d116
---
M Gruntfile.js
A src/SortedEmitterList.js
M tests/index.html
M tests/node-index.js
A tests/unit/SortedEmitterList.test.js
5 files changed, 294 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/oojs/core refs/changes/29/249329/1

diff --git a/Gruntfile.js b/Gruntfile.js
index 5f20890..c8d0f78 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -39,6 +39,7 @@
                                        'src/util.js',
                                        'src/EventEmitter.js',
                                        'src/EmitterList.js',
+                                       'src/SortedEmitterList.js',
                                        'src/Registry.js',
                                        'src/Factory.js',
                                        'src/export.js',
@@ -56,6 +57,7 @@
                                        'src/util/jquery.js',
                                        'src/EventEmitter.js',
                                        'src/EmitterList.js',
+                                       'src/SortedEmitterList.js',
                                        'src/Registry.js',
                                        'src/Factory.js',
                                        'src/export.js',
diff --git a/src/SortedEmitterList.js b/src/SortedEmitterList.js
new file mode 100644
index 0000000..259b756
--- /dev/null
+++ b/src/SortedEmitterList.js
@@ -0,0 +1,122 @@
+/**
+ * @class oo.SortedEmitterList
+ * Contains and a sorted OO.EmitterList
+ *
+ * @constructor
+ */
+oo.SortedEmitterList = function OoSortedEmitterList() {
+       // Mixin constructors
+       oo.EmitterList.call( this );
+
+       this.setSortingCallback( function ( a, b ) {
+               var aHash = oo.getHash( a ),
+                       bHash = oo.getHash( b );
+
+               return aHash > bHash ? 1 :
+                       ( aHash < bHash ? -1 : 0 );
+       } );
+};
+
+oo.mixinClass( oo.SortedEmitterList, oo.EmitterList );
+
+/**
+ * Set the sorting callback for this sorted list.
+ *
+ * @param {Function} sortingCallback Sorting callback
+ */
+oo.SortedEmitterList.prototype.setSortingCallback = function ( sortingCallback 
) {
+       this.sortingCallback = sortingCallback;
+};
+
+/**
+ * Add items to the sorted list.
+ *
+ * @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
+ *  an array of items to add
+ */
+oo.SortedEmitterList.prototype.addItems = function ( items ) {
+       var index, i;
+
+       if ( !Array.isArray( items ) ) {
+               items = [ items ];
+       }
+
+       if ( items.length === 0 ) {
+               return this;
+       }
+
+       // Call parent mixin
+       for ( i = 0; i < items.length; i++ ) {
+               // Find the insertion index for this item
+               index = this.findInsertionIndex( items[ i ] );
+               // Make sure the item does not yet exit. If it does
+               // exist it is already in the correct position in
+               // the list
+               if ( this.items[ index ] !== items[ i ] ) {
+                       // insert item at index
+                       index = this.insertItem( items[ i ], index );
+                       this.emit( 'add', items[ i ], index );
+               }
+       }
+};
+
+/**
+ * Normalize requested index to fit into the array.
+ * In the case of a sorted list, the index
+ *
+ * @param {OO.EventEmitter} item Items to insert
+ * @return {number} The index the item should be inserted into
+ */
+oo.SortedEmitterList.prototype.findInsertionIndex = function ( item ) {
+       var list = this;
+
+       return this.binarySearchIndex(
+               this.items,
+               // Fake a this.sortingCallback.bind( null, item ) call here
+               // otherwise this doesn't pass tests in phantomJS
+               function ( otherItem ) {
+                       return list.sortingCallback( item, otherItem );
+               },
+               true
+       );
+
+};
+
+/**
+ * Use binary search to locate an element in a sorted array.
+ *
+ * searchFunc is given an element from the array. `searchFunc(elem)` must 
return a number
+ * above 0 if the element we're searching for is to the right of (has a higher 
index than) elem,
+ * below 0 if it is to the left of elem, or zero if it's equal to elem.
+ *
+ * To search for a specific value with a comparator function (a `function 
cmp(a,b)` that returns
+ * above 0 if `a > b`, below 0 if `a < b`, and 0 if `a == b`), you can use
+ * `searchFunc = cmp.bind( null, value )`.
+ *
+ * @param {Array} arr Array to search in
+ * @param {Function} searchFunc Search function
+ * @param {boolean} [forInsertion] If not found, return index where val could 
be inserted
+ * @return {number|null} Index where val was found, or null if not found
+ */
+oo.SortedEmitterList.prototype.binarySearchIndex = function ( arr, searchFunc, 
forInsertion ) {
+       // TODO: Replace this with OO.binarySearch
+       // See https://gerrit.wikimedia.org/r/#/c/246813/
+       var mid, cmpResult,
+               left = 0,
+               right = arr.length;
+
+       while ( left < right ) {
+               // Equivalent to Math.floor( ( left + right ) / 2 ) but much 
faster
+               /*jshint bitwise:false */
+               mid = ( left + right ) >> 1;
+               cmpResult = searchFunc( arr[ mid ] );
+               if ( cmpResult < 0 ) {
+                       right = mid;
+               } else if ( cmpResult > 0 ) {
+                       left = mid + 1;
+               } else {
+                       return mid;
+               }
+       }
+       return forInsertion ? right : null;
+};
diff --git a/tests/index.html b/tests/index.html
index dcd5de1..cd1049c 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -15,6 +15,7 @@
                <script src="unit/core.test.js"></script>
                <script src="unit/EventEmitter.test.js"></script>
                <script src="unit/EmitterList.test.js"></script>
+               <script src="unit/SortedEmitterList.test.js"></script>
                <script src="unit/Factory.test.js"></script>
                <script src="unit/Prioritizer.test.js"></script>
                <script src="unit/Registry.test.js"></script>
diff --git a/tests/node-index.js b/tests/node-index.js
index dec4126..2f90173 100644
--- a/tests/node-index.js
+++ b/tests/node-index.js
@@ -18,6 +18,7 @@
                './tests/unit/util.test.js',
                './tests/unit/EventEmitter.test.js',
                './tests/unit/EmitterList.test.js',
+               './tests/unit/SortedEmitterList.test.js',
                './tests/unit/Registry.test.js',
                './tests/unit/Factory.test.js'
        ]
diff --git a/tests/unit/SortedEmitterList.test.js 
b/tests/unit/SortedEmitterList.test.js
new file mode 100644
index 0000000..4929530
--- /dev/null
+++ b/tests/unit/SortedEmitterList.test.js
@@ -0,0 +1,168 @@
+( function ( oo ) {
+
+       var SortedTestList, TestItem;
+
+       // Define a test list object using the oo.SortedEmitterList mixin
+       SortedTestList = function TestList() {
+               // Mixin constructor
+               oo.EventEmitter.call( this );
+               // Mixin constructor
+               oo.SortedEmitterList.call( this );
+       };
+       oo.mixinClass( SortedTestList, oo.EventEmitter );
+       oo.mixinClass( SortedTestList, oo.SortedEmitterList );
+
+       // Define a test item object
+       TestItem = function TestItem( content ) {
+               // Mixin constructor
+               oo.EventEmitter.call( this );
+
+               this.content = content;
+       };
+       oo.mixinClass( TestItem, oo.EventEmitter );
+
+       // Helper method to recognize items by their contents
+       TestItem.prototype.getContent = function () {
+               return this.content;
+       };
+
+       // Helper method to get an array of item contents
+       // for testing
+       function getContentArray( arr ) {
+               return arr.map( function ( item ) {
+                       return item.getContent();
+               } );
+       }
+
+       QUnit.module( 'SortedEmitterList' );
+
+       QUnit.test( 'addItems', function ( assert ) {
+               var initialItems = [
+                               new TestItem( 'aa' ),
+                               new TestItem( 'bb' ),
+                               new TestItem( 'cc' )
+                       ],
+                       cases = [
+                               {
+                                       items: [
+                                               initialItems[ 1 ], // bb
+                                               initialItems[ 0 ], // aa
+                                               initialItems[ 2 ] // cc
+                                       ],
+                                       expected: [ 'aa', 'bb', 'cc' ],
+                                       msg: 'Inserting items in sorted order.'
+                               },
+                               {
+                                       items: initialItems,
+                                       add: {
+                                               items: [
+                                                       new TestItem( 'ba' ),
+                                                       new TestItem( 'ab' ),
+                                                       new TestItem( 'cd' ),
+                                                       new TestItem( 'bc' )
+                                               ]
+                                       },
+                                       expected: [ 'aa', 'ab', 'ba', 'bb', 
'bc', 'cc', 'cd' ],
+                                       msg: 'Inserting items into the correct 
places in an existing list'
+                               }
+                       ];
+
+               assert.expect( cases.length );
+
+               cases.forEach( function ( test ) {
+                       var list = new SortedTestList();
+                       // Sort by content
+                       list.setSortingCallback( function ( a, b ) {
+                               return a.getContent() > b.getContent() ? 1 :
+                                       (
+                                               a.getContent() < b.getContent() 
? -1 :
+                                               0
+                                       );
+                       } );
+
+                       list.addItems( test.items );
+
+                       if ( test.add ) {
+                               list.addItems( test.add.items, test.add.index );
+                       }
+
+                       assert.deepEqual( getContentArray( list.getItems() ), 
test.expected, test.msg );
+               }, this );
+       } );
+
+       QUnit.test( 'Events', 1, function ( assert ) {
+               var result = [],
+                       list = new SortedTestList(),
+                       items = [
+                               new TestItem( 'aa' ),
+                               new TestItem( 'bb' ),
+                               new TestItem( 'cc' ),
+                               new TestItem( 'dd' )
+                       ],
+                       stringifyEvent = function ( type, item, index ) {
+                               var result = type;
+                               if ( item ) {
+                                       result += ':' + item.getContent();
+                               }
+                               if ( index !== undefined ) {
+                                       result += '#' + index;
+                               }
+                               return result;
+                       };
+
+               // Register
+               list.on( 'add', function ( item, index ) {
+                       result.push( stringifyEvent( 'add', item, index ) );
+               } );
+               list.on( 'move', function ( item, index ) {
+                       result.push( stringifyEvent( 'move', item, index ) );
+               } );
+               list.on( 'remove', function ( item, index ) {
+                       result.push( stringifyEvent( 'remove', item, index ) );
+               } );
+               list.on( 'clear', function () {
+                       result.push( stringifyEvent( 'clear' ) );
+               } );
+
+               // Trigger events
+               list.addItems( items );
+               list.addItems( [ new TestItem( 'ca' ) ] );
+               list.removeItems( items[ 2 ] );
+               list.addItems( [
+                       new TestItem( 'ab' ),
+                       new TestItem( 'ba' ),
+                       new TestItem( 'bc' ),
+                       new TestItem( 'cd' )
+               ] );
+               list.clearItems();
+               // Add items out of sequence
+               list.addItems( [
+                       new TestItem( 'dd' ),
+                       new TestItem( 'bb' ),
+                       new TestItem( 'aa' ),
+                       new TestItem( 'cc' )
+               ] );
+
+               assert.deepEqual( result, [
+                       // addItems
+                       'add:aa#0', // [ aa ]
+                       'add:bb#1', // [ aa, bb ]
+                       'add:cc#2', // [ aa, bb, cc ]
+                       'add:dd#3', // [ aa, bb, cc, dd ]
+                       // addItems
+                       'add:ca#2', // [ aa, bb, ca, cc, dd ]
+                       // removeItems
+                       'remove:cc#3', // [ aa, bb, ca, dd ]
+                       // addItems
+                       'add:ab#1', // [ aa, ab, bb, ca, dd ]
+                       'add:ba#2', // [ aa, ab, ba, bb, ca, dd ]
+                       'add:bc#4', // [ aa, ab, ba, bb, bc, ca, dd ]
+                       'add:cd#6', // [ aa, ab, ba, bb, bc, ca, cd, dd ]
+                       'clear',
+                       'add:dd#0', // [ dd ]
+                       'add:bb#0', // [ bb, dd]
+                       'add:aa#0', // [ aa, bb, dd]
+                       'add:cc#2' // [ aa, bb, cc, dd]
+               ], 'Events intercepted successfuly.' );
+       } );
+}( OO ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/249329
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3fd569691549f24c134c0ef0c3a670c0d5a2d116
Gerrit-PatchSet: 1
Gerrit-Project: oojs/core
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to