diff --git a/src/SmsLength.php b/src/SmsLength.php index b767887..0d31459 100644 --- a/src/SmsLength.php +++ b/src/SmsLength.php @@ -80,14 +80,21 @@ class SmsLength */ private $messageCount; + /** + * @var string + */ + private $messageContent; + /** * Constructor * * @param string $messageContent SMS message content (UTF-8) + * * @throws InvalidArgumentException */ public function __construct($messageContent) { + $this->messageContent = $messageContent; $this->inspect($messageContent); } @@ -121,6 +128,16 @@ public function getMessageCount() return $this->messageCount; } + /** + * Get number of message content + * + * @return string + */ + public function getMessageContent() + { + return $this->messageContent; + } + /** * Get upper breakpoint for the current message count * @@ -157,10 +174,91 @@ public function validate() return true; } + /** + * Return a new instance with the message truncated to a set part count + * + * @param int $parts + * + * @return self + */ + public function truncate($parts) + { + if ($this->messageCount <= $parts) { + return $this; + } + + if ($this->encoding === '7-bit') { + return new self($this->truncate7Bit($this->messageContent, $parts)); + } + + return new self($this->truncateUcs2($this->messageContent, $parts)); + } + + private function truncate7Bit($message, $parts) + { + $size = 0; + $newMessage = ''; + + $mbLength = mb_strlen($message, 'UTF-8'); + + for ($i = 0; $i < $mbLength; $i++) { + $char = mb_substr($message, $i, 1, 'UTF-8'); + + if (in_array($char, self::GSM0338_BASIC)) { + $charSize = 1; + } elseif (in_array($char, self::GSM0338_EXTENDED)) { + $charSize = 2; + } else { + continue; + } + + if ($parts === 1 && $size + $charSize > self::MAXIMUM_CHARACTERS_7BIT_SINGLE) { + return $newMessage; + } + + if ($parts > 1 && $size + $charSize > $parts * self::MAXIMUM_CHARACTERS_7BIT_CONCATENATED) { + return $newMessage; + } + + $size += $charSize; + $newMessage .= $char; + } + + return $newMessage; + } + + private function truncateUcs2($message, $parts) + { + $size = 0; + $newMessage = ''; + + $mbLength = mb_strlen($message, 'UTF-8'); + + for ($i = 0; $i < $mbLength; $i++) { + $char = mb_substr($message, $i, 1, 'UTF-8'); + $utf16Hex = bin2hex(mb_convert_encoding($char, 'UTF-16', 'UTF-8')); + $charSize = strlen($utf16Hex) / 4; + + if ($parts === 1 && $size + $charSize > self::MAXIMUM_CHARACTERS_UCS2_SINGLE) { + return $newMessage; + } + + if ($parts > 1 && $size + $charSize >= $parts * self::MAXIMUM_CHARACTERS_UCS2_CONCATENATED) { + return $newMessage; + } + + $size += $charSize; + $newMessage .= $char; + } + + return $newMessage; + } + /** * Parse content to discover size characteristics * * @param string $messageContent + * * @throws InvalidArgumentException */ private function inspect($messageContent) diff --git a/tests/SmsLengthTest.php b/tests/SmsLengthTest.php index c03ebd6..cd1d2e1 100644 --- a/tests/SmsLengthTest.php +++ b/tests/SmsLengthTest.php @@ -90,17 +90,19 @@ public function providerSize() /** * @dataProvider providerTooLarge + * * @param string $content * @param string $encoding * @param int $characters * @param int $messageCount * @param int $upperBreak + * * @medium Expect tests to take >1 but <10 - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Message count cannot exceed 255 */ public function testTooLarge($content, $encoding, $characters, $messageCount, $upperBreak) { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Message count cannot exceed 255"); $size = new SmsLength($content); $this->assertSame($encoding, $size->getEncoding()); @@ -129,4 +131,63 @@ public function providerTooLarge() [str_repeat("simple msg plus \xf0\x9f\x93\xb1", 950), 'ucs-2', 17100, 256, 17152] ]; } + + /** + * @dataProvider providerTruncate + */ + public function testTruncate($content, $parts, $expected) + { + $original = new SmsLength($content); + + $new = $original->truncate($parts); + + $this->assertSame($parts, $new->getMessageCount()); + $this->assertSame($expected, $new->getMessageContent()); + } + + public function providerTruncate() + { + return [ + 'message under one part' => [ + 'message' => 'La La La', + 'parts' => 1, + 'expected' => 'La La La', + ], + 'message over one part, gsm7' => [ + 'message' => str_repeat('abcd', 45), + 'parts' => 1, + 'expected' => str_repeat('abcd', 40), + ], + 'message over two part, gsm7' => [ + 'message' => str_repeat('abcd', 100), + 'parts' => 2, + 'expected' => str_repeat('abcd', 76) . 'ab', + ], + 'message over one part, gsm7 + ext' => [ + 'message' => str_repeat('abcd[', 30), + 'parts' => 1, + 'expected' => str_repeat('abcd[', 26) . 'abcd', // each part is 6 chars, should be 26 reps + 4 chars leftover + ], + 'message over one part, uc2 1 part char' => [ + 'message' => str_repeat('•', 100), + 'parts' => 1, + 'expected' => str_repeat('•', 70), + ], + 'message over one part, uc2 3 byte' => [ + 'message' => str_repeat('⏩', 100), + 'parts' => 1, + 'expected' => str_repeat('⏩', 70), + ], + 'message over one part, uc2 4 byte' => [ + 'message' => str_repeat('🌐', 100), + 'parts' => 1, + 'expected' => str_repeat('🌐', 35), + ], + 'message over two parts, uc2 4 byte' => [ + 'message' => str_repeat('🌐', 200), + 'parts' => 2, + 'expected' => str_repeat('🌐', 66), + ] + ]; + } }