NumberInChunks.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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. if (is_float($positiveInt)) { // -PHP_INT_MIN is bigger than PHP_INT_MAX
  80. return self::fromNumericString((string) $int, $chunkSize);
  81. }
  82. $int = $positiveInt;
  83. }
  84. $bitMask = (1 << $chunkSize) - 1;
  85. $chunks = array();
  86. while ($int !== 0) {
  87. $chunks[] = $int & $bitMask;
  88. $int >>= $chunkSize;
  89. }
  90. return new self($negative, array_reverse($chunks), $chunkSize);
  91. }
  92. /**
  93. * @param string $numericString a string normalized with BinaryMath::normalizeIntegerString()
  94. * @param int $chunkSize
  95. *
  96. * @return \IPLib\Service\NumberInChunks
  97. */
  98. public static function fromNumericString($numericString, $chunkSize)
  99. {
  100. if ($numericString === '0') {
  101. return new self(false, array(0), $chunkSize);
  102. }
  103. $negative = $numericString[0] === '-';
  104. if ($negative) {
  105. $numericString = substr($numericString, 1);
  106. }
  107. $chunks = array();
  108. while ($numericString !== '0') {
  109. $chunks[] = self::modulo($numericString, $chunkSize);
  110. $numericString = self::divide($numericString, $chunkSize);
  111. }
  112. return new self($negative, array_reverse($chunks), $chunkSize);
  113. }
  114. /**
  115. * @param string $numericString
  116. * @param int $chunkSize
  117. *
  118. * @return int
  119. */
  120. private static function modulo($numericString, $chunkSize)
  121. {
  122. $divisor = 1 << $chunkSize;
  123. $carry = 0;
  124. $len = strlen($numericString);
  125. for ($i = 0; $i < $len; $i++) {
  126. $digit = (int) $numericString[$i];
  127. $carry = ($carry * 10 + $digit) % $divisor;
  128. }
  129. return $carry;
  130. }
  131. /**
  132. * @param string $numericString
  133. * @param int $chunkSize
  134. *
  135. * @return string
  136. */
  137. private static function divide($numericString, $chunkSize)
  138. {
  139. $divisor = 1 << $chunkSize;
  140. $quotient = '';
  141. $carry = 0;
  142. $len = strlen($numericString);
  143. for ($i = 0; $i < $len; $i++) {
  144. $digit = (int) $numericString[$i];
  145. $value = $carry * 10 + $digit;
  146. $quotient .= (string) ($value >> $chunkSize);
  147. $carry = $value % $divisor;
  148. }
  149. return ltrim($quotient, '0') ?: '0';
  150. }
  151. /**
  152. * @param int[] $addend1
  153. * @param int[] $addend2
  154. * @param int $chunkSize
  155. *
  156. * @return int[]
  157. */
  158. private static function addChunks(array $addend1, array $addend2, $chunkSize)
  159. {
  160. $divisor = 1 << $chunkSize;
  161. $result = array();
  162. $carry = 0;
  163. while ($addend1 !== array() || $addend2 !== array()) {
  164. $sum = $carry + (array_pop($addend1) ?: 0) + (array_pop($addend2) ?: 0);
  165. $result[] = $sum % $divisor;
  166. $carry = $sum >> $chunkSize;
  167. }
  168. if ($carry !== 0) {
  169. $result[] = $carry;
  170. }
  171. return array_reverse($result);
  172. }
  173. /**
  174. * @param int[] $minuend
  175. * @param int[] $subtrahend
  176. * @param int $chunkSize
  177. *
  178. * @return array
  179. */
  180. private static function substractChunks(array $minuend, array $subtrahend, $chunkSize)
  181. {
  182. $minuendCount = count($minuend);
  183. $subtrahendCount = count($subtrahend);
  184. if ($minuendCount > $subtrahendCount) {
  185. $count = $minuendCount;
  186. $negative = false;
  187. } elseif ($minuendCount < $subtrahendCount) {
  188. $count = $subtrahendCount;
  189. $negative = true;
  190. } else {
  191. $count = $minuendCount;
  192. $negative = false;
  193. for ($i = 0; $i < $count; $i++) {
  194. $delta = $minuend[$i] - $subtrahend[$i];
  195. if ($delta === 0) {
  196. continue;
  197. }
  198. if ($delta < 0) {
  199. $negative = true;
  200. }
  201. break;
  202. }
  203. }
  204. if ($negative) {
  205. list($minuend, $subtrahend) = array($subtrahend, $minuend);
  206. }
  207. $subtrahend = array_pad($subtrahend, -$count, 0);
  208. $borrowValue = 1 << $chunkSize;
  209. $result = array();
  210. $borrow = 0;
  211. for ($i = $count - 1; $i >= 0; $i--) {
  212. $value = $minuend[$i] - $subtrahend[$i] - $borrow;
  213. if ($value < 0) {
  214. $value += $borrowValue;
  215. $borrow = 1;
  216. } else {
  217. $borrow = 0;
  218. }
  219. $result[] = $value;
  220. }
  221. while (isset($result[1])) {
  222. $value = array_pop($result);
  223. if ($value !== 0) {
  224. $result[] = $value;
  225. break;
  226. }
  227. }
  228. return array($negative, array_reverse($result));
  229. }
  230. }