BinaryMath.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. namespace IPLib\Service;
  3. /**
  4. * Helper class to work with unsigned binary integers.
  5. *
  6. * @internal
  7. */
  8. class BinaryMath
  9. {
  10. /**
  11. * @var \IPLib\Service\BinaryMath|null
  12. */
  13. private static $instance;
  14. /**
  15. * @return \IPLib\Service\BinaryMath
  16. */
  17. public static function getInstance()
  18. {
  19. if (self::$instance === null) {
  20. self::$instance = new self();
  21. }
  22. return self::$instance;
  23. }
  24. /**
  25. * Trim the leading zeroes from a non-negative integer represented in binary form.
  26. *
  27. * @param string $value
  28. *
  29. * @return string
  30. */
  31. public function reduce($value)
  32. {
  33. $value = ltrim($value, '0');
  34. return $value === '' ? '0' : $value;
  35. }
  36. /**
  37. * Compare two non-negative integers represented in binary form.
  38. *
  39. * @param string $a
  40. * @param string $b
  41. *
  42. * @return int 1 if $a is greater than $b, -1 if $b is greater than $b, 0 if they are the same
  43. */
  44. public function compare($a, $b)
  45. {
  46. list($a, $b) = $this->toSameLength($a, $b);
  47. return $a < $b ? -1 : ($a > $b ? 1 : 0);
  48. }
  49. /**
  50. * Add 1 to a non-negative integer represented in binary form.
  51. *
  52. * @param string $value
  53. *
  54. * @return string
  55. */
  56. public function increment($value)
  57. {
  58. $lastZeroIndex = strrpos($value, '0');
  59. if ($lastZeroIndex === false) {
  60. return '1' . str_repeat('0', strlen($value));
  61. }
  62. return ltrim(substr($value, 0, $lastZeroIndex), '0') . '1' . str_repeat('0', strlen($value) - $lastZeroIndex - 1);
  63. }
  64. /**
  65. * Calculate the bitwise AND of two non-negative integers represented in binary form.
  66. *
  67. * @param string $operand1
  68. * @param string $operand2
  69. *
  70. * @return string
  71. */
  72. public function andX($operand1, $operand2)
  73. {
  74. $operand1 = $this->reduce($operand1);
  75. $operand2 = $this->reduce($operand2);
  76. $numBits = min(strlen($operand1), strlen($operand2));
  77. $operand1 = substr(str_pad($operand1, $numBits, '0', STR_PAD_LEFT), -$numBits);
  78. $operand2 = substr(str_pad($operand2, $numBits, '0', STR_PAD_LEFT), -$numBits);
  79. $result = '';
  80. for ($index = 0; $index < $numBits; $index++) {
  81. $result .= $operand1[$index] === '1' && $operand2[$index] === '1' ? '1' : '0';
  82. }
  83. return $this->reduce($result);
  84. }
  85. /**
  86. * Calculate the bitwise OR of two non-negative integers represented in binary form.
  87. *
  88. * @param string $operand1
  89. * @param string $operand2
  90. *
  91. * @return string
  92. */
  93. public function orX($operand1, $operand2)
  94. {
  95. list($operand1, $operand2, $numBits) = $this->toSameLength($operand1, $operand2);
  96. $result = '';
  97. for ($index = 0; $index < $numBits; $index++) {
  98. $result .= $operand1[$index] === '1' || $operand2[$index] === '1' ? '1' : '0';
  99. }
  100. return $result;
  101. }
  102. /**
  103. * Compute 2 raised to the given exponent.
  104. *
  105. * If the result fits into a native PHP integer, an int is returned.
  106. * If the result exceeds PHP_INT_MAX, a string containing the exact decimal representation is returned.
  107. *
  108. * @param int $exponent The non-negative exponent
  109. *
  110. * @return int|numeric-string
  111. */
  112. public function pow2string($exponent)
  113. {
  114. if ($exponent < PHP_INT_SIZE * 8 - 1) {
  115. return 1 << $exponent;
  116. }
  117. $digits = array(1);
  118. for ($i = 0; $i < $exponent; $i++) {
  119. $carry = 0;
  120. foreach ($digits as $index => $digit) {
  121. $product = $digit * 2 + $carry;
  122. $digits[$index] = $product % 10;
  123. $carry = (int) ($product / 10);
  124. }
  125. if ($carry !== 0) {
  126. $digits[] = $carry;
  127. }
  128. }
  129. $result = implode('', array_reverse($digits));
  130. /** @var numeric-string $result */
  131. return $result;
  132. }
  133. /**
  134. * @param numeric-string|mixed $value
  135. *
  136. * @return numeric-string|'' empty string if $value is not a valid numeric string
  137. */
  138. public function normalizeIntegerString($value)
  139. {
  140. if (!is_string($value) || $value === '') {
  141. return '';
  142. }
  143. $sign = $value[0];
  144. if ($sign === '-' || $sign === '+') {
  145. $value = substr($value, 1);
  146. }
  147. $matches = null;
  148. if (!preg_match('/^0*([0-9]+)$/', $value, $matches)) {
  149. return '';
  150. }
  151. $numericString = $matches[1];
  152. if ($sign === '-' && $numericString !== '0') {
  153. $numericString = '-' . $numericString;
  154. }
  155. /** @var numeric-string $numericString */
  156. return $numericString;
  157. }
  158. /**
  159. * @param numeric-string $value a string that has been normalized with normalizeIntegerString()
  160. *
  161. * @return numeric-string
  162. */
  163. public function add1ToIntegerString($value)
  164. {
  165. if ($value[0] === '-') {
  166. if ($value === '-1') {
  167. return '0';
  168. }
  169. $digits = str_split(substr($value, 1));
  170. $i = count($digits) - 1;
  171. while ($i >= 0) {
  172. if ($digits[$i] !== '0') {
  173. $digits[$i] = (string) ((int) $digits[$i] - 1);
  174. break;
  175. }
  176. $digits[$i] = '9';
  177. $i--;
  178. }
  179. $imploded = implode('', $digits);
  180. if ($imploded[0] === '0') {
  181. $imploded = substr($imploded, 1);
  182. }
  183. $result = '-' . $imploded;
  184. /** @var numeric-string $result */
  185. return $result; // @phpstan-ignore varTag.nativeType
  186. }
  187. $digits = str_split($value);
  188. $carry = 1;
  189. for ($i = count($digits) - 1; $i >= 0; $i--) {
  190. $sum = (int) $digits[$i] + $carry;
  191. $digits[$i] = (string) ($sum % 10);
  192. $carry = (int) ($sum / 10);
  193. if ($carry === 0) {
  194. break;
  195. }
  196. if ($i === 0) {
  197. array_unshift($digits, (string) $carry);
  198. }
  199. }
  200. $result = implode('', $digits);
  201. /** @var numeric-string $result */
  202. return $result;
  203. }
  204. /**
  205. * Zero-padding of two non-negative integers represented in binary form, so that they have the same length.
  206. *
  207. * @param string $num1
  208. * @param string $num2
  209. *
  210. * @return array{string, string, int} The first array element is $num1 (padded), the first array element is $num2 (padded), the third array element is the number of bits
  211. */
  212. private function toSameLength($num1, $num2)
  213. {
  214. $num1 = $this->reduce($num1);
  215. $num2 = $this->reduce($num2);
  216. $numBits = max(strlen($num1), strlen($num2));
  217. return array(
  218. str_pad($num1, $numBits, '0', STR_PAD_LEFT),
  219. str_pad($num2, $numBits, '0', STR_PAD_LEFT),
  220. $numBits,
  221. );
  222. }
  223. }