Отступ абзаца с помощью cout

Имея строку неизвестной длины, как вы можете вывести ее с помощью cout, чтобы вся строка отображалась в виде блока текста с отступом на консоли? (так что даже если строка переносится на новую строку, вторая строка будет иметь тот же уровень отступа)

Пример:

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;

И желаемый результат:

Это короткая строка без отступа.

    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...

Изменить: домашнее задание, над которым я работаю, выполнено. Назначение не имеет ничего общего с форматированием вывода, как в приведенном выше примере, поэтому мне, вероятно, не следовало включать тег «домашнее задание». Это просто для моего собственного просвещения.

Я знаю, что могу пересчитать символы в строке, увидеть, когда я дойду до конца строки, затем выплюнуть новую строку и каждый раз выводить -x- количество пробелов. Мне интересно узнать, есть ли более простой, идиоматический способ С++ для выполнения вышеуказанного.


person Eric G    schedule 12.03.2011    source источник
comment
что ты уже испробовал? Вы написали код, который читает данный текст?   -  person Amir Afghani    schedule 12.03.2011
comment
Я пробовал использовать std::setw(). Я написал код, который читает данный текст. Я пытаюсь выяснить, есть ли простой способ заставить cout автоматически дополнять каждую строку (включая строки, полученные в результате переноса строк) заданным количеством символов.   -  person Eric G    schedule 12.03.2011
comment
Возможно, мне не следовало использовать тег «домашнее задание» — я работаю над домашним заданием, но задание не связано с форматированием, используемым для вывода. Мне просто было любопытно, как это форматирование может быть выполнено.   -  person Eric G    schedule 12.03.2011
comment
Одна проблема, с которой вы столкнетесь, заключается в том, что нет способа получить ширину консоли из библиотеки C++. Вы должны были бы сделать некоторые предположения об этом. (80 символов кажутся довольно стандартными.) Вы также захотите увидеть длину следующего слова и сравнить ее с тем, сколько места у вас есть в текущей строке. Вставьте разрыв строки, если у вас недостаточно места.   -  person dappawit    schedule 12.03.2011
comment
классный вопрос. Благодарю.   -  person Geoff    schedule 08.09.2015


Ответы (5)


Вот несколько решений, которые будут работать, если вы готовы отказаться от любых множественных пробелов и/или других пробелов между словами.

Первый подход, который является наиболее простым, заключается в чтении текста в istringstream и извлечении слов из потока. Перед печатью каждого слова проверьте, поместится ли слово в текущей строке, и напечатайте новую строку, если нет. Эта конкретная реализация не будет корректно обрабатывать слова длиннее максимальной длины строки, но ее несложно модифицировать для разделения длинных слов.

#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;
}

Второй, более «продвинутый» вариант — написать собственный ostream_iterator, который форматирует строки так, как вы ожидаете. Я назвал это ff_ostream_iterator для «забавного форматирования», но вы можете назвать его как-то более подходящим, если хотите его использовать. Эта реализация правильно разделяет длинные слова.

Хотя реализация итератора немного сложна, использование довольно простое:

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));
}

Фактическая реализация итератора выглядит следующим образом:

#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_;
};

[Если вы скопируете и вставите этот фрагмент кода и main сверху, он должен скомпилироваться и запуститься, если ваш компилятор поддерживает C++0x std::shared_ptr; вы можете заменить это на boost::shared_ptr или std::tr1::shared_ptr, если ваш компилятор еще не поддерживает C++0x.]

Этот подход немного сложен, потому что итераторы должны быть копируемыми, и мы должны быть уверены, что любой оставшийся буферизованный текст будет напечатан только один раз. Мы делаем это, полагаясь на тот факт, что каждый раз, когда производится запись в выходной итератор, любые его копии больше нельзя использовать.

person James McNellis    schedule 12.03.2011
comment
Хм; Теперь я понимаю, что я потерял из виду, что короткие строки не требуют отступа. Извини за это; Я немного отвлекся. Это было бы не слишком сложно реализовать, поскольку вы можете просто заранее проверить длину строки и решить, следует ли печатать ее с отступом или без отступа. - person James McNellis; 12.03.2011
comment
Не могли бы вы так любезно взглянуть на мой ответ? Я разместил его намного позже большинства других, поэтому я не думаю, что на него посмотрели. Мне было бы особенно интересно, если бы вы могли подумать о чем-нибудь, что я пропустил (или у вас есть хорошее предложение о том, как обращаться с '\b' - я думал об этом, но я не уверен во всем, что нужно, особенно если вы выполняете возврат на табуляцию, за начало строки и т. д.) - person Jerry Coffin; 12.03.2011
comment
О, еще одно: для отступа, как вы думаете, лучше использовать символы пробела или любой другой символ «заполнения» для потока? - person Jerry Coffin; 12.03.2011
comment
@Jerry: Это очень хорошая идея - использовать собственный streambuf! - person James McNellis; 12.03.2011
comment
Я не имел в виду, что короткие строки не требуют отступа. Я включил строку без отступа, чтобы отступ следующей строки был очевиден. Спасибо за Ваш ответ! - person Eric G; 12.03.2011

Это все еще может потребовать некоторой работы (например, indent, вероятно, следует реализовать как манипулятор, но манипуляторы с аргументами трудно писать переносимыми — стандарт на самом деле не поддерживает/не определяет их ). Вероятно, есть по крайней мере пара угловых случаев, которые не идеальны (например, прямо сейчас он обрабатывает back-space, как если бы это был обычный символ).

#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.";
}

Обратите внимание, что indent(0) — это особый случай. Обычно отступ начинается с 0. Вызов yourstream.indent(number), где number является либо положительным, либо отрицательным, регулирует отступ относительно предыдущего значения. yourstream.indent(0) не будет ничего делать, но я присвоил ему особый случай, чтобы сбросить отступ до 0 (абсолютно). Если это найдет серьезное применение, я не уверен, что это сработает наилучшим образом в долгосрочной перспективе, но, по крайней мере, для демонстрации этого достаточно.

person Jerry Coffin    schedule 12.03.2011
comment
Очень хорошо. Подход, основанный на streambuf, определенно имеет больше смысла, чем stream_iterator. Возможно, было бы чище иметь reset_indent(int) вместо хака 0. Я не уверен насчет обработки \b: я полагаю, что наиболее естественным подходом было бы продолжать буферизацию до тех пор, пока overflow не получит \r или \n, поскольку вы (обычно?) не можете вернуться на новую строку, но тогда вы можете в конечном итоге сделать много ненужной буферизации. Это хороший вопрос о пробелах и заполняющих символах; Я полагаю, что использование символа заполнения было бы более гибким, поскольку вы все равно могли бы установить его на пробел. - person James McNellis; 12.03.2011

Я не уверен, что это правильный способ сделать это, но если вы знаете или можете предположить ширину экрана, моя первая мысль состоит в том, чтобы удалить первые screenWidth - indent символов из строки и напечатать их с предшествующим пробелы, и продолжайте делать это, пока не сделаете всю строку.

person Brad Mace    schedule 12.03.2011

Вы всегда можете использовать '\t' для отступа строки вместо заданного количества символов, но я не знаю более простого способа реализовать логику без использования внешних библиотек.

person Chuck Callebs    schedule 12.03.2011

Эту проблему можно свести к задаче минимизации количества неиспользуемого пространства в каждой строке. Предположим, у нас есть слова следующих длин

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

Если мы подсчитаем количество места, которое будет потрачено впустую, когда мы упорядочим последние n слов, не разбивая их, и поместим его в таблицу, то мы сможем найти оптимальное количество слов для печати в текущей строке вперед.

Вот пример для 20-символьного широкого экрана. В этой таблице первый столбец - это количество последних слов, второй столбец - длина n-го слова с конца, а третий - минимальное потраченное пространство:

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

Например, когда у нас есть только одно последнее слово из 10 букв, 10 букв тратятся впустую, если у нас есть 2 слова со вторым с конца 4 символом длиной, у нас будет 5 букв впустую (один пробел между словами) дополнительное 3-буквенное слово оставит только одно места потрачены впустую. Добавление еще одного слова из 10 букв оставляет нам 11 букв впустую на 2 строки и так далее.

Пример

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

Если мы решим напечатать 2 слова в первой строке, потерянное пространство действительно равно 14. Числа в () показывают потраченное впустую пространство.

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

Я думаю, что это хорошо известная проблема, и алгоритм, который я описал, является примером динамического программирования.

person Alexei Polkhanov    schedule 12.03.2011