diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,3 +3,6 @@ * Sébastien Santoro aka Dereckson
https://www.dereckson.be
_Car la connaissance s'accroît quand on la partage._ + +* Ronald Ulysses Swanson aka Wes +https://twitter.com/WesNetmo diff --git a/src/Strings/Multibyte/OmniString.php b/src/Strings/Multibyte/OmniString.php new file mode 100644 --- /dev/null +++ b/src/Strings/Multibyte/OmniString.php @@ -0,0 +1,68 @@ +value = $value; + $this->setEncoding($encoding ?? "UTF-8"); + } + + /// + /// Magic methods + /// + + public function __toString() : string { + return $this->value; + } + + /// + /// Helper methods + /// + + public function pad( + int $padLength = 0, + string $padString = ' ', + int $padType = STR_PAD_RIGHT + ) : string { + return (new StringPad) + ->setInput($this->value) + ->setEncoding($this->encoding) + ->setPadLength($padLength) + ->setPadString($padString) + ->setPadType($padType) + ->pad(); + } + + /** + * @return string + */ + public function getValue () : string { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue (string $value) { + $this->value = $value; + } + +} diff --git a/src/Strings/Multibyte/StringPad.php b/src/Strings/Multibyte/StringPad.php new file mode 100644 --- /dev/null +++ b/src/Strings/Multibyte/StringPad.php @@ -0,0 +1,213 @@ +input = $input; + $this->padLength = $padLength; + $this->padString = $padString; + + $this->setPadType($padType); + $this->setEncoding($encoding ?? mb_internal_encoding()); + } + + /// + /// Getters and setters + /// + + public function getInput () : string { + return $this->input; + } + + public function setInput (string $input) : StringPad { + $this->input = $input; + + return $this; + } + + public function getPadLength () : int { + return $this->padLength; + } + + public function setPadLength (int $padLength) : StringPad { + $this->padLength = $padLength; + + return $this; + } + + public function getPadString () : string { + return $this->padString; + } + + public function setPadString (string $padString) : StringPad { + $this->padString = $padString; + + return $this; + } + + public function getPadType () : int { + return $this->padType; + } + + public function setPadType (int $padType) : StringPad { + if (!self::isValidPadType($padType)) { + throw new InvalidArgumentException; + } + + $this->padType = $padType; + + return $this; + } + + /// + /// Helper methods to get and set + /// + + public function setBothPad () : StringPad { + $this->padType = STR_PAD_BOTH; + + return $this; + } + + public function setLeftPad () : StringPad { + $this->padType = STR_PAD_LEFT; + + return $this; + } + + public function setRightPad () : StringPad { + $this->padType = STR_PAD_RIGHT; + + return $this; + } + + public static function isValidPadType (int $padType) : bool { + return $padType >= 0 && $padType <= 2; + } + + /// + /// Pad methods + /// + + public function pad () : string { + $this->computeLengths(); + return $this->getLeftPad() . $this->input . $this->getRightPad(); + } + + private function getLeftPad () : string { + if (!$this->hasPaddingBefore()) { + return ''; + } + + $length = (int)floor($this->targetLength); + return mb_substr($this->repeatedString, 0, $length, $this->encoding); + } + + private function getRightPad () : string { + if (!$this->hasPaddingAfter()) { + return ''; + } + + $length = (int)ceil($this->targetLength); + return mb_substr($this->repeatedString, 0, $length, $this->encoding); + } + + private function computeLengths () : void { + $this->targetLength = $this->computeNeededPadLength(); + $this->repeatedString = $this->computeRepeatedString(); + } + + private function computeRepeatedString () : string { + // Inspired by Ronald Ulysses Swanson method + // https://stackoverflow.com/a/27194169/1930997 + // who followed the str_pad PHP implementation. + + $strToRepeatLength = mb_strlen($this->padString, $this->encoding); + $repeatTimes = (int)ceil($this->targetLength / $strToRepeatLength); + + // Safe if used with valid Unicode sequences (any charset). + return str_repeat($this->padString, max(0, $repeatTimes)); + } + + private function computeNeededPadLength () : float { + $length = $this->padLength - mb_strlen($this->input, $this->encoding); + + if ($this->hasPaddingBeforeAndAfter()) { + return $length / 2; + } + + return $length; + } + + private function hasPaddingBefore () : bool { + return $this->padType === STR_PAD_LEFT || $this->padType === STR_PAD_BOTH; + } + + private function hasPaddingAfter () : bool { + return $this->padType === STR_PAD_RIGHT || $this->padType === STR_PAD_BOTH; + } + + private function hasPaddingBeforeAndAfter () : bool { + return + $this->padType === STR_PAD_BOTH + || + ($this->padType === STR_PAD_LEFT && $this->padType === STR_PAD_RIGHT) + ; + } + +} diff --git a/src/Strings/Multibyte/StringUtilities.php b/src/Strings/Multibyte/StringUtilities.php new file mode 100644 --- /dev/null +++ b/src/Strings/Multibyte/StringUtilities.php @@ -0,0 +1,46 @@ +setInput($input) + ->setPadLength($padLength) + ->setPadString($padString) + ->setPadType($padType) + ->setEncoding($encoding) + ->pad(); + } + + public static function isSupportedEncoding (string $encoding) : bool { + foreach (mb_list_encodings() as $supportedEncoding) { + if ($encoding === $supportedEncoding) { + return true; + } + } + + return false; + } + +} diff --git a/src/Strings/Multibyte/WithEncoding.php b/src/Strings/Multibyte/WithEncoding.php new file mode 100644 --- /dev/null +++ b/src/Strings/Multibyte/WithEncoding.php @@ -0,0 +1,29 @@ +encoding; + } + + public function setEncoding (string $encoding) : self { + if (!StringUtilities::isSupportedEncoding($encoding)) { + throw new InvalidArgumentException; + } + + $this->encoding = $encoding; + + return $this; + } + +} diff --git a/tests/Strings/Multibyte/OmniStringTest.php b/tests/Strings/Multibyte/OmniStringTest.php new file mode 100644 --- /dev/null +++ b/tests/Strings/Multibyte/OmniStringTest.php @@ -0,0 +1,32 @@ +string = new OmniString("foo"); + } + + public function testToString () : void { + $this->assertEquals("foo", (string)$this->string); + $this->assertEquals("foo", $this->string->__toString()); + } + + public function testPad () : void { + $paddedString = $this->string->pad(9, '-=-', STR_PAD_BOTH); + $this->assertEquals("-=-foo-=-", $paddedString); + } + +} diff --git a/tests/Strings/Multibyte/StringPadTest.php b/tests/Strings/Multibyte/StringPadTest.php new file mode 100644 --- /dev/null +++ b/tests/Strings/Multibyte/StringPadTest.php @@ -0,0 +1,56 @@ +expectException(InvalidArgumentException::class); + + $pad = new Pad; + $pad->setPadType(7); + } + + public function testIsValidPadType () : void { + $this->assertTrue(Pad::isValidPadType(STR_PAD_LEFT)); + $this->assertTrue(Pad::isValidPadType(STR_PAD_RIGHT)); + $this->assertTrue(Pad::isValidPadType(STR_PAD_BOTH)); + + $this->assertFalse(Pad::isValidPadType(7)); + } + + public function testSetPadTypeWithBogusEncoding () : void { + $this->expectException(InvalidArgumentException::class); + + $pad = new Pad; + $pad->setEncoding("notexisting"); + } + + public function testSetLeftPad () : void { + $pad = new Pad; + $pad->setLeftPad(); + + $this->assertEquals(STR_PAD_LEFT, $pad->getPadType()); + } + + public function testSetRightPad () : void { + $pad = new Pad; + $pad->setRightPad(); + + $this->assertEquals(STR_PAD_RIGHT, $pad->getPadType()); + } + + public function testSetBothPad () : void { + $pad = new Pad; + $pad->setBothPad(); + + $this->assertEquals(STR_PAD_BOTH, $pad->getPadType()); + } + +} diff --git a/tests/Strings/Multibyte/StringUtilitiesTest.php b/tests/Strings/Multibyte/StringUtilitiesTest.php new file mode 100644 --- /dev/null +++ b/tests/Strings/Multibyte/StringUtilitiesTest.php @@ -0,0 +1,69 @@ +assertEquals($expected, $paddedString); + } + + public function testSupportedEncoding () : void { + $this->assertTrue(StringUtilities::isSupportedEncoding("UTF-8")); + $this->assertFalse(StringUtilities::isSupportedEncoding("notexisting")); + } + + /// + /// Data providers + /// + + public function providePadding () : iterable { + // Tests from http://3v4l.org/UnXTF + // http://web.archive.org/web/20150711100913/http://3v4l.org/UnXTF + + yield ['àèòàFOOàèòà', "FOO", 11, "àèò", STR_PAD_BOTH]; + yield ['àèòFOOàèòà', "FOO", 10, "àèò", STR_PAD_BOTH]; + yield ['àèòBAAZàèòà', "BAAZ", 11, "àèò", STR_PAD_BOTH]; + yield ['àèòBAAZàèò', "BAAZ", 10, "àèò", STR_PAD_BOTH]; + yield ['FOOBAR', "FOOBAR", 6, "àèò", STR_PAD_BOTH]; + yield ['FOOBAR', "FOOBAR", 1, "àèò", STR_PAD_BOTH]; + yield ['FOOBAR', "FOOBAR", 0, "àèò", STR_PAD_BOTH]; + yield ['FOOBAR', "FOOBAR", -10, "àèò", STR_PAD_BOTH]; + + yield ['àèòàèòàèFOO', "FOO", 11, "àèò", STR_PAD_LEFT]; + yield ['àèòàèòàFOO', "FOO", 10, "àèò", STR_PAD_LEFT]; + yield ['àèòàèòàBAAZ', "BAAZ", 11, "àèò", STR_PAD_LEFT]; + yield ['àèòàèòBAAZ', "BAAZ", 10, "àèò", STR_PAD_LEFT]; + yield ['FOOBAR', "FOOBAR", 6, "àèò", STR_PAD_LEFT]; + yield ['FOOBAR', "FOOBAR", 1, "àèò", STR_PAD_LEFT]; + yield ['FOOBAR', "FOOBAR", 0, "àèò", STR_PAD_LEFT]; + yield ['FOOBAR', "FOOBAR", -10, "àèò", STR_PAD_LEFT]; + + yield ['FOOàèòàèòàè', "FOO", 11, "àèò", STR_PAD_RIGHT]; + yield ['FOOàèòàèòà', "FOO", 10, "àèò", STR_PAD_RIGHT]; + yield ['BAAZàèòàèòà', "BAAZ", 11, "àèò", STR_PAD_RIGHT]; + yield ['BAAZàèòàèò', "BAAZ", 10, "àèò", STR_PAD_RIGHT]; + yield ['FOOBAR', "FOOBAR", 6, "àèò", STR_PAD_RIGHT]; + yield ['FOOBAR', "FOOBAR", 1, "àèò", STR_PAD_RIGHT]; + yield ['FOOBAR', "FOOBAR", 0, "àèò", STR_PAD_RIGHT]; + yield ['FOOBAR', "FOOBAR", -10, "àèò", STR_PAD_RIGHT]; + } +}