Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.55% covered (success)
98.55%
68 / 69
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DiffWithinRangeChecker
98.55% covered (success)
98.55%
68 / 69
80.00% covered (warning)
80.00%
4 / 5
28
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSupportedContextTypes
n/a
0 / 0
n/a
0 / 0
1
 getDefaultContextTypes
n/a
0 / 0
n/a
0 / 0
1
 getSupportedEntityTypes
n/a
0 / 0
n/a
0 / 0
1
 parseConstraintParameters
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 rangeInYears
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 checkConstraint
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
15
 checkConstraintParameters
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
4
5use DataValues\QuantityValue;
6use MediaWiki\Config\Config;
7use Wikibase\DataModel\Entity\NumericPropertyId;
8use Wikibase\DataModel\Snak\PropertyValueSnak;
9use Wikibase\DataModel\Statement\Statement;
10use WikibaseQuality\ConstraintReport\Constraint;
11use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
12use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
13use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException;
14use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
15use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\RangeCheckerHelper;
16use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
17use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
18use WikibaseQuality\ConstraintReport\Role;
19
20/**
21 * @author BP2014N1
22 * @license GPL-2.0-or-later
23 */
24class DiffWithinRangeChecker implements ConstraintChecker {
25
26    /**
27     * @var ConstraintParameterParser
28     */
29    private $constraintParameterParser;
30
31    /**
32     * @var RangeCheckerHelper
33     */
34    private $rangeCheckerHelper;
35
36    /**
37     * @var Config
38     */
39    private $config;
40
41    public function __construct(
42        ConstraintParameterParser $constraintParameterParser,
43        RangeCheckerHelper $rangeCheckerHelper,
44        Config $config
45    ) {
46        $this->constraintParameterParser = $constraintParameterParser;
47        $this->rangeCheckerHelper = $rangeCheckerHelper;
48        $this->config = $config;
49    }
50
51    /**
52     * @codeCoverageIgnore This method is purely declarative.
53     */
54    public function getSupportedContextTypes() {
55        return self::ALL_CONTEXT_TYPES_SUPPORTED;
56    }
57
58    /**
59     * @codeCoverageIgnore This method is purely declarative.
60     */
61    public function getDefaultContextTypes() {
62        return Context::ALL_CONTEXT_TYPES;
63    }
64
65    /** @codeCoverageIgnore This method is purely declarative. */
66    public function getSupportedEntityTypes() {
67        return self::ALL_ENTITY_TYPES_SUPPORTED;
68    }
69
70    /**
71     * @param Constraint $constraint
72     *
73     * @throws ConstraintParameterException
74     * @return array [ DataValue|null $min, DataValue|null $max, NumericPropertyId $property ]
75     */
76    private function parseConstraintParameters( Constraint $constraint ) {
77        [ $min, $max ] = $this->constraintParameterParser->parseQuantityRangeParameter(
78            $constraint->getConstraintParameters(),
79            $constraint->getConstraintTypeItemId()
80        );
81        $property = $this->constraintParameterParser->parsePropertyParameter(
82            $constraint->getConstraintParameters(),
83            $constraint->getConstraintTypeItemId()
84        );
85
86        return [ $min, $max, $property ];
87    }
88
89    /**
90     * Check whether the endpoints of a range are in years or not.
91     * @param QuantityValue|null $min
92     * @param QuantityValue|null $max
93     *
94     * @return bool
95     */
96    private function rangeInYears( $min, $max ) {
97        $yearUnit = $this->config->get( 'WBQualityConstraintsYearUnit' );
98
99        if ( $min !== null && $min->getUnit() === $yearUnit ) {
100            return true;
101        }
102        if ( $max !== null && $max->getUnit() === $yearUnit ) {
103            return true;
104        }
105
106        return false;
107    }
108
109    /**
110     * Checks 'Diff within range' constraint.
111     *
112     * @param Context $context
113     * @param Constraint $constraint
114     *
115     * @throws ConstraintParameterException
116     * @return CheckResult
117     */
118    public function checkConstraint( Context $context, Constraint $constraint ) {
119        if ( $context->getSnakRank() === Statement::RANK_DEPRECATED ) {
120            return new CheckResult( $context, $constraint, CheckResult::STATUS_DEPRECATED );
121        }
122
123        $snak = $context->getSnak();
124
125        if ( !$snak instanceof PropertyValueSnak ) {
126            // nothing to check
127            return new CheckResult( $context, $constraint, CheckResult::STATUS_COMPLIANCE );
128        }
129
130        $minuend = $snak->getDataValue();
131        '@phan-var \DataValues\TimeValue|\DataValues\QuantityValue|\DataValues\UnboundedQuantityValue $minuend';
132
133        /** @var NumericPropertyId $property */
134        [ $min, $max, $property ] = $this->parseConstraintParameters( $constraint );
135
136        // checks only the first occurrence of the referenced property
137        foreach ( $context->getSnakGroup( Context::GROUP_NON_DEPRECATED ) as $otherSnak ) {
138            if (
139                !$property->equals( $otherSnak->getPropertyId() ) ||
140                !$otherSnak instanceof PropertyValueSnak
141            ) {
142                continue;
143            }
144
145            $subtrahend = $otherSnak->getDataValue();
146            '@phan-var \DataValues\TimeValue|\DataValues\QuantityValue|\DataValues\UnboundedQuantityValue $subtrahend';
147            if ( $subtrahend->getType() === $minuend->getType() ) {
148                $diff = $this->rangeInYears( $min, $max ) && $minuend->getType() === 'time' ?
149                    $this->rangeCheckerHelper->getDifferenceInYears( $minuend, $subtrahend ) :
150                    $this->rangeCheckerHelper->getDifference( $minuend, $subtrahend );
151
152                if ( $this->rangeCheckerHelper->getComparison( $min, $diff ) > 0 ||
153                    $this->rangeCheckerHelper->getComparison( $diff, $max ) > 0
154                ) {
155                    // at least one of $min, $max is set at this point, otherwise there could be no violation
156                    $openness = $min !== null ? ( $max !== null ? '' : '-rightopen' ) : '-leftopen';
157                    // possible message keys:
158                    // wbqc-violation-message-diff-within-range
159                    // wbqc-violation-message-diff-within-range-leftopen
160                    // wbqc-violation-message-diff-within-range-rightopen
161                    $message = ( new ViolationMessage( "wbqc-violation-message-diff-within-range$openness" ) )
162                        ->withEntityId( $context->getSnak()->getPropertyId(), Role::PREDICATE )
163                        ->withDataValue( $minuend, Role::OBJECT )
164                        ->withEntityId( $otherSnak->getPropertyId(), Role::PREDICATE )
165                        ->withDataValue( $subtrahend, Role::OBJECT );
166                    if ( $min !== null ) {
167                        $message = $message->withDataValue( $min, Role::OBJECT );
168                    }
169                    if ( $max !== null ) {
170                        $message = $message->withDataValue( $max, Role::OBJECT );
171                    }
172                    $status = CheckResult::STATUS_VIOLATION;
173                } else {
174                    $message = null;
175                    $status = CheckResult::STATUS_COMPLIANCE;
176                }
177            } else {
178                $message = new ViolationMessage( 'wbqc-violation-message-diff-within-range-must-have-equal-types' );
179                $status = CheckResult::STATUS_VIOLATION;
180            }
181
182            return new CheckResult( $context, $constraint, $status, $message );
183        }
184
185        return new CheckResult( $context, $constraint, CheckResult::STATUS_COMPLIANCE );
186    }
187
188    public function checkConstraintParameters( Constraint $constraint ) {
189        $constraintParameters = $constraint->getConstraintParameters();
190        $constraintTypeItemId = $constraint->getConstraintTypeItemId();
191        $exceptions = [];
192        try {
193            $this->constraintParameterParser->parseQuantityRangeParameter(
194                $constraintParameters,
195                $constraintTypeItemId
196            );
197        } catch ( ConstraintParameterException $e ) {
198            $exceptions[] = $e;
199        }
200        try {
201            $this->constraintParameterParser->parsePropertyParameter(
202                $constraintParameters,
203                $constraintTypeItemId
204            );
205        } catch ( ConstraintParameterException $e ) {
206            $exceptions[] = $e;
207        }
208        return $exceptions;
209    }
210
211}