From f069c227194e649ffa9a2d0f0023b8445a865606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiemo=20M=C3=A4ttig?= Date: Mon, 7 Nov 2016 16:50:57 +0100 Subject: [PATCH] Add StatementListChanger --- composer.json | 2 +- src/Statement/StatementListChanger.php | 169 +++++++++++ .../Statement/StatementListChangerTest.php | 282 ++++++++++++++++++ 3 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 src/Statement/StatementListChanger.php create mode 100644 tests/unit/Statement/StatementListChangerTest.php diff --git a/composer.json b/composer.json index 311514e2..edfb0c43 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "vendor/bin/phpcs src/* tests/* --standard=phpcs.xml --extensions=php -sp" ], "phpmd": [ - "vendor/bin/phpmd src/,tests/unit/ text phpmd.xml" + "vendor/bin/phpmd src/ text phpmd.xml" ] } } diff --git a/src/Statement/StatementListChanger.php b/src/Statement/StatementListChanger.php new file mode 100644 index 00000000..9117018f --- /dev/null +++ b/src/Statement/StatementListChanger.php @@ -0,0 +1,169 @@ +statementList = $statementList; + } + + /** + * Removes all statements from the $statementList object given in the constructor. + */ + public function clear() { + foreach ( $this->statementList->toArray() as $statement ) { + $this->statementList->removeStatementsWithGuid( $statement->getGuid() ); + } + } + + /** + * Makes sure all statements with the same property are next to each other (forming a group), + * and reorders them if necessary. The position of the group in the list is determined by the + * first statement with the same property. + */ + public function groupByProperty() { + $byId = []; + + foreach ( $this->statementList->toArray() as $statement ) { + $id = $statement->getPropertyId()->getSerialization(); + $byId[$id][] = $statement; + } + + $this->clear(); + + foreach ( $byId as $statements ) { + foreach ( $statements as $statement ) { + $this->statementList->addStatement( $statement ); + } + } + } + + /** + * @param Statement $newStatement + * @param int|null $index An absolute index in the list. If the index is not next to a statement + * with the same property, the closest possible position is used instead. Default is null, + * which adds the new statement after the last statement with the same property, or to the end. + */ + public function addToGroup( Statement $newStatement, $index = null ) { + $statements = $this->statementList->toArray(); + $id = $newStatement->getPropertyId(); + + if ( $index === null ) { + $index = $this->getLastIndexWithinGroup( $statements, $id ); + } else { + // Limit search range to avoid looping non-existing positions + $validIndex = min( max( 0, $index ), count( $statements ) ); + $index = $this->getClosestIndexWithinGroup( $statements, $id, $validIndex ); + if ( $index === null ) { + $index = $this->getClosestIndexAtGroupBorder( $statements, $validIndex ); + } + } + + $this->statementList->addStatement( $newStatement, $index ); + } + + /** + * @param Statement[] $statements + * @param PropertyId $id + * + * @return int|null + */ + private function getLastIndexWithinGroup( array $statements, PropertyId $id ) { + // Start searching from the end and stop at the first match + for ( $i = count( $statements ); $i > 0; $i-- ) { + if ( $statements[$i - 1]->getPropertyId()->equals( $id ) ) { + return $i; + } + } + + return null; + } + + /** + * @param Statement[] $statements + * @param PropertyId $id + * @param int $index + * + * @return int|null + */ + private function getClosestIndexWithinGroup( array $statements, PropertyId $id, $index ) { + $longestDistance = max( $index, count( $statements ) - $index ); + + for ( $i = 0; $i <= $longestDistance; $i++ ) { + if ( $this->isWithinGroup( $statements, $id, $index - $i ) ) { + return $index - $i; + } elseif ( $i && $this->isWithinGroup( $statements, $id, $index + $i ) ) { + return $index + $i; + } + } + + return null; + } + + /** + * @param Statement[] $statements + * @param int $index + * + * @return int|null + */ + private function getClosestIndexAtGroupBorder( array $statements, $index ) { + $longestDistance = max( $index, count( $statements ) - $index ); + + for ( $i = 0; $i <= $longestDistance; $i++ ) { + if ( $this->isGroupBorder( $statements, $index - $i ) ) { + return $index - $i; + } elseif ( $i && $this->isGroupBorder( $statements, $index + $i ) ) { + return $index + $i; + } + } + + return null; + } + + /** + * @param Statement[] $statements + * @param PropertyId $id + * @param int $index + * + * @return bool + */ + private function isWithinGroup( array $statements, PropertyId $id, $index ) { + $count = count( $statements ); + + // Valid if the index either prepends ot appends a statement with the same property + return $index > 0 && $index <= $count && $statements[$index - 1]->getPropertyId()->equals( $id ) + || $index >= 0 && $index < $count && $statements[$index]->getPropertyId()->equals( $id ); + } + + /** + * @param Statement[] $statements + * @param int $index + * + * @return bool + */ + private function isGroupBorder( array $statements, $index ) { + // First and last possible position is always a border + return $index <= 0 + || $index >= count( $statements ) + || !$statements[$index - 1]->getPropertyId()->equals( $statements[$index]->getPropertyId() ); + } + +} diff --git a/tests/unit/Statement/StatementListChangerTest.php b/tests/unit/Statement/StatementListChangerTest.php new file mode 100644 index 00000000..9b297035 --- /dev/null +++ b/tests/unit/Statement/StatementListChangerTest.php @@ -0,0 +1,282 @@ +assertInstanceOf( StatementListChanger::class, $instance ); + } + + public function testClear() { + $statementList = $this->newStatementList( [ 'P1$a' ] ); + + $instance = new StatementListChanger( $statementList ); + $instance->clear(); + + $this->assertTrue( $statementList->isEmpty() ); + } + + public function groupByPropertyIdProvider() { + return [ + [ + [], + [] + ], + [ + [ 'P1$a' ], + [ 'P1$a' ] + ], + [ + [ 'P1$a', 'P2$b', 'P1$c', 'P2$d' ], + [ 'P1$a', 'P1$c', 'P2$b', 'P2$d' ] + ], + [ + [ 'P1$a', 'P1$b', 'P2$c', 'P3$d', 'P1$e', 'P2$f' ], + [ 'P1$a', 'P1$b', 'P1$e', 'P2$c', 'P2$f', 'P3$d' ] + ], + ]; + } + + /** + * @dataProvider groupByPropertyIdProvider + */ + public function testGroupByPropertyId( array $guids, array $expectedGuids ) { + $statementList = $this->newStatementList( $guids ); + + $instance = new StatementListChanger( $statementList ); + $instance->groupByProperty(); + + $this->assertGuids( $expectedGuids, $statementList ); + } + + public function addToGroupProvider() { + return [ + 'add to empty list' => [ + [], + 'P1$new', + [ 'P1$new' ] + ], + 'append' => [ + [ 'P1$a' ], + 'P2$new', + [ 'P1$a', 'P2$new' ] + ], + 'insert' => [ + [ 'P1$a', 'P2$b' ], + 'P1$new', + [ 'P1$a', 'P1$new', 'P2$b' ] + ], + 'prefer last group when not ordered' => [ + [ 'P1$a', 'P2$b', 'P1$c', 'P2$d' ], + 'P1$new', + [ 'P1$a', 'P2$b', 'P1$c', 'P1$new', 'P2$d' ] + ], + ]; + } + + /** + * @dataProvider addToGroupProvider + */ + public function testAddToGroup( array $guids, $newGuid, array $expectedGuids ) { + $statementList = $this->newStatementList( $guids ); + $statement = $this->newStatement( $newGuid ); + + $instance = new StatementListChanger( $statementList ); + $instance->addToGroup( $statement ); + + $this->assertGuids( $expectedGuids, $statementList ); + } + + public function addToGroupByIndexProvider() { + return [ + // Add to an empty list + 'add to empty list with exact index' => [ + [], + 'P1$new', + 0, + [ 'P1$new' ] + ], + 'add to empty list with extreme index' => [ + [], + 'P1$new', + 100, + [ 'P1$new' ] + ], + 'add to empty list with negative index' => [ + [], + 'P1$new', + -100, + [ 'P1$new' ] + ], + + // Add the second statement with the same property + 'append with exact index' => [ + [ 'P1$a' ], + 'P1$new', + 1, + [ 'P1$a', 'P1$new' ] + ], + 'prepend with exact index' => [ + [ 'P1$a' ], + 'P1$new', + 0, + [ 'P1$new', 'P1$a' ] + ], + + // Add to a list with multiple properties + 'insert with exact index' => [ + [ 'P1$a', 'P2$b' ], + 'P1$new', + 1, + [ 'P1$a', 'P1$new', 'P2$b' ] + ], + 'insert with extreme index' => [ + [ 'P1$a', 'P2$b' ], + 'P1$new', + 100, + [ 'P1$a', 'P1$new', 'P2$b' ] + ], + 'prepend with negative index' => [ + [ 'P1$a', 'P2$b' ], + 'P1$new', + -100, + [ 'P1$new', 'P1$a', 'P2$b' ] + ], + + // Add to a list that has multiple groups with the same property + 'decrease index to closest match' => [ + [ 'P1$a', 'P2$b', 'P2$c', 'P2$d', 'P1$e' ], + 'P1$new', + 2, + [ 'P1$a', 'P1$new', 'P2$b', 'P2$c', 'P2$d', 'P1$e' ], + ], + 'increase index to closest match' => [ + [ 'P1$a', 'P2$b', 'P2$c', 'P2$d', 'P1$e' ], + 'P1$new', + 3, + [ 'P1$a', 'P2$b', 'P2$c', 'P2$d', 'P1$new', 'P1$e' ], + ], + 'prefer decreasing when no closer match' => [ + [ 'P1$a', 'P2$b', 'P2$c', 'P1$d' ], + 'P1$new', + 2, + [ 'P1$a', 'P1$new', 'P2$b', 'P2$c', 'P1$d' ], + ], + + // Add a new property to a list that has internal group borders + 'decrease index to closest group border' => [ + [ 'P1$a', 'P2$b', 'P2$c', 'P2$d', 'P1$e' ], + 'P3$new', + 2, + [ 'P1$a', 'P3$new', 'P2$b', 'P2$c', 'P2$d', 'P1$e' ], + ], + 'increase index to closest group border' => [ + [ 'P1$a', 'P2$b', 'P2$c', 'P2$d', 'P1$e' ], + 'P3$new', + 3, + [ 'P1$a', 'P2$b', 'P2$c', 'P2$d', 'P3$new', 'P1$e' ], + ], + 'prefer decreasing when no closer group border' => [ + [ 'P1$a', 'P2$b', 'P2$c', 'P1$d' ], + 'P3$new', + 2, + [ 'P1$a', 'P3$new', 'P2$b', 'P2$c', 'P1$d' ], + ], + + // Add a new property to a list that has no internal group borders + 'decrease index to closest list limit' => [ + [ 'P1$a', 'P1$b', 'P1$c' ], + 'P2$new', + 1, + [ 'P2$new', 'P1$a', 'P1$b', 'P1$c' ], + ], + 'increase index to closest list limit' => [ + [ 'P1$a', 'P1$b', 'P1$c' ], + 'P2$new', + 2, + [ 'P1$a', 'P1$b', 'P1$c', 'P2$new' ], + ], + 'prefer decreasing when no closer list limit' => [ + [ 'P1$a', 'P1$b' ], + 'P2$new', + 1, + [ 'P2$new', 'P1$a', 'P1$b' ], + ], + ]; + } + + /** + * @dataProvider addToGroupByIndexProvider + */ + public function testAddToGroupByIndex( array $guids, $newGuid, $index, array $expectedGuids ) { + $statementList = $this->newStatementList( $guids ); + $statement = $this->newStatement( $newGuid ); + + $instance = new StatementListChanger( $statementList ); + $instance->addToGroup( $statement, $index ); + + $this->assertGuids( $expectedGuids, $statementList ); + } + + /** + * @param string[] $guids + * + * @return StatementList + */ + private function newStatementList( array $guids ) { + $statementList = new StatementList(); + + foreach ( $guids as $guid ) { + $statementList->addStatement( $this->newStatement( $guid ) ); + } + + return $statementList; + } + + /** + * @param string $guid + * + * @return Statement + */ + private function newStatement( $guid ) { + list( $propertyId, ) = explode( '$', $guid, 2 ); + + return new Statement( + new PropertyNoValueSnak( new PropertyId( $propertyId ) ), + null, + null, + $guid + ); + } + + /** + * @param string[] $expectedGuids + * @param StatementList $statementList + */ + private function assertGuids( array $expectedGuids, StatementList $statementList ) { + $guids = []; + + foreach ( $statementList->toArray() as $statement ) { + $guids[] = $statement->getGuid(); + } + + $this->assertSame( $expectedGuids, $guids ); + } + +}