Henning Snater has uploaded a new change for review. https://gerrit.wikimedia.org/r/91179
Change subject: Upgraded ByPropertyIdArray ...................................................................... Upgraded ByPropertyIdArray ByPropertyIdArray may act as an interface between a flat list of objects and its objects grouped by property now. Objects can be added to and moved within the structure. Change-Id: I771507cea8e38c117af9d27ed4af7b573bc1e897 --- M DataModel/ByPropertyIdArray.php M tests/phpunit/ByPropertyIdArrayTest.php 2 files changed, 525 insertions(+), 1 deletion(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/WikibaseDataModel refs/changes/79/91179/1 diff --git a/DataModel/ByPropertyIdArray.php b/DataModel/ByPropertyIdArray.php index 2fb165b..90770af 100644 --- a/DataModel/ByPropertyIdArray.php +++ b/DataModel/ByPropertyIdArray.php @@ -24,6 +24,7 @@ * * @licence GNU GPL v2+ * @author Jeroen De Dauw < [email protected] > + * @author H. Snater < [email protected] > */ class ByPropertyIdArray extends \ArrayObject { @@ -96,4 +97,342 @@ return $this->byId[$propertyId->getSerialization()]; } + /** + * Returns the absolute index of an object or false if the object could not be found. + * @since 0.5 + * + * @param object $object + * @return bool|int + * + * @throws RuntimeException + */ + public function indexOf( $object ) { + if ( $this->byId === null ) { + throw new RuntimeException( 'Index not build, call buildIndex first' ); + } + + $i = 0; + foreach( $this as $o ) { + if( $o === $object ) { + return $i; + } + $i++; + } + return false; + } + + /** + * Returns the objects in a flat array (using the indexed form for generating the array). + * @since 0.5 + * + * @return object[] + * + * @throws RuntimeException + */ + public function toArray() { + if ( $this->byId === null ) { + throw new RuntimeException( 'Index not build, call buildIndex first' ); + } + + $array = array(); + foreach( $this->byId as $objects ) { + foreach( $objects as $object ) { + $array[] = $object; + } + } + return $array; + } + + /** + * Returns the absolute numeric indices of objects featuring the same property id. + * @since 0.5 + * + * @param PropertyId $propertyId + * @return int[] + * + * @throws RuntimeException + */ + protected function getNumericIndices( PropertyId $propertyId ) { + if ( $this->byId === null ) { + throw new RuntimeException( 'Index not build, call buildIndex first' ); + } + + $propertyIndices = array(); + $i = 0; + + foreach( $this->byId as $objects ) { + foreach( $objects as $object ) { + if( $object->getPropertyId()->equals( $propertyId ) ) { + $propertyIndices[] = $i; + } + $i++; + } + } + + return $propertyIndices; + } + + /** + * Moves an object within its "property group". + * @since 0.5 + * + * @param object $object + * @param int $toIndex Absolute index within a "property group". + * + * @throws OutOfBoundsException + */ + protected function moveInPropertyGroup( $object, $toIndex ) { + $currentIndex = $this->indexOf( $object ); + + if( $toIndex === $currentIndex ) { + return; + } + + $propertyId = $object->getPropertyId(); + $propertyIdSerialization = $object->getPropertyId()->getSerialization(); + + $numericIndices = $this->getNumericIndices( $propertyId ); + + if( + $toIndex > $numericIndices[count( $numericIndices ) - 1] + 1 + || $toIndex < $numericIndices[0] + ) { + throw new OutOfBoundsException( 'Object cannot be moved to ' . $toIndex ); + } + + $propertyGroup = array_combine( $numericIndices, $this->getByPropertyId( $propertyId ) ); + + if( $toIndex > $numericIndices[count( $numericIndices ) - 1] ) { + // Move to the end. + unset( $propertyGroup[$currentIndex] ); + $propertyGroup[] = $object; + $this->byId[$propertyIdSerialization] = $propertyGroup; + } else { + $insertBefore = $propertyGroup[$toIndex]; + unset( $propertyGroup[$currentIndex] ); + + $this->byId[$propertyIdSerialization] = array(); + + foreach( $propertyGroup as $o ) { + if( $o === $insertBefore ) { + $this->byId[$propertyIdSerialization][] = $object; + } + $this->byId[$propertyIdSerialization][] = $o; + } + } + + $this->exchangeArray( $this->toArray() ); + } + + /** + * Moves a whole "property group". + * @since 0.5 + * + * @param PropertyId $propertyId + * @param int $toIndex + */ + protected function movePropertyGroup( PropertyId $propertyId, $toIndex ) { + if( $this->getPropertyGroupIndex( $propertyId ) === $toIndex ) { + return; + } + + /** + * @var PropertyId + */ + $insertBefore = null; + + foreach( $this->getPropertyIds() as $pId ) { + // Accepting other than the exact index by using <= letting the "property group" "latch" + // in the next slot. + if( $toIndex <= $this->getPropertyGroupIndex( $pId ) ) { + $insertBefore = $pId; + break; + } + } + + if( $propertyId->equals( $insertBefore ) ) { + return; + } + + $clone = $this->byId; + $serializedPropertyId = $propertyId->getSerialization(); + $this->byId = array(); + + foreach( $clone as $serializedPId => $objects ) { + $pId = new PropertyId( $serializedPId ); + if( $pId->equals( $propertyId ) ) { + continue; + } elseif( $pId->equals( $insertBefore ) ) { + $this->byId[$serializedPropertyId] = $clone[$serializedPropertyId]; + } + $this->byId[$serializedPId] = $objects; + } + + if( is_null( $insertBefore ) ) { + $this->byId[$serializedPropertyId] = $clone[$serializedPropertyId]; + } + + $this->exchangeArray( $this->toArray() ); + } + + /** + * Returns the index of a "property group" (the first object in the flat array that features + * the specified property). Returns false if property id could not be found. + * @since 0.5 + * + * @param PropertyId $propertyId + * @return bool|int + * + * @throws RuntimeException + */ + protected function getPropertyGroupIndex( PropertyId $propertyId ) { + if ( $this->byId === null ) { + throw new RuntimeException( 'Index not build, call buildIndex first' ); + } + + $i = 0; + + foreach( $this->byId as $serializedPropertyId => $objects ) { + $pId = new PropertyId( $serializedPropertyId ); + if( $pId->equals( $propertyId ) ) { + return $i; + } + $i += count( $objects ); + } + + return false; + } + + /** + * Moves an existing object to a new index. Specifying an index outside the object's "property + * group" will move the object to the edge of the "property group" and shift the whole group + * to achieve the designated index for the object to move. + * @since 0.5 + * + * @param object $object + * @param int $toIndex Absolute index where to move the object to. + * + * @throws RuntimeException|OutOfBoundsException + */ + public function move( $object, $toIndex ) { + if ( $this->byId === null ) { + throw new RuntimeException( 'Index not build, call buildIndex first' ); + } + + if( !in_array( $object, $this->toArray() ) ) { + throw new OutOfBoundsException( 'Object not present in array' ); + } elseif( $toIndex < 0 || $toIndex > count( $this ) ) { + throw new OutOfBoundsException( 'Specified index is out of bounds' ); + } elseif( $this->indexOf( $object ) === $toIndex ) { + return; + } + + // Determine whether to simply reindex the object within its "property group": + $propertyIndices = $this->getNumericIndices( $object->getPropertyId() ); + $propertyIndices[] = $propertyIndices[count( $propertyIndices ) - 1] + 1; + + if( in_array( $toIndex, $propertyIndices ) ) { + $this->moveInPropertyGroup( $object, $toIndex ); + } else { + $edgeIndex = ( $toIndex < $propertyIndices[0] ) + ? $propertyIndices[0] + : $propertyIndices[count( $propertyIndices ) - 1]; + + $this->moveInPropertyGroup( $object, $edgeIndex ); + $this->movePropertyGroup( $object->getPropertyId(), $toIndex ); + } + + $this->exchangeArray( $this->toArray() ); + } + + /** + * Adds an object at a specific index. If no index is specified, the object will be append to + * the end of its "property group" or - if no objects featuring the same property exist - to the + * absolute end of the array. + * Specifying an index outside a "property group" will place the new object at the specified + * index with the existing "property group" objects being shifted towards the new new object. + * @since 0.5 + * + * @param object $object + * @param int $index Absolute index where to place the new object. + * + * @throws RuntimeException + */ + public function add( $object, $index = null ) { + if ( $this->byId === null ) { + throw new RuntimeException( 'Index not build, call buildIndex first' ); + } + + $propertyId = $object->getPropertyId(); + $validIndices = $this->getNumericIndices( $propertyId ); + + if( count( $this ) === 0 ) { + // Array is empty, just append object. + $this->append( $object ); + + } elseif( count( $validIndices ) === 0 ) { + // No objects featuring that property exist. The object may be inserted at a place + // between existing "property groups". + $this->append( $object ); + if( !is_null( $index ) ) { + $this->buildIndex(); + $this->move( $object, $index ); + } + + } else { + // Objects featuring the same property as the object which is about to be added already + // exist in the array. + $this->addToPropertyGroup( $object, $index ); + } + + $this->buildIndex(); + } + + /* + * Adds an object to an existing property group at the specified absolute index. + * @since 0.5 + * + * @param object $object + * @param int $index + * + * @throws OutOfBoundsException + */ + protected function addToPropertyGroup( $object, $index = null ) { + $propertyId = $object->getPropertyId(); + $validIndices = $this->getNumericIndices( $propertyId ); + + if( count( $validIndices ) === 0 ) { + throw new OutOfBoundsException( 'No objects featuring the object\'s property exist' ); + } + + // Add index to allow placing object after the last object of the "property group": + $validIndices[] = $validIndices[count( $validIndices ) - 1] + 1; + + if( is_null( $index ) ) { + // If index is null, append object to "property group". + $index = $validIndices[count( $validIndices ) - 1]; + } + + if( in_array( $index, $validIndices ) ) { + // Add object at index within "property group". + $this->byId[$propertyId->getSerialization()][] = $object; + $this->exchangeArray( $this->toArray() ); + $this->move( $object, $index ); + + } else { + // Index is out of the "property group"; The whole group needs to be moved. + $this->movePropertyGroup( $propertyId, $index ); + + // Move new object to the edge of the "property group" to receive its designated + // index: + if( $index < $validIndices[0] ) { + array_unshift( $this->byId[$propertyId->getSerialization()], $object ); + } else { + $this->byId[$propertyId->getSerialization()][] = $object; + } + } + + $this->exchangeArray( $this->toArray() ); + } + } diff --git a/tests/phpunit/ByPropertyIdArrayTest.php b/tests/phpunit/ByPropertyIdArrayTest.php index 54843b4..ca95321 100644 --- a/tests/phpunit/ByPropertyIdArrayTest.php +++ b/tests/phpunit/ByPropertyIdArrayTest.php @@ -5,7 +5,6 @@ use DataValues\StringValue; use Wikibase\ByPropertyIdArray; use Wikibase\DataModel\Entity\PropertyId; -use Wikibase\Property; use Wikibase\Snak; use Wikibase\Claim; use Wikibase\PropertyNoValueSnak; @@ -28,6 +27,7 @@ * * @licence GNU GPL v2+ * @author Jeroen De Dauw < [email protected] > + * @author H. Snater < [email protected] > */ class ByPropertyIdArrayTest extends \PHPUnit_Framework_TestCase { @@ -65,6 +65,27 @@ } return $argLists; + } + + /** + * @return Claim[] + */ + protected function claimsProvider() { + $snaks = array( + new PropertyNoValueSnak( new PropertyId( 'P1' ) ), + new PropertySomeValueSnak( new PropertyId( 'P1' ) ), + new PropertyValueSnak( new PropertyId( 'P2' ), new StringValue( 'a' ) ), + new PropertyValueSnak( new PropertyId( 'P2' ), new StringValue( 'b' ) ), + new PropertyValueSnak( new PropertyId( 'P2' ), new StringValue( 'c' ) ), + new PropertySomeValueSnak( new PropertyId( 'P3' ) ), + ); + + return array_map( + function( Snak $snak ) { + return new Claim( $snak ); + }, + $snaks + ); } /** @@ -145,4 +166,168 @@ $indexedArray->getPropertyIds(); } + /** + * @dataProvider listProvider + * @param array $objects + */ + public function testIndexOf( array $objects ) { + $indexedArray = new ByPropertyIdArray( $objects ); + $indexedArray->buildIndex(); + + $indicesSource = array(); + $indicesDestination = array(); + + $i = 0; + foreach( $objects as $object ) { + $indicesSource[$i++] = $object; + $indicesDestination[$indexedArray->indexOf( $object )] = $object; + } + + $this->assertEquals( $indicesSource, $indicesDestination ); + } + + /** + * @dataProvider listProvider + * @param array $objects + */ + public function testToArray( array $objects ) { + $indexedArray = new ByPropertyIdArray( $objects ); + $indexedArray->buildIndex(); + + $this->assertEquals( $objects, $indexedArray->toArray() ); + } + + public function moveProvider() { + $c = $this->claimsProvider(); + $argLists = array(); + + $argLists[] = array( $c, $c[0], 0, $c ); + $argLists[] = array( $c, $c[0], 1, $c ); + $argLists[] = array( $c, $c[0], 2, array( $c[1], $c[0], $c[2], $c[3], $c[4], $c[5] ) ); + $argLists[] = array( $c, $c[0], 3, array( $c[2], $c[3], $c[4], $c[1], $c[0], $c[5] ) ); + $argLists[] = array( $c, $c[0], 4, array( $c[2], $c[3], $c[4], $c[1], $c[0], $c[5] ) ); + $argLists[] = array( $c, $c[0], 5, array( $c[2], $c[3], $c[4], $c[1], $c[0], $c[5] ) ); + $argLists[] = array( $c, $c[0], 6, array( $c[2], $c[3], $c[4], $c[5], $c[1], $c[0] ) ); + + $argLists[] = array( $c, $c[1], 0, array( $c[1], $c[0], $c[2], $c[3], $c[4], $c[5] ) ); + $argLists[] = array( $c, $c[1], 5, array( $c[2], $c[3], $c[4], $c[0], $c[1], $c[5] ) ); + + $argLists[] = array( $c, $c[2], 0, array( $c[2], $c[3], $c[4], $c[0], $c[1], $c[5] ) ); + $argLists[] = array( $c, $c[2], 4, array( $c[0], $c[1], $c[3], $c[2], $c[4], $c[5] ) ); + $argLists[] = array( $c, $c[2], 5, array( $c[0], $c[1], $c[3], $c[4], $c[2], $c[5] ) ); + $argLists[] = array( $c, $c[2], 6, array( $c[0], $c[1], $c[5], $c[3], $c[4], $c[2] ) ); + + $argLists[] = array( $c, $c[3], 0, array( $c[3], $c[2], $c[4], $c[0], $c[1], $c[5] ) ); + $argLists[] = array( $c, $c[3], 1, array( $c[0], $c[1], $c[3], $c[2], $c[4], $c[5] ) ); + $argLists[] = array( $c, $c[3], 2, array( $c[0], $c[1], $c[3], $c[2], $c[4], $c[5] ) ); + + $argLists[] = array( $c, $c[4], 0, array( $c[4], $c[2], $c[3], $c[0], $c[1], $c[5] ) ); + $argLists[] = array( $c, $c[4], 2, array( $c[0], $c[1], $c[4], $c[2], $c[3], $c[5] ) ); + + $argLists[] = array( $c, $c[5], 0, array( $c[5], $c[0], $c[1], $c[2], $c[3], $c[4] ) ); + $argLists[] = array( $c, $c[5], 1, array( $c[0], $c[1], $c[5], $c[2], $c[3], $c[4] ) ); + $argLists[] = array( $c, $c[5], 2, array( $c[0], $c[1], $c[5], $c[2], $c[3], $c[4] ) ); + + return $argLists; + } + + /** + * @dataProvider moveProvider + * @param array $objectsSource + * @param object $object + * @param int $toIndex + * @param array $objectsDestination + */ + public function testMove( + array $objectsSource, + $object, + $toIndex, + array $objectsDestination + ) { + $indexedArray = new ByPropertyIdArray( $objectsSource ); + $indexedArray->buildIndex(); + + $indexedArray->move( $object, $toIndex ); + + // Not using $indexedArray->toArray() here to test whether native array has been exchanged: + $reindexedArray = array(); + foreach( $indexedArray as $o ) { + $reindexedArray[] = $o; + } + + $this->assertEquals( $objectsDestination, $reindexedArray ); + } + + public function testMoveThrowingOutOfBoundsExceptionIfObjectNotPresent() { + $claims = $this->claimsProvider(); + $indexedArray = new ByPropertyIdArray( $claims ); + $indexedArray->buildIndex(); + + $this->setExpectedException( 'OutOfBoundsException' ); + + $indexedArray->move( new Claim( new PropertyNoValueSnak( new PropertyId( 'P9999' ) ) ), 0 ); + } + + public function testMoveThrowingOutOfBoundsExceptionOnInvalidIndex() { + $claims = $this->claimsProvider(); + $indexedArray = new ByPropertyIdArray( $claims ); + $indexedArray->buildIndex(); + + $this->setExpectedException( 'OutOfBoundsException' ); + + $indexedArray->move( $claims[0], 9999 ); + } + + public function addProvider() { + $c = $this->claimsProvider(); + + $argLists = array(); + + $argLists[] = array( array(), $c[0], null, array( $c[0] ) ); + $argLists[] = array( array(), $c[0], 1, array( $c[0] ) ); + $argLists[] = array( array( $c[0] ), $c[2], 0, array( $c[2], $c[0] ) ); + $argLists[] = array( array( $c[2], $c[1] ), $c[0], 0, array( $c[0], $c[1], $c[2] ) ); + $argLists[] = array( + array( $c[0], $c[1], $c[3] ), + $c[5], + 1, + array( $c[0], $c[1], $c[5], $c[3] ) + ); + $argLists[] = array( + array( $c[0], $c[1], $c[5], $c[3] ), + $c[2], + 2, + array( $c[0], $c[1], $c[2], $c[3], $c[5] ) + ); + $argLists[] = array( + array( $c[0], $c[1], $c[2], $c[3], $c[5] ), + $c[4], + null, + array( $c[0], $c[1], $c[2], $c[3], $c[4], $c[5] ) + ); + + return $argLists; + } + + /** + * @dataProvider addProvider + * @param array $objectsSource + * @param object $object + * @param int $index + * @param array $objectsDestination + */ + public function testAdd( + array $objectsSource, + $object, + $index, + array $objectsDestination + ) { + $indexedArray = new ByPropertyIdArray( $objectsSource ); + $indexedArray->buildIndex(); + + $indexedArray->add( $object, $index ); + + $this->assertEquals( $objectsDestination, $indexedArray->toArray() ); + } + } -- To view, visit https://gerrit.wikimedia.org/r/91179 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I771507cea8e38c117af9d27ed4af7b573bc1e897 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/WikibaseDataModel Gerrit-Branch: master Gerrit-Owner: Henning Snater <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
