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
|
98 |
|
public function rootifyDisjunctions(array $simplification_options) |
23
|
|
|
{ |
24
|
98 |
|
if ( ! $this->isNormalizationAllowed($simplification_options)) { |
25
|
|
|
return $this; |
|
|
|
|
26
|
|
|
} |
27
|
|
|
|
28
|
98 |
|
$this->moveSimplificationStepForward( self::rootify_disjunctions, $simplification_options ); |
29
|
|
|
|
30
|
98 |
|
$upLiftedOperands = []; |
31
|
98 |
|
foreach ($this->getOperands() as $operand) { |
32
|
95 |
|
$operand = $operand->copy(); |
33
|
95 |
|
if ($operand instanceof AbstractOperationRule) { |
34
|
56 |
|
$operand = $operand->rootifyDisjunctions($simplification_options); |
35
|
56 |
|
} |
36
|
|
|
|
37
|
95 |
|
$upLiftedOperands[] = $operand; |
38
|
98 |
|
} |
39
|
|
|
|
40
|
|
|
// If the AndRule doesn't contain any OrRule , there is nothing to uplift |
41
|
|
|
if ( ! array_filter($upLiftedOperands, function($operand) { |
42
|
95 |
|
return $operand instanceof OrRule; |
43
|
98 |
|
})) { |
44
|
87 |
|
return new AndRule($upLiftedOperands); |
|
|
|
|
45
|
|
|
} |
46
|
|
|
|
47
|
47 |
|
$firstAndOperand = new AndRule(); |
48
|
|
|
|
49
|
|
|
// This OrRule should contain only AndRules during its generation |
50
|
47 |
|
$upLiftedOr = new OrRule([ |
51
|
47 |
|
$firstAndOperand, |
52
|
47 |
|
]); |
53
|
|
|
|
54
|
|
|
// var_dump($upLiftedOperands); |
55
|
|
|
// $this->dump(true); |
56
|
|
|
|
57
|
47 |
|
foreach ($upLiftedOperands as $i => $operand) { |
58
|
47 |
|
if ($operand instanceof NotRule) { |
59
|
8 |
|
if ( ($operand instanceof NotEqualRule || $operand instanceof NotInRule) |
60
|
8 |
|
&& ! $operand->isNormalizationAllowed($simplification_options) |
61
|
8 |
|
) { |
62
|
8 |
|
foreach ($upLiftedOr->getOperands() as $upLifdtedOperand) { |
63
|
8 |
|
$upLifdtedOperand->addOperand( $operand->copy() ); |
|
|
|
|
64
|
8 |
|
} |
65
|
8 |
|
} |
66
|
|
|
else { |
67
|
|
|
throw new \LogicException( |
68
|
|
|
"Rootifying disjunctions MUST be done after negations removal instead of '".$operand."' \n" |
69
|
|
|
.$operand |
70
|
|
|
); |
71
|
|
|
} |
72
|
9 |
|
} |
73
|
47 |
|
elseif ($operand instanceof OrRule && $operand->isNormalizationAllowed($simplification_options)) { |
74
|
|
|
|
75
|
|
|
// If an operand is an Or, me transform the current |
76
|
|
|
// (A' || A") && (B') <=> (A' && B') || (A" && B'); |
77
|
|
|
// (A' || A") && (B' || B") <=> (A' && B') || (A' && B") || (A" && B') || (A" && B"); |
78
|
|
|
// (A' || A") && (B' || B") && (C' || C") <=> |
79
|
|
|
// (A' && B' && C') || (A' && B' && C") || (A' && B" && C') || (A' && B" && C") |
80
|
|
|
// || (A" && B' && C') || (A" && B' && C") || (A" && B" && C') || (A" && B" && C"); |
81
|
40 |
|
$newUpLiftedOr = new OrRule; |
82
|
40 |
|
foreach ($operand->getOperands() as $subOperand) { |
83
|
38 |
|
foreach ($upLiftedOr->getOperands() as $upLiftedOrSubOperand) { |
84
|
38 |
|
$newUpLiftedOrSubOperand = $upLiftedOrSubOperand->copy(); |
85
|
38 |
|
$newUpLiftedOrSubOperand->addOperand( $subOperand->copy() ); |
86
|
38 |
|
if ($newUpLiftedOrSubOperand->simplify($simplification_options)->hasSolution($simplification_options)) { |
87
|
37 |
|
$newUpLiftedOr->addOperand( $newUpLiftedOrSubOperand ); |
88
|
37 |
|
} |
89
|
38 |
|
} |
90
|
40 |
|
} |
91
|
|
|
|
92
|
40 |
|
$upLiftedOr = $newUpLiftedOr; |
93
|
40 |
|
} |
94
|
|
|
else { |
95
|
|
|
// append the operand to all the operands of the $upLiftedOr |
96
|
28 |
|
foreach ($upLiftedOr->getOperands() as $upLifdtedOperand) { |
97
|
28 |
|
if ( ! $upLifdtedOperand instanceof AndRule) { |
98
|
|
|
throw new \LogicException( |
99
|
|
|
"Operands of the uplifted OrRule MUST be AndRules during" |
100
|
|
|
."the combination." |
101
|
|
|
); |
102
|
|
|
} |
103
|
|
|
|
104
|
28 |
|
$upLifdtedOperand->addOperand( $operand->copy() ); |
105
|
28 |
|
} |
106
|
|
|
} |
107
|
47 |
|
} |
108
|
|
|
|
109
|
47 |
|
return $upLiftedOr; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @param array $options + show_instance=false Display the operator of the rule or its instance id |
114
|
|
|
* |
115
|
|
|
* @return array |
116
|
|
|
* |
117
|
|
|
* @todo same as OrRule |
118
|
|
|
*/ |
119
|
127 |
View Code Duplication |
public function toArray(array $options=[]) |
|
|
|
|
120
|
|
|
{ |
121
|
|
|
$default_options = [ |
122
|
127 |
|
'show_instance' => false, |
123
|
127 |
|
'sort_operands' => false, |
124
|
127 |
|
'semantic' => false, |
125
|
127 |
|
]; |
126
|
127 |
|
foreach ($default_options as $default_option => &$default_value) { |
127
|
127 |
|
if ( ! isset($options[ $default_option ])) { |
128
|
127 |
|
$options[ $default_option ] = $default_value; |
129
|
127 |
|
} |
130
|
127 |
|
} |
131
|
|
|
|
132
|
127 |
|
if ( ! $options['show_instance'] && ! empty($this->cache['array'])) { |
133
|
5 |
|
return $this->cache['array']; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$operands_as_array = [ |
137
|
127 |
|
$options['show_instance'] ? $this->getInstanceId() : self::operator, |
138
|
127 |
|
]; |
139
|
|
|
|
140
|
127 |
|
$operands = $this->operands; |
141
|
127 |
|
if ($options['semantic']) { |
142
|
|
|
// Semantic array: ['operator', 'semantic_id_of_operand1', 'semantic_id_of_operand2', ...] |
143
|
|
|
// with sorted semantic ids |
144
|
122 |
|
$operands_semantic_ids = array_keys($operands); |
145
|
122 |
|
sort($operands_semantic_ids); |
146
|
122 |
|
return array_merge( |
147
|
122 |
|
[self::operator], |
148
|
|
|
$operands_semantic_ids |
149
|
122 |
|
); |
150
|
|
|
} |
151
|
|
|
else { |
152
|
68 |
|
foreach ($operands as $operand) { |
153
|
60 |
|
$operands_as_array[] = $operand->toArray($options); |
|
|
|
|
154
|
68 |
|
} |
155
|
|
|
|
156
|
68 |
|
if ( ! $options['show_instance']) { |
157
|
68 |
|
return $this->cache['array'] = $operands_as_array; |
158
|
|
|
} |
159
|
|
|
else { |
160
|
|
|
return $operands_as_array; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
*/ |
167
|
4 |
View Code Duplication |
public function toString(array $options=[]) |
|
|
|
|
168
|
|
|
{ |
169
|
4 |
|
$operator = self::operator; |
170
|
4 |
|
if ( ! $this->operands) { |
|
|
|
|
171
|
1 |
|
return $this->cache['string'] = "['{$operator}']"; |
172
|
|
|
} |
173
|
|
|
|
174
|
3 |
|
$indent_unit = isset($options['indent_unit']) ? $options['indent_unit'] : ''; |
175
|
3 |
|
$line_break = $indent_unit ? "\n" : ''; |
176
|
|
|
|
177
|
3 |
|
$out = "['{$operator}',$line_break"; |
178
|
|
|
|
179
|
3 |
|
foreach ($this->operands as $operand) { |
180
|
|
|
$out .= implode("\n", array_map(function($line) use (&$indent_unit) { |
181
|
3 |
|
return $indent_unit.$line; |
182
|
3 |
|
}, explode("\n", $operand->toString($options)) )) . ",$line_break"; |
|
|
|
|
183
|
3 |
|
} |
184
|
|
|
|
185
|
3 |
|
$out .= ']'; |
186
|
|
|
|
187
|
3 |
|
return $this->cache['string'] = $out; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Remove AndRules operands of AndRules |
192
|
|
|
*/ |
193
|
99 |
View Code Duplication |
public function removeSameOperationOperands() |
|
|
|
|
194
|
|
|
{ |
195
|
99 |
|
foreach ($this->operands as $i => $operand) { |
196
|
99 |
|
if ( ! is_a($operand, AndRule::class)) { |
197
|
99 |
|
continue; |
198
|
|
|
} |
199
|
|
|
|
200
|
58 |
|
if ( ! $operands = $operand->getOperands()) { |
|
|
|
|
201
|
1 |
|
continue; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
// Id AND is an operand on AND they can be merge (and the same with OR) |
205
|
58 |
|
foreach ($operands as $sub_operand) { |
206
|
58 |
|
$this->addOperand( $sub_operand->copy() ); |
207
|
58 |
|
} |
208
|
58 |
|
unset($this->operands[$i]); |
209
|
|
|
|
210
|
|
|
// possibility of mono-operand or dupicates |
211
|
58 |
|
$has_been_changed = true; |
212
|
99 |
|
} |
213
|
|
|
|
214
|
99 |
|
return ! empty($has_been_changed); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* Removes rule branches that cannot produce result like: |
219
|
|
|
* A = 1 || (B < 2 && B > 3) <=> A = 1 |
220
|
|
|
* |
221
|
|
|
* @return AndRule $this |
222
|
|
|
*/ |
223
|
93 |
|
public function removeInvalidBranches(array $simplification_options) |
224
|
|
|
{ |
225
|
93 |
|
if ( ! $this->isNormalizationAllowed($simplification_options)) { |
226
|
|
|
return $this; |
227
|
|
|
} |
228
|
|
|
|
229
|
93 |
|
$this->moveSimplificationStepForward(self::remove_invalid_branches, $simplification_options); |
230
|
|
|
|
231
|
93 |
|
foreach ($this->operands as $i => $operand) { |
232
|
|
|
// if ($operand instanceof AndRule || $operand instanceof OrRule ) { |
233
|
89 |
|
if ( in_array( get_class($operand), [AndRule::class, OrRule::class]) ) { |
234
|
1 |
|
$this->operands[$i] = $operand->removeInvalidBranches($simplification_options); |
235
|
1 |
|
if ( ! $this->operands[$i]->hasSolution()) { |
236
|
1 |
|
$this->operands = []; |
237
|
1 |
|
return $this; |
238
|
|
|
} |
239
|
|
|
} |
240
|
93 |
|
} |
241
|
|
|
|
242
|
93 |
|
$operandsByFields = $this->groupOperandsByFieldAndOperator(); |
243
|
|
|
|
244
|
|
|
// $this->dump(true); |
245
|
|
|
|
246
|
93 |
|
foreach ($operandsByFields as $field => $operandsByOperator) { |
247
|
88 |
|
if ( ! empty($operandsByOperator[ EqualRule::operator ])) { |
248
|
64 |
|
foreach ($operandsByOperator[ EqualRule::operator ] as $equalRule) { |
249
|
|
|
// Multiple equal rules without the same value is invalid |
250
|
64 |
|
if (isset($previousEqualRule) && $previousEqualRule->getValue() != $equalRule->getValue()) { |
251
|
|
|
$this->operands = []; |
252
|
|
|
return $this; |
253
|
|
|
} |
254
|
64 |
|
$previousEqualRule = $equalRule; |
255
|
64 |
|
} |
256
|
64 |
|
unset($previousEqualRule); |
257
|
|
|
|
258
|
64 |
|
$equalRule = reset($operandsByOperator[ EqualRule::operator ]); |
259
|
|
|
|
260
|
64 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowRule::operator ]) |
|
|
|
|
261
|
64 |
|
&& null === $equalRule->getValue() |
262
|
64 |
|
) { |
263
|
1 |
|
$this->operands = []; |
264
|
1 |
|
return $this; |
265
|
|
|
} |
266
|
|
|
|
267
|
64 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowRule::operator ]) |
|
|
|
|
268
|
64 |
|
&& $equalRule->getValue() >= reset($operandsByOperator[ BelowRule::operator ])->getMaximum() |
269
|
64 |
|
) { |
270
|
7 |
|
$this->operands = []; |
271
|
7 |
|
return $this; |
272
|
|
|
} |
273
|
|
|
|
274
|
64 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ]) |
|
|
|
|
275
|
64 |
|
&& null === $equalRule->getValue() |
276
|
64 |
|
) { |
277
|
1 |
|
$this->operands = []; |
278
|
1 |
|
return $this; |
279
|
|
|
} |
280
|
|
|
|
281
|
63 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ]) |
|
|
|
|
282
|
63 |
|
&& $equalRule->getValue() <= reset($operandsByOperator[ AboveRule::operator ])->getMinimum() |
283
|
63 |
|
) { |
284
|
8 |
|
$this->operands = []; |
285
|
8 |
|
return $this; |
286
|
|
|
} |
287
|
|
|
|
288
|
58 |
View Code Duplication |
if ( ! empty($operandsByOperator[ NotEqualRule::operator ]) |
|
|
|
|
289
|
58 |
|
&& $equalRule->getValue() == reset($operandsByOperator[ NotEqualRule::operator ])->getValue() |
290
|
58 |
|
) { |
291
|
2 |
|
$this->operands = []; |
292
|
2 |
|
return $this; |
293
|
|
|
} |
294
|
|
|
|
295
|
57 |
|
if ( ! empty($operandsByOperator[ NotEqualRule::operator ]) |
296
|
57 |
|
&& null === $equalRule->getValue() |
297
|
57 |
|
&& null === reset($operandsByOperator[ NotEqualRule::operator ])->getValue() |
298
|
57 |
|
) { |
299
|
|
|
$this->operands = []; |
300
|
|
|
return $this; |
301
|
|
|
} |
302
|
57 |
|
} |
303
|
54 |
|
elseif ( ! empty($operandsByOperator[ BelowRule::operator ]) |
304
|
54 |
|
&& ! empty($operandsByOperator[ AboveRule::operator ])) { |
305
|
14 |
|
$aboveRule = reset($operandsByOperator[ AboveRule::operator ]); |
306
|
14 |
|
$belowRule = reset($operandsByOperator[ BelowRule::operator ]); |
307
|
|
|
|
308
|
14 |
|
if ($belowRule->getMaximum() <= $aboveRule->getMinimum()) { |
309
|
12 |
|
$this->operands = []; |
310
|
12 |
|
return $this; |
311
|
|
|
} |
312
|
6 |
|
} |
313
|
87 |
|
} |
314
|
|
|
|
315
|
87 |
|
return $this; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Checks if a simplified AndRule has incompatible operands like: |
320
|
|
|
* + a = 3 && a > 4 |
321
|
|
|
* + a = 3 && a < 2 |
322
|
|
|
* + a > 3 && a < 2 |
323
|
|
|
* |
324
|
|
|
* @return bool If the AndRule can have a solution or not |
325
|
|
|
*/ |
326
|
19 |
|
public function hasSolution(array $contextual_options=[]) |
|
|
|
|
327
|
|
|
{ |
328
|
19 |
View Code Duplication |
if ( ! $this->simplicationStepReached(self::simplified)) { |
|
|
|
|
329
|
|
|
throw new \LogicException( |
330
|
|
|
"hasSolution has no sens if the rule is not simplified instead of being at: " |
331
|
|
|
.var_export($this->current_simplification_step, true) |
332
|
|
|
); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
// atomic rules |
336
|
19 |
|
foreach ($this->getOperands() as $operand) { |
337
|
12 |
|
if (method_exists($operand, 'hasSolution') && ! $operand->hasSolution()) { |
|
|
|
|
338
|
|
|
return false; |
339
|
|
|
} |
340
|
19 |
|
} |
341
|
|
|
|
342
|
19 |
|
return ! empty($this->getOperands()); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* + if A > 2 && A > 1 <=> A > 2 |
348
|
|
|
* + if A < 2 && A < 1 <=> A < 1 |
349
|
|
|
*/ |
350
|
98 |
|
protected static function simplifySameOperands(array $operandsByFields) |
351
|
|
|
{ |
352
|
|
|
// unifying same operands |
353
|
98 |
|
foreach ($operandsByFields as $field => $operandsByOperator) { |
354
|
98 |
|
foreach ($operandsByOperator as $operator => $operands) { |
355
|
98 |
|
unset($previous_operand); |
356
|
|
|
|
357
|
|
|
try { |
358
|
98 |
|
if (AboveRule::operator == $operator) { |
359
|
|
|
usort($operands, function( AboveRule $a, AboveRule $b ) { |
360
|
9 |
|
if (null === $a->getMinimum()) { |
|
|
|
|
361
|
|
|
return 1; |
362
|
|
|
} |
363
|
|
|
|
364
|
9 |
|
if (null === $b->getMinimum()) { |
|
|
|
|
365
|
1 |
|
return -1; |
366
|
|
|
} |
367
|
|
|
|
368
|
9 |
|
if ($a->getMinimum() > $b->getMinimum()) { |
|
|
|
|
369
|
9 |
|
return -1; |
370
|
|
|
} |
371
|
|
|
|
372
|
1 |
|
return 1; |
373
|
44 |
|
}); |
374
|
44 |
|
$operands = [reset($operands)]; |
375
|
44 |
|
} |
376
|
92 |
View Code Duplication |
elseif (BelowRule::operator == $operator) { |
|
|
|
|
377
|
|
|
usort($operands, function( BelowRule $a, BelowRule $b ) { |
378
|
7 |
|
if (null === $a->getMaximum()) { |
|
|
|
|
379
|
1 |
|
return 1; |
380
|
|
|
} |
381
|
|
|
|
382
|
7 |
|
if (null === $b->getMaximum()) { |
|
|
|
|
383
|
|
|
return -1; |
384
|
|
|
} |
385
|
|
|
|
386
|
7 |
|
if ($a->getMaximum() < $b->getMaximum()) { |
|
|
|
|
387
|
2 |
|
return -1; |
388
|
|
|
} |
389
|
|
|
|
390
|
6 |
|
return 1; |
391
|
36 |
|
}); |
392
|
36 |
|
$operands = [reset($operands)]; |
393
|
36 |
|
} |
394
|
90 |
|
elseif (EqualRule::operator == $operator) { |
395
|
|
|
// TODO add an option for the support strict comparison |
396
|
69 |
|
foreach ($operands as $i => $operand) { |
397
|
69 |
|
if ( ! isset($previous_operand)) { |
398
|
69 |
|
$previous_operand = $operand; |
399
|
69 |
|
continue; |
400
|
|
|
} |
401
|
|
|
|
402
|
5 |
|
if ($previous_operand == $operand) { |
403
|
1 |
|
unset($operands[$i]); |
404
|
1 |
|
continue; |
405
|
|
|
} |
406
|
|
|
else { |
407
|
|
|
// Same field expected to be two differents |
408
|
|
|
// values at the same time has no sens so |
409
|
|
|
// we remove all the operands of the current |
410
|
|
|
// AndRule (TODO FalseRule) |
411
|
4 |
|
return []; |
412
|
|
|
} |
413
|
66 |
|
} |
414
|
66 |
|
} |
415
|
63 |
View Code Duplication |
elseif (InRule::operator == $operator) { |
|
|
|
|
416
|
19 |
|
$first_in = reset($operands); |
417
|
|
|
|
418
|
19 |
|
foreach ($operands as $i => $next_in) { |
419
|
19 |
|
if ($first_in === $next_in) { |
420
|
19 |
|
continue; |
421
|
|
|
} |
422
|
|
|
|
423
|
4 |
|
$first_in->setPossibilities( array_intersect( |
424
|
4 |
|
$first_in->getPossibilities(), |
425
|
4 |
|
$next_in->getPossibilities() |
426
|
4 |
|
) ); |
427
|
|
|
|
428
|
4 |
|
unset($operands[$i]); |
429
|
19 |
|
} |
430
|
|
|
|
431
|
|
|
// [field in []] <=> false |
432
|
19 |
|
if ( ! $first_in->getPossibilities()) { |
433
|
|
|
return []; |
434
|
|
|
} |
435
|
19 |
|
} |
436
|
50 |
View Code Duplication |
elseif (NotInRule::operator == $operator) { |
|
|
|
|
437
|
14 |
|
$first_not_in = reset($operands); |
438
|
|
|
|
439
|
14 |
|
foreach ($operands as $i => $next_not_in) { |
440
|
14 |
|
if ($first_not_in === $next_not_in) { |
441
|
14 |
|
continue; |
442
|
|
|
} |
443
|
|
|
|
444
|
2 |
|
$first_not_in->setPossibilities( array_merge( |
445
|
2 |
|
$first_not_in->getPossibilities(), |
446
|
2 |
|
$next_not_in->getPossibilities() |
447
|
2 |
|
) ); |
448
|
|
|
|
449
|
2 |
|
unset($operands[$i]); |
450
|
14 |
|
} |
451
|
14 |
|
} |
452
|
|
|
} |
453
|
95 |
|
catch (\Exception $e) { |
454
|
|
|
VisibilityViolator::setHiddenProperty($e, 'message', $e->getMessage() . "\n" . var_export([ |
|
|
|
|
455
|
|
|
'operands' => $operands, |
456
|
|
|
// 'this' => $this, |
457
|
|
|
], true) |
458
|
|
|
); |
459
|
|
|
|
460
|
|
|
// \Debug::dumpJson($this->toArray(), true); |
461
|
|
|
throw $e; |
462
|
|
|
} |
463
|
|
|
|
464
|
95 |
|
$operandsByFields[ $field ][ $operator ] = $operands; |
465
|
95 |
|
} |
466
|
98 |
|
} |
467
|
|
|
|
468
|
98 |
|
return $operandsByFields; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
*/ |
473
|
98 |
|
protected static function simplifyDifferentOperands(array $operandsByFields) |
474
|
|
|
{ |
475
|
98 |
|
foreach ($operandsByFields as $field => &$operandsByOperator) { |
476
|
|
|
foreach ([ |
477
|
95 |
|
EqualRule::operator, |
478
|
95 |
|
AboveRule::operator, |
479
|
95 |
|
AboveRule::operator, |
480
|
95 |
|
InRule::operator, |
481
|
95 |
|
NotInRule::operator, |
482
|
95 |
|
BelowOrEqualRule::operator, |
483
|
95 |
|
AboveOrEqualRule::operator, |
484
|
|
|
] |
485
|
|
|
as $unifyable_operator |
486
|
95 |
|
) { |
487
|
95 |
|
if ( ! empty($operandsByOperator[ $unifyable_operator ])) { |
488
|
92 |
|
if (1 != count($operandsByOperator[ $unifyable_operator ])) { |
489
|
|
|
throw new \LogicException( |
490
|
|
|
__METHOD__ . " MUST be called after unifyAtomicOperands() " |
491
|
|
|
."to have only one '$unifyable_operator' predicate istead of:\n" |
492
|
|
|
."[\n".implode( ",\n", array_map(function ($rule) { |
493
|
|
|
return $rule->toString(); |
494
|
|
|
}, $operandsByOperator[ $unifyable_operator ]) |
495
|
|
|
)."\n]" |
496
|
|
|
); |
497
|
|
|
} |
498
|
92 |
|
} |
499
|
95 |
|
} |
500
|
|
|
|
501
|
95 |
|
$operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator); |
502
|
|
|
// If tyhere is no more operands for a given field it means there |
503
|
|
|
// is no possible solutions for it so all the current and_case |
504
|
|
|
// is invalidated. |
505
|
95 |
|
if ( ! $operandsByOperator) { |
506
|
2 |
|
return []; |
507
|
|
|
} |
508
|
98 |
|
} |
509
|
|
|
|
510
|
98 |
|
return $operandsByFields; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* + if A = 2 && A > 1 <=> A = 2 |
515
|
|
|
* + if A = 2 && A < 4 <=> A = 2 |
516
|
|
|
*/ |
517
|
95 |
|
protected static function simplifyDifferentOperandsForField($field, array $operandsByOperator) |
518
|
|
|
{ |
519
|
|
|
// EqualRule comparisons |
520
|
95 |
|
if ( ! empty($operandsByOperator[ EqualRule::operator ])) { |
521
|
66 |
|
$equalRule = reset( $operandsByOperator[ EqualRule::operator ] ); |
522
|
|
|
|
523
|
66 |
|
if ( ! empty($operandsByOperator[ NotEqualRule::operator ])) { |
524
|
4 |
|
foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $not_equal_rule) { |
525
|
4 |
|
if (null !== $equalRule->getValue()) { |
526
|
3 |
|
if (null === $not_equal_rule->getValue()) { // means if exists <=> equals something |
527
|
2 |
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
528
|
2 |
|
} |
529
|
1 |
|
elseif ($not_equal_rule->getValue() != $equalRule->getValue()) { |
530
|
|
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
531
|
|
|
} |
532
|
3 |
|
} |
533
|
1 |
|
elseif (null === $equalRule->getValue() ) { |
534
|
1 |
|
if (null !== $not_equal_rule->getValue()) { |
535
|
|
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
536
|
|
|
} |
537
|
|
|
// else we let the "equal null" and the "not equal null" for the romeInvalidBranches step |
538
|
1 |
|
} |
539
|
4 |
|
} |
540
|
4 |
|
} |
541
|
|
|
|
542
|
66 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ])) { |
|
|
|
|
543
|
12 |
|
$aboveRule = reset($operandsByOperator[ AboveRule::operator ]); |
544
|
12 |
|
if (null !== $equalRule->getValue() && $aboveRule->getMinimum() < $equalRule->getValue()) { |
545
|
3 |
|
unset($operandsByOperator[ AboveRule::operator ]); |
546
|
3 |
|
} |
547
|
12 |
|
} |
548
|
|
|
|
549
|
66 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowRule::operator ])) { |
|
|
|
|
550
|
11 |
|
$belowRule = reset($operandsByOperator[ BelowRule::operator ]); |
551
|
11 |
|
if (null !== $equalRule->getValue() && $belowRule->getMaximum() > $equalRule->getValue()) { |
552
|
3 |
|
unset($operandsByOperator[ BelowRule::operator ]); |
553
|
3 |
|
} |
554
|
11 |
|
} |
555
|
|
|
|
556
|
66 |
|
if ( ! empty($operandsByOperator[ InRule::operator ])) { |
557
|
3 |
|
$possibilities = reset($operandsByOperator[ InRule::operator ])->getPossibilities(); |
558
|
|
|
|
559
|
3 |
|
if (in_array($equalRule->getValue(), $possibilities)) { |
560
|
3 |
|
unset($operandsByOperator[ InRule::operator ]); |
561
|
3 |
|
} |
562
|
|
|
else { |
563
|
|
|
// We flush possibilities of the InRule |
564
|
|
|
// TODO Replace it by a FalseRule |
565
|
1 |
|
$operandsByOperator[ InRule::operator ][0]->setPossibilities([]); |
566
|
|
|
// and also remove the equal rule to shorten the reste of the simplification process |
567
|
1 |
|
unset($operandsByOperator[ EqualRule::operator ]); |
568
|
|
|
} |
569
|
3 |
|
} |
570
|
|
|
|
571
|
66 |
|
if ( ! empty($operandsByOperator[ NotInRule::operator ])) { |
572
|
2 |
|
$notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
573
|
2 |
|
if (in_array($equalRule->getValue(), $notInRule->getPossibilities())) { |
574
|
|
|
// ['field', '=', 4] && ['field', '!in', [4]...] <=> false |
575
|
2 |
|
return []; |
576
|
|
|
} |
577
|
|
|
else { |
578
|
1 |
|
unset($operandsByOperator[ NotInRule::operator ]); |
579
|
|
|
} |
580
|
|
|
// $notInRule->dump(true); |
581
|
1 |
|
} |
582
|
|
|
|
583
|
66 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowOrEqualRule::operator ])) { |
|
|
|
|
584
|
1 |
|
$belowOrEqualRule = reset($operandsByOperator[ BelowOrEqualRule::operator ]); |
585
|
1 |
|
if ($equalRule->getValue() <= $belowOrEqualRule->getMaximum()) { |
586
|
1 |
|
unset($operandsByOperator[ BelowOrEqualRule::operator ]); |
587
|
1 |
|
} |
588
|
|
|
else { |
589
|
|
|
// ['field', '=', 4] && ['field', '<=', [3]...] <=> false |
590
|
|
|
return []; |
591
|
|
|
} |
592
|
1 |
|
} |
593
|
|
|
|
594
|
66 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveOrEqualRule::operator ])) { |
|
|
|
|
595
|
1 |
|
$aboveOrEqualRule = reset($operandsByOperator[ AboveOrEqualRule::operator ]); |
596
|
1 |
|
if ($equalRule->getValue() >= $aboveOrEqualRule->getMinimum()) { |
597
|
1 |
|
unset($operandsByOperator[ AboveOrEqualRule::operator ]); |
598
|
1 |
|
} |
599
|
|
|
else { |
600
|
|
|
// ['field', '=', 4] && ['field', '<=', [3]...] <=> false |
601
|
|
|
return []; |
602
|
|
|
} |
603
|
1 |
|
} |
604
|
66 |
|
} |
605
|
|
|
|
606
|
|
|
// NotEqualRule null comparisons |
607
|
95 |
|
if ( ! empty($operandsByOperator[ NotEqualRule::operator ])) { |
608
|
18 |
|
if ( ! empty($operandsByOperator[ NotEqualRule::operator ])) { |
609
|
18 |
|
foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $notEqualRule) { |
610
|
18 |
|
if (null === $notEqualRule->getValue()) { |
611
|
8 |
|
if ( ! empty($operandsByOperator[ AboveRule::operator ])) { |
612
|
2 |
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
613
|
2 |
|
} |
614
|
|
|
|
615
|
8 |
|
if ( ! empty($operandsByOperator[ BelowRule::operator ])) { |
616
|
2 |
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
617
|
2 |
|
} |
618
|
|
|
|
619
|
8 |
|
if ( ! empty($operandsByOperator[ EqualRule::operator ])) { |
620
|
1 |
|
if (null !== reset($operandsByOperator[ EqualRule::operator ])->getValue()) { |
621
|
|
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
622
|
|
|
} |
623
|
1 |
|
} |
624
|
8 |
|
} |
625
|
|
|
else { |
626
|
10 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ])) { |
|
|
|
|
627
|
2 |
|
if ($operandsByOperator[ AboveRule::operator ][0]->getMinimum() >= $notEqualRule->getValue()) { |
628
|
2 |
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
629
|
2 |
|
} |
630
|
2 |
|
} |
631
|
|
|
|
632
|
10 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowRule::operator ])) { |
|
|
|
|
633
|
|
|
if ($operandsByOperator[ BelowRule::operator ][0]->getMaximum() <= $notEqualRule->getValue()) { |
634
|
|
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
635
|
|
|
} |
636
|
|
|
} |
637
|
|
|
} |
638
|
|
|
|
639
|
18 |
|
if ( ! empty($operandsByOperator[ NotInRule::operator ])) { |
640
|
2 |
|
$notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
641
|
2 |
|
if ( ! in_array($notEqualRule->getValue(), $notInRule->getPossibilities())) { |
642
|
|
|
// TODO Replace it by a FalseRule |
643
|
2 |
|
$operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
644
|
2 |
|
array_merge($notInRule->getPossibilities(), [$notEqualRule->getValue()]) |
645
|
2 |
|
); |
646
|
2 |
|
} |
647
|
|
|
|
648
|
2 |
|
unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
649
|
2 |
|
} |
650
|
|
|
|
651
|
18 |
|
if ( ! empty($operandsByOperator[ InRule::operator ])) { |
652
|
|
|
$inRule = reset($operandsByOperator[ InRule::operator ]); |
653
|
|
|
|
654
|
|
|
$operandsByOperator[ InRule::operator ][0]->setPossibilities( |
655
|
|
|
array_diff($inRule->getPossibilities(), [$notEqualRule->getValue()]) |
656
|
|
|
); |
657
|
|
|
} |
658
|
18 |
|
} |
659
|
18 |
|
} |
660
|
18 |
|
} |
661
|
|
|
|
662
|
|
|
// Comparison between InRules and NotInRules |
663
|
|
|
// This is an optimization to avoid NotIn explosion |
664
|
95 |
|
if ( ! empty($operandsByOperator[ InRule::operator ])) { |
665
|
18 |
|
$inRule = $operandsByOperator[ InRule::operator ][0]; |
666
|
|
|
|
667
|
18 |
View Code Duplication |
if ( ! empty($operandsByOperator[ NotInRule::operator ])) { |
|
|
|
|
668
|
2 |
|
$notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
669
|
2 |
|
$operandsByOperator[ InRule::operator ][0]->setPossibilities( |
670
|
2 |
|
array_diff( $inRule->getPossibilities(), $notInRule->getPossibilities()) |
671
|
2 |
|
); |
672
|
2 |
|
unset($operandsByOperator[ NotInRule::operator ]); |
673
|
2 |
|
} |
674
|
|
|
|
675
|
18 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowRule::operator ])) { |
|
|
|
|
676
|
2 |
|
$upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getMaximum(); |
677
|
|
|
|
678
|
2 |
|
$operandsByOperator[ InRule::operator ][0]->setPossibilities( |
679
|
|
|
array_filter( $inRule->getPossibilities(), function ($possibility) use ($upper_limit) { |
680
|
2 |
|
return $possibility < $upper_limit; |
681
|
2 |
|
} ) |
682
|
2 |
|
); |
683
|
|
|
|
684
|
2 |
|
unset($operandsByOperator[ BelowRule::operator ]); |
685
|
2 |
|
} |
686
|
|
|
|
687
|
18 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ])) { |
|
|
|
|
688
|
1 |
|
$lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum(); |
689
|
|
|
|
690
|
1 |
|
$operandsByOperator[ InRule::operator ][0]->setPossibilities( |
691
|
|
|
array_filter( $inRule->getPossibilities(), function ($possibility) use ($lower_limit) { |
692
|
1 |
|
return $possibility > $lower_limit; |
693
|
1 |
|
} ) |
694
|
1 |
|
); |
695
|
|
|
|
696
|
1 |
|
unset($operandsByOperator[ AboveRule::operator ]); |
697
|
1 |
|
} |
698
|
18 |
|
} |
699
|
|
|
|
700
|
|
|
// Comparison between NotInRules and > or < |
701
|
95 |
|
if ( ! empty($operandsByOperator[ NotInRule::operator ])) { |
702
|
11 |
|
$notInRule = $operandsByOperator[ NotInRule::operator ][0]; |
703
|
|
|
|
704
|
11 |
View Code Duplication |
if ( ! empty($operandsByOperator[ BelowRule::operator ])) { |
|
|
|
|
705
|
1 |
|
$upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit(); |
706
|
|
|
|
707
|
1 |
|
$operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
708
|
|
|
array_filter( $notInRule->getPossibilities(), function ($possibility) use ($upper_limit) { |
709
|
1 |
|
return $possibility < $upper_limit; |
710
|
1 |
|
} ) |
711
|
1 |
|
); |
712
|
1 |
|
} |
713
|
|
|
|
714
|
11 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ])) { |
|
|
|
|
715
|
|
|
$lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum(); |
716
|
|
|
|
717
|
|
|
$operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
718
|
|
|
array_filter( $notInRule->getPossibilities(), function ($possibility) use ($lower_limit) { |
719
|
|
|
return $possibility > $lower_limit; |
720
|
|
|
} ) |
721
|
|
|
); |
722
|
|
|
} |
723
|
11 |
|
} |
724
|
|
|
|
725
|
|
|
// Comparison between <= and > or < |
726
|
95 |
|
if ( ! empty($operandsByOperator[ BelowOrEqualRule::operator ])) { |
727
|
5 |
|
$belowOrEqualRule = $operandsByOperator[ BelowOrEqualRule::operator ][0]; |
728
|
|
|
|
729
|
5 |
|
if ( ! empty($operandsByOperator[ BelowRule::operator ])) { |
730
|
|
|
$upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit(); |
731
|
|
|
|
732
|
|
|
if ($belowOrEqualRule->getMaximum() >= $upper_limit) { |
733
|
|
|
// [field < 3] && [field <= 3] |
734
|
|
|
// [field < 3] && [field <= 4] |
735
|
|
|
unset($operandsByOperator[ BelowOrEqualRule::operator ][0]); |
736
|
|
|
} |
737
|
|
|
else { |
738
|
|
|
// [field < 3] && [field <= 2] |
739
|
|
|
unset($operandsByOperator[ BelowRule::operator ][0]); |
740
|
|
|
} |
741
|
|
|
} |
742
|
|
|
|
743
|
5 |
View Code Duplication |
if ( ! empty($operandsByOperator[ AboveRule::operator ])) { |
|
|
|
|
744
|
1 |
|
$lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getLowerLimit(); |
745
|
|
|
|
746
|
1 |
|
if ($belowOrEqualRule->getMaximum() <= $lower_limit) { |
747
|
|
|
// [field > 3] && [field <= 2] <=> false |
748
|
|
|
return []; |
749
|
|
|
} |
750
|
1 |
|
} |
751
|
|
|
|
752
|
5 |
|
if ( ! empty($operandsByOperator[ AboveOrEqualRule::operator ])) { |
753
|
2 |
|
$minimum = reset($operandsByOperator[ AboveOrEqualRule::operator ])->getMinimum(); |
754
|
|
|
|
755
|
2 |
|
if ($belowOrEqualRule->getMaximum() < $minimum) { |
756
|
|
|
// [field <= 3] && [field >= 4] <=> false |
757
|
|
|
return []; |
758
|
|
|
} |
759
|
2 |
|
elseif ($belowOrEqualRule->getMaximum() == $minimum) { |
760
|
|
|
// [field <= 3] && [field >= 3] <=> [field = 3] |
761
|
1 |
|
unset($operandsByOperator[ BelowOrEqualRule::operator ]); |
762
|
1 |
|
unset($operandsByOperator[ AboveOrEqualRule::operator ]); |
763
|
1 |
|
$operandsByOperator[ EqualRule::operator ][] = new EqualRule($field, $minimum); |
764
|
|
|
|
765
|
1 |
|
if (count($operandsByOperator[ EqualRule::operator ]) > 1) { |
766
|
|
|
$operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator); |
767
|
|
|
} |
768
|
1 |
|
} |
769
|
2 |
|
} |
770
|
5 |
|
} |
771
|
|
|
|
772
|
95 |
|
return $operandsByOperator; |
773
|
|
|
} |
774
|
|
|
|
775
|
|
|
/** |
776
|
|
|
* This method is meant to be used during simplification that would |
777
|
|
|
* need to change the class of the current instance by a normal one. |
778
|
|
|
* |
779
|
|
|
* @return AndRule The current instance (of or or subclass) or a new AndRule |
780
|
|
|
*/ |
781
|
101 |
|
public function setOperandsOrReplaceByOperation($new_operands) |
782
|
|
|
{ |
783
|
|
|
try { |
784
|
101 |
|
return $this->setOperands( $new_operands ); |
785
|
|
|
} |
786
|
|
|
catch (\LogicException $e) { |
787
|
|
|
return new AndRule( $new_operands ); |
788
|
|
|
} |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
/**/ |
792
|
|
|
} |
793
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.