TRational::float2rational()   F
last analyzed

Complexity

Conditions 28
Paths 137

Size

Total Lines 62
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 28
eloc 52
c 1
b 0
f 0
nc 137
nop 3
dl 0
loc 62
rs 3.8583

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TRational class file
5
 *
6
 * @author Brad Anderson <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Util\Math;
12
13
use Prado\Exceptions\TInvalidDataValueException;
14
use Prado\Util\Helpers\TBitHelper;
15
16
/**
17
 * TRational class.
18
 *
19
 * TRational implements a fraction in the form of one integer {@see getNumerator
20
 * Numerator} divided by another integer {@see getDenominator Denominator}.
21
 *
22
 * The class can be {@see __construct initialized} or its {@see setValue Value}
23
 * set as a string, a float value, or an array.  A string in the format
24
 * `$numerator . '/' . $denominator`, eg. "21/13", will set the both the numerator
25
 * and denominator.  A string that is simply a numeric will be interpreted as a
26
 * float value.  An array in the format of `[$numerator, $denominator]` can be used
27
 * to set the numerator and denominator as well.
28
 *
29
 * Setting Float values are processed through a Continued Fraction function to a
30
 * specified tolerance to calculate the integer numerator and integer denominator.
31
 * INF is "-1/0" and NAN (Not A Number) has the denominator equal zero (to avoid a
32
 * divide by zero error).
33
 *
34
 * The numerator and denominator can be accessed by {@see getNumerator} and {@see
35
 * getDenominator}, respectively.  These values can be accessed by array as well,
36
 * where the numerator is mapped to `[0]` and `['numerator']` and the denominator is
37
 * mapped to `[1]` and `['denominator']`.  By setting `[]` the value can be set
38
 * and numerator and denominator computed.  By getting `[null]` the value may be
39
 * retrieved.  Setting the value with a specific tolerance requires {@see setValue}.
40
 *
41
 * TRational implements {@see __toString} and outputs a string of `$numerator . '/'
42
 * . $denominator`, the string format for rationals.  eg.  "13/8".
43
 * ```php
44
 *		$rational = new TRational(1.618033988749895);
45
 *		$value = $rational->getValue();
46
 *		$value = $rational[null];
47
 *		$numerator = $rational->getNumerator();
48
 *		$numerator = $rational[0];
49
 *		$denominator = $rational->getDenominator();
50
 *		$denominator = $rational[1];
51
 *		$rational[] = 1.5;
52
 *		$rational[0] === 3;
53
 *		$rational[1] === 2;
54
 *		$rational[null] = '21/13';
55
 *		$rational[0] === 21;
56
 *		$rational[1] === 13;
57
 * ```
58
 *
59
 * The Rational data format is used by EXIF and, in particular, the GPS Image File
60
 * Directory of EXIF.
61
 *
62
 * @author Brad Anderson <[email protected]>
63
 * @since 4.3.0
64
 * @see https://en.wikipedia.org/wiki/Continued_fraction
65
 */
66
class TRational implements \ArrayAccess
67
{
68
	public const NUMERATOR = 'numerator';
69
70
	public const DENOMINATOR = 'denominator';
71
72
	/* The Default Tolerance when null is provided for a tolerance */
73
	public const DEFAULT_TOLERANCE = 1.0e-6;
74
75
	/** @var float|int The numerator of the rational number. default 0. */
76
	protected $_numerator = 0;
77
78
	/** @var float|int The denominator of the rational number. default 1. */
79
	protected $_denominator = 1;
80
81
	/**
82
	 * @return bool Is the class unsigned. Returns false.
83
	 */
84
	public static function getIsUnsigned(): bool
85
	{
86
		return false;
87
	}
88
89
	/**
90
	 * This initializes a TRational with no value [null], a float that gets deconstructed
91
	 * into a numerator and denominator, a string with the numerator and denominator
92
	 * with a '/' between them, an array with [0 => $numerator, 1 => $denominator],
93
	 * or both the numerator and denominator as two parameters.
94
	 * @param null|array|false|float|int|string $numerator Null or false as nothing,
95
	 *   int and float as values, array of numerator and denominator.
96
	 * @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...
97
	 *   $numerator is a value to be deconstructed.
98
	 */
99
	public function __construct($numerator = null, $denominator = null)
100
	{
101
		if ($numerator !== null && $numerator !== false) {
102
			if ($denominator === null || $denominator === false) {
103
				$this->setValue($numerator);
104
			} else {
105
				$this->setValue([$numerator, $denominator]);
106
			}
107
		}
108
	}
109
110
	/**
111
	 * @return float|int The numerator.
112
	 */
113
	public function getNumerator()
114
	{
115
		return $this->_numerator;
116
	}
117
118
	/**
119
	 * @param float|int|string $value The numerator.
120
	 * @return TRational Returns $this.
121
	 */
122
	public function setNumerator($value): TRational
123
	{
124
		$this->_numerator = (int) min(max(TBitHelper::PHP_INT32_MIN, $value), TBitHelper::PHP_INT32_MAX);
125
		return $this;
126
	}
127
128
	/**
129
	 * Unless specifically set, the denominator usually only has a positive value.
130
	 * @return float|int The denominator.
131
	 */
132
	public function getDenominator()
133
	{
134
		return $this->_denominator;
135
	}
136
137
	/**
138
	 * @param float|int|string $value The denominator.
139
	 * @return TRational Returns $this.
140
	 */
141
	public function setDenominator($value): TRational
142
	{
143
		$this->_denominator = (int) min(max(TBitHelper::PHP_INT32_MIN, $value), TBitHelper::PHP_INT32_MAX);
144
		return $this;
145
	}
146
147
	/**
148
	 * This returns the float value of the Numerator divided by the denominator.
149
	 * Returns INF (Infinity) float value if the {@see getNumerator Numerator} is
150
	 * 0xFFFFFFFF (-1) and {@see getDenominator Denominator} is 0.   Returns NAN
151
	 * (Not A Number) float value if the {@see getDenominator Denominator} is zero.
152
	 * @return float The float value of the Numerator divided by denominator.
153
	 */
154
	public function getValue(): float
155
	{
156
		if ($this->_numerator === -1 && $this->_denominator === 0) {
157
			return INF;
158
		}
159
		if ($this->_denominator === 0) {
160
			return NAN;
161
		}
162
		return ((float) $this->_numerator) / ((float) $this->_denominator);
163
	}
164
165
	/**
166
	 * When setting a float value, this computes the numerator and denominator from
167
	 * the Continued Fraction mathematical computation to a specific $tolerance.
168
	 * @param array|numeric|string $value The numeric to compute the int numerator and
169
	 *   denominator, or a string in the format numerator - '/' character - and then
170
	 *   the denominator; eg. '511/333'. or an array of [numerator, denominator].
171
	 * @param ?float $tolerance the tolerance to compute the numerator and denominator
172
	 *   from the numeric $value.  Default null for "1.0e-6".
173
	 * @return TRational Returns $this.
174
	 */
175
	public function setValue($value, ?float $tolerance = null): TRational
176
	{
177
		$numerator = $denominator = null;
178
		if (is_array($value)) {
179
			if (array_key_exists(0, $value)) {
180
				$numerator = $value[0];
181
			} elseif (array_key_exists(self::NUMERATOR, $value)) {
182
				$numerator = $value[self::NUMERATOR];
183
			} else {
184
				$numerator = 0;
185
			}
186
			if (array_key_exists(1, $value)) {
187
				$denominator = $value[1];
188
			} elseif (array_key_exists(self::DENOMINATOR, $value)) {
189
				$denominator = $value[self::DENOMINATOR];
190
			} else {
191
				$denominator = null;
192
			}
193
			if ($denominator === null) {
194
				$value = $numerator;
195
				$numerator = null;
196
			}
197
		} elseif (is_string($value) && strpos($value, '/') !== false) {
198
			[$numerator, $denominator] = explode('/', $value, 2);
199
		}
200
		$unsigned = $this->getIsUnsigned();
201
		if ($numerator !== null) {
202
			$numerator = (float) $numerator;
203
			$denominator = (float) $denominator;
204
			if ($unsigned) {
0 ignored issues
show
introduced by
The condition $unsigned is always false.
Loading history...
205
				$negNum = $numerator < 0;
206
				$negDen = $denominator < 0;
207
				if ($negNum && $negDen) {
208
					$numerator = -$numerator;
209
					$denominator = -$denominator;
210
				} elseif ($negNum ^ $negDen) {
211
					$numerator = 0;
212
					$denominator = 1;
213
				}
214
			}
215
			$max = $unsigned ? TBitHelper::PHP_INT32_UMAX : TBitHelper::PHP_INT32_MAX;
0 ignored issues
show
introduced by
The condition $unsigned is always false.
Loading history...
216
			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...
217
				$value = ($denominator === 0) ? NAN : $numerator / $denominator;
0 ignored issues
show
introduced by
The condition $denominator === 0 is always false.
Loading history...
218
			} else {
219
				$this->setNumerator($numerator);
220
				$this->setDenominator($denominator);
221
				return $this;
222
			}
223
		}
224
		if ($value !== null) {
225
			[$this->_numerator, $this->_denominator] = self::float2rational((float) $value, $tolerance, $unsigned);
226
		}
227
		return $this;
228
	}
229
230
	/**
231
	 * @return string Returns a string of the Numerator - '/' character - and then the
232
	 *   denominator.  eg. "13/8"
233
	 */
234
	public function __toString(): string
235
	{
236
		$n = $this->_numerator;
237
		if (is_float($n)) {
238
			$n = number_format($n, 0, '.', '');
239
		}
240
		$d = $this->_denominator;
241
		if (is_float($d)) {
242
			$d = number_format($d, 0, '.', '');
243
		}
244
		return $n . '/' . $d;
245
	}
246
247
	/**
248
	 * @return array returns an array of [$numerator, $denominator]
249
	 */
250
	public function toArray()
251
	{
252
		return [$this->_numerator, $this->_denominator];
253
	}
254
255
	/**
256
	 * Checks for the existence of the values TRational uses: 0, 1, 'numerator', and
257
	 * 'denominator'.
258
	 * @param mixed $offset The numerator or denominator of the TRational.
259
	 * @return bool Does the property exist for the TRational.
260
	 */
261
	public function offsetExists(mixed $offset): bool
262
	{
263
		if ($offset === null || is_numeric($offset) && ($offset == 0 || $offset == 1) || is_string($offset) && ($offset === self::NUMERATOR || $offset === self::DENOMINATOR)) {
264
			return true;
265
		}
266
		return false;
267
	}
268
269
	/**
270
	 * This is a convenience method for getting the numerator and denominator.
271
	 * Index '0' and 'numerator' will get the {@see getNumerator Numerator}, and
272
	 * Index '1' and 'denominator' will get the {@see getDenominator Denominator}.
273
	 * @param mixed $offset Which property of the Rational to retrieve.
274
	 * @throws TInvalidDataValueException When $offset is not a property of the Rational.
275
	 * @return mixed The numerator or denominator.
276
	 */
277
	public function offsetGet(mixed $offset): mixed
278
	{
279
		if (is_numeric($offset)) {
280
			if ($offset == 0) {
281
				return $this->getNumerator();
282
			} elseif ($offset == 1) {
283
				return $this->getDenominator();
284
			}
285
		} elseif (is_string($offset)) {
286
			if ($offset == self::NUMERATOR) {
287
				return $this->getNumerator();
288
			} elseif ($offset == self::DENOMINATOR) {
289
				return $this->getDenominator();
290
			}
291
		} elseif ($offset === null) {
292
			return $this->getValue();
293
		}
294
		throw new TInvalidDataValueException('rational_bad_offset', $offset);
295
	}
296
297
	/**
298
	 * This is a convenience method for setting the numerator and denominator.
299
	 * Index '0' and 'numerator' will set the {@see setNumerator Numerator}, and
300
	 * Index '1' and 'denominator' will set the {@see setDenominator Denominator}.
301
	 * @param mixed $offset Which property to set.
302
	 * @param mixed $value The numerator or denominator.
303
	 * @throws TInvalidDataValueException When $offset is not a property of the Rational.
304
	 */
305
	public function offsetSet(mixed $offset, mixed $value): void
306
	{
307
		if (is_numeric($offset)) {
308
			if ($offset == 0) {
309
				$this->setNumerator($value);
310
				return;
311
			} elseif ($offset == 1) {
312
				$this->setDenominator($value);
313
				return;
314
			}
315
		} elseif (is_string($offset)) {
316
			if ($offset == self::NUMERATOR) {
317
				$this->setNumerator($value);
318
				return;
319
			} elseif ($offset == self::DENOMINATOR) {
320
				$this->setDenominator($value);
321
				return;
322
			}
323
		} elseif ($offset === null) {
324
			$this->setValue($value);
325
			return;
326
		}
327
		throw new TInvalidDataValueException('rational_bad_offset', $offset);
328
	}
329
330
	/**
331
	 * This is a convenience method for resetting the numerator and denominator to
332
	 * default.  Index '0' and 'numerator' will reset the {@see setNumerator Numerator}
333
	 * to "0", and Index '1' and 'denominator' will reset the {@see setDenominator
334
	 * Denominator} to "1".
335
	 * @param mixed $offset Which property to reset.
336
	 * @throws TInvalidDataValueException When $offset is not a property of the Rational.
337
	 */
338
	public function offsetUnset(mixed $offset): void
339
	{
340
		if (is_numeric($offset)) {
341
			if ($offset == 0) {
342
				$this->setNumerator(0);
343
				return;
344
			} elseif ($offset == 1) {
345
				$this->setDenominator(1);
346
				return;
347
			}
348
		} elseif (is_string($offset)) {
349
			if ($offset == self::NUMERATOR) {
350
				$this->setNumerator(0);
351
				return;
352
			} elseif ($offset == self::DENOMINATOR) {
353
				$this->setDenominator(1);
354
				return;
355
			}
356
		} elseif ($offset === null) {
357
			$this->setNumerator(0);
358
			$this->setDenominator(1);
359
			return;
360
		}
361
		throw new TInvalidDataValueException('rational_bad_offset', $offset);
362
	}
363
364
	/**
365
	 * This uses the Continued Fraction to make a float into a fraction of two integers.
366
	 * 	- Given INF, this returns [0xFFFFFFFF, 0].
367
	 *  - Given NAN, this returns [0, 0].
368
	 *  - Given 0 or values proximal to 0, this returns [0, 1].
369
	 * Only the numerator can go negative if the $value is negative.
370
	 * @param float $value The float value to deconstruct into a fraction of two integers.
371
	 * @param float $tolerance How precise does the continued fraction need to be to break.  Default 1.e-6
372
	 * @param ?bool $unsigned Is the result an unsigned 32 bit int (vs signed 32 bit int), default false
373
	 * @return array An array of numerator at [0] and denominator at [1].
374
	 * @see https://en.wikipedia.org/wiki/Continued_fraction
375
	 */
376
	public static function float2rational(float $value, ?float $tolerance = null, ?bool $unsigned = false): array
377
	{
378
		if (is_infinite($value)) {
379
			return [$unsigned ? TBitHelper::PHP_INT32_UMAX : -1, 0];
380
		}
381
		if (is_nan($value)) {
382
			return [0, 0];
383
		}
384
		if (($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...
385
			return [0, 1];
386
		}
387
		if ($unsigned) {
388
			if ($value > TBitHelper::PHP_INT32_UMAX) {
389
				return [TBitHelper::PHP_INT32_UMAX, 1];
390
			} elseif ($value < 1.5 / TBitHelper::PHP_INT32_UMAX) {
391
				return [1, TBitHelper::PHP_INT32_UMAX];
392
			}
393
		} else {
394
			if ($value > TBitHelper::PHP_INT32_MAX) {
395
				return [TBitHelper::PHP_INT32_MAX, 1];
396
			} elseif ($value < TBitHelper::PHP_INT32_MIN) {
397
				return [TBitHelper::PHP_INT32_MIN, 1];
398
			} elseif (abs($value) < 1.5 / TBitHelper::PHP_INT32_MAX) {
399
				return [1, TBitHelper::PHP_INT32_MAX];
400
			}
401
		}
402
		if ($tolerance === null) {
403
			$tolerance = self::DEFAULT_TOLERANCE;
404
		}
405
		$sign = $value < 0 ? -1 : 1;
406
		$offset = $value < 0 ? 1.0 : 0.0; // Negative values go to +1 max over positive max.
407
		$value = abs($value);
408
		$h = 1.0;
409
		$lh = 0.0;
410
		$k = 0.0;
411
		$lk = 1.0;
412
		$b = 1.0 / $value;
413
		$tolerance *= $value;
414
		do {
415
			$b = 1.0 / $b;
416
			$a = floor($b);
417
			$tmp = $h;
418
			$h = $a * $h + $lh;
419
			$lh = $tmp;
420
			$tmp = $k;
421
			$k = $a * $k + $lk;
422
			$lk = $tmp;
423
			if ($h > ($unsigned ? TBitHelper::PHP_INT32_UMAX - 1 : (TBitHelper::PHP_INT32_MAX + $offset)) || $k > ($unsigned ? TBitHelper::PHP_INT32_UMAX : TBitHelper::PHP_INT32_MAX)) {
424
				$h = $lh;
425
				$k = $lk;
426
				break;
427
			}
428
			$b = $b - $a;
429
		} while ($b !== 0.0 && abs($value - $h / $k) > $tolerance);
430
		if (PHP_INT_SIZE > 4 || $h <= PHP_INT_MAX + $offset && $k <= PHP_INT_MAX) {
431
			return [$sign * ((int) $h), ((int) $k)];
432
		} elseif ($h <= PHP_INT_MAX + $offset) {
433
			return [$sign * ((int) $h), $k];
434
		} elseif ($k <= PHP_INT_MAX) {
435
			return [$sign * $h, ((int) $k)];
436
		} else {
437
			return [$sign * $h, $k];
438
		}
439
	}
440
}
441