Indenting Paragraph With cout
Asked Answered
T

5

8

Given a string of unknown length, how can you output it using cout so that the entire string displays as an indented block of text on the console? (so that even if the string wraps to a new line, the second line would have the same level of indentation)

Example:

cout << "This is a short string that isn't indented." << endl;
cout << /* Indenting Magic */ << "This is a very long string that will wrap to the next line because it is a very long string that will wrap to the next line..." << endl;

And the desired output:

This is a short string that isn't indented.

    This is a very long string that will
    wrap to the next line because it is a
    very long string that will wrap to the
    next line...

Edit: The homework assignment I'm working on is complete. The assignment has nothing to do with getting the output to format as in the above example, so I probably shouldn't have included the homework tag. This is just for my own enlightment.

I know I could count through the characters in the string, see when I get to the end of a line, then spit out a newline and output -x- number of spaces each time. I'm interested to know if there is a simpler, idiomatic C++ way to accomplish the above.

Tannenberg answered 12/3, 2011 at 5:49 Comment(5)
What have you tried so far? Have you written the code that reads the given text?Stylize
I have tried using std::setw(). I have written the code that reads the given text. I'm trying to see if there is a simple way to get cout to automatically pad each line (including lines produced from linewrapping) with a given number of characters.Tannenberg
I possibly shouldn't have used the homework tag - I'm working on a homework assignment, but the assignment is unrelated to the formatting used for the output. I was just curious how this formatting could be accomplished.Tannenberg
One problem you will find is that there is no way to get the console width from the C++ library. You would have to make some assumptions about that. (80 characters seems to be fairly standard.) You'll also want to see how long the next "word" is and compare that to how much room you have in the current line. Insert the line break if you don't have enough room.Ikeda
cool question. thanks.Quite
A
11

Here are a couple of solutions that will work if you are willing to throw out any multiple spacing and/or other whitespace between words.

The first approach, which is the most straightforward, would be to read the text into an istringstream and extract words from the stream. Before printing each word, check to see whether the word will fit on the current line and print a newline if it won't. This particular implementation won't handle words longer than the maximum line length correctly, but it wouldn't be difficult to modify it to split long words.

#include <iostream>
#include <sstream>
#include <string>

int main() {
    const unsigned max_line_length(40);
    const std::string line_prefix("    ");

    const std::string text(
        "Friends, Romans, countrymen, lend me your ears; I come to bury Caesar,"
        " not to praise him.  The evil that men do lives after them; The good "
        "is oft interred with their bones; So let it be with Caesar.");

    std::istringstream text_iss(text);

    std::string word;
    unsigned characters_written = 0;

    std::cout << line_prefix;
    while (text_iss >> word) {

        if (word.size() + characters_written > max_line_length) {
            std::cout << "\n" << line_prefix;
            characters_written = 0;
        }

        std::cout << word << " ";
        characters_written += word.size() + 1;
    }
    std::cout << std::endl;
}

A second, more "advanced" option, would be to write a custom ostream_iterator that formats lines as you expect them to be formatted. I've named this ff_ostream_iterator, for "funny formatting," but you could name it something more appropriate if you wanted to use it. This implementation does correctly split long words.

While the iterator implementation is a bit complex, the usage is quite straightforward:

int main() {
    const std::string text(
        "Friends, Romans, countrymen, lend me your ears; I come to bury Caesar,"
        " not to praise him.  The evil that men do lives after them; The good "
        "is oft interred with their bones; So let it be with Caesar. ReallyLong"
        "WordThatWontFitOnOneLineBecauseItIsSoFreakinLongSeriouslyHowLongIsThis"
        "Word");

    std::cout << "    ========================================" << std::endl;

    std::copy(text.begin(), text.end(), 
              ff_ostream_iterator(std::cerr, "    ", 40));
}

The actual implementation of the iterator is as follows:

#include <cctype>
#include <iostream>
#include <iterator>
#include <memory>
#include <sstream>
#include <string>

class ff_ostream_iterator 
    : public std::iterator<std::output_iterator_tag, char, void, void, void>
{
public:

    ff_ostream_iterator() { }

    ff_ostream_iterator(std::ostream& os,
                        std::string line_prefix, 
                        unsigned max_line_length)
        : os_(&os),
          line_prefix_(line_prefix), 
          max_line_length_(max_line_length),
          current_line_length_(),
          active_instance_(new ff_ostream_iterator*(this))
    { 
        *os_ << line_prefix;
    }

    ~ff_ostream_iterator() {
        if (*active_instance_ == this)
            insert_word();
    }

    ff_ostream_iterator& operator=(char c) {
        *active_instance_ = this;
        if (std::isspace(c)) {
            if (word_buffer_.size() > 0) {
                insert_word();
            }
        }
        else {
            word_buffer_.push_back(c);
        }
        return *this;
    }

    ff_ostream_iterator& operator*()     { return *this; }
    ff_ostream_iterator& operator++()    { return *this; }
    ff_ostream_iterator  operator++(int) { return *this; }


private:

    void insert_word() {
        if (word_buffer_.size() == 0)
            return; 

        if (word_buffer_.size() + current_line_length_ <= max_line_length_) {
            write_word(word_buffer_);
        }
        else { 
            *os_ << '\n' << line_prefix_;

            if (word_buffer_.size() <= max_line_length_) {
                current_line_length_ = 0;
                write_word(word_buffer_);
            }
            else {
                for (unsigned i(0);i<word_buffer_.size();i+=max_line_length_) 
                {
                    current_line_length_ = 0;
                    write_word(word_buffer_.substr(i, max_line_length_));
                    if (current_line_length_ == max_line_length_) {
                        *os_ << '\n' << line_prefix_;
                    }
                }
            }
        }

        word_buffer_ = "";
    }

    void write_word(const std::string& word) {
        *os_ << word;
        current_line_length_ += word.size();
        if (current_line_length_ != max_line_length_) {
            *os_ << ' ';
            ++current_line_length_;
        }
    }

    std::ostream* os_;
    std::string word_buffer_;

    std::string line_prefix_;
    unsigned max_line_length_;
    unsigned current_line_length_;

    std::shared_ptr<ff_ostream_iterator*> active_instance_;
};

[If you copy and paste this code snippet and the main from above it, it should compile and run if your compiler supports the C++0x std::shared_ptr; you can replace that with boost::shared_ptr or std::tr1::shared_ptr if your compiler doesn't have C++0x support yet.]

This approach is a bit tricky because iterators have to be copyable and we have to be sure that any remaining buffered text is only printed exactly once. We do this by relying on the fact that any time an output iterator is written to, any copies of it are no longer usable.

Afire answered 12/3, 2011 at 6:15 Comment(5)
Hmm; I now realize that I lost track of the "short strings aren't indented" requirement. Sorry about that; I got a bit sidetracked. That wouldn't be too difficult to implement, though, since you can just test the length of the string beforehand and decide whether to print it indented or unindented.Afire
Could you be so kind as to take a look at my answer? I posted it well after most of the others, so I don't think it's gotten looked at. I'd be particularly interested if you can think of anything I missed (or have a good suggestion for how to handle '\b' -- I've thought of it, but I'm not sure of all what's needed, especially if you backspace across a tab, past the beginning of the line, etc.)Cheryl
Oh, one other thing: for the indentation, do you think it's best to use space characters, or whatever the 'fill' character for the stream has been set to?Cheryl
@Jerry: That's a very good idea, using a custom streambuf!Afire
I didn't mean to imply a "short strings aren't indented" requirement. I included the non-indented string just so the indenting of the next string was apparent. Thanks for your answer!Tannenberg
C
5

This could still use a little bit of work (e.g., the indent should probably be implemented as a manipulator, but manipulators with arguments are hard to write portably -- the standard doesn't really support/define them). There are probably at least a couple of corner cases that aren't perfect (e.g., right now, it treats back-space as if it were a normal character).

#include <iostream>
#include <streambuf>
#include <iomanip>

class widthbuf: public std::streambuf {
public:
    widthbuf(int w, std::streambuf* s): indent_width(0), def_width(w), width(w), sbuf(s), count(0) {}
    ~widthbuf() { overflow('\n'); }
    void set_indent(int w) { 
        if (w == 0) {
            prefix.clear();
            indent_width = 0;
            width = def_width;
        }
        else {
            indent_width += w; 
            prefix = std::string(indent_width, ' ');
            width -= w; 
        }
    }
private:
    typedef std::basic_string<char_type> string;

    // This is basically a line-buffering stream buffer.
    // The algorithm is: 
    // - Explicit end of line ("\r" or "\n"): we flush our buffer 
    //   to the underlying stream's buffer, and set our record of
    //   the line length to 0.
    // - An "alert" character: sent to the underlying stream
    //   without recording its length, since it doesn't normally
    //   affect the a appearance of the output.
    // - tab: treated as moving to the next tab stop, which is
    //   assumed as happening every tab_width characters. 
    // - Everything else: really basic buffering with word wrapping. 
    //   We try to add the character to the buffer, and if it exceeds
    //   our line width, we search for the last space/tab in the 
    //   buffer and break the line there. If there is no space/tab, 
    //   we break the line at the limit.
    int_type overflow(int_type c) {
        if (traits_type::eq_int_type(traits_type::eof(), c))
            return traits_type::not_eof(c);
        switch (c) {
        case '\n':
        case '\r': {
                        buffer += c;
                        count = 0;
                        sbuf->sputn(prefix.c_str(), indent_width);
                        int_type rc = sbuf->sputn(buffer.c_str(), buffer.size());
                        buffer.clear();
                        return rc;
                   }
        case '\a':
            return sbuf->sputc(c);
        case '\t':
            buffer += c;
            count += tab_width - count % tab_width;
            return c;
        default:
            if (count >= width) {
                size_t wpos = buffer.find_last_of(" \t");
                if (wpos != string::npos) {
                    sbuf->sputn(prefix.c_str(), indent_width);
                    sbuf->sputn(buffer.c_str(), wpos);
                    count = buffer.size()-wpos-1;
                    buffer = string(buffer, wpos+1);
                }
                else {
                    sbuf->sputn(prefix.c_str(), indent_width);
                    sbuf->sputn(buffer.c_str(), buffer.size());
                    buffer.clear();
                    count = 0;
                }
                sbuf->sputc('\n');
            }
            buffer += c;
            ++count;
            return c;
        }
    }

    size_t indent_width;
    size_t width, def_width;
    size_t count;
    size_t tab_count;
    static const int tab_width = 8;
    std::string prefix;

    std::streambuf* sbuf;

    string buffer;
};

class widthstream : public std::ostream {
    widthbuf buf;
public:
    widthstream(size_t width, std::ostream &os) : buf(width, os.rdbuf()), std::ostream(&buf) {}
    widthstream &indent(int w) { buf.set_indent(w); return *this; }
};

int main() {
    widthstream out(30, std::cout);
    out.indent(10) << "This is a very long string that will wrap to the next line because it is a very long string that will wrap to the next line.\n";
    out.indent(0) << "This is\tsome\tmore text that should not be indented but should still be word wrapped to 30 columns.";
}

Note that indent(0) is a special case. Normally indentation starts out at 0. calling yourstream.indent(number) where number is either positive or negative adjusts the indentation relative to the previous value. yourstream.indent(0) wouldn't do anything, but I've special-cased it to reset the indentation to 0 (as an absolute). If this gets put to serious use, I'm not sure that'll work out the best in the long term, but at least for the demo it appears sufficient.

Cheryl answered 12/3, 2011 at 10:44 Comment(1)
Very nice. A streambuf-based approach definitely makes more sense than a stream_iterator. It might be cleaner to have a reset_indent(int) instead of the 0 hack. I'm not sure about \b handling: I suppose the most natural approach would be to keep buffering until overflow gets a \r or \n since you (usually?) can't backspace across a newline, but then you might end up doing a lot of unnecessary buffering. That's a good question about spaces vs. fill character; I suppose that using the fill character would be more "flexible," since you could still set that to a space.Afire
C
1

I'm not sure this is the way to do it, but if you know or can assume the screen width, my first thought is to remove the first screenWidth - indent chars from the string and print them with the preceding spaces, and keep doing that until you've done the whole string.

Conspecific answered 12/3, 2011 at 6:13 Comment(0)
C
1

You could always use '\t' to indent the line instead of a set number of characters, but I know of no simpler way to implement the logic without introducing external libraries.

Crepe answered 12/3, 2011 at 6:14 Comment(0)
W
0

This problem can be reduced to a task of minimizing amount of wasted space on each line. Suppose we have words of following lengths

{ 6,7,6,8,10,3,4,10 }

If we calculate amount of space that will be wasted when we arrange last n words without breaking them and put it into a table then we can find optimum number of words to print on current line going forward.

Here is an example for 20 character wide screen. In this table first column is number of last words, second column is length of n'th word from the end and third is the minimum space wasted:

8 6 1
7 7 7
6 5 14
5 8 2
4 10 11
3 3 1 
2 4 5
1 10 10

For example when we have only one last word of 10 letters 10 letters are wasted, if we have 2 words with second from the end 4 characters long we will have 5 letters wasted (one space between words) extra 3 letter word will leave only one spaces wasted. Adding another 10 letter word leaves us with 11 letters wasted total on 2 lines and so on.

Example

6, 7, 5 (0)
8, 10 (1)
3, 4, 10 (1) 

If we choose to print 2 words on first line wasted space is indeed 14. Numbers in () show wasted space.

6, 7 (6)
5, 8 (6)
10, 3, 4 (2)
4, 10 (6)

I think this is a well known problem and algorithm I have described is an example of dynamic programming.

Wheezy answered 12/3, 2011 at 8:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.