CodePointString.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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. /**
  24. * @param string $string
  25. */
  26. public function __construct($string = '')
  27. {
  28. if ('' !== $string && !preg_match('//u', $string)) {
  29. throw new InvalidArgumentException('Invalid UTF-8 string.');
  30. }
  31. $this->string = $string;
  32. }
  33. /**
  34. * @return $this
  35. * @param string ...$suffix
  36. */
  37. public function append(...$suffix)
  38. {
  39. $str = clone $this;
  40. $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);
  41. if (!preg_match('//u', $str->string)) {
  42. throw new InvalidArgumentException('Invalid UTF-8 string.');
  43. }
  44. return $str;
  45. }
  46. /**
  47. * @param int $length
  48. */
  49. public function chunk($length = 1)
  50. {
  51. if (1 > $length) {
  52. throw new InvalidArgumentException('The chunk length must be greater than zero.');
  53. }
  54. if ('' === $this->string) {
  55. return [];
  56. }
  57. $rx = '/(';
  58. while (65535 < $length) {
  59. $rx .= '.{65535}';
  60. $length -= 65535;
  61. }
  62. $rx .= '.{'.$length.'})/us';
  63. $str = clone $this;
  64. $chunks = [];
  65. foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) {
  66. $str->string = $chunk;
  67. $chunks[] = clone $str;
  68. }
  69. return $chunks;
  70. }
  71. /**
  72. * @param int $offset
  73. */
  74. public function codePointsAt($offset)
  75. {
  76. $str = $offset ? $this->slice($offset, 1) : $this;
  77. return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')];
  78. }
  79. /**
  80. * @param string|mixed[]|\Symfony\Component\String\AbstractString $suffix
  81. */
  82. public function endsWith($suffix)
  83. {
  84. if ($suffix instanceof AbstractString) {
  85. $suffix = $suffix->string;
  86. } elseif (!\is_string($suffix)) {
  87. return parent::endsWith($suffix);
  88. }
  89. if ('' === $suffix || !preg_match('//u', $suffix)) {
  90. return false;
  91. }
  92. if ($this->ignoreCase) {
  93. return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string);
  94. }
  95. return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix));
  96. }
  97. /**
  98. * @param string|mixed[]|\Symfony\Component\String\AbstractString $string
  99. */
  100. public function equalsTo($string)
  101. {
  102. if ($string instanceof AbstractString) {
  103. $string = $string->string;
  104. } elseif (!\is_string($string)) {
  105. return parent::equalsTo($string);
  106. }
  107. if ('' !== $string && $this->ignoreCase) {
  108. return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8');
  109. }
  110. return $string === $this->string;
  111. }
  112. /**
  113. * @param string|mixed[]|\Symfony\Component\String\AbstractString $needle
  114. * @param int $offset
  115. */
  116. public function indexOf($needle, $offset = 0)
  117. {
  118. if ($needle instanceof AbstractString) {
  119. $needle = $needle->string;
  120. } elseif (!\is_string($needle)) {
  121. return parent::indexOf($needle, $offset);
  122. }
  123. if ('' === $needle) {
  124. return null;
  125. }
  126. $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8');
  127. return false === $i ? null : $i;
  128. }
  129. /**
  130. * @param string|mixed[]|\Symfony\Component\String\AbstractString $needle
  131. * @param int $offset
  132. */
  133. public function indexOfLast($needle, $offset = 0)
  134. {
  135. if ($needle instanceof AbstractString) {
  136. $needle = $needle->string;
  137. } elseif (!\is_string($needle)) {
  138. return parent::indexOfLast($needle, $offset);
  139. }
  140. if ('' === $needle) {
  141. return null;
  142. }
  143. $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8');
  144. return false === $i ? null : $i;
  145. }
  146. public function length()
  147. {
  148. return mb_strlen($this->string, 'UTF-8');
  149. }
  150. /**
  151. * @return $this
  152. * @param string ...$prefix
  153. */
  154. public function prepend(...$prefix)
  155. {
  156. $str = clone $this;
  157. $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string;
  158. if (!preg_match('//u', $str->string)) {
  159. throw new InvalidArgumentException('Invalid UTF-8 string.');
  160. }
  161. return $str;
  162. }
  163. /**
  164. * @return $this
  165. * @param string $from
  166. * @param string $to
  167. */
  168. public function replace($from, $to)
  169. {
  170. $str = clone $this;
  171. if ('' === $from || !preg_match('//u', $from)) {
  172. return $str;
  173. }
  174. if ('' !== $to && !preg_match('//u', $to)) {
  175. throw new InvalidArgumentException('Invalid UTF-8 string.');
  176. }
  177. if ($this->ignoreCase) {
  178. $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string));
  179. } else {
  180. $str->string = str_replace($from, $to, $this->string);
  181. }
  182. return $str;
  183. }
  184. /**
  185. * @return $this
  186. * @param int $start
  187. * @param int|null $length
  188. */
  189. public function slice($start = 0, $length = null)
  190. {
  191. $str = clone $this;
  192. $str->string = mb_substr($this->string, $start, $length, 'UTF-8');
  193. return $str;
  194. }
  195. /**
  196. * @return $this
  197. * @param string $replacement
  198. * @param int $start
  199. * @param int|null $length
  200. */
  201. public function splice($replacement, $start = 0, $length = null)
  202. {
  203. if (!preg_match('//u', $replacement)) {
  204. throw new InvalidArgumentException('Invalid UTF-8 string.');
  205. }
  206. $str = clone $this;
  207. $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0;
  208. $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length;
  209. $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);
  210. return $str;
  211. }
  212. /**
  213. * @param string $delimiter
  214. * @param int|null $limit
  215. * @param int|null $flags
  216. */
  217. public function split($delimiter, $limit = null, $flags = null)
  218. {
  219. if (1 > ($limit = $limit ?? \PHP_INT_MAX)) {
  220. throw new InvalidArgumentException('Split limit must be a positive integer.');
  221. }
  222. if ('' === $delimiter) {
  223. throw new InvalidArgumentException('Split delimiter is empty.');
  224. }
  225. if (null !== $flags) {
  226. return parent::split($delimiter.'u', $limit, $flags);
  227. }
  228. if (!preg_match('//u', $delimiter)) {
  229. throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.');
  230. }
  231. $str = clone $this;
  232. $chunks = $this->ignoreCase
  233. ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit)
  234. : explode($delimiter, $this->string, $limit);
  235. foreach ($chunks as &$chunk) {
  236. $str->string = $chunk;
  237. $chunk = clone $str;
  238. }
  239. return $chunks;
  240. }
  241. /**
  242. * @param string|mixed[]|\Symfony\Component\String\AbstractString $prefix
  243. */
  244. public function startsWith($prefix)
  245. {
  246. if ($prefix instanceof AbstractString) {
  247. $prefix = $prefix->string;
  248. } elseif (!\is_string($prefix)) {
  249. return parent::startsWith($prefix);
  250. }
  251. if ('' === $prefix || !preg_match('//u', $prefix)) {
  252. return false;
  253. }
  254. if ($this->ignoreCase) {
  255. return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8');
  256. }
  257. return 0 === strncmp($this->string, $prefix, \strlen($prefix));
  258. }
  259. }