BigNumber.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. <?php
  2. declare(strict_types=1);
  3. namespace Brick\Math;
  4. use Brick\Math\Exception\DivisionByZeroException;
  5. use Brick\Math\Exception\MathException;
  6. use Brick\Math\Exception\NumberFormatException;
  7. use Brick\Math\Exception\RoundingNecessaryException;
  8. /**
  9. * Common interface for arbitrary-precision rational numbers.
  10. *
  11. * @psalm-immutable
  12. */
  13. abstract class BigNumber implements \JsonSerializable
  14. {
  15. /**
  16. * The regular expression used to parse integer or decimal numbers.
  17. */
  18. private const PARSE_REGEXP_NUMERICAL =
  19. '/^' .
  20. '(?<sign>[\-\+])?' .
  21. '(?<integral>[0-9]+)?' .
  22. '(?<point>\.)?' .
  23. '(?<fractional>[0-9]+)?' .
  24. '(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
  25. '$/';
  26. /**
  27. * The regular expression used to parse rational numbers.
  28. */
  29. private const PARSE_REGEXP_RATIONAL =
  30. '/^' .
  31. '(?<sign>[\-\+])?' .
  32. '(?<numerator>[0-9]+)' .
  33. '\/?' .
  34. '(?<denominator>[0-9]+)' .
  35. '$/';
  36. /**
  37. * Creates a BigNumber of the given value.
  38. *
  39. * The concrete return type is dependent on the given value, with the following rules:
  40. *
  41. * - BigNumber instances are returned as is
  42. * - integer numbers are returned as BigInteger
  43. * - floating point numbers are converted to a string then parsed as such
  44. * - strings containing a `/` character are returned as BigRational
  45. * - strings containing a `.` character or using an exponential notation are returned as BigDecimal
  46. * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
  47. *
  48. * @throws NumberFormatException If the format of the number is not valid.
  49. * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
  50. *
  51. * @psalm-pure
  52. */
  53. final public static function of(BigNumber|int|float|string $value) : static
  54. {
  55. $value = self::_of($value);
  56. if (static::class === BigNumber::class) {
  57. // https://github.com/vimeo/psalm/issues/10309
  58. assert($value instanceof static);
  59. return $value;
  60. }
  61. return static::from($value);
  62. }
  63. /**
  64. * @psalm-pure
  65. */
  66. private static function _of(BigNumber|int|float|string $value) : BigNumber
  67. {
  68. if ($value instanceof BigNumber) {
  69. return $value;
  70. }
  71. if (\is_int($value)) {
  72. return new BigInteger((string) $value);
  73. }
  74. if (is_float($value)) {
  75. $value = (string) $value;
  76. }
  77. if (str_contains($value, '/')) {
  78. // Rational number
  79. if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
  80. throw NumberFormatException::invalidFormat($value);
  81. }
  82. $sign = $matches['sign'];
  83. $numerator = $matches['numerator'];
  84. $denominator = $matches['denominator'];
  85. assert($numerator !== null);
  86. assert($denominator !== null);
  87. $numerator = self::cleanUp($sign, $numerator);
  88. $denominator = self::cleanUp(null, $denominator);
  89. if ($denominator === '0') {
  90. throw DivisionByZeroException::denominatorMustNotBeZero();
  91. }
  92. return new BigRational(
  93. new BigInteger($numerator),
  94. new BigInteger($denominator),
  95. false
  96. );
  97. } else {
  98. // Integer or decimal number
  99. if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
  100. throw NumberFormatException::invalidFormat($value);
  101. }
  102. $sign = $matches['sign'];
  103. $point = $matches['point'];
  104. $integral = $matches['integral'];
  105. $fractional = $matches['fractional'];
  106. $exponent = $matches['exponent'];
  107. if ($integral === null && $fractional === null) {
  108. throw NumberFormatException::invalidFormat($value);
  109. }
  110. if ($integral === null) {
  111. $integral = '0';
  112. }
  113. if ($point !== null || $exponent !== null) {
  114. $fractional = ($fractional ?? '');
  115. $exponent = ($exponent !== null) ? (int)$exponent : 0;
  116. if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
  117. throw new NumberFormatException('Exponent too large.');
  118. }
  119. $unscaledValue = self::cleanUp($sign, $integral . $fractional);
  120. $scale = \strlen($fractional) - $exponent;
  121. if ($scale < 0) {
  122. if ($unscaledValue !== '0') {
  123. $unscaledValue .= \str_repeat('0', -$scale);
  124. }
  125. $scale = 0;
  126. }
  127. return new BigDecimal($unscaledValue, $scale);
  128. }
  129. $integral = self::cleanUp($sign, $integral);
  130. return new BigInteger($integral);
  131. }
  132. }
  133. /**
  134. * Overridden by subclasses to convert a BigNumber to an instance of the subclass.
  135. *
  136. * @throws MathException If the value cannot be converted.
  137. *
  138. * @psalm-pure
  139. */
  140. abstract protected static function from(BigNumber $number): static;
  141. /**
  142. * Proxy method to access BigInteger's protected constructor from sibling classes.
  143. *
  144. * @internal
  145. * @psalm-pure
  146. */
  147. final protected function newBigInteger(string $value) : BigInteger
  148. {
  149. return new BigInteger($value);
  150. }
  151. /**
  152. * Proxy method to access BigDecimal's protected constructor from sibling classes.
  153. *
  154. * @internal
  155. * @psalm-pure
  156. */
  157. final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
  158. {
  159. return new BigDecimal($value, $scale);
  160. }
  161. /**
  162. * Proxy method to access BigRational's protected constructor from sibling classes.
  163. *
  164. * @internal
  165. * @psalm-pure
  166. */
  167. final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
  168. {
  169. return new BigRational($numerator, $denominator, $checkDenominator);
  170. }
  171. /**
  172. * Returns the minimum of the given values.
  173. *
  174. * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
  175. * to an instance of the class this method is called on.
  176. *
  177. * @throws \InvalidArgumentException If no values are given.
  178. * @throws MathException If an argument is not valid.
  179. *
  180. * @psalm-pure
  181. */
  182. final public static function min(BigNumber|int|float|string ...$values) : static
  183. {
  184. $min = null;
  185. foreach ($values as $value) {
  186. $value = static::of($value);
  187. if ($min === null || $value->isLessThan($min)) {
  188. $min = $value;
  189. }
  190. }
  191. if ($min === null) {
  192. throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
  193. }
  194. return $min;
  195. }
  196. /**
  197. * Returns the maximum of the given values.
  198. *
  199. * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
  200. * to an instance of the class this method is called on.
  201. *
  202. * @throws \InvalidArgumentException If no values are given.
  203. * @throws MathException If an argument is not valid.
  204. *
  205. * @psalm-pure
  206. */
  207. final public static function max(BigNumber|int|float|string ...$values) : static
  208. {
  209. $max = null;
  210. foreach ($values as $value) {
  211. $value = static::of($value);
  212. if ($max === null || $value->isGreaterThan($max)) {
  213. $max = $value;
  214. }
  215. }
  216. if ($max === null) {
  217. throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
  218. }
  219. return $max;
  220. }
  221. /**
  222. * Returns the sum of the given values.
  223. *
  224. * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
  225. * to an instance of the class this method is called on.
  226. *
  227. * @throws \InvalidArgumentException If no values are given.
  228. * @throws MathException If an argument is not valid.
  229. *
  230. * @psalm-pure
  231. */
  232. final public static function sum(BigNumber|int|float|string ...$values) : static
  233. {
  234. /** @var static|null $sum */
  235. $sum = null;
  236. foreach ($values as $value) {
  237. $value = static::of($value);
  238. $sum = $sum === null ? $value : self::add($sum, $value);
  239. }
  240. if ($sum === null) {
  241. throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
  242. }
  243. return $sum;
  244. }
  245. /**
  246. * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
  247. *
  248. * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
  249. * concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
  250. * depending on their ability to perform the operation. This will also require a version bump because we're
  251. * potentially breaking custom BigNumber implementations (if any...)
  252. *
  253. * @psalm-pure
  254. */
  255. private static function add(BigNumber $a, BigNumber $b) : BigNumber
  256. {
  257. if ($a instanceof BigRational) {
  258. return $a->plus($b);
  259. }
  260. if ($b instanceof BigRational) {
  261. return $b->plus($a);
  262. }
  263. if ($a instanceof BigDecimal) {
  264. return $a->plus($b);
  265. }
  266. if ($b instanceof BigDecimal) {
  267. return $b->plus($a);
  268. }
  269. /** @var BigInteger $a */
  270. return $a->plus($b);
  271. }
  272. /**
  273. * Removes optional leading zeros and applies sign.
  274. *
  275. * @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
  276. * @param string $number The number, validated as a non-empty string of digits.
  277. *
  278. * @psalm-pure
  279. */
  280. private static function cleanUp(string|null $sign, string $number) : string
  281. {
  282. $number = \ltrim($number, '0');
  283. if ($number === '') {
  284. return '0';
  285. }
  286. return $sign === '-' ? '-' . $number : $number;
  287. }
  288. /**
  289. * Checks if this number is equal to the given one.
  290. */
  291. final public function isEqualTo(BigNumber|int|float|string $that) : bool
  292. {
  293. return $this->compareTo($that) === 0;
  294. }
  295. /**
  296. * Checks if this number is strictly lower than the given one.
  297. */
  298. final public function isLessThan(BigNumber|int|float|string $that) : bool
  299. {
  300. return $this->compareTo($that) < 0;
  301. }
  302. /**
  303. * Checks if this number is lower than or equal to the given one.
  304. */
  305. final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
  306. {
  307. return $this->compareTo($that) <= 0;
  308. }
  309. /**
  310. * Checks if this number is strictly greater than the given one.
  311. */
  312. final public function isGreaterThan(BigNumber|int|float|string $that) : bool
  313. {
  314. return $this->compareTo($that) > 0;
  315. }
  316. /**
  317. * Checks if this number is greater than or equal to the given one.
  318. */
  319. final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
  320. {
  321. return $this->compareTo($that) >= 0;
  322. }
  323. /**
  324. * Checks if this number equals zero.
  325. */
  326. final public function isZero() : bool
  327. {
  328. return $this->getSign() === 0;
  329. }
  330. /**
  331. * Checks if this number is strictly negative.
  332. */
  333. final public function isNegative() : bool
  334. {
  335. return $this->getSign() < 0;
  336. }
  337. /**
  338. * Checks if this number is negative or zero.
  339. */
  340. final public function isNegativeOrZero() : bool
  341. {
  342. return $this->getSign() <= 0;
  343. }
  344. /**
  345. * Checks if this number is strictly positive.
  346. */
  347. final public function isPositive() : bool
  348. {
  349. return $this->getSign() > 0;
  350. }
  351. /**
  352. * Checks if this number is positive or zero.
  353. */
  354. final public function isPositiveOrZero() : bool
  355. {
  356. return $this->getSign() >= 0;
  357. }
  358. /**
  359. * Returns the sign of this number.
  360. *
  361. * @psalm-return -1|0|1
  362. *
  363. * @return int -1 if the number is negative, 0 if zero, 1 if positive.
  364. */
  365. abstract public function getSign() : int;
  366. /**
  367. * Compares this number to the given one.
  368. *
  369. * @psalm-return -1|0|1
  370. *
  371. * @return int -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
  372. *
  373. * @throws MathException If the number is not valid.
  374. */
  375. abstract public function compareTo(BigNumber|int|float|string $that) : int;
  376. /**
  377. * Converts this number to a BigInteger.
  378. *
  379. * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
  380. */
  381. abstract public function toBigInteger() : BigInteger;
  382. /**
  383. * Converts this number to a BigDecimal.
  384. *
  385. * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
  386. */
  387. abstract public function toBigDecimal() : BigDecimal;
  388. /**
  389. * Converts this number to a BigRational.
  390. */
  391. abstract public function toBigRational() : BigRational;
  392. /**
  393. * Converts this number to a BigDecimal with the given scale, using rounding if necessary.
  394. *
  395. * @param int $scale The scale of the resulting `BigDecimal`.
  396. * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
  397. *
  398. * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
  399. * This only applies when RoundingMode::UNNECESSARY is used.
  400. */
  401. abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
  402. /**
  403. * Returns the exact value of this number as a native integer.
  404. *
  405. * If this number cannot be converted to a native integer without losing precision, an exception is thrown.
  406. * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
  407. *
  408. * @throws MathException If this number cannot be exactly converted to a native integer.
  409. */
  410. abstract public function toInt() : int;
  411. /**
  412. * Returns an approximation of this number as a floating-point value.
  413. *
  414. * Note that this method can discard information as the precision of a floating-point value
  415. * is inherently limited.
  416. *
  417. * If the number is greater than the largest representable floating point number, positive infinity is returned.
  418. * If the number is less than the smallest representable floating point number, negative infinity is returned.
  419. */
  420. abstract public function toFloat() : float;
  421. /**
  422. * Returns a string representation of this number.
  423. *
  424. * The output of this method can be parsed by the `of()` factory method;
  425. * this will yield an object equal to this one, without any information loss.
  426. */
  427. abstract public function __toString() : string;
  428. final public function jsonSerialize() : string
  429. {
  430. return $this->__toString();
  431. }
  432. }