1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Stdlib\StringWrapper;
11
12use Zend\Stdlib\Exception;
13use Zend\Stdlib\StringUtils;
14
15abstract class AbstractStringWrapper implements StringWrapperInterface
16{
17    /**
18     * The character encoding working on
19     * @var string|null
20     */
21    protected $encoding = 'UTF-8';
22
23    /**
24     * An optionally character encoding to convert to
25     * @var string|null
26     */
27    protected $convertEncoding;
28
29    /**
30     * Check if the given character encoding is supported by this wrapper
31     * and the character encoding to convert to is also supported.
32     *
33     * @param  string      $encoding
34     * @param  string|null $convertEncoding
35     * @return bool
36     */
37    public static function isSupported($encoding, $convertEncoding = null)
38    {
39        $supportedEncodings = static::getSupportedEncodings();
40
41        if (!in_array(strtoupper($encoding), $supportedEncodings)) {
42            return false;
43        }
44
45        if ($convertEncoding !== null && !in_array(strtoupper($convertEncoding), $supportedEncodings)) {
46            return false;
47        }
48
49        return true;
50    }
51
52    /**
53     * Set character encoding working with and convert to
54     *
55     * @param string      $encoding         The character encoding to work with
56     * @param string|null $convertEncoding  The character encoding to convert to
57     * @return StringWrapperInterface
58     */
59    public function setEncoding($encoding, $convertEncoding = null)
60    {
61        $supportedEncodings = static::getSupportedEncodings();
62
63        $encodingUpper = strtoupper($encoding);
64        if (!in_array($encodingUpper, $supportedEncodings)) {
65            throw new Exception\InvalidArgumentException(
66                'Wrapper doesn\'t support character encoding "' . $encoding . '"'
67            );
68        }
69
70        if ($convertEncoding !== null) {
71            $convertEncodingUpper = strtoupper($convertEncoding);
72            if (!in_array($convertEncodingUpper, $supportedEncodings)) {
73                throw new Exception\InvalidArgumentException(
74                    'Wrapper doesn\'t support character encoding "' . $convertEncoding . '"'
75                );
76            }
77
78            $this->convertEncoding = $convertEncodingUpper;
79        } else {
80            $this->convertEncoding = null;
81        }
82        $this->encoding = $encodingUpper;
83
84        return $this;
85    }
86
87    /**
88     * Get the defined character encoding to work with
89     *
90     * @return string
91     * @throws Exception\LogicException If no encoding was defined
92     */
93    public function getEncoding()
94    {
95        return $this->encoding;
96    }
97
98    /**
99     * Get the defined character encoding to convert to
100     *
101     * @return string|null
102    */
103    public function getConvertEncoding()
104    {
105        return $this->convertEncoding;
106    }
107
108    /**
109     * Convert a string from defined character encoding to the defined convert encoding
110     *
111     * @param string  $str
112     * @param bool $reverse
113     * @return string|false
114     */
115    public function convert($str, $reverse = false)
116    {
117        $encoding        = $this->getEncoding();
118        $convertEncoding = $this->getConvertEncoding();
119        if ($convertEncoding === null) {
120            throw new Exception\LogicException(
121                'No convert encoding defined'
122            );
123        }
124
125        if ($encoding === $convertEncoding) {
126            return $str;
127        }
128
129        $from = $reverse ? $convertEncoding : $encoding;
130        $to   = $reverse ? $encoding : $convertEncoding;
131        throw new Exception\RuntimeException(sprintf(
132            'Converting from "%s" to "%s" isn\'t supported by this string wrapper',
133            $from,
134            $to
135        ));
136    }
137
138    /**
139     * Wraps a string to a given number of characters
140     *
141     * @param  string  $string
142     * @param  int $width
143     * @param  string  $break
144     * @param  bool $cut
145     * @return string|false
146     */
147    public function wordWrap($string, $width = 75, $break = "\n", $cut = false)
148    {
149        $string = (string) $string;
150        if ($string === '') {
151            return '';
152        }
153
154        $break = (string) $break;
155        if ($break === '') {
156            throw new Exception\InvalidArgumentException('Break string cannot be empty');
157        }
158
159        $width = (int) $width;
160        if ($width === 0 && $cut) {
161            throw new Exception\InvalidArgumentException('Cannot force cut when width is zero');
162        }
163
164        if (StringUtils::isSingleByteEncoding($this->getEncoding())) {
165            return wordwrap($string, $width, $break, $cut);
166        }
167
168        $stringWidth = $this->strlen($string);
169        $breakWidth  = $this->strlen($break);
170
171        $result    = '';
172        $lastStart = $lastSpace = 0;
173
174        for ($current = 0; $current < $stringWidth; $current++) {
175            $char = $this->substr($string, $current, 1);
176
177            $possibleBreak = $char;
178            if ($breakWidth !== 1) {
179                $possibleBreak = $this->substr($string, $current, $breakWidth);
180            }
181
182            if ($possibleBreak === $break) {
183                $result    .= $this->substr($string, $lastStart, $current - $lastStart + $breakWidth);
184                $current   += $breakWidth - 1;
185                $lastStart  = $lastSpace = $current + 1;
186                continue;
187            }
188
189            if ($char === ' ') {
190                if ($current - $lastStart >= $width) {
191                    $result    .= $this->substr($string, $lastStart, $current - $lastStart) . $break;
192                    $lastStart  = $current + 1;
193                }
194
195                $lastSpace = $current;
196                continue;
197            }
198
199            if ($current - $lastStart >= $width && $cut && $lastStart >= $lastSpace) {
200                $result    .= $this->substr($string, $lastStart, $current - $lastStart) . $break;
201                $lastStart  = $lastSpace = $current;
202                continue;
203            }
204
205            if ($current - $lastStart >= $width && $lastStart < $lastSpace) {
206                $result    .= $this->substr($string, $lastStart, $lastSpace - $lastStart) . $break;
207                $lastStart  = $lastSpace = $lastSpace + 1;
208                continue;
209            }
210        }
211
212        if ($lastStart !== $current) {
213            $result .= $this->substr($string, $lastStart, $current - $lastStart);
214        }
215
216        return $result;
217    }
218
219    /**
220     * Pad a string to a certain length with another string
221     *
222     * @param  string  $input
223     * @param  int $padLength
224     * @param  string  $padString
225     * @param  int $padType
226     * @return string
227     */
228    public function strPad($input, $padLength, $padString = ' ', $padType = STR_PAD_RIGHT)
229    {
230        if (StringUtils::isSingleByteEncoding($this->getEncoding())) {
231            return str_pad($input, $padLength, $padString, $padType);
232        }
233
234        $lengthOfPadding = $padLength - $this->strlen($input);
235        if ($lengthOfPadding <= 0) {
236            return $input;
237        }
238
239        $padStringLength = $this->strlen($padString);
240        if ($padStringLength === 0) {
241            return $input;
242        }
243
244        $repeatCount = floor($lengthOfPadding / $padStringLength);
245
246        if ($padType === STR_PAD_BOTH) {
247            $repeatCountLeft = $repeatCountRight = ($repeatCount - $repeatCount % 2) / 2;
248
249            $lastStringLength       = $lengthOfPadding - 2 * $repeatCountLeft * $padStringLength;
250            $lastStringLeftLength   = $lastStringRightLength = floor($lastStringLength / 2);
251            $lastStringRightLength += $lastStringLength % 2;
252
253            $lastStringLeft  = $this->substr($padString, 0, $lastStringLeftLength);
254            $lastStringRight = $this->substr($padString, 0, $lastStringRightLength);
255
256            return str_repeat($padString, $repeatCountLeft) . $lastStringLeft
257                . $input
258                . str_repeat($padString, $repeatCountRight) . $lastStringRight;
259        }
260
261        $lastString = $this->substr($padString, 0, $lengthOfPadding % $padStringLength);
262
263        if ($padType === STR_PAD_LEFT) {
264            return str_repeat($padString, $repeatCount) . $lastString . $input;
265        }
266
267        return $input . str_repeat($padString, $repeatCount) . $lastString;
268    }
269}
270