diff --git a/framework/Exceptions/messages/messages.txt b/framework/Exceptions/messages/messages.txt
index e209c787e..58fa84193 100644
--- a/framework/Exceptions/messages/messages.txt
+++ b/framework/Exceptions/messages/messages.txt
@@ -581,6 +581,8 @@ dbcron_property_unchangeable = TDbCronModule.{0} is already initialized and ca
timescheduler_invalid_string = TTimeScheduler could not parse the schedule element '{0}'
+rational_bad_offset = "{0}" is not a valid property accessor of TRational.
+
bithelper_bad_fp_format = TBitHelper cannot work with '{0}' exponent bits, '{1}' mantissa bits, '{2}' exponent bias, in a {3} bit PHP environment.
bithelper_invalid_color_in = TBitHelper must have at least one In Bit. '{0}' was given.
bithelper_invalid_color_out = TBitHelper must have at least one Out Bit. '{0}' was given.
diff --git a/framework/Util/Math/TRational.php b/framework/Util/Math/TRational.php
new file mode 100644
index 000000000..660935191
--- /dev/null
+++ b/framework/Util/Math/TRational.php
@@ -0,0 +1,435 @@
+
+ * @link https://github.com/pradosoft/prado
+ * @license https://github.com/pradosoft/prado/blob/master/LICENSE
+ */
+
+namespace Prado\Util\Math;
+
+use Prado\Exceptions\TInvalidDataValueException;
+use Prado\Util\TBitHelper;
+
+/**
+ * TRational class.
+ *
+ * TRational implements a fraction in the form of one integer {@link getNumerator
+ * Numerator} divided by another integer {@link getDenominator Denominator}.
+ *
+ * The class can be {@link __construct initialized} or its {@link setValue Value}
+ * set as a string, a float value, or an array. A string in the format
+ * `$numerator . '/' . $denominator`, eg. "21/13", will set the both the numerator
+ * and denominator. A string that is simply a numeric will be interpreted as a
+ * float value. An array in the format of `[$numerator, $denominator]` can be used
+ * to set the numerator and denominator as well.
+ *
+ * Setting Float values are processed through a Continued Fraction function to a
+ * specified tolerance to calculate the integer numerator and integer denominator.
+ * INF is "-1/0" and NAN (Not A Number) has the denominator equal zero (to avoid a
+ * divide by zero error).
+ *
+ * The numerator and denominator can be accessed by {@link getNumerator} and {@link
+ * getDenominator}, respectively. These values can be accessed by array as well,
+ * where the numerator is mapped to `[0]` and `['numerator']` and the denominator is
+ * mapped to `[1]` and `['denominator']`. By setting `[]` the value can be set
+ * and numerator and denominator computed. By getting `[null]` the value may be
+ * retrieved. Setting the value with a specific tolerance requires {@link setValue}.
+ *
+ * TRational implements {@link __toString} and outputs a string of `$numerator . '/'
+ * . $denominator`, the string format for rationals. eg. "13/8".
+ *
+ * $rational = new TRational(1.618033988749895);
+ * $value = $rational->getValue();
+ * $value = $rational[null];
+ * $numerator = $rational->getNumerator();
+ * $numerator = $rational[0];
+ * $denominator = $rational->getDenominator();
+ * $denominator = $rational[1];
+ * $rational[] = 1.5;
+ * $rational[0] === 3;
+ * $rational[1] === 2;
+ * $rational[null] = '21/13';
+ * $rational[0] === 21;
+ * $rational[1] === 13;
+ *
+ *
+ * The Rational data format is used by EXIF and, in particular, the GPS Image File
+ * Directory of EXIF.
+ *
+ * @author Brad Anderson
+ * @since 4.2.3
+ * @see https://en.wikipedia.org/wiki/Continued_fraction
+ */
+class TRational implements \ArrayAccess
+{
+ public const NUMERATOR = 'numerator';
+
+ public const DENOMINATOR = 'denominator';
+
+ /* The Default Tolerance when null is provided for a tolerance */
+ public const DEFAULT_TOLERANCE = 1.0e-6;
+
+ /** @var float|int The numerator of the rational number. default 0. */
+ protected $_numerator = 0;
+
+ /** @var float|int The denominator of the rational number. default 1. */
+ protected $_denominator = 1;
+
+ /**
+ * @return bool Is the class unsigned. Returns false.
+ */
+ public static function getIsUnsigned(): bool
+ {
+ return false;
+ }
+
+ /**
+ * This initializes a TRational with no value [null], a float that gets deconstructed
+ * into a numerator and denominator, a string with the numerator and denominator
+ * with a '/' between them, an array with [0 => $numerator, 1 => $denominator],
+ * or both the numerator and denominator as two parameters.
+ * @param null|array|false|float|int|string $numerator Null or false as nothing,
+ * int and float as values, array of numerator and denominator.
+ * @param null|false|numeric $denominator The denominator. Default null for the
+ * $numerator is a value to be deconstructed.
+ */
+ public function __construct($numerator = null, $denominator = null)
+ {
+ if ($numerator !== null && $numerator !== false) {
+ if ($denominator === null || $denominator === false) {
+ $this->setValue($numerator);
+ } else {
+ $this->setValue([$numerator, $denominator]);
+ }
+ }
+ }
+
+ /**
+ * @return float|int The numerator.
+ */
+ public function getNumerator()
+ {
+ return $this->_numerator;
+ }
+
+ /**
+ * @param float|int|string $value The numerator.
+ * @return TRational Returns $this.
+ */
+ public function setNumerator($value): TRational
+ {
+ $this->_numerator = (int) min(max(TBitHelper::PHP_INT32_MIN, $value), TBitHelper::PHP_INT32_MAX);
+ return $this;
+ }
+
+ /**
+ * Unless specifically set, the denominator usually only has a positive value.
+ * @return float|int The denominator.
+ */
+ public function getDenominator()
+ {
+ return $this->_denominator;
+ }
+
+ /**
+ * @param float|int|string $value The denominator.
+ * @return TRational Returns $this.
+ */
+ public function setDenominator($value): TRational
+ {
+ $this->_denominator = (int) min(max(TBitHelper::PHP_INT32_MIN, $value), TBitHelper::PHP_INT32_MAX);
+ return $this;
+ }
+
+ /**
+ * This returns the float value of the Numerator divided by the denominator.
+ * Returns INF (Infinity) float value if the {@link getNumerator Numerator} is
+ * 0xFFFFFFFF (-1) and {@link getDenominator Denominator} is 0. Returns NAN
+ * (Not A Number) float value if the {@link getDenominator Denominator} is zero.
+ * @return float The float value of the Numerator divided by denominator.
+ */
+ public function getValue(): float
+ {
+ if ($this->_numerator === -1 && $this->_denominator === 0) {
+ return INF;
+ }
+ if ($this->_denominator === 0) {
+ return NAN;
+ }
+ return ((float) $this->_numerator) / ((float) $this->_denominator);
+ }
+
+ /**
+ * When setting a float value, this computes the numerator and denominator from
+ * the Continued Fraction mathematical computation to a specific $tolerance.
+ * @param array|numeric|string $value The numeric to compute the int numerator and
+ * denominator, or a string in the format numerator - '/' character - and then
+ * the denominator; eg. '511/333'. or an array of [numerator, denominator].
+ * @param ?float $tolerance the tolerance to compute the numerator and denominator
+ * from the numeric $value. Default null for "1.0e-6".
+ * @return TRational Returns $this.
+ */
+ public function setValue($value, ?float $tolerance = null): TRational
+ {
+ $numerator = $denominator = null;
+ if (is_array($value)) {
+ if (array_key_exists(0, $value)) {
+ $numerator = $value[0];
+ } elseif (array_key_exists(self::NUMERATOR, $value)) {
+ $numerator = $value[self::NUMERATOR];
+ } else {
+ $numerator = 0;
+ }
+ if (array_key_exists(1, $value)) {
+ $denominator = $value[1];
+ } elseif (array_key_exists(self::DENOMINATOR, $value)) {
+ $denominator = $value[self::DENOMINATOR];
+ } else {
+ $denominator = null;
+ }
+ if ($denominator === null) {
+ $value = $numerator;
+ $numerator = null;
+ }
+ } elseif (is_string($value) && strpos($value, '/') !== false) {
+ [$numerator, $denominator] = explode('/', $value, 2);
+ }
+ $unsigned = $this->getIsUnsigned();
+ if ($numerator !== null) {
+ $numerator = (float) $numerator;
+ $denominator = (float) $denominator;
+ if ($unsigned) {
+ $negNum = $numerator < 0;
+ $negDen = $denominator < 0;
+ if ($negNum && $negDen) {
+ $numerator = -$numerator;
+ $denominator = -$denominator;
+ } elseif ($negNum ^ $negDen) {
+ $numerator = 0;
+ $denominator = 1;
+ }
+ }
+ $max = $unsigned ? TBitHelper::PHP_INT32_UMAX : TBitHelper::PHP_INT32_MAX;
+ if ($numerator > $max || $denominator > $max || (!$unsigned && ($numerator < TBitHelper::PHP_INT32_MIN || $denominator < TBitHelper::PHP_INT32_MIN))) {
+ $value = ($denominator === 0) ? NAN : $numerator / $denominator;
+ } else {
+ $this->setNumerator($numerator);
+ $this->setDenominator($denominator);
+ return $this;
+ }
+ }
+ if ($value !== null) {
+ [$this->_numerator, $this->_denominator] = self::float2rational((float) $value, $tolerance, $unsigned);
+ }
+ return $this;
+ }
+
+ /**
+ * @return string Returns a string of the Numerator - '/' character - and then the
+ * denominator. eg. "13/8"
+ */
+ public function __toString(): string
+ {
+ $n = $this->_numerator;
+ if (is_float($n)) {
+ $n = number_format($n, 0, '.', '');
+ }
+ $d = $this->_denominator;
+ if (is_float($d)) {
+ $d = number_format($d, 0, '.', '');
+ }
+ return $n . '/' . $d;
+ }
+
+ /**
+ * @return array returns an array of [$numerator, $denominator]
+ */
+ public function toArray()
+ {
+ return [$this->_numerator, $this->_denominator];
+ }
+
+ /**
+ * Checks for the existence of the values TRational uses: 0, 1, 'numerator', and
+ * 'denominator'.
+ * @param mixed $offset The numerator or denominator of the TRational.
+ * @return bool Does the property exist for the TRational.
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ if (is_numeric($offset) && ($offset == 0 || $offset == 1) || is_string($offset) && ($offset === self::NUMERATOR || $offset === self::DENOMINATOR)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This is a convenience method for getting the numerator and denominator.
+ * Index '0' and 'numerator' will get the {@link getNumerator Numerator}, and
+ * Index '1' and 'denominator' will get the {@link getDenominator Denominator}.
+ * @param mixed $offset Which property of the Rational to retrieve.
+ * @throws TInvalidDataValueException When $offset is not a property of the Rational.
+ * @return mixed The numerator or denominator.
+ */
+ public function offsetGet(mixed $offset): mixed
+ {
+ if (is_numeric($offset)) {
+ if ($offset == 0) {
+ return $this->getNumerator();
+ } elseif ($offset == 1) {
+ return $this->getDenominator();
+ }
+ } elseif (is_string($offset)) {
+ if ($offset == self::NUMERATOR) {
+ return $this->getNumerator();
+ } elseif ($offset == self::DENOMINATOR) {
+ return $this->getDenominator();
+ }
+ } elseif ($offset === null) {
+ return $this->getValue();
+ }
+ throw new TInvalidDataValueException('rational_bad_offset', $offset);
+ }
+
+ /**
+ * This is a convenience method for setting the numerator and denominator.
+ * Index '0' and 'numerator' will set the {@link setNumerator Numerator}, and
+ * Index '1' and 'denominator' will set the {@link setDenominator Denominator}.
+ * @param mixed $offset Which property to set.
+ * @param mixed $value The numerator or denominator.
+ * @throws TInvalidDataValueException When $offset is not a property of the Rational.
+ */
+ public function offsetSet(mixed $offset, mixed $value): void
+ {
+ if (is_numeric($offset)) {
+ if ($offset == 0) {
+ $this->setNumerator($value);
+ return;
+ } elseif ($offset == 1) {
+ $this->setDenominator($value);
+ return;
+ }
+ } elseif (is_string($offset)) {
+ if ($offset == self::NUMERATOR) {
+ $this->setNumerator($value);
+ return;
+ } elseif ($offset == self::DENOMINATOR) {
+ $this->setDenominator($value);
+ return;
+ }
+ } elseif ($offset === null) {
+ $this->setValue($value);
+ return;
+ }
+ throw new TInvalidDataValueException('rational_bad_offset', $offset);
+ }
+
+ /**
+ * This is a convenience method for resetting the numerator and denominator to
+ * default. Index '0' and 'numerator' will reset the {@link setNumerator Numerator}
+ * to "0", and Index '1' and 'denominator' will reset the {@link setDenominator
+ * Denominator} to "1".
+ * @param mixed $offset Which property to reset.
+ * @throws TInvalidDataValueException When $offset is not a property of the Rational.
+ */
+ public function offsetUnset(mixed $offset): void
+ {
+ if (is_numeric($offset)) {
+ if ($offset == 0) {
+ $this->setNumerator(0);
+ return;
+ } elseif ($offset == 1) {
+ $this->setDenominator(1);
+ return;
+ }
+ } elseif (is_string($offset)) {
+ if ($offset == self::NUMERATOR) {
+ $this->setNumerator(0);
+ return;
+ } elseif ($offset == self::DENOMINATOR) {
+ $this->setDenominator(1);
+ return;
+ }
+ }
+ throw new TInvalidDataValueException('rational_bad_offset', $offset);
+ }
+
+ /**
+ * This uses the Continued Fraction to make a float into a fraction of two integers.
+ * - Given INF, this returns [0xFFFFFFFF, 0].
+ * - Given NAN, this returns [0, 0].
+ * - Given 0 or values proximal to 0, this returns [0, 1].
+ * Only the numerator can go negative if the $value is negative.
+ * @param float $value The float value to deconstruct into a fraction of two integers.
+ * @param float $tolerance How precise does the continued fraction need to be to break. Default 1.e-6
+ * @param ?bool $unsigned Is the result an unsigned 32 bit int (vs signed 32 bit int), default false
+ * @return array An array of numerator at [0] and denominator at [1].
+ * @see https://en.wikipedia.org/wiki/Continued_fraction
+ */
+ public static function float2rational(float $value, ?float $tolerance = null, ?bool $unsigned = false): array
+ {
+ if (is_infinite($value)) {
+ return [$unsigned ? TBitHelper::PHP_INT32_UMAX : -1, 0];
+ }
+ if (is_nan($value)) {
+ return [0, 0];
+ }
+ if ($value === 0.0 || ($unsigned && $value < 0.5 / TBitHelper::PHP_INT32_UMAX) || (!$unsigned && abs($value) < 0.5 / TBitHelper::PHP_INT32_MAX)) {
+ return [0, 1];
+ }
+ if ($unsigned) {
+ if ($value > TBitHelper::PHP_INT32_UMAX) {
+ return [TBitHelper::PHP_INT32_UMAX, 1];
+ } elseif ($value < 1.5 / TBitHelper::PHP_INT32_UMAX) {
+ return [1, TBitHelper::PHP_INT32_UMAX];
+ }
+ } else {
+ if ($value > TBitHelper::PHP_INT32_MAX) {
+ return [TBitHelper::PHP_INT32_MAX, 1];
+ } elseif ($value < TBitHelper::PHP_INT32_MIN) {
+ return [TBitHelper::PHP_INT32_MIN, 1];
+ } elseif (abs($value) < 1.5 / TBitHelper::PHP_INT32_MAX) {
+ return [1, TBitHelper::PHP_INT32_MAX];
+ }
+ }
+ if ($tolerance === null) {
+ $tolerance = self::DEFAULT_TOLERANCE;
+ }
+ $sign = $value < 0 ? -1 : 1;
+ $offset = $value < 0 ? 1.0 : 0.0; // Negative values go to +1 max over positive max.
+ $value = abs($value);
+ $h = 1.0;
+ $lh = 0.0;
+ $k = 0.0;
+ $lk = 1.0;
+ $b = 1.0 / $value;
+ $tolerance *= $value;
+ do {
+ $b = 1.0 / $b;
+ $a = floor($b);
+ $tmp = $h;
+ $h = $a * $h + $lh;
+ $lh = $tmp;
+ $tmp = $k;
+ $k = $a * $k + $lk;
+ $lk = $tmp;
+ if ($h > ($unsigned ? TBitHelper::PHP_INT32_UMAX - 1 : (TBitHelper::PHP_INT32_MAX + $offset)) || $k > ($unsigned ? TBitHelper::PHP_INT32_UMAX : TBitHelper::PHP_INT32_MAX)) {
+ $h = $lh;
+ $k = $lk;
+ break;
+ }
+ $b = $b - $a;
+ } while ($b !== 0.0 && abs($value - $h / $k) > $tolerance);
+ if (PHP_INT_SIZE > 4 || $h <= PHP_INT_MAX + $offset && $k <= PHP_INT_MAX) {
+ return [$sign * ((int) $h), ((int) $k)];
+ } elseif ($h <= PHP_INT_MAX + $offset) {
+ return [$sign * ((int) $h), $k];
+ } elseif ($k <= PHP_INT_MAX) {
+ return [$sign * $h, ((int) $k)];
+ } else {
+ return [$sign * $h, $k];
+ }
+ }
+}
diff --git a/framework/Util/Math/TURational.php b/framework/Util/Math/TURational.php
new file mode 100644
index 000000000..01a2e8b31
--- /dev/null
+++ b/framework/Util/Math/TURational.php
@@ -0,0 +1,98 @@
+
+ * @link https://github.com/pradosoft/prado
+ * @license https://github.com/pradosoft/prado/blob/master/LICENSE
+ */
+
+namespace Prado\Util\Math;
+
+use Prado\Util\TBitHelper;
+
+/**
+ * TURational class.
+ *
+ * TURational implements a fraction in the form of one unsigned integer {@link getNumerator
+ * Numerator} divided by another unsigned integer {@link getDenominator Denominator}.
+ *
+ * TURational is a specialization of {@link TRational} and TRational has information
+ * about how these classes work.
+ *
+ * INF is "4294967295/0" and NAN (Not A Number) has the denominator equal zero
+ * (to avoid a divide by zero error).
+ *
+ * When setting a {@link setNumerator Numerator} and {@link setDenominator Denominator},
+ * the PHP instance is checked if it is 32 bit or 64 bit. 64 Bit PHP can represent
+ * integers in the range [2147483648, 4294967295] as an integer, but on a 32 bit
+ * PHP instance, these high bit integers are converted to float to be more accurately
+ * represented.
+ *
+ * The Rational data format is used by EXIF and, in particular, the GPS Image File
+ * Directory.
+ *
+ * @author Brad Anderson
+ * @see TRational
+ * @since 4.2.3
+ */
+class TURational extends TRational
+{
+ /**
+ * @return bool Is the class unsigned. Returns true.
+ */
+ public static function getIsUnsigned(): bool
+ {
+ return true;
+ }
+
+ /**
+ * This only accepts 0 and positive values. For 32 bit systems this accepts a float
+ * to represent numbers larger than PHP_INT_MAX.
+ * @param float|int|string $value The numerator.
+ */
+ public function setNumerator($value): TURational
+ {
+ $value = min(max(0, $value), TBitHelper::PHP_INT32_UMAX);
+ if (PHP_INT_SIZE > 4 || $value <= PHP_INT_MAX) {
+ $this->_numerator = (int) $value;
+ } else {
+ $this->_numerator = (float) $value;
+ }
+ return $this;
+ }
+
+ /**
+ * This only accepts 0 and positive values. For 32 bit systems this accepts a float
+ * to represent numbers larger than PHP_INT_MAX.
+ * @param float|int|string $value The denominator.
+ */
+ public function setDenominator($value): TURational
+ {
+ $value = min(max(0, $value), TBitHelper::PHP_INT32_UMAX);
+ if (PHP_INT_SIZE > 4 || $value <= PHP_INT_MAX) {
+ $this->_denominator = (int) $value;
+ } else {
+ $this->_denominator = (float) $value;
+ }
+ return $this;
+ }
+
+ /**
+ * This returns the float value of the Numerator divided by the denominator.
+ * Returns INF (Infinity) float value if the {@link getNumerator Numerator} is
+ * 0xFFFFFFFF (4294967295) and {@link getDenominator Denominator} is 0. Returns
+ * NAN (Not A Number) float value if the {@link getDenominator Denominator} is zero.
+ * @return float The float value of the Numerator divided by denominator.
+ */
+ public function getValue(): float
+ {
+ if ($this->_numerator === TBitHelper::PHP_INT32_UMAX && $this->_denominator === 0) {
+ return INF;
+ }
+ if ($this->_denominator === 0) {
+ return NAN;
+ }
+ return ((float) $this->_numerator) / ((float) $this->_denominator);
+ }
+}
diff --git a/framework/Util/TBitHelper.php b/framework/Util/TBitHelper.php
index 3590a490f..76e092d76 100644
--- a/framework/Util/TBitHelper.php
+++ b/framework/Util/TBitHelper.php
@@ -64,6 +64,21 @@
*/
class TBitHelper
{
+ // Defined constants for 32 bit computation
+ public const PHP_INT32_MIN = -2147483648; // 0x80000000
+ public const PHP_INT32_MAX = 2147483647; // 0x7FFFFFFF
+ // on 32 bit systems the PHP_INT64_UMAX is a float and not a integer.
+ public const PHP_INT32_UMAX = 4294967295; // 0xFFFFFFFF (unsigned)
+ public const PHP_INT32_MASK = (PHP_INT_SIZE > 4) ? self::PHP_INT32_UMAX : -1;
+
+ // Defined constants for 64 bit computation
+ // on 32 bit systems these values are only approximate floats and not integers.
+ public const PHP_INT64_MIN = -9223372036854775808; // 0x80000000_00000000
+ public const PHP_INT64_MAX = 9223372036854775807; // 0x7FFFFFFF_FFFFFFFF
+ //PHP_INT64_UMAX is a float that only approximates the maximum, unless using 16 byte int
+ public const PHP_INT64_UMAX = 18446744073709551615; // 0xFFFFFFFF_FFFFFFFF (unsigned)
+ public const PHP_INT64_MASK = -1; // Assuming 64 bit is validated.
+
public const Level1 = (PHP_INT_SIZE >= 8) ? 0x5555555555555555 : 0x55555555;
public const NLevel1 = ~self::Level1;
public const Mask1 = (PHP_INT_SIZE >= 8) ? 0x7FFFFFFFFFFFFFFF : 0x7FFFFFFF;
diff --git a/framework/classes.php b/framework/classes.php
index c990df6cb..e18e69a6e 100644
--- a/framework/classes.php
+++ b/framework/classes.php
@@ -284,6 +284,8 @@
'IDynamicMethods' => 'Prado\Util\IDynamicMethods',
'IInstanceCheck' => 'Prado\Util\IInstanceCheck',
'IPluginModule' => 'Prado\Util\IPluginModule',
+'TRational' => 'Prado\Util\Math\TRational',
+'TURational' => 'Prado\Util\Math\TURational',
'TBaseBehavior' => 'Prado\Util\TBaseBehavior',
'TBehavior' => 'Prado\Util\TBehavior',
'TBehaviorsModule' => 'Prado\Util\TBehaviorsModule',
diff --git a/tests/unit/Util/Math/TRationalTest.php b/tests/unit/Util/Math/TRationalTest.php
new file mode 100755
index 000000000..e11d84b97
--- /dev/null
+++ b/tests/unit/Util/Math/TRationalTest.php
@@ -0,0 +1,356 @@
+getTestClass();
+ $this->obj = new $class();
+ }
+
+ protected function tearDown(): void
+ {
+ $this->obj = null;
+ }
+
+ public function testGetIsUnsigned()
+ {
+ self::assertFalse($this->obj->getIsUnsigned());
+ }
+
+ public function testConstruct()
+ {
+ $class = $this->getTestClass();
+
+ self::assertEquals(0, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+
+ $rational = new $class(1.5);
+ self::assertEquals(3, $rational->getNumerator());
+ self::assertEquals(2, $rational->getDenominator());
+
+ $rational = new $class('911/0');
+ self::assertEquals(911, $rational->getNumerator());
+ self::assertEquals(0, $rational->getDenominator());
+
+ $rational = new $class(33, 10);
+ self::assertEquals(33, $rational->getNumerator());
+ self::assertEquals(10, $rational->getDenominator());
+
+ $rational = new $class(11, null);
+ self::assertEquals(11, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class(13, false);
+ self::assertEquals(13, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class(NAN);
+ self::assertEquals(0, $rational->getNumerator());
+ self::assertEquals(0, $rational->getDenominator());
+
+ $rational = new $class(null);
+ self::assertEquals(0, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class(false);
+ self::assertEquals(0, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class(['55.5']);
+ self::assertEquals(111, $rational->getNumerator());
+ self::assertEquals(2, $rational->getDenominator());
+ }
+
+ public function testConstructSpecific()
+ {
+ $class = $this->getTestClass();
+
+ $rational = new $class(-1.5);
+ self::assertEquals(-3, $rational->getNumerator());
+ self::assertEquals(2, $rational->getDenominator());
+
+ $rational = new $class(4294967294.0, 3);
+ self::assertEquals(1431655764, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class(INF);
+ self::assertEquals(-1, $rational->getNumerator());
+ self::assertEquals(0, $rational->getDenominator());
+
+ $rational = new $class('-21/-13');
+ self::assertEquals(-21, $rational->getNumerator());
+ self::assertEquals(-13, $rational->getDenominator());
+
+ $rational = new $class(['-21', '-13']);
+ self::assertEquals(-21, $rational->getNumerator());
+ self::assertEquals(-13, $rational->getDenominator());
+
+ $rational = new $class([-34, 21]);
+ self::assertEquals(-34, $rational->getNumerator());
+ self::assertEquals(21, $rational->getDenominator());
+
+ $rational = new $class([34, -21]);
+ self::assertEquals(34, $rational->getNumerator());
+ self::assertEquals(-21, $rational->getDenominator());
+ }
+
+ public function testNumerator()
+ {
+ self::assertEquals($this->obj, $this->obj->setNumerator(11));
+ self::assertEquals(11, $this->obj->getNumerator());
+
+ $this->obj->setNumerator(13.3);
+ self::assertEquals(13, $this->obj->getNumerator());
+
+ $this->obj->setNumerator(0);
+ self::assertEquals(0, $this->obj->getNumerator());
+ }
+
+ public function testNumeratorSpecific()
+ {
+ $this->obj->setNumerator(-3.89);
+ self::assertEquals(-3, $this->obj->getNumerator());
+
+ $this->obj->setNumerator(-1);
+ self::assertEquals(-1, $this->obj->getNumerator());
+ }
+
+ public function testDenominator()
+ {
+ self::assertEquals($this->obj, $this->obj->setDenominator(11));
+ self::assertEquals(11, $this->obj->getDenominator());
+
+ $this->obj->setDenominator(13.3);
+ self::assertEquals(13, $this->obj->getDenominator());
+
+ $this->obj->setNumerator(-3.89);
+ self::assertEquals($this->obj::getIsUnsigned() ? 0 : -3, $this->obj->getNumerator());
+
+ $this->obj->setDenominator(-1);
+ self::assertEquals($this->obj::getIsUnsigned() ? 0 : -1, $this->obj->getDenominator());
+
+ $this->obj->setDenominator(0);
+ self::assertEquals(0, $this->obj->getDenominator());
+ }
+
+ public function testDenominatorSpecific()
+ {
+ $this->obj->setDenominator(-3.89);
+ self::assertEquals($this->obj::getIsUnsigned() ? 0 : -3, $this->obj->getDenominator());
+
+ $this->obj->setDenominator(-1);
+ self::assertEquals($this->obj::getIsUnsigned() ? 0 : -1, $this->obj->getDenominator());
+ }
+
+ public function testValue()
+ {
+ self::assertEquals(0, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+ self::assertEquals(0, $this->obj->getValue());
+
+ self::assertEquals($this->obj, $this->obj->setValue(11.0));
+ self::assertEquals(11, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+ self::assertEquals(11.0, $this->obj->getValue());
+
+ $this->obj->setValue(0);
+ self::assertEquals(0, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+ self::assertEquals(0, $this->obj->getValue());
+
+ $this->obj->setValue(1.0/3.0);
+ self::assertEquals(1, $this->obj->getNumerator());
+ self::assertEquals(3, $this->obj->getDenominator());
+ self::assertEquals(0.3333333333333333, $this->obj->getValue());
+
+ $phi = (1.0 + sqrt(5)) / 2.0; // The "most irrational" number. 1.61803399....
+ $this->obj->setValue($phi);
+ self::assertEquals(987, $this->obj->getNumerator());
+ self::assertEquals(610, $this->obj->getDenominator());
+ self::assertEquals(1.618032786885246, $this->obj->getValue());
+
+ $this->obj->setValue($phi, 0);
+ self::assertEquals(165580141, $this->obj->getNumerator());
+ self::assertEquals(102334155, $this->obj->getDenominator());
+ self::assertEquals(1.618033988749895, $this->obj->getValue());
+
+ $this->obj->setValue(2147483647.5, 0);
+ self::assertEquals(2147483647, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+ self::assertEquals(2147483647.0, $this->obj->getValue());
+
+ self::assertEquals($this->obj, $this->obj->setValue(['1', '0']));
+ self::assertEquals(1, $this->obj->getNumerator());
+ self::assertEquals(0, $this->obj->getDenominator());
+ self::assertTrue(is_nan($this->obj->getValue()));
+
+ $this->obj->setValue([987, 610]);
+ self::assertEquals(987, $this->obj->getNumerator());
+ self::assertEquals(610, $this->obj->getDenominator());
+ self::assertEquals(1.618032786885246, $this->obj->getValue());
+
+ $this->obj->setValue([NAN]);
+ self::assertEquals(0, $this->obj->getNumerator());
+ self::assertEquals(0, $this->obj->getDenominator());
+ self::asserttrue(is_nan($this->obj->getValue()));
+ }
+
+ public function testValueSpecific()
+ {
+ $this->obj->setValue(-11.5);
+ self::assertEquals(-23, $this->obj->getNumerator());
+ self::assertEquals(2, $this->obj->getDenominator());
+ self::assertEquals(-11.5, $this->obj->getValue());
+
+ $this->obj->setValue(2147483646.5, 0);
+ self::assertEquals(2147483646, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+ self::assertEquals(2147483646.0, $this->obj->getValue());
+
+ // Set infinity
+ $this->obj->setValue('-1/0');
+ self::assertEquals(-1, $this->obj->getNumerator());
+ self::assertEquals(0, $this->obj->getDenominator());
+ self::assertEquals(INF, $this->obj->getValue());
+
+ $this->obj->setValue(INF);
+ self::assertEquals(-1, $this->obj->getNumerator());
+ self::assertEquals(0, $this->obj->getDenominator());
+ self::assertEquals(INF, $this->obj->getValue());
+ }
+
+ public function testToString()
+ {
+ $this->obj->setValue(1.5);
+ self::assertEquals('3/2', (string) $this->obj);
+
+ $this->obj->setValue(NAN);
+ self::assertEquals('0/0', (string) $this->obj);
+ }
+
+ public function testToStringSpecific()
+ {
+ $this->obj->setValue(-1.5);
+ self::assertEquals('-3/2', (string) $this->obj);
+
+ $this->obj->setValue(INF);
+ self::assertEquals('-1/0', (string) $this->obj);
+ }
+
+ public function testToArray()
+ {
+ $this->obj->setValue(1.5);
+ self::assertEquals([3, 2], $this->obj->toArray());
+
+ $this->obj->setValue(NAN);
+ self::assertEquals([0, 0], $this->obj->toArray());
+ }
+
+ public function testToArraySpecific()
+ {
+ $this->obj->setValue(INF);
+ self::assertEquals([-1, 0], $this->obj->toArray());
+ }
+
+ public function testOffsetExists()
+ {
+ self::assertFalse($this->obj->offsetExists(-1));
+ self::assertTrue($this->obj->offsetExists(0));
+ self::assertTrue($this->obj->offsetExists(1));
+ self::assertFalse($this->obj->offsetExists(2));
+ self::assertTrue($this->obj->offsetExists('numerator'));
+ self::assertTrue($this->obj->offsetExists('denominator'));
+ self::assertFalse($this->obj->offsetExists('not_a_value'));
+ }
+
+ public function testOffsetGet()
+ {
+ $this->obj->setNumerator(3);
+ $this->obj->setDenominator(2);
+ self::assertEquals(3, $this->obj->offsetGet(0));
+ self::assertEquals(2, $this->obj->offsetGet(1));
+ self::assertEquals(3, $this->obj->offsetGet('numerator'));
+ self::assertEquals(2, $this->obj->offsetGet('denominator'));
+
+ self::assertEquals(1.5, $this->obj->offsetGet(null));
+ self::assertEquals(1.5, $this->obj[null]);
+
+ self::expectException(TInvalidDataValueException::class);
+ self::assertFalse($this->obj->offsetGet(2));
+ }
+
+ public function testOffsetSet()
+ {
+ $this->obj->offsetSet(0, 3);
+ self::assertEquals(3, $this->obj->getNumerator());
+ $this->obj->offsetSet(1, 2);
+ self::assertEquals(2, $this->obj->getDenominator());
+ $this->obj->offsetSet('numerator', 8);
+ self::assertEquals(8, $this->obj->getNumerator());
+ $this->obj->offsetSet('denominator', 5);
+ self::assertEquals(5, $this->obj->getDenominator());
+
+ $this->obj[0] = 13;
+ self::assertEquals(13, $this->obj->getNumerator());
+
+ $this->obj[] = 1.5;
+ self::assertEquals(3, $this->obj->getNumerator());
+ self::assertEquals(2, $this->obj->getDenominator());
+
+ self::expectException(TInvalidDataValueException::class);
+ self::assertFalse($this->obj->offsetSet(2, 8));
+ }
+
+ public function testOffsetUnset()
+ {
+ $this->obj->setValue([3, 2]);
+ self::assertEquals(3, $this->obj->getNumerator(), "Numerator was not initialized properly");
+ self::assertEquals(2, $this->obj->getDenominator(), "Denominator was not initialized properly");
+
+ $this->obj->offsetUnset(0);
+ self::assertEquals(0, $this->obj->getNumerator());
+ $this->obj->offsetUnset(1);
+ self::assertEquals(1, $this->obj->getDenominator());
+
+ $this->obj->setValue([3, 2]);
+ $this->obj->offsetUnset('numerator');
+ self::assertEquals(0, $this->obj->getNumerator());
+ $this->obj->offsetUnset('denominator');
+ self::assertEquals(1, $this->obj->getDenominator());
+
+ self::expectException(TInvalidDataValueException::class);
+ $this->obj->offsetUnset(2);
+ }
+
+ public function testFloat2rational()
+ {// main algorithm works. Boundary Conditions
+ $class = $this->getTestClass();
+
+ self::assertEquals([-1, 0], $class::float2rational(INF));
+ self::assertEquals([0, 0], $class::float2rational(NAN));
+ self::assertEquals([0, 1], $class::float2rational(0.0));
+ self::assertEquals([0, 1], $class::float2rational(0.5 / 2147483647.5));
+ self::assertEquals([2147483647, 1], $class::float2rational(2147483648.0));
+ self::assertEquals([-2147483648, 1], $class::float2rational(-2147483649.0));
+ self::assertEquals([1, 2147483647], $class::float2rational(0.5 / 2147483647.0));
+ self::assertEquals([1, 2147483647], $class::float2rational(1.0 / 2147483647.0));
+ self::assertEquals([1, 2147483647], $class::float2rational(1.499 / 2147483647.0));
+
+ $maxValue = PHP_INT_SIZE > 4 ? 4294967295 : 4294967295.0;
+ self::assertEquals([$maxValue, 0], $class::float2rational(INF, null, true));
+ self::assertEquals([0, 1], $class::float2rational(0.5 / 4294967295.5, null, true));
+ self::assertEquals([1, $maxValue], $class::float2rational(0.5 / 4294967295.0, null, true));
+ self::assertEquals([1, $maxValue], $class::float2rational(1.0 / 4294967295.0, null, true));
+ self::assertEquals([1, $maxValue], $class::float2rational(1.49999 / 4294967295.0, null, true));
+ }
+}
diff --git a/tests/unit/Util/Math/TURationalTest.php b/tests/unit/Util/Math/TURationalTest.php
new file mode 100755
index 000000000..1a192854a
--- /dev/null
+++ b/tests/unit/Util/Math/TURationalTest.php
@@ -0,0 +1,104 @@
+obj->getIsUnsigned());
+ }
+
+ public function testConstructSpecific()
+ {
+ $class = $this->getTestClass();
+
+ $rational = new $class(-1.5);
+ self::assertEquals(0, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class(4294967294.0, 3);
+ self::assertEquals(4294967294, $rational->getNumerator());
+ self::assertEquals(3, $rational->getDenominator());
+
+ $rational = new $class(INF);
+ self::assertEquals(4294967295, $rational->getNumerator());
+ self::assertEquals(0, $rational->getDenominator());
+
+ $rational = new $class('-21/-13');
+ self::assertEquals(21, $rational->getNumerator());
+ self::assertEquals(13, $rational->getDenominator());
+
+ $rational = new $class(['-21', '-13']);
+ self::assertEquals(21, $rational->getNumerator());
+ self::assertEquals(13, $rational->getDenominator());
+
+ $rational = new $class([-34, 21]);
+ self::assertEquals(0, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+
+ $rational = new $class([34, -21]);
+ self::assertEquals(0, $rational->getNumerator());
+ self::assertEquals(1, $rational->getDenominator());
+ }
+
+ public function testNumeratorSpecific()
+ {
+ $this->obj->setNumerator(-3.89);
+ self::assertEquals(0, $this->obj->getNumerator());
+
+ $this->obj->setNumerator(-1);
+ self::assertEquals(0, $this->obj->getNumerator());
+ }
+
+ public function testDenominatorSpecific()
+ {
+ $this->obj->setDenominator(-3.89);
+ self::assertEquals(0, $this->obj->getDenominator());
+
+ $this->obj->setDenominator(-1);
+ self::assertEquals(0, $this->obj->getDenominator());
+ }
+
+
+ public function testValueSpecific()
+ {
+ $this->obj->setValue(-11.5);
+ self::assertEquals(0, $this->obj->getNumerator());
+ self::assertEquals(1, $this->obj->getDenominator());
+ self::assertEquals(0, $this->obj->getValue());
+
+ $this->obj->setValue(2147483646.5, 0);
+ self::assertEquals(4294967293, $this->obj->getNumerator());
+ self::assertEquals(2, $this->obj->getDenominator());
+ self::assertEquals(2147483646.5, $this->obj->getValue());
+
+ // Set infinity
+ $this->obj->setValue('4294967295/0');
+ self::assertEquals(4294967295, $this->obj->getNumerator());
+ self::assertEquals(0, $this->obj->getDenominator());
+ self::assertEquals(INF, $this->obj->getValue());
+
+ $this->obj->setValue(INF);
+ self::assertEquals(4294967295, $this->obj->getNumerator());
+ self::assertEquals(0, $this->obj->getDenominator());
+ self::assertEquals(INF, $this->obj->getValue());
+ }
+
+ public function testToStringSpecific()
+ {
+ $this->obj->setValue(INF);
+ self::assertEquals('4294967295/0', (string) $this->obj);
+ }
+
+ public function testToArraySpecific()
+ {
+ $this->obj->setValue(INF);
+ self::assertEquals([4294967295, 0], $this->obj->toArray());
+ }
+}