Test Failed
Push — master ( c140b5...9f5388 )
by Jean
03:10
created

AndRule::removeInvalidBranches()   D

Complexity

Conditions 26
Paths 59

Size

Total Lines 94
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 52
nc 59
nop 1
dl 0
loc 94
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace JClaveau\LogicalFilter\Rule;
3
use       JClaveau\VisibilityViolator\VisibilityViolator;
4
5
/**
6
 * Logical conjunction:
7
 * @see https://en.wikipedia.org/wiki/Logical_conjunction
8
 */
9
class AndRule extends AbstractOperationRule
10
{
11
    /** @var string operator */
12
    const operator = 'and';
13
14
    /**
15
     * Replace all the OrRules of the RuleTree by one OrRule at its root.
16
     *
17
     * @todo rename as RootifyDisjunjctions?
18
     * @todo return $this (implements a Rule monad?)
19
     *
20
     * @return OrRule copied operands with one OR at its root
21
     */
22
    public function rootifyDisjunctions(array $simplification_options)
23
    {
24
        if (!$this->isNormalizationAllowed($simplification_options))
25
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type JClaveau\LogicalFilter\Rule\AndRule which is incompatible with the documented return type JClaveau\LogicalFilter\Rule\OrRule.
Loading history...
26
27
        $this->moveSimplificationStepForward( self::rootify_disjunctions, $simplification_options );
28
29
        $upLiftedOperands = [];
30
        foreach ($this->getOperands() as $operand) {
31
            $operand = $operand->copy();
32
            if ($operand instanceof AbstractOperationRule)
33
                $operand = $operand->rootifyDisjunctions($simplification_options);
0 ignored issues
show
Bug introduced by
The method rootifyDisjunctions() does not exist on JClaveau\LogicalFilter\Rule\AbstractOperationRule. Since it exists in all sub-types, consider adding an abstract or default implementation to JClaveau\LogicalFilter\Rule\AbstractOperationRule. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

33
                /** @scrutinizer ignore-call */ 
34
                $operand = $operand->rootifyDisjunctions($simplification_options);
Loading history...
34
35
            $upLiftedOperands[] = $operand;
36
        }
37
38
        // If the AndRule doesn't contain any OrRule , there is nothing to uplift
39
        if (!array_filter($upLiftedOperands, function($operand) {
40
            return $operand instanceof OrRule;
41
        })) {
42
            return new AndRule($upLiftedOperands);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new JClaveau\Logi...Rule($upLiftedOperands) returns the type JClaveau\LogicalFilter\Rule\AndRule which is incompatible with the documented return type JClaveau\LogicalFilter\Rule\OrRule.
Loading history...
43
        }
44
45
        $firstAndOperand = new AndRule();
46
47
        // This OrRule should contain only AndRules during its generation
48
        $upLiftedOr = new OrRule([
49
            $firstAndOperand
50
        ]);
51
52
        // var_dump($upLiftedOperands);
53
        // $this->dump(true);
54
55
        foreach ($upLiftedOperands as $i => $operand) {
56
57
            if ($operand instanceof NotRule) {
58
                if (    ($operand instanceof NotEqualRule || $operand instanceof NotInRule)
59
                    && ! $operand->isNormalizationAllowed($simplification_options)
60
                ) {
61
                    foreach ($upLiftedOr->getOperands() as $upLifdtedOperand) {
62
                        $upLifdtedOperand->addOperand( $operand->copy() );
63
                    }
64
                }
65
                else {
66
                    throw new \LogicException(
67
                        "Rootifying disjunctions MUST be done after negations removal instead of '".$operand."' \n"
68
                        .$operand
69
                    );
70
                }
71
            }
72
            elseif ($operand instanceof OrRule && $operand->isNormalizationAllowed($simplification_options)) {
73
74
                // If an operand is an Or, me transform the current
75
                // (A' || A") && (B')       <=> (A' && B') || (A" && B');
76
                // (A' || A") && (B' || B") <=> (A' && B') || (A' && B") || (A" && B') || (A" && B");
77
                // (A' || A") && (B' || B") && (C' || C") <=>
78
                //    (A' && B' && C') || (A' && B' && C") || (A' && B" && C') || (A' && B" && C")
79
                // || (A" && B' && C') || (A" && B' && C") || (A" && B" && C') || (A" && B" && C");
80
                $newUpLiftedOr = new OrRule;
81
                foreach ($operand->getOperands() as $subOperand) {
82
                    foreach ($upLiftedOr->getOperands() as $upLiftedOrSubOperand) {
83
                        $newUpLiftedOrSubOperand = $upLiftedOrSubOperand->copy();
84
                        $newUpLiftedOrSubOperand->addOperand( $subOperand->copy() );
85
                        if ($newUpLiftedOrSubOperand->simplify($simplification_options)->hasSolution($simplification_options))
86
                            $newUpLiftedOr->addOperand( $newUpLiftedOrSubOperand );
87
                    }
88
                }
89
90
                $upLiftedOr = $newUpLiftedOr;
91
            }
92
            else {
93
                // append the operand to all the operands of the $upLiftedOr
94
                foreach ($upLiftedOr->getOperands() as $upLifdtedOperand) {
95
                    if (!$upLifdtedOperand instanceof AndRule) {
96
                        throw new \LogicException(
97
                             "Operands of the uplifted OrRule MUST be AndRules during"
98
                            ."the combination."
99
                        );
100
                    }
101
102
                    $upLifdtedOperand->addOperand( $operand->copy() );
103
                }
104
            }
105
        }
106
107
        return $upLiftedOr;
108
    }
109
110
    /**
111
     * @param array $options   + show_instance=false Display the operator of the rule or its instance id
112
     *
113
     * @return array
114
     *
115
     * @todo same as OrRule
116
     */
117
    public function toArray(array $options=[])
118
    {
119
        $default_options = [
120
            'show_instance' => false,
121
            'sort_operands' => false,
122
            'semantic'      => false,
123
        ];
124
        foreach ($default_options as $default_option => &$default_value) {
125
            if (!isset($options[ $default_option ]))
126
                $options[ $default_option ] = $default_value;
127
        }
128
129
        if (!$options['show_instance'] && !empty($this->cache['array']))
130
            return $this->cache['array'];
131
132
        $operands_as_array = [
133
            $options['show_instance'] ? $this->getInstanceId() : self::operator,
134
        ];
135
136
        $operands = $this->operands;
137
        if ($options['semantic']) {
138
            // Semantic array: ['operator', 'semantic_id_of_operand1', 'semantic_id_of_operand2', ...]
139
            // with sorted semantic ids
140
            $operands_semantic_ids = array_keys($operands);
141
            sort($operands_semantic_ids);
142
            return array_merge(
143
                [self::operator],
144
                $operands_semantic_ids
145
            );
146
        }
147
        else {
148
            foreach ($operands as $operand)
149
                $operands_as_array[] = $operand->toArray($options);
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on JClaveau\LogicalFilter\Rule\AbstractRule. It seems like you code against a sub-type of said class. However, the method does not exist in JClaveau\LogicalFilter\Rule\AbstractOperationRule. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
                /** @scrutinizer ignore-call */ 
150
                $operands_as_array[] = $operand->toArray($options);
Loading history...
150
151
            if (!$options['show_instance'])
152
                return $this->cache['array'] = $operands_as_array;
153
            else
154
                return $operands_as_array;
155
        }
156
    }
157
158
    /**
159
     */
160
    public function toString(array $options=[])
161
    {
162
        $operator = self::operator;
163
        if (!$this->operands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->operands of type JClaveau\LogicalFilter\Rule\AbstractRule[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
164
            return $this->cache['string'] = "['{$operator}']";
165
        }
166
167
        $indent_unit = isset($options['indent_unit']) ? $options['indent_unit'] : '';
168
        $line_break  = $indent_unit ? "\n" : '';
169
170
        $out = "['{$operator}',$line_break";
171
172
        foreach ($this->operands as $operand) {
173
            $out .= implode("\n", array_map(function($line) use (&$indent_unit) {
174
                return $indent_unit.$line;
175
            }, explode("\n", $operand->toString($options)) )) . ",$line_break";
0 ignored issues
show
Bug introduced by
The method toString() does not exist on JClaveau\LogicalFilter\Rule\AbstractRule. Did you maybe mean __toString()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

175
            }, explode("\n", $operand->/** @scrutinizer ignore-call */ toString($options)) )) . ",$line_break";

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
176
        }
177
178
        $out .= ']';
179
180
        return $this->cache['string'] = $out;
181
    }
182
183
    /**
184
     * Remove AndRules operands of AndRules
185
     */
186
    public function removeSameOperationOperands()
187
    {
188
        foreach ($this->operands as $i => $operand) {
189
            if ( ! is_a($operand, AndRule::class))
190
                continue;
191
192
            if ( ! $operands = $operand->getOperands())
193
                continue;
194
195
            // Id AND is an operand on AND they can be merge (and the same with OR)
196
            foreach ($operands as $sub_operand) {
197
                $this->addOperand( $sub_operand->copy() );
198
            }
199
            unset($this->operands[$i]);
200
201
            // possibility of mono-operand or dupicates
202
            $has_been_changed = true;
203
        }
204
205
        return !empty($has_been_changed);
206
    }
207
208
    /**
209
     * Removes rule branches that cannot produce result like:
210
     * A = 1 || (B < 2 && B > 3) <=> A = 1
211
     *
212
     * @return AndRule $this
213
     */
214
    public function removeInvalidBranches(array $simplification_options)
215
    {
216
        if (!$this->isNormalizationAllowed($simplification_options))
217
            return $this;
218
219
        $this->moveSimplificationStepForward(self::remove_invalid_branches, $simplification_options);
220
221
        foreach ($this->operands as $i => $operand) {
222
            // if ($operand instanceof AndRule || $operand instanceof OrRule ) {
223
            if ( in_array( get_class($operand), [AndRule::class, OrRule::class]) ) {
224
                $this->operands[$i] = $operand->removeInvalidBranches($simplification_options);
0 ignored issues
show
Bug introduced by
The method removeInvalidBranches() does not exist on JClaveau\LogicalFilter\Rule\AbstractRule. It seems like you code against a sub-type of JClaveau\LogicalFilter\Rule\AbstractRule such as JClaveau\LogicalFilter\Rule\AndRule or JClaveau\LogicalFilter\Rule\OrRule. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

224
                /** @scrutinizer ignore-call */ 
225
                $this->operands[$i] = $operand->removeInvalidBranches($simplification_options);
Loading history...
225
                if (!$this->operands[$i]->hasSolution()) {
0 ignored issues
show
Bug introduced by
The method hasSolution() does not exist on JClaveau\LogicalFilter\Rule\AbstractRule. It seems like you code against a sub-type of said class. However, the method does not exist in JClaveau\LogicalFilter\Rule\AbstractOperationRule or JClaveau\LogicalFilter\Rule\AbstractAtomicRule. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

225
                if (!$this->operands[$i]->/** @scrutinizer ignore-call */ hasSolution()) {
Loading history...
226
                    $this->operands = [];
227
                    return $this;
228
                }
229
            }
230
        }
231
232
        $operandsByFields = $this->groupOperandsByFieldAndOperator();
233
234
        // $this->dump(true);
235
236
        foreach ($operandsByFields as $field => $operandsByOperator) {
237
238
            if (!empty($operandsByOperator[ EqualRule::operator ])) {
239
240
                foreach ($operandsByOperator[ EqualRule::operator ] as $equalRule) {
241
                    // Multiple equal rules without the same value is invalid
242
                    if (isset($previousEqualRule) && $previousEqualRule->getValue() != $equalRule->getValue()) {
243
                        $this->operands = [];
244
                        return $this;
245
                    }
246
                    $previousEqualRule = $equalRule;
247
                }
248
                unset($previousEqualRule);
249
250
                $equalRule = reset($operandsByOperator[ EqualRule::operator ]);
251
252
                if (   !empty($operandsByOperator[ BelowRule::operator ])
253
                    && $equalRule->getValue() === null
254
                ) {
255
                    $this->operands = [];
256
                    return $this;
257
                }
258
259
                if (   !empty($operandsByOperator[ BelowRule::operator ])
260
                    && $equalRule->getValue() >= reset($operandsByOperator[ BelowRule::operator ])->getMaximum()
261
                ) {
262
                    $this->operands = [];
263
                    return $this;
264
                }
265
266
                if (   !empty($operandsByOperator[ AboveRule::operator ])
267
                    && $equalRule->getValue() === null
268
                ) {
269
                    $this->operands = [];
270
                    return $this;
271
                }
272
273
                if (   !empty($operandsByOperator[ AboveRule::operator ])
274
                    && $equalRule->getValue() <= reset($operandsByOperator[ AboveRule::operator ])->getMinimum()
275
                ) {
276
                    $this->operands = [];
277
                    return $this;
278
                }
279
280
                if (   !empty($operandsByOperator[ NotEqualRule::operator ])
281
                    && $equalRule->getValue() == reset($operandsByOperator[ NotEqualRule::operator ])->getValue()
282
                ) {
283
                    $this->operands = [];
284
                    return $this;
285
                }
286
287
                if (   !empty($operandsByOperator[ NotEqualRule::operator ])
288
                    && $equalRule->getValue() === null
289
                    && reset($operandsByOperator[ NotEqualRule::operator ])->getValue() === null
290
                ) {
291
                    $this->operands = [];
292
                    return $this;
293
                }
294
            }
295
            elseif (   !empty($operandsByOperator[ BelowRule::operator ])
296
                    && !empty($operandsByOperator[ AboveRule::operator ])) {
297
                $aboveRule = reset($operandsByOperator[ AboveRule::operator ]);
298
                $belowRule = reset($operandsByOperator[ BelowRule::operator ]);
299
300
                if ($belowRule->getMaximum() <= $aboveRule->getMinimum()) {
301
                    $this->operands = [];
302
                    return $this;
303
                }
304
            }
305
        }
306
307
        return $this;
308
    }
309
310
    /**
311
     * Checks if a simplified AndRule has incompatible operands like:
312
     * + a = 3 && a > 4
313
     * + a = 3 && a < 2
314
     * + a > 3 && a < 2
315
     *
316
     * @return bool If the AndRule can have a solution or not
317
     */
318
    public function hasSolution(array $contextual_options=[])
319
    {
320
        if (!$this->simplicationStepReached(self::simplified)) {
321
            throw new \LogicException(
322
                "hasSolution has no sens if the rule is not simplified instead of being at: "
323
                .var_export($this->current_simplification_step, true)
324
            );
325
        }
326
327
        // atomic rules
328
        foreach ($this->getOperands() as $operand) {
329
            if (method_exists($operand, 'hasSolution') && !$operand->hasSolution())
330
                return false;
331
        }
332
333
        return !empty($this->getOperands());
334
    }
335
336
337
    /**
338
     * + if A > 2 && A > 1 <=> A > 2
339
     * + if A < 2 && A < 1 <=> A < 1
340
     */
341
    protected static function simplifySameOperands(array $operandsByFields)
342
    {
343
        // unifying same operands
344
        foreach ($operandsByFields as $field => $operandsByOperator) {
345
346
            foreach ($operandsByOperator as $operator => $operands) {
347
                unset($previous_operand);
348
349
                try {
350
                    if ($operator == AboveRule::operator) {
351
                        usort($operands, function( AboveRule $a, AboveRule $b ) {
352
                            if ($a->getMinimum() === null)
0 ignored issues
show
introduced by
The condition $a->getMinimum() === null is always false.
Loading history...
Deprecated Code introduced by
The function JClaveau\LogicalFilter\R...AboveRule::getMinimum() has been deprecated: getLowerLimit ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

352
                            if (/** @scrutinizer ignore-deprecated */ $a->getMinimum() === null)

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
353
                                return 1;
354
355
                            if ($b->getMinimum() === null)
0 ignored issues
show
introduced by
The condition $b->getMinimum() === null is always false.
Loading history...
Deprecated Code introduced by
The function JClaveau\LogicalFilter\R...AboveRule::getMinimum() has been deprecated: getLowerLimit ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

355
                            if (/** @scrutinizer ignore-deprecated */ $b->getMinimum() === null)

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
356
                                return -1;
357
358
                            if ($a->getMinimum() > $b->getMinimum())
0 ignored issues
show
Deprecated Code introduced by
The function JClaveau\LogicalFilter\R...AboveRule::getMinimum() has been deprecated: getLowerLimit ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

358
                            if (/** @scrutinizer ignore-deprecated */ $a->getMinimum() > $b->getMinimum())

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
359
                                return -1;
360
361
                            return 1;
362
                        });
363
                        $operands = [reset($operands)];
364
                    }
365
                    elseif ($operator == BelowRule::operator) {
366
                        usort($operands, function( BelowRule $a, BelowRule $b ) {
367
                            if ($a->getMaximum() === null)
0 ignored issues
show
introduced by
The condition $a->getMaximum() === null is always false.
Loading history...
Deprecated Code introduced by
The function JClaveau\LogicalFilter\R...BelowRule::getMaximum() has been deprecated: getUpperLimit ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

367
                            if (/** @scrutinizer ignore-deprecated */ $a->getMaximum() === null)

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
368
                                return 1;
369
370
                            if ($b->getMaximum() === null)
0 ignored issues
show
introduced by
The condition $b->getMaximum() === null is always false.
Loading history...
Deprecated Code introduced by
The function JClaveau\LogicalFilter\R...BelowRule::getMaximum() has been deprecated: getUpperLimit ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

370
                            if (/** @scrutinizer ignore-deprecated */ $b->getMaximum() === null)

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
371
                                return -1;
372
373
                            if ($a->getMaximum() < $b->getMaximum())
0 ignored issues
show
Deprecated Code introduced by
The function JClaveau\LogicalFilter\R...BelowRule::getMaximum() has been deprecated: getUpperLimit ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

373
                            if (/** @scrutinizer ignore-deprecated */ $a->getMaximum() < $b->getMaximum())

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
374
                                return -1;
375
376
                            return 1;
377
                        });
378
                        $operands = [reset($operands)];
379
                    }
380
                    elseif ($operator == EqualRule::operator) {
381
                        // TODO add an option for the support strict comparison
382
                        foreach ($operands as $i => $operand) {
383
                            if (!isset($previous_operand)) {
384
                                $previous_operand = $operand;
385
                                continue;
386
                            }
387
388
                            if ($previous_operand == $operand) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $previous_operand does not seem to be defined for all execution paths leading up to this point.
Loading history...
389
                                unset($operands[$i]);
390
                                continue;
391
                            }
392
                            else {
393
                                // Same field expected to be two differents
394
                                // values at the same time has no sens so
395
                                // we remove all the operands of the current
396
                                // AndRule (TODO FalseRule)
397
                                return [];
398
                            }
399
                        }
400
                    }
401
                    elseif ($operator == InRule::operator) {
402
                        $first_in = reset($operands);
403
404
                        foreach ($operands as $i => $next_in) {
405
                            if ($first_in === $next_in)
406
                                continue;
407
408
                            $first_in->setPossibilities( array_intersect(
409
                                $first_in->getPossibilities(),
410
                                $next_in->getPossibilities()
411
                            ) );
412
413
                            unset($operands[$i]);
414
                        }
415
416
                        // [field in []] <=> false
417
                        if (!$first_in->getPossibilities())
418
                            return [];
419
                    }
420
                    elseif ($operator == NotInRule::operator) {
421
                        $first_not_in = reset($operands);
422
423
                        foreach ($operands as $i => $next_not_in) {
424
                            if ($first_not_in === $next_not_in)
425
                                continue;
426
427
                            $first_not_in->setPossibilities( array_merge(
428
                                $first_not_in->getPossibilities(),
429
                                $next_not_in->getPossibilities()
430
                            ) );
431
432
                            unset($operands[$i]);
433
                        }
434
                    }
435
                }
436
                catch (\Exception $e) {
437
                    VisibilityViolator::setHiddenProperty($e, 'message', $e->getMessage() . "\n" . var_export([
0 ignored issues
show
Bug introduced by
$e->getMessage() . ' ' ....s' => $operands), true) of type string is incompatible with the type JClaveau\VisibilityViolator\the expected by parameter $value of JClaveau\VisibilityViola...or::setHiddenProperty(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

437
                    VisibilityViolator::setHiddenProperty($e, 'message', /** @scrutinizer ignore-type */ $e->getMessage() . "\n" . var_export([
Loading history...
Bug introduced by
'message' of type string is incompatible with the type JClaveau\VisibilityViolator\of expected by parameter $name of JClaveau\VisibilityViola...or::setHiddenProperty(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

437
                    VisibilityViolator::setHiddenProperty($e, /** @scrutinizer ignore-type */ 'message', $e->getMessage() . "\n" . var_export([
Loading history...
438
                            'operands' => $operands,
439
                            // 'this'     => $this,
440
                        ], true)
441
                    );
442
443
                    // \Debug::dumpJson($this->toArray(), true);
444
                    throw $e;
445
                }
446
447
                $operandsByFields[ $field ][ $operator ] = $operands;
448
            }
449
        }
450
451
        return $operandsByFields;
452
    }
453
454
    /**
455
     */
456
    protected static function simplifyDifferentOperands(array $operandsByFields)
457
    {
458
        foreach ($operandsByFields as $field => &$operandsByOperator) {
459
460
            foreach ([
461
                    EqualRule::operator,
462
                    AboveRule::operator,
463
                    AboveRule::operator,
464
                    InRule::operator,
465
                    NotInRule::operator,
466
                    BelowOrEqualRule::operator,
467
                    AboveOrEqualRule::operator,
468
                ]
469
                as $unifyable_operator
470
            ) {
471
                if (!empty($operandsByOperator[ $unifyable_operator ])) {
472
                    if (count($operandsByOperator[ $unifyable_operator ]) != 1) {
473
                        throw new \LogicException(
474
                            __METHOD__ . " MUST be called after unifyAtomicOperands() "
475
                            ."to have only one '$unifyable_operator' predicate istead of:\n"
476
                            ."[\n".implode( ",\n", array_map(function ($rule) {
477
                                    return $rule->toString();
478
                                }, $operandsByOperator[ $unifyable_operator ])
479
                            )."\n]"
480
                        );
481
                    }
482
                }
483
            }
484
485
            $operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator);
486
            // If tyhere is no more operands for a given field it means there
487
            // is no possible solutions for it so all the current and_case
488
            // is invalidated.
489
            if (! $operandsByOperator)
490
                return [];
491
        }
492
493
        return $operandsByFields;
494
    }
495
496
    /**
497
     * + if A = 2 && A > 1 <=> A = 2
498
     * + if A = 2 && A < 4 <=> A = 2
499
     */
500
    protected static function simplifyDifferentOperandsForField($field, array $operandsByOperator)
501
    {
502
        // EqualRule comparisons
503
        if (!empty($operandsByOperator[ EqualRule::operator ])) {
504
505
            $equalRule = reset( $operandsByOperator[ EqualRule::operator ] );
506
507
            if (!empty($operandsByOperator[ NotEqualRule::operator ])) {
508
                foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $not_equal_rule) {
509
510
                    if ($equalRule->getValue() !== null) {
511
                        if ($not_equal_rule->getValue() === null) // means if exists <=> equals something
512
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
513
                        elseif ($not_equal_rule->getValue() != $equalRule->getValue())
514
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
515
                    }
516
                    elseif ($equalRule->getValue() === null ) {
517
                        if ($not_equal_rule->getValue() !== null)
518
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
519
                        // else we let the "equal null" and the "not equal null" for the romeInvalidBranches step
520
                    }
521
                }
522
            }
523
524
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
525
526
                $aboveRule = reset($operandsByOperator[ AboveRule::operator ]);
527
                if ($equalRule->getValue() !== null && $aboveRule->getMinimum() < $equalRule->getValue())
528
                    unset($operandsByOperator[ AboveRule::operator ]);
529
            }
530
531
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
532
533
                $belowRule = reset($operandsByOperator[ BelowRule::operator ]);
534
                if ($equalRule->getValue() !== null && $belowRule->getMaximum() > $equalRule->getValue())
535
                    unset($operandsByOperator[ BelowRule::operator ]);
536
            }
537
538
            if (!empty($operandsByOperator[ InRule::operator ])) {
539
540
                $possibilities = reset($operandsByOperator[ InRule::operator ])->getPossibilities();
541
542
                if (in_array($equalRule->getValue(), $possibilities)) {
543
                    unset($operandsByOperator[ InRule::operator ]);
544
                }
545
                else {
546
                    // We flush possibilities of the InRule
547
                    // TODO Replace it by a FalseRule
548
                    $operandsByOperator[ InRule::operator ][0]->setPossibilities([]);
549
                    // and also remove the equal rule to shorten the reste of the simplification process
550
                    unset($operandsByOperator[ EqualRule::operator ]);
551
                }
552
            }
553
554
            if (!empty($operandsByOperator[ NotInRule::operator ])) {
555
556
                $notInRule = reset($operandsByOperator[ NotInRule::operator ]);
557
                if (in_array($equalRule->getValue(), $notInRule->getPossibilities())) {
558
                    // ['field', '=', 4] && ['field', '!in', [4]...] <=> false
559
                    return [];
560
                }
561
                else {
562
                    unset($operandsByOperator[ NotInRule::operator ]);
563
                }
564
                // $notInRule->dump(true);
565
            }
566
567
            if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) {
568
569
                $belowOrEqualRule = reset($operandsByOperator[ BelowOrEqualRule::operator ]);
570
                if ($equalRule->getValue() <= $belowOrEqualRule->getMaximum()) {
571
                    unset($operandsByOperator[ BelowOrEqualRule::operator ]);
572
                }
573
                else {
574
                    // ['field', '=', 4] && ['field', '<=', [3]...] <=> false
575
                    return [];
576
                }
577
            }
578
579
            if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) {
580
581
                $aboveOrEqualRule = reset($operandsByOperator[ AboveOrEqualRule::operator ]);
582
                if ($equalRule->getValue() >= $aboveOrEqualRule->getMinimum()) {
583
                    unset($operandsByOperator[ AboveOrEqualRule::operator ]);
584
                }
585
                else {
586
                    // ['field', '=', 4] && ['field', '<=', [3]...] <=> false
587
                    return [];
588
                }
589
            }
590
        }
591
592
        // NotEqualRule null comparisons
593
        if (!empty($operandsByOperator[ NotEqualRule::operator ])) {
594
            if (!empty($operandsByOperator[ NotEqualRule::operator ])) {
595
                foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $notEqualRule) {
596
597
                    if ($notEqualRule->getValue() === null) {
598
                        if (!empty($operandsByOperator[ AboveRule::operator ])) {
599
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
600
                        }
601
602
                        if (!empty($operandsByOperator[ BelowRule::operator ])) {
603
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
604
                        }
605
606
                        if (!empty($operandsByOperator[ EqualRule::operator ])) {
607
                            if (reset($operandsByOperator[ EqualRule::operator ])->getValue() !== null)
608
                                unset($operandsByOperator[ NotEqualRule::operator ][$i]);
609
                        }
610
                    }
611
                    else {
612
                        if (!empty($operandsByOperator[ AboveRule::operator ])) {
613
                            if ($operandsByOperator[ AboveRule::operator ][0]->getMinimum() >= $notEqualRule->getValue())
614
                                unset($operandsByOperator[ NotEqualRule::operator ][$i]);
615
                        }
616
617
                        if (!empty($operandsByOperator[ BelowRule::operator ])) {
618
                            if ($operandsByOperator[ BelowRule::operator ][0]->getMaximum() <= $notEqualRule->getValue())
619
                                unset($operandsByOperator[ NotEqualRule::operator ][$i]);
620
                        }
621
                    }
622
623
                    if (!empty($operandsByOperator[ NotInRule::operator ])) {
624
                        $notInRule = reset($operandsByOperator[ NotInRule::operator ]);
625
                        if (!in_array($notEqualRule->getValue(), $notInRule->getPossibilities())) {
626
                            // TODO Replace it by a FalseRule
627
                            $operandsByOperator[ NotInRule::operator ][0]->setPossibilities(
628
                                array_merge($notInRule->getPossibilities(), [$notEqualRule->getValue()])
629
                            );
630
                        }
631
632
                        unset($operandsByOperator[ NotEqualRule::operator ][$i]);
633
                    }
634
635
                    if (!empty($operandsByOperator[ InRule::operator ])) {
636
                        $inRule = reset($operandsByOperator[ InRule::operator ]);
637
638
                        $operandsByOperator[ InRule::operator ][0]->setPossibilities(
639
                            array_diff($inRule->getPossibilities(), [$notEqualRule->getValue()])
640
                        );
641
                    }
642
                }
643
            }
644
        }
645
646
        // Comparison between InRules and NotInRules
647
        // This is an optimization to avoid NotIn explosion
648
        if (!empty($operandsByOperator[ InRule::operator ])) {
649
            $inRule = $operandsByOperator[ InRule::operator ][0];
650
651
            if (!empty($operandsByOperator[ NotInRule::operator ])) {
652
                $notInRule = reset($operandsByOperator[ NotInRule::operator ]);
653
                $operandsByOperator[ InRule::operator ][0]->setPossibilities(
654
                    array_diff( $inRule->getPossibilities(), $notInRule->getPossibilities())
655
                );
656
                unset($operandsByOperator[ NotInRule::operator ]);
657
            }
658
659
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
660
                $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getMaximum();
661
662
                $operandsByOperator[ InRule::operator ][0]->setPossibilities(
663
                    array_filter( $inRule->getPossibilities(), function ($possibility) use ($upper_limit) {
664
                        return $possibility < $upper_limit;
665
                    } )
666
                );
667
668
                unset($operandsByOperator[ BelowRule::operator ]);
669
            }
670
671
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
672
                $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum();
673
674
                $operandsByOperator[ InRule::operator ][0]->setPossibilities(
675
                    array_filter( $inRule->getPossibilities(), function ($possibility) use ($lower_limit) {
676
                        return $possibility > $lower_limit;
677
                    } )
678
                );
679
680
                unset($operandsByOperator[ AboveRule::operator ]);
681
            }
682
        }
683
684
        // Comparison between NotInRules and > or <
685
        if (!empty($operandsByOperator[ NotInRule::operator ])) {
686
            $notInRule = $operandsByOperator[ NotInRule::operator ][0];
687
688
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
689
                $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit();
690
691
                $operandsByOperator[ NotInRule::operator ][0]->setPossibilities(
692
                    array_filter( $notInRule->getPossibilities(), function ($possibility) use ($upper_limit) {
693
                        return $possibility < $upper_limit;
694
                    } )
695
                );
696
            }
697
698
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
699
                $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum();
700
701
                $operandsByOperator[ NotInRule::operator ][0]->setPossibilities(
702
                    array_filter( $notInRule->getPossibilities(), function ($possibility) use ($lower_limit) {
703
                        return $possibility > $lower_limit;
704
                    } )
705
                );
706
            }
707
        }
708
709
        // Comparison between <= and > or <
710
        if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) {
711
            $belowOrEqualRule = $operandsByOperator[ BelowOrEqualRule::operator ][0];
712
713
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
714
                $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit();
715
716
                if ($belowOrEqualRule->getMaximum() >= $upper_limit) {
717
                    // [field < 3] && [field <= 3]
718
                    // [field < 3] && [field <= 4]
719
                    unset($operandsByOperator[ BelowOrEqualRule::operator ][0]);
720
                }
721
                else {
722
                    // [field < 3] && [field <= 2]
723
                    unset($operandsByOperator[ BelowRule::operator ][0]);
724
                }
725
            }
726
727
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
728
                $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getLowerLimit();
729
730
                if ($belowOrEqualRule->getMaximum() <= $lower_limit) {
731
                    // [field > 3] && [field <= 2] <=> false
732
                    return [];
733
                }
734
            }
735
736
            if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) {
737
                $minimum = reset($operandsByOperator[ AboveOrEqualRule::operator ])->getMinimum();
738
739
                if ($belowOrEqualRule->getMaximum() < $minimum) {
740
                    // [field <= 3] && [field >= 4] <=> false
741
                    return [];
742
                }
743
                elseif ($belowOrEqualRule->getMaximum() == $minimum) {
744
                    // [field <= 3] && [field >= 3] <=> [field = 3]
745
                    unset($operandsByOperator[ BelowOrEqualRule::operator ]);
746
                    unset($operandsByOperator[ AboveOrEqualRule::operator ]);
747
                    $operandsByOperator[ EqualRule::operator ][] = new EqualRule($field, $minimum);
748
749
                    if (count($operandsByOperator[ EqualRule::operator ]) > 1) {
750
                        $operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator);
751
                    }
752
                }
753
            }
754
        }
755
756
        return $operandsByOperator;
757
    }
758
759
    /**
760
     * This method is meant to be used during simplification that would
761
     * need to change the class of the current instance by a normal one.
762
     *
763
     * @return AndRule The current instance (of or or subclass) or a new AndRule
764
     */
765
    public function setOperandsOrReplaceByOperation($new_operands)
766
    {
767
        try {
768
            return $this->setOperands( $new_operands );
769
        }
770
        catch (\LogicException $e) {
771
            return new AndRule( $new_operands );
772
        }
773
    }
774
775
    /**/
776
}
777