BigNumber.php 16 KB

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