NumberInChunks.php 6.8 KB

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