Center text in fixed-width field with stream manipulators in C++
Asked Answered
S

6

24

I am refactoring some legacy code which is using printf with longs strings (without any actual formatting) to print out plain text table headers which looks notionally like this:

|  Table   |  Column  | Header  |

which are currently being produced like this:

printf("|  Table   |  Column  | Header  |");

I would like to produce the above with code to the effect of1:

outputStream << "|" << std::setw(10) << std::center << "Table"
             << "|" << std::setw(10) << std::center << "Column"
             << "|" << std::setw(9) << std::center << "Header"
             << "|" << std::endl;

which does not compile because <iomanip> has the stream manipulators std::left, std::right and std::internal, but does not seem to have any std::center. Is there a clean way to do this already in standard C++ libraries, or will I have to manually compute the necessary spacing?


1Even though this is more verbose than the C code, it will be less verbose in the long run because of the number of printf statements and the amount of infixed duplication in their strings. It will also be more extensible and maintainable.

Syringomyelia answered 13/2, 2013 at 19:2 Comment(0)
D
17

Here's a helper class that accomplish what you want:

#include <string>
#include <iostream>
#include <iomanip>

template<typename charT, typename traits = std::char_traits<charT> >
class center_helper {
    std::basic_string<charT, traits> str_;
public:
    center_helper(std::basic_string<charT, traits> str) : str_(str) {}
    template<typename a, typename b>
    friend std::basic_ostream<a, b>& operator<<(std::basic_ostream<a, b>& s, const center_helper<a, b>& c);
};

template<typename charT, typename traits = std::char_traits<charT> >
center_helper<charT, traits> centered(std::basic_string<charT, traits> str) {
    return center_helper<charT, traits>(str);
}

// redeclare for std::string directly so we can support anything that implicitly converts to std::string
center_helper<std::string::value_type, std::string::traits_type> centered(const std::string& str) {
    return center_helper<std::string::value_type, std::string::traits_type>(str);
}

template<typename charT, typename traits>
std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& s, const center_helper<charT, traits>& c) {
    std::streamsize w = s.width();
    if (w > c.str_.length()) {
        std::streamsize left = (w + c.str_.length()) / 2;
        s.width(left);
        s << c.str_;
        s.width(w - left);
        s << "";
    } else {
        s << c.str_;
    }
    return s;
}

It's used simply by calling centered("String"), like so:

int main(int argc, char *argv[]) {
    std::cout << "|" << std::setw(10) << centered("Table")
              << "|" << std::setw(10) << centered("Column")
              << "|" << std::setw(9)  << centered("Header") << "|"
              << std::endl;
}
Davidoff answered 13/2, 2013 at 19:18 Comment(20)
Ah!! I like that. Very clever.Syringomyelia
+1. I tested it here : test-codeAiriness
Is this good only for C++0x? In MSVS 2010 I get "default template arguments are only allowed on a class template". According to this SO answer this has been fixed in C++0x.Emoryemote
I get numerous errors with this under c++11, including the one about the default template arguments.Eulaeulachon
@jep: If you're getting a message about default template arguments, then you probably aren't compiling with C++11.Davidoff
@KevinBallard: No, that's not the problem. My CMake flags include -std=c++11. And trust me, LOTS of stuff in my code wouldn't work if that was off. :)Eulaeulachon
@jep: The only other reason why you should be getting compiler errors is if you don't have all the right #includes.Davidoff
If you edit your comment to include the #includes you included, I'll include them in my #includes.Eulaeulachon
@jep: Ok, I added the #includes.Davidoff
Okay, my entire program is just that code plus a main func. I get: pastebin.com/raw.php?i=zmRG3Xcf Environment is cygwin 2.0.2 64bit/gcc 4.9.2/CMake 3.2.2.Eulaeulachon
Oh, and in CMakeLists.txt: set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")Eulaeulachon
@jep: Strange. It works perfectly fine in Clang. On line 24, try removing the default template parameter, so it's just template<typename charT, typename traits>. Does that work?Davidoff
Yep, that did it! Wish I could give you more than one upvote for the effort. One last thing if you're up for it. clion gives the old red squiggle to line 21's return center_helper<char>(str) bit with the claim: Returning 'center_helper<char>' from a function returning 'center_helper': Class 'center_helper<char>' is not compatible with class 'center_helper' at line 21 As it compiles fine, it seems like a bug I need to report to them. Do you have any advice on where it's going wrong so I can give a better report?Eulaeulachon
@jep: What happens if you change that line to say return center_helper<std::string::value_type, std::string::traits_type>(str);?Davidoff
Also, I just edited my answer to remove the default template argument on line 24. It's not actually doing anything, I'm not really sure why I had it there in the first place, probably just because I was matching the template arguments to the center_helper class.Davidoff
Right as rain now. Thanks, as that will help me give JetBrains a bit more info for my report.Eulaeulachon
@jep: Awesome. I'll edit the answer again to include that final change.Davidoff
Proposal 1): pass by value and move into member for enhanced efficiency of the str argument of the center_helper template 2) Add explicit in front of the constructor to avoid unwanted automatic conversions happening. Like explicit center_helper(std::basic_string<charT, traits> str) : str_(std::move(str)) {}Ampulla
@LilyBallard Greetings from the future (2019). I am having a hell of a time making this work in its own .h and .cpp files. Do you have these available in a github repository?Belanger
@Belanger I don't have this available anywhere outside of this SO answer. However I just tested and it still works. Make sure you're using C++11 or later. I've only tested single-file setups but I can't imagine why putting it in separate files wouldn't work.Davidoff
L
27

In C++20 you'll be able to use std::format to do this:

outputStream << std::format("|{:^10}|{:^10}|{:^9}|\n",
                            "Table", "Column", "Header");

Output:

|  Table   |  Column  | Header  |

In the meantime you can use the {fmt} library, std::format is based on. {fmt} also provides the print function that makes this even easier and more efficient (godbolt):

fmt::print("|{:^10}|{:^10}|{:^9}|\n", "Table", "Column", "Header");

Disclaimer: I'm the author of {fmt} and C++20 std::format.

Lenora answered 16/12, 2020 at 15:34 Comment(0)
D
17

Here's a helper class that accomplish what you want:

#include <string>
#include <iostream>
#include <iomanip>

template<typename charT, typename traits = std::char_traits<charT> >
class center_helper {
    std::basic_string<charT, traits> str_;
public:
    center_helper(std::basic_string<charT, traits> str) : str_(str) {}
    template<typename a, typename b>
    friend std::basic_ostream<a, b>& operator<<(std::basic_ostream<a, b>& s, const center_helper<a, b>& c);
};

template<typename charT, typename traits = std::char_traits<charT> >
center_helper<charT, traits> centered(std::basic_string<charT, traits> str) {
    return center_helper<charT, traits>(str);
}

// redeclare for std::string directly so we can support anything that implicitly converts to std::string
center_helper<std::string::value_type, std::string::traits_type> centered(const std::string& str) {
    return center_helper<std::string::value_type, std::string::traits_type>(str);
}

template<typename charT, typename traits>
std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& s, const center_helper<charT, traits>& c) {
    std::streamsize w = s.width();
    if (w > c.str_.length()) {
        std::streamsize left = (w + c.str_.length()) / 2;
        s.width(left);
        s << c.str_;
        s.width(w - left);
        s << "";
    } else {
        s << c.str_;
    }
    return s;
}

It's used simply by calling centered("String"), like so:

int main(int argc, char *argv[]) {
    std::cout << "|" << std::setw(10) << centered("Table")
              << "|" << std::setw(10) << centered("Column")
              << "|" << std::setw(9)  << centered("Header") << "|"
              << std::endl;
}
Davidoff answered 13/2, 2013 at 19:18 Comment(20)
Ah!! I like that. Very clever.Syringomyelia
+1. I tested it here : test-codeAiriness
Is this good only for C++0x? In MSVS 2010 I get "default template arguments are only allowed on a class template". According to this SO answer this has been fixed in C++0x.Emoryemote
I get numerous errors with this under c++11, including the one about the default template arguments.Eulaeulachon
@jep: If you're getting a message about default template arguments, then you probably aren't compiling with C++11.Davidoff
@KevinBallard: No, that's not the problem. My CMake flags include -std=c++11. And trust me, LOTS of stuff in my code wouldn't work if that was off. :)Eulaeulachon
@jep: The only other reason why you should be getting compiler errors is if you don't have all the right #includes.Davidoff
If you edit your comment to include the #includes you included, I'll include them in my #includes.Eulaeulachon
@jep: Ok, I added the #includes.Davidoff
Okay, my entire program is just that code plus a main func. I get: pastebin.com/raw.php?i=zmRG3Xcf Environment is cygwin 2.0.2 64bit/gcc 4.9.2/CMake 3.2.2.Eulaeulachon
Oh, and in CMakeLists.txt: set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")Eulaeulachon
@jep: Strange. It works perfectly fine in Clang. On line 24, try removing the default template parameter, so it's just template<typename charT, typename traits>. Does that work?Davidoff
Yep, that did it! Wish I could give you more than one upvote for the effort. One last thing if you're up for it. clion gives the old red squiggle to line 21's return center_helper<char>(str) bit with the claim: Returning 'center_helper<char>' from a function returning 'center_helper': Class 'center_helper<char>' is not compatible with class 'center_helper' at line 21 As it compiles fine, it seems like a bug I need to report to them. Do you have any advice on where it's going wrong so I can give a better report?Eulaeulachon
@jep: What happens if you change that line to say return center_helper<std::string::value_type, std::string::traits_type>(str);?Davidoff
Also, I just edited my answer to remove the default template argument on line 24. It's not actually doing anything, I'm not really sure why I had it there in the first place, probably just because I was matching the template arguments to the center_helper class.Davidoff
Right as rain now. Thanks, as that will help me give JetBrains a bit more info for my report.Eulaeulachon
@jep: Awesome. I'll edit the answer again to include that final change.Davidoff
Proposal 1): pass by value and move into member for enhanced efficiency of the str argument of the center_helper template 2) Add explicit in front of the constructor to avoid unwanted automatic conversions happening. Like explicit center_helper(std::basic_string<charT, traits> str) : str_(std::move(str)) {}Ampulla
@LilyBallard Greetings from the future (2019). I am having a hell of a time making this work in its own .h and .cpp files. Do you have these available in a github repository?Belanger
@Belanger I don't have this available anywhere outside of this SO answer. However I just tested and it still works. Make sure you're using C++11 or later. I've only tested single-file setups but I can't imagine why putting it in separate files wouldn't work.Davidoff
N
9

There is no std::center manipulator. I am afraid you have to do it yourself. You could write a helper function to calculate the spaces given the width and the string, to reduce the efforts.

Here's a sample of what a helper function might look like. It needs some work to make it more efficient, etc.

string helper(int width, const string& str) {
    int len = str.length();
    if(width < len) { return str; }

    int diff = width - len;
    int pad1 = diff/2;
    int pad2 = diff - pad1;
    return string(pad1, ' ') + str + string(pad2, ' ');
}
Notate answered 13/2, 2013 at 19:7 Comment(0)
S
3

I'm afraid you'll have to do it manually. But it's not that hard if you work with strings. Something like:

std::string
centered( std::string const& original, int targetSize )
{
    assert( targetSize >= 0 );
    int padding = targetSize - checked_cast<int>( original.size() );
    return padding > 0
        ? std::string( padding / 2, ' ' ) 
            + original
            + std::string( targetSize - (padding / 2), ' ' )
        : original;
}

should do the trick.

Setser answered 13/2, 2013 at 19:10 Comment(0)
V
1

This is just for the ones who don't need any additional changes in the code and not include any new libraries like me.

There are many answers given above which require new helper classes or additional libraries, but there is a simple hack instead which wont require any additional changes. You can divide the width by 2 and add the setw() method on either side of the element to be printed. As a solution to the question :

outputStream << "|" << std::setw(5) << "Table" << std::setw(5)
             << "|" << std::setw(5) << "Column" << std::setw(5)
             << "|" << std::setw(5) << "Header" << std::setw(5)
             << "|" << std::endl;

The above code will do the work.

Output:

|  Table   |  Column  | Header  |
Velda answered 27/6, 2021 at 12:3 Comment(0)
S
0

you could use the NCURSES (#include <ncurses.h>) library... it's kinda funky and you have to add "-lncurses" to your compile command (gcc yourProgram.c/cpp -lncurses) and it'll work plus you can do colors and other 'cool' stuff (for CLI anyway). just read the manual, centering stuff is pretty easy. http://tldp.org/HOWTO/NCURSES-Programming-HOWTO/

Swigart answered 22/4, 2017 at 0:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.