Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I wrap text to a given width with Guava?

I would like to be able to wrap a long String to a fixed length. Is there a way to do that in Guava?

Apache Commons / Lang has the method WordUtils.wrap(String, length) that does exactly what I need. Does Guava have a simple means to accomplish this?

I know I can do a hard wrap using Splitter.fixedLength(int), but I would like a soft wrap.


UPDATE: There is now a bounty for this question.

Obviously this functionality isn't available in Guava out of the Box, so the bounty goes to the most concise (or most complete) and Guava-like answer that uses what's there in Guava. No libs except Guava allowed.

like image 299
Sean Patrick Floyd Avatar asked Apr 14 '11 10:04

Sean Patrick Floyd


4 Answers

We (Guava) strongly recommend you use ICU4J's BreakIterator class to handle the mechanics of finding break points in user text.

like image 55
Kevin Bourrillion Avatar answered Oct 11 '22 18:10

Kevin Bourrillion


Here's my own answer, for inspiration:

public final class TextWrapper {

    enum Strategy implements WrapStrategy {
        HARD {

            @Override
            public String wrap(final Iterable<String> words, final int width) {
                return Joiner.on('\n')
                             .join(Splitter
                                    .fixedLength(width)
                                    .split(
                                        Joiner.on(' ').join(words)));
            }
        },
        SOFT {
            @Override
            public String wrap(final Iterable<String> words, final int width) {
                final StringBuilder sb = new StringBuilder();
                int lineLength = 0;
                final Iterator<String> iterator = words.iterator();
                if (iterator.hasNext()) {
                    sb.append(iterator.next());
                    lineLength=sb.length();
                    while (iterator.hasNext()) {
                        final String word = iterator.next();
                        if(word.length()+1+lineLength>width) {
                            sb.append('\n');
                            lineLength=0;
                        } else {
                            lineLength++;
                            sb.append(' ');
                        }
                        sb.append(word);
                        lineLength+=word.length();
                    }
                }
                return sb.toString();
            }
        }
    }

    interface WrapStrategy {
        String wrap(Iterable<String> words, int width);
    }

    public static TextWrapper forWidth(final int i) {
        return new TextWrapper(Strategy.SOFT, CharMatcher.WHITESPACE, i);
    }

    private final WrapStrategy  strategy;

    private final CharMatcher   delimiter;

    private final int           width;

    TextWrapper(final WrapStrategy strategy,
                final CharMatcher delimiter, final int width) {
        this.strategy = strategy;
        this.delimiter = delimiter;
        this.width = width;
    }

    public TextWrapper hard(){
        return new TextWrapper(Strategy.HARD, this.delimiter, this.width);
    }
    public TextWrapper respectExistingBreaks() {
        return new TextWrapper(
            this.strategy, CharMatcher.anyOf(" \t"), this.width);
    }

    public String wrap(final String text) {
        return this.strategy.wrap(
            Splitter.on(this.delimiter).split(text), this.width);
    }

}

Sample Usage 1: (hard wrapping at 80 chars)

TextWrapper.forWidth(80)
        .hard()
        .wrap("Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n" +
            "Maecenas porttitor risus vitae urna hendrerit ac condimentum " +
            "odio tincidunt.\nDonec porttitor felis quis nulla aliquet " +
            "lobortis. Suspendisse mattis sapien ut metus congue tincidunt. " +
            "Quisque gravida, augue sed congue tempor, tortor augue rhoncus " +
            "leo, eget luctus nisl risus id erat. Nunc tempor pretium gravida.");

Output:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas porttitor risu
s vitae urna hendrerit ac condimentum odio tincidunt. Donec porttitor felis quis
 nulla aliquet lobortis. Suspendisse mattis sapien ut metus congue tincidunt. Qu
isque gravida, augue sed congue tempor, tortor augue rhoncus leo, eget luctus ni
sl risus id erat. Nunc tempor pretium gravida.

Sample Usage 2: (soft wrapping at or or before 60 chars, keep existing line breaks)

TextWrapper.forWidth(60)
    .respectExistingBreaks()
    .wrap("Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n" +
    "Maecenas porttitor risus vitae urna hendrerit ac condimentum " +
    "odio tincidunt.\nDonec porttitor felis quis nulla aliquet " +
    "lobortis. Suspendisse mattis sapien ut metus congue tincidunt. " +
    "Quisque gravida, augue sed congue tempor, tortor augue rhoncus " +
    "leo, eget luctus nisl risus id erat. Nunc tempor pretium gravida.");

Output:

Lorem ipsum dolor sit amet, consectetur adipiscing
elit.
Maecenas porttitor risus vitae urna hendrerit ac
condimentum odio tincidunt.
Donec porttitor felis quis nulla
aliquet lobortis. Suspendisse mattis sapien ut metus congue
tincidunt. Quisque gravida, augue sed congue tempor, tortor
augue rhoncus leo, eget luctus nisl risus id erat. Nunc
tempor pretium gravida.
like image 36
Sean Patrick Floyd Avatar answered Oct 11 '22 20:10

Sean Patrick Floyd


why use guava to do something more simple without guava?

In fact, the Splitter class allows you to do an hard wrap using fixedLength() method, otherwise you can split a string depending on a separator char or String. If you want to use guava, you can rely on Splitter.on(' ').split(string), but you have also to join the results replacing ' ' with '\n' depending on maxLength value.

Without using guava, you can also do what you want. A few lines of code, with no dependencies. Basically, you can use the commons-lang approach, simplifying it. This is my wrap method:

public static String wrap(String str, int wrapLength) {
    int offset = 0;
    StringBuilder resultBuilder = new StringBuilder();

    while ((str.length() - offset) > wrapLength) {
        if (str.charAt(offset) == ' ') {
            offset++;
            continue;
        }

        int spaceToWrapAt = str.lastIndexOf(' ', wrapLength + offset);
        // if the next string with length maxLength doesn't contain ' '
        if (spaceToWrapAt < offset) {
            spaceToWrapAt = str.indexOf(' ', wrapLength + offset);
            // if no more ' '
            if (spaceToWrapAt < 0) {
                break;
            }
        }

        resultBuilder.append(str.substring(offset, spaceToWrapAt));
        resultBuilder.append("\n");
        offset = spaceToWrapAt + 1;
    }

    resultBuilder.append(str.substring(offset));
    return resultBuilder.toString();
}

Yes, it's very similar to the original commons-lang method, but shorter, easier and based on your needs, I guess. Maybe, this solution is also more efficient than yours, isn't it?

I've tested it with your text, comparing my result with commons-lang result. It seems to work:

public static void main(String[] args) {

    String string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"
            + "Maecenas porttitor risus vitae urna hendrerit ac condimentum "
            + "odio tincidunt.\nDonec porttitor felis quis nulla aliquet "
            + "lobortis. Suspendisse mattis sapien ut metus congue tincidunt. "
            + "Quisque gravida, augue sed congue tempor, tortor augue rhoncus "
            + "leo, eget luctus nisl risus id erat. Nunc tempor pretium gravida.";

    for (int maxLength = 2; maxLength < string.length(); maxLength++) {
        String expectedResult = WordUtils.wrap(string, maxLength);
        String actualResult = wrap(string, maxLength);

        if (!expectedResult.equals(actualResult)) {
            System.out.println("expectedResult: \n" + expectedResult);
            System.out.println("\nactualResult: \n" + actualResult);
            throw new RuntimeException(
                    "actualResult is not the same as expectedResult (maxLength:"
                            + maxLength + ")");
        }
    }
}

So, the matter is: do you really want to use guava to do this? What are the benefits related to this choice?

like image 9
javanna Avatar answered Oct 11 '22 18:10

javanna


I did this for fun just to do as much in guava as possible. javanna's answer is better though,

import java.util.Iterator;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;


public class SoftSplit {

    public static String softSplit(String string, int length) {
        //break up into words
        Iterable<String> words = Splitter.on(' ').split(string);

        //an iterator that will return the words with appropriate
        //white space added
        final SoftSplitIterator softIter = new SoftSplitIterator(words, length);
        return Joiner.on("").join(new Iterable<String>() {
            @Override
            public Iterator<String> iterator() {
                return softIter;
            }
        });
    }

    static class SoftSplitIterator implements Iterator<String> {
        private final int maxLength;
        private final PeekingIterator<String> words;
        private int currentLineLength;

        SoftSplitIterator(Iterable<String> words, int maxLength) {
            this.words = Iterators.peekingIterator(words.iterator());
            this.maxLength = maxLength;
        }

        @Override
        public boolean hasNext() {
            return words.hasNext();
        }

        @Override
        public String next() {
            String current = words.next();

            //strip leading spaces at the start of a line
            if(current.length() == 0 && currentLineLength == 0) {
                return "";
            }
            //nothing left after us
            if(!words.hasNext()) {
                return current;
            }
            String next = words.peek();

            if(currentLineLength + current.length() + next.length() < maxLength) {
                //this word and the next one won't put us over limit
                currentLineLength += current.length();
                return current + " ";
            } else {
                //the next word will put us over the limit 
                //add a line break
                currentLineLength = 0;
                return current + "\n";
            }
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    public static void main(String[] args) {
        String text = 
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
            "Maecenas porttitor risus vitae urna hendrerit ac condimentum " +
            "odio tincidunt. Donec porttitor felis quis nulla aliquet " +
            "lobortis. Suspendisse mattis sapien ut metus congue tincidunt. " +
            "Quisque gravida, augue sed congue tempor, tortor augue rhoncus " +
            "leo, eget luctus nisl risus id erat. Nunc tempor pretium gravida.";
        System.out.println(softSplit(text, 60));
    }
}
like image 8
sbridges Avatar answered Oct 11 '22 18:10

sbridges