Skip to content

Commit 7ca9951

Browse files
committed
Add StatementListChanger
1 parent 28508dc commit 7ca9951

File tree

3 files changed

+443
-1
lines changed

3 files changed

+443
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"wikimedia/assert": "~0.2.2"
2828
},
2929
"require-dev": {
30+
"phpunit/phpunit": "~4.8",
3031
"mediawiki/mediawiki-codesniffer": "~0.7",
3132
"phpmd/phpmd": "~2.3"
3233
},
@@ -57,7 +58,7 @@
5758
"vendor/bin/phpcs src/* tests/* --standard=phpcs.xml --extensions=php -sp"
5859
],
5960
"phpmd": [
60-
"vendor/bin/phpmd src/,tests/unit/ text phpmd.xml"
61+
"vendor/bin/phpmd src/ text phpmd.xml"
6162
]
6263
}
6364
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace Wikibase\DataModel\Services\Statement;
4+
5+
use Wikibase\DataModel\Entity\PropertyId;
6+
use Wikibase\DataModel\Statement\Statement;
7+
use Wikibase\DataModel\Statement\StatementList;
8+
9+
/**
10+
* A collection of higher level utility functions to manipulate StatementList objects.
11+
*
12+
* @since 3.7
13+
*
14+
* @license GPL-2.0+
15+
* @author Thiemo Mättig
16+
*/
17+
class StatementListChanger {
18+
19+
/**
20+
* @var StatementList
21+
*/
22+
private $statementList;
23+
24+
public function __construct( StatementList $statementList ) {
25+
$this->statementList = $statementList;
26+
}
27+
28+
/**
29+
* Makes sure all statements with the same property are next to each other (forming a group),
30+
* and reorders them if necessary. The position of the group in the list is determined by the
31+
* first statement with the same property.
32+
*/
33+
public function groupByProperty() {
34+
$byId = [];
35+
36+
foreach ( $this->statementList->toArray() as $statement ) {
37+
$id = $statement->getPropertyId()->getSerialization();
38+
$byId[$id][] = $statement;
39+
}
40+
41+
// FIXME: Use StatementList::clear, see https://github.com/wmde/WikibaseDataModel/pull/649
42+
$this->clear();
43+
44+
foreach ( $byId as $statements ) {
45+
foreach ( $statements as $statement ) {
46+
$this->statementList->addStatement( $statement );
47+
}
48+
}
49+
}
50+
51+
private function clear() {
52+
foreach ( $this->statementList->toArray() as $statement ) {
53+
$this->statementList->removeStatementsWithGuid( $statement->getGuid() );
54+
}
55+
}
56+
57+
/**
58+
* Adds a new statement to the list while respecting existing PropertyId groups.
59+
*
60+
* In contrast to the previously used ByPropertyIdArray implementation this method never moves
61+
* existing statements around. The provided index is only a hint. The new statement is either
62+
* added to an existing group of statements with the same PropertyId, or at a group border close
63+
* to the provided index.
64+
*
65+
* @param Statement $newStatement
66+
* @param int|null $index An absolute index in the list. If the index is not next to a statement
67+
* with the same property, the closest possible position is used instead. Default is null,
68+
* which adds the new statement after the last statement with the same property, or to the end.
69+
*/
70+
public function addToGroup( Statement $newStatement, $index = null ) {
71+
$statements = $this->statementList->toArray();
72+
$id = $newStatement->getPropertyId();
73+
74+
if ( $index === null ) {
75+
$index = $this->getLastIndexWithinGroup( $statements, $id );
76+
} else {
77+
// Limit search range to avoid looping non-existing positions
78+
$validIndex = min( max( 0, $index ), count( $statements ) );
79+
$index = $this->getClosestIndexWithinGroup( $statements, $id, $validIndex );
80+
if ( $index === null ) {
81+
$index = $this->getClosestIndexAtGroupBorder( $statements, $validIndex );
82+
}
83+
}
84+
85+
$this->statementList->addStatement( $newStatement, $index );
86+
}
87+
88+
/**
89+
* @param Statement[] $statements
90+
* @param PropertyId $id
91+
*
92+
* @return int|null
93+
*/
94+
private function getLastIndexWithinGroup( array $statements, PropertyId $id ) {
95+
// Start searching from the end and stop at the first match
96+
for ( $i = count( $statements ); $i > 0; $i-- ) {
97+
if ( $statements[$i - 1]->getPropertyId()->equals( $id ) ) {
98+
return $i;
99+
}
100+
}
101+
102+
return null;
103+
}
104+
105+
/**
106+
* @param Statement[] $statements
107+
* @param PropertyId $id
108+
* @param int $index
109+
*
110+
* @return int|null
111+
*/
112+
private function getClosestIndexWithinGroup( array $statements, PropertyId $id, $index ) {
113+
$longestDistance = max( $index, count( $statements ) - $index );
114+
115+
for ( $i = 0; $i <= $longestDistance; $i++ ) {
116+
if ( $this->isWithinGroup( $statements, $id, $index - $i ) ) {
117+
return $index - $i;
118+
} elseif ( $i && $this->isWithinGroup( $statements, $id, $index + $i ) ) {
119+
return $index + $i;
120+
}
121+
}
122+
123+
return null;
124+
}
125+
126+
/**
127+
* @param Statement[] $statements
128+
* @param int $index
129+
*
130+
* @return int|null
131+
*/
132+
private function getClosestIndexAtGroupBorder( array $statements, $index ) {
133+
$longestDistance = max( $index, count( $statements ) - $index );
134+
135+
for ( $i = 0; $i <= $longestDistance; $i++ ) {
136+
if ( $this->isGroupBorder( $statements, $index - $i ) ) {
137+
return $index - $i;
138+
} elseif ( $i && $this->isGroupBorder( $statements, $index + $i ) ) {
139+
return $index + $i;
140+
}
141+
}
142+
143+
return null;
144+
}
145+
146+
/**
147+
* @param Statement[] $statements
148+
* @param PropertyId $id
149+
* @param int $index
150+
*
151+
* @return bool
152+
*/
153+
private function isWithinGroup( array $statements, PropertyId $id, $index ) {
154+
$count = count( $statements );
155+
156+
// Valid if the index either precedes or succeeds a statement with the same property
157+
return $index > 0 && $index <= $count && $statements[$index - 1]->getPropertyId()->equals( $id )
158+
|| $index >= 0 && $index < $count && $statements[$index]->getPropertyId()->equals( $id );
159+
}
160+
161+
/**
162+
* @param Statement[] $statements
163+
* @param int $index
164+
*
165+
* @return bool
166+
*/
167+
private function isGroupBorder( array $statements, $index ) {
168+
// First and last possible position is always a border
169+
return $index <= 0
170+
|| $index >= count( $statements )
171+
|| !$statements[$index - 1]->getPropertyId()->equals( $statements[$index]->getPropertyId() );
172+
}
173+
174+
}

0 commit comments

Comments
 (0)