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];
+ }
+}