1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan;
4:
5: use PHPStan\Type\BooleanType;
6: use PHPStan\Type\Constant\ConstantBooleanType;
7: use function array_column;
8: use function max;
9: use function min;
10:
11: /**
12: * @api
13: * @see https://en.wikipedia.org/wiki/Three-valued_logic
14: */
15: class TrinaryLogic
16: {
17:
18: private const YES = 1;
19: private const MAYBE = 0;
20: private const NO = -1;
21:
22: /** @var self[] */
23: private static array $registry = [];
24:
25: private function __construct(private int $value)
26: {
27: }
28:
29: public static function createYes(): self
30: {
31: return self::$registry[self::YES] ??= new self(self::YES);
32: }
33:
34: public static function createNo(): self
35: {
36: return self::$registry[self::NO] ??= new self(self::NO);
37: }
38:
39: public static function createMaybe(): self
40: {
41: return self::$registry[self::MAYBE] ??= new self(self::MAYBE);
42: }
43:
44: public static function createFromBoolean(bool $value): self
45: {
46: $yesNo = $value ? self::YES : self::NO;
47: return self::$registry[$yesNo] ??= new self($yesNo);
48: }
49:
50: private static function create(int $value): self
51: {
52: self::$registry[$value] ??= new self($value);
53: return self::$registry[$value];
54: }
55:
56: public function yes(): bool
57: {
58: return $this->value === self::YES;
59: }
60:
61: public function maybe(): bool
62: {
63: return $this->value === self::MAYBE;
64: }
65:
66: public function no(): bool
67: {
68: return $this->value === self::NO;
69: }
70:
71: public function toBooleanType(): BooleanType
72: {
73: if ($this->value === self::MAYBE) {
74: return new BooleanType();
75: }
76:
77: return new ConstantBooleanType($this->value === self::YES);
78: }
79:
80: public function and(self ...$operands): self
81: {
82: $operandValues = array_column($operands, 'value');
83: $operandValues[] = $this->value;
84: return self::create(min($operandValues));
85: }
86:
87: /**
88: * @template T
89: * @param T[] $objects
90: * @param callable(T): self $callback
91: */
92: public function lazyAnd(
93: array $objects,
94: callable $callback,
95: ): self
96: {
97: $results = [];
98: foreach ($objects as $object) {
99: $result = $callback($object);
100: if ($result->no()) {
101: return $result;
102: }
103:
104: $results[] = $result;
105: }
106:
107: return $this->and(...$results);
108: }
109:
110: public function or(self ...$operands): self
111: {
112: $operandValues = array_column($operands, 'value');
113: $operandValues[] = $this->value;
114: return self::create(max($operandValues));
115: }
116:
117: /**
118: * @template T
119: * @param T[] $objects
120: * @param callable(T): self $callback
121: */
122: public function lazyOr(
123: array $objects,
124: callable $callback,
125: ): self
126: {
127: $results = [];
128: foreach ($objects as $object) {
129: $result = $callback($object);
130: if ($result->yes()) {
131: return $result;
132: }
133:
134: $results[] = $result;
135: }
136:
137: return $this->or(...$results);
138: }
139:
140: public static function extremeIdentity(self ...$operands): self
141: {
142: if ($operands === []) {
143: throw new ShouldNotHappenException();
144: }
145: $operandValues = array_column($operands, 'value');
146: $min = min($operandValues);
147: $max = max($operandValues);
148: return self::create($min === $max ? $min : self::MAYBE);
149: }
150:
151: /**
152: * @template T
153: * @param T[] $objects
154: * @param callable(T): self $callback
155: */
156: public static function lazyExtremeIdentity(
157: array $objects,
158: callable $callback,
159: ): self
160: {
161: $lastResult = null;
162: $results = [];
163: foreach ($objects as $object) {
164: $result = $callback($object);
165: if ($lastResult === null) {
166: $lastResult = $result;
167: $results[] = $result;
168: continue;
169: }
170: if ($lastResult->equals($result)) {
171: $results[] = $result;
172: continue;
173: }
174:
175: return self::createMaybe();
176: }
177:
178: return self::extremeIdentity(...$results);
179: }
180:
181: public static function maxMin(self ...$operands): self
182: {
183: if ($operands === []) {
184: throw new ShouldNotHappenException();
185: }
186: $operandValues = array_column($operands, 'value');
187: return self::create(max($operandValues) > 0 ? 1 : min($operandValues));
188: }
189:
190: /**
191: * @template T
192: * @param T[] $objects
193: * @param callable(T): self $callback
194: */
195: public static function lazyMaxMin(
196: array $objects,
197: callable $callback,
198: ): self
199: {
200: $results = [];
201: foreach ($objects as $object) {
202: $result = $callback($object);
203: if ($result->yes()) {
204: return $result;
205: }
206:
207: $results[] = $result;
208: }
209:
210: return self::maxMin(...$results);
211: }
212:
213: public function negate(): self
214: {
215: return self::create(-$this->value);
216: }
217:
218: public function equals(self $other): bool
219: {
220: return $this === $other;
221: }
222:
223: public function compareTo(self $other): ?self
224: {
225: if ($this->value > $other->value) {
226: return $this;
227: } elseif ($other->value > $this->value) {
228: return $other;
229: }
230:
231: return null;
232: }
233:
234: public function describe(): string
235: {
236: static $labels = [
237: self::NO => 'No',
238: self::MAYBE => 'Maybe',
239: self::YES => 'Yes',
240: ];
241:
242: return $labels[$this->value];
243: }
244:
245: /**
246: * @param mixed[] $properties
247: */
248: public static function __set_state(array $properties): self
249: {
250: return self::create($properties['value']);
251: }
252:
253: }
254: