By using the wordwrap function. It splits the texts in multiple lines such that the maximum width is the one you specified, breaking at word boundaries. After splitting, you simply take the first line:
substr($string, 0, strpos(wordwrap($string, $your_desired_width), "\n"));
One thing this oneliner doesn't handle is the case when the text itself is shorter than the desired width. To handle this edge-case, one should do something like:
if (strlen($string) > $your_desired_width)
{
$string = wordwrap($string, $your_desired_width);
$string = substr($string, 0, strpos($string, "\n"));
}
The above solution has the problem of prematurely cutting the text if it contains a newline before the actual cutpoint. Here a version which solves this problem:
function tokenTruncate($string, $your_desired_width) {
$parts = preg_split('/([\s\n\r]+)/', $string, null, PREG_SPLIT_DELIM_CAPTURE);
$parts_count = count($parts);
$length = 0;
$last_part = 0;
for (; $last_part < $parts_count; ++$last_part) {
$length += strlen($parts[$last_part]);
if ($length > $your_desired_width) { break; }
}
return implode(array_slice($parts, 0, $last_part));
}
Also, here is the PHPUnit testclass used to test the implementation:
class TokenTruncateTest extends PHPUnit_Framework_TestCase {
public function testBasic() {
$this->assertEquals("1 3 5 7 9 ",
tokenTruncate("1 3 5 7 9 11 14", 10));
}
public function testEmptyString() {
$this->assertEquals("",
tokenTruncate("", 10));
}
public function testShortString() {
$this->assertEquals("1 3",
tokenTruncate("1 3", 10));
}
public function testStringTooLong() {
$this->assertEquals("",
tokenTruncate("toooooooooooolooooong", 10));
}
public function testContainingNewline() {
$this->assertEquals("1 3\n5 7 9 ",
tokenTruncate("1 3\n5 7 9 11 14", 10));
}
}
EDIT :
Special UTF8 characters like 'à' are not handled. Add 'u' at the end of the REGEX to handle it:
$parts = preg_split('/([\s\n\r]+)/u', $string, null, PREG_SPLIT_DELIM_CAPTURE);
Best Solution
As suggested by kdgregory, using a
StringBuilder
would probably be an easier way to work with string manipulation.Since I wasn't quite sure if the number of characters before the newline is inserted is the word before or after 30 characters, I opted to go for the word after 30 characters, as the implementation is probably easier.
The approach is to find the instance of the
" "
which occurs at least 30 characters after the current character which is being viewed by usingStringBuilder.indexOf
. When a space occurs, a\n
is inserted byStringBuilder.insert
.(We'll assume that a newline is
\n
here -- the actual line separator used in the current environment can be retrieved bySystem.getProperty("line.separator");
).Here's the example:
Result:
I should add that the code above hasn't been tested out, except for the example
String
that I've shown in the code. It wouldn't be too surprising if it didn't work under certain circumstances.Edit
The loop in the sample code has been replaced by a
while
loop rather than afor
loop which wasn't very appropriate in this example.Also, the
StringBuilder.insert
method was replaced by theStringBuilder.replace
method, as Kevin Stich mentioned in the comments that thereplace
method was used rather than theinsert
to get the desired behavior.