Test Failed
Push — master ( 6c792f...176748 )
by Jean
02:26
created

AndRule::setOperandsOrReplaceByOperation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
        }
487
488
        return $operandsByFields;
489
    }
490
491
    /**
492
     * + if A = 2 && A > 1 <=> A = 2
493
     * + if A = 2 && A < 4 <=> A = 2
494
     */
495
    protected static function simplifyDifferentOperandsForField($field, array $operandsByOperator)
496
    {
497
        // EqualRule comparisons
498
        if (!empty($operandsByOperator[ EqualRule::operator ])) {
499
500
            $equalRule = reset( $operandsByOperator[ EqualRule::operator ] );
501
502
            if (!empty($operandsByOperator[ NotEqualRule::operator ])) {
503
                foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $not_equal_rule) {
504
505
                    if ($equalRule->getValue() !== null) {
506
                        if ($not_equal_rule->getValue() === null) // means if exists <=> equals something
507
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
508
                        elseif ($not_equal_rule->getValue() != $equalRule->getValue())
509
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
510
                    }
511
                    elseif ($equalRule->getValue() === null ) {
512
                        if ($not_equal_rule->getValue() !== null)
513
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
514
                        // else we let the "equal null" and the "not equal null" for the romeInvalidBranches step
515
                    }
516
                }
517
            }
518
519
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
520
521
                $aboveRule = reset($operandsByOperator[ AboveRule::operator ]);
522
                if ($equalRule->getValue() !== null && $aboveRule->getMinimum() < $equalRule->getValue())
523
                    unset($operandsByOperator[ AboveRule::operator ]);
524
            }
525
526
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
527
528
                $belowRule = reset($operandsByOperator[ BelowRule::operator ]);
529
                if ($equalRule->getValue() !== null && $belowRule->getMaximum() > $equalRule->getValue())
530
                    unset($operandsByOperator[ BelowRule::operator ]);
531
            }
532
533
            if (!empty($operandsByOperator[ InRule::operator ])) {
534
535
                $possibilities = reset($operandsByOperator[ InRule::operator ])->getPossibilities();
536
537
                if (in_array($equalRule->getValue(), $possibilities)) {
538
                    unset($operandsByOperator[ InRule::operator ]);
539
                }
540
                else {
541
                    // We flush possibilities of the InRule
542
                    // TODO Replace it by a FalseRule
543
                    $operandsByOperator[ InRule::operator ][0]->setPossibilities([]);
544
                    // and also remove the equal rule to shorten the reste of the simplification process
545
                    unset($operandsByOperator[ EqualRule::operator ]);
546
                }
547
            }
548
549
            if (!empty($operandsByOperator[ NotInRule::operator ])) {
550
551
                $notInRule = reset($operandsByOperator[ NotInRule::operator ]);
552
                if (in_array($equalRule->getValue(), $notInRule->getPossibilities())) {
553
                    // ['field', '=', 4] && ['field', '!in', [4]...] <=> false
554
                    return [];
555
                }
556
                else {
557
                    unset($operandsByOperator[ NotInRule::operator ]);
558
                }
559
                // $notInRule->dump(true);
560
            }
561
562
            if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) {
563
564
                $belowOrEqualRule = reset($operandsByOperator[ BelowOrEqualRule::operator ]);
565
                if ($equalRule->getValue() <= $belowOrEqualRule->getMaximum()) {
566
                    unset($operandsByOperator[ BelowOrEqualRule::operator ]);
567
                }
568
                else {
569
                    // ['field', '=', 4] && ['field', '<=', [3]...] <=> false
570
                    return [];
571
                }
572
            }
573
574
            if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) {
575
576
                $aboveOrEqualRule = reset($operandsByOperator[ AboveOrEqualRule::operator ]);
577
                if ($equalRule->getValue() >= $aboveOrEqualRule->getMinimum()) {
578
                    unset($operandsByOperator[ AboveOrEqualRule::operator ]);
579
                }
580
                else {
581
                    // ['field', '=', 4] && ['field', '<=', [3]...] <=> false
582
                    return [];
583
                }
584
            }
585
        }
586
587
        // NotEqualRule null comparisons
588
        if (!empty($operandsByOperator[ NotEqualRule::operator ])) {
589
            if (!empty($operandsByOperator[ NotEqualRule::operator ])) {
590
                foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $notEqualRule) {
591
592
                    if ($notEqualRule->getValue() === null) {
593
                        if (!empty($operandsByOperator[ AboveRule::operator ])) {
594
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
595
                        }
596
597
                        if (!empty($operandsByOperator[ BelowRule::operator ])) {
598
                            unset($operandsByOperator[ NotEqualRule::operator ][$i]);
599
                        }
600
601
                        if (!empty($operandsByOperator[ EqualRule::operator ])) {
602
                            if (reset($operandsByOperator[ EqualRule::operator ])->getValue() !== null)
603
                                unset($operandsByOperator[ NotEqualRule::operator ][$i]);
604
                        }
605
                    }
606
                    else {
607
                        if (!empty($operandsByOperator[ AboveRule::operator ])) {
608
                            if ($operandsByOperator[ AboveRule::operator ][0]->getMinimum() >= $notEqualRule->getValue())
609
                                unset($operandsByOperator[ NotEqualRule::operator ][$i]);
610
                        }
611
612
                        if (!empty($operandsByOperator[ BelowRule::operator ])) {
613
                            if ($operandsByOperator[ BelowRule::operator ][0]->getMaximum() <= $notEqualRule->getValue())
614
                                unset($operandsByOperator[ NotEqualRule::operator ][$i]);
615
                        }
616
                    }
617
618
                    if (!empty($operandsByOperator[ NotInRule::operator ])) {
619
                        $notInRule = reset($operandsByOperator[ NotInRule::operator ]);
620
                        if (!in_array($notEqualRule->getValue(), $notInRule->getPossibilities())) {
621
                            // TODO Replace it by a FalseRule
622
                            $operandsByOperator[ NotInRule::operator ][0]->setPossibilities(
623
                                array_merge($notInRule->getPossibilities(), [$notEqualRule->getValue()])
624
                            );
625
                        }
626
627
                        unset($operandsByOperator[ NotEqualRule::operator ][$i]);
628
                    }
629
630
                    if (!empty($operandsByOperator[ InRule::operator ])) {
631
                        $inRule = reset($operandsByOperator[ InRule::operator ]);
632
633
                        $operandsByOperator[ InRule::operator ][0]->setPossibilities(
634
                            array_diff($inRule->getPossibilities(), [$notEqualRule->getValue()])
635
                        );
636
                    }
637
                }
638
            }
639
        }
640
641
        // Comparison between InRules and NotInRules
642
        // This is an optimization to avoid NotIn explosion
643
        if (!empty($operandsByOperator[ InRule::operator ])) {
644
            $inRule = $operandsByOperator[ InRule::operator ][0];
645
646
            if (!empty($operandsByOperator[ NotInRule::operator ])) {
647
                $notInRule = reset($operandsByOperator[ NotInRule::operator ]);
648
                $operandsByOperator[ InRule::operator ][0]->setPossibilities(
649
                    array_diff( $inRule->getPossibilities(), $notInRule->getPossibilities())
650
                );
651
                unset($operandsByOperator[ NotInRule::operator ]);
652
            }
653
654
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
655
                $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getMaximum();
656
657
                $operandsByOperator[ InRule::operator ][0]->setPossibilities(
658
                    array_filter( $inRule->getPossibilities(), function ($possibility) use ($upper_limit) {
659
                        return $possibility < $upper_limit;
660
                    } )
661
                );
662
663
                unset($operandsByOperator[ BelowRule::operator ]);
664
            }
665
666
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
667
                $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum();
668
669
                $operandsByOperator[ InRule::operator ][0]->setPossibilities(
670
                    array_filter( $inRule->getPossibilities(), function ($possibility) use ($lower_limit) {
671
                        return $possibility > $lower_limit;
672
                    } )
673
                );
674
675
                unset($operandsByOperator[ AboveRule::operator ]);
676
            }
677
        }
678
679
        // Comparison between NotInRules and > or <
680
        if (!empty($operandsByOperator[ NotInRule::operator ])) {
681
            $notInRule = $operandsByOperator[ NotInRule::operator ][0];
682
683
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
684
                $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit();
685
686
                $operandsByOperator[ NotInRule::operator ][0]->setPossibilities(
687
                    array_filter( $notInRule->getPossibilities(), function ($possibility) use ($upper_limit) {
688
                        return $possibility < $upper_limit;
689
                    } )
690
                );
691
            }
692
693
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
694
                $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum();
695
696
                $operandsByOperator[ NotInRule::operator ][0]->setPossibilities(
697
                    array_filter( $notInRule->getPossibilities(), function ($possibility) use ($lower_limit) {
698
                        return $possibility > $lower_limit;
699
                    } )
700
                );
701
            }
702
        }
703
704
        // Comparison between <= and > or <
705
        if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) {
706
            $belowOrEqualRule = $operandsByOperator[ BelowOrEqualRule::operator ][0];
707
708
            if (!empty($operandsByOperator[ BelowRule::operator ])) {
709
                $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit();
710
711
                if ($belowOrEqualRule->getMaximum() >= $upper_limit) {
712
                    // [field < 3] && [field <= 3]
713
                    // [field < 3] && [field <= 4]
714
                    unset($operandsByOperator[ BelowOrEqualRule::operator ][0]);
715
                }
716
                else {
717
                    // [field < 3] && [field <= 2]
718
                    unset($operandsByOperator[ BelowRule::operator ][0]);
719
                }
720
            }
721
722
            if (!empty($operandsByOperator[ AboveRule::operator ])) {
723
                $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getLowerLimit();
724
725
                if ($belowOrEqualRule->getMaximum() <= $lower_limit) {
726
                    // [field > 3] && [field <= 2] <=> false
727
                    return [];
728
                }
729
            }
730
731
            if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) {
732
                $minimum = reset($operandsByOperator[ AboveOrEqualRule::operator ])->getMinimum();
733
734
                if ($belowOrEqualRule->getMaximum() < $minimum) {
735
                    // [field <= 3] && [field >= 4] <=> false
736
                    return [];
737
                }
738
                elseif ($belowOrEqualRule->getMaximum() == $minimum) {
739
                    // [field <= 3] && [field >= 3] <=> [field = 3]
740
                    unset($operandsByOperator[ BelowOrEqualRule::operator ]);
741
                    unset($operandsByOperator[ AboveOrEqualRule::operator ]);
742
                    $operandsByOperator[ EqualRule::operator ][] = new EqualRule($field, $minimum);
743
744
                    if (count($operandsByOperator[ EqualRule::operator ]) > 1) {
745
                        $operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator);
746
                    }
747
                }
748
            }
749
        }
750
751
        return $operandsByOperator;
752
    }
753
754
    /**
755
     * This method is meant to be used during simplification that would
756
     * need to change the class of the current instance by a normal one.
757
     *
758
     * @return AndRule The current instance (of or or subclass) or a new AndRule
759
     */
760
    public function setOperandsOrReplaceByOperation($new_operands)
761
    {
762
        try {
763
            return $this->setOperands( $new_operands );
764
        }
765
        catch (\LogicException $e) {
766
            return new AndRule( $new_operands );
767
        }
768
    }
769
770
    /**/
771
}
772