Indenting Paragraph With cout
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..." &l开发者_如何转开发t;< 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.
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.
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.
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.
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.
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.
精彩评论