?\u{00A1}ABCDEFGHIJKLMNOPQRSTUVWXYZ\u{00C4}\u{00D6}\u{00D1}\u{00DC}\u{00A7}\u{00BF}abcdefghijklmnopqrstuvwxyz\u{00E4}\u{00F6}\u{00F1}\u{00FC}\u{00E0}"; private const GSM_EXTENDED = "^{}\\[~]|\u{20AC}"; public function fitsInOneSms(string $message): bool { return $this->units($message) <= $this->singleSmsCapacity($message); } public function units(string $message): int { if (! $this->usesGsmAlphabet($message)) { return intdiv(strlen(mb_convert_encoding($message, 'UTF-16BE', 'UTF-8')), 2); } return array_sum(array_map( fn (string $character): int => str_contains(self::GSM_EXTENDED, $character) ? 2 : 1, mb_str_split($message), )); } public function singleSmsCapacity(string $message): int { return $this->usesGsmAlphabet($message) ? 160 : 70; } /** * Truncate a value so that prefix + value + suffix still fits in one SMS. */ public function truncateBetween(string $prefix, string $value, string $suffix, string $marker = '[...]'): string { if ($this->fitsInOneSms($prefix.$value.$suffix)) { return $value; } $truncated = ''; foreach (mb_str_split($value) as $character) { if (! $this->fitsInOneSms($prefix.$truncated.$character.$marker.$suffix)) { break; } $truncated .= $character; } if ($this->fitsInOneSms($prefix.$truncated.$marker.$suffix)) { return rtrim($truncated).$marker; } return rtrim($truncated); } private function usesGsmAlphabet(string $message): bool { foreach (mb_str_split($message) as $character) { if (! str_contains(self::GSM_BASIC, $character) && ! str_contains(self::GSM_EXTENDED, $character)) { return false; } } return true; } }