From 47d4e18e9128f1d13fb902b30c7f0bf44a91c217 Mon Sep 17 00:00:00 2001 From: Jordan LeDoux Date: Mon, 28 Jun 2021 17:49:37 -0700 Subject: [PATCH 1/2] Provides a direct implementation of random value functions As the random number library we were using is no longer maintained and now uses deprecated/removed features, I've created an implementation that directly uses the random providers in PHP. This also allows random values which are not integers, and random values outside of PHP_INT_MAX and PHP_INT_MIN. --- composer.json | 1 - .../Fermat/Provider/PolyfillProvider.php | 31 --- .../Fermat/Provider/RandomProvider.php | 218 ++++++++++++++++++ src/Samsara/Fermat/Types/NumberCollection.php | 4 +- 4 files changed, 220 insertions(+), 34 deletions(-) delete mode 100644 src/Samsara/Fermat/Provider/PolyfillProvider.php create mode 100644 src/Samsara/Fermat/Provider/RandomProvider.php diff --git a/composer.json b/composer.json index 803372e2..d09849af 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ ], "license": "GPL-2.0-or-later", "require": { - "ircmaxell/random-lib": "^1.1", "riimu/kit-baseconversion": "^1", "samsara/common": "^1", "php": ">=8.0" diff --git a/src/Samsara/Fermat/Provider/PolyfillProvider.php b/src/Samsara/Fermat/Provider/PolyfillProvider.php deleted file mode 100644 index b57c0b8c..00000000 --- a/src/Samsara/Fermat/Provider/PolyfillProvider.php +++ /dev/null @@ -1,31 +0,0 @@ -getMediumStrengthGenerator(); - $num = $generator->generateInt($min, $max); - } - - return $num; - - } - -} \ No newline at end of file diff --git a/src/Samsara/Fermat/Provider/RandomProvider.php b/src/Samsara/Fermat/Provider/RandomProvider.php new file mode 100644 index 00000000..19076d41 --- /dev/null +++ b/src/Samsara/Fermat/Provider/RandomProvider.php @@ -0,0 +1,218 @@ +isFloat() || $maxDecimal->isFloat()) { + throw new IntegrityConstraint( + 'Random integers cannot be generated with boundaries which are floats', + 'Provide only whole number, integer values for min and max.', + 'An attempt was made to generate a random integer with boundaries which are non-integers. Min Provided: '.$min->getValue().' -- Max Provided: '.$max->getValue() + ); + } + + /** + * Because of optimistic optimizing with the rand() and random_int() functions, we do + * need the arguments to be provided in the correct order. + */ + if ($minDecimal->isGreaterThan($maxDecimal)) { + throw new IntegrityConstraint( + 'Minimum is larger than maximum.', + 'Please provide your arguments in the correct order.', + 'The supplied minimum value for randomInt() was greater than the supplied maximum value.' + ); + } + + /** + * For some applications it might be better to throw an exception here, however it + * would probably be hard to recover in most applications from a situation which + * resulted in this situation. + * + * So instead we will trigger a language level warning and return the only valid + * value for the parameters given. + */ + if ($minDecimal->isEqual($maxDecimal)) { + trigger_error( + 'Attempted to get a random value for a range of no size, with minimum of '.$minDecimal->getValue().' and maximum of '.$maxDecimal->getValue(), + E_USER_WARNING + ); + + return $minDecimal; + } + + if ($minDecimal->isLessThanOrEqualTo(PHP_INT_MAX) && $minDecimal->isGreaterThanOrEqualTo(PHP_INT_MIN)) { + $min = $minDecimal->asInt(); + } + + if ($maxDecimal->isLessThanOrEqualTo(PHP_INT_MAX) && $maxDecimal->isGreaterThanOrEqualTo(PHP_INT_MIN)) { + $max = $maxDecimal->asInt(); + } + + if (is_int($min) && is_int($max)) { + if ($mode == self::MODE_ENTROPY || $max > getrandmax() || $max < 0 || $min < 0) { + /** + * The random_int() function is cryptographically secure, and takes somewhere on the order + * of 15 times as long to execute as rand(). However, rand() also has a smaller range than + * the entire PHP integer size, so there are some situations where we need to use this + * function even if MODE_SPEED is selected. + * + * In those cases, random_int() is still faster than calls to random_bytes() and manual + * masking. + */ + try { + $num = random_int($min, $max); + return new ImmutableDecimal($num); + } catch (\Exception $e) { + throw new OptionalExit( + 'System error from random_bytes().', + 'Ensure your system is configured correctly.', + 'A call to random_bytes() threw a system level exception. Most often this is due to a problem with entropy sources in your configuration. Original exception message: ' . $e->getMessage() + ); + } + } elseif ($mode == self::MODE_SPEED) { + /** + * If it is possible to do so with the range given and the program has indicated that it + * would prefer speed over true randomness in the result, then we will use the deterministic + * pseudo-random function rand() as it is faster to reach completion. + */ + $num = rand($min, $max); + return new ImmutableDecimal($num); + } else { + throw new IntegrityConstraint( + 'Mode on random functions must be an implemented mode.', + 'Choose modes using the class constants.', + 'A mode was provided to randomInt() that does not correspond to any implementation. Please only use the class constants for selecting the mode.' + ); + } + } else { + $two = Numbers::make(Numbers::IMMUTABLE, 2, 0); + + /** + * We only need to request enough bytes to find a number within the range, since we + * will be adding the minimum value to it at the end. + */ + $range = $maxDecimal->subtract($minDecimal); + $bitsNeeded = $range->ln(1)->divide($two->ln(1), 1)->floor()->add(1); + $bytesNeeded = $bitsNeeded->divide(8)->ceil(); + + do { + try { + /** + * Returns random bytes based on sources of entropy within the system. + * + * For documentation on these sources please see: + * + * https://www.php.net/manual/en/function.random-bytes.php + */ + $entropyBytes = random_bytes($bytesNeeded->asInt()); + } catch (\Exception $e) { + throw new OptionalExit( + 'System error from random_bytes().', + 'Ensure your system is configured correctly.', + 'A call to random_bytes() threw a system level exception. Most often this is due to a problem with entropy sources in your configuration. Original exception message: ' . $e->getMessage() + ); + } + + $randomValue = Numbers::make( + type: Numbers::IMMUTABLE, + value: $entropyBytes, + base: 2 + ); + + /** + * Since the number of digits is equal to the bits needed, but random_bytes() only + * returns in chunks of 8 bits (duh, bytes), we can substr() from the right to + * select only the correct number of digits by multiplying the number of bits + * needed by -1 and using that as the starting point. + */ + $num = Numbers::make( + type: Numbers::IMMUTABLE, + value: substr($randomValue->getValue(), $bitsNeeded->multiply(-1)->asInt()) + ); + } while ($num->isGreaterThan($range)); + /** + * It is strictly speaking possible for this to loop infinitely. In the worst case + * scenario where 50% of possible values are invalid, it takes 7 loops for there to + * be a less than a 1% chance of still not having an answer. + * + * After only 10 loops the chance is less than 1/1000. + */ + + /** + * Add the minimum since we effectively subtracted it by finding a random number + * bounded between 0 and range. If our requested range included negative numbers, + * this operation will also return those values into our data by effectively + * shifting the result window. + */ + return $num->add($minDecimal); + } + } + + public static function randomDecimal(int $scale = 10, int $mode = self::MODE_ENTROPY): ImmutableDecimal + { + $min = new ImmutableDecimal(0); + $max = new ImmutableDecimal(str_pad('1', $scale+1, '0', STR_PAD_RIGHT)); + + $randomValue = self::randomInt($min, $max, $mode); + + if ($randomValue->isEqual($min) || $randomValue->isEqual($max)) { + return $randomValue->isPositive() ? new ImmutableDecimal(1) : $min; + } + + return new ImmutableDecimal('0.'.str_pad($randomValue->getValue(), $scale, '0', STR_PAD_LEFT)); + } + + public static function randomReal( + int|string|DecimalInterface $min, + int|string|DecimalInterface $max, + int $scale, + int $mode = self::MODE_ENTROPY + ): ImmutableDecimal + { + $min = new ImmutableDecimal($min); + $max = new ImmutableDecimal($max); + + $intPart = self::randomInt($min->floor(), $max->floor(), $mode); + $decPart = self::randomDecimal($scale, $mode); + + $num = $intPart->add($decPart); + + $num = $num->isGreaterThan($max) ? $max : $num; + $num = $num->isLessThan($min) ? $min : $num; + + return $num; + } + +} \ No newline at end of file diff --git a/src/Samsara/Fermat/Types/NumberCollection.php b/src/Samsara/Fermat/Types/NumberCollection.php index dc6a5004..faf8f5e3 100644 --- a/src/Samsara/Fermat/Types/NumberCollection.php +++ b/src/Samsara/Fermat/Types/NumberCollection.php @@ -10,7 +10,7 @@ use Samsara\Fermat\Provider\Distribution\Exponential; use Samsara\Fermat\Provider\Distribution\Normal; use Samsara\Fermat\Provider\Distribution\Poisson; -use Samsara\Fermat\Provider\PolyfillProvider; +use Samsara\Fermat\Provider\RandomProvider; use Samsara\Fermat\Types\Base\Interfaces\Groups\NumberCollectionInterface; use Samsara\Fermat\Types\Base\Interfaces\Numbers\NumberInterface; use Ds\Vector; @@ -294,7 +294,7 @@ public function getRandom(): NumberInterface { $maxKey = $this->getCollection()->count() - 1; - $key = PolyfillProvider::randomInt(0, $maxKey); + $key = RandomProvider::randomInt(0, $maxKey, RandomProvider::MODE_SPEED)->asInt(); return $this->get($key); } From 549af5704efda0f76c2fe15a2afab811a047c446 Mon Sep 17 00:00:00 2001 From: Jordan LeDoux Date: Tue, 29 Jun 2021 17:54:34 -0700 Subject: [PATCH 2/2] Completed the initial development of the RandomProvider Now includes functions for getting an int, decimal, or real. Test coverage also included. Also added tests for the hyperbolic functions. --- .../Fermat/Provider/RandomProvider.php | 239 +++++++++++++++++- src/Samsara/Fermat/Types/Decimal.php | 10 +- src/Samsara/Fermat/Types/NumberCollection.php | 13 +- .../Traits/Decimal/TrigonometryTrait.php | 10 +- .../Fermat/Values/ImmutableDecimal.php | 8 +- .../Fermat/Provider/RandomProviderTest.php | 195 ++++++++++++++ .../Fermat/Values/ImmutableDecimalTest.php | 114 +++++++++ 7 files changed, 563 insertions(+), 26 deletions(-) create mode 100644 tests/Samsara/Fermat/Provider/RandomProviderTest.php diff --git a/src/Samsara/Fermat/Provider/RandomProvider.php b/src/Samsara/Fermat/Provider/RandomProvider.php index 19076d41..77d711b8 100644 --- a/src/Samsara/Fermat/Provider/RandomProvider.php +++ b/src/Samsara/Fermat/Provider/RandomProvider.php @@ -2,7 +2,11 @@ namespace Samsara\Fermat\Provider; +use Exception; +use JetBrains\PhpStorm\ExpectedValues; +use JetBrains\PhpStorm\Pure; use Samsara\Exceptions\UsageError\IntegrityConstraint; +use Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState; use Samsara\Exceptions\UsageError\OptionalExit; use Samsara\Fermat\Numbers; use Samsara\Fermat\Types\Base\Interfaces\Numbers\DecimalInterface; @@ -14,17 +18,21 @@ class RandomProvider const MODE_ENTROPY = 1; const MODE_SPEED = 2; + /** @noinspection PhpDocMissingThrowsInspection */ /** * @param int|string|DecimalInterface $min * @param int|string|DecimalInterface $max - * + * @param int $mode * @return ImmutableDecimal * @throws IntegrityConstraint * @throws OptionalExit + * @throws IncompatibleObjectState */ + #[Pure] public static function randomInt( int|string|DecimalInterface $min, int|string|DecimalInterface $max, + #[ExpectedValues([self::MODE_ENTROPY, self::MODE_SPEED])] int $mode = self::MODE_ENTROPY ): ImmutableDecimal { @@ -73,10 +81,12 @@ public static function randomInt( } if ($minDecimal->isLessThanOrEqualTo(PHP_INT_MAX) && $minDecimal->isGreaterThanOrEqualTo(PHP_INT_MIN)) { + /** @noinspection PhpUnhandledExceptionInspection */ $min = $minDecimal->asInt(); } if ($maxDecimal->isLessThanOrEqualTo(PHP_INT_MAX) && $maxDecimal->isGreaterThanOrEqualTo(PHP_INT_MIN)) { + /** @noinspection PhpUnhandledExceptionInspection */ $max = $maxDecimal->asInt(); } @@ -94,7 +104,7 @@ public static function randomInt( try { $num = random_int($min, $max); return new ImmutableDecimal($num); - } catch (\Exception $e) { + } catch (Exception $e) { throw new OptionalExit( 'System error from random_bytes().', 'Ensure your system is configured correctly.', @@ -123,7 +133,9 @@ public static function randomInt( * We only need to request enough bytes to find a number within the range, since we * will be adding the minimum value to it at the end. */ + /** @noinspection PhpUnhandledExceptionInspection */ $range = $maxDecimal->subtract($minDecimal); + /** @noinspection PhpUnhandledExceptionInspection */ $bitsNeeded = $range->ln(1)->divide($two->ln(1), 1)->floor()->add(1); $bytesNeeded = $bitsNeeded->divide(8)->ceil(); @@ -137,7 +149,11 @@ public static function randomInt( * https://www.php.net/manual/en/function.random-bytes.php */ $entropyBytes = random_bytes($bytesNeeded->asInt()); - } catch (\Exception $e) { + $baseTwoBytes = ''; + for($i = 0; $i < strlen($entropyBytes); $i++){ + $baseTwoBytes .= decbin( ord( $entropyBytes[$i] ) ); + } + } catch (Exception $e) { throw new OptionalExit( 'System error from random_bytes().', 'Ensure your system is configured correctly.', @@ -147,11 +163,13 @@ public static function randomInt( $randomValue = Numbers::make( type: Numbers::IMMUTABLE, - value: $entropyBytes, + value: $baseTwoBytes, base: 2 ); /** + * @var ImmutableDecimal $num + * * Since the number of digits is equal to the bits needed, but random_bytes() only * returns in chunks of 8 bits (duh, bytes), we can substr() from the right to * select only the correct number of digits by multiplying the number of bits @@ -159,8 +177,9 @@ public static function randomInt( */ $num = Numbers::make( type: Numbers::IMMUTABLE, - value: substr($randomValue->getValue(), $bitsNeeded->multiply(-1)->asInt()) - ); + value: substr($randomValue->getValue(), $bitsNeeded->multiply(-1)->asInt()), + base: 2 + )->convertToBase(10); } while ($num->isGreaterThan($range)); /** * It is strictly speaking possible for this to loop infinitely. In the worst case @@ -176,43 +195,237 @@ public static function randomInt( * this operation will also return those values into our data by effectively * shifting the result window. */ + /** @noinspection PhpUnhandledExceptionInspection */ return $num->add($minDecimal); } } - public static function randomDecimal(int $scale = 10, int $mode = self::MODE_ENTROPY): ImmutableDecimal + /** + * @param int $scale + * @param int $mode + * @return ImmutableDecimal + * @throws IntegrityConstraint + * @throws OptionalExit + * @throws IncompatibleObjectState + */ + #[Pure] + public static function randomDecimal( + int $scale = 10, + #[ExpectedValues([self::MODE_ENTROPY, self::MODE_SPEED])] + int $mode = self::MODE_ENTROPY + ): ImmutableDecimal { + /** + * Select the min and max as if we were looking for the decimal part as an integer. + */ $min = new ImmutableDecimal(0); $max = new ImmutableDecimal(str_pad('1', $scale+1, '0', STR_PAD_RIGHT)); + /** + * This allows us to utilize the same randomInt() function. + */ $randomValue = self::randomInt($min, $max, $mode); + /** + * If the random value exactly equals our min or max, that means we need to return + * either 1 or 0. + */ if ($randomValue->isEqual($min) || $randomValue->isEqual($max)) { return $randomValue->isPositive() ? new ImmutableDecimal(1) : $min; } + /** + * In all other cases we need to reformat our integer as being the decimal portion + * of our number at the given scale. + */ return new ImmutableDecimal('0.'.str_pad($randomValue->getValue(), $scale, '0', STR_PAD_LEFT)); } + /** @noinspection PhpDocMissingThrowsInspection */ + /** + * @param int|string|DecimalInterface $min + * @param int|string|DecimalInterface $max + * @param int $scale + * @param int $mode + * @return ImmutableDecimal + * @throws IntegrityConstraint + * @throws OptionalExit + * @throws IncompatibleObjectState + */ + #[Pure] public static function randomReal( int|string|DecimalInterface $min, int|string|DecimalInterface $max, int $scale, + #[ExpectedValues([self::MODE_ENTROPY, self::MODE_SPEED])] int $mode = self::MODE_ENTROPY ): ImmutableDecimal { $min = new ImmutableDecimal($min); $max = new ImmutableDecimal($max); - $intPart = self::randomInt($min->floor(), $max->floor(), $mode); - $decPart = self::randomDecimal($scale, $mode); + if ($min->isEqual($max)) { + trigger_error( + 'Attempted to get a random value for a range of no size, with minimum of '.$min->getValue().' and maximum of '.$max->getValue(), + E_USER_WARNING + ); + + return $min; + } + + /** + * We do this because randomDecimal() can return 1, so if max is a natural number we need to + * remove it from the result set. Otherwise, we would be grabbing extra values and be shifting + * them to somewhere else in the result set, which skews the relative probabilities. + */ + if ($max->isNatural()) { + /** @noinspection PhpUnhandledExceptionInspection */ + $maxIntRange = $max->subtract(1); + } else { + $maxIntRange = $max->floor(); + } + + if (!$min->floor()->isEqual($maxIntRange)) { + $intPart = self::randomInt($min->floor(), $maxIntRange, $mode); + $repeatProbability = Numbers::makeZero(); + + /** + * If min and max aren't bounded by the same integers, then we need to adjust the likelihood + * of an integer on the ends of the range being selected according to the percentage of + * numbers within that range which are available. + */ + if ($min->ceil()->isEqual($max->floor())) { + /** + * This is a special case where min and max are less than 1 apart, but they straddle an + * integer. In this case, we want to consider the relative likelihood, instead of the + * portion of real numbers available. + */ + $minCeil = $min->ceil(); + $minRepeat = $minCeil->subtract($min); + $maxFloor = $max->floor(); + /** @noinspection PhpUnhandledExceptionInspection */ + $maxRepeat = $max->subtract($maxFloor); + $one = Numbers::makeOne(10); + + /** @noinspection PhpUnhandledExceptionInspection */ + $repeatProbability = $one->subtract($maxRepeat->divide($minRepeat, 10)); + } elseif ($intPart->isEqual($min->floor())) { + /** + * In this case, the integer includes the min. Since it's possible that not all reals + * in this range are actually available to choose from, the likelihood that this integer + * was chosen relative to any other integer in the range can be adjusted by making a + * recursive call with probability X, where X is min - floor(min). + */ + $minFloor = $min->floor(); + /** @noinspection PhpUnhandledExceptionInspection */ + $repeatProbability = $min->subtract($minFloor); + } elseif ($intPart->isEqual($max->floor())) { + /** + * In this case, the integer includes the max. Since it's possible that not all reals + * in this range are actually available to choose from, the likelihood that this integer + * was chosen relative to any other integer in the range can be adjusted by making a + * recursive call with probability X, where X is ceil(max) - max. + */ + $maxCeil = $max->ceil(); + $repeatProbability = $maxCeil->subtract($max); + } + + /** + * This will never be true unless one of the special cases above occurred. We use short circuiting + * to prevent a needless additional random generation in situations where there is zero probability + * adjustment. + */ + if ($repeatProbability->isGreaterThan(0) && $repeatProbability->isGreaterThan(self::randomDecimal(10, $mode))) { + return self::randomReal($min, $max, $scale, $mode); + } + } else { + /** + * In the case where min and max are bounded by the same integers, we can just set the integer + * part to floor(min) without any further calculation. All of the randomness of the value will + * come from the decimal part. + */ + $intPart = $min->floor(); + } + + if (!$intPart->isEqual($max->floor()) && !$intPart->isEqual($min->floor())) { + /** + * Because we know at this point that min and max are not equal prior to the conditions in + * this statement, we can be certain that the entire decimal range is available for selection + * if it passes these checks. + * + * The situations in which the entire decimal is a valid part of the result set are all covered + * by checking that intPart isn't equal to the floor of either min or max, since those are the only + * two integers which have bounded decimal ranges. + */ + $decPart = self::randomDecimal($scale, $mode); + } else { + if ($min->isNatural() || $intPart->isGreaterThan($min->floor())) { + /** + * The greater than check is also true any time min is a natural number (integer), however the check + * for min being an integer is much faster, so we're taking advantage of short circuiting. + */ + $minDecimal = Numbers::makeZero(); + } else { + /** + * The min is guaranteed to have a decimal portion here, since we already checked if it's natural. + * + * First we use string manipulation to extract the decimal portion as an integer value, the we right + * pad with zeroes to make sure that the entire scale is part of the valid result set. + */ + $minDecimal = substr($min->getValue(), strpos($min->getValue(), '.') + 1); + $minDecimal = str_pad($minDecimal, $scale, '0', STR_PAD_RIGHT); + } + + if ($intPart->isLessThan($max->floor())) { + /** + * We cannot take advantage of a more efficient check for the top end of the range, so the + * less than check is all we need. + */ + $maxDecimal = str_pad('1', $scale + 1, '0', STR_PAD_RIGHT); + } else { + /** + * The max value is guaranteed to have a decimal portion here since we excluded max being + * a natural number and part of the result set for intPart. + * + * First we use string manipulation to extract the decimal portion as an integer value, the we right + * pad with zeroes to make sure that the entire scale is part of the valid result set. + */ + $maxDecimal = substr($max->getValue(), strpos($max->getValue(), '.')+1); + $maxDecimal = str_pad($maxDecimal, $scale, '0', STR_PAD_RIGHT); + } - $num = $intPart->add($decPart); + /** + * Now that we have the correct bounds for the integers we're bounded by, figure out what the decimal + * portion of the random number is by utilizing randomInt(). + */ + $decPartAsInt = self::randomInt($minDecimal, $maxDecimal, $mode); - $num = $num->isGreaterThan($max) ? $max : $num; - $num = $num->isLessThan($min) ? $min : $num; + if ($decPartAsInt->isEqual($maxDecimal) && strlen($maxDecimal) > $scale) { + /** + * In the case where maxDecimal was returned by randomInt, we want to specifically translate + * that to 1 instead of treating it as a decimal value. But that's only the case if maxDecimal + * was larger than our scale. + * + * This is another case of us using short circuiting on a more efficient call. + */ + $decPart = Numbers::makeOne($scale); + } else { + /** + * In this section we know with certainty that the result of randomInt represents a decimal value + * that we can simply append as a string with padding to ensure correct scale. + */ + $decPart = new ImmutableDecimal( + value: '0.'.str_pad($decPartAsInt->getValue(), $scale, '0', STR_PAD_LEFT), + scale: $scale + ); + } + } - return $num; + /** + * Combine the integer and decimal portions of the random value. + */ + /** @noinspection PhpUnhandledExceptionInspection */ + return $intPart->add($decPart); } } \ No newline at end of file diff --git a/src/Samsara/Fermat/Types/Decimal.php b/src/Samsara/Fermat/Types/Decimal.php index 52265d12..20b9a3f1 100644 --- a/src/Samsara/Fermat/Types/Decimal.php +++ b/src/Samsara/Fermat/Types/Decimal.php @@ -94,7 +94,7 @@ protected function translateValue(string $value) $this->sign = false; } - if (strpos($value, '.') !== false) { + if (str_contains($value, '.')) { if (strpos($value, 'E')) { [$baseNum, $exp] = explode('E', $value); [$left, $right] = explode('.', $baseNum); @@ -182,7 +182,11 @@ public function getAsBaseTenRealNumber(): string public function getValue($base = null): string // TODO: Check usages to see if it should be replaced with rawString() { - $value = $this->convertObject(); + if (is_null($base)) { + $value = $this->convertObject(); + } else { + $value = $this->convertValue($this->getAsBaseTenRealNumber(), 10, $base); + } if ($this->isImaginary()) { $value .= 'i'; @@ -222,7 +226,7 @@ public function compare($value): int */ public function convertToBase($base) { - return $this->setValue($this->getValue($base), null, $base); + return $this->setValue($this->getValue(10), null, $base); } /** diff --git a/src/Samsara/Fermat/Types/NumberCollection.php b/src/Samsara/Fermat/Types/NumberCollection.php index faf8f5e3..fae95f70 100644 --- a/src/Samsara/Fermat/Types/NumberCollection.php +++ b/src/Samsara/Fermat/Types/NumberCollection.php @@ -12,6 +12,7 @@ use Samsara\Fermat\Provider\Distribution\Poisson; use Samsara\Fermat\Provider\RandomProvider; use Samsara\Fermat\Types\Base\Interfaces\Groups\NumberCollectionInterface; +use Samsara\Fermat\Types\Base\Interfaces\Numbers\DecimalInterface; use Samsara\Fermat\Types\Base\Interfaces\Numbers\NumberInterface; use Ds\Vector; use Samsara\Fermat\Values\Algebra\PolynomialFunction; @@ -314,13 +315,21 @@ public function sum(): NumberInterface } /** - * @return NumberInterface + * @return DecimalInterface */ - public function mean(): NumberInterface + public function mean(): DecimalInterface { return $this->sum()->divide($this->getCollection()->count()); } + /** + * @return DecimalInterface + */ + public function average(): DecimalInterface + { + return $this->mean(); + } + /** * @return Normal * @throws IntegrityConstraint|MissingPackage diff --git a/src/Samsara/Fermat/Types/Traits/Decimal/TrigonometryTrait.php b/src/Samsara/Fermat/Types/Traits/Decimal/TrigonometryTrait.php index 0671b2a1..bfca4ef2 100644 --- a/src/Samsara/Fermat/Types/Traits/Decimal/TrigonometryTrait.php +++ b/src/Samsara/Fermat/Types/Traits/Decimal/TrigonometryTrait.php @@ -293,9 +293,9 @@ public function sinh($scale = null, $round = true): DecimalInterface $this->scale = $scale; - $num = Numbers::makeOrDont(Numbers::IMMUTABLE, $this, $scale); + $num = Numbers::makeOrDont(Numbers::IMMUTABLE, $this, $scale+2); - $answer = $num->multiply(2)->exp()->subtract(1)->divide($two->multiply($num->exp())); + $answer = $num->multiply(2)->exp($scale+2)->subtract(1)->divide($two->multiply($num->exp($scale+2)), $scale+2); if ($round) { $answer = $answer->roundToScale($scale); @@ -354,9 +354,9 @@ public function coth($scale = null, $round = true): DecimalInterface $scale = $scale ?? $this->getScale(); - $num = Numbers::makeOrDont(Numbers::IMMUTABLE, $this, $scale); + $num = Numbers::makeOrDont(Numbers::IMMUTABLE, $this, $scale+2); - $answer = $num->cosh($scale+1, false)->divide($num->sinh($scale+1, false)); + $answer = $num->cosh($scale+1, false)->divide($num->sinh($scale+1, false), $scale+2); if ($round) { $answer = $answer->roundToScale($scale); @@ -376,7 +376,7 @@ public function sech($scale = null, $round = true): DecimalInterface $one = Numbers::makeOne(); $num = Numbers::makeOrDont(Numbers::IMMUTABLE, $this, $scale); - $answer = $one->divide($num->cosh($scale+1, false)); + $answer = $one->divide($num->cosh($scale+2, false), $scale+2); if ($round) { $answer = $answer->roundToScale($scale); diff --git a/src/Samsara/Fermat/Values/ImmutableDecimal.php b/src/Samsara/Fermat/Values/ImmutableDecimal.php index 4cb58cb0..643f582a 100644 --- a/src/Samsara/Fermat/Values/ImmutableDecimal.php +++ b/src/Samsara/Fermat/Values/ImmutableDecimal.php @@ -48,7 +48,7 @@ public function continuousModulo($mod): DecimalInterface * @return ImmutableDecimal * @throws IntegrityConstraint */ - protected function setValue(string $value, int $scale = null, int $base = 10) + protected function setValue(string $value, int $scale = null, int $base = null) { $imaginary = false; @@ -59,11 +59,13 @@ protected function setValue(string $value, int $scale = null, int $base = 10) $imaginary = true; } - if ($base !== 10 || $this->getBase() !== 10) { - $base = $base === 10 ? $this->getBase() : $base; + if (!is_null($base) || $this->getBase() !== 10) { + $base = is_null($base) ? $this->getBase() : $base; $value = $this->convertValue($value, 10, $base); } + $base = $base ?? 10; + if ($imaginary) { $value .= 'i'; } diff --git a/tests/Samsara/Fermat/Provider/RandomProviderTest.php b/tests/Samsara/Fermat/Provider/RandomProviderTest.php new file mode 100644 index 00000000..3a6a82af --- /dev/null +++ b/tests/Samsara/Fermat/Provider/RandomProviderTest.php @@ -0,0 +1,195 @@ +add($num1); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomInt($num1, $num2); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + } + + /** + * @medium + */ + public function testRandomIntSmallNumbers() + { + + $num1 = new ImmutableDecimal('0'); + $num2 = new ImmutableDecimal('100'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomInt($num1, $num2); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + $num1 = 0; + $num2 = 100; + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomInt($num1, $num2); + $this->assertGreaterThanOrEqual(0, $rand->asInt()); + $this->assertLessThanOrEqual(100, $rand->asInt()); + } + + $num1 = new ImmutableDecimal('0'); + $num2 = new ImmutableDecimal('100'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomInt($num1, $num2, RandomProvider::MODE_SPEED); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + } + + public function testRandomIntNegativeNumbers() + { + + $num1 = new ImmutableDecimal('-100'); + $num2 = new ImmutableDecimal('-50'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomInt($num1, $num2); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + } + + public function testRandomIntPosNegNumbers() + { + + $num1 = new ImmutableDecimal('-100'); + $num2 = new ImmutableDecimal('100'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomInt($num1, $num2); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + } + + public function testRandomIntEqualInput() + { + + $num1 = new ImmutableDecimal('100'); + $num2 = new ImmutableDecimal('100'); + + $this->expectWarning(); + $this->expectWarningMessage('Attempted to get a random value for a range of no size, with minimum of 100 and maximum of 100'); + $this->assertEquals('100', RandomProvider::randomInt($num1, $num2)->getValue()); + + } + + public function testRandomDecimal() + { + + $num1 = new ImmutableDecimal('0'); + $num2 = new ImmutableDecimal('1'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomDecimal(3); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + $num1 = new ImmutableDecimal('0'); + $num2 = new ImmutableDecimal('1'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomDecimal(3, RandomProvider::MODE_SPEED); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + } + + } + + public function testRandomRealStraddle() + { + + $num1 = new ImmutableDecimal('0.9'); + $num2 = new ImmutableDecimal('1.1'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomReal($num1, $num2, 3); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + $this->assertLessThanOrEqual(5, strlen($rand->getValue())); + } + + $num1 = new ImmutableDecimal('0.9'); + $num2 = new ImmutableDecimal('1.1'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomReal($num1, $num2, 8); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + $this->assertLessThanOrEqual(10, strlen($rand->getValue())); + } + + } + + public function testRandomRealBounded() + { + + $num1 = new ImmutableDecimal('0.4'); + $num2 = new ImmutableDecimal('0.5'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomReal($num1, $num2, 3); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + $this->assertLessThanOrEqual(5, strlen($rand->getValue())); + } + + $num1 = new ImmutableDecimal('4.04'); + $num2 = new ImmutableDecimal('4.05'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomReal($num1, $num2, 8); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + $this->assertLessThanOrEqual(10, strlen($rand->getValue())); + } + + } + + public function testRandomRealRange() + { + + $num1 = new ImmutableDecimal('0.4'); + $num2 = new ImmutableDecimal('2.6'); + + for ($i=0;$i<20;$i++) { + $rand = RandomProvider::randomReal($num1, $num2, 3); + $this->assertTrue($num1->isLessThanOrEqualTo($rand)); + $this->assertTrue($num2->isGreaterThanOrEqualTo($rand)); + $this->assertLessThanOrEqual(5, strlen($rand->getValue())); + } + + } + +} \ No newline at end of file diff --git a/tests/Samsara/Fermat/Values/ImmutableDecimalTest.php b/tests/Samsara/Fermat/Values/ImmutableDecimalTest.php index 6d1be05a..b8299b64 100644 --- a/tests/Samsara/Fermat/Values/ImmutableDecimalTest.php +++ b/tests/Samsara/Fermat/Values/ImmutableDecimalTest.php @@ -7,6 +7,7 @@ use Samsara\Exceptions\SystemError\PlatformError\MissingPackage; use Samsara\Exceptions\UsageError\IntegrityConstraint; use Samsara\Fermat\Numbers; +use Samsara\Fermat\Types\Decimal; use Samsara\Fermat\Types\NumberCollection; /** @@ -623,6 +624,95 @@ public function testArccsc() $oneHalf->arccsc(); } + + public function testSinh() + { + + $one = Numbers::makeOne(); + + $this->assertEquals('1.1752011936', $one->sinh(10)->getValue()); + $this->assertEquals('1.1752012', $one->sinh(7)->getValue()); + $this->assertEquals('1.1752011', $one->sinh(7, false)->getValue()); + + $one = $one->truncateToScale(5); + + $this->assertEquals('1.1752', $one->sinh()->getValue()); + + } + + public function testCosh() + { + + $one = Numbers::makeOne(); + + $this->assertEquals('1.5430806348', $one->cosh(10)->getValue()); + $this->assertEquals('1.543081', $one->cosh(6)->getValue()); + $this->assertEquals('1.54308', $one->cosh(6, false)->getValue()); + + $one = $one->truncateToScale(5); + + $this->assertEquals('1.54308', $one->cosh()->getValue()); + + } + + public function testTanh() + { + + $one = Numbers::makeOne(); + + $this->assertEquals('0.761594156', $one->tanh(10)->getValue()); + $this->assertEquals('0.7615941559', $one->tanh(10, false)->getValue()); + + $one = $one->truncateToScale(5); + + $this->assertEquals('0.76159', $one->tanh()->getValue()); + + } + + public function testSech() + { + + $one = Numbers::makeOne(); + + $this->assertEquals('0.6480542737', $one->sech(10)->getValue()); + $this->assertEquals('0.6480542736', $one->sech(10, false)->getValue()); + + $one = $one->truncateToScale(5); + + $this->assertEquals('0.64805', $one->sech()->getValue()); + + } + + public function testCsch() + { + + $one = Numbers::makeOne(); + + $this->assertEquals('0.8509181282', $one->csch(10)->getValue()); + $this->assertEquals('0.85091812', $one->csch(8, false)->getValue()); + $this->assertEquals('0.85091813', $one->csch(8)->getValue()); + + $one = $one->truncateToScale(5); + + $this->assertEquals('0.85092', $one->csch()->getValue()); + + } + + public function testCoth() + { + + $one = Numbers::makeOne(); + + $this->assertEquals('1.3130352855', $one->coth(10)->getValue()); + $this->assertEquals('1.3130352', $one->coth(7, false)->getValue()); + $this->assertEquals('1.3130353', $one->coth(7)->getValue()); + + $one = $one->truncateToScale(5); + + $this->assertEquals('1.31303', $one->coth()->getValue()); + + } + /** * @medium */ @@ -679,6 +769,30 @@ public function testConverts() $this->assertEquals('14', $five->add($four)->getValue()); $this->assertEquals('20', $five->add($five)->getValue()); + $five = new ImmutableDecimal(10, null, 5); + + $this->assertEquals('10', $five->getValue()); + $this->assertEquals('5', $five->getValue(10)); + $this->assertEquals('5', $five->convertToBase(10)->getValue()); + + $five = new ImmutableDecimal(5, null, 5, true); + + $this->assertEquals('10', $five->getValue()); + $this->assertEquals('5', $five->getValue(10)); + $this->assertEquals('5', $five->convertToBase(10)->getValue()); + + $negFive = new ImmutableDecimal(-10, null, 5); + + $this->assertEquals('-10', $negFive->getValue()); + $this->assertEquals('-5', $negFive->getValue(10)); + $this->assertEquals('-5', $negFive->convertToBase(10)->getValue()); + + $negFive = new ImmutableDecimal(-5, null, 5, true); + + $this->assertEquals('-10', $negFive->getValue()); + $this->assertEquals('-5', $negFive->getValue(10)); + $this->assertEquals('-5', $negFive->convertToBase(10)->getValue()); + } /** * @medium