Completed
Push — rm-hasharray ( fa3d03...016649 )
by no
16:27
created

SnakList::removeDuplicates()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A SnakList::offsetSet() 0 3 1
1
<?php
2
3
namespace Wikibase\DataModel\Snak;
4
5
use ArrayObject;
6
use Comparable;
7
use Hashable;
8
use InvalidArgumentException;
9
use Traversable;
10
use Wikibase\DataModel\Internal\MapValueHasher;
11
12
/**
13
 * List of Snak objects.
14
 * Indexes the snaks by hash and ensures no more the one snak with the same hash are in the list.
15
 *
16
 * @since 0.1
17
 *
18
 * @license GPL-2.0+
19
 * @author Jeroen De Dauw < [email protected] >
20
 * @author Addshore
21
 */
22
class SnakList extends ArrayObject implements Hashable, Comparable {
23
24
	/**
25
	 * Maps snak hashes to their offsets.
26
	 *
27
	 * @var array [ snak hash (string) => array [ snak offset (string|int) ] | snak offset (string|int) ]
28
	 */
29
	private $offsetHashes = [];
30
31
	/**
32
	 * @var integer
33
	 */
34
	private $indexOffset = 0;
35
36
	/**
37
	 * @see ArrayObject::__construct
38
	 *
39
	 * @param array|Traversable|null $input
40
	 * @param int $flags
41
	 * @param string $iteratorClass
42
	 *
43
	 * @throws InvalidArgumentException
44
	 */
45
	public function __construct( $input = null, $flags = 0, $iteratorClass = 'ArrayIterator' ) {
46
		parent::__construct( [], $flags, $iteratorClass );
47
48
		if ( $input !== null ) {
49
			if ( !is_array( $input ) && !( $input instanceof Traversable ) ) {
50
				throw new InvalidArgumentException( '$input must be an array or Traversable' );
51
			}
52
53
			foreach ( $input as $offset => $value ) {
54
				$this->offsetSet( $offset, $value );
55
			}
56
		}
57
	}
58
59
	/**
60
	 * Finds a new offset for when appending an element.
61
	 * The base class does this, so it would be better to integrate,
62
	 * but there does not appear to be any way to do this...
63
	 *
64
	 * @return integer
65
	 */
66
	private function getNewOffset() {
67
		while ( $this->offsetExists( $this->indexOffset ) ) {
68
			$this->indexOffset++;
69
		}
70
71
		return $this->indexOffset;
72
	}
73
74
	/**
75
	 * Gets called before a new element is added to the list.
76
	 *
77
	 * Should return a boolean. When false is returned the element
78
	 * does not get added to the ArrayObject.
79
	 *
80
	 * @since 0.1
81
	 *
82
	 * @param int|string $index
83
	 * @param Hashable $hashable
84
	 *
85
	 * @return bool
86
	 */
87
	private function preSetElement( $index, $hashable ) {
88
		$hash = $hashable->getHash();
89
90
		$hasHash = $this->hasSnakHash( $hash );
91
92
		if ( $hasHash ) {
93
			return false;
94
		}
95
96
		if ( $hasHash ) {
97
			if ( !is_array( $this->offsetHashes[$hash] ) ) {
98
				$this->offsetHashes[$hash] = [ $this->offsetHashes[$hash] ];
99
			}
100
101
			$this->offsetHashes[$hash][] = $index;
102
		}
103
		else {
104
			$this->offsetHashes[$hash] = $index;
105
		}
106
107
		return true;
108
	}
109
110
	/**
111
	 * @see ArrayObject::offsetUnset
112
	 *
113
	 * @since 0.1
114
	 *
115
	 * @param mixed $index
116
	 */
117
	public function offsetUnset( $index ) {
118
		if ( $this->offsetExists( $index ) ) {
119
			/**
120
			 * @var Hashable $element
121
			 */
122
			$element = $this->offsetGet( $index );
123
124
			$hash = $element->getHash();
125
126
			if ( array_key_exists( $hash, $this->offsetHashes )
127
				&& is_array( $this->offsetHashes[$hash] )
128
				&& count( $this->offsetHashes[$hash] ) > 1 ) {
129
				$this->offsetHashes[$hash] = array_filter(
130
					$this->offsetHashes[$hash],
131
					function( $value ) use ( $index ) {
132
						return $value !== $index;
133
					}
134
				);
135
			}
136
			else {
137
				unset( $this->offsetHashes[$hash] );
138
			}
139
140
			parent::offsetUnset( $index );
141
		}
142
	}
143
144
	/**
145
	 * @see Hashable::getHash
146
	 *
147
	 * The hash is purely valuer based. Order of the elements in the array is not held into account.
148
	 *
149
	 * @since 0.1
150
	 *
151
	 * @return string
152
	 */
153
	public function getHash() {
154
		$hasher = new MapValueHasher();
155
		return $hasher->hash( $this );
156
	}
157
158
	/**
159
	 * @see Comparable::equals
160
	 *
161
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
162
	 *
163
	 * @since 0.3
164
	 *
165
	 * @param mixed $target
166
	 *
167
	 * @return bool
168
	 */
169
	public function equals( $target ) {
170
		if ( $this === $target ) {
171
			return true;
172
		}
173
174
		return $target instanceof self
175
			&& $this->getHash() === $target->getHash();
176
	}
177
178
	/**
179
	 * @see ArrayObject::append
180
	 *
181
	 * @param mixed $value
182
	 */
183
	public function append( $value ) {
184
		$this->setElement( null, $value );
185
	}
186
187
	/**
188
	 * @see ArrayObject::offsetSet()
189
	 *
190
	 * @param mixed $index
191
	 * @param mixed $value
192
	 */
193
	public function offsetSet( $index, $value ) {
194
		$this->setElement( $index, $value );
195
	}
196
197
	/**
198
	 * Method that actually sets the element and holds
199
	 * all common code needed for set operations, including
200
	 * type checking and offset resolving.
201
	 *
202
	 * If you want to do additional indexing or have code that
203
	 * otherwise needs to be executed whenever an element is added,
204
	 * you can overload @see preSetElement.
205
	 *
206
	 * @param mixed $index
207
	 * @param mixed $value
208
	 *
209
	 * @throws InvalidArgumentException
210
	 */
211
	private function setElement( $index, $value ) {
212
		if ( ! $value instanceof Snak ) {
213
			$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
214
215
			throw new InvalidArgumentException( '$value must be an instance of Snak; got ' . $type );
216
		}
217
218
		if ( $index === null ) {
219
			$index = $this->getNewOffset();
220
		}
221
222
		if ( $this->preSetElement( $index, $value ) ) {
223
			parent::offsetSet( $index, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of setElement()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
224
		}
225
	}
226
227
	/**
228
	 * @see Serializable::serialize
229
	 *
230
	 * @return string
231
	 */
232
	public function serialize() {
233
		return serialize( [
234
			'data' => $this->getArrayCopy(),
235
			'index' => $this->indexOffset,
236
		] );
237
	}
238
239
	/**
240
	 * @see Serializable::unserialize
241
	 *
242
	 * @param string $serialized
243
	 */
244
	public function unserialize( $serialized ) {
245
		$serializationData = unserialize( $serialized );
246
247
		foreach ( $serializationData['data'] as $offset => $value ) {
248
			// Just set the element, bypassing checks and offset resolving,
249
			// as these elements have already gone through this.
250
			parent::offsetSet( $offset, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of unserialize()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
251
		}
252
253
		$this->indexOffset = $serializationData['index'];
254
	}
255
256
	/**
257
	 * Returns if the ArrayObject has no elements.
258
	 *
259
	 * @return bool
260
	 */
261
	public function isEmpty() {
262
		return !$this->getIterator()->valid();
263
	}
264
265
	/**
266
	 * @since 0.1
267
	 *
268
	 * @param string $snakHash
269
	 *
270
	 * @return boolean
271
	 */
272
	public function hasSnakHash( $snakHash ) {
273
		return array_key_exists( $snakHash, $this->offsetHashes );
274
	}
275
276
	/**
277
	 * @since 0.1
278
	 *
279
	 * @param string $snakHash
280
	 */
281
	public function removeSnakHash( $snakHash ) {
282
		if ( $this->hasSnakHash( $snakHash ) ) {
283
			$offset = $this->offsetHashes[$snakHash];
284
285
			if ( is_array( $offset ) ) {
286
				$offset = reset( $offset );
287
			}
288
289
			$this->offsetUnset( $offset );
290
		}
291
	}
292
293
	/**
294
	 * @since 0.1
295
	 *
296
	 * @param Snak $snak
297
	 *
298
	 * @return boolean Indicates if the snak was added or not.
299
	 */
300
	public function addSnak( Snak $snak ) {
301
		if ( $this->hasSnak( $snak ) ) {
302
			return false;
303
		}
304
305
		$this->append( $snak );
306
		return true;
307
	}
308
309
	/**
310
	 * @since 0.1
311
	 *
312
	 * @param Snak $snak
313
	 *
314
	 * @return boolean
315
	 */
316
	public function hasSnak( Snak $snak ) {
317
		return $this->hasSnakHash( $snak->getHash() );
318
	}
319
320
	/**
321
	 * @since 0.1
322
	 *
323
	 * @param Snak $snak
324
	 */
325
	public function removeSnak( Snak $snak ) {
326
		$this->removeSnakHash( $snak->getHash() );
327
	}
328
329
	/**
330
	 * @since 0.1
331
	 *
332
	 * @param string $snakHash
333
	 *
334
	 * @return Snak|bool
335
	 */
336
	public function getSnak( $snakHash ) {
337
		if ( !$this->hasSnakHash( $snakHash ) ) {
338
			return false;
339
		}
340
341
		$offset = $this->offsetHashes[$snakHash];
342
343
		if ( is_array( $offset ) ) {
344
			$offset = reset( $offset );
345
		}
346
347
		return $this->offsetGet( $offset );
348
	}
349
350
	/**
351
	 * Orders the snaks in the list grouping them by property.
352
	 *
353
	 * @param string[] $order List of serliazed property ids to order by.
354
	 *
355
	 * @since 0.5
356
	 */
357
	public function orderByProperty( array $order = [] ) {
358
		$snaksByProperty = $this->getSnaksByProperty();
359
		$orderedProperties = array_unique( array_merge( $order, array_keys( $snaksByProperty ) ) );
360
361
		foreach ( $orderedProperties as $property ) {
362
			if ( array_key_exists( $property, $snaksByProperty ) ) {
363
				$snaks = $snaksByProperty[$property];
364
				$this->moveSnaksToBottom( $snaks );
365
			}
366
		}
367
	}
368
369
	/**
370
	 * @param Snak[] $snaks to remove and re add
371
	 */
372
	private function moveSnaksToBottom( array $snaks ) {
373
		foreach ( $snaks as $snak ) {
374
			$this->removeSnak( $snak );
375
			$this->addSnak( $snak );
376
		}
377
	}
378
379
	/**
380
	 * Gets the snaks in the current object in an array
381
	 * grouped by property id
382
	 *
383
	 * @return array[]
384
	 */
385
	private function getSnaksByProperty() {
386
		$snaksByProperty = [];
387
388
		foreach ( $this as $snak ) {
389
			/** @var Snak $snak */
390
			$propertyId = $snak->getPropertyId()->getSerialization();
391
			if ( !isset( $snaksByProperty[$propertyId] ) ) {
392
				$snaksByProperty[$propertyId] = [];
393
			}
394
			$snaksByProperty[$propertyId][] = $snak;
395
		}
396
397
		return $snaksByProperty;
398
	}
399
400
}
401