NumberInChunks.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <?php
  2. namespace IPLib\Service;
  3. use InvalidArgumentException;
  4. /**
  5. * @internal
  6. *
  7. * @readonly
  8. */
  9. class NumberInChunks
  10. {
  11. const CHUNKSIZE_BYTES = 8;
  12. const CHUNKSIZE_WORDS = 16;
  13. /**
  14. * @var bool
  15. */
  16. public $negative;
  17. /**
  18. * @var int[]
  19. */
  20. public $chunks;
  21. /**
  22. * @var int
  23. */
  24. public $chunkSize;
  25. /**
  26. * @param bool $negative
  27. * @param int[] $chunks
  28. * @param int $chunkSize
  29. */
  30. public function __construct($negative, array $chunks, $chunkSize)
  31. {
  32. $this->negative = $negative;
  33. $this->chunks = $chunks;
  34. $this->chunkSize = $chunkSize;
  35. }
  36. /**
  37. * @throws \InvalidArgumentException if $other has a $chunkSize that's not the same as the $chunkSize of this
  38. *
  39. * @return \IPLib\Service\NumberInChunks
  40. */
  41. public function negate()
  42. {
  43. return new self($this->chunks === array(0) ? false : !$this->negative, $this->chunks, $this->chunkSize);
  44. }
  45. /**
  46. * @throws \InvalidArgumentException if $other has a $chunkSize that's not the same as the $chunkSize of this
  47. *
  48. * @return \IPLib\Service\NumberInChunks
  49. */
  50. public function add(NumberInChunks $that)
  51. {
  52. if ($this->chunkSize !== $that->chunkSize) {
  53. throw new InvalidArgumentException('Incompatible chunk size');
  54. }
  55. if ($this->negative === $that->negative) {
  56. return new self($this->negative, self::addChunks($this->chunks, $that->chunks, $this->chunkSize), $this->chunkSize);
  57. }
  58. if ($that->negative) {
  59. list($negative, $chunks) = self::substractChunks($this->chunks, $that->chunks, $this->chunkSize);
  60. } else {
  61. list($negative, $chunks) = self::substractChunks($that->chunks, $this->chunks, $this->chunkSize);
  62. }
  63. return new self($negative, $chunks, $this->chunkSize);
  64. }
  65. /**
  66. * @param int $int
  67. * @param int $chunkSize
  68. *
  69. * @return \IPLib\Service\NumberInChunks
  70. */
  71. public static function fromInteger($int, $chunkSize)
  72. {
  73. if ($int === 0) {
  74. return new self(false, array(0), $chunkSize);
  75. }
  76. $negative = $int < 0;
  77. if ($negative) {
  78. $positiveInt = -$int;
  79. /** @var int|float $positiveInt may be float because -PHP_INT_MIN is bigger than PHP_INT_MAX */
  80. if (is_float($positiveInt)) {
  81. return self::fromNumericString((string) $int, $chunkSize);
  82. }
  83. $int = $positiveInt;
  84. }
  85. $bitMask = (1 << $chunkSize) - 1;
  86. $chunks = array();
  87. while ($int !== 0) {
  88. $chunks[] = $int & $bitMask;
  89. $int >>= $chunkSize;
  90. }
  91. return new self($negative, array_reverse($chunks), $chunkSize);
  92. }
  93. /**
  94. * @param string $numericString a string normalized with BinaryMath::normalizeIntegerString()
  95. * @param int $chunkSize
  96. *
  97. * @return \IPLib\Service\NumberInChunks
  98. */
  99. public static function fromNumericString($numericString, $chunkSize)
  100. {
  101. if ($numericString === '0') {
  102. return new self(false, array(0), $chunkSize);
  103. }
  104. $negative = $numericString[0] === '-';
  105. if ($negative) {
  106. $numericString = substr($numericString, 1);
  107. }
  108. $chunks = array();
  109. while ($numericString !== '0') {
  110. $chunks[] = self::modulo($numericString, $chunkSize);
  111. $numericString = self::divide($numericString, $chunkSize);
  112. }
  113. return new self($negative, array_reverse($chunks), $chunkSize);
  114. }
  115. /**
  116. * @param string $numericString
  117. * @param int $chunkSize
  118. *
  119. * @return int
  120. */
  121. private static function modulo($numericString, $chunkSize)
  122. {
  123. $divisor = 1 << $chunkSize;
  124. $carry = 0;
  125. $len = strlen($numericString);
  126. for ($i = 0; $i < $len; $i++) {
  127. $digit = (int) $numericString[$i];
  128. $carry = ($carry * 10 + $digit) % $divisor;
  129. }
  130. return $carry;
  131. }
  132. /**
  133. * @param string $numericString
  134. * @param int $chunkSize
  135. *
  136. * @return string
  137. */
  138. private static function divide($numericString, $chunkSize)
  139. {
  140. $divisor = 1 << $chunkSize;
  141. $quotient = '';
  142. $carry = 0;
  143. $len = strlen($numericString);
  144. for ($i = 0; $i < $len; $i++) {
  145. $digit = (int) $numericString[$i];
  146. $value = $carry * 10 + $digit;
  147. $quotient .= (string) ($value >> $chunkSize);
  148. $carry = $value % $divisor;
  149. }
  150. return ltrim($quotient, '0') ?: '0';
  151. }
  152. /**
  153. * @param int[] $addend1
  154. * @param int[] $addend2
  155. * @param int $chunkSize
  156. *
  157. * @return int[]
  158. */
  159. private static function addChunks(array $addend1, array $addend2, $chunkSize)
  160. {
  161. $divisor = 1 << $chunkSize;
  162. $result = array();
  163. $carry = 0;
  164. while ($addend1 !== array() || $addend2 !== array()) {
  165. $sum = $carry + (array_pop($addend1) ?: 0) + (array_pop($addend2) ?: 0);
  166. $result[] = $sum % $divisor;
  167. $carry = $sum >> $chunkSize;
  168. }
  169. if ($carry !== 0) {
  170. $result[] = $carry;
  171. }
  172. return array_reverse($result);
  173. }
  174. /**
  175. * @param int[] $minuend
  176. * @param int[] $subtrahend
  177. * @param int $chunkSize
  178. *
  179. * @return array{bool, int[]}
  180. */
  181. private static function substractChunks(array $minuend, array $subtrahend, $chunkSize)
  182. {
  183. $minuendCount = count($minuend);
  184. $subtrahendCount = count($subtrahend);
  185. if ($minuendCount > $subtrahendCount) {
  186. $count = $minuendCount;
  187. $negative = false;
  188. } elseif ($minuendCount < $subtrahendCount) {
  189. $count = $subtrahendCount;
  190. $negative = true;
  191. } else {
  192. $count = $minuendCount;
  193. $negative = false;
  194. for ($i = 0; $i < $count; $i++) {
  195. $delta = $minuend[$i] - $subtrahend[$i];
  196. if ($delta === 0) {
  197. continue;
  198. }
  199. if ($delta < 0) {
  200. $negative = true;
  201. }
  202. break;
  203. }
  204. }
  205. if ($negative) {
  206. list($minuend, $subtrahend) = array($subtrahend, $minuend);
  207. }
  208. $subtrahend = array_pad($subtrahend, -$count, 0);
  209. $borrowValue = 1 << $chunkSize;
  210. $result = array();
  211. $borrow = 0;
  212. for ($i = $count - 1; $i >= 0; $i--) {
  213. $value = $minuend[$i] - $subtrahend[$i] - $borrow;
  214. if ($value < 0) {
  215. $value += $borrowValue;
  216. $borrow = 1;
  217. } else {
  218. $borrow = 0;
  219. }
  220. $result[] = $value;
  221. }
  222. while (isset($result[1])) {
  223. $value = array_pop($result);
  224. if ($value !== 0) {
  225. $result[] = $value;
  226. break;
  227. }
  228. }
  229. return array($negative, array_reverse($result));
  230. }
  231. }