Fmt: flexible std::complex formatter

Created on 9 Dec 2019  路  11Comments  路  Source: fmtlib/fmt

So... c++20 will have a formatter for std::complex (https://www.zverovich.net/2019/07/23/std-format-cpp20.html, https://wg21.link/p1636r0). In the N years between now and when that's widely available, is there something that will vaguely approximate what the standard will be? is there a proposed implementation yet? google doesn't find me anything, so...

I've come up with this, is it ok? Any ideas on how to improve it? It doesnt handle width and align properly yet. Wanted to share it at least, because I couldn't find anything like it, and hopefully someone can improve it....

If this is totally uninteresting feel free to close the issue, at least the next person searching for a std::complex formatter will be able to find a starting place :)

/// This formats a
/// complex number (a+bi) as (a+bi), offering the same formatting
/// options as the underlying type - with the addition of three
/// optional format options, only one of which may appear directly
/// after the ':' in the format spec (before any fill or align): '$'
/// (the default if no flag is specified), '*', and ',".  The '*' flag
/// adds a * before the 'i', producing (a+b*i), where a and b are the
/// formatted value_type values.  The ',' flag simply prints the real
/// and complex parts separated by a comma (same as iostreams' format).
/// As a concrete exmple, this formatter can produce either (3+5.4i)
/// or (3+5.4*i) or (3,5.4) for a complex<double> using the specs {:g}
/// | {:$g}, {:*g}, or {:,g}, respectively.  (this implementation is a
/// bit hacky - glad for cleanups).
///
template <typename T, typename Char>
struct fmt::formatter<std::complex<T>,Char> : public fmt::formatter<T,Char>
{
  typedef fmt::formatter<T,Char> base;
  enum style { expr, star, pair } style_ = expr;
  internal::dynamic_format_specs<Char> specs_;
  constexpr auto parse(format_parse_context & ctx) -> decltype(ctx.begin()) {
    using handler_type = internal::dynamic_specs_handler<format_parse_context>;
    auto type = internal::type_constant<T, Char>::value;
    internal::specs_checker<handler_type> handler(handler_type(specs_, ctx), type);
    auto it = ctx.begin();
    switch (*it) {
    case '$': style_ = style::expr; ctx.advance_to(++it); break;
    case '*': style_ = style::star; ctx.advance_to(++it); break;
    case ',': style_ = style::pair; ctx.advance_to(++it); break;
    default: break;
    }
    if (style_ != style::pair)
      parse_format_specs(ctx.begin(), ctx.end(), handler);
    //todo: fixup alignment
    return base::parse(ctx);
  }
  template <typename FormatCtx>
  auto format(const std::complex<T> & x, FormatCtx & ctx) -> decltype(ctx.out()) {
    format_to(ctx.out(), "(");
    if (x.real() || !x.imag())
      base::format(x.real(), ctx);
    if (x.imag()) {
      if (style_ == style::pair)
        format_to(ctx.out(), ",");
      else
        if (x.real() && x.imag() >= 0 && specs_.sign != sign::plus)
          format_to(ctx.out(), "+");
      base::format(x.imag(), ctx);
      if (style_ != style::pair) {
        if (style_ == style::star)
          format_to(ctx.out(), "*i");
        else
          format_to(ctx.out(), "i");
        if (std::is_same<typename std::decay<T>::type,float>::value)       format_to(ctx.out(), "f");
        if (std::is_same<typename std::decay<T>::type,long double>::value) format_to(ctx.out(), "l");
      }
    }
    return format_to(ctx.out(), ")");
  }
};

Most helpful comment

Ugh, of course that has a bug in it -- here's a link instead for future passers-by : https://gitlab.com/tesch1/cppduals/blob/master/duals/dual#L1276 at least they'll land somewhere close to a more tested version of the above.

Just a side note, in the above, I did a little hacky re-parsing of the fmt string when inheriting from a standard formatter: it would have been nice if the specs_ member was protected rather than private, to allow reuse of parse() from custom formatters that subclass formatter<> of a basic type.

On the accusation of niche -- I have to defend std::complex's honor a little here :o) a lot of people do use std::complex, probably more than use libfmt, even. A surprising amount of what Feynmann would have called science is done in C++, and I'd wager a limb that a lot of them also dont love the default (a,b) iostreams formatting and wish for something a little easier on the eyes.

I apologize for the noise, but this seems like a good place to more thoroughly collect some complex number formatting options. A brief survey:

language|basic format|result of sqrt(-1)|result of sqrt(-1)-sqrt(-1)
---|---|---|---
C++ iostreams|(3,4)|(0,1)|(0,0)
numpy|(3+4j)|1j|0j
julia|3.0 + 4.0im|0.0 + 1.0im|0.0 + 0.0im
octave|3 + 4i|0 + 1i|0
mathematica|1+i|i*|0
R|(3+4i)|(0+1i)|(0+0i)
c++14 literals,float|3+4if|1if|0if
go|(3+4i)|(0+1i)|(0+0i)

* - checked via wolframalpha

The variables :

  • default number format (I think inheriting the value_type's default format is the right thing)
  • whether to include parenthesis
    a) always (R) or
    b) only when both re and im parts are non-zero (numpy) or
    c) never
  • the amount of space around the +
  • the form of the imaginary unit: j, i, i, im
  • zero complex number should be 0, 0j, or 0+0j, mutually-exclusively:

    • if the real part should be omitted if it's zero,

    • if the imag part should be omitted if it's zero

  • whether the imaginary unit can be concatenated to indicate product (didn't include an example above, but it's true for a language i'm using) ie, (3+4*i)
  • whether there is a precision suffix, to be compatible with the std::complex string literals (float,double,long double) -> (1if, 1i, 1il)
    -> whether -0.0 and 0.0 are displayed as the same object

edit: clarify that c++ meant iostreams
edit: add go to survey table

All 11 comments

Looks interesting. I don't think that it needs to be in the core library considering that complex is somewhat niche and that it's not hard to implement its formatting via the extension API as you did. Thanks for sharing the code and hopefully someone finds it useful!

Ugh, of course that has a bug in it -- here's a link instead for future passers-by : https://gitlab.com/tesch1/cppduals/blob/master/duals/dual#L1276 at least they'll land somewhere close to a more tested version of the above.

Just a side note, in the above, I did a little hacky re-parsing of the fmt string when inheriting from a standard formatter: it would have been nice if the specs_ member was protected rather than private, to allow reuse of parse() from custom formatters that subclass formatter<> of a basic type.

On the accusation of niche -- I have to defend std::complex's honor a little here :o) a lot of people do use std::complex, probably more than use libfmt, even. A surprising amount of what Feynmann would have called science is done in C++, and I'd wager a limb that a lot of them also dont love the default (a,b) iostreams formatting and wish for something a little easier on the eyes.

I apologize for the noise, but this seems like a good place to more thoroughly collect some complex number formatting options. A brief survey:

language|basic format|result of sqrt(-1)|result of sqrt(-1)-sqrt(-1)
---|---|---|---
C++ iostreams|(3,4)|(0,1)|(0,0)
numpy|(3+4j)|1j|0j
julia|3.0 + 4.0im|0.0 + 1.0im|0.0 + 0.0im
octave|3 + 4i|0 + 1i|0
mathematica|1+i|i*|0
R|(3+4i)|(0+1i)|(0+0i)
c++14 literals,float|3+4if|1if|0if
go|(3+4i)|(0+1i)|(0+0i)

* - checked via wolframalpha

The variables :

  • default number format (I think inheriting the value_type's default format is the right thing)
  • whether to include parenthesis
    a) always (R) or
    b) only when both re and im parts are non-zero (numpy) or
    c) never
  • the amount of space around the +
  • the form of the imaginary unit: j, i, i, im
  • zero complex number should be 0, 0j, or 0+0j, mutually-exclusively:

    • if the real part should be omitted if it's zero,

    • if the imag part should be omitted if it's zero

  • whether the imaginary unit can be concatenated to indicate product (didn't include an example above, but it's true for a language i'm using) ie, (3+4*i)
  • whether there is a precision suffix, to be compatible with the std::complex string literals (float,double,long double) -> (1if, 1i, 1il)
    -> whether -0.0 and 0.0 are displayed as the same object

edit: clarify that c++ meant iostreams
edit: add go to survey table

a lot of them also dont love the default (a,b) iostreams formatting and wish for something a little easier on the eyes.

P1636 didn't make it into C++20, so there is an opportunity to improve the default formatting for complex in std::format. Since you've already done some research, would you be willing to write a small ISO C++ proposal to do that? I'll be happy to review and champion it at the ISO C++ standards committee meeting if you are unable to attend yourself.

Yeah, would be glad to. I guess if I complain I should be willing to try to improve things too.

Another data point: python's discussion on the issue: https://bugs.python.org/issue1588

Although, I ~think they screwed it up a bit too, from a correctness perspective.~ make incorrect assumptions from time to time.

edit: an ounce of humility

Hey @tesch1, are you still interested in writing a proposal?

hey, yeah, theoretically I am.. practically I dont have time. I started on it a bit here: https://github.com/tesch1/complex_fmt_scn if someone wants to take over that's perfectly ok w/ me.

I think the paper already contains all the important pieces. We don't really need a wording for the first revision. I can add examples, submit and champion it on your behalf.

Sounds great to me!

@tesch1 Thanks for the legwork. I ran into a snag, ASan throws a fit + segtault unless I test the it before dereferencing:

    auto it = ctx.begin();
    if (it != ctx.end()) {

Repro:

#include <complex>
#include <vector>
#include <numeric>
#include <fmt/ranges.h>

template <typename T, typename Char>
struct fmt::formatter<std::complex<T>, Char> : public fmt::formatter<T, Char> {
    typedef fmt::formatter<T, Char> base;
    enum style { expr, star, pair } style_ = expr;
    internal::dynamic_format_specs<Char> specs_;
    constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
        using handler_type =
            internal::dynamic_specs_handler<format_parse_context>;
        auto type = internal::type_constant<T, Char>::value;
        internal::specs_checker<handler_type> handler(handler_type(specs_, ctx),
                                                      type);
        auto it = ctx.begin();
        /*if (it != ctx.end())*/ { // uncomment to remove SEGV
            switch (*it) {
                case '$': style_ = style::expr; ctx.advance_to(++it); break;
                case '*': style_ = style::star; ctx.advance_to(++it); break;
                case ',': style_ = style::pair; ctx.advance_to(++it); break;
                default: break;
            }
        }
        if (style_ != style::pair)
            parse_format_specs(ctx.begin(), ctx.end(), handler);
        // todo: fixup alignment
        return base::parse(ctx);
    }
    template <typename FormatCtx>
    auto format(const std::complex<T>& x, FormatCtx& ctx)
        -> decltype(ctx.out()) {
        format_to(ctx.out(), "(");
        if (x.real() || !x.imag())
            base::format(x.real(), ctx);
        if (x.imag()) {
            if (style_ == style::pair)
                format_to(ctx.out(), ",");
            else if (x.real() && x.imag() >= 0 && specs_.sign != sign::plus)
                format_to(ctx.out(), "+");
            base::format(x.imag(), ctx);
            if (style_ != style::pair) {
                if (style_ == style::star)
                    format_to(ctx.out(), "*i");
                else
                    format_to(ctx.out(), "i");
                if (std::is_same<typename std::decay<T>::type, float>::value)
                    format_to(ctx.out(), "f");
                if (std::is_same<typename std::decay<T>::type,
                                 long double>::value)
                    format_to(ctx.out(), "l");
            }
        }
        return format_to(ctx.out(), ")");
    }
};

int main() {
    using V = std::complex<double>;
    std::vector<V> v{ { 1, 1 }, { 2, 2 } };
    auto r = std::accumulate(v.begin(), v.end(), V{});

    fmt::print("Sum({}) -> {}\n", v, r);
}

I made minor corrections, added examples and rudimentary wording: https://fmt.dev/papers/d2197r0.html. If there are no objections I'll submit this to the next mailing.

lgtm

Was this page helpful?
0 / 5 - 0 ratings