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()); + } +}