Passed
Push — master ( 223051...44a4ce )
by Fabio
06:19 queued 01:38
created

TRational   F

Complexity

Total Complexity 99

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 99
eloc 174
c 1
b 0
f 0
dl 0
loc 368
rs 2

15 Methods

Rating   Name   Duplication   Size   Complexity  
B offsetGet() 0 18 8
A __construct() 0 7 5
A getValue() 0 9 4
A setDenominator() 0 4 1
F setValue() 0 53 22
A __toString() 0 11 3
A setNumerator() 0 4 1
A getNumerator() 0 3 1
B offsetUnset() 0 20 7
B offsetSet() 0 23 8
A getDenominator() 0 3 1
B offsetExists() 0 6 7
A toArray() 0 3 1
A getIsUnsigned() 0 3 1
F float2rational() 0 62 29

How to fix   Complexity   

Complex Class

Complex classes like TRational often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TRational, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * TRational class file
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Util\Math;
11
12
use Prado\Exceptions\TInvalidDataValueException;
13
use Prado\Util\TBitHelper;
14
15
/**
16
 * TRational class.
17
 *
18
 * TRational implements a fraction in the form of one integer {@link getNumerator
19
 * Numerator} divided by another integer {@link getDenominator Denominator}.
20
 *
21
 * The class can be {@link __construct initialized} or its {@link setValue Value}
22
 * set as a string, a float value, or an array.  A string in the format
23
 * `$numerator . '/' . $denominator`, eg. "21/13", will set the both the numerator
24
 * and denominator.  A string that is simply a numeric will be interpreted as a
25
 * float value.  An array in the format of `[$numerator, $denominator]` can be used
26
 * to set the numerator and denominator as well.
27
 *
28
 * Setting Float values are processed through a Continued Fraction function to a
29
 * specified tolerance to calculate the integer numerator and integer denominator.
30
 * INF is "-1/0" and NAN (Not A Number) has the denominator equal zero (to avoid a
31
 * divide by zero error).
32
 *
33
 * The numerator and denominator can be accessed by {@link getNumerator} and {@link
34
 * getDenominator}, respectively.  These values can be accessed by array as well,
35
 * where the numerator is mapped to `[0]` and `['numerator']` and the denominator is
36
 * mapped to `[1]` and `['denominator']`.  By setting `[]` the value can be set
37
 * and numerator and denominator computed.  By getting `[null]` the value may be
38
 * retrieved.  Setting the value with a specific tolerance requires {@link setValue}.
39
 *
40
 * TRational implements {@link __toString} and outputs a string of `$numerator . '/'
41
 * . $denominator`, the string format for rationals.  eg.  "13/8".
42
 * <code>
43
 *		$rational = new TRational(1.618033988749895);
44
 *		$value = $rational->getValue();
45
 *		$value = $rational[null];
46
 *		$numerator = $rational->getNumerator();
47
 *		$numerator = $rational[0];
48
 *		$denominator = $rational->getDenominator();
49
 *		$denominator = $rational[1];
50
 *		$rational[] = 1.5;
51
 *		$rational[0] === 3;
52
 *		$rational[1] === 2;
53
 *		$rational[null] = '21/13';
54
 *		$rational[0] === 21;
55
 *		$rational[1] === 13;
56
 * </code>
57
 *
58
 * The Rational data format is used by EXIF and, in particular, the GPS Image File
59
 * Directory of EXIF.
60
 *
61
 * @author Brad Anderson <[email protected]>
62
 * @since 4.2.3
63
 * @see https://en.wikipedia.org/wiki/Continued_fraction
64
 */
65
class TRational implements \ArrayAccess
66
{
67
	public const NUMERATOR = 'numerator';
68
69
	public const DENOMINATOR = 'denominator';
70
71
	/* The Default Tolerance when null is provided for a tolerance */
72
	public const DEFAULT_TOLERANCE = 1.0e-6;
73
74
	/** @var float|int The numerator of the rational number. default 0. */
75
	protected $_numerator = 0;
76
77
	/** @var float|int The denominator of the rational number. default 1. */
78
	protected $_denominator = 1;
79
80
	/**
81
	 * @return bool Is the class unsigned. Returns false.
82
	 */
83
	public static function getIsUnsigned(): bool
84
	{
85
		return false;
86
	}
87
88
	/**
89
	 * This initializes a TRational with no value [null], a float that gets deconstructed
90
	 * into a numerator and denominator, a string with the numerator and denominator
91
	 * with a '/' between them, an array with [0 => $numerator, 1 => $denominator],
92
	 * or both the numerator and denominator as two parameters.
93
	 * @param null|array|false|float|int|string $numerator Null or false as nothing,
94
	 *   int and float as values, array of numerator and denominator.
95
	 * @param null|false|numeric $denominator The denominator. Default null for the
0 ignored issues
show
Bug introduced by
The type Prado\Util\Math\numeric was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
96
	 *   $numerator is a value to be deconstructed.
97
	 */
98
	public function __construct($numerator = null, $denominator = null)
99
	{
100
		if ($numerator !== null && $numerator !== false) {
101
			if ($denominator === null || $denominator === false) {
102
				$this->setValue($numerator);
103
			} else {
104
				$this->setValue([$numerator, $denominator]);
105
			}
106
		}
107
	}
108
109
	/**
110
	 * @return float|int The numerator.
111
	 */
112
	public function getNumerator()
113
	{
114
		return $this->_numerator;
115
	}
116
117
	/**
118
	 * @param float|int|string $value The numerator.
119
	 * @return TRational Returns $this.
120
	 */
121
	public function setNumerator($value): TRational
122
	{
123
		$this->_numerator = (int) min(max(TBitHelper::PHP_INT32_MIN, $value), TBitHelper::PHP_INT32_MAX);
124
		return $this;
125
	}
126
127
	/**
128
	 * Unless specifically set, the denominator usually only has a positive value.
129
	 * @return float|int The denominator.
130
	 */
131
	public function getDenominator()
132
	{
133
		return $this->_denominator;
134
	}
135
136
	/**
137
	 * @param float|int|string $value The denominator.
138
	 * @return TRational Returns $this.
139
	 */
140
	public function setDenominator($value): TRational
141
	{
142
		$this->_denominator = (int) min(max(TBitHelper::PHP_INT32_MIN, $value), TBitHelper::PHP_INT32_MAX);
143
		return $this;
144
	}
145
146
	/**
147
	 * This returns the float value of the Numerator divided by the denominator.
148
	 * Returns INF (Infinity) float value if the {@link getNumerator Numerator} is
149
	 * 0xFFFFFFFF (-1) and {@link getDenominator Denominator} is 0.   Returns NAN
150
	 * (Not A Number) float value if the {@link getDenominator Denominator} is zero.
151
	 * @return float The float value of the Numerator divided by denominator.
152
	 */
153
	public function getValue(): float
154
	{
155
		if ($this->_numerator === -1 && $this->_denominator === 0) {
156
			return INF;
157
		}
158
		if ($this->_denominator === 0) {
159
			return NAN;
160
		}
161
		return ((float) $this->_numerator) / ((float) $this->_denominator);
162
	}
163
164
	/**
165
	 * When setting a float value, this computes the numerator and denominator from
166
	 * the Continued Fraction mathematical computation to a specific $tolerance.
167
	 * @param array|numeric|string $value The numeric to compute the int numerator and
168
	 *   denominator, or a string in the format numerator - '/' character - and then
169
	 *   the denominator; eg. '511/333'. or an array of [numerator, denominator].
170
	 * @param ?float $tolerance the tolerance to compute the numerator and denominator
171
	 *   from the numeric $value.  Default null for "1.0e-6".
172
	 * @return TRational Returns $this.
173
	 */
174
	public function setValue($value, ?float $tolerance = null): TRational
175
	{
176
		$numerator = $denominator = null;
177
		if (is_array($value)) {
178
			if (array_key_exists(0, $value)) {
179
				$numerator = $value[0];
180
			} elseif (array_key_exists(self::NUMERATOR, $value)) {
181
				$numerator = $value[self::NUMERATOR];
182
			} else {
183
				$numerator = 0;
184
			}
185
			if (array_key_exists(1, $value)) {
186
				$denominator = $value[1];
187
			} elseif (array_key_exists(self::DENOMINATOR, $value)) {
188
				$denominator = $value[self::DENOMINATOR];
189
			} else {
190
				$denominator = null;
191
			}
192
			if ($denominator === null) {
193
				$value = $numerator;
194
				$numerator = null;
195
			}
196
		} elseif (is_string($value) && strpos($value, '/') !== false) {
197
			[$numerator, $denominator] = explode('/', $value, 2);
198
		}
199
		$unsigned = $this->getIsUnsigned();
200
		if ($numerator !== null) {
201
			$numerator = (float) $numerator;
202
			$denominator = (float) $denominator;
203
			if ($unsigned) {
0 ignored issues
show
introduced by
The condition $unsigned is always false.
Loading history...
204
				$negNum = $numerator < 0;
205
				$negDen = $denominator < 0;
206
				if ($negNum && $negDen) {
207
					$numerator = -$numerator;
208
					$denominator = -$denominator;
209
				} elseif ($negNum ^ $negDen) {
210
					$numerator = 0;
211
					$denominator = 1;
212
				}
213
			}
214
			$max = $unsigned ? TBitHelper::PHP_INT32_UMAX : TBitHelper::PHP_INT32_MAX;
0 ignored issues
show
introduced by
The condition $unsigned is always false.
Loading history...
215
			if ($numerator > $max || $denominator > $max || (!$unsigned && ($numerator < TBitHelper::PHP_INT32_MIN || $denominator < TBitHelper::PHP_INT32_MIN))) {
0 ignored issues
show
introduced by
The condition $unsigned is always false.
Loading history...
216
				$value = ($denominator === 0) ? NAN : $numerator / $denominator;
0 ignored issues
show
introduced by
The condition $denominator === 0 is always false.
Loading history...
217
			} else {
218
				$this->setNumerator($numerator);
219
				$this->setDenominator($denominator);
220
				return $this;
221
			}
222
		}
223
		if ($value !== null) {
224
			[$this->_numerator, $this->_denominator] = self::float2rational((float) $value, $tolerance, $unsigned);
225
		}
226
		return $this;
227
	}
228
229
	/**
230
	 * @return string Returns a string of the Numerator - '/' character - and then the
231
	 *   denominator.  eg. "13/8"
232
	 */
233
	public function __toString(): string
234
	{
235
		$n = $this->_numerator;
236
		if (is_float($n)) {
237
			$n = number_format($n, 0, '.', '');
238
		}
239
		$d = $this->_denominator;
240
		if (is_float($d)) {
241
			$d = number_format($d, 0, '.', '');
242
		}
243
		return $n . '/' . $d;
244
	}
245
246
	/**
247
	 * @return array returns an array of [$numerator, $denominator]
248
	 */
249
	public function toArray()
250
	{
251
		return [$this->_numerator, $this->_denominator];
252
	}
253
254
	/**
255
	 * Checks for the existence of the values TRational uses: 0, 1, 'numerator', and
256
	 * 'denominator'.
257
	 * @param mixed $offset The numerator or denominator of the TRational.
258
	 * @return bool Does the property exist for the TRational.
259
	 */
260
	public function offsetExists(mixed $offset): bool
261
	{
262
		if (is_numeric($offset) && ($offset == 0 || $offset == 1) || is_string($offset) && ($offset === self::NUMERATOR || $offset === self::DENOMINATOR)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_numeric($offset) && ...t === self::DENOMINATOR, Probably Intended Meaning: is_numeric($offset) && (... === self::DENOMINATOR)
Loading history...
263
			return true;
264
		}
265
		return false;
266
	}
267
268
	/**
269
	 * This is a convenience method for getting the numerator and denominator.
270
	 * Index '0' and 'numerator' will get the {@link getNumerator Numerator}, and
271
	 * Index '1' and 'denominator' will get the {@link getDenominator Denominator}.
272
	 * @param mixed $offset Which property of the Rational to retrieve.
273
	 * @throws TInvalidDataValueException When $offset is not a property of the Rational.
274
	 * @return mixed The numerator or denominator.
275
	 */
276
	public function offsetGet(mixed $offset): mixed
277
	{
278
		if (is_numeric($offset)) {
279
			if ($offset == 0) {
280
				return $this->getNumerator();
281
			} elseif ($offset == 1) {
282
				return $this->getDenominator();
283
			}
284
		} elseif (is_string($offset)) {
285
			if ($offset == self::NUMERATOR) {
286
				return $this->getNumerator();
287
			} elseif ($offset == self::DENOMINATOR) {
288
				return $this->getDenominator();
289
			}
290
		} elseif ($offset === null) {
291
			return $this->getValue();
292
		}
293
		throw new TInvalidDataValueException('rational_bad_offset', $offset);
294
	}
295
296
	/**
297
	 * This is a convenience method for setting the numerator and denominator.
298
	 * Index '0' and 'numerator' will set the {@link setNumerator Numerator}, and
299
	 * Index '1' and 'denominator' will set the {@link setDenominator Denominator}.
300
	 * @param mixed $offset Which property to set.
301
	 * @param mixed $value The numerator or denominator.
302
	 * @throws TInvalidDataValueException When $offset is not a property of the Rational.
303
	 */
304
	public function offsetSet(mixed $offset, mixed $value): void
305
	{
306
		if (is_numeric($offset)) {
307
			if ($offset == 0) {
308
				$this->setNumerator($value);
309
				return;
310
			} elseif ($offset == 1) {
311
				$this->setDenominator($value);
312
				return;
313
			}
314
		} elseif (is_string($offset)) {
315
			if ($offset == self::NUMERATOR) {
316
				$this->setNumerator($value);
317
				return;
318
			} elseif ($offset == self::DENOMINATOR) {
319
				$this->setDenominator($value);
320
				return;
321
			}
322
		} elseif ($offset === null) {
323
			$this->setValue($value);
324
			return;
325
		}
326
		throw new TInvalidDataValueException('rational_bad_offset', $offset);
327
	}
328
329
	/**
330
	 * This is a convenience method for resetting the numerator and denominator to
331
	 * default.  Index '0' and 'numerator' will reset the {@link setNumerator Numerator}
332
	 * to "0", and Index '1' and 'denominator' will reset the {@link setDenominator
333
	 * Denominator} to "1".
334
	 * @param mixed $offset Which property to reset.
335
	 * @throws TInvalidDataValueException When $offset is not a property of the Rational.
336
	 */
337
	public function offsetUnset(mixed $offset): void
338
	{
339
		if (is_numeric($offset)) {
340
			if ($offset == 0) {
341
				$this->setNumerator(0);
342
				return;
343
			} elseif ($offset == 1) {
344
				$this->setDenominator(1);
345
				return;
346
			}
347
		} elseif (is_string($offset)) {
348
			if ($offset == self::NUMERATOR) {
349
				$this->setNumerator(0);
350
				return;
351
			} elseif ($offset == self::DENOMINATOR) {
352
				$this->setDenominator(1);
353
				return;
354
			}
355
		}
356
		throw new TInvalidDataValueException('rational_bad_offset', $offset);
357
	}
358
359
	/**
360
	 * This uses the Continued Fraction to make a float into a fraction of two integers.
361
	 * 	- Given INF, this returns [0xFFFFFFFF, 0].
362
	 *  - Given NAN, this returns [0, 0].
363
	 *  - Given 0 or values proximal to 0, this returns [0, 1].
364
	 * Only the numerator can go negative if the $value is negative.
365
	 * @param float $value The float value to deconstruct into a fraction of two integers.
366
	 * @param float $tolerance How precise does the continued fraction need to be to break.  Default 1.e-6
367
	 * @param ?bool $unsigned Is the result an unsigned 32 bit int (vs signed 32 bit int), default false
368
	 * @return array An array of numerator at [0] and denominator at [1].
369
	 * @see https://en.wikipedia.org/wiki/Continued_fraction
370
	 */
371
	public static function float2rational(float $value, ?float $tolerance = null, ?bool $unsigned = false): array
372
	{
373
		if (is_infinite($value)) {
374
			return [$unsigned ? TBitHelper::PHP_INT32_UMAX : -1, 0];
375
		}
376
		if (is_nan($value)) {
377
			return [0, 0];
378
		}
379
		if ($value === 0.0 || ($unsigned && $value < 0.5 / TBitHelper::PHP_INT32_UMAX) || (!$unsigned && abs($value) < 0.5 / TBitHelper::PHP_INT32_MAX)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unsigned of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
380
			return [0, 1];
381
		}
382
		if ($unsigned) {
383
			if ($value > TBitHelper::PHP_INT32_UMAX) {
384
				return [TBitHelper::PHP_INT32_UMAX, 1];
385
			} elseif ($value < 1.5 / TBitHelper::PHP_INT32_UMAX) {
386
				return [1, TBitHelper::PHP_INT32_UMAX];
387
			}
388
		} else {
389
			if ($value > TBitHelper::PHP_INT32_MAX) {
390
				return [TBitHelper::PHP_INT32_MAX, 1];
391
			} elseif ($value < TBitHelper::PHP_INT32_MIN) {
392
				return [TBitHelper::PHP_INT32_MIN, 1];
393
			} elseif (abs($value) < 1.5 / TBitHelper::PHP_INT32_MAX) {
394
				return [1, TBitHelper::PHP_INT32_MAX];
395
			}
396
		}
397
		if ($tolerance === null) {
398
			$tolerance = self::DEFAULT_TOLERANCE;
399
		}
400
		$sign = $value < 0 ? -1 : 1;
401
		$offset = $value < 0 ? 1.0 : 0.0; // Negative values go to +1 max over positive max.
402
		$value = abs($value);
403
		$h = 1.0;
404
		$lh = 0.0;
405
		$k = 0.0;
406
		$lk = 1.0;
407
		$b = 1.0 / $value;
408
		$tolerance *= $value;
409
		do {
410
			$b = 1.0 / $b;
411
			$a = floor($b);
412
			$tmp = $h;
413
			$h = $a * $h + $lh;
414
			$lh = $tmp;
415
			$tmp = $k;
416
			$k = $a * $k + $lk;
417
			$lk = $tmp;
418
			if ($h > ($unsigned ? TBitHelper::PHP_INT32_UMAX - 1 : (TBitHelper::PHP_INT32_MAX + $offset)) || $k > ($unsigned ? TBitHelper::PHP_INT32_UMAX : TBitHelper::PHP_INT32_MAX)) {
419
				$h = $lh;
420
				$k = $lk;
421
				break;
422
			}
423
			$b = $b - $a;
424
		} while ($b !== 0.0 && abs($value - $h / $k) > $tolerance);
425
		if (PHP_INT_SIZE > 4 || $h <= PHP_INT_MAX + $offset && $k <= PHP_INT_MAX) {
426
			return [$sign * ((int) $h), ((int) $k)];
427
		} elseif ($h <= PHP_INT_MAX + $offset) {
428
			return [$sign * ((int) $h), $k];
429
		} elseif ($k <= PHP_INT_MAX) {
430
			return [$sign * $h, ((int) $k)];
431
		} else {
432
			return [$sign * $h, $k];
433
		}
434
	}
435
}
436