Fraction.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <?php
  2. /*
  3. * This file is part of the Phospr Fraction package.
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. //namespace Phospr;
  9. //use InvalidArgumentException;
  10. //use Phospr\Exception\Fraction\InvalidDenominatorException;
  11. //use Phospr\Exception\Fraction\InvalidNumeratorException;
  12. /**
  13. * Fraction
  14. *
  15. * Representation of a fraction, e.g. 3/4, 76/123, etc.
  16. *
  17. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  18. * @since 0.1.0
  19. */
  20. class Fraction
  21. {
  22. /**
  23. * From string regex pattern
  24. *
  25. * @var string
  26. */
  27. const PATTERN_FROM_STRING = '#^(-?\d+)(?:(?: (\d+))?/(\d+))?$#';
  28. /**
  29. * numerator
  30. *
  31. * @var integer
  32. */
  33. private $numerator;
  34. /**
  35. * denominator
  36. *
  37. * @var integer
  38. */
  39. private $denominator;
  40. /**
  41. * __construct
  42. *
  43. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  44. * @since 0.1.0
  45. *
  46. * @param integer $numerator
  47. * @param integer $denominator
  48. */
  49. public function __construct($numerator, $denominator = 1)
  50. {
  51. list($numerator, $denominator) = $this->checkLimits(
  52. $numerator, $denominator
  53. );
  54. if (!is_int($numerator)) {
  55. //throw new InvalidNumeratorException(
  56. // 'Numerator must be an integer'
  57. //);
  58. }
  59. if (!is_int($denominator)) {
  60. //throw new InvalidDenominatorException(
  61. // 'Denominator must be an integer'
  62. //);
  63. }
  64. if ((int) $denominator < 1) {
  65. //throw new InvalidDenominatorException(
  66. // 'Denominator must be an integer greater than zero'
  67. //);
  68. }
  69. if (0 == $numerator) {
  70. $this->numerator = (int) 0;
  71. $this->denominator = (int) 1;
  72. return;
  73. }
  74. $this->numerator = (int) $numerator;
  75. $this->denominator = (int) $denominator;
  76. $this->simplify();
  77. }
  78. /**
  79. * Check limits
  80. *
  81. * @param mixed $numerator
  82. * @param mixed $denominator
  83. * @return array
  84. */
  85. protected function checkLimits($numerator, $denominator)
  86. {
  87. if (($max = max(abs($numerator), abs($denominator))) < PHP_INT_MAX) {
  88. return [$numerator, $denominator];
  89. }
  90. $divisor = min(
  91. abs($this->getDivisor($max)),
  92. abs($numerator),
  93. abs($denominator)
  94. );
  95. return [
  96. intval($numerator / $divisor),
  97. intval($denominator / $divisor),
  98. ];
  99. }
  100. /**
  101. * Get divisor
  102. *
  103. * @param mixed $number
  104. * @return integer
  105. */
  106. protected function getDivisor($number)
  107. {
  108. $divisor = 1;
  109. while ($number >= PHP_INT_MAX) {
  110. $divisor *= 10;
  111. $number /= 10;
  112. }
  113. return $divisor;
  114. }
  115. /**
  116. * __toString
  117. *
  118. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  119. * @since 0.1.0
  120. *
  121. * @return string
  122. */
  123. public function __toString()
  124. {
  125. if ($this->numerator === $this->denominator) {
  126. return '1';
  127. }
  128. if (-1*$this->numerator === $this->denominator) {
  129. return '-1';
  130. }
  131. if (1 === $this->denominator) {
  132. return (string) $this->numerator;
  133. }
  134. if (abs($this->numerator) > abs($this->denominator)) {
  135. $whole = floor(abs($this->numerator) / $this->denominator);
  136. if ($this->numerator < 0) {
  137. $whole *= -1;
  138. }
  139. return sprintf('%d %d/%d',
  140. $whole,
  141. abs($this->numerator % $this->denominator),
  142. $this->denominator
  143. );
  144. }
  145. return sprintf('%d/%d',
  146. $this->numerator,
  147. $this->denominator
  148. );
  149. }
  150. /**
  151. * Get numerator
  152. *
  153. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  154. * @since 0.1.0
  155. *
  156. * @return integer
  157. */
  158. public function getNumerator()
  159. {
  160. return $this->numerator;
  161. }
  162. /**
  163. * Get denominator
  164. *
  165. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  166. * @since 0.1.0
  167. *
  168. * @return integer
  169. */
  170. public function getDenominator()
  171. {
  172. return $this->denominator;
  173. }
  174. /**
  175. * Simplify
  176. *
  177. * e.g. transform 2/4 into 1/2
  178. *
  179. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  180. * @since 0.1.0
  181. *
  182. * @return integer
  183. */
  184. private function simplify()
  185. {
  186. $gcd = $this->getGreatestCommonDivisor();
  187. $this->numerator /= $gcd;
  188. $this->denominator /= $gcd;
  189. }
  190. /**
  191. * Get greatest common divisor
  192. *
  193. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  194. * @since 0.1.0
  195. *
  196. * @return integer
  197. */
  198. private function getGreatestCommonDivisor()
  199. {
  200. $a = $this->numerator;
  201. $b = $this->denominator;
  202. // ensure no negative values
  203. $a = abs($a);
  204. $b = abs($b);
  205. // ensure $a is greater than $b
  206. if ($a < $b) {
  207. list($b, $a) = array($a, $b);
  208. }
  209. // see if $b is already the greatest common divisor
  210. $r = $a % $b;
  211. // if not, then keep trying
  212. while ($r > 0) {
  213. $a = $b;
  214. $b = $r;
  215. $r = $a % $b;
  216. }
  217. return $b;
  218. }
  219. /**
  220. * Multiply this fraction by a given fraction
  221. *
  222. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  223. * @since 0.1.0
  224. *
  225. * @param Fraction $fraction
  226. *
  227. * @return Fraction
  228. */
  229. public function multiply(Fraction $fraction)
  230. {
  231. $numerator = $this->getNumerator() * $fraction->getNumerator();
  232. $denominator = $this->getDenominator() * $fraction->getDenominator();
  233. return new static($numerator, $denominator);
  234. }
  235. /**
  236. * Divide this fraction by a given fraction
  237. *
  238. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  239. * @since 0.1.0
  240. *
  241. * @param Fraction $fraction
  242. *
  243. * @return Fraction
  244. */
  245. public function divide(Fraction $fraction)
  246. {
  247. $numerator = $this->getNumerator() * $fraction->getDenominator();
  248. $denominator = $this->getDenominator() * $fraction->getNumerator();
  249. if ($denominator < 0) {
  250. $numerator *= -1;
  251. }
  252. return new static($numerator, abs($denominator));
  253. }
  254. /**
  255. * Add this fraction to a given fraction
  256. *
  257. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  258. * @since 0.1.0
  259. *
  260. * @param Fraction $fraction
  261. *
  262. * @return Fraction
  263. */
  264. public function add(Fraction $fraction)
  265. {
  266. $numerator = ($this->getNumerator() * $fraction->getDenominator())
  267. + ($fraction->getNumerator() * $this->getDenominator());
  268. $denominator = $this->getDenominator()
  269. * $fraction->getDenominator();
  270. return new static($numerator, $denominator);
  271. }
  272. /**
  273. * Subtract a given fraction from this fraction
  274. *
  275. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  276. * @since 0.1.0
  277. *
  278. * @param Fraction $fraction
  279. *
  280. * @return Fraction
  281. */
  282. public function subtract(Fraction $fraction)
  283. {
  284. $numerator = ($this->getNumerator() * $fraction->getDenominator())
  285. - ($fraction->getNumerator() * $this->getDenominator());
  286. $denominator = $this->getDenominator()
  287. * $fraction->getDenominator();
  288. return new static($numerator, $denominator);
  289. }
  290. /**
  291. * Whether or not this fraction is an integer
  292. *
  293. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  294. * @since 0.1.0
  295. *
  296. * @return boolean
  297. */
  298. public function isInteger()
  299. {
  300. return (1 === $this->getDenominator());
  301. }
  302. /**
  303. * Create from float
  304. *
  305. * @author Christopher Tatro <c.m.tatro@gmail.com>
  306. * @since 0.2.0
  307. *
  308. * @param float $float
  309. *
  310. * return Fraction
  311. */
  312. public static function fromFloat($float)
  313. {
  314. if (is_int($float)) {
  315. return new self($float);
  316. }
  317. if (!is_numeric($float)) {
  318. //throw new InvalidArgumentException(
  319. // 'Argument passed is not a numeric value.'
  320. //);
  321. }
  322. // Make sure the float is a float not scientific notation.
  323. // Limit a max of 8 chars to prevent float errors
  324. $float = rtrim(sprintf('%.8F', $float), '0');
  325. // Find and grab the decimal space and everything after it
  326. $denominator = strstr($float, '.');
  327. // Pad a one with zeros for the length of the decimal places
  328. // ie 0.1 = 10; 0.02 = 100; 0.01234 = 100000;
  329. $denominator = (int) str_pad('1', strlen($denominator), '0');
  330. // Multiply to get rid of the decimal places.
  331. $numerator = (int) ($float*$denominator);
  332. return new self($numerator, $denominator);
  333. }
  334. /**
  335. * Create from string, e.g.
  336. *
  337. * * 1/3
  338. * * 1/20
  339. * * 40
  340. * * 3 4/5
  341. * * 20 34/67
  342. *
  343. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  344. * @since 0.4.0
  345. *
  346. * @param string $string
  347. *
  348. * return Fraction
  349. */
  350. public static function fromString($string)
  351. {
  352. if (preg_match(self::PATTERN_FROM_STRING, trim($string), $matches)) {
  353. if (2 === count($matches)) {
  354. // whole number
  355. return new self((int) $matches[1]);
  356. } else {
  357. // either x y/z or x/y
  358. if ($matches[2]) {
  359. // x y/z
  360. $whole = new self((int) $matches[1]);
  361. return $whole->add(new self(
  362. (int) $matches[2],
  363. (int) $matches[3]
  364. ));
  365. }
  366. // x/y
  367. return new self((int) $matches[1], (int) $matches[3]);
  368. }
  369. }
  370. //throw new InvalidArgumentException(sprintf(
  371. // 'Cannot parse "%s"',
  372. // $string
  373. //));
  374. }
  375. /**
  376. * Get value as float
  377. *
  378. * @author Tom Haskins-Vaughan <tom@tomhv.uk>
  379. * @since 0.1.0
  380. *
  381. * @return float
  382. */
  383. public function toFloat()
  384. {
  385. return $this->getNumerator()/$this->getDenominator();
  386. }
  387. /**
  388. * isSameValueAs
  389. *
  390. * ValueObject comparison
  391. *
  392. * @author Christopher Tatro <c.m.tatro@gmail.com>
  393. * @since 1.1.0
  394. *
  395. * @param Fraction $fraction
  396. *
  397. * @return bool
  398. */
  399. public function isSameValueAs(Fraction $fraction)
  400. {
  401. if ($this->getNumerator() != $fraction->getNumerator()) {
  402. return false;
  403. }
  404. if ($this->getDenominator() != $fraction->getDenominator()) {
  405. return false;
  406. }
  407. return true;
  408. }
  409. }