You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
7.9 KiB

3 years ago
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\String;
  11. use Symfony\Component\String\Exception\ExceptionInterface;
  12. use Symfony\Component\String\Exception\InvalidArgumentException;
  13. /**
  14. * Represents a string of Unicode code points encoded as UTF-8.
  15. *
  16. * @author Nicolas Grekas <p@tchwork.com>
  17. * @author Hugo Hamon <hugohamon@neuf.fr>
  18. *
  19. * @throws ExceptionInterface
  20. */
  21. class CodePointString extends AbstractUnicodeString
  22. {
  23. public function __construct(string $string = '')
  24. {
  25. if ('' !== $string && !preg_match('//u', $string)) {
  26. throw new InvalidArgumentException('Invalid UTF-8 string.');
  27. }
  28. $this->string = $string;
  29. }
  30. public function append(string ...$suffix): AbstractString
  31. {
  32. $str = clone $this;
  33. $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);
  34. if (!preg_match('//u', $str->string)) {
  35. throw new InvalidArgumentException('Invalid UTF-8 string.');
  36. }
  37. return $str;
  38. }
  39. public function chunk(int $length = 1): array
  40. {
  41. if (1 > $length) {
  42. throw new InvalidArgumentException('The chunk length must be greater than zero.');
  43. }
  44. if ('' === $this->string) {
  45. return [];
  46. }
  47. $rx = '/(';
  48. while (65535 < $length) {
  49. $rx .= '.{65535}';
  50. $length -= 65535;
  51. }
  52. $rx .= '.{'.$length.'})/us';
  53. $str = clone $this;
  54. $chunks = [];
  55. foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) {
  56. $str->string = $chunk;
  57. $chunks[] = clone $str;
  58. }
  59. return $chunks;
  60. }
  61. public function codePointsAt(int $offset): array
  62. {
  63. $str = $offset ? $this->slice($offset, 1) : $this;
  64. return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')];
  65. }
  66. public function endsWith($suffix): bool
  67. {
  68. if ($suffix instanceof AbstractString) {
  69. $suffix = $suffix->string;
  70. } elseif (\is_array($suffix) || $suffix instanceof \Traversable) {
  71. return parent::endsWith($suffix);
  72. } else {
  73. $suffix = (string) $suffix;
  74. }
  75. if ('' === $suffix || !preg_match('//u', $suffix)) {
  76. return false;
  77. }
  78. if ($this->ignoreCase) {
  79. return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string);
  80. }
  81. return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix));
  82. }
  83. public function equalsTo($string): bool
  84. {
  85. if ($string instanceof AbstractString) {
  86. $string = $string->string;
  87. } elseif (\is_array($string) || $string instanceof \Traversable) {
  88. return parent::equalsTo($string);
  89. } else {
  90. $string = (string) $string;
  91. }
  92. if ('' !== $string && $this->ignoreCase) {
  93. return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8');
  94. }
  95. return $string === $this->string;
  96. }
  97. public function indexOf($needle, int $offset = 0): ?int
  98. {
  99. if ($needle instanceof AbstractString) {
  100. $needle = $needle->string;
  101. } elseif (\is_array($needle) || $needle instanceof \Traversable) {
  102. return parent::indexOf($needle, $offset);
  103. } else {
  104. $needle = (string) $needle;
  105. }
  106. if ('' === $needle) {
  107. return null;
  108. }
  109. $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8');
  110. return false === $i ? null : $i;
  111. }
  112. public function indexOfLast($needle, int $offset = 0): ?int
  113. {
  114. if ($needle instanceof AbstractString) {
  115. $needle = $needle->string;
  116. } elseif (\is_array($needle) || $needle instanceof \Traversable) {
  117. return parent::indexOfLast($needle, $offset);
  118. } else {
  119. $needle = (string) $needle;
  120. }
  121. if ('' === $needle) {
  122. return null;
  123. }
  124. $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8');
  125. return false === $i ? null : $i;
  126. }
  127. public function length(): int
  128. {
  129. return mb_strlen($this->string, 'UTF-8');
  130. }
  131. public function prepend(string ...$prefix): AbstractString
  132. {
  133. $str = clone $this;
  134. $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string;
  135. if (!preg_match('//u', $str->string)) {
  136. throw new InvalidArgumentException('Invalid UTF-8 string.');
  137. }
  138. return $str;
  139. }
  140. public function replace(string $from, string $to): AbstractString
  141. {
  142. $str = clone $this;
  143. if ('' === $from || !preg_match('//u', $from)) {
  144. return $str;
  145. }
  146. if ('' !== $to && !preg_match('//u', $to)) {
  147. throw new InvalidArgumentException('Invalid UTF-8 string.');
  148. }
  149. if ($this->ignoreCase) {
  150. $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string));
  151. } else {
  152. $str->string = str_replace($from, $to, $this->string);
  153. }
  154. return $str;
  155. }
  156. public function slice(int $start = 0, int $length = null): AbstractString
  157. {
  158. $str = clone $this;
  159. $str->string = mb_substr($this->string, $start, $length, 'UTF-8');
  160. return $str;
  161. }
  162. public function splice(string $replacement, int $start = 0, int $length = null): AbstractString
  163. {
  164. if (!preg_match('//u', $replacement)) {
  165. throw new InvalidArgumentException('Invalid UTF-8 string.');
  166. }
  167. $str = clone $this;
  168. $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0;
  169. $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length;
  170. $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);
  171. return $str;
  172. }
  173. public function split(string $delimiter, int $limit = null, int $flags = null): array
  174. {
  175. if (1 > $limit = $limit ?? \PHP_INT_MAX) {
  176. throw new InvalidArgumentException('Split limit must be a positive integer.');
  177. }
  178. if ('' === $delimiter) {
  179. throw new InvalidArgumentException('Split delimiter is empty.');
  180. }
  181. if (null !== $flags) {
  182. return parent::split($delimiter.'u', $limit, $flags);
  183. }
  184. if (!preg_match('//u', $delimiter)) {
  185. throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.');
  186. }
  187. $str = clone $this;
  188. $chunks = $this->ignoreCase
  189. ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit)
  190. : explode($delimiter, $this->string, $limit);
  191. foreach ($chunks as &$chunk) {
  192. $str->string = $chunk;
  193. $chunk = clone $str;
  194. }
  195. return $chunks;
  196. }
  197. public function startsWith($prefix): bool
  198. {
  199. if ($prefix instanceof AbstractString) {
  200. $prefix = $prefix->string;
  201. } elseif (\is_array($prefix) || $prefix instanceof \Traversable) {
  202. return parent::startsWith($prefix);
  203. } else {
  204. $prefix = (string) $prefix;
  205. }
  206. if ('' === $prefix || !preg_match('//u', $prefix)) {
  207. return false;
  208. }
  209. if ($this->ignoreCase) {
  210. return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8');
  211. }
  212. return 0 === strncmp($this->string, $prefix, \strlen($prefix));
  213. }
  214. }